From 5815487544103a601bf424709d67275371d85010 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 8 Mar 2020 16:57:24 +0100 Subject: [PATCH 01/15] Don't normalize the dockerfile anymore The newline breaks logic is now in the parser, which will help retain more precise information on where errors happen --- src/Language/Docker/Parser.hs | 220 ++++++++++++++++++++--------- test/Language/Docker/EDSLSpec.hs | 1 + test/Language/Docker/ParserSpec.hs | 10 ++ 3 files changed, 163 insertions(+), 68 deletions(-) diff --git a/src/Language/Docker/Parser.hs b/src/Language/Docker/Parser.hs index 523be59..d35f578 100644 --- a/src/Language/Docker/Parser.hs +++ b/src/Language/Docker/Parser.hs @@ -6,16 +6,17 @@ module Language.Docker.Parser ( parseText , parseFile , parseStdin + , someUnless , Parser , Error , DockerfileError(..) ) where -import Control.Monad (void) +import Control.Monad (void, when) import qualified Data.ByteString as B import Data.Data import Data.List.NonEmpty (NonEmpty, fromList) -import Data.Maybe (listToMaybe) +import Data.Maybe (fromMaybe, listToMaybe) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T @@ -26,9 +27,10 @@ import Text.Megaparsec hiding (Label, label) import Text.Megaparsec.Char hiding (eol) import qualified Text.Megaparsec.Char.Lexer as L -import Language.Docker.Normalize import Language.Docker.Syntax +import Text.Megaparsec.Debug + data DockerfileError = DuplicateFlagError String | NoValueFlagError String @@ -65,6 +67,21 @@ instance ShowErrorComponent DockerfileError where showErrorComponent (QuoteError t str) = "unexpected end of " ++ t ++ " quoted string " ++ str ++ " (unmatched quote)" +-- Spaces are sometimes significant information in a dockerfile, this type records +-- thee presence of lack of such whitespace in certain lines. +data FoundWhitespace = FoundWhitespace | MissingWhitespace deriving (Eq, Show) + +-- There is no need to remember how mamny spaces we found in a line, so we can +-- cheaply remmeber that we already whitenessed some significant whitespace while +-- parsing an expression by concatenating smaller results +instance Semigroup FoundWhitespace where + FoundWhitespace <> _ = FoundWhitespace + _ <> a = a + +instance Monoid FoundWhitespace where + mempty = MissingWhitespace + + ------------------------------------ -- Utilities ------------------------------------ @@ -72,8 +89,14 @@ instance ShowErrorComponent DockerfileError where customError :: DockerfileError -> Parser a customError = fancyFailure . S.singleton . ErrorCustom +castToSpace :: FoundWhitespace -> Text +castToSpace FoundWhitespace = " " +castToSpace MissingWhitespace = "" + eol :: Parser () -eol = void $ takeWhile1P (Just "whitespace") isSpaceNl +eol = do + whitespace + void (takeWhile1P (Just "whitespace") isSpaceNl) reserved :: Text -> Parser () reserved name = void (lexeme (string' name) T.unpack name) @@ -82,7 +105,7 @@ natural :: Parser Integer natural = L.decimal "positive number" commaSep :: Parser a -> Parser [a] -commaSep p = sepBy (p <* spaces) (symbol ",") +commaSep p = sepBy (p <* whitespace) (symbol ",") stringLiteral :: Parser Text stringLiteral = do @@ -91,55 +114,123 @@ stringLiteral = do return (T.pack lit) brackets :: Parser a -> Parser a -brackets = between (symbol "[" *> spaces) (spaces *> symbol "]") +brackets = between (symbol "[" *> whitespace) (whitespace *> symbol "]") + +onlySpaces :: Parser Text +onlySpaces = takeWhileP (Just "spaces") (\c -> c == ' ' || c == '\t') -spaces1 :: Parser () -spaces1 = void (takeWhile1P (Just "at least one space") (\c -> c == ' ' || c == '\t')) +onlySpaces1 :: Parser Text +onlySpaces1 = takeWhile1P (Just "at least one space") (\c -> c == ' ' || c == '\t') -spaces :: Parser () -spaces = void (takeWhileP (Just "at least one space") (\c -> c == ' ' || c == '\t')) +foundSpacesOnly :: Parser FoundWhitespace +foundSpacesOnly = choice + [ FoundWhitespace <$ onlySpaces1 + , pure MissingWhitespace + ] + +escapedLineBreaks :: Parser FoundWhitespace +escapedLineBreaks = mconcat <$> breaks + where + breaks = many $ do + try (char '\\' *> onlySpaces *> newlines) + void (many . try $ onlySpaces *> comment *> newlines) + -- Spaces before the next '\' have a special significance + -- so we remembeer the fact that we found some + foundSpacesOnly + newlines = takeWhile1P Nothing isNl + +foundWhitespace :: Parser FoundWhitespace +foundWhitespace = do + leading <- foundSpacesOnly + extra <- escapedLineBreaks + trailing <- foundSpacesOnly + return (leading <> extra <> trailing) + +whitespace :: Parser () +whitespace = void foundWhitespace + +requiredWhitespace :: Parser () +requiredWhitespace = do + ws <- foundWhitespace + case ws of + FoundWhitespace -> pure () + MissingWhitespace -> fail "missing whitespace" + +-- Parse value until end of line is reached +-- after consuming all escaped newlines +untilEol :: String -> Parser Text +untilEol name = do + res <- mconcat <$> predicate + when (res == "") $ fail ("expecting " ++ name) + pure res + where + predicate = many $ do + x <- takeWhile1P (Just name) (\c -> c /= '\n' && c /= '\\') + ws <- escapedLineBreaks + case ws of + FoundWhitespace -> pure (x <> " ") + MissingWhitespace -> pure x symbol :: Text -> Parser Text symbol name = do x <- string name - spaces + whitespace return x caseInsensitiveString :: Text -> Parser Text caseInsensitiveString = string' -charsWithEscapedSpaces :: String -> Parser Text -charsWithEscapedSpaces stopChars = do - buf <- takeWhile1P Nothing (`notElem` ("\n\t\\ " ++ stopChars)) - try (jumpEscapeSequence buf) <|> try (backslashFollowedByChars buf) <|> return buf +stringWithEscaped :: Char -> Maybe (Char -> Bool) -> Parser Text +stringWithEscaped quote maybeStopCondition = mconcat <$> sequences where - backslashFollowedByChars buf = do - backslashes <- takeWhile1P Nothing (== '\\') - notFollowedBy (char ' ') - rest <- charsWithEscapedSpaces stopChars - return $ T.concat [buf, backslashes, rest] - jumpEscapeSequence buf = do - void $ string "\\ " - rest <- charsWithEscapedSpaces stopChars - return $ T.concat [buf, " ", rest] + sequences = many $ choice + [ inner + , try $ takeWhile1P Nothing (== '\\') <* notFollowedBy (char quote) + , quoteText <$ string ("\\" <> quoteText) + ] + inner = do + pre <- escapedLineBreaks + x <- takeWhile1P Nothing (\c -> c /= '\\' && c /= '\n' && c /= quote && stopCondition c) + post <- escapedLineBreaks + return $ castToSpace pre <> x <> castToSpace post + quoteText = T.singleton quote + stopCondition = fromMaybe (const True) maybeStopCondition lexeme :: Parser a -> Parser a lexeme p = do x <- p - spaces1 + requiredWhitespace return x isNl :: Char -> Bool isNl c = c == '\n' isSpaceNl :: Char -> Bool -isSpaceNl c = c == ' ' || c == '\t' || c == '\n' +isSpaceNl c = c == ' ' || c == '\t' || c == '\n' || c == '\\' anyUnless :: (Char -> Bool) -> Parser Text -anyUnless predicate = takeWhileP Nothing (\c -> not (isSpaceNl c || predicate c)) +anyUnless predicate = someUnless "" predicate <|> pure "" someUnless :: String -> (Char -> Bool) -> Parser Text -someUnless name predicate = takeWhile1P (Just name) (\c -> not (isSpaceNl c || predicate c)) +someUnless name predicate = do + res <- applyPredicate + case res of + [] -> fail ("expecting " ++ name) + _ -> pure (mconcat res) + where + applyPredicate = many $ do + pre <- escapedLineBreaks + x <- someUnlessOrSpaces name predicate + post <- escapedLineBreaks + return $ castToSpace pre <> x <> castToSpace post + +someUnlessOrSpaces :: String -> (Char -> Bool) -> Parser Text +someUnlessOrSpaces name predicate = + takeWhile1P (Just name) (\c -> not (isSpaceNl c || predicate c)) + +anyUnlessOrSpaces :: (Char -> Bool) -> Parser Text +anyUnlessOrSpaces predicate = + takeWhileP Nothing (\c -> not (isSpaceNl c || predicate c)) ------------------------------------ -- DOCKER INSTRUCTIONS PARSER @@ -162,7 +253,7 @@ parsePlatform :: Parser Platform parsePlatform = do void $ string "--platform=" p <- someUnless "the platform for the FROM image" (== ' ') - spaces1 + requiredWhitespace return p parseBaseImage :: (Text -> Parser (Maybe Tag)) -> Parser BaseImage @@ -199,11 +290,11 @@ untaggedImage = parseBaseImage notInvalidTag return Nothing maybeImageAlias :: Parser (Maybe ImageAlias) -maybeImageAlias = Just <$> (spaces1 >> imageAlias) <|> return Nothing +maybeImageAlias = Just <$> (requiredWhitespace >> imageAlias) <|> return Nothing imageAlias :: Parser ImageAlias imageAlias = do - void (try (reserved "AS") "AS followed by the image alias") + void (try (reserved "AS") "'AS' followed by the image alias") alias <- someUnless "the image alias" (== '\n') return $ ImageAlias alias @@ -225,7 +316,7 @@ cmd = do copy :: Parser Instr copy = do reserved "COPY" - flags <- copyFlag `sepEndBy` spaces1 + flags <- copyFlag `sepEndBy` requiredWhitespace let chownFlags = [c | FlagChown c <- flags] let sourceFlags = [f | FlagSource f <- flags] let invalid = [i | FlagInvalid i <- flags] @@ -280,7 +371,7 @@ fileList name constr = do [_] -> customError $ FileListError (T.unpack name) _ -> return $ constr (SourcePath <$> fromList (init paths)) (TargetPath $ last paths) where - spaceSeparated = anyUnless (== ' ') `sepBy1` (try spaces1 "at least another file path") + spaceSeparated = anyUnless (== ' ') `sepBy1` (try requiredWhitespace "at least another file path") stringList = brackets $ commaSep stringLiteral unexpectedFlag :: Text -> Text -> Parser a @@ -303,15 +394,15 @@ stopsignal = do -- and therefore have to implement quoted values by ourselves doubleQuotedValue :: Parser Text doubleQuotedValue = - between (string "\"") (string "\"") (takeWhileP Nothing (\c -> c /= '"' && c /= '\n')) + between (string "\"") (string "\"") (stringWithEscaped '"' Nothing) singleQuotedValue :: Parser Text singleQuotedValue = - between (string "'") (string "'") (takeWhileP Nothing (\c -> c /= '\'' && c /= '\n')) + between (string "'") (string "'") (stringWithEscaped '\'' Nothing) -unquotedString :: String -> Parser Text -unquotedString stopChars = do - str <- charsWithEscapedSpaces stopChars +unquotedString :: (Char -> Bool) -> Parser Text +unquotedString stopCondition = do + str <- stringWithEscaped ' ' (Just stopCondition) checkFaults str where checkFaults str @@ -320,21 +411,28 @@ unquotedString stopChars = do | T.head str == '\"' = customError $ QuoteError "double" (T.unpack str) | otherwise = return str -singleValue :: String -> Parser Text -singleValue stopChars = - try doubleQuotedValue <|> -- Quotes or no quotes are fine - try singleQuotedValue <|> - (try (unquotedString stopChars) "a string with no quotes") +singleValue :: (Char -> Bool) -> Parser Text +singleValue stopCondition = choice + [ doubleQuotedValue "a string inside double quotes" + , singleQuotedValue "a string inside single quotes" + , unquotedString stopCondition "a string with no quotes" + ] pair :: Parser (Text, Text) pair = do - key <- singleValue "=" - void $ char '=' - value <- singleValue "" + key <- singleValue (/= '=') + value <- withEqualSign <|> withoutEqualSign return (key, value) + where + withEqualSign = do + void $ char '=' + singleValue (\c -> c /= ' ' && c /= '\t') + withoutEqualSign = do + requiredWhitespace + untilEol "value" -pairsList :: Parser Pairs -pairsList = pair `sepBy1` spaces1 +pairs :: Parser Pairs +pairs = (pair "a key value pair (key=value)") `sepEndBy1` requiredWhitespace label :: Parser Instr label = do @@ -360,16 +458,6 @@ env = do p <- pairs return $ Env p -pairs :: Parser Pairs -pairs = try pairsList <|> try singlePair - -singlePair :: Parser Pairs -singlePair = do - key <- anyUnless (== '=') - spaces1 "a space followed by the value for the variable '" ++ T.unpack key ++ "'" - val <- untilEol "the variable value" - return [(key, val)] - user :: Parser Instr user = do reserved "USER" @@ -400,7 +488,7 @@ port = (try portInt "a valid port number") ports :: Parser Ports -ports = Ports <$> port `sepEndBy` spaces1 +ports = Ports <$> port `sepEndBy` requiredWhitespace portRange :: Parser Port portRange = do @@ -442,10 +530,6 @@ run = do c <- arguments return $ Run c --- Parse value until end of line is reached -untilEol :: String -> Parser Text -untilEol name = takeWhile1P (Just name) (not . isNl) - workdir :: Parser Instr workdir = do reserved "WORKDIR" @@ -499,11 +583,11 @@ healthcheck = do noCheck = string "NONE" >> return NoCheck allFlags = do flags <- someFlags - spaces1 "another flag" + requiredWhitespace "another flag" return flags someFlags = do x <- checkFlag - cont <- try (spaces1 >> lookAhead (string "--") >> return True) <|> return False + cont <- try (requiredWhitespace >> lookAhead (string "--") >> return True) <|> return False if cont then do xs <- someFlags @@ -597,17 +681,17 @@ dockerfile = return $ InstructionPos i (T.pack . sourceName $ pos) (unPos . sourceLine $ pos) parseText :: Text -> Either Error Dockerfile -parseText s = parse (contents dockerfile) "" $ normalizeEscapedLines s +parseText = parse (contents dockerfile) "" parseFile :: FilePath -> IO (Either Error Dockerfile) parseFile file = doParse <$> B.readFile file where doParse = - parse (contents dockerfile) file . normalizeEscapedLines . E.decodeUtf8With E.lenientDecode + parse (contents dockerfile) file . E.decodeUtf8With E.lenientDecode -- | Reads the standard input until the end and parses the contents as a Dockerfile parseStdin :: IO (Either Error Dockerfile) parseStdin = doParse <$> B.getContents where doParse = - parse (contents dockerfile) "/dev/stdin" . normalizeEscapedLines . E.decodeUtf8With E.lenientDecode + parse (contents dockerfile) "/dev/stdin" . E.decodeUtf8With E.lenientDecode diff --git a/test/Language/Docker/EDSLSpec.hs b/test/Language/Docker/EDSLSpec.hs index 7723b29..515e278 100644 --- a/test/Language/Docker/EDSLSpec.hs +++ b/test/Language/Docker/EDSLSpec.hs @@ -132,5 +132,6 @@ spec = do str `shouldBe` printed [ "FROM ubuntu" , "ADD ./test/Language/Docker/EDSLSpec.hs /app/EDSLSpec.hs" , "ADD ./test/Language/Docker/ExamplesSpec.hs /app/ExamplesSpec.hs" + , "ADD ./test/Language/Docker/IntegrationSpec.hs /app/IntegrationSpec.hs" , "ADD ./test/Language/Docker/ParserSpec.hs /app/ParserSpec.hs" ] diff --git a/test/Language/Docker/ParserSpec.hs b/test/Language/Docker/ParserSpec.hs index 03051cc..5373471 100644 --- a/test/Language/Docker/ParserSpec.hs +++ b/test/Language/Docker/ParserSpec.hs @@ -284,6 +284,16 @@ spec = do normalizedDockerfile = Text.unlines ["ENV foo=bar baz=foz", ""] in normalizeEscapedLines dockerfile `shouldBe` normalizedDockerfile + it "many escaped lines" $ + let dockerfile = Text.unlines [ "ENV A=\"a.sh\" \\" + , " # comment a" + , " B=\"b.sh\" \\" + , " c=\"true\"" + , "" + ] + in assertAst dockerfile [ Env [("A", "a.sh"), ("B", "b.sh"), ("c", "true")] + ] + it "join long CMD" $ let longEscapedCmd = Text.unlines From f56c2e73cb491a11b714ef2a474fd7e96128cf1e Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 8 Mar 2020 19:29:22 +0100 Subject: [PATCH 02/15] Fixed a few bugs in the parser aftere previous refectoring --- src/Language/Docker/Parser.hs | 77 +++++++++++++----------------- test/Language/Docker/ParserSpec.hs | 4 ++ 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/src/Language/Docker/Parser.hs b/src/Language/Docker/Parser.hs index d35f578..61b3877 100644 --- a/src/Language/Docker/Parser.hs +++ b/src/Language/Docker/Parser.hs @@ -122,29 +122,24 @@ onlySpaces = takeWhileP (Just "spaces") (\c -> c == ' ' || c == '\t') onlySpaces1 :: Parser Text onlySpaces1 = takeWhile1P (Just "at least one space") (\c -> c == ' ' || c == '\t') -foundSpacesOnly :: Parser FoundWhitespace -foundSpacesOnly = choice - [ FoundWhitespace <$ onlySpaces1 - , pure MissingWhitespace - ] - escapedLineBreaks :: Parser FoundWhitespace escapedLineBreaks = mconcat <$> breaks where - breaks = many $ do + breaks = some $ do try (char '\\' *> onlySpaces *> newlines) void (many . try $ onlySpaces *> comment *> newlines) -- Spaces before the next '\' have a special significance -- so we remembeer the fact that we found some - foundSpacesOnly + (FoundWhitespace <$ onlySpaces1 <|> pure MissingWhitespace) newlines = takeWhile1P Nothing isNl foundWhitespace :: Parser FoundWhitespace -foundWhitespace = do - leading <- foundSpacesOnly - extra <- escapedLineBreaks - trailing <- foundSpacesOnly - return (leading <> extra <> trailing) +foundWhitespace = mconcat <$> found + where + found = many $ choice + [ FoundWhitespace <$ onlySpaces1 + , escapedLineBreaks + ] whitespace :: Parser () whitespace = void foundWhitespace @@ -164,12 +159,11 @@ untilEol name = do when (res == "") $ fail ("expecting " ++ name) pure res where - predicate = many $ do - x <- takeWhile1P (Just name) (\c -> c /= '\n' && c /= '\\') - ws <- escapedLineBreaks - case ws of - FoundWhitespace -> pure (x <> " ") - MissingWhitespace -> pure x + predicate = many $ choice + [ castToSpace <$> escapedLineBreaks + , takeWhile1P (Just name) (\c -> c /= '\n' && c /= '\\') + , takeWhile1P Nothing (== '\\') <* notFollowedBy (char '\n') + ] symbol :: Text -> Parser Text symbol name = do @@ -184,15 +178,14 @@ stringWithEscaped :: Char -> Maybe (Char -> Bool) -> Parser Text stringWithEscaped quote maybeStopCondition = mconcat <$> sequences where sequences = many $ choice - [ inner + [ mconcat <$> inner , try $ takeWhile1P Nothing (== '\\') <* notFollowedBy (char quote) , quoteText <$ string ("\\" <> quoteText) ] - inner = do - pre <- escapedLineBreaks - x <- takeWhile1P Nothing (\c -> c /= '\\' && c /= '\n' && c /= quote && stopCondition c) - post <- escapedLineBreaks - return $ castToSpace pre <> x <> castToSpace post + inner = some $ choice + [ castToSpace <$> escapedLineBreaks + , takeWhile1P Nothing (\c -> c /= '\\' && c /= '\n' && c /= quote && stopCondition c) + ] quoteText = T.singleton quote stopCondition = fromMaybe (const True) maybeStopCondition @@ -218,19 +211,10 @@ someUnless name predicate = do [] -> fail ("expecting " ++ name) _ -> pure (mconcat res) where - applyPredicate = many $ do - pre <- escapedLineBreaks - x <- someUnlessOrSpaces name predicate - post <- escapedLineBreaks - return $ castToSpace pre <> x <> castToSpace post - -someUnlessOrSpaces :: String -> (Char -> Bool) -> Parser Text -someUnlessOrSpaces name predicate = - takeWhile1P (Just name) (\c -> not (isSpaceNl c || predicate c)) - -anyUnlessOrSpaces :: (Char -> Bool) -> Parser Text -anyUnlessOrSpaces predicate = - takeWhileP Nothing (\c -> not (isSpaceNl c || predicate c)) + applyPredicate = many $ choice + [ castToSpace <$> escapedLineBreaks + , takeWhile1P (Just name) (\c -> not (isSpaceNl c || predicate c)) + ] ------------------------------------ -- DOCKER INSTRUCTIONS PARSER @@ -402,21 +386,24 @@ singleQuotedValue = unquotedString :: (Char -> Bool) -> Parser Text unquotedString stopCondition = do - str <- stringWithEscaped ' ' (Just stopCondition) + str <- stringWithEscaped ' ' (Just (\c -> stopCondition c && c /= '"' && c /= '\'')) checkFaults str where checkFaults str - | T.null str = return str + | T.null str = fail "a non empty string" | T.head str == '\'' = customError $ QuoteError "single" (T.unpack str) | T.head str == '\"' = customError $ QuoteError "double" (T.unpack str) | otherwise = return str singleValue :: (Char -> Bool) -> Parser Text -singleValue stopCondition = choice - [ doubleQuotedValue "a string inside double quotes" - , singleQuotedValue "a string inside single quotes" - , unquotedString stopCondition "a string with no quotes" - ] +singleValue stopCondition = mconcat <$> variants + where + variants = many $ + choice + [ doubleQuotedValue "a string inside double quotes" + , singleQuotedValue "a string inside single quotes" + , unquotedString stopCondition "a string with no quotes" + ] pair :: Parser (Text, Text) pair = do diff --git a/test/Language/Docker/ParserSpec.hs b/test/Language/Docker/ParserSpec.hs index 5373471..cb37f18 100644 --- a/test/Language/Docker/ParserSpec.hs +++ b/test/Language/Docker/ParserSpec.hs @@ -294,6 +294,10 @@ spec = do in assertAst dockerfile [ Env [("A", "a.sh"), ("B", "b.sh"), ("c", "true")] ] + it "accepts backslash inside string" $ + let dockerfile = "RUN grep 'foo \\.'" + in assertAst dockerfile [Run (ArgumentsText "grep 'foo \\.'")] + it "join long CMD" $ let longEscapedCmd = Text.unlines From ad12106d8553bba15c165d19a6a93de55fd2d785 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 8 Mar 2020 20:53:45 +0100 Subject: [PATCH 03/15] Fixed two other regressions --- src/Language/Docker/Parser.hs | 20 +++++++++++--------- test/Language/Docker/ParserSpec.hs | 10 ++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/Language/Docker/Parser.hs b/src/Language/Docker/Parser.hs index 61b3877..e7574c7 100644 --- a/src/Language/Docker/Parser.hs +++ b/src/Language/Docker/Parser.hs @@ -94,9 +94,13 @@ castToSpace FoundWhitespace = " " castToSpace MissingWhitespace = "" eol :: Parser () -eol = do - whitespace - void (takeWhile1P (Just "whitespace") isSpaceNl) +eol = void ws "end of line" + where + ws = some $ choice + [ void onlySpaces1 + , void $ takeWhile1P Nothing (== '\n') + , void escapedLineBreaks + ] reserved :: Text -> Parser () reserved name = void (lexeme (string' name) T.unpack name) @@ -214,6 +218,7 @@ someUnless name predicate = do applyPredicate = many $ choice [ castToSpace <$> escapedLineBreaks , takeWhile1P (Just name) (\c -> not (isSpaceNl c || predicate c)) + , takeWhile1P Nothing (\c -> c == '\\' && not (predicate c)) <* notFollowedBy (char '\n') ] ------------------------------------ @@ -246,9 +251,9 @@ parseBaseImage tagParser = do notFollowedBy (string "--") registryName <- (Just <$> try parseRegistry) <|> return Nothing name <- someUnless "the image name with a tag" (\c -> c == '@' || c == ':') - maybeTag <- tagParser name + maybeTag <- tagParser name <|> return Nothing maybeDigest <- (Just <$> try parseDigest) <|> return Nothing - maybeAlias <- maybeImageAlias + maybeAlias <- (Just <$> try (requiredWhitespace *> imageAlias)) <|> return Nothing return $ BaseImage (Image registryName name) maybeTag maybeDigest maybeAlias maybePlatform taggedImage :: Parser BaseImage @@ -273,9 +278,6 @@ untaggedImage = parseBaseImage notInvalidTag try (notFollowedBy $ string ":") "no ':' or a valid image tag string (example: " ++ T.unpack name ++ ":valid-tag)" return Nothing -maybeImageAlias :: Parser (Maybe ImageAlias) -maybeImageAlias = Just <$> (requiredWhitespace >> imageAlias) <|> return Nothing - imageAlias :: Parser ImageAlias imageAlias = do void (try (reserved "AS") "'AS' followed by the image alias") @@ -355,7 +357,7 @@ fileList name constr = do [_] -> customError $ FileListError (T.unpack name) _ -> return $ constr (SourcePath <$> fromList (init paths)) (TargetPath $ last paths) where - spaceSeparated = anyUnless (== ' ') `sepBy1` (try requiredWhitespace "at least another file path") + spaceSeparated = anyUnless (== ' ') `sepEndBy1` (try requiredWhitespace "at least another file path") stringList = brackets $ commaSep stringLiteral unexpectedFlag :: Text -> Text -> Parser a diff --git a/test/Language/Docker/ParserSpec.hs b/test/Language/Docker/ParserSpec.hs index cb37f18..dc0d32f 100644 --- a/test/Language/Docker/ParserSpec.hs +++ b/test/Language/Docker/ParserSpec.hs @@ -52,6 +52,11 @@ spec = do "FROM ubuntu:14.04@sha256:0ef2e08ed3fab" [From (taggedImage "ubuntu" "14.04" `withDigest` "sha256:0ef2e08ed3fab")] + it "parse image with spaces at the end" $ + assertAst + "FROM dockerfile/mariadb " + [From (untaggedImage "dockerfile/mariadb")] + describe "parse aliased FROM" $ do it "parse untagged image" $ assertAst "FROM busybox as foo" [From (untaggedImage "busybox" `withAlias` "foo")] @@ -439,6 +444,11 @@ spec = do in assertAst file [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") (CopySource "node") ] + it "supports windows paths" $ + let file = Text.unlines ["COPY C:\\\\go C:\\\\go"] + in assertAst file [ Copy $ CopyArgs (fmap SourcePath ["C:\\\\go"]) (TargetPath "C:\\\\go") NoChown NoSource + ] + assertAst :: HasCallStack => Text.Text -> [Instruction Text.Text] -> Assertion assertAst s ast = case parseText s of From 03cf47525b8935e5448c73b11e0d637a7ca5e7d4 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 8 Mar 2020 21:43:03 +0100 Subject: [PATCH 04/15] Fixed another regression, remove blacklist in integration tests --- integration-tests/parse_files.sh | 4 +--- src/Language/Docker/Parser.hs | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/integration-tests/parse_files.sh b/integration-tests/parse_files.sh index acb1a1e..05ae21a 100755 --- a/integration-tests/parse_files.sh +++ b/integration-tests/parse_files.sh @@ -5,9 +5,7 @@ set -o nounset readonly CWD="$PWD" readonly TESTS_DIR="integration-tests/Dockerfiles" -BLACKLIST="./Dockerfiles/dockerfiles/nylas/sync-engine/Dockerfile" -BLACKLIST=$BLACKLIST" ./Dockerfiles/docker-images/OracleWebLogic/samples/12212-domain/Dockerfile" -BLACKLIST=$BLACKLIST" ./Dockerfiles/docker-images/OracleWebLogic/samples/12213-domain/Dockerfile" +BLACKLIST="" function git_clone() { local git_url="$1" diff --git a/src/Language/Docker/Parser.hs b/src/Language/Docker/Parser.hs index e7574c7..27730ed 100644 --- a/src/Language/Docker/Parser.hs +++ b/src/Language/Docker/Parser.hs @@ -178,20 +178,20 @@ symbol name = do caseInsensitiveString :: Text -> Parser Text caseInsensitiveString = string' -stringWithEscaped :: Char -> Maybe (Char -> Bool) -> Parser Text -stringWithEscaped quote maybeStopCondition = mconcat <$> sequences +stringWithEscaped :: [Char] -> Maybe (Char -> Bool) -> Parser Text +stringWithEscaped quoteChars maybeAcceptCondition = mconcat <$> sequences where sequences = many $ choice [ mconcat <$> inner - , try $ takeWhile1P Nothing (== '\\') <* notFollowedBy (char quote) - , quoteText <$ string ("\\" <> quoteText) + , try $ takeWhile1P Nothing (== '\\') <* notFollowedBy quoteParser + , string "\\" *> quoteParser ] inner = some $ choice [ castToSpace <$> escapedLineBreaks - , takeWhile1P Nothing (\c -> c /= '\\' && c /= '\n' && c /= quote && stopCondition c) + , takeWhile1P Nothing (\c -> c /= '\\' && c /= '\n' && c `notElem`quoteChars && acceptCondition c) ] - quoteText = T.singleton quote - stopCondition = fromMaybe (const True) maybeStopCondition + quoteParser = T.singleton <$> choice (fmap char quoteChars) + acceptCondition = fromMaybe (const True) maybeAcceptCondition lexeme :: Parser a -> Parser a lexeme p = do @@ -380,15 +380,15 @@ stopsignal = do -- and therefore have to implement quoted values by ourselves doubleQuotedValue :: Parser Text doubleQuotedValue = - between (string "\"") (string "\"") (stringWithEscaped '"' Nothing) + between (string "\"") (string "\"") (stringWithEscaped ['"'] Nothing) singleQuotedValue :: Parser Text singleQuotedValue = - between (string "'") (string "'") (stringWithEscaped '\'' Nothing) + between (string "'") (string "'") (stringWithEscaped ['\''] Nothing) unquotedString :: (Char -> Bool) -> Parser Text -unquotedString stopCondition = do - str <- stringWithEscaped ' ' (Just (\c -> stopCondition c && c /= '"' && c /= '\'')) +unquotedString acceptCondition = do + str <- stringWithEscaped [' ', '\t'] (Just (\c -> acceptCondition c && c /= '"' && c /= '\'')) checkFaults str where checkFaults str @@ -398,13 +398,13 @@ unquotedString stopCondition = do | otherwise = return str singleValue :: (Char -> Bool) -> Parser Text -singleValue stopCondition = mconcat <$> variants +singleValue acceptCondition = mconcat <$> variants where variants = many $ choice [ doubleQuotedValue "a string inside double quotes" , singleQuotedValue "a string inside single quotes" - , unquotedString stopCondition "a string with no quotes" + , unquotedString acceptCondition "a string with no quotes" ] pair :: Parser (Text, Text) From 2f84322e4da1800957793de859cb958a13f8d3fd Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 17 May 2020 16:54:04 +0200 Subject: [PATCH 05/15] Adding more tests and cleeaning up --- language-docker.cabal | 98 +++++++++++++++++++++++++ src/Language/Docker/Parser.hs | 36 +++------ stack.yaml | 2 +- stack.yaml.lock | 8 +- test/Language/Docker/IntegrationSpec.hs | 27 +++++++ test/fixtures/1.Dockerfile | 17 +++++ test/fixtures/2.Dockerfile | 13 ++++ 7 files changed, 172 insertions(+), 29 deletions(-) create mode 100644 language-docker.cabal create mode 100644 test/Language/Docker/IntegrationSpec.hs create mode 100644 test/fixtures/1.Dockerfile create mode 100644 test/fixtures/2.Dockerfile diff --git a/language-docker.cabal b/language-docker.cabal new file mode 100644 index 0000000..0e857e7 --- /dev/null +++ b/language-docker.cabal @@ -0,0 +1,98 @@ +cabal-version: 1.12 + +-- This file has been generated from package.yaml by hpack version 0.31.2. +-- +-- see: https://github.com/sol/hpack +-- +-- hash: db7b75721410a3295c9c0a76e6d609fbaac59c11dd5d6836d427ab893f8019dc + +name: language-docker +version: 8.1.0 +synopsis: Dockerfile parser, pretty-printer and embedded DSL +description: All functions for parsing, printing and writting Dockerfiles are exported through @Language.Docker@. For more fine-grained operations look for specific modules that implement a certain functionality. + See the for the source-code and examples. +category: Development +homepage: https://github.com/hadolint/language-docker#readme +bug-reports: https://github.com/hadolint/language-docker/issues +author: Lukas Martinelli, + Pedro Tacla Yamada, + José Lorenzo Rodríguez +maintainer: lorenzo@seatgeek.com +copyright: Lukas Martinelli, Copyright (c) 2016, + Pedro Tacla Yamada, Copyright (c) 2016, + José Lorenzo Rodríguez, Copyright (c) 2017 +license: GPL-3 +license-file: LICENSE +build-type: Simple +extra-source-files: + README.md + +source-repository head + type: git + location: https://github.com/hadolint/language-docker + +library + exposed-modules: + Language.Docker + Language.Docker.Parser + Language.Docker.PrettyPrint + Language.Docker.Normalize + Language.Docker.Syntax + Language.Docker.Syntax.Lift + Language.Docker.EDSL + Language.Docker.EDSL.Quasi + Language.Docker.EDSL.Types + other-modules: + Paths_language_docker + hs-source-dirs: + src + ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -fno-warn-unused-do-bind -fno-warn-orphans + build-depends: + base >=4.13 && <5 + , bytestring >=0.10 + , containers + , free + , megaparsec >=7.0 + , mtl + , prettyprinter + , split >=0.2 + , template-haskell + , text + , th-lift + , time + default-language: Haskell2010 + +test-suite hspec + type: exitcode-stdio-1.0 + main-is: Spec.hs + other-modules: + Language.Docker.EDSL.QuasiSpec + Language.Docker.EDSLSpec + Language.Docker.ExamplesSpec + Language.Docker.IntegrationSpec + Language.Docker.ParserSpec + Paths_language_docker + hs-source-dirs: + test + build-depends: + Glob + , HUnit >=1.2 + , QuickCheck + , base >=4.13 && <5 + , bytestring >=0.10 + , containers + , directory + , filepath + , free + , hspec + , language-docker + , megaparsec >=7.0 + , mtl + , prettyprinter + , process + , split >=0.2 + , template-haskell + , text + , th-lift + , time + default-language: Haskell2010 diff --git a/src/Language/Docker/Parser.hs b/src/Language/Docker/Parser.hs index 27730ed..25b473e 100644 --- a/src/Language/Docker/Parser.hs +++ b/src/Language/Docker/Parser.hs @@ -29,8 +29,6 @@ import qualified Text.Megaparsec.Char.Lexer as L import Language.Docker.Syntax -import Text.Megaparsec.Debug - data DockerfileError = DuplicateFlagError String | NoValueFlagError String @@ -131,10 +129,10 @@ escapedLineBreaks = mconcat <$> breaks where breaks = some $ do try (char '\\' *> onlySpaces *> newlines) - void (many . try $ onlySpaces *> comment *> newlines) + skipMany . try $ onlySpaces *> comment *> newlines -- Spaces before the next '\' have a special significance -- so we remembeer the fact that we found some - (FoundWhitespace <$ onlySpaces1 <|> pure MissingWhitespace) + FoundWhitespace <$ onlySpaces1 <|> pure MissingWhitespace newlines = takeWhile1P Nothing isNl foundWhitespace :: Parser FoundWhitespace @@ -290,14 +288,12 @@ baseImage = try taggedImage <|> untaggedImage from :: Parser Instr from = do reserved "FROM" - image <- baseImage - return $ From image + From <$> baseImage cmd :: Parser Instr cmd = do reserved "CMD" - args <- arguments - return $ Cmd args + Cmd <$> arguments copy :: Parser Instr copy = do @@ -367,8 +363,7 @@ unexpectedFlag name _ = customFailure $ InvalidFlagError (T.unpack name) shell :: Parser Instr shell = do reserved "SHELL" - args <- arguments - return $ Shell args + Shell <$> arguments stopsignal :: Parser Instr stopsignal = do @@ -426,8 +421,7 @@ pairs = (pair "a key value pair (key=value)") `sepEndBy1` requiredWhitespace label :: Parser Instr label = do reserved "LABEL" - p <- pairs - return $ Label p + Label <$> pairs arg :: Parser Instr arg = do @@ -444,8 +438,7 @@ arg = do env :: Parser Instr env = do reserved "ENV" - p <- pairs - return $ Env p + Env <$> pairs user :: Parser Instr user = do @@ -466,8 +459,7 @@ add = do expose :: Parser Instr expose = do reserved "EXPOSE" - ps <- ports - return $ Expose ps + Expose <$> ports port :: Parser Port port = @@ -504,8 +496,7 @@ portInt = do portWithProtocol :: Parser Port portWithProtocol = do portNumber <- natural - proto <- protocol - return $ Port (fromIntegral portNumber) proto + Port (fromIntegral portNumber) <$> protocol portVariable :: Parser Port portVariable = do @@ -516,8 +507,7 @@ portVariable = do run :: Parser Instr run = do reserved "RUN" - c <- arguments - return $ Run c + Run <$> arguments workdir :: Parser Instr workdir = do @@ -555,14 +545,12 @@ arguments = try argumentsExec <|> try argumentsShell entrypoint :: Parser Instr entrypoint = do reserved "ENTRYPOINT" - args <- arguments - return $ Entrypoint args + Entrypoint <$> arguments onbuild :: Parser Instr onbuild = do reserved "ONBUILD" - i <- parseInstruction - return $ OnBuild i + OnBuild <$> parseInstruction healthcheck :: Parser Instr healthcheck = do diff --git a/stack.yaml b/stack.yaml index 008b7e0..f191033 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,4 +1,4 @@ -resolver: lts-15.1 +resolver: lts-15.13 packages: - '.' flags: {} diff --git a/stack.yaml.lock b/stack.yaml.lock index 7e51098..ea454db 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -6,7 +6,7 @@ packages: [] snapshots: - completed: - size: 489011 - url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/15/1.yaml - sha256: d4ecc42b7125d68e4c3c036a08046ad0cd02ae0d9efbe3af2223a00ff8cc16f3 - original: lts-15.1 + size: 496112 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/15/13.yaml + sha256: 75a1a0f870e1876898b117b0e443f911b3fa2985bfabb53158c81ab5765beda5 + original: lts-15.13 diff --git a/test/Language/Docker/IntegrationSpec.hs b/test/Language/Docker/IntegrationSpec.hs new file mode 100644 index 0000000..1a05a3e --- /dev/null +++ b/test/Language/Docker/IntegrationSpec.hs @@ -0,0 +1,27 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE OverloadedLists #-} +module Language.Docker.IntegrationSpec where + +import Language.Docker.Parser +import Language.Docker.Syntax + + +import Test.HUnit hiding (Label) +import Test.Hspec +import Text.Megaparsec hiding (Label) +import qualified Data.Text as Text + +spec :: Spec +spec = do + describe "1" $ do + it "no erors" $ do + parsed <- parseFile "test/fixtures/1.Dockerfile" + case parsed of + Right a -> print a + Left err -> assertFailure $ errorBundlePretty err + describe "2" $ do + it "no erors" $ do + parsed <- parseFile "test/fixtures/2.Dockerfile" + case parsed of + Right a -> print a + Left err -> assertFailure $ errorBundlePretty err diff --git a/test/fixtures/1.Dockerfile b/test/fixtures/1.Dockerfile new file mode 100644 index 0000000..b8e07ed --- /dev/null +++ b/test/fixtures/1.Dockerfile @@ -0,0 +1,17 @@ +FROM foo:7-slim + +# An extra space after the env value should be no problem +ENV container=false\ + container2=true + +ENV A="a.sh" D="c"\ + B="installDBBinaries.sh" + +ENV X "Y" Z + +ENV DOG=Rex\ The\ Dog\ + CAT=Top\ Cat + +ENV DOCKER_TLS_CERTDIR= +ENV foo\ a=afoo' bar 'baz"qu\"z" +ENV BASE_PATH /var/spool/apt-mirror diff --git a/test/fixtures/2.Dockerfile b/test/fixtures/2.Dockerfile new file mode 100644 index 0000000..fd41cf8 --- /dev/null +++ b/test/fixtures/2.Dockerfile @@ -0,0 +1,13 @@ +FROM scratch + +RUN set -ex; \ + apt-get update; \ + if ! which gpg; then \ + apt-get install -y --no-install-recommends gnupg; \ + fi; \ + if ! gpg --version | grep -q '^gpg (GnuPG) 1\.'; then \ +# Ubuntu includes "gnupg" (not "gnupg2", but still 2.x), but not dirmngr, and gnupg 2.x requires dirmngr +# so, if we're not running gnupg 1.x, explicitly install dirmngr too + apt-get install -y --no-install-recommends dirmngr; \ + fi; \ + rm -rf /var/lib/apt/lists/* From ef7ef31f298dae1af7bc30f8604b5ed3651b488c Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Tue, 19 May 2020 17:14:25 +0200 Subject: [PATCH 06/15] Starting to work on newer run flags --- src/Language/Docker/Parser.hs | 10 ++++- src/Language/Docker/Syntax.hs | 73 ++++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/Language/Docker/Parser.hs b/src/Language/Docker/Parser.hs index 25b473e..d6feb18 100644 --- a/src/Language/Docker/Parser.hs +++ b/src/Language/Docker/Parser.hs @@ -56,6 +56,11 @@ data CheckFlag | FlagRetries Retries | CFlagInvalid (Text, Text) +data RunFlag + = RunFlagMount RunMount + | RunFlagSecurity RunSecurity + | RunFlagNetwrok RunNetwork + instance ShowErrorComponent DockerfileError where showErrorComponent (DuplicateFlagError f) = "duplicate flag: " ++ f showErrorComponent (FileListError f) = @@ -507,7 +512,10 @@ portVariable = do run :: Parser Instr run = do reserved "RUN" - Run <$> arguments + Run <$> runArguments + +runArguments :: Parser (RunArgs Text) +runArguments = undefined workdir :: Parser Instr workdir = do diff --git a/src/Language/Docker/Syntax.hs b/src/Language/Docker/Syntax.hs index 66d5631..f00baca 100644 --- a/src/Language/Docker/Syntax.hs +++ b/src/Language/Docker/Syntax.hs @@ -162,6 +162,77 @@ data CheckArgs args = CheckArgs type Pairs = [(Text, Text)] +data RunMount + = BindMount !BindOpts + | CacheMount !CacheOpts + | TmpfsMount !TmpOpts + | SecretMount !SecretOpts + | SshMount !SecretOpts + deriving (Eq, Show, Ord) + +data BindOpts = BindOpts + { target :: !TargetPath + , source :: !(Maybe SourcePath) + , fromImage :: !(Maybe Text) + , readWrite :: !(Maybe Bool) + } deriving (Show, Eq, Ord) + +data CacheOpts = CacheOpts + { target :: !TargetPath + , id :: !(Maybe Text) + , sharing :: !CacheSharing + , readOnly :: !(Maybe Bool) + , fromImage :: !(Maybe Text) + , source :: !(Maybe SourcePath) + , mode :: !(Maybe Text) + , uid :: !(Maybe Int) + , gid :: !(Maybe Int) + } deriving (Show, Eq, Ord) + + +newtype TmpOpts = TmpOpts { target :: TargetPath } deriving (Eq, Show, Ord) + +data SecretOpts = SecretOpts + { target :: !TargetPath + , id :: !(Maybe Text) + , isRequired :: !(Maybe Bool) + , mode :: !(Maybe Text) + , uid :: !(Maybe Int) + , gid :: !(Maybe Int) + } deriving (Eq, Show, Ord) + +data CacheSharing + = Shared + | Private + | Locked + deriving (Show, Eq, Ord) + +data RunSecurity + = Insecure + | Sandbox + deriving (Show, Eq, Ord) + +data RunNetwork + = NetworkNone + | NetworkHost + | NetworkDefault + deriving (Show, Eq, Ord) + +data RunArgs args = RunArgs + { mount :: !(Maybe RunMount) + , security :: !(Maybe RunSecurity) + , network :: !(Maybe RunNetwork) + , commands :: !(Arguments args) + } deriving (Show, Eq, Ord, Functor) + +instance IsString (RunArgs Text) where + fromString s = RunArgs + { commands = ArgumentsText . Text.pack $ s + , security = Nothing + , network = Nothing + , mount = Nothing + } + -- | All commands available in Dockerfiles data Instruction args = From !BaseImage @@ -170,7 +241,7 @@ data Instruction args | Label !Pairs | Stopsignal !Text | Copy !CopyArgs - | Run !(Arguments args) + | Run !(RunArgs args) | Cmd !(Arguments args) | Shell !(Arguments args) | Workdir !Directory From b39760c728bc61bb5e5439055641897024b7884f Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Wed, 27 May 2020 14:17:02 +0200 Subject: [PATCH 07/15] Adding better version constraints added a new dependency to make life easier here --- package.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.yaml b/package.yaml index cf8b528..0803ea0 100644 --- a/package.yaml +++ b/package.yaml @@ -26,7 +26,7 @@ extra-source-files: dependencies: - base >=4.13 && <5 - bytestring >=0.10 - - megaparsec >=7.0 + - megaparsec >=8.0 - prettyprinter - split >=0.2 - free @@ -36,6 +36,7 @@ dependencies: - text - time - containers + - data-default-class library: source-dirs: src From cad42836d8773276b6ab485d075a1b1a94094fe0 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Wed, 27 May 2020 14:18:29 +0200 Subject: [PATCH 08/15] Refactoring code out into other files Using ormolu in those file for formatting instead of hindent --- language-docker.cabal | 15 +- src/Language/Docker/Parser.hs | 586 +--------------------- src/Language/Docker/Parser/Arguments.hs | 23 + src/Language/Docker/Parser/Cmd.hs | 15 + src/Language/Docker/Parser/Copy.hs | 93 ++++ src/Language/Docker/Parser/Expose.hs | 58 +++ src/Language/Docker/Parser/From.hs | 74 +++ src/Language/Docker/Parser/Healthcheck.hs | 98 ++++ src/Language/Docker/Parser/Pairs.hs | 67 +++ src/Language/Docker/Parser/Prelude.hs | 249 +++++++++ src/Language/Docker/Parser/Run.hs | 294 +++++++++++ src/Language/Docker/PrettyPrint.hs | 2 +- src/Language/Docker/Syntax.hs | 472 +++++++++-------- 13 files changed, 1278 insertions(+), 768 deletions(-) create mode 100644 src/Language/Docker/Parser/Arguments.hs create mode 100644 src/Language/Docker/Parser/Cmd.hs create mode 100644 src/Language/Docker/Parser/Copy.hs create mode 100644 src/Language/Docker/Parser/Expose.hs create mode 100644 src/Language/Docker/Parser/From.hs create mode 100644 src/Language/Docker/Parser/Healthcheck.hs create mode 100644 src/Language/Docker/Parser/Pairs.hs create mode 100644 src/Language/Docker/Parser/Prelude.hs create mode 100644 src/Language/Docker/Parser/Run.hs diff --git a/language-docker.cabal b/language-docker.cabal index 0e857e7..fbbdc36 100644 --- a/language-docker.cabal +++ b/language-docker.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: db7b75721410a3295c9c0a76e6d609fbaac59c11dd5d6836d427ab893f8019dc +-- hash: 16b417222b143d0446815001e459ca1bf6fd63e67b3862c3443a11aa44a9931a name: language-docker version: 8.1.0 @@ -43,6 +43,15 @@ library Language.Docker.EDSL.Quasi Language.Docker.EDSL.Types other-modules: + Language.Docker.Parser.Arguments + Language.Docker.Parser.Cmd + Language.Docker.Parser.Copy + Language.Docker.Parser.Expose + Language.Docker.Parser.From + Language.Docker.Parser.Healthcheck + Language.Docker.Parser.Pairs + Language.Docker.Parser.Prelude + Language.Docker.Parser.Run Paths_language_docker hs-source-dirs: src @@ -51,8 +60,9 @@ library base >=4.13 && <5 , bytestring >=0.10 , containers + , data-default-class , free - , megaparsec >=7.0 + , megaparsec >=8.0 , mtl , prettyprinter , split >=0.2 @@ -81,6 +91,7 @@ test-suite hspec , base >=4.13 && <5 , bytestring >=0.10 , containers + , data-default-class , directory , filepath , free diff --git a/src/Language/Docker/Parser.hs b/src/Language/Docker/Parser.hs index d6feb18..d0f8d9a 100644 --- a/src/Language/Docker/Parser.hs +++ b/src/Language/Docker/Parser.hs @@ -1,369 +1,34 @@ -{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE DeriveDataTypeable #-} module Language.Docker.Parser ( parseText , parseFile , parseStdin - , someUnless , Parser , Error , DockerfileError(..) ) where -import Control.Monad (void, when) -import qualified Data.ByteString as B -import Data.Data -import Data.List.NonEmpty (NonEmpty, fromList) -import Data.Maybe (fromMaybe, listToMaybe) -import qualified Data.Set as S -import Data.Text (Text) + import qualified Data.Text as T +import Language.Docker.Parser.Prelude +import Language.Docker.Syntax import qualified Data.Text.Encoding as E import qualified Data.Text.Encoding.Error as E -import Data.Time.Clock (secondsToDiffTime) -import Text.Megaparsec hiding (Label, label) -import Text.Megaparsec.Char hiding (eol) -import qualified Text.Megaparsec.Char.Lexer as L - -import Language.Docker.Syntax - -data DockerfileError - = DuplicateFlagError String - | NoValueFlagError String - | InvalidFlagError String - | FileListError String - | QuoteError String - String - deriving (Eq, Data, Typeable, Ord, Read, Show) - -type Parser = Parsec DockerfileError Text - -type Error = ParseErrorBundle Text DockerfileError - -type Instr = Instruction Text - -data CopyFlag - = FlagChown Chown - | FlagSource CopySource - | FlagInvalid (Text, Text) - -data CheckFlag - = FlagInterval Duration - | FlagTimeout Duration - | FlagStartPeriod Duration - | FlagRetries Retries - | CFlagInvalid (Text, Text) - -data RunFlag - = RunFlagMount RunMount - | RunFlagSecurity RunSecurity - | RunFlagNetwrok RunNetwork - -instance ShowErrorComponent DockerfileError where - showErrorComponent (DuplicateFlagError f) = "duplicate flag: " ++ f - showErrorComponent (FileListError f) = - "unexpected end of line. At least two arguments are required for " ++ f - showErrorComponent (NoValueFlagError f) = "unexpected flag " ++ f ++ " with no value" - showErrorComponent (InvalidFlagError f) = "invalid flag: " ++ f - showErrorComponent (QuoteError t str) = - "unexpected end of " ++ t ++ " quoted string " ++ str ++ " (unmatched quote)" - --- Spaces are sometimes significant information in a dockerfile, this type records --- thee presence of lack of such whitespace in certain lines. -data FoundWhitespace = FoundWhitespace | MissingWhitespace deriving (Eq, Show) - --- There is no need to remember how mamny spaces we found in a line, so we can --- cheaply remmeber that we already whitenessed some significant whitespace while --- parsing an expression by concatenating smaller results -instance Semigroup FoundWhitespace where - FoundWhitespace <> _ = FoundWhitespace - _ <> a = a - -instance Monoid FoundWhitespace where - mempty = MissingWhitespace - - ------------------------------------- --- Utilities ------------------------------------- --- | End parsing signaling a “conversion error”. -customError :: DockerfileError -> Parser a -customError = fancyFailure . S.singleton . ErrorCustom - -castToSpace :: FoundWhitespace -> Text -castToSpace FoundWhitespace = " " -castToSpace MissingWhitespace = "" - -eol :: Parser () -eol = void ws "end of line" - where - ws = some $ choice - [ void onlySpaces1 - , void $ takeWhile1P Nothing (== '\n') - , void escapedLineBreaks - ] - -reserved :: Text -> Parser () -reserved name = void (lexeme (string' name) T.unpack name) - -natural :: Parser Integer -natural = L.decimal "positive number" - -commaSep :: Parser a -> Parser [a] -commaSep p = sepBy (p <* whitespace) (symbol ",") - -stringLiteral :: Parser Text -stringLiteral = do - void (char '"') - lit <- manyTill L.charLiteral (char '"') - return (T.pack lit) - -brackets :: Parser a -> Parser a -brackets = between (symbol "[" *> whitespace) (whitespace *> symbol "]") - -onlySpaces :: Parser Text -onlySpaces = takeWhileP (Just "spaces") (\c -> c == ' ' || c == '\t') - -onlySpaces1 :: Parser Text -onlySpaces1 = takeWhile1P (Just "at least one space") (\c -> c == ' ' || c == '\t') - -escapedLineBreaks :: Parser FoundWhitespace -escapedLineBreaks = mconcat <$> breaks - where - breaks = some $ do - try (char '\\' *> onlySpaces *> newlines) - skipMany . try $ onlySpaces *> comment *> newlines - -- Spaces before the next '\' have a special significance - -- so we remembeer the fact that we found some - FoundWhitespace <$ onlySpaces1 <|> pure MissingWhitespace - newlines = takeWhile1P Nothing isNl - -foundWhitespace :: Parser FoundWhitespace -foundWhitespace = mconcat <$> found - where - found = many $ choice - [ FoundWhitespace <$ onlySpaces1 - , escapedLineBreaks - ] - -whitespace :: Parser () -whitespace = void foundWhitespace - -requiredWhitespace :: Parser () -requiredWhitespace = do - ws <- foundWhitespace - case ws of - FoundWhitespace -> pure () - MissingWhitespace -> fail "missing whitespace" - --- Parse value until end of line is reached --- after consuming all escaped newlines -untilEol :: String -> Parser Text -untilEol name = do - res <- mconcat <$> predicate - when (res == "") $ fail ("expecting " ++ name) - pure res - where - predicate = many $ choice - [ castToSpace <$> escapedLineBreaks - , takeWhile1P (Just name) (\c -> c /= '\n' && c /= '\\') - , takeWhile1P Nothing (== '\\') <* notFollowedBy (char '\n') - ] - -symbol :: Text -> Parser Text -symbol name = do - x <- string name - whitespace - return x - -caseInsensitiveString :: Text -> Parser Text -caseInsensitiveString = string' - -stringWithEscaped :: [Char] -> Maybe (Char -> Bool) -> Parser Text -stringWithEscaped quoteChars maybeAcceptCondition = mconcat <$> sequences - where - sequences = many $ choice - [ mconcat <$> inner - , try $ takeWhile1P Nothing (== '\\') <* notFollowedBy quoteParser - , string "\\" *> quoteParser - ] - inner = some $ choice - [ castToSpace <$> escapedLineBreaks - , takeWhile1P Nothing (\c -> c /= '\\' && c /= '\n' && c `notElem`quoteChars && acceptCondition c) - ] - quoteParser = T.singleton <$> choice (fmap char quoteChars) - acceptCondition = fromMaybe (const True) maybeAcceptCondition - -lexeme :: Parser a -> Parser a -lexeme p = do - x <- p - requiredWhitespace - return x - -isNl :: Char -> Bool -isNl c = c == '\n' - -isSpaceNl :: Char -> Bool -isSpaceNl c = c == ' ' || c == '\t' || c == '\n' || c == '\\' - -anyUnless :: (Char -> Bool) -> Parser Text -anyUnless predicate = someUnless "" predicate <|> pure "" - -someUnless :: String -> (Char -> Bool) -> Parser Text -someUnless name predicate = do - res <- applyPredicate - case res of - [] -> fail ("expecting " ++ name) - _ -> pure (mconcat res) - where - applyPredicate = many $ choice - [ castToSpace <$> escapedLineBreaks - , takeWhile1P (Just name) (\c -> not (isSpaceNl c || predicate c)) - , takeWhile1P Nothing (\c -> c == '\\' && not (predicate c)) <* notFollowedBy (char '\n') - ] +import qualified Data.ByteString as B +import Language.Docker.Parser.Expose (parseExpose) +import Language.Docker.Parser.Copy (parseCopy, parseAdd) +import Language.Docker.Parser.From (parseFrom) +import Language.Docker.Parser.Cmd (parseCmd) +import Language.Docker.Parser.Healthcheck (parseHealthcheck) +import Language.Docker.Parser.Run (parseRun) +import Language.Docker.Parser.Arguments (arguments) +import Language.Docker.Parser.Pairs (parseEnv, parseLabel) ------------------------------------ -- DOCKER INSTRUCTIONS PARSER ------------------------------------ -comment :: Parser Instr -comment = do - void $ char '#' - text <- takeWhileP Nothing (not . isNl) - return $ Comment text - -parseRegistry :: Parser Registry -parseRegistry = do - domain <- someUnless "a domain name" (== '.') - void $ char '.' - tld <- someUnless "a TLD" (== '/') - void $ char '/' - return $ Registry (domain <> "." <> tld) - -parsePlatform :: Parser Platform -parsePlatform = do - void $ string "--platform=" - p <- someUnless "the platform for the FROM image" (== ' ') - requiredWhitespace - return p - -parseBaseImage :: (Text -> Parser (Maybe Tag)) -> Parser BaseImage -parseBaseImage tagParser = do - maybePlatform <- (Just <$> try parsePlatform) <|> return Nothing - notFollowedBy (string "--") - registryName <- (Just <$> try parseRegistry) <|> return Nothing - name <- someUnless "the image name with a tag" (\c -> c == '@' || c == ':') - maybeTag <- tagParser name <|> return Nothing - maybeDigest <- (Just <$> try parseDigest) <|> return Nothing - maybeAlias <- (Just <$> try (requiredWhitespace *> imageAlias)) <|> return Nothing - return $ BaseImage (Image registryName name) maybeTag maybeDigest maybeAlias maybePlatform - -taggedImage :: Parser BaseImage -taggedImage = parseBaseImage tagParser - where - tagParser _ = do - void $ char ':' - tag <- someUnless "the image tag" (\c -> c == '@' || c == ':') - return (Just . Tag $ tag) - -parseDigest :: Parser Digest -parseDigest = do - void $ char '@' - d <- someUnless "the image digest" (== '@') - return $ Digest d - -untaggedImage :: Parser BaseImage -untaggedImage = parseBaseImage notInvalidTag - where - notInvalidTag :: Text -> Parser (Maybe Tag) - notInvalidTag name = do - try (notFollowedBy $ string ":") "no ':' or a valid image tag string (example: " ++ T.unpack name ++ ":valid-tag)" - return Nothing - -imageAlias :: Parser ImageAlias -imageAlias = do - void (try (reserved "AS") "'AS' followed by the image alias") - alias <- someUnless "the image alias" (== '\n') - return $ ImageAlias alias - -baseImage :: Parser BaseImage -baseImage = try taggedImage <|> untaggedImage - -from :: Parser Instr -from = do - reserved "FROM" - From <$> baseImage -cmd :: Parser Instr -cmd = do - reserved "CMD" - Cmd <$> arguments - -copy :: Parser Instr -copy = do - reserved "COPY" - flags <- copyFlag `sepEndBy` requiredWhitespace - let chownFlags = [c | FlagChown c <- flags] - let sourceFlags = [f | FlagSource f <- flags] - let invalid = [i | FlagInvalid i <- flags] - -- Let's do some validation on the flags - case (invalid, chownFlags, sourceFlags) of - ((k, v):_, _, _) -> unexpectedFlag k v - (_, _:_:_, _) -> customError $ DuplicateFlagError "--chown" - (_, _, _:_:_) -> customError $ DuplicateFlagError "--from" - _ -> do - let ch = - case chownFlags of - [] -> NoChown - c:_ -> c - let fr = - case sourceFlags of - [] -> NoSource - f:_ -> f - fileList "COPY" (\src dest -> Copy (CopyArgs src dest ch fr)) - -copyFlag :: Parser CopyFlag -copyFlag = - (FlagChown <$> try chown "only one --chown") <|> - (FlagSource <$> try copySource "only one --from") <|> - (FlagInvalid <$> try anyFlag "no other flags") - -chown :: Parser Chown -chown = do - void $ string "--chown=" - ch <- someUnless "the user and group for chown" (== ' ') - return $ Chown ch - -copySource :: Parser CopySource -copySource = do - void $ string "--from=" - src <- someUnless "the copy source path" isNl - return $ CopySource src - -anyFlag :: Parser (Text, Text) -anyFlag = do - void $ string "--" - name <- someUnless "the flag value" (== '=') - void $ char '=' - val <- anyUnless (== ' ') - return (T.append "--" name, val) - -fileList :: Text -> (NonEmpty SourcePath -> TargetPath -> Instr) -> Parser Instr -fileList name constr = do - paths <- - (try stringList "an array of strings [\"src_file\", \"dest_file\"]") <|> - (try spaceSeparated "a space separated list of file paths") - case paths of - [_] -> customError $ FileListError (T.unpack name) - _ -> return $ constr (SourcePath <$> fromList (init paths)) (TargetPath $ last paths) - where - spaceSeparated = anyUnless (== ' ') `sepEndBy1` (try requiredWhitespace "at least another file path") - stringList = brackets $ commaSep stringLiteral - -unexpectedFlag :: Text -> Text -> Parser a -unexpectedFlag name "" = customFailure $ NoValueFlagError (T.unpack name) -unexpectedFlag name _ = customFailure $ InvalidFlagError (T.unpack name) shell :: Parser Instr shell = do @@ -376,57 +41,6 @@ stopsignal = do args <- untilEol "the stop signal" return $ Stopsignal args --- We cannot use string literal because it swallows space --- and therefore have to implement quoted values by ourselves -doubleQuotedValue :: Parser Text -doubleQuotedValue = - between (string "\"") (string "\"") (stringWithEscaped ['"'] Nothing) - -singleQuotedValue :: Parser Text -singleQuotedValue = - between (string "'") (string "'") (stringWithEscaped ['\''] Nothing) - -unquotedString :: (Char -> Bool) -> Parser Text -unquotedString acceptCondition = do - str <- stringWithEscaped [' ', '\t'] (Just (\c -> acceptCondition c && c /= '"' && c /= '\'')) - checkFaults str - where - checkFaults str - | T.null str = fail "a non empty string" - | T.head str == '\'' = customError $ QuoteError "single" (T.unpack str) - | T.head str == '\"' = customError $ QuoteError "double" (T.unpack str) - | otherwise = return str - -singleValue :: (Char -> Bool) -> Parser Text -singleValue acceptCondition = mconcat <$> variants - where - variants = many $ - choice - [ doubleQuotedValue "a string inside double quotes" - , singleQuotedValue "a string inside single quotes" - , unquotedString acceptCondition "a string with no quotes" - ] - -pair :: Parser (Text, Text) -pair = do - key <- singleValue (/= '=') - value <- withEqualSign <|> withoutEqualSign - return (key, value) - where - withEqualSign = do - void $ char '=' - singleValue (\c -> c /= ' ' && c /= '\t') - withoutEqualSign = do - requiredWhitespace - untilEol "value" - -pairs :: Parser Pairs -pairs = (pair "a key value pair (key=value)") `sepEndBy1` requiredWhitespace - -label :: Parser Instr -label = do - reserved "LABEL" - Label <$> pairs arg :: Parser Instr arg = do @@ -437,13 +51,9 @@ arg = do nameWithDefault = do name <- someUnless "the argument name" (== '=') void $ char '=' - def <- untilEol "the argument value" - return $ Arg name (Just def) + df <- untilEol "the argument value" + return $ Arg name (Just df) -env :: Parser Instr -env = do - reserved "ENV" - Env <$> pairs user :: Parser Instr user = do @@ -451,71 +61,6 @@ user = do username <- untilEol "the user" return $ User username -add :: Parser Instr -add = do - reserved "ADD" - flag <- lexeme copyFlag <|> return (FlagChown NoChown) - notFollowedBy (string "--") "only the --chown flag or the src and dest paths" - case flag of - FlagChown ch -> fileList "ADD" (\src dest -> Add (AddArgs src dest ch)) - FlagSource _ -> customError $ InvalidFlagError "--from" - FlagInvalid (k, v) -> unexpectedFlag k v - -expose :: Parser Instr -expose = do - reserved "EXPOSE" - Expose <$> ports - -port :: Parser Port -port = - (try portVariable "a variable") <|> -- There a many valid representations of ports - (try portRange "a port range optionally followed by the protocol (udp/tcp)") <|> - (try portWithProtocol "a port with its protocol (udp/tcp)") <|> - (try portInt "a valid port number") - -ports :: Parser Ports -ports = Ports <$> port `sepEndBy` requiredWhitespace - -portRange :: Parser Port -portRange = do - start <- natural - void $ char '-' - finish <- try natural - proto <- try protocol <|> return TCP - return $ PortRange (fromIntegral start) (fromIntegral finish) proto - -protocol :: Parser Protocol -protocol = do - void (char '/') - tcp <|> udp - where - tcp = caseInsensitiveString "tcp" >> return TCP - udp = caseInsensitiveString "udp" >> return UDP - -portInt :: Parser Port -portInt = do - portNumber <- natural - notFollowedBy (string "/" <|> string "-") - return $ Port (fromIntegral portNumber) TCP - -portWithProtocol :: Parser Port -portWithProtocol = do - portNumber <- natural - Port (fromIntegral portNumber) <$> protocol - -portVariable :: Parser Port -portVariable = do - void (char '$') - variable <- someUnless "the variable name" (== '$') - return $ PortStr (T.append "$" variable) - -run :: Parser Instr -run = do - reserved "RUN" - Run <$> runArguments - -runArguments :: Parser (RunArgs Text) -runArguments = undefined workdir :: Parser Instr workdir = do @@ -535,21 +80,6 @@ maintainer = do name <- untilEol "the maintainer name" return $ Maintainer name --- Parse arguments of a command in the exec form -argumentsExec :: Parser (Arguments Text) -argumentsExec = do - args <- brackets $ commaSep stringLiteral - return $ ArgumentsList (T.unwords args) - --- Parse arguments of a command in the shell form -argumentsShell :: Parser (Arguments Text) -argumentsShell = ArgumentsText <$> toEnd - where - toEnd = untilEol "the shell arguments" - -arguments :: Parser (Arguments Text) -arguments = try argumentsExec <|> try argumentsShell - entrypoint :: Parser Instr entrypoint = do reserved "ENTRYPOINT" @@ -560,70 +90,6 @@ onbuild = do reserved "ONBUILD" OnBuild <$> parseInstruction -healthcheck :: Parser Instr -healthcheck = do - reserved "HEALTHCHECK" - Healthcheck <$> (fullCheck <|> noCheck) - where - noCheck = string "NONE" >> return NoCheck - allFlags = do - flags <- someFlags - requiredWhitespace "another flag" - return flags - someFlags = do - x <- checkFlag - cont <- try (requiredWhitespace >> lookAhead (string "--") >> return True) <|> return False - if cont - then do - xs <- someFlags - return (x : xs) - else return [x] - fullCheck = do - flags <- allFlags <|> return [] - let intervals = [x | FlagInterval x <- flags] - let timeouts = [x | FlagTimeout x <- flags] - let startPeriods = [x | FlagStartPeriod x <- flags] - let retriesD = [x | FlagRetries x <- flags] - let invalid = [x | CFlagInvalid x <- flags] - -- Let's do some validation on the flags - case (invalid, intervals, timeouts, startPeriods, retriesD) of - ((k, v):_, _, _, _, _) -> unexpectedFlag k v - (_, _:_:_, _, _, _) -> customError $ DuplicateFlagError "--interval" - (_, _, _:_:_, _, _) -> customError $ DuplicateFlagError "--timeout" - (_, _, _, _:_:_, _) -> customError $ DuplicateFlagError "--start-period" - (_, _, _, _, _:_:_) -> customError $ DuplicateFlagError "--retries" - _ -> do - Cmd checkCommand <- cmd - let interval = listToMaybe intervals - let timeout = listToMaybe timeouts - let startPeriod = listToMaybe startPeriods - let retries = listToMaybe retriesD - return $ Check CheckArgs {..} - -checkFlag :: Parser CheckFlag -checkFlag = - (FlagInterval <$> durationFlag "--interval=" "--interval") <|> - (FlagTimeout <$> durationFlag "--timeout=" "--timeout") <|> - (FlagStartPeriod <$> durationFlag "--start-period=" "--start-period") <|> - (FlagRetries <$> retriesFlag "--retries") <|> - (CFlagInvalid <$> anyFlag "no flags") - -durationFlag :: Text -> Parser Duration -durationFlag flagName = do - void $ try (string flagName) - scale <- natural - unit <- char 's' <|> char 'm' <|> char 'h' "either 's', 'm' or 'h' as the unit" - case unit of - 's' -> return $ Duration (secondsToDiffTime scale) - 'm' -> return $ Duration (secondsToDiffTime (scale * 60)) - 'h' -> return $ Duration (secondsToDiffTime (scale * 60 * 60)) - _ -> fail "only 's', 'm' or 'h' are allowed as the duration" - -retriesFlag :: Parser Retries -retriesFlag = do - void $ try (string "--retries=") - n <- try natural "the number of retries" - return $ Retries (fromIntegral n) ------------------------------------ -- Main Parser @@ -631,24 +97,24 @@ retriesFlag = do parseInstruction :: Parser Instr parseInstruction = onbuild <|> -- parse all main instructions - from <|> - copy <|> - run <|> + parseFrom <|> + parseCopy <|> + parseRun <|> workdir <|> entrypoint <|> volume <|> - expose <|> - env <|> + parseExpose <|> + parseEnv <|> arg <|> user <|> - label <|> + parseLabel <|> stopsignal <|> - cmd <|> + parseCmd <|> shell <|> maintainer <|> - add <|> + parseAdd <|> comment <|> - healthcheck + parseHealthcheck contents :: Parser a -> Parser a contents p = do @@ -671,12 +137,10 @@ parseText = parse (contents dockerfile) "" parseFile :: FilePath -> IO (Either Error Dockerfile) parseFile file = doParse <$> B.readFile file where - doParse = - parse (contents dockerfile) file . E.decodeUtf8With E.lenientDecode + doParse = parse (contents dockerfile) file . E.decodeUtf8With E.lenientDecode -- | Reads the standard input until the end and parses the contents as a Dockerfile parseStdin :: IO (Either Error Dockerfile) parseStdin = doParse <$> B.getContents where - doParse = - parse (contents dockerfile) "/dev/stdin" . E.decodeUtf8With E.lenientDecode + doParse = parse (contents dockerfile) "/dev/stdin" . E.decodeUtf8With E.lenientDecode diff --git a/src/Language/Docker/Parser/Arguments.hs b/src/Language/Docker/Parser/Arguments.hs new file mode 100644 index 0000000..7069430 --- /dev/null +++ b/src/Language/Docker/Parser/Arguments.hs @@ -0,0 +1,23 @@ +module Language.Docker.Parser.Arguments + ( arguments, + ) +where + +import qualified Data.Text as T +import Language.Docker.Parser.Prelude +import Language.Docker.Syntax + +-- Parse arguments of a command in the exec form +argumentsExec :: Parser (Arguments Text) +argumentsExec = do + args <- brackets $ commaSep stringLiteral + return $ ArgumentsList (T.unwords args) + +-- Parse arguments of a command in the shell form +argumentsShell :: Parser (Arguments Text) +argumentsShell = ArgumentsText <$> toEnd + where + toEnd = untilEol "the shell arguments" + +arguments :: Parser (Arguments Text) +arguments = try argumentsExec <|> try argumentsShell diff --git a/src/Language/Docker/Parser/Cmd.hs b/src/Language/Docker/Parser/Cmd.hs new file mode 100644 index 0000000..fbf4a96 --- /dev/null +++ b/src/Language/Docker/Parser/Cmd.hs @@ -0,0 +1,15 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Language.Docker.Parser.Cmd + ( parseCmd, + ) +where + +import Language.Docker.Parser.Arguments +import Language.Docker.Parser.Prelude +import Language.Docker.Syntax + +parseCmd :: Parser Instr +parseCmd = do + reserved "CMD" + Cmd <$> arguments diff --git a/src/Language/Docker/Parser/Copy.hs b/src/Language/Docker/Parser/Copy.hs new file mode 100644 index 0000000..6096f60 --- /dev/null +++ b/src/Language/Docker/Parser/Copy.hs @@ -0,0 +1,93 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Language.Docker.Parser.Copy + ( parseCopy, + parseAdd, + ) +where + +import Data.List.NonEmpty (NonEmpty, fromList) +import qualified Data.Text as T +import Language.Docker.Parser.Prelude +import Language.Docker.Syntax + +data CopyFlag + = FlagChown Chown + | FlagSource CopySource + | FlagInvalid (Text, Text) + +parseCopy :: Parser Instr +parseCopy = do + reserved "COPY" + flags <- copyFlag `sepEndBy` requiredWhitespace + let chownFlags = [c | FlagChown c <- flags] + let sourceFlags = [f | FlagSource f <- flags] + let invalid = [i | FlagInvalid i <- flags] + -- Let's do some validation on the flags + case (invalid, chownFlags, sourceFlags) of + ((k, v) : _, _, _) -> unexpectedFlag k v + (_, _ : _ : _, _) -> customError $ DuplicateFlagError "--chown" + (_, _, _ : _ : _) -> customError $ DuplicateFlagError "--from" + _ -> do + let ch = + case chownFlags of + [] -> NoChown + c : _ -> c + let fr = + case sourceFlags of + [] -> NoSource + f : _ -> f + fileList "COPY" (\src dest -> Copy (CopyArgs src dest ch fr)) + +parseAdd :: Parser Instr +parseAdd = do + reserved "ADD" + flag <- lexeme copyFlag <|> return (FlagChown NoChown) + notFollowedBy (string "--") "only the --chown flag or the src and dest paths" + case flag of + FlagChown ch -> fileList "ADD" (\src dest -> Add (AddArgs src dest ch)) + FlagSource _ -> customError $ InvalidFlagError "--from" + FlagInvalid (k, v) -> unexpectedFlag k v + +fileList :: Text -> (NonEmpty SourcePath -> TargetPath -> Instr) -> Parser Instr +fileList name constr = do + paths <- + (try stringList "an array of strings [\"src_file\", \"dest_file\"]") + <|> (try spaceSeparated "a space separated list of file paths") + case paths of + [_] -> customError $ FileListError (T.unpack name) + _ -> return $ constr (SourcePath <$> fromList (init paths)) (TargetPath $ last paths) + where + spaceSeparated = + anyUnless (== ' ') `sepEndBy1` (try requiredWhitespace "at least another file path") + stringList = brackets $ commaSep stringLiteral + +unexpectedFlag :: Text -> Text -> Parser a +unexpectedFlag name "" = customFailure $ NoValueFlagError (T.unpack name) +unexpectedFlag name _ = customFailure $ InvalidFlagError (T.unpack name) + +copyFlag :: Parser CopyFlag +copyFlag = + (FlagChown <$> try chown "only one --chown") + <|> (FlagSource <$> try copySource "only one --from") + <|> (FlagInvalid <$> try anyFlag "no other flags") + +chown :: Parser Chown +chown = do + void $ string "--chown=" + ch <- someUnless "the user and group for chown" (== ' ') + return $ Chown ch + +copySource :: Parser CopySource +copySource = do + void $ string "--from=" + src <- someUnless "the copy source path" isNl + return $ CopySource src + +anyFlag :: Parser (Text, Text) +anyFlag = do + void $ string "--" + name <- someUnless "the flag value" (== '=') + void $ char '=' + val <- anyUnless (== ' ') + return (T.append "--" name, val) diff --git a/src/Language/Docker/Parser/Expose.hs b/src/Language/Docker/Parser/Expose.hs new file mode 100644 index 0000000..c71e821 --- /dev/null +++ b/src/Language/Docker/Parser/Expose.hs @@ -0,0 +1,58 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Language.Docker.Parser.Expose + ( parseExpose, + ) +where + +import qualified Data.Text as T +import Language.Docker.Parser.Prelude +import Language.Docker.Syntax + +parseExpose :: Parser Instr +parseExpose = do + reserved "EXPOSE" + Expose <$> ports + +port :: Parser Port +port = + (try portVariable "a variable") + <|> (try portRange "a port range optionally followed by the protocol (udp/tcp)") -- There a many valid representations of ports + <|> (try portWithProtocol "a port with its protocol (udp/tcp)") + <|> (try portInt "a valid port number") + +ports :: Parser Ports +ports = Ports <$> port `sepEndBy` requiredWhitespace + +portRange :: Parser Port +portRange = do + start <- natural + void $ char '-' + finish <- try natural + proto <- try protocol <|> return TCP + return $ PortRange (fromIntegral start) (fromIntegral finish) proto + +protocol :: Parser Protocol +protocol = do + void (char '/') + tcp <|> udp + where + tcp = caseInsensitiveString "tcp" >> return TCP + udp = caseInsensitiveString "udp" >> return UDP + +portInt :: Parser Port +portInt = do + portNumber <- natural + notFollowedBy (string "/" <|> string "-") + return $ Port (fromIntegral portNumber) TCP + +portWithProtocol :: Parser Port +portWithProtocol = do + portNumber <- natural + Port (fromIntegral portNumber) <$> protocol + +portVariable :: Parser Port +portVariable = do + void (char '$') + variable <- someUnless "the variable name" (== '$') + return $ PortStr (T.append "$" variable) diff --git a/src/Language/Docker/Parser/From.hs b/src/Language/Docker/Parser/From.hs new file mode 100644 index 0000000..1789298 --- /dev/null +++ b/src/Language/Docker/Parser/From.hs @@ -0,0 +1,74 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Language.Docker.Parser.From + ( parseFrom, + ) +where + +import qualified Data.Text as T +import Language.Docker.Parser.Prelude +import Language.Docker.Syntax + +parseRegistry :: Parser Registry +parseRegistry = do + domain <- someUnless "a domain name" (== '.') + void $ char '.' + tld <- someUnless "a TLD" (== '/') + void $ char '/' + return $ Registry (domain <> "." <> tld) + +parsePlatform :: Parser Platform +parsePlatform = do + void $ string "--platform=" + p <- someUnless "the platform for the FROM image" (== ' ') + requiredWhitespace + return p + +parseBaseImage :: (Text -> Parser (Maybe Tag)) -> Parser BaseImage +parseBaseImage tagParser = do + maybePlatform <- (Just <$> try parsePlatform) <|> return Nothing + notFollowedBy (string "--") + regName <- (Just <$> try parseRegistry) <|> return Nothing + name <- someUnless "the image name with a tag" (\c -> c == '@' || c == ':') + maybeTag <- tagParser name <|> return Nothing + maybeDigest <- (Just <$> try parseDigest) <|> return Nothing + maybeAlias <- (Just <$> try (requiredWhitespace *> imageAlias)) <|> return Nothing + return $ BaseImage (Image regName name) maybeTag maybeDigest maybeAlias maybePlatform + +taggedImage :: Parser BaseImage +taggedImage = parseBaseImage tagParser + where + tagParser _ = do + void $ char ':' + t <- someUnless "the image tag" (\c -> c == '@' || c == ':') + return (Just . Tag $ t) + +parseDigest :: Parser Digest +parseDigest = do + void $ char '@' + d <- someUnless "the image digest" (== '@') + return $ Digest d + +untaggedImage :: Parser BaseImage +untaggedImage = parseBaseImage notInvalidTag + where + notInvalidTag :: Text -> Parser (Maybe Tag) + notInvalidTag name = do + try (notFollowedBy $ string ":") "no ':' or a valid image tag string (example: " + ++ T.unpack name + ++ ":valid-tag)" + return Nothing + +imageAlias :: Parser ImageAlias +imageAlias = do + void (try (reserved "AS") "'AS' followed by the image alias") + aka <- someUnless "the image alias" (== '\n') + return $ ImageAlias aka + +baseImage :: Parser BaseImage +baseImage = try taggedImage <|> untaggedImage + +parseFrom :: Parser Instr +parseFrom = do + reserved "FROM" + From <$> baseImage diff --git a/src/Language/Docker/Parser/Healthcheck.hs b/src/Language/Docker/Parser/Healthcheck.hs new file mode 100644 index 0000000..9d98752 --- /dev/null +++ b/src/Language/Docker/Parser/Healthcheck.hs @@ -0,0 +1,98 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +module Language.Docker.Parser.Healthcheck + ( parseHealthcheck, + ) +where + +import Data.Maybe (listToMaybe) +import qualified Data.Text as T +import Data.Time.Clock (secondsToDiffTime) +import Language.Docker.Parser.Cmd (parseCmd) +import Language.Docker.Parser.Prelude +import Language.Docker.Syntax + +data CheckFlag + = FlagInterval Duration + | FlagTimeout Duration + | FlagStartPeriod Duration + | FlagRetries Retries + | CFlagInvalid (Text, Text) + +parseHealthcheck :: Parser Instr +parseHealthcheck = do + reserved "HEALTHCHECK" + Healthcheck <$> (fullCheck <|> noCheck) + where + noCheck = string "NONE" >> return NoCheck + allFlags = do + flags <- someFlags + requiredWhitespace "another flag" + return flags + someFlags = do + x <- checkFlag + cont <- try (requiredWhitespace >> lookAhead (string "--") >> return True) <|> return False + if cont + then do + xs <- someFlags + return (x : xs) + else return [x] + fullCheck = do + flags <- allFlags <|> return [] + let intervals = [x | FlagInterval x <- flags] + let timeouts = [x | FlagTimeout x <- flags] + let startPeriods = [x | FlagStartPeriod x <- flags] + let retriesD = [x | FlagRetries x <- flags] + let invalid = [x | CFlagInvalid x <- flags] + -- Let's do some validation on the flags + case (invalid, intervals, timeouts, startPeriods, retriesD) of + ((k, v) : _, _, _, _, _) -> unexpectedFlag k v + (_, _ : _ : _, _, _, _) -> customError $ DuplicateFlagError "--interval" + (_, _, _ : _ : _, _, _) -> customError $ DuplicateFlagError "--timeout" + (_, _, _, _ : _ : _, _) -> customError $ DuplicateFlagError "--start-period" + (_, _, _, _, _ : _ : _) -> customError $ DuplicateFlagError "--retries" + _ -> do + Cmd checkCommand <- parseCmd + let interval = listToMaybe intervals + let timeout = listToMaybe timeouts + let startPeriod = listToMaybe startPeriods + let retries = listToMaybe retriesD + return $ Check CheckArgs {..} + +checkFlag :: Parser CheckFlag +checkFlag = + (FlagInterval <$> durationFlag "--interval=" "--interval") + <|> (FlagTimeout <$> durationFlag "--timeout=" "--timeout") + <|> (FlagStartPeriod <$> durationFlag "--start-period=" "--start-period") + <|> (FlagRetries <$> retriesFlag "--retries") + <|> (CFlagInvalid <$> anyFlag "no flags") + +durationFlag :: Text -> Parser Duration +durationFlag flagName = do + void $ try (string flagName) + scale <- natural + unit <- char 's' <|> char 'm' <|> char 'h' "either 's', 'm' or 'h' as the unit" + case unit of + 's' -> return $ Duration (secondsToDiffTime scale) + 'm' -> return $ Duration (secondsToDiffTime (scale * 60)) + 'h' -> return $ Duration (secondsToDiffTime (scale * 60 * 60)) + _ -> fail "only 's', 'm' or 'h' are allowed as the duration" + +retriesFlag :: Parser Retries +retriesFlag = do + void $ try (string "--retries=") + n <- try natural "the number of retries" + return $ Retries (fromIntegral n) + +anyFlag :: Parser (Text, Text) +anyFlag = do + void $ string "--" + name <- someUnless "the flag value" (== '=') + void $ char '=' + val <- anyUnless (== ' ') + return (T.append "--" name, val) + +unexpectedFlag :: Text -> Text -> Parser a +unexpectedFlag name "" = customFailure $ NoValueFlagError (T.unpack name) +unexpectedFlag name _ = customFailure $ InvalidFlagError (T.unpack name) diff --git a/src/Language/Docker/Parser/Pairs.hs b/src/Language/Docker/Parser/Pairs.hs new file mode 100644 index 0000000..4a430f0 --- /dev/null +++ b/src/Language/Docker/Parser/Pairs.hs @@ -0,0 +1,67 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Language.Docker.Parser.Pairs + ( parseEnv, + parseLabel, + ) +where + +import qualified Data.Text as T +import Language.Docker.Parser.Prelude +import Language.Docker.Syntax + +-- We cannot use string literal because it swallows space +-- and therefore have to implement quoted values by ourselves +doubleQuotedValue :: Parser Text +doubleQuotedValue = between (string "\"") (string "\"") (stringWithEscaped ['"'] Nothing) + +singleQuotedValue :: Parser Text +singleQuotedValue = between (string "'") (string "'") (stringWithEscaped ['\''] Nothing) + +unquotedString :: (Char -> Bool) -> Parser Text +unquotedString acceptCondition = do + str <- stringWithEscaped [' ', '\t'] (Just (\c -> acceptCondition c && c /= '"' && c /= '\'')) + checkFaults str + where + checkFaults str + | T.null str = fail "a non empty string" + | T.head str == '\'' = customError $ QuoteError "single" (T.unpack str) + | T.head str == '\"' = customError $ QuoteError "double" (T.unpack str) + | otherwise = return str + +singleValue :: (Char -> Bool) -> Parser Text +singleValue acceptCondition = mconcat <$> variants + where + variants = + many $ + choice + [ doubleQuotedValue "a string inside double quotes", + singleQuotedValue "a string inside single quotes", + unquotedString acceptCondition "a string with no quotes" + ] + +pair :: Parser (Text, Text) +pair = do + key <- singleValue (/= '=') + value <- withEqualSign <|> withoutEqualSign + return (key, value) + where + withEqualSign = do + void $ char '=' + singleValue (\c -> c /= ' ' && c /= '\t') + withoutEqualSign = do + requiredWhitespace + untilEol "value" + +pairs :: Parser Pairs +pairs = (pair "a key value pair (key=value)") `sepEndBy1` requiredWhitespace + +parseLabel :: Parser Instr +parseLabel = do + reserved "LABEL" + Label <$> pairs + +parseEnv :: Parser Instr +parseEnv = do + reserved "ENV" + Env <$> pairs diff --git a/src/Language/Docker/Parser/Prelude.hs b/src/Language/Docker/Parser/Prelude.hs new file mode 100644 index 0000000..f68fad2 --- /dev/null +++ b/src/Language/Docker/Parser/Prelude.hs @@ -0,0 +1,249 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE OverloadedStrings #-} + +module Language.Docker.Parser.Prelude + ( customError, + comment, + eol, + reserved, + natural, + commaSep, + stringLiteral, + brackets, + whitespace, + requiredWhitespace, + untilEol, + symbol, + caseInsensitiveString, + stringWithEscaped, + lexeme, + isNl, + isSpaceNl, + anyUnless, + someUnless, + Parser, + Error, + DockerfileError (..), + Instr, + module Megaparsec, + char, + string, + void, + when, + Text, + module Data.Default.Class + ) +where + +import Control.Monad (void, when) +import Data.Data +import Data.Maybe (fromMaybe) +import qualified Data.Set as S +import Data.Text (Text) +import qualified Data.Text as T +import Language.Docker.Syntax +import Text.Megaparsec as Megaparsec hiding (Label) +import Text.Megaparsec.Char hiding (eol) +import qualified Text.Megaparsec.Char.Lexer as L +import Data.Default.Class (Default(def)) + +data DockerfileError + = DuplicateFlagError String + | NoValueFlagError String + | InvalidFlagError String + | FileListError String + | MissingArgument [Text] + | DuplicateArgument Text + | UnexpectedArgument Text Text + | QuoteError + String + String + deriving (Eq, Data, Typeable, Ord, Read, Show) + +type Parser = Parsec DockerfileError Text + +type Error = ParseErrorBundle Text DockerfileError + +type Instr = Instruction Text + +instance ShowErrorComponent DockerfileError where + showErrorComponent (DuplicateFlagError f) = "duplicate flag: " ++ f + showErrorComponent (FileListError f) = + "unexpected end of line. At least two arguments are required for " ++ f + showErrorComponent (NoValueFlagError f) = "unexpected flag " ++ f ++ " with no value" + showErrorComponent (InvalidFlagError f) = "invalid flag: " ++ f + showErrorComponent (MissingArgument f) = "missing required argument(s) for mount flag: " ++ show f + showErrorComponent (DuplicateArgument f) = "duplicate argument for mount flag: " ++ T.unpack f + showErrorComponent (UnexpectedArgument a b) = "unexpected argument '" ++ T.unpack a ++ "' for mount of type '" ++ T.unpack b ++ "'" + showErrorComponent (QuoteError t str) = + "unexpected end of " ++ t ++ " quoted string " ++ str ++ " (unmatched quote)" + +-- Spaces are sometimes significant information in a dockerfile, this type records +-- thee presence of lack of such whitespace in certain lines. +data FoundWhitespace + = FoundWhitespace + | MissingWhitespace + deriving (Eq, Show) + +-- There is no need to remember how mamny spaces we found in a line, so we can +-- cheaply remmeber that we already whitenessed some significant whitespace while +-- parsing an expression by concatenating smaller results +instance Semigroup FoundWhitespace where + FoundWhitespace <> _ = FoundWhitespace + _ <> a = a + +instance Monoid FoundWhitespace where + mempty = MissingWhitespace + +------------------------------------ +-- Utilities +------------------------------------ + +-- | End parsing signaling a “conversion error”. +customError :: DockerfileError -> Parser a +customError = fancyFailure . S.singleton . ErrorCustom + +castToSpace :: FoundWhitespace -> Text +castToSpace FoundWhitespace = " " +castToSpace MissingWhitespace = "" + +eol :: Parser () +eol = void ws "end of line" + where + ws = + some $ + choice [void onlySpaces1, void $ takeWhile1P Nothing (== '\n'), void escapedLineBreaks] + +reserved :: Text -> Parser () +reserved name = void (lexeme (string' name) T.unpack name) + +natural :: Parser Integer +natural = L.decimal "positive number" + +commaSep :: Parser a -> Parser [a] +commaSep p = sepBy (p <* whitespace) (symbol ",") + +stringLiteral :: Parser Text +stringLiteral = do + void (char '"') + lit <- manyTill L.charLiteral (char '"') + return (T.pack lit) + +brackets :: Parser a -> Parser a +brackets = between (symbol "[" *> whitespace) (whitespace *> symbol "]") + +onlySpaces :: Parser Text +onlySpaces = takeWhileP (Just "spaces") (\c -> c == ' ' || c == '\t') + +onlySpaces1 :: Parser Text +onlySpaces1 = takeWhile1P (Just "at least one space") (\c -> c == ' ' || c == '\t') + +escapedLineBreaks :: Parser FoundWhitespace +escapedLineBreaks = mconcat <$> breaks + where + breaks = + some $ do + try (char '\\' *> onlySpaces *> newlines) + skipMany . try $ onlySpaces *> comment *> newlines + -- Spaces before the next '\' have a special significance + -- so we remembeer the fact that we found some + FoundWhitespace <$ onlySpaces1 <|> pure MissingWhitespace + newlines = takeWhile1P Nothing isNl + +foundWhitespace :: Parser FoundWhitespace +foundWhitespace = mconcat <$> found + where + found = many $ choice [FoundWhitespace <$ onlySpaces1, escapedLineBreaks] + +whitespace :: Parser () +whitespace = void foundWhitespace + +requiredWhitespace :: Parser () +requiredWhitespace = do + ws <- foundWhitespace + case ws of + FoundWhitespace -> pure () + MissingWhitespace -> fail "missing whitespace" + +-- Parse value until end of line is reached +-- after consuming all escaped newlines +untilEol :: String -> Parser Text +untilEol name = do + res <- mconcat <$> predicate + when (res == "") $ fail ("expecting " ++ name) + pure res + where + predicate = + many $ + choice + [ castToSpace <$> escapedLineBreaks, + takeWhile1P (Just name) (\c -> c /= '\n' && c /= '\\'), + takeWhile1P Nothing (== '\\') <* notFollowedBy (char '\n') + ] + +symbol :: Text -> Parser Text +symbol name = do + x <- string name + whitespace + return x + +caseInsensitiveString :: Text -> Parser Text +caseInsensitiveString = string' + +stringWithEscaped :: [Char] -> Maybe (Char -> Bool) -> Parser Text +stringWithEscaped quoteChars maybeAcceptCondition = mconcat <$> sequences + where + sequences = + many $ + choice + [ mconcat <$> inner, + try $ takeWhile1P Nothing (== '\\') <* notFollowedBy quoteParser, + string "\\" *> quoteParser + ] + inner = + some $ + choice + [ castToSpace <$> escapedLineBreaks, + takeWhile1P + Nothing + (\c -> c /= '\\' && c /= '\n' && c `notElem` quoteChars && acceptCondition c) + ] + quoteParser = T.singleton <$> choice (fmap char quoteChars) + acceptCondition = fromMaybe (const True) maybeAcceptCondition + +lexeme :: Parser a -> Parser a +lexeme p = do + x <- p + requiredWhitespace + return x + +isNl :: Char -> Bool +isNl c = c == '\n' + +isSpaceNl :: Char -> Bool +isSpaceNl c = c == ' ' || c == '\t' || c == '\n' || c == '\\' + +anyUnless :: (Char -> Bool) -> Parser Text +anyUnless predicate = someUnless "" predicate <|> pure "" + +someUnless :: String -> (Char -> Bool) -> Parser Text +someUnless name predicate = do + res <- applyPredicate + case res of + [] -> fail ("expecting " ++ name) + _ -> pure (mconcat res) + where + applyPredicate = + many $ + choice + [ castToSpace <$> escapedLineBreaks, + takeWhile1P (Just name) (\c -> not (isSpaceNl c || predicate c)), + takeWhile1P Nothing (\c -> c == '\\' && not (predicate c)) + <* notFollowedBy (char '\n') + ] + +comment :: Parser Instr +comment = do + void $ char '#' + text <- takeWhileP Nothing (not . isNl) + return $ Comment text diff --git a/src/Language/Docker/Parser/Run.hs b/src/Language/Docker/Parser/Run.hs new file mode 100644 index 0000000..0815983 --- /dev/null +++ b/src/Language/Docker/Parser/Run.hs @@ -0,0 +1,294 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedStrings #-} + +module Language.Docker.Parser.Run + ( parseRun, + runFlags, + ) +where + +import Data.Functor (($>)) +import qualified Data.Set as Set +import Language.Docker.Parser.Arguments (arguments) +import Language.Docker.Parser.Prelude +import Language.Docker.Syntax + +data RunFlag + = RunFlagMount RunMount + | RunFlagSecurity RunSecurity + | RunFlagNetwork RunNetwork + deriving (Show) + +data RunMountArg + = MountArgFromImage Text + | MountArgGid Integer + | MountArgId Text + | MountArgMode Text + | MountArgReadOnly Bool + | MountArgRequired + | MountArgSharing CacheSharing + | MountArgSource SourcePath + | MountArgTarget TargetPath + | MountArgType Text + | MountArgUid Integer + deriving (Show) + +data MountType + = Bind + | Cache + | Tmpfs + | Secret + | Ssh + +parseRun :: Parser Instr +parseRun = do + reserved "RUN" + Run <$> runArguments + +runArguments :: Parser (RunArgs Text) +runArguments = do + presentFlags <- choice [runFlags, pure (RunFlags Nothing Nothing Nothing)] + requiredWhitespace + args <- arguments + return $ RunArgs args presentFlags + +runFlags :: Parser RunFlags +runFlags = do + flags <- runFlag `sepBy` flagSeparator + return $ foldr toRunFlags emptyFlags flags + where + flagSeparator = try (requiredWhitespace *> lookAhead (string "--")) <|> fail "expected flag" + emptyFlags = RunFlags Nothing Nothing Nothing + toRunFlags (RunFlagMount m) rf = rf {mount = Just m} + toRunFlags (RunFlagNetwork n) rf = rf {network = Just n} + toRunFlags (RunFlagSecurity s) rf = rf {security = Just s} + +runFlag :: Parser RunFlag +runFlag = + choice + [RunFlagMount <$> runFlagMount, RunFlagSecurity <$> runFlagSecurity, RunFlagNetwork <$> runFlagNetwork] + +runFlagSecurity :: Parser RunSecurity +runFlagSecurity = do + void $ string "--security=" + choice [Insecure <$ string "insecure", Sandbox <$ string "sandbox"] + +runFlagNetwork :: Parser RunNetwork +runFlagNetwork = do + void $ string "--network=" + choice [NetworkNone <$ string "none", NetworkHost <$ string "host", NetworkDefault <$ string "default"] + +runFlagMount :: Parser RunMount +runFlagMount = do + void $ string "--mount=" + maybeType <- + choice + [ string "type=" + *> choice + [ Just Bind <$ string "bind", + Just Cache <$ string "cache", + Just Tmpfs <$ string "tmpfs", + Just Secret <$ string "secret", + Just Ssh <$ "ssh" + ], + pure Nothing + ] + (mountType, args) <- return $ + case maybeType of + Nothing -> (Bind, argsParser Bind) + Just Ssh -> (Ssh, choice [argsParser Ssh, pure []]) + Just t -> (t, string "," *> argsParser t) + case mountType of + Bind -> BindMount <$> (bindMount =<< args) + Cache -> CacheMount <$> (cacheMount =<< args) + Tmpfs -> TmpfsMount <$> (tmpfsMount =<< args) + Secret -> SecretMount <$> (secretMount =<< args) + Ssh -> SshMount <$> (secretMount =<< args) + +argsParser :: MountType -> Parser [RunMountArg] +argsParser mountType = mountChoices mountType `sepBy1` string "," + +bindMount :: [RunMountArg] -> Parser BindOpts +bindMount args = + case validArgs "bind" allowed required args of + Left e -> customError e + Right as -> return $ foldr bindOpts def as + where + allowed = Set.fromList ["target", "source", "from", "ro"] + required = Set.singleton "target" + bindOpts :: RunMountArg -> BindOpts -> BindOpts + bindOpts (MountArgTarget path) bo = bo {target = path} + bindOpts (MountArgSource path) bo = bo {source = Just path} + bindOpts (MountArgFromImage img) bo = bo {fromImage = Just img} + bindOpts (MountArgReadOnly ro) bo = bo {readOnly = Just ro} + bindOpts invalid _ = error $ "unhandled " <> show invalid <> " please report this bug" + +cacheMount :: [RunMountArg] -> Parser CacheOpts +cacheMount args = + case validArgs "cache" allowed required args of + Left e -> customError e + Right as -> return $ foldr cacheOpts def as + where + allowed = Set.fromList ["target", "sharing", "id", "ro", "from", "source", "mode", "uid", "gid"] + required = Set.fromList ["target", "sharing"] + cacheOpts :: RunMountArg -> CacheOpts -> CacheOpts + cacheOpts (MountArgTarget path) co = co {target = path} + cacheOpts (MountArgSharing sh) co = co {sharing = sh} + cacheOpts (MountArgId i) co = co {cacheId = Just i} + cacheOpts (MountArgReadOnly ro) co = co {readOnly = Just ro} + cacheOpts (MountArgFromImage img) co = co {fromImage = Just img} + cacheOpts (MountArgSource path) co = co {source = Just path} + cacheOpts (MountArgMode m) co = co {mode = Just m} + cacheOpts (MountArgUid u) co = co {uid = Just u} + cacheOpts (MountArgGid g) co = co {gid = Just g} + cacheOpts invalid _ = error $ "unhandled " <> show invalid <> " please report this bug" + +tmpfsMount :: [RunMountArg] -> Parser TmpOpts +tmpfsMount args = + case validArgs "tmpfs" required required args of + Left e -> customError e + Right as -> return $ foldr tmpOpts def as + where + required = Set.singleton "target" + tmpOpts :: RunMountArg -> TmpOpts -> TmpOpts + tmpOpts (MountArgTarget path) t = t {target = path} + tmpOpts invalid _ = error $ "unhandled " <> show invalid <> " please report this bug" + +secretMount :: [RunMountArg] -> Parser SecretOpts +secretMount args = + case validArgs "secret" allowed required args of + Left e -> customError e + Right as -> return $ foldr secretOpts def as + where + allowed = Set.fromList ["target", "id", "required", "source", "mode", "uid", "gid"] + required = Set.empty + secretOpts :: RunMountArg -> SecretOpts -> SecretOpts + secretOpts (MountArgTarget path) co = co {target = Just path} + secretOpts (MountArgId i) co = co {cacheId = Just i} + secretOpts (MountArgSource path) co = co {source = Just path} + secretOpts (MountArgMode m) co = co {mode = Just m} + secretOpts (MountArgUid u) co = co {uid = Just u} + secretOpts (MountArgGid g) co = co {gid = Just g} + secretOpts invalid _ = error $ "unhandled " <> show invalid <> " please report this bug" + +validArgs :: + Foldable t => + Text -> + Set.Set Text -> + Set.Set Text -> + t RunMountArg -> + Either DockerfileError [RunMountArg] +validArgs typeName allowed required args = + let (result, seen) = foldr checkValidArg (Right [], Set.empty) args + in case Set.toList (Set.difference required seen) of + [] -> result + missing -> Left $ MissingArgument missing + where + checkValidArg _ x@(Left _, _) = x + checkValidArg a (Right as, seen) = + let name = toArgName a + in case (Set.member name allowed, Set.member name seen) of + (False, _) -> (Left (UnexpectedArgument name typeName), seen) + (_, True) -> (Left (DuplicateArgument name), seen) + (True, False) -> (Right (a : as), Set.insert name seen) + +mountChoices :: MountType -> Parser RunMountArg +mountChoices mountType = + choice $ + case mountType of + Bind -> + [ mountArgTarget, + mountArgSource, + mountArgFromImage, + mountArgReadOnly, + mountArgReadWrite + ] + Cache -> + [ mountArgTarget, + mountArgSource, + mountArgFromImage, + mountArgReadOnly, + mountArgReadWrite, + mountArgId, + mountArgSharing, + mountArgMode, + mountArgUid, + mountArgGid + ] + Tmpfs -> [mountArgTarget] + Secret -> + [ mountArgTarget, + mountArgId, + mountArgRequired, + mountArgMode, + mountArgUid, + mountArgGid + ] + Ssh -> + [ mountArgTarget, + mountArgId, + mountArgRequired, + mountArgMode, + mountArgUid, + mountArgGid + ] + +stringArg :: Parser Text +stringArg = choice [stringLiteral, someUnless "a string" (== ',')] + +key :: Text -> Parser a -> Parser a +key name p = string (name <> "=") *> p + +cacheSharing :: Parser CacheSharing +cacheSharing = + choice [Private <$ string "private", Shared <$ string "shared", Locked <$ string "locked"] + +mountArgFromImage :: Parser RunMountArg +mountArgFromImage = MountArgFromImage <$> key "from" stringArg + +mountArgGid :: Parser RunMountArg +mountArgGid = MountArgGid <$> key "gid" natural + +mountArgId :: Parser RunMountArg +mountArgId = MountArgId <$> key "id" stringArg + +mountArgMode :: Parser RunMountArg +mountArgMode = MountArgMode <$> key "mode" stringArg + +mountArgReadOnly :: Parser RunMountArg +mountArgReadOnly = MountArgReadOnly <$> (choice ["ro", "readonly"] $> True) + +mountArgReadWrite :: Parser RunMountArg +mountArgReadWrite = MountArgReadOnly <$> (choice ["rw", "readwrite"] $> False) + +mountArgRequired :: Parser RunMountArg +mountArgRequired = MountArgRequired <$ string "required" + +mountArgSharing :: Parser RunMountArg +mountArgSharing = MountArgSharing <$> key "sharing" cacheSharing + +mountArgSource :: Parser RunMountArg +mountArgSource = do + label "source=" $ choice [string "source=", string "src="] + MountArgSource . SourcePath <$> stringArg + +mountArgTarget :: Parser RunMountArg +mountArgTarget = do + label "target=" $ choice [string "target=", string "dest=", string "destination="] + MountArgTarget . TargetPath <$> stringArg + +mountArgUid :: Parser RunMountArg +mountArgUid = MountArgUid <$> key "uid" natural + +toArgName :: RunMountArg -> Text +toArgName (MountArgFromImage _) = "from" +toArgName (MountArgGid _) = "gid" +toArgName (MountArgId _) = "id" +toArgName (MountArgMode _) = "mode" +toArgName (MountArgReadOnly _) = "ro" +toArgName MountArgRequired = "required" +toArgName (MountArgSharing _) = "sharing" +toArgName (MountArgSource _) = "source" +toArgName (MountArgTarget _) = "target" +toArgName (MountArgType _) = "type" +toArgName (MountArgUid _) = "uid" diff --git a/src/Language/Docker/PrettyPrint.hs b/src/Language/Docker/PrettyPrint.hs index 071454e..0c85a3a 100644 --- a/src/Language/Docker/PrettyPrint.hs +++ b/src/Language/Docker/PrettyPrint.hs @@ -181,7 +181,7 @@ prettyPrintInstruction i = Volume dir -> do "VOLUME" pretty dir - Run c -> do + Run (RunArgs c _f) -> do "RUN" pretty c Copy CopyArgs {sourcePaths, targetPath, chownFlag, sourceFlag} -> do diff --git a/src/Language/Docker/Syntax.hs b/src/Language/Docker/Syntax.hs index f00baca..b87de41 100644 --- a/src/Language/Docker/Syntax.hs +++ b/src/Language/Docker/Syntax.hs @@ -1,261 +1,323 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving, TypeFamilies, - DuplicateRecordFields, FlexibleInstances, DeriveFunctor #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeFamilies #-} module Language.Docker.Syntax where +import Data.Default.Class (Default (..)) import Data.List (intercalate, isInfixOf) import Data.List.NonEmpty (NonEmpty) import Data.List.Split (endBy) -import Data.String (IsString(..)) +import Data.String (IsString (..)) import Data.Text (Text) import qualified Data.Text as Text import Data.Time.Clock (DiffTime) -import GHC.Exts (IsList(..)) +import GHC.Exts (IsList (..)) -data Image = Image - { registryName :: !(Maybe Registry) - , imageName :: !Text - } deriving (Show, Eq, Ord) +data Image + = Image + { registryName :: !(Maybe Registry), + imageName :: !Text + } + deriving (Show, Eq, Ord) instance IsString Image where - fromString img = - if "/" `isInfixOf` img - then let parts = endBy "/" img - in if "." `isInfixOf` head parts - then Image - (Just (Registry (Text.pack (head parts)))) - (Text.pack . intercalate "/" $ tail parts) - else Image Nothing (Text.pack img) - else Image Nothing (Text.pack img) - -newtype Registry = Registry - { unRegistry :: Text - } deriving (Show, Eq, Ord, IsString) - -newtype Tag = Tag - { unTag :: Text - } deriving (Show, Eq, Ord, IsString) - -newtype Digest = Digest - { unDigest :: Text - } deriving (Show, Eq, Ord, IsString) + fromString img = + if "/" `isInfixOf` img + then + let parts = endBy "/" img + in if "." `isInfixOf` head parts + then + Image + (Just (Registry (Text.pack (head parts)))) + (Text.pack . intercalate "/" $ tail parts) + else Image Nothing (Text.pack img) + else Image Nothing (Text.pack img) + +newtype Registry + = Registry + { unRegistry :: Text + } + deriving (Show, Eq, Ord, IsString) + +newtype Tag + = Tag + { unTag :: Text + } + deriving (Show, Eq, Ord, IsString) + +newtype Digest + = Digest + { unDigest :: Text + } + deriving (Show, Eq, Ord, IsString) data Protocol - = TCP - | UDP - deriving (Show, Eq, Ord) + = TCP + | UDP + deriving (Show, Eq, Ord) data Port - = Port !Int - !Protocol - | PortStr !Text - | PortRange !Int - !Int - !Protocol - deriving (Show, Eq, Ord) - -newtype Ports = Ports - { unPorts :: [Port] - } deriving (Show, Eq, Ord) + = Port + !Int + !Protocol + | PortStr !Text + | PortRange + !Int + !Int + !Protocol + deriving (Show, Eq, Ord) + +newtype Ports + = Ports + { unPorts :: [Port] + } + deriving (Show, Eq, Ord) instance IsList Ports where - type Item Ports = Port - fromList = Ports - toList (Ports ps) = ps + type Item Ports = Port + fromList = Ports + toList (Ports ps) = ps type Directory = Text type Platform = Text -newtype ImageAlias = ImageAlias - { unImageAlias :: Text - } deriving (Show, Eq, Ord, IsString) - -data BaseImage = BaseImage - { image :: !Image - , tag :: !(Maybe Tag) - , digest :: !(Maybe Digest) - , alias :: !(Maybe ImageAlias) - , platform :: !(Maybe Platform) - } deriving (Eq, Ord, Show) +newtype ImageAlias + = ImageAlias + { unImageAlias :: Text + } + deriving (Show, Eq, Ord, IsString) + +data BaseImage + = BaseImage + { image :: !Image, + tag :: !(Maybe Tag), + digest :: !(Maybe Digest), + alias :: !(Maybe ImageAlias), + platform :: !(Maybe Platform) + } + deriving (Eq, Ord, Show) -- | Type of the Dockerfile AST type Dockerfile = [InstructionPos Text] -newtype SourcePath = SourcePath - { unSourcePath :: Text - } deriving (Show, Eq, Ord, IsString) +newtype SourcePath + = SourcePath + { unSourcePath :: Text + } + deriving (Show, Eq, Ord, IsString) -newtype TargetPath = TargetPath - { unTargetPath :: Text - } deriving (Show, Eq, Ord, IsString) +newtype TargetPath + = TargetPath + { unTargetPath :: Text + } + deriving (Show, Eq, Ord, IsString) data Chown - = Chown !Text - | NoChown - deriving (Show, Eq, Ord) + = Chown !Text + | NoChown + deriving (Show, Eq, Ord) instance IsString Chown where - fromString ch = - case ch of - "" -> NoChown - _ -> Chown (Text.pack ch) + fromString ch = + case ch of + "" -> NoChown + _ -> Chown (Text.pack ch) data CopySource - = CopySource !Text - | NoSource - deriving (Show, Eq, Ord) + = CopySource !Text + | NoSource + deriving (Show, Eq, Ord) instance IsString CopySource where - fromString src = - case src of - "" -> NoSource - _ -> CopySource (Text.pack src) - -newtype Duration = Duration - { durationTime :: DiffTime - } deriving (Show, Eq, Ord, Num) - -newtype Retries = Retries - { times :: Int - } deriving (Show, Eq, Ord, Num) - -data CopyArgs = CopyArgs - { sourcePaths :: NonEmpty SourcePath - , targetPath :: !TargetPath - , chownFlag :: !Chown - , sourceFlag :: !CopySource - } deriving (Show, Eq, Ord) - -data AddArgs = AddArgs - { sourcePaths :: NonEmpty SourcePath - , targetPath :: !TargetPath - , chownFlag :: !Chown - } deriving (Show, Eq, Ord) + fromString src = + case src of + "" -> NoSource + _ -> CopySource (Text.pack src) + +newtype Duration + = Duration + { durationTime :: DiffTime + } + deriving (Show, Eq, Ord, Num) + +newtype Retries + = Retries + { times :: Int + } + deriving (Show, Eq, Ord, Num) + +data CopyArgs + = CopyArgs + { sourcePaths :: NonEmpty SourcePath, + targetPath :: !TargetPath, + chownFlag :: !Chown, + sourceFlag :: !CopySource + } + deriving (Show, Eq, Ord) + +data AddArgs + = AddArgs + { sourcePaths :: NonEmpty SourcePath, + targetPath :: !TargetPath, + chownFlag :: !Chown + } + deriving (Show, Eq, Ord) data Check args - = Check !(CheckArgs args) - | NoCheck - deriving (Show, Eq, Ord, Functor) + = Check !(CheckArgs args) + | NoCheck + deriving (Show, Eq, Ord, Functor) data Arguments args - = ArgumentsText args - | ArgumentsList args - deriving (Show, Eq, Ord, Functor) + = ArgumentsText args + | ArgumentsList args + deriving (Show, Eq, Ord, Functor) instance IsString (Arguments Text) where - fromString = ArgumentsText . Text.pack + fromString = ArgumentsText . Text.pack instance IsList (Arguments Text) where - type Item (Arguments Text) = Text - fromList = ArgumentsList . Text.unwords - toList (ArgumentsText ps) = Text.words ps - toList (ArgumentsList ps) = Text.words ps - -data CheckArgs args = CheckArgs - { checkCommand :: !(Arguments args) - , interval :: !(Maybe Duration) - , timeout :: !(Maybe Duration) - , startPeriod :: !(Maybe Duration) - , retries :: !(Maybe Retries) - } deriving (Show, Eq, Ord, Functor) + type Item (Arguments Text) = Text + fromList = ArgumentsList . Text.unwords + toList (ArgumentsText ps) = Text.words ps + toList (ArgumentsList ps) = Text.words ps + +data CheckArgs args + = CheckArgs + { checkCommand :: !(Arguments args), + interval :: !(Maybe Duration), + timeout :: !(Maybe Duration), + startPeriod :: !(Maybe Duration), + retries :: !(Maybe Retries) + } + deriving (Show, Eq, Ord, Functor) type Pairs = [(Text, Text)] data RunMount - = BindMount !BindOpts - | CacheMount !CacheOpts - | TmpfsMount !TmpOpts - | SecretMount !SecretOpts - | SshMount !SecretOpts - deriving (Eq, Show, Ord) - -data BindOpts = BindOpts - { target :: !TargetPath - , source :: !(Maybe SourcePath) - , fromImage :: !(Maybe Text) - , readWrite :: !(Maybe Bool) - } deriving (Show, Eq, Ord) - -data CacheOpts = CacheOpts - { target :: !TargetPath - , id :: !(Maybe Text) - , sharing :: !CacheSharing - , readOnly :: !(Maybe Bool) - , fromImage :: !(Maybe Text) - , source :: !(Maybe SourcePath) - , mode :: !(Maybe Text) - , uid :: !(Maybe Int) - , gid :: !(Maybe Int) - } deriving (Show, Eq, Ord) - - -newtype TmpOpts = TmpOpts { target :: TargetPath } deriving (Eq, Show, Ord) - -data SecretOpts = SecretOpts - { target :: !TargetPath - , id :: !(Maybe Text) - , isRequired :: !(Maybe Bool) - , mode :: !(Maybe Text) - , uid :: !(Maybe Int) - , gid :: !(Maybe Int) - } deriving (Eq, Show, Ord) + = BindMount !BindOpts + | CacheMount !CacheOpts + | TmpfsMount !TmpOpts + | SecretMount !SecretOpts + | SshMount !SecretOpts + deriving (Eq, Show, Ord) + +data BindOpts + = BindOpts + { target :: !TargetPath, + source :: !(Maybe SourcePath), + fromImage :: !(Maybe Text), + readOnly :: !(Maybe Bool) + } + deriving (Show, Eq, Ord) + +instance Default BindOpts where + def = BindOpts "" Nothing Nothing Nothing + +data CacheOpts + = CacheOpts + { target :: !TargetPath, + sharing :: !CacheSharing, + cacheId :: !(Maybe Text), + readOnly :: !(Maybe Bool), + fromImage :: !(Maybe Text), + source :: !(Maybe SourcePath), + mode :: !(Maybe Text), + uid :: !(Maybe Integer), + gid :: !(Maybe Integer) + } + deriving (Show, Eq, Ord) + +instance Default CacheOpts where + def = CacheOpts "" Shared Nothing Nothing Nothing Nothing Nothing Nothing Nothing + +newtype TmpOpts = TmpOpts {target :: TargetPath} deriving (Eq, Show, Ord) + +instance Default TmpOpts where + def = TmpOpts "" + +data SecretOpts + = SecretOpts + { target :: !(Maybe TargetPath), + cacheId :: !(Maybe Text), + isRequired :: !(Maybe Bool), + source :: !(Maybe SourcePath), + mode :: !(Maybe Text), + uid :: !(Maybe Integer), + gid :: !(Maybe Integer) + } + deriving (Eq, Show, Ord) + +instance Default SecretOpts where + def = SecretOpts Nothing Nothing Nothing Nothing Nothing Nothing Nothing data CacheSharing - = Shared - | Private - | Locked - deriving (Show, Eq, Ord) + = Shared + | Private + | Locked + deriving (Show, Eq, Ord) data RunSecurity - = Insecure - | Sandbox - deriving (Show, Eq, Ord) + = Insecure + | Sandbox + deriving (Show, Eq, Ord) data RunNetwork - = NetworkNone - | NetworkHost - | NetworkDefault - deriving (Show, Eq, Ord) - -data RunArgs args = RunArgs - { mount :: !(Maybe RunMount) - , security :: !(Maybe RunSecurity) - , network :: !(Maybe RunNetwork) - , commands :: !(Arguments args) - } deriving (Show, Eq, Ord, Functor) + = NetworkNone + | NetworkHost + | NetworkDefault + deriving (Show, Eq, Ord) + +data RunFlags + = RunFlags + { mount :: !(Maybe RunMount), + security :: !(Maybe RunSecurity), + network :: !(Maybe RunNetwork) + } + deriving (Show, Eq, Ord) + +data RunArgs args = RunArgs (Arguments args) RunFlags + deriving (Show, Eq, Ord, Functor) instance IsString (RunArgs Text) where - fromString s = RunArgs - { commands = ArgumentsText . Text.pack $ s - , security = Nothing - , network = Nothing - , mount = Nothing - } + fromString s = + RunArgs + (ArgumentsText . Text.pack $ s) + RunFlags + { security = Nothing, + network = Nothing, + mount = Nothing + } -- | All commands available in Dockerfiles data Instruction args - = From !BaseImage - | Add !AddArgs - | User !Text - | Label !Pairs - | Stopsignal !Text - | Copy !CopyArgs - | Run !(RunArgs args) - | Cmd !(Arguments args) - | Shell !(Arguments args) - | Workdir !Directory - | Expose !Ports - | Volume !Text - | Entrypoint !(Arguments args) - | Maintainer !Text - | Env !Pairs - | Arg !Text - !(Maybe Text) - | Healthcheck !(Check args) - | Comment !Text - | OnBuild !(Instruction args) - deriving (Eq, Ord, Show, Functor) + = From !BaseImage + | Add !AddArgs + | User !Text + | Label !Pairs + | Stopsignal !Text + | Copy !CopyArgs + | Run !(RunArgs args) + | Cmd !(Arguments args) + | Shell !(Arguments args) + | Workdir !Directory + | Expose !Ports + | Volume !Text + | Entrypoint !(Arguments args) + | Maintainer !Text + | Env !Pairs + | Arg + !Text + !(Maybe Text) + | Healthcheck !(Check args) + | Comment !Text + | OnBuild !(Instruction args) + deriving (Eq, Ord, Show, Functor) type Filename = Text @@ -263,8 +325,10 @@ type Linenumber = Int -- | 'Instruction' with additional location information required for creating -- good check messages -data InstructionPos args = InstructionPos - { instruction :: !(Instruction args) - , sourcename :: !Filename - , lineNumber :: !Linenumber - } deriving (Eq, Ord, Show, Functor) +data InstructionPos args + = InstructionPos + { instruction :: !(Instruction args), + sourcename :: !Filename, + lineNumber :: !Linenumber + } + deriving (Eq, Ord, Show, Functor) From 94f0dfe1e00e1a07357db9e56f90828a835092c7 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Mon, 1 Jun 2020 23:02:29 +0200 Subject: [PATCH 09/15] Added pretty printing for new run flags --- examples/complex.hs | 2 + language-docker.cabal | 8 +- package.yaml | 1 - src/Language/Docker/EDSL.hs | 5 +- src/Language/Docker/EDSL/Types.hs | 1 + src/Language/Docker/Normalize.hs | 94 -- src/Language/Docker/Parser.hs | 140 +-- src/Language/Docker/Parser/Cmd.hs | 2 +- src/Language/Docker/Parser/Copy.hs | 6 +- src/Language/Docker/Parser/Expose.hs | 2 +- src/Language/Docker/Parser/From.hs | 2 +- src/Language/Docker/Parser/Healthcheck.hs | 2 +- src/Language/Docker/Parser/Instruction.hs | 111 +++ src/Language/Docker/Parser/Pairs.hs | 4 +- src/Language/Docker/Parser/Prelude.hs | 11 +- src/Language/Docker/Parser/Run.hs | 62 +- src/Language/Docker/PrettyPrint.hs | 328 ++++--- src/Language/Docker/Syntax.hs | 45 +- src/Language/Docker/Syntax/Lift.hs | 24 +- test/Language/Docker/ParserSpec.hs | 1048 ++++++++++++--------- 20 files changed, 1038 insertions(+), 860 deletions(-) delete mode 100644 src/Language/Docker/Normalize.hs create mode 100644 src/Language/Docker/Parser/Instruction.hs diff --git a/examples/complex.hs b/examples/complex.hs index fceec50..d62436a 100644 --- a/examples/complex.hs +++ b/examples/complex.hs @@ -23,6 +23,8 @@ main = RUN cd ${DIR} && npm install stylus && npm install eslint-plugin-jsx-a11y + RUN --mount=type=cache,target=/foo,sharing=shared npm install + COPY . $DIR WORKDIR $DIR diff --git a/language-docker.cabal b/language-docker.cabal index fbbdc36..d4921af 100644 --- a/language-docker.cabal +++ b/language-docker.cabal @@ -4,10 +4,10 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 16b417222b143d0446815001e459ca1bf6fd63e67b3862c3443a11aa44a9931a +-- hash: 74f106305ab18c79412aa9b8d150b34cf232e1aebc61a50c65463fc4d9540d07 name: language-docker -version: 8.1.0 +version: 8.1.1 synopsis: Dockerfile parser, pretty-printer and embedded DSL description: All functions for parsing, printing and writting Dockerfiles are exported through @Language.Docker@. For more fine-grained operations look for specific modules that implement a certain functionality. See the for the source-code and examples. @@ -36,7 +36,6 @@ library Language.Docker Language.Docker.Parser Language.Docker.PrettyPrint - Language.Docker.Normalize Language.Docker.Syntax Language.Docker.Syntax.Lift Language.Docker.EDSL @@ -49,6 +48,7 @@ library Language.Docker.Parser.Expose Language.Docker.Parser.From Language.Docker.Parser.Healthcheck + Language.Docker.Parser.Instruction Language.Docker.Parser.Pairs Language.Docker.Parser.Prelude Language.Docker.Parser.Run @@ -69,6 +69,7 @@ library , template-haskell , text , th-lift + , th-lift-instances , time default-language: Haskell2010 @@ -105,5 +106,6 @@ test-suite hspec , template-haskell , text , th-lift + , th-lift-instances , time default-language: Haskell2010 diff --git a/package.yaml b/package.yaml index 62c60d0..64e4b37 100644 --- a/package.yaml +++ b/package.yaml @@ -53,7 +53,6 @@ library: - Language.Docker - Language.Docker.Parser - Language.Docker.PrettyPrint - - Language.Docker.Normalize - Language.Docker.Syntax - Language.Docker.Syntax.Lift - Language.Docker.EDSL diff --git a/src/Language/Docker/EDSL.hs b/src/Language/Docker/EDSL.hs index 6d6a182..7927170 100644 --- a/src/Language/Docker/EDSL.hs +++ b/src/Language/Docker/EDSL.hs @@ -19,6 +19,7 @@ import Data.Text (Text) import qualified Data.Text as Text import qualified Data.Text.Lazy as L import qualified Data.Text.Lazy.Encoding as E +import Data.Default.Class (def) import qualified Language.Docker.PrettyPrint as PrettyPrint import qualified Language.Docker.Syntax as Syntax @@ -61,7 +62,7 @@ runD (User u n) = runDef Syntax.User u n runD (Label ps n) = runDef Syntax.Label ps n runD (StopSignal s n) = runDef Syntax.Stopsignal s n runD (CopyArgs s d c f n) = runDef Syntax.Copy (Syntax.CopyArgs s d c f) n -runD (RunArgs as n) = runDef Syntax.Run as n +runD (RunArgs as fs n) = runDef Syntax.Run (Syntax.RunArgs as fs) n runD (Workdir d n) = runDef Syntax.Workdir d n runD (Expose ps n) = runDef Syntax.Expose ps n runD (Volume v n) = runDef Syntax.Volume v n @@ -185,7 +186,7 @@ aliased (EBaseImage n t d _ p) a = EBaseImage n t d (Just a) p -- run "apt-get install wget" -- @ run :: MonadFree EInstruction m => Syntax.Arguments Text -> m () -run = runArgs +run as = runArgs as def -- | Create an ENTRYPOINT instruction with the given arguments. -- diff --git a/src/Language/Docker/EDSL/Types.hs b/src/Language/Docker/EDSL/Types.hs index c929591..8a203e2 100644 --- a/src/Language/Docker/EDSL/Types.hs +++ b/src/Language/Docker/EDSL/Types.hs @@ -36,6 +36,7 @@ data EInstruction next Syntax.CopySource next | RunArgs (Syntax.Arguments Text) + Syntax.RunFlags next | CmdArgs (Syntax.Arguments Text) next diff --git a/src/Language/Docker/Normalize.hs b/src/Language/Docker/Normalize.hs deleted file mode 100644 index fac76f0..0000000 --- a/src/Language/Docker/Normalize.hs +++ /dev/null @@ -1,94 +0,0 @@ -module Language.Docker.Normalize - ( normalizeEscapedLines - ) where - -import Data.List (mapAccumL) -import Data.Maybe (catMaybes) -import Data.Text (Text) -import qualified Data.Text as Text -import Data.Text.Lazy (toStrict) -import qualified Data.Text.Lazy.Builder as Builder - -data NormalizedLine - = Continue - | Joined !Builder.Builder - !Int - --- Finds all lines ending with \ and joins them with the next line using --- a single space. If the next line is a comment, then the comment line is --- deleted. It finally adds the same amount of new lines for each of the --- lines it joined, in order to preserve the line count in the document. -normalize :: Text -> Text -normalize allLines = - let (lastState, res) -- mapAccumL is the idea of a for loop with a variable holding - -- some state and another variable where we append the final result - -- of the looping operation. For each line in lines, apply the transform - -- function. This function always returns a new state, and another element - -- to append to the final result. The ending result of mapAccumL is the final - -- state variale and the resulting list of values. We initialize the loop with - -- the 'Continue' state, which means "no special action to do next" - = mapAccumL transform Continue (Text.lines allLines) - in case lastState of - Continue -- The last line of the document is a normal line, cleanup and return - -> Text.unlines . catMaybes $ res - Joined l times -- The last line contains a \, so we need to add the buffered - -- line back to the result, pad with newlines and cleanup - -> Text.unlines (catMaybes res <> [toText (l <> padNewlines times)]) - where - toText = toStrict . Builder.toLazyText - -- | Checks the result of the previous operation in the loop (first argument) - -- - -- If the previous result is a 'Joined' operation, then we merge the previous - -- and the current line in a single line and return it. - -- - -- If the current line ends with a \, then we produce a 'Joined' state as result - -- of this looping operation. - -- - -- If the previous 2 conditions are true at the same time, then we produce a new - -- 'Joined' state holding the concatenation of the Joined buffer and the previous line - -- and we return 'Nothing' as an indication that this line does not form part of the - -- final result. - transform :: NormalizedLine -> Text -> (NormalizedLine, Maybe Text) - transform (Joined prev times) rawLine - -- If we are buffering lines and the next one is empty or it starts with a comment - -- we simply ignore the comment and remember to add a newline - | Text.null line || isComment line = (Joined prev (times + 1), Nothing) - -- If we are buffering lines, then we check whether the current line end with \, - -- if it does, then we merged it into the buffered state - | endsWithEscape line = (Joined (prev <> normalizeLast line) (times + 1), Nothing) - -- otherwise we just yield - -- the concatanation of the buffer and the current line as result, after padding with - -- newlines - | otherwise = (Continue, Just (toText (prev <> Builder.fromText line <> padNewlines times))) - where - line = Text.stripEnd rawLine - -- When not buffering lines, then we just check if we need to start doing it by checking - -- whether or not the current line ends with \. If it does not, then we just yield the - -- current line as part of the result - transform Continue rawLine - | isComment line = (Continue, Just line) - | endsWithEscape line = (Joined (normalizeLast line) 1, Nothing) - | otherwise = (Continue, Just line) - where - line = Text.stripEnd rawLine - -- - endsWithEscape t - | Text.null t = False - | otherwise = Text.last t == '\\' - -- - padNewlines times = Builder.fromText (Text.replicate times (Text.singleton '\n')) - -- - normalizeLast = Builder.fromText . Text.dropWhileEnd (== '\\') - -- - isComment line = - case (Text.uncons . Text.stripStart) line of - Just ('#', _) -> True - _ -> False - --- | Remove new line escapes and join escaped lines together on one line --- to simplify parsing later on. Escapes are replaced with line breaks --- to not alter the line numbers. -normalizeEscapedLines :: Text -> Text -normalizeEscapedLines = normalize - -{-# INLINE normalizeEscapedLines #-} diff --git a/src/Language/Docker/Parser.hs b/src/Language/Docker/Parser.hs index d0f8d9a..3af5871 100644 --- a/src/Language/Docker/Parser.hs +++ b/src/Language/Docker/Parser.hs @@ -1,135 +1,37 @@ {-# LANGUAGE OverloadedStrings #-} module Language.Docker.Parser - ( parseText - , parseFile - , parseStdin - , Parser - , Error - , DockerfileError(..) - ) where - + ( parseText, + parseFile, + parseStdin, + Parser, + Error, + DockerfileError (..), + ) +where +import qualified Data.ByteString as B import qualified Data.Text as T -import Language.Docker.Parser.Prelude -import Language.Docker.Syntax import qualified Data.Text.Encoding as E import qualified Data.Text.Encoding.Error as E -import qualified Data.ByteString as B - -import Language.Docker.Parser.Expose (parseExpose) -import Language.Docker.Parser.Copy (parseCopy, parseAdd) -import Language.Docker.Parser.From (parseFrom) -import Language.Docker.Parser.Cmd (parseCmd) -import Language.Docker.Parser.Healthcheck (parseHealthcheck) -import Language.Docker.Parser.Run (parseRun) -import Language.Docker.Parser.Arguments (arguments) -import Language.Docker.Parser.Pairs (parseEnv, parseLabel) ------------------------------------- --- DOCKER INSTRUCTIONS PARSER ------------------------------------- - - -shell :: Parser Instr -shell = do - reserved "SHELL" - Shell <$> arguments - -stopsignal :: Parser Instr -stopsignal = do - reserved "STOPSIGNAL" - args <- untilEol "the stop signal" - return $ Stopsignal args - - -arg :: Parser Instr -arg = do - reserved "ARG" - (try nameWithDefault "the arg name") <|> - Arg <$> untilEol "the argument name" <*> pure Nothing - where - nameWithDefault = do - name <- someUnless "the argument name" (== '=') - void $ char '=' - df <- untilEol "the argument value" - return $ Arg name (Just df) - - -user :: Parser Instr -user = do - reserved "USER" - username <- untilEol "the user" - return $ User username - - -workdir :: Parser Instr -workdir = do - reserved "WORKDIR" - directory <- untilEol "the workdir path" - return $ Workdir directory - -volume :: Parser Instr -volume = do - reserved "VOLUME" - directory <- untilEol "the volume path" - return $ Volume directory - -maintainer :: Parser Instr -maintainer = do - reserved "MAINTAINER" - name <- untilEol "the maintainer name" - return $ Maintainer name - -entrypoint :: Parser Instr -entrypoint = do - reserved "ENTRYPOINT" - Entrypoint <$> arguments - -onbuild :: Parser Instr -onbuild = do - reserved "ONBUILD" - OnBuild <$> parseInstruction - - ------------------------------------- --- Main Parser ------------------------------------- -parseInstruction :: Parser Instr -parseInstruction = - onbuild <|> -- parse all main instructions - parseFrom <|> - parseCopy <|> - parseRun <|> - workdir <|> - entrypoint <|> - volume <|> - parseExpose <|> - parseEnv <|> - arg <|> - user <|> - parseLabel <|> - stopsignal <|> - parseCmd <|> - shell <|> - maintainer <|> - parseAdd <|> - comment <|> - parseHealthcheck +import Language.Docker.Parser.Instruction (parseInstruction) +import Language.Docker.Parser.Prelude +import Language.Docker.Syntax contents :: Parser a -> Parser a contents p = do - void $ takeWhileP Nothing isSpaceNl - r <- p - eof - return r + void $ takeWhileP Nothing isSpaceNl + r <- p + eof + return r dockerfile :: Parser Dockerfile dockerfile = - many $ do - pos <- getSourcePos - i <- parseInstruction - eol <|> eof "a new line followed by the next instruction" - return $ InstructionPos i (T.pack . sourceName $ pos) (unPos . sourceLine $ pos) + many $ do + pos <- getSourcePos + i <- parseInstruction + eol <|> eof "a new line followed by the next instruction" + return $ InstructionPos i (T.pack . sourceName $ pos) (unPos . sourceLine $ pos) parseText :: Text -> Either Error Dockerfile parseText = parse (contents dockerfile) "" diff --git a/src/Language/Docker/Parser/Cmd.hs b/src/Language/Docker/Parser/Cmd.hs index fbf4a96..e9e84cf 100644 --- a/src/Language/Docker/Parser/Cmd.hs +++ b/src/Language/Docker/Parser/Cmd.hs @@ -9,7 +9,7 @@ import Language.Docker.Parser.Arguments import Language.Docker.Parser.Prelude import Language.Docker.Syntax -parseCmd :: Parser Instr +parseCmd :: Parser (Instruction Text) parseCmd = do reserved "CMD" Cmd <$> arguments diff --git a/src/Language/Docker/Parser/Copy.hs b/src/Language/Docker/Parser/Copy.hs index 6096f60..eedf66e 100644 --- a/src/Language/Docker/Parser/Copy.hs +++ b/src/Language/Docker/Parser/Copy.hs @@ -16,7 +16,7 @@ data CopyFlag | FlagSource CopySource | FlagInvalid (Text, Text) -parseCopy :: Parser Instr +parseCopy :: Parser (Instruction Text) parseCopy = do reserved "COPY" flags <- copyFlag `sepEndBy` requiredWhitespace @@ -39,7 +39,7 @@ parseCopy = do f : _ -> f fileList "COPY" (\src dest -> Copy (CopyArgs src dest ch fr)) -parseAdd :: Parser Instr +parseAdd :: Parser (Instruction Text) parseAdd = do reserved "ADD" flag <- lexeme copyFlag <|> return (FlagChown NoChown) @@ -49,7 +49,7 @@ parseAdd = do FlagSource _ -> customError $ InvalidFlagError "--from" FlagInvalid (k, v) -> unexpectedFlag k v -fileList :: Text -> (NonEmpty SourcePath -> TargetPath -> Instr) -> Parser Instr +fileList :: Text -> (NonEmpty SourcePath -> TargetPath -> Instruction Text) -> Parser (Instruction Text) fileList name constr = do paths <- (try stringList "an array of strings [\"src_file\", \"dest_file\"]") diff --git a/src/Language/Docker/Parser/Expose.hs b/src/Language/Docker/Parser/Expose.hs index c71e821..4db976c 100644 --- a/src/Language/Docker/Parser/Expose.hs +++ b/src/Language/Docker/Parser/Expose.hs @@ -9,7 +9,7 @@ import qualified Data.Text as T import Language.Docker.Parser.Prelude import Language.Docker.Syntax -parseExpose :: Parser Instr +parseExpose :: Parser (Instruction Text) parseExpose = do reserved "EXPOSE" Expose <$> ports diff --git a/src/Language/Docker/Parser/From.hs b/src/Language/Docker/Parser/From.hs index 1789298..79745fe 100644 --- a/src/Language/Docker/Parser/From.hs +++ b/src/Language/Docker/Parser/From.hs @@ -68,7 +68,7 @@ imageAlias = do baseImage :: Parser BaseImage baseImage = try taggedImage <|> untaggedImage -parseFrom :: Parser Instr +parseFrom :: Parser (Instruction Text) parseFrom = do reserved "FROM" From <$> baseImage diff --git a/src/Language/Docker/Parser/Healthcheck.hs b/src/Language/Docker/Parser/Healthcheck.hs index 9d98752..76fb27d 100644 --- a/src/Language/Docker/Parser/Healthcheck.hs +++ b/src/Language/Docker/Parser/Healthcheck.hs @@ -20,7 +20,7 @@ data CheckFlag | FlagRetries Retries | CFlagInvalid (Text, Text) -parseHealthcheck :: Parser Instr +parseHealthcheck :: Parser (Instruction Text) parseHealthcheck = do reserved "HEALTHCHECK" Healthcheck <$> (fullCheck <|> noCheck) diff --git a/src/Language/Docker/Parser/Instruction.hs b/src/Language/Docker/Parser/Instruction.hs new file mode 100644 index 0000000..eb1c16a --- /dev/null +++ b/src/Language/Docker/Parser/Instruction.hs @@ -0,0 +1,111 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Language.Docker.Parser.Instruction + ( parseInstruction, + parseShell, + parseStopSignal, + parseArg, + parseUser, + parseWorkdir, + parseVolume, + parseEntryPoint, + parseMaintainer, + parseOnbuild, + parseComment, + ) +where + +import Language.Docker.Parser.Arguments (arguments) +import Language.Docker.Parser.Cmd (parseCmd) +import Language.Docker.Parser.Copy (parseAdd, parseCopy) +import Language.Docker.Parser.Expose (parseExpose) +import Language.Docker.Parser.From (parseFrom) +import Language.Docker.Parser.Healthcheck (parseHealthcheck) +import Language.Docker.Parser.Pairs (parseEnv, parseLabel) +import Language.Docker.Parser.Prelude +import Language.Docker.Parser.Run (parseRun) +import Language.Docker.Syntax + +parseShell :: Parser (Instruction Text) +parseShell = do + reserved "SHELL" + Shell <$> arguments + +parseStopSignal :: Parser (Instruction Text) +parseStopSignal = do + reserved "STOPSIGNAL" + args <- untilEol "the stop signal" + return $ Stopsignal args + +parseArg :: Parser (Instruction Text) +parseArg = do + reserved "ARG" + (try nameWithDefault "the arg name") + <|> Arg <$> untilEol "the argument name" <*> pure Nothing + where + nameWithDefault = do + name <- someUnless "the argument name" (== '=') + void $ char '=' + df <- untilEol "the argument value" + return $ Arg name (Just df) + +parseUser :: Parser (Instruction Text) +parseUser = do + reserved "USER" + username <- untilEol "the user" + return $ User username + +parseWorkdir :: Parser (Instruction Text) +parseWorkdir = do + reserved "WORKDIR" + directory <- untilEol "the workdir path" + return $ Workdir directory + +parseVolume :: Parser (Instruction Text) +parseVolume = do + reserved "VOLUME" + directory <- untilEol "the volume path" + return $ Volume directory + +parseMaintainer :: Parser (Instruction Text) +parseMaintainer = do + reserved "MAINTAINER" + name <- untilEol "the maintainer name" + return $ Maintainer name + +parseEntryPoint :: Parser (Instruction Text) +parseEntryPoint = do + reserved "ENTRYPOINT" + Entrypoint <$> arguments + +parseOnbuild :: Parser (Instruction Text) +parseOnbuild = do + reserved "ONBUILD" + OnBuild <$> parseInstruction + +parseComment :: Parser (Instruction Text) +parseComment = Comment <$> comment + +parseInstruction :: Parser (Instruction Text) +parseInstruction = + choice + [ parseOnbuild, + parseFrom, + parseCopy, + parseRun, + parseWorkdir, + parseEntryPoint, + parseVolume, + parseExpose, + parseEnv, + parseArg, + parseUser, + parseLabel, + parseStopSignal, + parseCmd, + parseShell, + parseMaintainer, + parseAdd, + parseComment, + parseHealthcheck + ] diff --git a/src/Language/Docker/Parser/Pairs.hs b/src/Language/Docker/Parser/Pairs.hs index 4a430f0..72dee3a 100644 --- a/src/Language/Docker/Parser/Pairs.hs +++ b/src/Language/Docker/Parser/Pairs.hs @@ -56,12 +56,12 @@ pair = do pairs :: Parser Pairs pairs = (pair "a key value pair (key=value)") `sepEndBy1` requiredWhitespace -parseLabel :: Parser Instr +parseLabel :: Parser (Instruction Text) parseLabel = do reserved "LABEL" Label <$> pairs -parseEnv :: Parser Instr +parseEnv :: Parser (Instruction Text) parseEnv = do reserved "ENV" Env <$> pairs diff --git a/src/Language/Docker/Parser/Prelude.hs b/src/Language/Docker/Parser/Prelude.hs index f68fad2..6df6611 100644 --- a/src/Language/Docker/Parser/Prelude.hs +++ b/src/Language/Docker/Parser/Prelude.hs @@ -24,7 +24,6 @@ module Language.Docker.Parser.Prelude Parser, Error, DockerfileError (..), - Instr, module Megaparsec, char, string, @@ -41,7 +40,6 @@ import Data.Maybe (fromMaybe) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T -import Language.Docker.Syntax import Text.Megaparsec as Megaparsec hiding (Label) import Text.Megaparsec.Char hiding (eol) import qualified Text.Megaparsec.Char.Lexer as L @@ -64,8 +62,6 @@ type Parser = Parsec DockerfileError Text type Error = ParseErrorBundle Text DockerfileError -type Instr = Instruction Text - instance ShowErrorComponent DockerfileError where showErrorComponent (DuplicateFlagError f) = "duplicate flag: " ++ f showErrorComponent (FileListError f) = @@ -85,7 +81,7 @@ data FoundWhitespace | MissingWhitespace deriving (Eq, Show) --- There is no need to remember how mamny spaces we found in a line, so we can +-- There is no need to remember how many spaces we found in a line, so we can -- cheaply remmeber that we already whitenessed some significant whitespace while -- parsing an expression by concatenating smaller results instance Semigroup FoundWhitespace where @@ -242,8 +238,7 @@ someUnless name predicate = do <* notFollowedBy (char '\n') ] -comment :: Parser Instr +comment :: Parser Text comment = do void $ char '#' - text <- takeWhileP Nothing (not . isNl) - return $ Comment text + takeWhileP Nothing (not . isNl) diff --git a/src/Language/Docker/Parser/Run.hs b/src/Language/Docker/Parser/Run.hs index 0815983..80370bb 100644 --- a/src/Language/Docker/Parser/Run.hs +++ b/src/Language/Docker/Parser/Run.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TupleSections #-} module Language.Docker.Parser.Run ( parseRun, @@ -40,15 +41,14 @@ data MountType | Secret | Ssh -parseRun :: Parser Instr +parseRun :: Parser (Instruction Text) parseRun = do reserved "RUN" Run <$> runArguments runArguments :: Parser (RunArgs Text) runArguments = do - presentFlags <- choice [runFlags, pure (RunFlags Nothing Nothing Nothing)] - requiredWhitespace + presentFlags <- choice [runFlags <* requiredWhitespace, pure (RunFlags Nothing Nothing Nothing)] args <- arguments return $ RunArgs args presentFlags @@ -89,14 +89,14 @@ runFlagMount = do Just Cache <$ string "cache", Just Tmpfs <$ string "tmpfs", Just Secret <$ string "secret", - Just Ssh <$ "ssh" + Just Ssh <$ string "ssh" ], pure Nothing ] (mountType, args) <- return $ case maybeType of Nothing -> (Bind, argsParser Bind) - Just Ssh -> (Ssh, choice [argsParser Ssh, pure []]) + Just Ssh -> (Ssh, choice [string "," *> argsParser Ssh, pure []]) Just t -> (t, string "," *> argsParser t) case mountType of Bind -> BindMount <$> (bindMount =<< args) @@ -117,10 +117,10 @@ bindMount args = allowed = Set.fromList ["target", "source", "from", "ro"] required = Set.singleton "target" bindOpts :: RunMountArg -> BindOpts -> BindOpts - bindOpts (MountArgTarget path) bo = bo {target = path} - bindOpts (MountArgSource path) bo = bo {source = Just path} - bindOpts (MountArgFromImage img) bo = bo {fromImage = Just img} - bindOpts (MountArgReadOnly ro) bo = bo {readOnly = Just ro} + bindOpts (MountArgTarget path) bo = bo {bTarget = path} + bindOpts (MountArgSource path) bo = bo {bSource = Just path} + bindOpts (MountArgFromImage img) bo = bo {bFromImage = Just img} + bindOpts (MountArgReadOnly ro) bo = bo {bReadOnly = Just ro} bindOpts invalid _ = error $ "unhandled " <> show invalid <> " please report this bug" cacheMount :: [RunMountArg] -> Parser CacheOpts @@ -132,15 +132,15 @@ cacheMount args = allowed = Set.fromList ["target", "sharing", "id", "ro", "from", "source", "mode", "uid", "gid"] required = Set.fromList ["target", "sharing"] cacheOpts :: RunMountArg -> CacheOpts -> CacheOpts - cacheOpts (MountArgTarget path) co = co {target = path} - cacheOpts (MountArgSharing sh) co = co {sharing = sh} - cacheOpts (MountArgId i) co = co {cacheId = Just i} - cacheOpts (MountArgReadOnly ro) co = co {readOnly = Just ro} - cacheOpts (MountArgFromImage img) co = co {fromImage = Just img} - cacheOpts (MountArgSource path) co = co {source = Just path} - cacheOpts (MountArgMode m) co = co {mode = Just m} - cacheOpts (MountArgUid u) co = co {uid = Just u} - cacheOpts (MountArgGid g) co = co {gid = Just g} + cacheOpts (MountArgTarget path) co = co {cTarget = path} + cacheOpts (MountArgSharing sh) co = co {cSharing = sh} + cacheOpts (MountArgId i) co = co {cCacheId = Just i} + cacheOpts (MountArgReadOnly ro) co = co {cReadOnly = Just ro} + cacheOpts (MountArgFromImage img) co = co {cFromImage = Just img} + cacheOpts (MountArgSource path) co = co {cSource = Just path} + cacheOpts (MountArgMode m) co = co {cMode = Just m} + cacheOpts (MountArgUid u) co = co {cUid = Just u} + cacheOpts (MountArgGid g) co = co {cGid = Just g} cacheOpts invalid _ = error $ "unhandled " <> show invalid <> " please report this bug" tmpfsMount :: [RunMountArg] -> Parser TmpOpts @@ -151,7 +151,7 @@ tmpfsMount args = where required = Set.singleton "target" tmpOpts :: RunMountArg -> TmpOpts -> TmpOpts - tmpOpts (MountArgTarget path) t = t {target = path} + tmpOpts (MountArgTarget path) t = t {tTarget = path} tmpOpts invalid _ = error $ "unhandled " <> show invalid <> " please report this bug" secretMount :: [RunMountArg] -> Parser SecretOpts @@ -163,12 +163,13 @@ secretMount args = allowed = Set.fromList ["target", "id", "required", "source", "mode", "uid", "gid"] required = Set.empty secretOpts :: RunMountArg -> SecretOpts -> SecretOpts - secretOpts (MountArgTarget path) co = co {target = Just path} - secretOpts (MountArgId i) co = co {cacheId = Just i} - secretOpts (MountArgSource path) co = co {source = Just path} - secretOpts (MountArgMode m) co = co {mode = Just m} - secretOpts (MountArgUid u) co = co {uid = Just u} - secretOpts (MountArgGid g) co = co {gid = Just g} + secretOpts (MountArgTarget path) co = co {sTarget = Just path} + secretOpts (MountArgId i) co = co {sCacheId = Just i} + secretOpts MountArgRequired co = co {sIsRequired = Just True} + secretOpts (MountArgSource path) co = co {sSource = Just path} + secretOpts (MountArgMode m) co = co {sMode = Just m} + secretOpts (MountArgUid u) co = co {sUid = Just u} + secretOpts (MountArgGid g) co = co {sGid = Just g} secretOpts invalid _ = error $ "unhandled " <> show invalid <> " please report this bug" validArgs :: @@ -216,18 +217,11 @@ mountChoices mountType = mountArgGid ] Tmpfs -> [mountArgTarget] - Secret -> - [ mountArgTarget, - mountArgId, - mountArgRequired, - mountArgMode, - mountArgUid, - mountArgGid - ] - Ssh -> + _ -> -- Secret and Ssh [ mountArgTarget, mountArgId, mountArgRequired, + mountArgSource, mountArgMode, mountArgUid, mountArgGid diff --git a/src/Language/Docker/PrettyPrint.hs b/src/Language/Docker/PrettyPrint.hs index 0c85a3a..e90f509 100644 --- a/src/Language/Docker/PrettyPrint.hs +++ b/src/Language/Docker/PrettyPrint.hs @@ -1,35 +1,36 @@ -{-# LANGUAGE NoMonomorphismRestriction #-} -{-# LANGUAGE RebindableSyntax #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE MultiWayIf #-} -{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RebindableSyntax #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE NoMonomorphismRestriction #-} module Language.Docker.PrettyPrint where -import Data.List.NonEmpty as NonEmpty (NonEmpty(..), toList) +import Data.List.NonEmpty as NonEmpty (NonEmpty (..), toList) import Data.String (fromString) import Data.Text (Text) import qualified Data.Text as Text import qualified Data.Text.Lazy as L import qualified Data.Text.Lazy.Builder as B import Data.Text.Prettyprint.Doc -import Data.Text.Prettyprint.Doc.Internal (Doc(Empty)) +import Data.Text.Prettyprint.Doc.Internal (Doc (Empty)) import Data.Text.Prettyprint.Doc.Render.Text (renderLazy) import Language.Docker.Syntax import Prelude hiding ((<>), (>>)) -data EscapeAccum = EscapeAccum - { buffer :: !B.Builder - , count :: !Int - , escaping :: !Bool - } +data EscapeAccum + = EscapeAccum + { buffer :: !B.Builder, + count :: !Int, + escaping :: !Bool + } instance Pretty (Arguments Text) where - pretty = prettyPrintArguments + pretty = prettyPrintArguments -- | Pretty print a 'Dockerfile' to a 'Text' prettyPrint :: Dockerfile -> L.Text @@ -51,30 +52,30 @@ prettyPrintImage (Image Nothing name) = pretty name prettyPrintImage (Image (Just (Registry reg)) name) = pretty reg <> "/" <> pretty name prettyPrintBaseImage :: BaseImage -> Doc ann -prettyPrintBaseImage BaseImage{..} = do - prettyPlatform platform - prettyPrintImage image - prettyTag tag - prettyDigest digest - prettyAlias alias +prettyPrintBaseImage BaseImage {..} = do + prettyPlatform platform + prettyPrintImage image + prettyTag tag + prettyDigest digest + prettyAlias alias where (>>) = (<>) prettyPlatform maybePlatform = - case maybePlatform of - Nothing -> mempty - Just p -> "--platform=" <> pretty p <> " " + case maybePlatform of + Nothing -> mempty + Just p -> "--platform=" <> pretty p <> " " prettyTag maybeTag = - case maybeTag of - Nothing -> mempty - Just (Tag p) -> ":" <> pretty p + case maybeTag of + Nothing -> mempty + Just (Tag p) -> ":" <> pretty p prettyAlias maybeAlias = - case maybeAlias of - Nothing -> mempty - Just (ImageAlias a) -> " AS " <> pretty a + case maybeAlias of + Nothing -> mempty + Just (ImageAlias a) -> " AS " <> pretty a prettyDigest maybeDigest = - case maybeDigest of - Nothing -> mempty - Just (Digest d) -> "@" <> pretty d + case maybeDigest of + Nothing -> mempty + Just (Digest d) -> "@" <> pretty d prettyPrintPairs :: Pairs -> Doc ann prettyPrintPairs ps = align $ sepLine $ fmap prettyPrintPair ps @@ -99,22 +100,22 @@ doubleQoute w = enclose dquote dquote (pretty (escapeQuotes w)) escapeQuotes :: Text -> L.Text escapeQuotes text = - case Text.foldr accumulate (EscapeAccum mempty 0 False) text of - EscapeAccum buffer _ False -> B.toLazyText buffer - EscapeAccum buffer count True -> - case count `mod` 2 of - 0 -> B.toLazyText (B.singleton '\\' <> buffer) - _ -> B.toLazyText buffer + case Text.foldr accumulate (EscapeAccum mempty 0 False) text of + EscapeAccum buffer _ False -> B.toLazyText buffer + EscapeAccum buffer count True -> + case count `mod` 2 of + 0 -> B.toLazyText (B.singleton '\\' <> buffer) + _ -> B.toLazyText buffer where accumulate '"' EscapeAccum {buffer, escaping = False} = - EscapeAccum (B.singleton '"' <> buffer) 0 True + EscapeAccum (B.singleton '"' <> buffer) 0 True accumulate '\\' EscapeAccum {buffer, escaping = True, count} = - EscapeAccum (B.singleton '\\' <> buffer) (count + 1) True + EscapeAccum (B.singleton '\\' <> buffer) (count + 1) True accumulate c EscapeAccum {buffer, escaping = True, count} - | count `mod` 2 == 0 = EscapeAccum (B.singleton c <> B.singleton '\\' <> buffer) 0 False - | otherwise = EscapeAccum (B.singleton c <> buffer) 0 False -- It was already escaped + | count `mod` 2 == 0 = EscapeAccum (B.singleton c <> B.singleton '\\' <> buffer) 0 False + | otherwise = EscapeAccum (B.singleton c <> buffer) 0 False -- It was already escaped accumulate c EscapeAccum {buffer, escaping = False} = - EscapeAccum (B.singleton c <> buffer) 0 False + EscapeAccum (B.singleton c <> buffer) 0 False prettyPrintPort :: Port -> Doc ann prettyPrintPort (PortStr str) = pretty str @@ -125,24 +126,24 @@ prettyPrintPort (Port num UDP) = pretty num <> "/udp" prettyPrintFileList :: NonEmpty SourcePath -> TargetPath -> Doc ann prettyPrintFileList sources (TargetPath dest) = - let ending = - case (Text.isSuffixOf "/" dest, sources) of - (True, _) -> "" -- If the target ends with / then no extra ending is needed - (_, _fst :| _snd:_) -> "/" -- More than one source means that the target should end in / - _ -> "" - in hsep $ [pretty s | SourcePath s <- toList sources] ++ [pretty dest <> ending] + let ending = + case (Text.isSuffixOf "/" dest, sources) of + (True, _) -> "" -- If the target ends with / then no extra ending is needed + (_, _fst :| _snd : _) -> "/" -- More than one source means that the target should end in / + _ -> "" + in hsep $ [pretty s | SourcePath s <- toList sources] ++ [pretty dest <> ending] prettyPrintChown :: Chown -> Doc ann prettyPrintChown chown = - case chown of - Chown c -> "--chown=" <> pretty c - NoChown -> mempty + case chown of + Chown c -> "--chown=" <> pretty c + NoChown -> mempty prettyPrintCopySource :: CopySource -> Doc ann prettyPrintCopySource source = - case source of - CopySource c -> "--from=" <> pretty c - NoSource -> mempty + case source of + CopySource c -> "--from=" <> pretty c + NoSource -> mempty prettyPrintDuration :: Text -> Maybe Duration -> Doc ann prettyPrintDuration flagName = maybe mempty pp @@ -154,78 +155,153 @@ prettyPrintRetries = maybe mempty pp where pp (Retries r) = "--retries=" <> pretty r +prettyPrintRunMount :: Maybe RunMount -> Doc ann +prettyPrintRunMount Nothing = mempty +prettyPrintRunMount (Just mount) = "--mount=" + <> case mount of + BindMount BindOpts {..} -> + "type=bind" + <> printTarget bTarget + <> maybe mempty printSource bSource + <> maybe mempty printFromImage bFromImage + <> maybe mempty printReadOnly bReadOnly + CacheMount CacheOpts {..} -> + "type=cache" + <> printTarget cTarget + <> printSharing cSharing + <> maybe mempty printId cCacheId + <> maybe mempty printFromImage cFromImage + <> maybe mempty printSource cSource + <> maybe mempty printMode cMode + <> maybe mempty printUid cUid + <> maybe mempty printGid cGid + <> maybe mempty printReadOnly cReadOnly + SshMount SecretOpts {..} -> + "type=ssh" + <> maybe mempty printTarget sTarget + <> maybe mempty printId sCacheId + <> maybe mempty printSource sSource + <> maybe mempty printMode sMode + <> maybe mempty printUid sUid + <> maybe mempty printGid sGid + <> maybe mempty printRequired sIsRequired + SecretMount SecretOpts {..} -> + "type=secret" + <> maybe mempty printTarget sTarget + <> maybe mempty printId sCacheId + <> maybe mempty printSource sSource + <> maybe mempty printMode sMode + <> maybe mempty printUid sUid + <> maybe mempty printGid sGid + <> maybe mempty printRequired sIsRequired + TmpfsMount TmpOpts {..} -> "type=tmpfs" <> printTarget tTarget + where + printQuotable str + | Text.any (== '"') str = doubleQoute str + | otherwise = pretty str + printTarget (TargetPath t) = ",target=" <> printQuotable t + printSource (SourcePath s) = ",source=" <> printQuotable s + printFromImage f = ",from=" <> printQuotable f + printSharing sharing = ",sharing=" + <> case sharing of + Shared -> "shared" + Private -> "private" + Locked -> "locked" + printId i = ",id=" <> printQuotable i + printMode m = ",mode=" <> pretty m + printUid uid = ",uid=" <> pretty uid + printGid gid = ",gid=" <> pretty gid + printReadOnly True = ",ro" + printReadOnly False = ",rw" + printRequired True = ",required" + printRequired False = mempty + +prettyPrintRunNetwork :: Maybe RunNetwork -> Doc ann +prettyPrintRunNetwork Nothing = mempty +prettyPrintRunNetwork (Just NetworkHost) = "--network=host" +prettyPrintRunNetwork (Just NetworkNone) = "--network=none" +prettyPrintRunNetwork (Just NetworkDefault) = "--network=default" + +prettyPrintRunSecurity :: Maybe RunSecurity -> Doc ann +prettyPrintRunSecurity Nothing = mempty +prettyPrintRunSecurity (Just Sandbox) = "--security=sandbox" +prettyPrintRunSecurity (Just Insecure) = "--security=insecure" + prettyPrintInstruction :: Pretty (Arguments args) => Instruction args -> Doc ann prettyPrintInstruction i = - case i of - Maintainer m -> do - "MAINTAINER" - pretty m - Arg a Nothing -> do - "ARG" - pretty a - Arg k (Just v) -> do - "ARG" - pretty k <> "=" <> pretty v - Entrypoint e -> do - "ENTRYPOINT" - pretty e - Stopsignal s -> do - "STOPSIGNAL" - pretty s - Workdir w -> do - "WORKDIR" - pretty w - Expose (Ports ps) -> do - "EXPOSE" - hsep (fmap prettyPrintPort ps) - Volume dir -> do - "VOLUME" - pretty dir - Run (RunArgs c _f) -> do - "RUN" - pretty c - Copy CopyArgs {sourcePaths, targetPath, chownFlag, sourceFlag} -> do - "COPY" - prettyPrintChown chownFlag - prettyPrintCopySource sourceFlag - prettyPrintFileList sourcePaths targetPath - Cmd c -> do - "CMD" - pretty c - Label l -> do - "LABEL" - prettyPrintPairs l - Env ps -> do - "ENV" - prettyPrintPairs ps - User u -> do - "USER" - pretty u - Comment s -> do - pretty '#' - pretty s - OnBuild i' -> do - "ONBUILD" - prettyPrintInstruction i' - From b -> do - "FROM" - prettyPrintBaseImage b - Add AddArgs {sourcePaths, targetPath, chownFlag} -> do - "ADD" - prettyPrintChown chownFlag - prettyPrintFileList sourcePaths targetPath - Shell args -> do - "SHELL" - pretty args - Healthcheck NoCheck -> "HEALTHCHECK NONE" - Healthcheck (Check CheckArgs {..}) -> do - "HEALTHCHECK" - prettyPrintDuration "--interval=" interval - prettyPrintDuration "--timeout=" timeout - prettyPrintDuration "--start-period=" startPeriod - prettyPrintRetries retries - "CMD" - pretty checkCommand + case i of + Maintainer m -> do + "MAINTAINER" + pretty m + Arg a Nothing -> do + "ARG" + pretty a + Arg k (Just v) -> do + "ARG" + pretty k <> "=" <> pretty v + Entrypoint e -> do + "ENTRYPOINT" + pretty e + Stopsignal s -> do + "STOPSIGNAL" + pretty s + Workdir w -> do + "WORKDIR" + pretty w + Expose (Ports ps) -> do + "EXPOSE" + hsep (fmap prettyPrintPort ps) + Volume dir -> do + "VOLUME" + pretty dir + Run (RunArgs c RunFlags {mount, network, security}) -> do + "RUN" + prettyPrintRunMount mount + prettyPrintRunNetwork network + prettyPrintRunSecurity security + pretty c + Copy CopyArgs {sourcePaths, targetPath, chownFlag, sourceFlag} -> do + "COPY" + prettyPrintChown chownFlag + prettyPrintCopySource sourceFlag + prettyPrintFileList sourcePaths targetPath + Cmd c -> do + "CMD" + pretty c + Label l -> do + "LABEL" + prettyPrintPairs l + Env ps -> do + "ENV" + prettyPrintPairs ps + User u -> do + "USER" + pretty u + Comment s -> do + pretty '#' + pretty s + OnBuild i' -> do + "ONBUILD" + prettyPrintInstruction i' + From b -> do + "FROM" + prettyPrintBaseImage b + Add AddArgs {sourcePaths, targetPath, chownFlag} -> do + "ADD" + prettyPrintChown chownFlag + prettyPrintFileList sourcePaths targetPath + Shell args -> do + "SHELL" + pretty args + Healthcheck NoCheck -> "HEALTHCHECK NONE" + Healthcheck (Check CheckArgs {..}) -> do + "HEALTHCHECK" + prettyPrintDuration "--interval=" interval + prettyPrintDuration "--timeout=" timeout + prettyPrintDuration "--start-period=" startPeriod + prettyPrintRetries retries + "CMD" + pretty checkCommand where (>>) = spaceCat diff --git a/src/Language/Docker/Syntax.hs b/src/Language/Docker/Syntax.hs index b87de41..141e913 100644 --- a/src/Language/Docker/Syntax.hs +++ b/src/Language/Docker/Syntax.hs @@ -209,10 +209,10 @@ data RunMount data BindOpts = BindOpts - { target :: !TargetPath, - source :: !(Maybe SourcePath), - fromImage :: !(Maybe Text), - readOnly :: !(Maybe Bool) + { bTarget :: !TargetPath, + bSource :: !(Maybe SourcePath), + bFromImage :: !(Maybe Text), + bReadOnly :: !(Maybe Bool) } deriving (Show, Eq, Ord) @@ -221,35 +221,35 @@ instance Default BindOpts where data CacheOpts = CacheOpts - { target :: !TargetPath, - sharing :: !CacheSharing, - cacheId :: !(Maybe Text), - readOnly :: !(Maybe Bool), - fromImage :: !(Maybe Text), - source :: !(Maybe SourcePath), - mode :: !(Maybe Text), - uid :: !(Maybe Integer), - gid :: !(Maybe Integer) + { cTarget :: !TargetPath, + cSharing :: !CacheSharing, + cCacheId :: !(Maybe Text), + cReadOnly :: !(Maybe Bool), + cFromImage :: !(Maybe Text), + cSource :: !(Maybe SourcePath), + cMode :: !(Maybe Text), + cUid :: !(Maybe Integer), + cGid :: !(Maybe Integer) } deriving (Show, Eq, Ord) instance Default CacheOpts where def = CacheOpts "" Shared Nothing Nothing Nothing Nothing Nothing Nothing Nothing -newtype TmpOpts = TmpOpts {target :: TargetPath} deriving (Eq, Show, Ord) +newtype TmpOpts = TmpOpts {tTarget :: TargetPath} deriving (Eq, Show, Ord) instance Default TmpOpts where def = TmpOpts "" data SecretOpts = SecretOpts - { target :: !(Maybe TargetPath), - cacheId :: !(Maybe Text), - isRequired :: !(Maybe Bool), - source :: !(Maybe SourcePath), - mode :: !(Maybe Text), - uid :: !(Maybe Integer), - gid :: !(Maybe Integer) + { sTarget :: !(Maybe TargetPath), + sCacheId :: !(Maybe Text), + sIsRequired :: !(Maybe Bool), + sSource :: !(Maybe SourcePath), + sMode :: !(Maybe Text), + sUid :: !(Maybe Integer), + sGid :: !(Maybe Integer) } deriving (Eq, Show, Ord) @@ -281,6 +281,9 @@ data RunFlags } deriving (Show, Eq, Ord) +instance Default RunFlags where + def = RunFlags Nothing Nothing Nothing + data RunArgs args = RunArgs (Arguments args) RunFlags deriving (Show, Eq, Ord, Functor) diff --git a/src/Language/Docker/Syntax/Lift.hs b/src/Language/Docker/Syntax/Lift.hs index c953d02..26ebdca 100644 --- a/src/Language/Docker/Syntax/Lift.hs +++ b/src/Language/Docker/Syntax/Lift.hs @@ -6,10 +6,10 @@ module Language.Docker.Syntax.Lift where import Data.Fixed (Fixed) import Data.Time.Clock (DiffTime) import Instances.TH.Lift () -- Defines Lift instances for ByteString and Text -import Language.Haskell.TH.Lift -import Language.Haskell.TH.Syntax () import Language.Docker.Syntax +import Language.Haskell.TH.Lift +import Language.Haskell.TH.Syntax () deriveLift ''Fixed @@ -58,3 +58,23 @@ deriveLift ''Retries deriveLift ''CheckArgs deriveLift ''Check + +deriveLift ''BindOpts + +deriveLift ''CacheSharing + +deriveLift ''CacheOpts + +deriveLift ''TmpOpts + +deriveLift ''SecretOpts + +deriveLift ''RunMount + +deriveLift ''RunSecurity + +deriveLift ''RunNetwork + +deriveLift ''RunFlags + +deriveLift ''RunArgs diff --git a/test/Language/Docker/ParserSpec.hs b/test/Language/Docker/ParserSpec.hs index 7fd7413..0d31cdb 100644 --- a/test/Language/Docker/ParserSpec.hs +++ b/test/Language/Docker/ParserSpec.hs @@ -1,16 +1,15 @@ -{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} + module Language.Docker.ParserSpec where -import Language.Docker.Normalize +import Data.Default.Class (def) +import qualified Data.Text as Text import Language.Docker.Parser import Language.Docker.Syntax - - import Test.HUnit hiding (Label) import Test.Hspec import Text.Megaparsec hiding (Label) -import qualified Data.Text as Text untaggedImage :: Image -> BaseImage untaggedImage n = BaseImage n Nothing Nothing Nothing Nothing @@ -19,451 +18,618 @@ taggedImage :: Image -> Tag -> BaseImage taggedImage n t = BaseImage n (Just t) Nothing Nothing Nothing withDigest :: BaseImage -> Digest -> BaseImage -withDigest i d = i { digest = Just d } +withDigest i d = i {digest = Just d} withAlias :: BaseImage -> ImageAlias -> BaseImage -withAlias i a = i { alias = Just a } +withAlias i a = i {alias = Just a} withPlatform :: BaseImage -> Platform -> BaseImage -withPlatform i p = i { platform = Just p } - +withPlatform i p = i {platform = Just p} spec :: Spec spec = do - describe "parse ARG" $ do - it "no default" $ - assertAst "ARG FOO" [Arg "FOO" Nothing] - it "with default" $ - assertAst "ARG FOO=bar" [Arg "FOO" (Just "bar")] - - describe "parse FROM" $ do - it "parse untagged image" $ - assertAst "FROM busybox" [From (untaggedImage "busybox")] - it "parse tagged image" $ - assertAst - "FROM busybox:5.12-dev" - [From (taggedImage "busybox" "5.12-dev")] - it "parse digested image" $ - assertAst - "FROM ubuntu@sha256:0ef2e08ed3fab" - [From (untaggedImage "ubuntu" `withDigest` "sha256:0ef2e08ed3fab")] - it "parse digested image with tag" $ - assertAst - "FROM ubuntu:14.04@sha256:0ef2e08ed3fab" - [From (taggedImage "ubuntu" "14.04" `withDigest` "sha256:0ef2e08ed3fab")] - - it "parse image with spaces at the end" $ - assertAst - "FROM dockerfile/mariadb " - [From (untaggedImage "dockerfile/mariadb")] - - describe "parse aliased FROM" $ do - it "parse untagged image" $ - assertAst "FROM busybox as foo" [From (untaggedImage "busybox" `withAlias` "foo")] - it "parse tagged image" $ - assertAst "FROM busybox:5.12-dev AS foo-bar" - [ From (taggedImage "busybox" "5.12-dev" `withAlias` "foo-bar") - ] - it "parse diggested image" $ - assertAst "FROM ubuntu@sha256:0ef2e08ed3fab AS foo" - [ From (untaggedImage "ubuntu" `withDigest` "sha256:0ef2e08ed3fab" `withAlias` "foo") - ] - - describe "parse FROM with platform" $ do - it "parse untagged image with platform" $ - assertAst "FROM --platform=linux busybox" [From (untaggedImage "busybox" `withPlatform` "linux")] - - it "parse tagged image with platform" $ - assertAst "FROM --platform=linux busybox:foo" [From (taggedImage "busybox" "foo" `withPlatform` "linux")] - - describe "parse FROM with registry" $ do - it "registry without port" $ - assertAst "FROM foo.com/node" [From (untaggedImage (Image (Just "foo.com") "node"))] - it "parse with port and tag" $ - assertAst - "FROM myregistry.com:5000/imagename:5.12-dev" - [From (taggedImage (Image (Just "myregistry.com:5000") "imagename") "5.12-dev")] - it "Not a registry if no TLD" $ - assertAst - "FROM myfolder/imagename:5.12-dev" - [From (taggedImage (Image Nothing "myfolder/imagename") "5.12-dev")] - - describe "parse LABEL" $ do - it "parse label" $ assertAst "LABEL foo=bar" [Label[("foo", "bar")]] - it "parse space separated label" $ assertAst "LABEL foo bar baz" [Label[("foo", "bar baz")]] - it "parse quoted labels" $ assertAst "LABEL \"foo bar\"=baz" [Label[("foo bar", "baz")]] - it "parses multiline labels" $ - let dockerfile = Text.unlines [ "LABEL foo=bar \\", "hobo=mobo"] - ast = [ Label [("foo", "bar"), ("hobo", "mobo")] ] - in assertAst dockerfile ast - - describe "parse ENV" $ do - it "parses unquoted pair" $ assertAst "ENV foo=bar" [Env [("foo", "bar")]] - it "parse with space between key and value" $ - assertAst "ENV foo bar" [Env [("foo", "bar")]] - it "parse with more then one (white)space between key and value" $ - let dockerfile = "ENV NODE_VERSION \t v5.7.1" - in assertAst dockerfile [Env[("NODE_VERSION", "v5.7.1")]] - it "parse quoted value pair" $ assertAst "ENV foo=\"bar\"" [Env [("foo", "bar")]] - it "parse multiple unquoted pairs" $ - assertAst "ENV foo=bar baz=foo" [Env [("foo", "bar"), ("baz", "foo")]] - it "parse multiple quoted pairs" $ - assertAst "ENV foo=\"bar\" baz=\"foo\"" [Env [("foo", "bar"), ("baz", "foo")]] - it "env works before cmd" $ - let dockerfile = "ENV PATH=\"/root\"\nCMD [\"hadolint\",\"-i\"]" - ast = [Env [("PATH", "/root")], Cmd ["hadolint", "-i"]] - in assertAst dockerfile ast - it "parse with two spaces between" $ - let dockerfile = "ENV NODE_VERSION=v5.7.1 DEBIAN_FRONTEND=noninteractive" - in assertAst dockerfile [Env[("NODE_VERSION", "v5.7.1"), ("DEBIAN_FRONTEND", "noninteractive")]] - it "have envs on multiple lines" $ - let dockerfile = Text.unlines [ "FROM busybox" - , "ENV NODE_VERSION=v5.7.1 \\" - , "DEBIAN_FRONTEND=noninteractive" - ] - ast = [ From (untaggedImage "busybox") - , Env[("NODE_VERSION", "v5.7.1"), ("DEBIAN_FRONTEND", "noninteractive")] - ] - in assertAst dockerfile ast - it "parses long env over multiple lines" $ - let dockerfile = Text.unlines [ "ENV LD_LIBRARY_PATH=\"/usr/lib/\" \\" - , "APACHE_RUN_USER=\"www-data\" APACHE_RUN_GROUP=\"www-data\""] - ast = [Env [("LD_LIBRARY_PATH", "/usr/lib/") - ,("APACHE_RUN_USER", "www-data") - ,("APACHE_RUN_GROUP", "www-data") - ] - ] - in assertAst dockerfile ast - it "parse single var list" $ - assertAst "ENV foo val1 val2 val3 val4" [Env [("foo", "val1 val2 val3 val4")]] - it "parses many env lines with an equal sign in the value" $ - let dockerfile = Text.unlines [ "ENV TOMCAT_VERSION 9.0.2" - , "ENV TOMCAT_URL foo.com?q=1" - ] - ast = [ Env [("TOMCAT_VERSION", "9.0.2")] - , Env [("TOMCAT_URL", "foo.com?q=1")] - ] - in assertAst dockerfile ast - it "parses many env lines in mixed style" $ - let dockerfile = Text.unlines [ "ENV myName=\"John Doe\" myDog=Rex\\ The\\ Dog \\" - , " myCat=fluffy" - ] - ast = [ Env [("myName", "John Doe") - ,("myDog", "Rex The Dog") - ,("myCat", "fluffy") - ] - ] - in assertAst dockerfile ast - it "parses many env with backslashes" $ - let dockerfile = Text.unlines [ "ENV JAVA_HOME=C:\\\\jdk1.8.0_112" - ] - ast = [ Env [("JAVA_HOME", "C:\\\\jdk1.8.0_112")] - ] - in assertAst dockerfile ast - it "parses env with % in them" $ - let dockerfile = Text.unlines [ "ENV PHP_FPM_ACCESS_FORMAT=\"prefix \\\"quoted\\\" suffix\"" - ] - ast = [ Env [("PHP_FPM_ACCESS_FORMAT", "%R - %u %t \"%m %r\" %s")] - ] - in assertAst dockerfile ast - - it "parses env with % in them" $ - let dockerfile = Text.unlines [ "ENV PHP_FPM_ACCESS_FORMAT=\"%R - %u %t \\\"%m %r\\\" %s\"" - ] - ast = [ Env [("PHP_FPM_ACCESS_FORMAT", "%R - %u %t \"%m %r\" %s")] - ] - in assertAst dockerfile ast - - describe "parse RUN" $ do - it "escaped with space before" $ - let dockerfile = Text.unlines ["RUN yum install -y \\", "imagemagick \\", "mysql"] - in assertAst dockerfile [Run "yum install -y imagemagick mysql"] - - it "does not choke on unmatched brackets" $ - let dockerfile = Text.unlines ["RUN [foo"] - in assertAst dockerfile [Run "[foo"] - - it "Distinguishes between text and a list" $ - let dockerfile = Text.unlines [ "RUN echo foo" - , "RUN [\"echo\", \"foo\"]" - ] - in assertAst dockerfile [Run $ ArgumentsText "echo foo", Run $ ArgumentsList "echo foo"] - - it "Accepts spaces inside the brackets" $ - let dockerfile = Text.unlines [ "RUN [ \"echo\", \"foo\" ]" - ] - in assertAst dockerfile [Run $ ArgumentsList "echo foo"] - - describe "parse CMD" $ do - it "one line cmd" $ assertAst "CMD true" [Cmd "true"] - - it "cmd over several lines" $ - assertAst "CMD true \\\n && true" [Cmd "true && true"] - - it "quoted command params" $ assertAst "CMD [\"echo\", \"1\"]" [Cmd ["echo", "1"]] - - it "Parses commas correctly" $ assertAst "CMD [ \"echo\" ,\"-e\" , \"1\"]" [Cmd ["echo", "-e", "1"]] - - describe "parse SHELL" $ - it "quoted shell params" $ - assertAst "SHELL [\"/bin/bash\", \"-c\"]" [Shell ["/bin/bash", "-c"]] - - describe "parse HEALTHCHECK" $ do - it "parse healthcheck with interval" $ - assertAst - "HEALTHCHECK --interval=5m \\\nCMD curl -f http://localhost/" - [Healthcheck $ - Check $ - CheckArgs "curl -f http://localhost/" (Just 300) Nothing Nothing Nothing + describe "parse ARG" $ do + it "no default" $ + assertAst "ARG FOO" [Arg "FOO" Nothing] + it "with default" $ + assertAst "ARG FOO=bar" [Arg "FOO" (Just "bar")] + describe "parse FROM" $ do + it "parse untagged image" $ + assertAst "FROM busybox" [From (untaggedImage "busybox")] + it "parse tagged image" $ + assertAst + "FROM busybox:5.12-dev" + [From (taggedImage "busybox" "5.12-dev")] + it "parse digested image" $ + assertAst + "FROM ubuntu@sha256:0ef2e08ed3fab" + [From (untaggedImage "ubuntu" `withDigest` "sha256:0ef2e08ed3fab")] + it "parse digested image with tag" $ + assertAst + "FROM ubuntu:14.04@sha256:0ef2e08ed3fab" + [From (taggedImage "ubuntu" "14.04" `withDigest` "sha256:0ef2e08ed3fab")] + it "parse image with spaces at the end" $ + assertAst + "FROM dockerfile/mariadb " + [From (untaggedImage "dockerfile/mariadb")] + describe "parse aliased FROM" $ do + it "parse untagged image" $ + assertAst "FROM busybox as foo" [From (untaggedImage "busybox" `withAlias` "foo")] + it "parse tagged image" $ + assertAst + "FROM busybox:5.12-dev AS foo-bar" + [ From (taggedImage "busybox" "5.12-dev" `withAlias` "foo-bar") + ] + it "parse diggested image" $ + assertAst + "FROM ubuntu@sha256:0ef2e08ed3fab AS foo" + [ From (untaggedImage "ubuntu" `withDigest` "sha256:0ef2e08ed3fab" `withAlias` "foo") + ] + describe "parse FROM with platform" $ do + it "parse untagged image with platform" $ + assertAst "FROM --platform=linux busybox" [From (untaggedImage "busybox" `withPlatform` "linux")] + it "parse tagged image with platform" $ + assertAst "FROM --platform=linux busybox:foo" [From (taggedImage "busybox" "foo" `withPlatform` "linux")] + describe "parse FROM with registry" $ do + it "registry without port" $ + assertAst "FROM foo.com/node" [From (untaggedImage (Image (Just "foo.com") "node"))] + it "parse with port and tag" $ + assertAst + "FROM myregistry.com:5000/imagename:5.12-dev" + [From (taggedImage (Image (Just "myregistry.com:5000") "imagename") "5.12-dev")] + it "Not a registry if no TLD" $ + assertAst + "FROM myfolder/imagename:5.12-dev" + [From (taggedImage (Image Nothing "myfolder/imagename") "5.12-dev")] + describe "parse LABEL" $ do + it "parse label" $ assertAst "LABEL foo=bar" [Label [("foo", "bar")]] + it "parse space separated label" $ assertAst "LABEL foo bar baz" [Label [("foo", "bar baz")]] + it "parse quoted labels" $ assertAst "LABEL \"foo bar\"=baz" [Label [("foo bar", "baz")]] + it "parses multiline labels" $ + let dockerfile = Text.unlines ["LABEL foo=bar \\", "hobo=mobo"] + ast = [Label [("foo", "bar"), ("hobo", "mobo")]] + in assertAst dockerfile ast + describe "parse ENV" $ do + it "parses unquoted pair" $ assertAst "ENV foo=bar" [Env [("foo", "bar")]] + it "parse with space between key and value" $ + assertAst "ENV foo bar" [Env [("foo", "bar")]] + it "parse with more then one (white)space between key and value" $ + let dockerfile = "ENV NODE_VERSION \t v5.7.1" + in assertAst dockerfile [Env [("NODE_VERSION", "v5.7.1")]] + it "parse quoted value pair" $ assertAst "ENV foo=\"bar\"" [Env [("foo", "bar")]] + it "parse multiple unquoted pairs" $ + assertAst "ENV foo=bar baz=foo" [Env [("foo", "bar"), ("baz", "foo")]] + it "parse multiple quoted pairs" $ + assertAst "ENV foo=\"bar\" baz=\"foo\"" [Env [("foo", "bar"), ("baz", "foo")]] + it "env works before cmd" $ + let dockerfile = "ENV PATH=\"/root\"\nCMD [\"hadolint\",\"-i\"]" + ast = [Env [("PATH", "/root")], Cmd ["hadolint", "-i"]] + in assertAst dockerfile ast + it "parse with two spaces between" $ + let dockerfile = "ENV NODE_VERSION=v5.7.1 DEBIAN_FRONTEND=noninteractive" + in assertAst dockerfile [Env [("NODE_VERSION", "v5.7.1"), ("DEBIAN_FRONTEND", "noninteractive")]] + it "have envs on multiple lines" $ + let dockerfile = + Text.unlines + [ "FROM busybox", + "ENV NODE_VERSION=v5.7.1 \\", + "DEBIAN_FRONTEND=noninteractive" + ] + ast = + [ From (untaggedImage "busybox"), + Env [("NODE_VERSION", "v5.7.1"), ("DEBIAN_FRONTEND", "noninteractive")] + ] + in assertAst dockerfile ast + it "parses long env over multiple lines" $ + let dockerfile = + Text.unlines + [ "ENV LD_LIBRARY_PATH=\"/usr/lib/\" \\", + "APACHE_RUN_USER=\"www-data\" APACHE_RUN_GROUP=\"www-data\"" + ] + ast = + [ Env + [ ("LD_LIBRARY_PATH", "/usr/lib/"), + ("APACHE_RUN_USER", "www-data"), + ("APACHE_RUN_GROUP", "www-data") ] - - it "parse healthcheck with retries" $ - assertAst - "HEALTHCHECK --retries=10 CMD curl -f http://localhost/" - [Healthcheck $ - Check $ - CheckArgs "curl -f http://localhost/" Nothing Nothing Nothing (Just $ Retries 10) - ] - - it "parse healthcheck with timeout" $ - assertAst - "HEALTHCHECK --timeout=10s CMD curl -f http://localhost/" - [Healthcheck $ - Check $ - CheckArgs "curl -f http://localhost/" Nothing (Just 10) Nothing Nothing - ] - - it "parse healthcheck with start-period" $ - assertAst - "HEALTHCHECK --start-period=2m CMD curl -f http://localhost/" - [Healthcheck $ - Check $ - CheckArgs "curl -f http://localhost/" Nothing Nothing (Just 120) Nothing - ] - - it "parse healthcheck with all flags" $ - assertAst - "HEALTHCHECK --start-period=2s --timeout=1m --retries=3 --interval=5s CMD curl -f http://localhost/" - [Healthcheck $ - Check $ - CheckArgs - "curl -f http://localhost/" - (Just 5) - (Just 60) - (Just 2) - (Just $ Retries 3) + ] + in assertAst dockerfile ast + it "parse single var list" $ + assertAst "ENV foo val1 val2 val3 val4" [Env [("foo", "val1 val2 val3 val4")]] + it "parses many env lines with an equal sign in the value" $ + let dockerfile = + Text.unlines + [ "ENV TOMCAT_VERSION 9.0.2", + "ENV TOMCAT_URL foo.com?q=1" + ] + ast = + [ Env [("TOMCAT_VERSION", "9.0.2")], + Env [("TOMCAT_URL", "foo.com?q=1")] + ] + in assertAst dockerfile ast + it "parses many env lines in mixed style" $ + let dockerfile = + Text.unlines + [ "ENV myName=\"John Doe\" myDog=Rex\\ The\\ Dog \\", + " myCat=fluffy" + ] + ast = + [ Env + [ ("myName", "John Doe"), + ("myDog", "Rex The Dog"), + ("myCat", "fluffy") ] - - it "parse healthcheck with no flags" $ - assertAst - "HEALTHCHECK CMD curl -f http://localhost/" - [Healthcheck $ - Check $ - CheckArgs "curl -f http://localhost/" Nothing Nothing Nothing Nothing - ] - - describe "parse MAINTAINER" $ do - it "maintainer of untagged scratch image" $ - assertAst - "FROM scratch\nMAINTAINER hudu@mail.com" - [From (untaggedImage "scratch"), Maintainer "hudu@mail.com"] - it "maintainer with mail" $ - assertAst "MAINTAINER hudu@mail.com" [Maintainer "hudu@mail.com"] - it "maintainer only mail after from" $ - let maintainerFromProg = "FROM busybox\nMAINTAINER hudu@mail.com" - maintainerFromAst = [From (untaggedImage "busybox"), Maintainer "hudu@mail.com"] - in assertAst maintainerFromProg maintainerFromAst - describe "parse # comment " $ do - it "multiple comments before run" $ - let dockerfile = Text.unlines ["# line 1", "# line 2", "RUN apt-get update"] - in assertAst dockerfile [Comment " line 1", Comment " line 2", Run "apt-get update"] - it "multiple comments after run" $ - let dockerfile = Text.unlines ["RUN apt-get update", "# line 1", "# line 2"] - in assertAst - dockerfile - [Run "apt-get update", Comment " line 1", Comment " line 2"] - - it "empty comment" $ - let dockerfile = Text.unlines ["#", "# Hello"] - in assertAst dockerfile [Comment "", Comment " Hello"] - describe "normalize lines" $ do - it "join multiple ENV" $ - let dockerfile = Text.unlines [ "FROM busybox" - , "ENV NODE_VERSION=v5.7.1 \\" - , "DEBIAN_FRONTEND=noninteractive" - ] - normalizedDockerfile = Text.unlines [ "FROM busybox" - , "ENV NODE_VERSION=v5.7.1 DEBIAN_FRONTEND=noninteractive\n" - ] - in normalizeEscapedLines dockerfile `shouldBe` normalizedDockerfile - - it "join escaped lines" $ - let dockerfile = Text.unlines ["ENV foo=bar \\", "baz=foz"] - normalizedDockerfile = Text.unlines ["ENV foo=bar baz=foz", ""] - in normalizeEscapedLines dockerfile `shouldBe` normalizedDockerfile - - it "many escaped lines" $ - let dockerfile = Text.unlines [ "ENV A=\"a.sh\" \\" - , " # comment a" - , " B=\"b.sh\" \\" - , " c=\"true\"" - , "" - ] - in assertAst dockerfile [ Env [("A", "a.sh"), ("B", "b.sh"), ("c", "true")] - ] - - it "accepts backslash inside string" $ - let dockerfile = "RUN grep 'foo \\.'" - in assertAst dockerfile [Run (ArgumentsText "grep 'foo \\.'")] - - it "join long CMD" $ - let longEscapedCmd = - Text.unlines - [ "RUN wget https://download.com/${version}.tar.gz -O /tmp/logstash.tar.gz && \\" - , "(cd /tmp && tar zxf logstash.tar.gz && mv logstash-${version} /opt/logstash && \\" - , "rm logstash.tar.gz) && \\" - , "(cd /opt/logstash && \\" - , "/opt/logstash/bin/plugin install contrib)" - ] - longEscapedCmdExpected = - Text.concat - [ "RUN wget https://download.com/${version}.tar.gz -O /tmp/logstash.tar.gz && " - , "(cd /tmp && tar zxf logstash.tar.gz && mv logstash-${version} /opt/logstash && " - , "rm logstash.tar.gz) && " - , "(cd /opt/logstash && " - , "/opt/logstash/bin/plugin install contrib)\n" - , "\n" - , "\n" - , "\n" - , "\n" - ] - in normalizeEscapedLines longEscapedCmd `shouldBe` longEscapedCmdExpected - - it "tolerates spaces after a newline escape" $ - let dockerfile = Text.unlines [ "FROM busy\\ " - , "box" - , "RUN echo\\ " - , " hello" - ] - in assertAst dockerfile [ From (untaggedImage "busybox") - , Run "echo hello" - ] - - it "Correctly joins blank lines starting with comments" $ - let dockerfile = Text.unlines [ "FROM busybox" - , "# I forgot to remove the backslash \\" - , "# This is a comment" - , "RUN echo hello" - ] - in assertAst dockerfile [ From (untaggedImage "busybox") - , Comment " I forgot to remove the backslash \\" - , Comment " This is a comment" - , Run "echo hello" - ] - describe "expose" $ do - it "should handle number ports" $ - let content = "EXPOSE 8080" - in assertAst content [Expose (Ports [Port 8080 TCP])] - it "should handle many number ports" $ - let content = "EXPOSE 8080 8081" - in assertAst content [Expose (Ports [Port 8080 TCP, Port 8081 TCP])] - it "should handle ports with protocol" $ - let content = "EXPOSE 8080/TCP 8081/UDP" - in assertAst content [Expose (Ports [Port 8080 TCP, Port 8081 UDP])] - it "should handle ports with protocol and variables" $ - let content = "EXPOSE $PORT 8080 8081/UDP" - in assertAst content [Expose (Ports [PortStr "$PORT", Port 8080 TCP, Port 8081 UDP])] - it "should handle port ranges" $ - let content = "EXPOSE 80 81 8080-8085" - in assertAst content [Expose (Ports [Port 80 TCP, Port 81 TCP, PortRange 8080 8085 TCP])] - it "should handle udp port ranges" $ - let content = "EXPOSE 80 81 8080-8085/udp" - in assertAst content [Expose (Ports [Port 80 TCP, Port 81 TCP, PortRange 8080 8085 UDP])] - it "should handle multiline variables" $ - let content = "EXPOSE ${PORT} ${PORT_SSL} \\\n\ - \ ${PORT_HTTP} ${PORT_HTTPS} \\\n\ - \ ${PORT_REP} \\\n\ - \ ${PORT_ADMIN} ${PORT_ADMIN_HTTP}" - in assertAst content [ Expose (Ports [ PortStr "${PORT}" - , PortStr "${PORT_SSL}" - , PortStr "${PORT_HTTP}" - , PortStr "${PORT_HTTPS}" - , PortStr "${PORT_REP}" - , PortStr "${PORT_ADMIN}" - , PortStr "${PORT_ADMIN_HTTP}"]) - ] - - describe "syntax" $ do - it "should handle lowercase instructions (#7 - https://github.com/beijaflor-io/haskell-language-dockerfile/issues/7)" $ - let content = "from ubuntu" - in assertAst content [From (untaggedImage "ubuntu")] - - describe "ADD" $ do - it "simple ADD" $ - let file = Text.unlines ["ADD . /app", "ADD http://foo.bar/baz ."] - in assertAst file [ Add $ AddArgs [SourcePath "."] (TargetPath "/app") NoChown - , Add $ AddArgs [SourcePath "http://foo.bar/baz"] (TargetPath ".") NoChown - ] - it "multifiles ADD" $ - let file = Text.unlines ["ADD foo bar baz /app"] - in assertAst file [ Add $ AddArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown - ] - - it "list of quoted files" $ - let file = Text.unlines ["ADD [\"foo\", \"bar\", \"baz\", \"/app\"]"] - in assertAst file [ Add $ AddArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown - ] - - it "with chown flag" $ - let file = Text.unlines ["ADD --chown=root:root foo bar"] - in assertAst file [ Add $ AddArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "root:root") - ] - - it "list of quoted files and chown" $ - let file = Text.unlines ["ADD --chown=user:group [\"foo\", \"bar\", \"baz\", \"/app\"]"] - in assertAst file [ Add $ AddArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") (Chown "user:group") - ] - describe "COPY" $ do - it "simple COPY" $ - let file = Text.unlines ["COPY . /app", "COPY baz /some/long/path"] - in assertAst file [ Copy $ CopyArgs [SourcePath "."] (TargetPath "/app") NoChown NoSource - , Copy $ CopyArgs [SourcePath "baz"] (TargetPath "/some/long/path") NoChown NoSource - ] - it "multifiles COPY" $ - let file = Text.unlines ["COPY foo bar baz /app"] - in assertAst file [ Copy $ CopyArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown NoSource - ] - - it "list of quoted files" $ - let file = Text.unlines ["COPY [\"foo\", \"bar\", \"baz\", \"/app\"]"] - in assertAst file [ Copy $ CopyArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown NoSource - ] - - it "with chown flag" $ - let file = Text.unlines ["COPY --chown=user:group foo bar"] - in assertAst file [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") NoSource - ] - - it "with from flag" $ - let file = Text.unlines ["COPY --from=node foo bar"] - in assertAst file [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") NoChown (CopySource "node") - ] - it "with both flags" $ - let file = Text.unlines ["COPY --from=node --chown=user:group foo bar"] - in assertAst file [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") (CopySource "node") - ] - it "with both flags in different order" $ - let file = Text.unlines ["COPY --chown=user:group --from=node foo bar"] - in assertAst file [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") (CopySource "node") - ] - - it "supports windows paths" $ - let file = Text.unlines ["COPY C:\\\\go C:\\\\go"] - in assertAst file [ Copy $ CopyArgs (fmap SourcePath ["C:\\\\go"]) (TargetPath "C:\\\\go") NoChown NoSource - ] + ] + in assertAst dockerfile ast + it "parses many env with backslashes" $ + let dockerfile = + Text.unlines + [ "ENV JAVA_HOME=C:\\\\jdk1.8.0_112" + ] + ast = + [ Env [("JAVA_HOME", "C:\\\\jdk1.8.0_112")] + ] + in assertAst dockerfile ast + it "parses env with % in them" $ + let dockerfile = + Text.unlines + [ "ENV PHP_FPM_ACCESS_FORMAT=\"prefix \\\"quoted\\\" suffix\"" + ] + ast = + [ Env [("PHP_FPM_ACCESS_FORMAT", "prefix \"quoted\" suffix")] + ] + in assertAst dockerfile ast + it "parses env with % in them" $ + let dockerfile = + Text.unlines + [ "ENV PHP_FPM_ACCESS_FORMAT=\"%R - %u %t \\\"%m %r\\\" %s\"" + ] + ast = + [ Env [("PHP_FPM_ACCESS_FORMAT", "%R - %u %t \"%m %r\" %s")] + ] + in assertAst dockerfile ast + describe "parse RUN" $ do + it "escaped with space before" $ + let dockerfile = Text.unlines ["RUN yum install -y \\", "imagemagick \\", "mysql"] + in assertAst dockerfile [Run "yum install -y imagemagick mysql"] + it "does not choke on unmatched brackets" $ + let dockerfile = Text.unlines ["RUN [foo"] + in assertAst dockerfile [Run "[foo"] + it "Distinguishes between text and a list" $ + let dockerfile = + Text.unlines + [ "RUN echo foo", + "RUN [\"echo\", \"foo\"]" + ] + in assertAst dockerfile [Run $ RunArgs (ArgumentsText "echo foo") def, Run $ RunArgs (ArgumentsList "echo foo") def] + it "Accepts spaces inside the brackets" $ + let dockerfile = + Text.unlines + [ "RUN [ \"echo\", \"foo\" ]" + ] + in assertAst dockerfile [Run $ RunArgs (ArgumentsList "echo foo") def] + describe "parse CMD" $ do + it "one line cmd" $ assertAst "CMD true" [Cmd "true"] + it "cmd over several lines" $ + assertAst "CMD true \\\n && true" [Cmd "true && true"] + it "quoted command params" $ assertAst "CMD [\"echo\", \"1\"]" [Cmd ["echo", "1"]] + it "Parses commas correctly" $ assertAst "CMD [ \"echo\" ,\"-e\" , \"1\"]" [Cmd ["echo", "-e", "1"]] + describe "parse SHELL" + $ it "quoted shell params" + $ assertAst "SHELL [\"/bin/bash\", \"-c\"]" [Shell ["/bin/bash", "-c"]] + describe "parse HEALTHCHECK" $ do + it "parse healthcheck with interval" $ + assertAst + "HEALTHCHECK --interval=5m \\\nCMD curl -f http://localhost/" + [ Healthcheck + $ Check + $ CheckArgs "curl -f http://localhost/" (Just 300) Nothing Nothing Nothing + ] + it "parse healthcheck with retries" $ + assertAst + "HEALTHCHECK --retries=10 CMD curl -f http://localhost/" + [ Healthcheck + $ Check + $ CheckArgs "curl -f http://localhost/" Nothing Nothing Nothing (Just $ Retries 10) + ] + it "parse healthcheck with timeout" $ + assertAst + "HEALTHCHECK --timeout=10s CMD curl -f http://localhost/" + [ Healthcheck + $ Check + $ CheckArgs "curl -f http://localhost/" Nothing (Just 10) Nothing Nothing + ] + it "parse healthcheck with start-period" $ + assertAst + "HEALTHCHECK --start-period=2m CMD curl -f http://localhost/" + [ Healthcheck + $ Check + $ CheckArgs "curl -f http://localhost/" Nothing Nothing (Just 120) Nothing + ] + it "parse healthcheck with all flags" $ + assertAst + "HEALTHCHECK --start-period=2s --timeout=1m --retries=3 --interval=5s CMD curl -f http://localhost/" + [ Healthcheck + $ Check + $ CheckArgs + "curl -f http://localhost/" + (Just 5) + (Just 60) + (Just 2) + (Just $ Retries 3) + ] + it "parse healthcheck with no flags" $ + assertAst + "HEALTHCHECK CMD curl -f http://localhost/" + [ Healthcheck + $ Check + $ CheckArgs "curl -f http://localhost/" Nothing Nothing Nothing Nothing + ] + describe "parse MAINTAINER" $ do + it "maintainer of untagged scratch image" $ + assertAst + "FROM scratch\nMAINTAINER hudu@mail.com" + [From (untaggedImage "scratch"), Maintainer "hudu@mail.com"] + it "maintainer with mail" $ + assertAst "MAINTAINER hudu@mail.com" [Maintainer "hudu@mail.com"] + it "maintainer only mail after from" $ + let maintainerFromProg = "FROM busybox\nMAINTAINER hudu@mail.com" + maintainerFromAst = [From (untaggedImage "busybox"), Maintainer "hudu@mail.com"] + in assertAst maintainerFromProg maintainerFromAst + describe "parse # comment " $ do + it "multiple comments before run" $ + let dockerfile = Text.unlines ["# line 1", "# line 2", "RUN apt-get update"] + in assertAst dockerfile [Comment " line 1", Comment " line 2", Run "apt-get update"] + it "multiple comments after run" $ + let dockerfile = Text.unlines ["RUN apt-get update", "# line 1", "# line 2"] + in assertAst + dockerfile + [Run "apt-get update", Comment " line 1", Comment " line 2"] + it "empty comment" $ + let dockerfile = Text.unlines ["#", "# Hello"] + in assertAst dockerfile [Comment "", Comment " Hello"] + it "many escaped lines" $ + let dockerfile = + Text.unlines + [ "ENV A=\"a.sh\" \\", + " # comment a", + " B=\"b.sh\" \\", + " c=\"true\"", + "" + ] + in assertAst + dockerfile + [ Env [("A", "a.sh"), ("B", "b.sh"), ("c", "true")] + ] + it "accepts backslash inside string" $ + let dockerfile = "RUN grep 'foo \\.'" + in assertAst dockerfile [Run $ RunArgs (ArgumentsText "grep 'foo \\.'") def] + it "tolerates spaces after a newline escape" $ + let dockerfile = + Text.unlines + [ "FROM busy\\ ", + "box", + "RUN echo\\ ", + " hello" + ] + in assertAst + dockerfile + [ From (untaggedImage "busybox"), + Run "echo hello" + ] + it "Correctly joins blank lines starting with comments" $ + let dockerfile = + Text.unlines + [ "FROM busybox", + "# I forgot to remove the backslash \\", + "# This is a comment", + "RUN echo hello" + ] + in assertAst + dockerfile + [ From (untaggedImage "busybox"), + Comment " I forgot to remove the backslash \\", + Comment " This is a comment", + Run "echo hello" + ] + describe "expose" $ do + it "should handle number ports" $ + let content = "EXPOSE 8080" + in assertAst content [Expose (Ports [Port 8080 TCP])] + it "should handle many number ports" $ + let content = "EXPOSE 8080 8081" + in assertAst content [Expose (Ports [Port 8080 TCP, Port 8081 TCP])] + it "should handle ports with protocol" $ + let content = "EXPOSE 8080/TCP 8081/UDP" + in assertAst content [Expose (Ports [Port 8080 TCP, Port 8081 UDP])] + it "should handle ports with protocol and variables" $ + let content = "EXPOSE $PORT 8080 8081/UDP" + in assertAst content [Expose (Ports [PortStr "$PORT", Port 8080 TCP, Port 8081 UDP])] + it "should handle port ranges" $ + let content = "EXPOSE 80 81 8080-8085" + in assertAst content [Expose (Ports [Port 80 TCP, Port 81 TCP, PortRange 8080 8085 TCP])] + it "should handle udp port ranges" $ + let content = "EXPOSE 80 81 8080-8085/udp" + in assertAst content [Expose (Ports [Port 80 TCP, Port 81 TCP, PortRange 8080 8085 UDP])] + it "should handle multiline variables" $ + let content = + "EXPOSE ${PORT} ${PORT_SSL} \\\n\ + \ ${PORT_HTTP} ${PORT_HTTPS} \\\n\ + \ ${PORT_REP} \\\n\ + \ ${PORT_ADMIN} ${PORT_ADMIN_HTTP}" + in assertAst + content + [ Expose + ( Ports + [ PortStr "${PORT}", + PortStr "${PORT_SSL}", + PortStr "${PORT_HTTP}", + PortStr "${PORT_HTTPS}", + PortStr "${PORT_REP}", + PortStr "${PORT_ADMIN}", + PortStr "${PORT_ADMIN_HTTP}" + ] + ) + ] + describe "syntax" $ do + it "should handle lowercase instructions (#7 - https://github.com/beijaflor-io/haskell-language-dockerfile/issues/7)" $ + let content = "from ubuntu" + in assertAst content [From (untaggedImage "ubuntu")] + describe "ADD" $ do + it "simple ADD" $ + let file = Text.unlines ["ADD . /app", "ADD http://foo.bar/baz ."] + in assertAst + file + [ Add $ AddArgs [SourcePath "."] (TargetPath "/app") NoChown, + Add $ AddArgs [SourcePath "http://foo.bar/baz"] (TargetPath ".") NoChown + ] + it "multifiles ADD" $ + let file = Text.unlines ["ADD foo bar baz /app"] + in assertAst + file + [ Add $ AddArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown + ] + it "list of quoted files" $ + let file = Text.unlines ["ADD [\"foo\", \"bar\", \"baz\", \"/app\"]"] + in assertAst + file + [ Add $ AddArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown + ] + it "with chown flag" $ + let file = Text.unlines ["ADD --chown=root:root foo bar"] + in assertAst + file + [ Add $ AddArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "root:root") + ] + it "list of quoted files and chown" $ + let file = Text.unlines ["ADD --chown=user:group [\"foo\", \"bar\", \"baz\", \"/app\"]"] + in assertAst + file + [ Add $ AddArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") (Chown "user:group") + ] + describe "COPY" $ do + it "simple COPY" $ + let file = Text.unlines ["COPY . /app", "COPY baz /some/long/path"] + in assertAst + file + [ Copy $ CopyArgs [SourcePath "."] (TargetPath "/app") NoChown NoSource, + Copy $ CopyArgs [SourcePath "baz"] (TargetPath "/some/long/path") NoChown NoSource + ] + it "multifiles COPY" $ + let file = Text.unlines ["COPY foo bar baz /app"] + in assertAst + file + [ Copy $ CopyArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown NoSource + ] + it "list of quoted files" $ + let file = Text.unlines ["COPY [\"foo\", \"bar\", \"baz\", \"/app\"]"] + in assertAst + file + [ Copy $ CopyArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown NoSource + ] + it "with chown flag" $ + let file = Text.unlines ["COPY --chown=user:group foo bar"] + in assertAst + file + [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") NoSource + ] + it "with from flag" $ + let file = Text.unlines ["COPY --from=node foo bar"] + in assertAst + file + [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") NoChown (CopySource "node") + ] + it "with both flags" $ + let file = Text.unlines ["COPY --from=node --chown=user:group foo bar"] + in assertAst + file + [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") (CopySource "node") + ] + it "with both flags in different order" $ + let file = Text.unlines ["COPY --chown=user:group --from=node foo bar"] + in assertAst + file + [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") (CopySource "node") + ] + it "supports windows paths" $ + let file = Text.unlines ["COPY C:\\\\go C:\\\\go"] + in assertAst + file + [ Copy $ CopyArgs (fmap SourcePath ["C:\\\\go"]) (TargetPath "C:\\\\go") NoChown NoSource + ] + describe "RUN with experimental flags" $ do + it "--mount=type=bind and target" $ + let file = Text.unlines ["RUN --mount=type=bind,target=/foo echo foo"] + flags = def {mount = Just $ BindMount (def {bTarget = "/foo"})} + in assertAst + file + [ Run $ RunArgs (ArgumentsText "echo foo") flags + ] + it "--mount default to bind" $ + let file = Text.unlines ["RUN --mount=target=/foo echo foo"] + flags = def {mount = Just $ BindMount (def {bTarget = "/foo"})} + in assertAst + file + [ Run $ RunArgs (ArgumentsText "echo foo") flags + ] + it "--mount=type=bind all modifiers" $ + let file = Text.unlines ["RUN --mount=type=bind,target=/foo,source=/bar,from=ubuntu,ro echo foo"] + flags = def {mount = Just $ BindMount (BindOpts {bTarget = "/foo", bSource = Just "/bar", bFromImage = Just "ubuntu", bReadOnly = Just True})} + in assertAst + file + [ Run $ RunArgs (ArgumentsText "echo foo") flags + ] + it "--mount=type=cache with target and sharing" $ + let file = + Text.unlines + [ "RUN --mount=type=cache,target=/foo,sharing=private echo foo", + "RUN --mount=type=cache,target=/bar,sharing=shared echo foo", + "RUN --mount=type=cache,target=/baz,sharing=locked echo foo" + ] + flags1 = def {mount = Just $ CacheMount (def {cTarget = "/foo", cSharing = Private})} + flags2 = def {mount = Just $ CacheMount (def {cTarget = "/bar", cSharing = Shared})} + flags3 = def {mount = Just $ CacheMount (def {cTarget = "/baz", cSharing = Locked})} + in assertAst + file + [ Run $ RunArgs (ArgumentsText "echo foo") flags1, + Run $ RunArgs (ArgumentsText "echo foo") flags2, + Run $ RunArgs (ArgumentsText "echo foo") flags3 + ] + it "--mount=type=cache with all modifiers" $ + let file = + Text.unlines + [ "RUN --mount=type=cache,target=/foo,sharing=private,id=a,ro,from=ubuntu,source=/bar,mode=0700,uid=0,gid=0 echo foo" + ] + flags = + def + { mount = + Just $ + CacheMount + ( def + { cTarget = "/foo", + cSharing = Private, + cCacheId = Just "a", + cReadOnly = Just True, + cFromImage = Just "ubuntu", + cSource = Just "/bar", + cMode = Just "0700", + cUid = Just 0, + cGid = Just 0 + } + ) + } + in assertAst + file + [ Run $ RunArgs (ArgumentsText "echo foo") flags + ] + it "--mount=type=tmpfs" $ + let file = Text.unlines ["RUN --mount=type=tmpfs,target=/foo echo foo"] + flags = def {mount = Just $ TmpfsMount (def {tTarget = "/foo"})} + in assertAst + file + [ Run $ RunArgs (ArgumentsText "echo foo") flags + ] + it "--mount=type=ssh" $ + let file = Text.unlines ["RUN --mount=type=ssh echo foo"] + flags = def {mount = Just $ SshMount def} + in assertAst + file + [ Run $ RunArgs (ArgumentsText "echo foo") flags + ] + it "--mount=type=ssh all modifiers" $ + let file = Text.unlines ["RUN --mount=type=ssh,target=/foo,id=a,required,source=/bar,mode=0700,uid=0,gid=0 echo foo"] + flags = + def + { mount = + Just $ + SshMount + ( def + { sTarget = Just "/foo", + sCacheId = Just "a", + sIsRequired = Just True, + sSource = Just "/bar", + sMode = Just "0700", + sUid = Just 0, + sGid = Just 0 + } + ) + } + in assertAst + file + [ Run $ RunArgs (ArgumentsText "echo foo") flags + ] + it "--mount=type=secret all modifiers" $ + let file = Text.unlines ["RUN --mount=type=secret,target=/foo,id=a,required,source=/bar,mode=0700,uid=0,gid=0 echo foo"] + flags = + def + { mount = + Just $ + SecretMount + ( def + { sTarget = Just "/foo", + sCacheId = Just "a", + sIsRequired = Just True, + sSource = Just "/bar", + sMode = Just "0700", + sUid = Just 0, + sGid = Just 0 + } + ) + } + in assertAst + file + [ Run $ RunArgs (ArgumentsText "echo foo") flags + ] + it "--network=none" $ + let file = Text.unlines ["RUN --network=none echo foo"] + flags = def {network = Just NetworkNone} + in assertAst + file + [ Run $ RunArgs (ArgumentsText "echo foo") flags + ] + it "--network=host" $ + let file = Text.unlines ["RUN --network=host echo foo"] + flags = def {network = Just NetworkHost} + in assertAst + file + [ Run $ RunArgs (ArgumentsText "echo foo") flags + ] + it "--network=default" $ + let file = Text.unlines ["RUN --network=default echo foo"] + flags = def {network = Just NetworkDefault} + in assertAst + file + [ Run $ RunArgs (ArgumentsText "echo foo") flags + ] + it "--security=insecure" $ + let file = Text.unlines ["RUN --security=insecure echo foo"] + flags = def {security = Just Insecure} + in assertAst + file + [ Run $ RunArgs (ArgumentsText "echo foo") flags + ] + it "--security=sandbox" $ + let file = Text.unlines ["RUN --security=sandbox echo foo"] + flags = def {security = Just Sandbox} + in assertAst + file + [ Run $ RunArgs (ArgumentsText "echo foo") flags + ] + it "allows all flags" $ + let file = Text.unlines ["RUN --mount=target=/foo --network=none --security=sandbox echo foo"] + flags = + def + { security = Just Sandbox, + network = Just NetworkNone, + mount = Just $ BindMount $ def {bTarget = "/foo"} + } + in assertAst + file + [ Run $ RunArgs (ArgumentsText "echo foo") flags + ] assertAst :: HasCallStack => Text.Text -> [Instruction Text.Text] -> Assertion assertAst s ast = - case parseText s of - Left err -> assertFailure $ errorBundlePretty err - Right dockerfile -> assertEqual "ASTs are not equal" ast $ map instruction dockerfile + case parseText s of + Left err -> assertFailure $ errorBundlePretty err + Right dockerfile -> assertEqual "ASTs are not equal" ast $ map instruction dockerfile From 92111ce881ed25c28bcdf0eaace79d2055b84e2e Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Mon, 1 Jun 2020 23:05:57 +0200 Subject: [PATCH 10/15] reformatting with ormolu --- src/Language/Docker.hs | 208 ++++++++++++++++-------------- src/Language/Docker/EDSL.hs | 84 ++++++------ src/Language/Docker/EDSL/Quasi.hs | 29 ++--- src/Language/Docker/EDSL/Types.hs | 132 +++++++++++-------- 4 files changed, 242 insertions(+), 211 deletions(-) diff --git a/src/Language/Docker.hs b/src/Language/Docker.hs index 4f7a83f..693a82a 100644 --- a/src/Language/Docker.hs +++ b/src/Language/Docker.hs @@ -1,104 +1,116 @@ module Language.Docker - ( Language.Docker.Syntax.Dockerfile -- * Parsing Dockerfiles (@Language.Docker.Syntax@ and @Language.Docker.Parser@) - , parseText - , parseFile - , parseStdin + ( Language.Docker.Syntax.Dockerfile, + + -- * Parsing Dockerfiles (@Language.Docker.Syntax@ and @Language.Docker.Parser@) + parseText, + parseFile, + parseStdin, + -- * Re-exports from @megaparsec@ - , Text.Megaparsec.parseErrorPretty - , Text.Megaparsec.errorBundlePretty - -- * Pretty-printing Dockerfiles (@Language.Docker.PrettyPrint@) - , prettyPrint - , prettyPrintDockerfile - -- * Writting Dockerfiles (@Language.Docker.EDSL@) - , Language.Docker.EDSL.toDockerfileText - , Language.Docker.EDSL.toDockerfile - , Language.Docker.EDSL.putDockerfileStr - , Language.Docker.EDSL.writeDockerFile - , Language.Docker.EDSL.toDockerfileTextIO - , Language.Docker.EDSL.toDockerfileIO - , Language.Docker.EDSL.runDockerfileIO - , Language.Docker.EDSL.runDockerfileTextIO - , Control.Monad.IO.Class.liftIO - , Language.Docker.EDSL.from - -- ** Constructing base images - , Language.Docker.EDSL.tagged - , Language.Docker.EDSL.untagged - , Language.Docker.EDSL.digested - , Language.Docker.EDSL.aliased - -- ** Syntax - , Language.Docker.EDSL.add - , Language.Docker.EDSL.user - , Language.Docker.EDSL.label - , Language.Docker.EDSL.stopSignal - , Language.Docker.EDSL.copy - , Language.Docker.EDSL.copyFromStage - , Language.Docker.EDSL.to - , Language.Docker.EDSL.fromStage - , Language.Docker.EDSL.ownedBy - , Language.Docker.EDSL.toSources - , Language.Docker.EDSL.toTarget - , Language.Docker.EDSL.run - , Language.Docker.EDSL.runArgs - , Language.Docker.EDSL.cmd - , Language.Docker.EDSL.cmdArgs - , Language.Docker.EDSL.healthcheck - , Language.Docker.EDSL.check - , Language.Docker.EDSL.interval - , Language.Docker.EDSL.timeout - , Language.Docker.EDSL.startPeriod - , Language.Docker.EDSL.retries - , Language.Docker.EDSL.workdir - , Language.Docker.EDSL.expose - , Language.Docker.EDSL.ports - , Language.Docker.EDSL.tcpPort - , Language.Docker.EDSL.udpPort - , Language.Docker.EDSL.variablePort - , Language.Docker.EDSL.portRange - , Language.Docker.EDSL.udpPortRange - , Language.Docker.EDSL.volume - , Language.Docker.EDSL.entrypoint - , Language.Docker.EDSL.entrypointArgs - , Language.Docker.EDSL.maintainer - , Language.Docker.EDSL.env - , Language.Docker.EDSL.arg - , Language.Docker.EDSL.comment - , Language.Docker.EDSL.onBuild - , Language.Docker.EDSL.onBuildRaw - , Language.Docker.EDSL.embed - , Language.Docker.EDSL.Quasi.edockerfile - -- ** Support types for the EDSL - , Language.Docker.EDSL.EDockerfileM - , Language.Docker.EDSL.EDockerfileTM - , Language.Docker.EDSL.Types.EBaseImage(..) - -- * QuasiQuoter (@Language.Docker.EDSL.Quasi@) - , Language.Docker.EDSL.Quasi.dockerfile - -- * Types (@Language.Docker.Syntax@) - , Language.Docker.Syntax.Instruction(..) - , Language.Docker.Syntax.InstructionPos(..) - , Language.Docker.Syntax.BaseImage(..) - , Language.Docker.Syntax.SourcePath(..) - , Language.Docker.Syntax.TargetPath(..) - , Language.Docker.Syntax.Chown(..) - , Language.Docker.Syntax.CopySource(..) - , Language.Docker.Syntax.CopyArgs(..) - , Language.Docker.Syntax.AddArgs(..) - , Language.Docker.Syntax.Check(..) - , Language.Docker.Syntax.CheckArgs(..) - , Language.Docker.Syntax.Image(..) - , Language.Docker.Syntax.Registry(..) - , Language.Docker.Syntax.ImageAlias(..) - , Language.Docker.Syntax.Tag(..) - , Language.Docker.Syntax.Digest(..) - , Language.Docker.Syntax.Ports - , Language.Docker.Syntax.Directory - , Language.Docker.Syntax.Arguments - , Language.Docker.Syntax.Pairs - , Language.Docker.Syntax.Filename - , Language.Docker.Syntax.Platform - , Language.Docker.Syntax.Linenumber + Text.Megaparsec.parseErrorPretty, + Text.Megaparsec.errorBundlePretty, + + -- * Pretty-printing Dockerfiles (@Language.Docker.PrettyPrint@) + prettyPrint, + prettyPrintDockerfile, + + -- * Writting Dockerfiles (@Language.Docker.EDSL@) + Language.Docker.EDSL.toDockerfileText, + Language.Docker.EDSL.toDockerfile, + Language.Docker.EDSL.putDockerfileStr, + Language.Docker.EDSL.writeDockerFile, + Language.Docker.EDSL.toDockerfileTextIO, + Language.Docker.EDSL.toDockerfileIO, + Language.Docker.EDSL.runDockerfileIO, + Language.Docker.EDSL.runDockerfileTextIO, + Control.Monad.IO.Class.liftIO, + Language.Docker.EDSL.from, + + -- ** Constructing base images + Language.Docker.EDSL.tagged, + Language.Docker.EDSL.untagged, + Language.Docker.EDSL.digested, + Language.Docker.EDSL.aliased, + + -- ** Syntax + Language.Docker.EDSL.add, + Language.Docker.EDSL.user, + Language.Docker.EDSL.label, + Language.Docker.EDSL.stopSignal, + Language.Docker.EDSL.copy, + Language.Docker.EDSL.copyFromStage, + Language.Docker.EDSL.to, + Language.Docker.EDSL.fromStage, + Language.Docker.EDSL.ownedBy, + Language.Docker.EDSL.toSources, + Language.Docker.EDSL.toTarget, + Language.Docker.EDSL.run, + Language.Docker.EDSL.runArgs, + Language.Docker.EDSL.cmd, + Language.Docker.EDSL.cmdArgs, + Language.Docker.EDSL.healthcheck, + Language.Docker.EDSL.check, + Language.Docker.EDSL.interval, + Language.Docker.EDSL.timeout, + Language.Docker.EDSL.startPeriod, + Language.Docker.EDSL.retries, + Language.Docker.EDSL.workdir, + Language.Docker.EDSL.expose, + Language.Docker.EDSL.ports, + Language.Docker.EDSL.tcpPort, + Language.Docker.EDSL.udpPort, + Language.Docker.EDSL.variablePort, + Language.Docker.EDSL.portRange, + Language.Docker.EDSL.udpPortRange, + Language.Docker.EDSL.volume, + Language.Docker.EDSL.entrypoint, + Language.Docker.EDSL.entrypointArgs, + Language.Docker.EDSL.maintainer, + Language.Docker.EDSL.env, + Language.Docker.EDSL.arg, + Language.Docker.EDSL.comment, + Language.Docker.EDSL.onBuild, + Language.Docker.EDSL.onBuildRaw, + Language.Docker.EDSL.embed, + Language.Docker.EDSL.Quasi.edockerfile, + + -- ** Support types for the EDSL + Language.Docker.EDSL.EDockerfileM, + Language.Docker.EDSL.EDockerfileTM, + Language.Docker.EDSL.Types.EBaseImage (..), + + -- * QuasiQuoter (@Language.Docker.EDSL.Quasi@) + Language.Docker.EDSL.Quasi.dockerfile, + + -- * Types (@Language.Docker.Syntax@) + Language.Docker.Syntax.Instruction (..), + Language.Docker.Syntax.InstructionPos (..), + Language.Docker.Syntax.BaseImage (..), + Language.Docker.Syntax.SourcePath (..), + Language.Docker.Syntax.TargetPath (..), + Language.Docker.Syntax.Chown (..), + Language.Docker.Syntax.CopySource (..), + Language.Docker.Syntax.CopyArgs (..), + Language.Docker.Syntax.AddArgs (..), + Language.Docker.Syntax.Check (..), + Language.Docker.Syntax.CheckArgs (..), + Language.Docker.Syntax.Image (..), + Language.Docker.Syntax.Registry (..), + Language.Docker.Syntax.ImageAlias (..), + Language.Docker.Syntax.Tag (..), + Language.Docker.Syntax.Digest (..), + Language.Docker.Syntax.Ports, + Language.Docker.Syntax.Directory, + Language.Docker.Syntax.Arguments, + Language.Docker.Syntax.Pairs, + Language.Docker.Syntax.Filename, + Language.Docker.Syntax.Platform, + Language.Docker.Syntax.Linenumber, + -- * Instruction and InstructionPos helpers - , Language.Docker.EDSL.instructionPos - ) where + Language.Docker.EDSL.instructionPos, + ) +where import qualified Control.Monad.IO.Class import qualified Language.Docker.EDSL diff --git a/src/Language/Docker/EDSL.hs b/src/Language/Docker/EDSL.hs index 7927170..393c8c5 100644 --- a/src/Language/Docker/EDSL.hs +++ b/src/Language/Docker/EDSL.hs @@ -1,8 +1,8 @@ +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} module Language.Docker.EDSL where @@ -13,19 +13,17 @@ import Control.Monad.Trans.Free (FreeT, iterTM) import Control.Monad.Writer import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy.Char8 as B8 +import Data.Default.Class (def) import Data.List.NonEmpty (NonEmpty) import Data.String (fromString) import Data.Text (Text) import qualified Data.Text as Text import qualified Data.Text.Lazy as L import qualified Data.Text.Lazy.Encoding as E -import Data.Default.Class (def) - +import Language.Docker.EDSL.Types import qualified Language.Docker.PrettyPrint as PrettyPrint import qualified Language.Docker.Syntax as Syntax -import Language.Docker.EDSL.Types - -- | The type of 'Identity' based EDSL blocks type EDockerfileM = Free EInstruction @@ -42,9 +40,9 @@ runDockerWriter :: (MonadWriter [Syntax.Instruction Text] m) => EDockerfileM a - runDockerWriter = iterM runD runDockerWriterIO :: - (Monad m, MonadTrans t, MonadWriter [Syntax.Instruction Text] (t m)) - => EDockerfileTM m a - -> t m a + (Monad m, MonadTrans t, MonadWriter [Syntax.Instruction Text] (t m)) => + EDockerfileTM m a -> + t m a runDockerWriterIO = iterTM runD runDef :: MonadWriter [t] m => (t1 -> t) -> t1 -> m b -> m b @@ -74,8 +72,8 @@ runD (Comment c n) = runDef Syntax.Comment c n runD (Healthcheck c n) = runDef Syntax.Healthcheck c n runD (OnBuildRaw i n) = runDef Syntax.OnBuild i n runD (Embed is n) = do - tell (map Syntax.instruction is) - n + tell (map Syntax.instruction is) + n instructionPos :: Syntax.Instruction args -> Syntax.InstructionPos args instructionPos i = Syntax.InstructionPos i "" 0 @@ -84,8 +82,8 @@ instructionPos i = Syntax.InstructionPos i "" 0 -- or manipulate toDockerfile :: EDockerfileM a -> Syntax.Dockerfile toDockerfile e = - let (_, w) = runWriter (runDockerWriter e) - in map instructionPos w + let (_, w) = runWriter (runDockerWriter e) + in map instructionPos w -- | runs the Dockerfile EDSL and returns a 'Data.Text.Lazy' using -- 'Language.Docker.PrettyPrint' @@ -119,7 +117,7 @@ toDockerfileText = PrettyPrint.prettyPrint . toDockerfile -- @ writeDockerFile :: Text -> Syntax.Dockerfile -> IO () writeDockerFile filename = - BL.writeFile (Text.unpack filename) . E.encodeUtf8 . PrettyPrint.prettyPrint + BL.writeFile (Text.unpack filename) . E.encodeUtf8 . PrettyPrint.prettyPrint -- | Prints the dockerfile to stdout. Mainly used for debugging purposes -- @@ -221,11 +219,11 @@ copy (Syntax.CopyArgs sources dest ch src) = copyArgs sources dest ch src -- copyFromStage "builder" ["foo.js", "bar.js"] "." -- @ copyFromStage :: - MonadFree EInstruction m - => Syntax.CopySource - -> NonEmpty Syntax.SourcePath - -> Syntax.TargetPath - -> m () + MonadFree EInstruction m => + Syntax.CopySource -> + NonEmpty Syntax.SourcePath -> + Syntax.TargetPath -> + m () copyFromStage stage source dest = copy $ Syntax.CopyArgs source dest Syntax.NoChown stage -- | Create an ADD instruction. This is often used as a shorthand version @@ -312,38 +310,38 @@ udpPortRange a b = Syntax.PortRange a b Syntax.UDP check :: Syntax.Arguments args -> Syntax.Check args check command = - Syntax.Check - Syntax.CheckArgs - { Syntax.checkCommand = command - , Syntax.interval = Nothing - , Syntax.timeout = Nothing - , Syntax.startPeriod = Nothing - , Syntax.retries = Nothing - } + Syntax.Check + Syntax.CheckArgs + { Syntax.checkCommand = command, + Syntax.interval = Nothing, + Syntax.timeout = Nothing, + Syntax.startPeriod = Nothing, + Syntax.retries = Nothing + } interval :: Syntax.Check args -> Integer -> Syntax.Check args interval ch secs = - case ch of - Syntax.NoCheck -> Syntax.NoCheck - Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.interval = Just $ fromInteger secs} + case ch of + Syntax.NoCheck -> Syntax.NoCheck + Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.interval = Just $ fromInteger secs} timeout :: Syntax.Check args -> Integer -> Syntax.Check args timeout ch secs = - case ch of - Syntax.NoCheck -> Syntax.NoCheck - Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.timeout = Just $ fromInteger secs} + case ch of + Syntax.NoCheck -> Syntax.NoCheck + Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.timeout = Just $ fromInteger secs} startPeriod :: Syntax.Check args -> Integer -> Syntax.Check args startPeriod ch secs = - case ch of - Syntax.NoCheck -> Syntax.NoCheck - Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.startPeriod = Just $ fromInteger secs} + case ch of + Syntax.NoCheck -> Syntax.NoCheck + Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.startPeriod = Just $ fromInteger secs} retries :: Syntax.Check args -> Integer -> Syntax.Check args retries ch tries = - case ch of - Syntax.NoCheck -> Syntax.NoCheck - Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.retries = Just $ fromInteger tries} + case ch of + Syntax.NoCheck -> Syntax.NoCheck + Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.retries = Just $ fromInteger tries} noCheck :: Syntax.Check args noCheck = Syntax.NoCheck @@ -374,11 +372,11 @@ toDockerfileTextIO e = fmap snd (runDockerfileTextIO e) -- | Just runs the EDSL's writer monad runDockerfileIO :: MonadIO m => EDockerfileTM m t -> m (t, Syntax.Dockerfile) runDockerfileIO e = do - (r, w) <- runWriterT (runDockerWriterIO e) - return (r, map instructionPos w) + (r, w) <- runWriterT (runDockerWriterIO e) + return (r, map instructionPos w) -- | Runs the EDSL's writer monad and pretty-prints the result runDockerfileTextIO :: MonadIO m => EDockerfileTM m t -> m (t, L.Text) runDockerfileTextIO e = do - (r, w) <- runDockerfileIO e - return (r, PrettyPrint.prettyPrint w) + (r, w) <- runDockerfileIO e + return (r, PrettyPrint.prettyPrint w) diff --git a/src/Language/Docker/EDSL/Quasi.hs b/src/Language/Docker/EDSL/Quasi.hs index 81514a3..29b1283 100644 --- a/src/Language/Docker/EDSL/Quasi.hs +++ b/src/Language/Docker/EDSL/Quasi.hs @@ -3,14 +3,13 @@ module Language.Docker.EDSL.Quasi where -import Language.Haskell.TH -import Language.Haskell.TH.Quote -import Language.Haskell.TH.Syntax - import qualified Data.Text as Text import Language.Docker.EDSL import qualified Language.Docker.Parser as Parser import Language.Docker.Syntax.Lift () +import Language.Haskell.TH +import Language.Haskell.TH.Quote +import Language.Haskell.TH.Syntax import Text.Megaparsec (errorBundlePretty) -- | Quasiquoter for embedding dockerfiles on the EDSL @@ -29,21 +28,21 @@ edockerfile = dockerfile {quoteExp = edockerfileE} edockerfileE :: String -> ExpQ edockerfileE e = - case Parser.parseText (Text.pack e) of - Left err -> fail (errorBundlePretty err) - Right d -> [|embed d|] + case Parser.parseText (Text.pack e) of + Left err -> fail (errorBundlePretty err) + Right d -> [|embed d|] dockerfile :: QuasiQuoter dockerfile = - QuasiQuoter - { quoteExp = dockerfileE - , quoteDec = error "Can't use Dockerfile as a declaration" - , quotePat = error "Can't use Dockerfile as a pattern" - , quoteType = error "Can't use Dockerfile as a type" + QuasiQuoter + { quoteExp = dockerfileE, + quoteDec = error "Can't use Dockerfile as a declaration", + quotePat = error "Can't use Dockerfile as a pattern", + quoteType = error "Can't use Dockerfile as a type" } dockerfileE :: String -> ExpQ dockerfileE e = - case Parser.parseText (Text.pack e) of - Left err -> fail (errorBundlePretty err) - Right d -> lift d + case Parser.parseText (Text.pack e) of + Left err -> fail (errorBundlePretty err) + Right d -> lift d diff --git a/src/Language/Docker/EDSL/Types.hs b/src/Language/Docker/EDSL/Types.hs index 8a203e2..16c93c0 100644 --- a/src/Language/Docker/EDSL/Types.hs +++ b/src/Language/Docker/EDSL/Types.hs @@ -7,62 +7,84 @@ import Data.String import Data.Text (Text) import qualified Language.Docker.Syntax as Syntax -data EBaseImage = EBaseImage Syntax.Image - (Maybe Syntax.Tag) - (Maybe Syntax.Digest) - (Maybe Syntax.ImageAlias) - (Maybe Syntax.Platform) - deriving (Show, Eq, Ord) +data EBaseImage + = EBaseImage + Syntax.Image + (Maybe Syntax.Tag) + (Maybe Syntax.Digest) + (Maybe Syntax.ImageAlias) + (Maybe Syntax.Platform) + deriving (Show, Eq, Ord) instance IsString EBaseImage where - fromString s = EBaseImage (fromString s) Nothing Nothing Nothing Nothing + fromString s = EBaseImage (fromString s) Nothing Nothing Nothing Nothing data EInstruction next - = From EBaseImage - next - | AddArgs (NonEmpty Syntax.SourcePath) - Syntax.TargetPath - Syntax.Chown - next - | User Text - next - | Label Syntax.Pairs - next - | StopSignal Text - next - | CopyArgs (NonEmpty Syntax.SourcePath) - Syntax.TargetPath - Syntax.Chown - Syntax.CopySource - next - | RunArgs (Syntax.Arguments Text) - Syntax.RunFlags - next - | CmdArgs (Syntax.Arguments Text) - next - | Shell (Syntax.Arguments Text) - next - | Workdir Syntax.Directory - next - | Expose Syntax.Ports - next - | Volume Text - next - | EntrypointArgs (Syntax.Arguments Text) - next - | Maintainer Text - next - | Env Syntax.Pairs - next - | Arg Text - (Maybe Text) - next - | Comment Text - next - | Healthcheck (Syntax.Check Text) - next - | OnBuildRaw (Syntax.Instruction Text) - next - | Embed [Syntax.InstructionPos Text] - next - deriving (Functor) + = From + EBaseImage + next + | AddArgs + (NonEmpty Syntax.SourcePath) + Syntax.TargetPath + Syntax.Chown + next + | User + Text + next + | Label + Syntax.Pairs + next + | StopSignal + Text + next + | CopyArgs + (NonEmpty Syntax.SourcePath) + Syntax.TargetPath + Syntax.Chown + Syntax.CopySource + next + | RunArgs + (Syntax.Arguments Text) + Syntax.RunFlags + next + | CmdArgs + (Syntax.Arguments Text) + next + | Shell + (Syntax.Arguments Text) + next + | Workdir + Syntax.Directory + next + | Expose + Syntax.Ports + next + | Volume + Text + next + | EntrypointArgs + (Syntax.Arguments Text) + next + | Maintainer + Text + next + | Env + Syntax.Pairs + next + | Arg + Text + (Maybe Text) + next + | Comment + Text + next + | Healthcheck + (Syntax.Check Text) + next + | OnBuildRaw + (Syntax.Instruction Text) + next + | Embed + [Syntax.InstructionPos Text] + next + deriving (Functor) From 4d8377aaaf318753b69c7c93fe7c934f0cad206d Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Mon, 1 Jun 2020 23:36:24 +0200 Subject: [PATCH 11/15] Moving the EDSL stuff out It will live in another repo. We do this to make it easier for hadolint to be cross-compiled to oother architectures --- examples/complex.hs | 39 --- examples/edsl-quasi.hs | 14 - examples/edsl.hs | 11 - examples/parse-string.hs | 5 - examples/parse.hs | 4 - examples/pretty-print.hs | 6 - examples/templating-7.10.dockerfile | 7 - examples/templating-7.8.dockerfile | 7 - examples/templating-8.dockerfile | 7 - examples/templating.hs | 29 -- examples/test-dockerfile.dockerfile | 5 - examples/test-dockerfile.hs | 14 - language-docker.cabal | 29 +- package.yaml | 17 +- src/Language/Docker.hs | 75 ----- src/Language/Docker/EDSL.hs | 382 ------------------------- src/Language/Docker/EDSL/Quasi.hs | 48 ---- src/Language/Docker/EDSL/Types.hs | 90 ------ src/Language/Docker/Syntax/Lift.hs | 80 ------ test/Language/Docker/EDSL/QuasiSpec.hs | 46 --- test/Language/Docker/EDSLSpec.hs | 137 --------- test/Language/Docker/ExamplesSpec.hs | 20 -- 22 files changed, 6 insertions(+), 1066 deletions(-) delete mode 100644 examples/complex.hs delete mode 100644 examples/edsl-quasi.hs delete mode 100644 examples/edsl.hs delete mode 100644 examples/parse-string.hs delete mode 100644 examples/parse.hs delete mode 100644 examples/pretty-print.hs delete mode 100644 examples/templating-7.10.dockerfile delete mode 100644 examples/templating-7.8.dockerfile delete mode 100644 examples/templating-8.dockerfile delete mode 100644 examples/templating.hs delete mode 100644 examples/test-dockerfile.dockerfile delete mode 100644 examples/test-dockerfile.hs delete mode 100644 src/Language/Docker/EDSL.hs delete mode 100644 src/Language/Docker/EDSL/Quasi.hs delete mode 100644 src/Language/Docker/EDSL/Types.hs delete mode 100644 src/Language/Docker/Syntax/Lift.hs delete mode 100644 test/Language/Docker/EDSL/QuasiSpec.hs delete mode 100644 test/Language/Docker/EDSLSpec.hs delete mode 100644 test/Language/Docker/ExamplesSpec.hs diff --git a/examples/complex.hs b/examples/complex.hs deleted file mode 100644 index d62436a..0000000 --- a/examples/complex.hs +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env stack --silent runghc --package language-docker --package ShellCheck-0.4.4 ./dockerfile.hs --- https://github.com/mhart/alpine-node -{-# LANGUAGE QuasiQuotes #-} - -import Language.Docker - -main = - putDockerfileStr $ - [edockerfile| - # https://github.com/mhart/alpine-node - FROM mhart/alpine-node:5.5.0 - - ENV DIR=/opt/este PORT=8000 \ - # This is a docker comment - - NODE_ENV=production - - RUN apk add --update python python-dev build-base git libpng automake gettext libpng-dev autoconf make zlib-dev nasm - - COPY package.json ${DIR}/ - - RUN cd ${DIR} && npm install - - RUN cd ${DIR} && npm install stylus && npm install eslint-plugin-jsx-a11y - - RUN --mount=type=cache,target=/foo,sharing=shared npm install - - COPY . $DIR - - WORKDIR $DIR - - RUN NODE_ENV=production SERVER_URL="https://beijaflor.io" npm run build -- -p - - EXPOSE $PORT - - ENTRYPOINT ["npm"] - - CMD ["start"] -|] diff --git a/examples/edsl-quasi.hs b/examples/edsl-quasi.hs deleted file mode 100644 index f6d6970..0000000 --- a/examples/edsl-quasi.hs +++ /dev/null @@ -1,14 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE QuasiQuotes #-} - -import Language.Docker - -main = - putDockerfileStr $ do - from "node" - run "apt-get update" - [edockerfile| - RUN apt-get update - CMD node something.js - |] - -- ... diff --git a/examples/edsl.hs b/examples/edsl.hs deleted file mode 100644 index 0610369..0000000 --- a/examples/edsl.hs +++ /dev/null @@ -1,11 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE OverloadedLists #-} - -import Language.Docker - -main = - putDockerfileStr $ do - from "node" - run "apt-get update" - cmd ["node", "app.js"] - -- ... diff --git a/examples/parse-string.hs b/examples/parse-string.hs deleted file mode 100644 index d5ef3ca..0000000 --- a/examples/parse-string.hs +++ /dev/null @@ -1,5 +0,0 @@ -import Language.Docker - -main = do - c <- parseFile "./Dockerfile" - print c diff --git a/examples/parse.hs b/examples/parse.hs deleted file mode 100644 index 1f85578..0000000 --- a/examples/parse.hs +++ /dev/null @@ -1,4 +0,0 @@ -import Language.Docker -main = do - ef <- parseFile "./Dockerfile" - print ef diff --git a/examples/pretty-print.hs b/examples/pretty-print.hs deleted file mode 100644 index b63f234..0000000 --- a/examples/pretty-print.hs +++ /dev/null @@ -1,6 +0,0 @@ -import qualified Data.Text.Lazy.IO as L -import Language.Docker - -main = do - Right d <- parseFile "./Dockerfile" - L.putStr (prettyPrint d) diff --git a/examples/templating-7.10.dockerfile b/examples/templating-7.10.dockerfile deleted file mode 100644 index 9c2e908..0000000 --- a/examples/templating-7.10.dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM haskell:7.10 -RUN cabal sandbox init -RUN cabal update -ADD mypackage.cabal /app/mypackage.cabal -RUN cabal install --only-dep -j -ADD . /app/ -RUN cabal build diff --git a/examples/templating-7.8.dockerfile b/examples/templating-7.8.dockerfile deleted file mode 100644 index ab648bd..0000000 --- a/examples/templating-7.8.dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM haskell:7.8 -RUN cabal sandbox init -RUN cabal update -ADD mypackage.cabal /app/mypackage.cabal -RUN cabal install --only-dep -j -ADD . /app/ -RUN cabal build diff --git a/examples/templating-8.dockerfile b/examples/templating-8.dockerfile deleted file mode 100644 index a23c2a9..0000000 --- a/examples/templating-8.dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM haskell:8 -RUN cabal sandbox init -RUN cabal update -ADD mypackage.cabal /app/mypackage.cabal -RUN cabal install --only-dep -j -ADD . /app/ -RUN cabal build diff --git a/examples/templating.hs b/examples/templating.hs deleted file mode 100644 index 95600cb..0000000 --- a/examples/templating.hs +++ /dev/null @@ -1,29 +0,0 @@ -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE OverloadedLists #-} - -import Control.Monad -import Data.Semigroup ((<>)) -import Language.Docker -import Language.Docker.Syntax - -tags :: [Tag] -tags = ["7.8", "7.10", "8"] - -cabalSandboxBuild packageName = do - let cabalFile = packageName <> ".cabal" - run "cabal sandbox init" - run "cabal update" - add [SourcePath cabalFile] (TargetPath $ "/app/" <> cabalFile) - run "cabal install --only-dep -j" - add ["."] "/app/" - run "cabal build" - -main = - forM_ tags $ \tag -> do - let df = - toDockerfile $ do - from ("haskell" `tagged` tag) - cabalSandboxBuild "mypackage" - name = "./examples/templating-" <> unTag tag <> ".dockerfile" - writeDockerFile name df diff --git a/examples/test-dockerfile.dockerfile b/examples/test-dockerfile.dockerfile deleted file mode 100644 index c40acf8..0000000 --- a/examples/test-dockerfile.dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM fpco/stack-build:lts-6.9 -ADD . /app/language-docker -WORKDIR /app/language-docker -RUN stack build --test --only-dependencies -CMD stack test diff --git a/examples/test-dockerfile.hs b/examples/test-dockerfile.hs deleted file mode 100644 index 4fb7eec..0000000 --- a/examples/test-dockerfile.hs +++ /dev/null @@ -1,14 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE OverloadedLists #-} - -import Language.Docker - -main :: IO () -main = - writeDockerFile "./examples/test-dockerfile.dockerfile" $ - toDockerfile $ do - from (tagged "fpco/stack-build" "lts-6.9") - add ["."] "/app/language-docker" - workdir "/app/language-docker" - run "stack build --test --only-dependencies" - cmd "stack test" diff --git a/language-docker.cabal b/language-docker.cabal index d4921af..0f18481 100644 --- a/language-docker.cabal +++ b/language-docker.cabal @@ -4,10 +4,10 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 74f106305ab18c79412aa9b8d150b34cf232e1aebc61a50c65463fc4d9540d07 +-- hash: 005937a918829cd3728fed743544eec119cc21efef9ed052db7c971ecfcc16f0 name: language-docker -version: 8.1.1 +version: 9.0.0 synopsis: Dockerfile parser, pretty-printer and embedded DSL description: All functions for parsing, printing and writting Dockerfiles are exported through @Language.Docker@. For more fine-grained operations look for specific modules that implement a certain functionality. See the for the source-code and examples. @@ -37,10 +37,6 @@ library Language.Docker.Parser Language.Docker.PrettyPrint Language.Docker.Syntax - Language.Docker.Syntax.Lift - Language.Docker.EDSL - Language.Docker.EDSL.Quasi - Language.Docker.EDSL.Types other-modules: Language.Docker.Parser.Arguments Language.Docker.Parser.Cmd @@ -61,15 +57,10 @@ library , bytestring >=0.10 , containers , data-default-class - , free , megaparsec >=8.0 - , mtl , prettyprinter , split >=0.2 - , template-haskell , text - , th-lift - , th-lift-instances , time default-language: Haskell2010 @@ -77,35 +68,23 @@ test-suite hspec type: exitcode-stdio-1.0 main-is: Spec.hs other-modules: - Language.Docker.EDSL.QuasiSpec - Language.Docker.EDSLSpec - Language.Docker.ExamplesSpec Language.Docker.IntegrationSpec Language.Docker.ParserSpec Paths_language_docker hs-source-dirs: test build-depends: - Glob - , HUnit >=1.2 + HUnit >=1.2 , QuickCheck , base >=4.13 && <5 , bytestring >=0.10 , containers , data-default-class - , directory - , filepath - , free , hspec , language-docker - , megaparsec >=7.0 - , mtl + , megaparsec >=8.0 , prettyprinter - , process , split >=0.2 - , template-haskell , text - , th-lift - , th-lift-instances , time default-language: Haskell2010 diff --git a/package.yaml b/package.yaml index 64e4b37..4154dfe 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: language-docker -version: '8.1.1' +version: '9.0.0' synopsis: Dockerfile parser, pretty-printer and embedded DSL description: 'All functions for parsing, printing and writting Dockerfiles are exported through @Language.Docker@. For more fine-grained operations look for @@ -29,11 +29,6 @@ dependencies: - megaparsec >=8.0 - prettyprinter - split >=0.2 - - free - - mtl - - template-haskell - - th-lift - - th-lift-instances - text - time - containers @@ -54,10 +49,6 @@ library: - Language.Docker.Parser - Language.Docker.PrettyPrint - Language.Docker.Syntax - - Language.Docker.Syntax.Lift - - Language.Docker.EDSL - - Language.Docker.EDSL.Quasi - - Language.Docker.EDSL.Types tests: hspec: @@ -67,9 +58,5 @@ tests: - hspec - QuickCheck - language-docker - - Glob - - directory - - filepath - - process - HUnit >=1.2 - - megaparsec >=7.0 + - megaparsec >=8.0 diff --git a/src/Language/Docker.hs b/src/Language/Docker.hs index 693a82a..382118d 100644 --- a/src/Language/Docker.hs +++ b/src/Language/Docker.hs @@ -14,74 +14,6 @@ module Language.Docker prettyPrint, prettyPrintDockerfile, - -- * Writting Dockerfiles (@Language.Docker.EDSL@) - Language.Docker.EDSL.toDockerfileText, - Language.Docker.EDSL.toDockerfile, - Language.Docker.EDSL.putDockerfileStr, - Language.Docker.EDSL.writeDockerFile, - Language.Docker.EDSL.toDockerfileTextIO, - Language.Docker.EDSL.toDockerfileIO, - Language.Docker.EDSL.runDockerfileIO, - Language.Docker.EDSL.runDockerfileTextIO, - Control.Monad.IO.Class.liftIO, - Language.Docker.EDSL.from, - - -- ** Constructing base images - Language.Docker.EDSL.tagged, - Language.Docker.EDSL.untagged, - Language.Docker.EDSL.digested, - Language.Docker.EDSL.aliased, - - -- ** Syntax - Language.Docker.EDSL.add, - Language.Docker.EDSL.user, - Language.Docker.EDSL.label, - Language.Docker.EDSL.stopSignal, - Language.Docker.EDSL.copy, - Language.Docker.EDSL.copyFromStage, - Language.Docker.EDSL.to, - Language.Docker.EDSL.fromStage, - Language.Docker.EDSL.ownedBy, - Language.Docker.EDSL.toSources, - Language.Docker.EDSL.toTarget, - Language.Docker.EDSL.run, - Language.Docker.EDSL.runArgs, - Language.Docker.EDSL.cmd, - Language.Docker.EDSL.cmdArgs, - Language.Docker.EDSL.healthcheck, - Language.Docker.EDSL.check, - Language.Docker.EDSL.interval, - Language.Docker.EDSL.timeout, - Language.Docker.EDSL.startPeriod, - Language.Docker.EDSL.retries, - Language.Docker.EDSL.workdir, - Language.Docker.EDSL.expose, - Language.Docker.EDSL.ports, - Language.Docker.EDSL.tcpPort, - Language.Docker.EDSL.udpPort, - Language.Docker.EDSL.variablePort, - Language.Docker.EDSL.portRange, - Language.Docker.EDSL.udpPortRange, - Language.Docker.EDSL.volume, - Language.Docker.EDSL.entrypoint, - Language.Docker.EDSL.entrypointArgs, - Language.Docker.EDSL.maintainer, - Language.Docker.EDSL.env, - Language.Docker.EDSL.arg, - Language.Docker.EDSL.comment, - Language.Docker.EDSL.onBuild, - Language.Docker.EDSL.onBuildRaw, - Language.Docker.EDSL.embed, - Language.Docker.EDSL.Quasi.edockerfile, - - -- ** Support types for the EDSL - Language.Docker.EDSL.EDockerfileM, - Language.Docker.EDSL.EDockerfileTM, - Language.Docker.EDSL.Types.EBaseImage (..), - - -- * QuasiQuoter (@Language.Docker.EDSL.Quasi@) - Language.Docker.EDSL.Quasi.dockerfile, - -- * Types (@Language.Docker.Syntax@) Language.Docker.Syntax.Instruction (..), Language.Docker.Syntax.InstructionPos (..), @@ -106,16 +38,9 @@ module Language.Docker Language.Docker.Syntax.Filename, Language.Docker.Syntax.Platform, Language.Docker.Syntax.Linenumber, - - -- * Instruction and InstructionPos helpers - Language.Docker.EDSL.instructionPos, ) where -import qualified Control.Monad.IO.Class -import qualified Language.Docker.EDSL -import qualified Language.Docker.EDSL.Quasi -import qualified Language.Docker.EDSL.Types import Language.Docker.Parser import Language.Docker.PrettyPrint import qualified Language.Docker.Syntax diff --git a/src/Language/Docker/EDSL.hs b/src/Language/Docker/EDSL.hs deleted file mode 100644 index 393c8c5..0000000 --- a/src/Language/Docker/EDSL.hs +++ /dev/null @@ -1,382 +0,0 @@ -{-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE OverloadedLists #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TemplateHaskell #-} - -module Language.Docker.EDSL where - -import Control.Monad.Free -import Control.Monad.Free.TH -import Control.Monad.Trans.Free (FreeT, iterTM) -import Control.Monad.Writer -import qualified Data.ByteString.Lazy as BL -import qualified Data.ByteString.Lazy.Char8 as B8 -import Data.Default.Class (def) -import Data.List.NonEmpty (NonEmpty) -import Data.String (fromString) -import Data.Text (Text) -import qualified Data.Text as Text -import qualified Data.Text.Lazy as L -import qualified Data.Text.Lazy.Encoding as E -import Language.Docker.EDSL.Types -import qualified Language.Docker.PrettyPrint as PrettyPrint -import qualified Language.Docker.Syntax as Syntax - --- | The type of 'Identity' based EDSL blocks -type EDockerfileM = Free EInstruction - --- | The type of free monad EDSL blocks -type EDockerfileTM = FreeT EInstruction - -type EInstructionM = Free EInstruction - -type EInstructionTM = FreeT EInstruction - -makeFree ''EInstruction - -runDockerWriter :: (MonadWriter [Syntax.Instruction Text] m) => EDockerfileM a -> m a -runDockerWriter = iterM runD - -runDockerWriterIO :: - (Monad m, MonadTrans t, MonadWriter [Syntax.Instruction Text] (t m)) => - EDockerfileTM m a -> - t m a -runDockerWriterIO = iterTM runD - -runDef :: MonadWriter [t] m => (t1 -> t) -> t1 -> m b -> m b -runDef f a n = tell [f a] >> n - -runDef2 :: MonadWriter [t] m => (t1 -> t2 -> t) -> t1 -> t2 -> m b -> m b -runDef2 f a b n = tell [f a b] >> n - -runD :: MonadWriter [Syntax.Instruction Text] m => EInstruction (m b) -> m b -runD (From (EBaseImage name t d a p) n) = runDef Syntax.From (Syntax.BaseImage name t d a p) n -runD (CmdArgs as n) = runDef Syntax.Cmd as n -runD (Shell as n) = runDef Syntax.Shell as n -runD (AddArgs s d c n) = runDef Syntax.Add (Syntax.AddArgs s d c) n -runD (User u n) = runDef Syntax.User u n -runD (Label ps n) = runDef Syntax.Label ps n -runD (StopSignal s n) = runDef Syntax.Stopsignal s n -runD (CopyArgs s d c f n) = runDef Syntax.Copy (Syntax.CopyArgs s d c f) n -runD (RunArgs as fs n) = runDef Syntax.Run (Syntax.RunArgs as fs) n -runD (Workdir d n) = runDef Syntax.Workdir d n -runD (Expose ps n) = runDef Syntax.Expose ps n -runD (Volume v n) = runDef Syntax.Volume v n -runD (EntrypointArgs e n) = runDef Syntax.Entrypoint e n -runD (Maintainer m n) = runDef Syntax.Maintainer m n -runD (Env ps n) = runDef Syntax.Env ps n -runD (Arg k v n) = runDef2 Syntax.Arg k v n -runD (Comment c n) = runDef Syntax.Comment c n -runD (Healthcheck c n) = runDef Syntax.Healthcheck c n -runD (OnBuildRaw i n) = runDef Syntax.OnBuild i n -runD (Embed is n) = do - tell (map Syntax.instruction is) - n - -instructionPos :: Syntax.Instruction args -> Syntax.InstructionPos args -instructionPos i = Syntax.InstructionPos i "" 0 - --- | Runs the Dockerfile EDSL and returns a 'Dockerfile' you can pretty print --- or manipulate -toDockerfile :: EDockerfileM a -> Syntax.Dockerfile -toDockerfile e = - let (_, w) = runWriter (runDockerWriter e) - in map instructionPos w - --- | runs the Dockerfile EDSL and returns a 'Data.Text.Lazy' using --- 'Language.Docker.PrettyPrint' --- --- @ --- import Language.Docker --- --- main :: IO () --- main = print $ toDockerfileText $ do --- from (tagged "fpco/stack-build" "lts-6.9") --- add ["."] "/app/language-docker" --- workdir "/app/language-docker" --- run "stack build --test --only-dependencies" --- cmd "stack test" --- @ -toDockerfileText :: EDockerfileM a -> L.Text -toDockerfileText = PrettyPrint.prettyPrint . toDockerfile - --- | Writes the dockerfile to the given file path after pretty-printing it --- --- @ --- import Language.Docker --- --- main :: IO () --- main = writeDockerFile "build.Dockerfile" $ toDockerfile $ do --- from (tagged "fpco/stack-build" "lts-6.9") --- add ["."] "/app/language-docker" --- workdir "/app/language-docker" --- run "stack build --test --only-dependencies" --- cmd "stack test" --- @ -writeDockerFile :: Text -> Syntax.Dockerfile -> IO () -writeDockerFile filename = - BL.writeFile (Text.unpack filename) . E.encodeUtf8 . PrettyPrint.prettyPrint - --- | Prints the dockerfile to stdout. Mainly used for debugging purposes --- --- @ --- import Language.Docker --- --- main :: IO () --- main = putDockerfileStr $ do --- from (tagged "fpco/stack-build" "lts-6.9") --- add ["."] "/app/language-docker" --- workdir "/app/language-docker" --- run "stack build --test --only-dependencies" --- cmd "stack test" --- @ -putDockerfileStr :: EDockerfileM a -> IO () -putDockerfileStr = B8.putStrLn . E.encodeUtf8 . PrettyPrint.prettyPrint . toDockerfile - --- | Use a docker image in a FROM instruction without a tag --- --- The following two examples are equivalent --- --- @ --- from $ untagged "fpco/stack-build" --- @ --- --- Is equivalent to, when having OverloadedStrings: --- --- @ --- from "fpco/stack-build" --- @ -untagged :: Text -> EBaseImage -untagged s = EBaseImage (fromString . Text.unpack $ s) Nothing Nothing Nothing Nothing - --- | Use a specific tag for a docker image. This function is meant --- to be used as an infix operator. --- --- @ --- from $ "fpco/stack-build" `tagged` "lts-10.3" --- @ -tagged :: Syntax.Image -> Syntax.Tag -> EBaseImage -tagged imageName tag = EBaseImage imageName (Just tag) Nothing Nothing Nothing - --- | Adds a digest checksum so a FROM instruction --- This function is meant to be used as an infix operator. --- --- @ --- from $ "fpco/stack-build" `digested` "sha256:abcdef123" --- @ -digested :: EBaseImage -> Syntax.Digest -> EBaseImage -digested (EBaseImage n t _ a p) d = EBaseImage n t (Just d) a p - --- | Alias a FROM instruction to be used as a build stage. --- This function is meant to be used as an infix operator. --- --- @ --- from $ "fpco/stack-build" `aliased` "builder" --- @ -aliased :: EBaseImage -> Syntax.ImageAlias -> EBaseImage -aliased (EBaseImage n t d _ p) a = EBaseImage n t d (Just a) p - --- | Create a RUN instruction with the given arguments. --- --- @ --- run "apt-get install wget" --- @ -run :: MonadFree EInstruction m => Syntax.Arguments Text -> m () -run as = runArgs as def - --- | Create an ENTRYPOINT instruction with the given arguments. --- --- @ --- entrypoint "/usr/local/bin/program --some-flag" --- @ -entrypoint :: MonadFree EInstruction m => Syntax.Arguments Text -> m () -entrypoint = entrypointArgs - --- | Create a CMD instruction with the given arguments. --- --- @ --- cmd "my-program --some-flag" --- @ -cmd :: MonadFree EInstruction m => Syntax.Arguments Text -> m () -cmd = cmdArgs - --- | Create a COPY instruction. This function is meant to be --- used with the compinators 'to', 'fromStage' and 'ownedBy' --- --- @ --- copy $ ["foo.js", "bar.js"] `to` "." --- copy $ ["some_file"] `to` "/some/path" `fromStage` "builder" --- @ -copy :: MonadFree EInstruction m => Syntax.CopyArgs -> m () -copy (Syntax.CopyArgs sources dest ch src) = copyArgs sources dest ch src - --- | Create a COPY instruction from a given build stage. --- This is a shorthand version of using 'copy' with combinators. --- --- @ --- copyFromStage "builder" ["foo.js", "bar.js"] "." --- @ -copyFromStage :: - MonadFree EInstruction m => - Syntax.CopySource -> - NonEmpty Syntax.SourcePath -> - Syntax.TargetPath -> - m () -copyFromStage stage source dest = copy $ Syntax.CopyArgs source dest Syntax.NoChown stage - --- | Create an ADD instruction. This is often used as a shorthand version --- of copy when no extra options are needed. Currently there is no way to --- pass extra options to ADD, so you are encouraged to use 'copy' instead. --- --- @ --- add ["foo.js", "bar.js"] "." --- @ -add :: MonadFree EInstruction m => NonEmpty Syntax.SourcePath -> Syntax.TargetPath -> m () -add sources dest = addArgs sources dest Syntax.NoChown - --- | Converts a NonEmpty list of strings to a NonEmpty list of 'Syntax.SourcePath' --- --- This is a convenience function when you need to pass a non-static list of --- strings that you build somewhere as an argument for 'copy' or 'add' --- --- @ --- someFiles <- glob "*.js" --- copy $ (toSources someFiles) `to` "." --- @ -toSources :: NonEmpty Text -> NonEmpty Syntax.SourcePath -toSources = fmap Syntax.SourcePath - --- | Converts a Text into a 'Syntax.TargetPath' --- --- This is a convenience function when you need to pass a string variable --- as an argument for 'copy' or 'add' --- --- @ --- let destination = buildSomePath pwd --- add ["foo.js"] (toTarget destination) --- @ -toTarget :: Text -> Syntax.TargetPath -toTarget = Syntax.TargetPath - --- | Adds the --from= option to a COPY instruction. --- --- This function is meant to be used as an infix operator: --- --- @ --- copy $ ["foo.js"] `to` "." `fromStage` "builder" --- @ -fromStage :: Syntax.CopyArgs -> Syntax.CopySource -> Syntax.CopyArgs -fromStage args src = args {Syntax.sourceFlag = src} - --- | Adds the --chown= option to a COPY instruction. --- --- This function is meant to be used as an infix operator: --- --- @ --- copy $ ["foo.js"] `to` "." `ownedBy` "www-data:www-data" --- @ -ownedBy :: Syntax.CopyArgs -> Syntax.Chown -> Syntax.CopyArgs -ownedBy args owner = args {Syntax.chownFlag = owner} - --- | Usedto join source paths with atarget path as an arguments for 'copy' --- --- This function is meant to be used as an infix operator: --- --- @ --- copy $ ["foo.js"] `to` "." `ownedBy` --- @ -to :: NonEmpty Syntax.SourcePath -> Syntax.TargetPath -> Syntax.CopyArgs -to sources dest = Syntax.CopyArgs sources dest Syntax.NoChown Syntax.NoSource - -ports :: [Syntax.Port] -> Syntax.Ports -ports = Syntax.Ports - -tcpPort :: Int -> Syntax.Port -tcpPort = flip Syntax.Port Syntax.TCP - -udpPort :: Int -> Syntax.Port -udpPort = flip Syntax.Port Syntax.UDP - -variablePort :: Text -> Syntax.Port -variablePort varName = Syntax.PortStr ("$" <> varName) - -portRange :: Int -> Int -> Syntax.Port -portRange a b = Syntax.PortRange a b Syntax.TCP - -udpPortRange :: Int -> Int -> Syntax.Port -udpPortRange a b = Syntax.PortRange a b Syntax.UDP - -check :: Syntax.Arguments args -> Syntax.Check args -check command = - Syntax.Check - Syntax.CheckArgs - { Syntax.checkCommand = command, - Syntax.interval = Nothing, - Syntax.timeout = Nothing, - Syntax.startPeriod = Nothing, - Syntax.retries = Nothing - } - -interval :: Syntax.Check args -> Integer -> Syntax.Check args -interval ch secs = - case ch of - Syntax.NoCheck -> Syntax.NoCheck - Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.interval = Just $ fromInteger secs} - -timeout :: Syntax.Check args -> Integer -> Syntax.Check args -timeout ch secs = - case ch of - Syntax.NoCheck -> Syntax.NoCheck - Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.timeout = Just $ fromInteger secs} - -startPeriod :: Syntax.Check args -> Integer -> Syntax.Check args -startPeriod ch secs = - case ch of - Syntax.NoCheck -> Syntax.NoCheck - Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.startPeriod = Just $ fromInteger secs} - -retries :: Syntax.Check args -> Integer -> Syntax.Check args -retries ch tries = - case ch of - Syntax.NoCheck -> Syntax.NoCheck - Syntax.Check chArgs -> Syntax.Check chArgs {Syntax.retries = Just $ fromInteger tries} - -noCheck :: Syntax.Check args -noCheck = Syntax.NoCheck - --- | ONBUILD Dockerfile instruction --- --- Each nested instruction gets emitted as a separate @ONBUILD@ block --- --- @ --- 'toDockerfile' $ do --- from "node" --- run "apt-get update" --- onBuild $ do --- run "echo more-stuff" --- run "echo here" --- @ -onBuild :: MonadFree EInstruction m => EDockerfileM a -> m () -onBuild b = mapM_ (onBuildRaw . Syntax.instruction) (toDockerfile b) - --- | A version of 'toDockerfile' which allows IO actions -toDockerfileIO :: MonadIO m => EDockerfileTM m t -> m Syntax.Dockerfile -toDockerfileIO e = fmap snd (runDockerfileIO e) - --- | A version of 'toDockerfileText' which allows IO actions -toDockerfileTextIO :: MonadIO m => EDockerfileTM m t -> m L.Text -toDockerfileTextIO e = fmap snd (runDockerfileTextIO e) - --- | Just runs the EDSL's writer monad -runDockerfileIO :: MonadIO m => EDockerfileTM m t -> m (t, Syntax.Dockerfile) -runDockerfileIO e = do - (r, w) <- runWriterT (runDockerWriterIO e) - return (r, map instructionPos w) - --- | Runs the EDSL's writer monad and pretty-prints the result -runDockerfileTextIO :: MonadIO m => EDockerfileTM m t -> m (t, L.Text) -runDockerfileTextIO e = do - (r, w) <- runDockerfileIO e - return (r, PrettyPrint.prettyPrint w) diff --git a/src/Language/Docker/EDSL/Quasi.hs b/src/Language/Docker/EDSL/Quasi.hs deleted file mode 100644 index 29b1283..0000000 --- a/src/Language/Docker/EDSL/Quasi.hs +++ /dev/null @@ -1,48 +0,0 @@ -{-# LANGUAGE QuasiQuotes #-} -{-# LANGUAGE TemplateHaskell #-} - -module Language.Docker.EDSL.Quasi where - -import qualified Data.Text as Text -import Language.Docker.EDSL -import qualified Language.Docker.Parser as Parser -import Language.Docker.Syntax.Lift () -import Language.Haskell.TH -import Language.Haskell.TH.Quote -import Language.Haskell.TH.Syntax -import Text.Megaparsec (errorBundlePretty) - --- | Quasiquoter for embedding dockerfiles on the EDSL --- --- @ --- putStr $ 'toDockerfile' $ do --- from "node" --- run "apt-get update" --- [edockerfile| --- RUN apt-get update --- CMD node something.js --- |] --- @ -edockerfile :: QuasiQuoter -edockerfile = dockerfile {quoteExp = edockerfileE} - -edockerfileE :: String -> ExpQ -edockerfileE e = - case Parser.parseText (Text.pack e) of - Left err -> fail (errorBundlePretty err) - Right d -> [|embed d|] - -dockerfile :: QuasiQuoter -dockerfile = - QuasiQuoter - { quoteExp = dockerfileE, - quoteDec = error "Can't use Dockerfile as a declaration", - quotePat = error "Can't use Dockerfile as a pattern", - quoteType = error "Can't use Dockerfile as a type" - } - -dockerfileE :: String -> ExpQ -dockerfileE e = - case Parser.parseText (Text.pack e) of - Left err -> fail (errorBundlePretty err) - Right d -> lift d diff --git a/src/Language/Docker/EDSL/Types.hs b/src/Language/Docker/EDSL/Types.hs deleted file mode 100644 index 16c93c0..0000000 --- a/src/Language/Docker/EDSL/Types.hs +++ /dev/null @@ -1,90 +0,0 @@ -{-# LANGUAGE DeriveFunctor #-} - -module Language.Docker.EDSL.Types where - -import Data.List.NonEmpty (NonEmpty) -import Data.String -import Data.Text (Text) -import qualified Language.Docker.Syntax as Syntax - -data EBaseImage - = EBaseImage - Syntax.Image - (Maybe Syntax.Tag) - (Maybe Syntax.Digest) - (Maybe Syntax.ImageAlias) - (Maybe Syntax.Platform) - deriving (Show, Eq, Ord) - -instance IsString EBaseImage where - fromString s = EBaseImage (fromString s) Nothing Nothing Nothing Nothing - -data EInstruction next - = From - EBaseImage - next - | AddArgs - (NonEmpty Syntax.SourcePath) - Syntax.TargetPath - Syntax.Chown - next - | User - Text - next - | Label - Syntax.Pairs - next - | StopSignal - Text - next - | CopyArgs - (NonEmpty Syntax.SourcePath) - Syntax.TargetPath - Syntax.Chown - Syntax.CopySource - next - | RunArgs - (Syntax.Arguments Text) - Syntax.RunFlags - next - | CmdArgs - (Syntax.Arguments Text) - next - | Shell - (Syntax.Arguments Text) - next - | Workdir - Syntax.Directory - next - | Expose - Syntax.Ports - next - | Volume - Text - next - | EntrypointArgs - (Syntax.Arguments Text) - next - | Maintainer - Text - next - | Env - Syntax.Pairs - next - | Arg - Text - (Maybe Text) - next - | Comment - Text - next - | Healthcheck - (Syntax.Check Text) - next - | OnBuildRaw - (Syntax.Instruction Text) - next - | Embed - [Syntax.InstructionPos Text] - next - deriving (Functor) diff --git a/src/Language/Docker/Syntax/Lift.hs b/src/Language/Docker/Syntax/Lift.hs deleted file mode 100644 index 26ebdca..0000000 --- a/src/Language/Docker/Syntax/Lift.hs +++ /dev/null @@ -1,80 +0,0 @@ -{-# LANGUAGE PolyKinds #-} -{-# LANGUAGE TemplateHaskell #-} - -module Language.Docker.Syntax.Lift where - -import Data.Fixed (Fixed) -import Data.Time.Clock (DiffTime) -import Instances.TH.Lift () -- Defines Lift instances for ByteString and Text - -import Language.Docker.Syntax -import Language.Haskell.TH.Lift -import Language.Haskell.TH.Syntax () - -deriveLift ''Fixed - -deriveLift ''DiffTime - -deriveLift ''Protocol - -deriveLift ''Port - -deriveLift ''Ports - -deriveLift ''Registry - -deriveLift ''Image - -deriveLift ''ImageAlias - -deriveLift ''Tag - -deriveLift ''Digest - -deriveLift ''BaseImage - -deriveLift ''Arguments - -deriveLift ''Instruction - -deriveLift ''InstructionPos - -deriveLift ''SourcePath - -deriveLift ''TargetPath - -deriveLift ''Chown - -deriveLift ''CopySource - -deriveLift ''CopyArgs - -deriveLift ''AddArgs - -deriveLift ''Duration - -deriveLift ''Retries - -deriveLift ''CheckArgs - -deriveLift ''Check - -deriveLift ''BindOpts - -deriveLift ''CacheSharing - -deriveLift ''CacheOpts - -deriveLift ''TmpOpts - -deriveLift ''SecretOpts - -deriveLift ''RunMount - -deriveLift ''RunSecurity - -deriveLift ''RunNetwork - -deriveLift ''RunFlags - -deriveLift ''RunArgs diff --git a/test/Language/Docker/EDSL/QuasiSpec.hs b/test/Language/Docker/EDSL/QuasiSpec.hs deleted file mode 100644 index 311f8a7..0000000 --- a/test/Language/Docker/EDSL/QuasiSpec.hs +++ /dev/null @@ -1,46 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE OverloadedLists #-} -{-# LANGUAGE QuasiQuotes #-} -module Language.Docker.EDSL.QuasiSpec - where - -import Language.Docker.EDSL -import Language.Docker.EDSL.Quasi -import Language.Docker.Syntax -import Test.Hspec - -spec :: Spec -spec = do - describe "dockerfile" $ - it "parses a dockerfile and returns its ast" $ do - let df = map instruction [dockerfile| - FROM node - RUN apt-get update - CMD ["node", "something.js"] - |] - df `shouldBe` [ From (BaseImage "node" Nothing Nothing Nothing Nothing) - , Run "apt-get update" - , Cmd ["node", "something.js"] - ] - - describe "edockerfile" $ - it "lets us use parsed dockerfiles seamlessly in our DSL" $ do - let d = do - from ("node" `aliased` "node-build") - expose (ports [tcpPort 8080, variablePort "PORT"]) - [edockerfile| - RUN apt-get update - CMD node something.js - |] - df = map instruction (toDockerfile d) - df `shouldBe` [ From (BaseImage - { image = "node" - , alias = Just "node-build" - , tag = Nothing - , digest = Nothing - , platform = Nothing} - ) - , Expose (Ports [Port 8080 TCP, PortStr "$PORT"]) - , Run "apt-get update" - , Cmd "node something.js" - ] diff --git a/test/Language/Docker/EDSLSpec.hs b/test/Language/Docker/EDSLSpec.hs deleted file mode 100644 index 515e278..0000000 --- a/test/Language/Docker/EDSLSpec.hs +++ /dev/null @@ -1,137 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE OverloadedLists #-} -module Language.Docker.EDSLSpec where - -import Control.Monad.IO.Class -import Data.List (sort) -import Language.Docker.EDSL -import Language.Docker.PrettyPrint -import qualified Language.Docker.Syntax as Syntax -import System.Directory -import System.FilePath -import System.FilePath.Glob -import Test.Hspec -import qualified Data.Text.Lazy as L -import qualified Data.Text as Text -import Data.Semigroup ((<>)) - -printed :: [L.Text] -> L.Text -printed = L.unlines - -spec :: Spec -spec = do - describe "toDockerfile s" $ - it "allows us to write haskell code that represents Dockerfiles" $ do - let r = map Syntax.instruction $ toDockerfile (do - from "node" - cmdArgs ["node", "-e", "'console.log(\'hey\')'"]) - r `shouldBe` [ Syntax.From $ - Syntax.BaseImage "node" - Nothing - Nothing - Nothing - Nothing - , Syntax.Cmd ["node", "-e", "'console.log(\'hey\')'"] - ] - - describe "prettyPrint $ toDockerfile s" $ do - it "allows us to write haskell code that represents Dockerfiles" $ do - let r = prettyPrint $ toDockerfile (do - from "node" - shell ["cmd", "/S"] - entrypoint ["/tini", "--"] - cmdArgs ["node", "-e", "'console.log(\'hey\')'"] - healthcheck $ check "curl -f http://localhost/ || exit 1" `interval` 300) - r `shouldBe` printed [ "FROM node" - , "SHELL [\"cmd\", \"/S\"]" - , "ENTRYPOINT [\"/tini\", \"--\"]" - , "CMD [\"node\", \"-e\", \"'console.log(\'hey\')'\"]" - , "HEALTHCHECK --interval=300s CMD curl -f http://localhost/ || exit 1" - ] - it "print expose instructions correctly" $ do - let r = prettyPrint $ toDockerfile (do - from "scratch" - expose $ ports [variablePort "PORT", tcpPort 80, udpPort 51] - expose $ ports [portRange 90 100] - expose $ ports [udpPortRange 190 200]) - r `shouldBe` printed [ "FROM scratch" - , "EXPOSE $PORT 80/tcp 51/udp" - , "EXPOSE 90-100" - , "EXPOSE 190-200/udp" - ] - - it "onBuild let's us nest statements" $ do - let r = prettyPrint $ toDockerfile $ do - from "node" - cmdArgs ["node", "-e", "'console.log(\'hey\')'"] - onBuild $ do - run "echo \"hello world\"" - run "echo \"hello world2\"" - r `shouldBe` printed [ "FROM node" - , "CMD [\"node\", \"-e\", \"'console.log(\'hey\')'\"]" - , "ONBUILD RUN echo \"hello world\"" - , "ONBUILD RUN echo \"hello world2\"" - ] - - it "parses and prints from aliases correctly" $ do - let r = prettyPrint $ toDockerfile $ do - from $ "node" `tagged` "10.1" `aliased` "node-build" - run "echo foo" - r `shouldBe` printed [ "FROM node:10.1 AS node-build" - , "RUN echo foo" - ] - - it "parses and prints from with a registry" $ do - let r = prettyPrint $ toDockerfile $ do - from "opensuse/tumbleweed" - run "echo foo" - r `shouldBe` printed [ "FROM opensuse/tumbleweed" - , "RUN echo foo" - ] - - it "parses and prints copy instructions" $ do - let r = prettyPrint $ toDockerfile $ do - from "scratch" - copy $ ["foo.js"] `to` "bar.js" - copy $ ["foo.js", "bar.js"] `to` "." - copy $ ["foo.js", "bar.js"] `to` "baz/" - copy $ ["something"] `to` "crazy" `fromStage` "builder" - copy $ ["this"] `to` "that" `fromStage` "builder" `ownedBy` "www-data" - r `shouldBe` printed [ "FROM scratch" - , "COPY foo.js bar.js" - , "COPY foo.js bar.js ./" - , "COPY foo.js bar.js baz/" - , "COPY --from=builder something crazy" - , "COPY --chown=www-data --from=builder this that" - ] - it "quotes label and env correctly" $ do - let r = prettyPrint $ toDockerfile $ do - from "scratch" - label [("email", "Example ")] - label [("escape", "Escape this\" thing")] - env [("foo", "bar baz")] - env [("double_escape", "escape this \\\"")] - r `shouldBe` printed [ "FROM scratch" - , "LABEL email=\"Example \"" - , "LABEL escape=\"Escape this\\\" thing\"" - , "ENV foo=\"bar baz\"" - , "ENV double_escape=\"escape this \\\"\"" - ] - - describe "toDockerfileTextIO" $ - it "let's us run in the IO monad" $ do - -- TODO - "glob" is a really useful combinator - str <- toDockerfileTextIO $ do - fs <- liftIO $ do - cwd <- getCurrentDirectory - fs <- glob "./test/Language/Docker/*.hs" - return (map (makeRelative cwd) (sort fs)) - from "ubuntu" - let file = Text.pack . takeFileName - mapM_ (\f -> add [Syntax.SourcePath (Text.pack f)] (Syntax.TargetPath $ "/app/" <> file f)) fs - str `shouldBe` printed [ "FROM ubuntu" - , "ADD ./test/Language/Docker/EDSLSpec.hs /app/EDSLSpec.hs" - , "ADD ./test/Language/Docker/ExamplesSpec.hs /app/ExamplesSpec.hs" - , "ADD ./test/Language/Docker/IntegrationSpec.hs /app/IntegrationSpec.hs" - , "ADD ./test/Language/Docker/ParserSpec.hs /app/ParserSpec.hs" - ] diff --git a/test/Language/Docker/ExamplesSpec.hs b/test/Language/Docker/ExamplesSpec.hs deleted file mode 100644 index 805e7a4..0000000 --- a/test/Language/Docker/ExamplesSpec.hs +++ /dev/null @@ -1,20 +0,0 @@ -module Language.Docker.ExamplesSpec where - -import Control.Monad -import Data.Monoid -import System.Directory -import System.FilePath -import System.FilePath.Glob -import System.Process -import Test.Hspec - -stackRunGhc e = callProcess "stack" ["runghc", "--package", "language-docker", e] - -spec :: Spec -spec = do - cwd <- runIO getCurrentDirectory - exampleSources <- runIO $ glob "./examples/*.hs" - forM_ exampleSources $ \exampleSource -> do - let exampleSource' = makeRelative cwd exampleSource - describe exampleSource $ it ("stack runghc " <> exampleSource') $ - stackRunGhc exampleSource From 05c1b8c39c97fa1041f1604ba26476b267e136fa Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Mon, 1 Jun 2020 23:53:01 +0200 Subject: [PATCH 12/15] making the test output more useful --- Dockerfile | 3 - README.md | 97 ------------------------- test/Language/Docker/IntegrationSpec.hs | 6 +- 3 files changed, 4 insertions(+), 102 deletions(-) delete mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index e8d8491..0000000 --- a/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM haskell:8 -RUN cabal update -RUN cabal install language-docker diff --git a/README.md b/README.md index 48b2ca6..1c324a2 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,6 @@ writting Dockerfiles in Haskell. - [Parsing files](#parsing-files) - [Parsing strings](#parsing-strings) - [Pretty-printing files](#pretty-printing-files) -- [Writing Dockerfiles in Haskell](#writing-dockerfiles-in-haskell) -- [Using the QuasiQuoter](#using-the-quasiquoter) -- [Templating Dockerfiles in Haskell](#templating-dockerfiles-in-haskell) -- [Using IO in the DSL](#using-io-in-the-dsl) ## Parsing files @@ -35,99 +31,6 @@ main = do print (parseString c) ``` -## Pretty-printing files - -```haskell -import Language.Docker -main = do - Right d <- parseFile "./Dockerfile" - putStr (prettyPrint d) -``` - -## Writing Dockerfiles in Haskell - -```haskell -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE OverloadedLists #-} -import Language.Docker - -main = putDockerfileStr $ do - from "node" - run "apt-get update" - run ["apt-get", "install", "something"] - -- ... -``` - -## Using the QuasiQuoter - -```haskell -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE QuasiQuotes #-} -import Language.Docker -main = putDockerfileStr $ do - from "node" - run "apt-get update" - [edockerfile| - RUN apt-get update - CMD node something.js - |] - -- ... -``` - -## Templating Dockerfiles in Haskell - -```haskell -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE OverloadedLists #-} -import Control.Monad -import Language.Docker -import Data.String (fromString) -import qualified Data.Text.Lazy.IO as L - -tags = ["7.8", "7.10", "8"] -cabalSandboxBuild packageName = do - let cabalFile = packageName ++ ".cabal" - run "cabal sandbox init" - run "cabal update" - add [fromString cabalFile] (fromString $ "/app/" ++ cabalFile) - run "cabal install --only-dep -j" - add "." "/app/" - run "cabal build" -main = - forM_ tags $ \tag -> do - let df = toDockerfileText $ do - from ("haskell" `tagged` tag) - cabalSandboxBuild "mypackage" - L.writeFile ("./examples/templating-" ++ tag ++ ".dockerfile") df -``` - -## Using IO in the DSL -By default the DSL runs in the `Identity` monad. By running in IO we can -support more features like file globbing: - -```haskell -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE OverloadedLists #-} -import Language.Docker -import qualified System.Directory as Directory -import qualified System.FilePath as FilePath -import qualified System.FilePath.Glob as Glob -import Data.List.NonEmpty (fromList) -import qualified Data.Text.Lazy.IO as L - -main = do - str <- toDockerfileTextIO $ do - fs <- liftIO $ do - cwd <- Directory.getCurrentDirectory - fs <- Glob.glob "./test/*.hs" - let relativeFiles = map (FilePath.makeRelative cwd) fs - return (fromList relativeFiles) - from "ubuntu" - copy $ (toSources fs) `to` "/app/" - L.putStr str -``` - [hackage-img]: https://img.shields.io/hackage/v/language-docker.svg [hackage]: https://hackage.haskell.org/package/language-docker [travis-img]: https://travis-ci.org/hadolint/language-docker.svg?branch=master diff --git a/test/Language/Docker/IntegrationSpec.hs b/test/Language/Docker/IntegrationSpec.hs index 1a05a3e..ec3f7b7 100644 --- a/test/Language/Docker/IntegrationSpec.hs +++ b/test/Language/Docker/IntegrationSpec.hs @@ -4,6 +4,8 @@ module Language.Docker.IntegrationSpec where import Language.Docker.Parser import Language.Docker.Syntax +import Language.Docker.PrettyPrint (prettyPrint) +import qualified Data.Text.Lazy.IO as L import Test.HUnit hiding (Label) @@ -17,11 +19,11 @@ spec = do it "no erors" $ do parsed <- parseFile "test/fixtures/1.Dockerfile" case parsed of - Right a -> print a + Right a -> L.putStr $ prettyPrint a Left err -> assertFailure $ errorBundlePretty err describe "2" $ do it "no erors" $ do parsed <- parseFile "test/fixtures/2.Dockerfile" case parsed of - Right a -> print a + Right a -> L.putStr $ prettyPrint a Left err -> assertFailure $ errorBundlePretty err From 79939add6c77640d9717febe286dee3b533b55ed Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Tue, 2 Jun 2020 00:05:20 +0200 Subject: [PATCH 13/15] Link to dockerfile-creator --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 1c324a2..db6449a 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,10 @@ main = do print (parseString c) ``` +## Create Dockerfiles + +Use the [dockerfile-creator package](https://github.com/hadolint/dockerfile-creator) + [hackage-img]: https://img.shields.io/hackage/v/language-docker.svg [hackage]: https://hackage.haskell.org/package/language-docker [travis-img]: https://travis-ci.org/hadolint/language-docker.svg?branch=master From 4ed3264cbbfb125392c5b70191a3a4e9bbcac353 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Tue, 2 Jun 2020 00:14:47 +0200 Subject: [PATCH 14/15] Remove OSX build to save time Since this is a pure library, nothing wiill be found on OSX of interest --- .travis.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index de7b650..7114c1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,13 +20,6 @@ matrix: # Nightly builds are allowed to fail - env: ARGS="--resolver nightly" - # Build on OS X in addition to Linux - - env: ARGS="--resolver lts" - os: osx - - - env: ARGS="--resolver nightly" - os: osx - - env: - PURPOSE="Integration tests" - ARGS="" From b16e62df236309961b7a6cf84c972f25e38cadb4 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Tue, 2 Jun 2020 10:32:43 +0200 Subject: [PATCH 15/15] small change in package description --- language-docker.cabal | 4 ++-- package.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/language-docker.cabal b/language-docker.cabal index 0f18481..fa33a05 100644 --- a/language-docker.cabal +++ b/language-docker.cabal @@ -4,12 +4,12 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 005937a918829cd3728fed743544eec119cc21efef9ed052db7c971ecfcc16f0 +-- hash: 4064f903443322af4bc07da09abc22ee61ae143bfcffb403add7b327cd7b13dd name: language-docker version: 9.0.0 synopsis: Dockerfile parser, pretty-printer and embedded DSL -description: All functions for parsing, printing and writting Dockerfiles are exported through @Language.Docker@. For more fine-grained operations look for specific modules that implement a certain functionality. +description: All functions for parsing and pretty-printing Dockerfiles are exported through @Language.Docker@. For more fine-grained operations look for specific modules that implement a certain functionality. See the for the source-code and examples. category: Development homepage: https://github.com/hadolint/language-docker#readme diff --git a/package.yaml b/package.yaml index 4154dfe..cc10d02 100644 --- a/package.yaml +++ b/package.yaml @@ -1,7 +1,7 @@ name: language-docker version: '9.0.0' synopsis: Dockerfile parser, pretty-printer and embedded DSL -description: 'All functions for parsing, printing and writting Dockerfiles are +description: 'All functions for parsing and pretty-printing Dockerfiles are exported through @Language.Docker@. For more fine-grained operations look for specific modules that implement a certain functionality.