├── .gitattributes ├── .gitignore ├── README.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── integration-test-groovy ├── Dockerfile ├── build.gradle └── settings.gradle ├── integration-test-kotlin ├── Dockerfile ├── build.gradle.kts └── settings.gradle.kts ├── plugin ├── build.gradle.kts └── src │ ├── functionalTest │ └── kotlin │ │ └── io │ │ └── github │ │ └── dogacel │ │ └── dsl │ │ └── dockerfile │ │ ├── KotlinDockerfileDslPluginGroovyFunctionalTest.kt │ │ └── KotlinDockerfileDslPluginKotlinFunctionalTest.kt │ ├── main │ └── kotlin │ │ └── io │ │ └── github │ │ └── dogacel │ │ └── dsl │ │ └── dockerfile │ │ ├── DockerfileDsl.kt │ │ └── DockerfileDslPlugin.kt │ └── test │ └── kotlin │ └── io │ └── github │ └── dogacel │ └── dsl │ └── dockerfile │ ├── DockerfileDslPluginTest.kt │ └── DockerfileDslTest.kt └── settings.gradle.kts /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf 9 | 10 | # Binary files should be left untouched 11 | *.jar binary 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | 7 | .idea 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dockerfile Kotlin DSL 2 | 3 | Streamline docker image building process for _Gradle_ based build systems with extended configuration options with 4 | minimal external scripting. 5 | 6 | This gradle plugin allows you to define your Dockerfile using Kotlin DSL inside your gradle configuration 7 | 8 | ## Usage 9 | 10 | ```kotlin 11 | 12 | plugins { 13 | id("io.github.dogacel:dockerfile-kotlin-dsl") version "0.0.1" 14 | } 15 | 16 | // Define a root dockerfile named `Dockerfile`. 17 | dockerfile { 18 | from("openjdk:21-jdk-slim") 19 | workdir("/app") 20 | 21 | +"Copy the JAR file into the Docker Image" 22 | copy { 23 | source = "app.jar" 24 | destination = "/app/app.jar" 25 | } 26 | 27 | +"Download assets" 28 | val assetPath = when (System.getenv("MODE")) { 29 | "local" -> "/first_100_assets.zip" 30 | "staging" -> "/compressed_assets.zip" 31 | else -> "/full_assets.zip" 32 | } 33 | 34 | run("curl", "-o", "assets.zip", "https://example.com/" + assetPath) 35 | run("unzip", "assets.zip", "-d", "/app/assets", "&&", "rm", "assets.zip") 36 | 37 | when (System.getenv("MODE")) { 38 | "local" -> expose(80) 39 | else -> {} 40 | } 41 | 42 | expose(443) 43 | 44 | cmd("java", "-jar", "app.jar") 45 | } 46 | ``` 47 | 48 | To create a dockerfile, run `./gradlew dockerfileGenerate`. You can pass a `--name=Test.dockerfile` parameter to specify 49 | the file name. The file will be created under your project root. 50 | 51 | ## Why? 52 | 53 | Regular `Dockerfile`s do not allow configuring image build process. Multiple docker images might built to serve 54 | different purposes, such as minimal test environment images, local images, integration test images, performance tweaked 55 | load test images, production ready images, localized images for different audiences etc. 56 | 57 | ## Example 58 | 59 | Let's say we are creating a microservice that serves some static metadata for our _Book Store_ mobile application. 60 | Regularly, a person would create a single Dockerfile for their specific needs or they might create multiple dockerfiles 61 | with a lot of duplication to customize for their specific needs. 62 | 63 | ```dockerfile 64 | # Use the official OpenJDK 21 image from Docker Hub 65 | FROM openjdk:21-jdk-slim 66 | 67 | # Set the working directory inside the container 68 | WORKDIR /app 69 | 70 | # Copy the JAR file into the Docker image 71 | COPY app.jar /app/app.jar 72 | 73 | # Download assets 74 | RUN curl -o assets.zip https://example.com/path/to/assets.zip 75 | RUN unzip assets.zip -d /app/assets && rm assets.zip 76 | 77 | EXPOSE 443 78 | 79 | # Specify the command to run the JAR file 80 | CMD ["java", "-jar", "app.jar"] 81 | ``` 82 | 83 | There are some things to consider: 84 | 85 | - Assets might be too big for the testing environment. We can use a smaller subset of assets or compressed assets for 86 | non-prod environments. 87 | - TLS is hard to configure for local environments. 88 | - Environment variables can be set to change logging behavior in test / prod environments. 89 | 90 | It is possible to fix those issues by distributing different Dockerfiles for each environment, however, over time 91 | entropy will take over and those Dockerfiles will start to deviate each other and will cause maintenance overhead due to 92 | high levels of code duplication. 93 | 94 | Let's create a _Dockerfile_ definition in our `build.gradle.kts`, 95 | 96 | ```kotlin 97 | dockerfile { 98 | from("openjdk:21-jdk-slim") 99 | workdir("/app") 100 | 101 | +"Copy the JAR file into the Docker Image" 102 | copy { 103 | source = "app.jar" 104 | destination = "/app/app.jar" 105 | } 106 | 107 | cmd("java", "-jar", "app.jar") 108 | } 109 | ``` 110 | 111 | Let's download the right assets for our environment. 112 | 113 | ```kotlin 114 | dockerfile { 115 | ... 116 | +"Download assets" 117 | val assetPath = 118 | when (System.getenv("MODE")) { 119 | "local" -> "/first_100_assets.zip" 120 | "staging" -> "/compressed_assets.zip" 121 | else -> "/full_assets.zip" 122 | } 123 | 124 | run("curl", "-o", "assets.zip", "https://example.com/" + assetPath) 125 | run("unzip", "assets.zip", "-d", "/app/assets", "&&", "rm", "assets.zip") 126 | } 127 | ``` 128 | 129 | Now, let's enable HTTP endpoint for our local environment so we don't need to setup TLS. 130 | 131 | ```kotlin 132 | dockerfile { 133 | ... 134 | when (System.getenv("MODE")) { 135 | "local" -> expose(80) 136 | else -> {} 137 | } 138 | 139 | expose(443) 140 | } 141 | ``` 142 | 143 | Let's enable gRPC reflection & documentation service in our local environment. 144 | 145 | ```kotlin 146 | dockerfile { 147 | ... 148 | // Enable dev tools 149 | when (System.getenv("MODE")) { 150 | "local" -> { 151 | env("ENABLE_GRPC_REFLECTION", "true") 152 | env("ENABLE_DOC_SERVICE", "true") 153 | } 154 | else -> {} 155 | } 156 | } 157 | ``` 158 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dogacel/dockerfile-kotlin-dsl/44fbb85e03abd210f30c9bbcd6fe01361c1840fc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /integration-test-groovy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:21-jdk-slim 2 | WORKDIR /app 3 | # Copy the JAR file into the Docker Image 4 | COPY app.jar /app/app.jar 5 | # Download assets 6 | RUN curl -o assets.zip https://example.com//full_assets.zip 7 | RUN unzip assets.zip -d /app/assets && rm assets.zip 8 | EXPOSE 443/tcp 9 | CMD java -jar app.jar -------------------------------------------------------------------------------- /integration-test-groovy/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.jvm") version "1.9.23" 3 | 4 | id("io.github.dogacel.dsl.dockerfile") 5 | } 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | dockerfile { 12 | from("openjdk:21-jdk-slim") 13 | workdir("/app") 14 | 15 | comment("Copy the JAR file into the Docker Image") 16 | copy { 17 | source = "app.jar" 18 | destination = "/app/app.jar" 19 | } 20 | 21 | comment("Download assets") 22 | 23 | def assetPath = "full_assets.zip" 24 | switch (System.env["MODE"]) { 25 | case "local": 26 | assetPath = "first_100_assets.zip" 27 | break 28 | 29 | case "staging": 30 | assetPath = "compressed_assets.zip" 31 | break 32 | } 33 | 34 | run("curl", "-o", "assets.zip", "https://example.com/$assetPath") 35 | run("unzip", "assets.zip", "-d", "/app/assets", "&&", "rm", "assets.zip") 36 | 37 | switch (System.env["MODE"]) { 38 | case "local": 39 | expose(80) 40 | break 41 | } 42 | 43 | expose(443) 44 | 45 | cmd("java", "-jar", "app.jar") 46 | } 47 | -------------------------------------------------------------------------------- /integration-test-groovy/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | includeBuild("../plugin") 3 | } 4 | -------------------------------------------------------------------------------- /integration-test-kotlin/Dockerfile: -------------------------------------------------------------------------------- 1 | ADD build/libs/*.jar --keep-git-dir --checksum=sha256:1234567890 --chown=root:root --chmod=755 --link /jars/ 2 | ARG MODE 3 | ARG JAR_FILE=app.jar 4 | CMD java -jar ${JAR_FILE} 5 | COPY src/main/resources --from=builder --chown=root:root --chmod=755 --link --parents /resources/ 6 | ENTRYPOINT java -jar ${JAR_FILE} 7 | ENV JAVA_OPTS=-Xmx512m 8 | EXPOSE 80/tcp 9 | EXPOSE 8080/udp 10 | FROM openjdk:8-jdk-alpine 11 | FROM --platform=linux/amd64 openjdk:8-jdk-alpine AS builder 12 | HEALTHCHECK --interval=10s --timeout=5s --start-period=0s --start-interval=2s --retries=5 CMD curl -f http://localhost:8080/health 13 | LABEL foo=bar 14 | MAINTAINER John Doe 15 | RUN apk add curl 16 | RUN echo "Hello, World!" 17 | SHELL ["/bin/bash", "-x"] 18 | STOPSIGNAL SIGTERM 19 | USER root 20 | VOLUME /data /logs 21 | WORKDIR /app -------------------------------------------------------------------------------- /integration-test-kotlin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import io.github.dogacel.dsl.dockerfile.Expose.Protocol.UDP 2 | import kotlin.time.Duration.Companion.seconds 3 | 4 | plugins { 5 | kotlin("jvm") version "1.9.23" 6 | 7 | id("io.github.dogacel.dsl.dockerfile") 8 | } 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | dockerfile { 15 | add { 16 | source = "build/libs/*.jar" 17 | destination = "/jars/" 18 | keepGitDir = true 19 | checksum = "sha256:1234567890" 20 | chown = "root:root" 21 | chmod = "755" 22 | link = true 23 | exclude("build/libs/*.jar") 24 | } 25 | 26 | arg("MODE") 27 | arg("JAR_FILE", "app.jar") 28 | 29 | cmd("java", "-jar", "\${JAR_FILE}") 30 | 31 | copy { 32 | source = "src/main/resources" 33 | destination = "/resources/" 34 | from = "builder" 35 | chown = "root:root" 36 | chmod = "755" 37 | link = true 38 | parents = true 39 | exclude("src/main/resources/*.properties") 40 | } 41 | 42 | entryPoint("java", "-jar", "\${JAR_FILE}") 43 | 44 | env("JAVA_OPTS", "-Xmx512m") 45 | 46 | expose(80) 47 | expose(8080, UDP) 48 | 49 | from("openjdk:8-jdk-alpine") 50 | from("openjdk:8-jdk-alpine", "linux/amd64", "builder") 51 | 52 | healthcheck { 53 | interval = 10.seconds 54 | timeout = 5.seconds 55 | startPeriod = 0.seconds 56 | startInterval = 2.seconds 57 | retries = 5 58 | cmd("curl", "-f", "http://localhost:8080/health") 59 | } 60 | 61 | label("foo", "bar") 62 | 63 | maintainer("John Doe ") 64 | 65 | onBuild { 66 | add { 67 | source = "build/libs/*.jar" 68 | destination = "/app.jar" 69 | } 70 | } 71 | 72 | run("apk", "add", "curl") 73 | run("echo", "\"Hello, World!\"") 74 | 75 | shell("/bin/bash", "-x") 76 | 77 | stopSignal("SIGTERM") 78 | 79 | user("root") 80 | 81 | volume("/data", "/logs") 82 | 83 | workdir("/app") 84 | } 85 | -------------------------------------------------------------------------------- /integration-test-kotlin/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | includeBuild("../plugin") 3 | } 4 | -------------------------------------------------------------------------------- /plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | // Apply the Java Gradle plugin development plugin to add support for developing Gradle plugins 3 | `java-gradle-plugin` 4 | 5 | // Apply the Kotlin JVM plugin to add support for Kotlin. 6 | kotlin("jvm") version "1.9.23" 7 | id("com.gradle.plugin-publish") version "1.2.1" 8 | } 9 | 10 | group = "io.github.dogacel" 11 | version = "0.0.1" 12 | 13 | gradlePlugin { 14 | website = "https://github.com/Dogacel/dockerfile-kotlin-dsl" 15 | vcsUrl = "https://github.com/Dogacel/dockerfile-kotlin-dsl.git" 16 | plugins { 17 | create("dockerfileKotlinDslPlugin") { 18 | id = "io.github.dogacel.dsl.dockerfile" 19 | displayName = "Dockerfile Kotlin DSL" 20 | description = "Streamlined Dockerfile generation using Kotlin DSL inside Gradle build scripts." 21 | tags = listOf("docker", "dockerfile", "kotlin", "dsl", "build", "config") 22 | implementationClass = "io.github.dogacel.dsl.dockerfile.DockerfileDslPlugin" 23 | } 24 | } 25 | } 26 | 27 | repositories { 28 | // Use Maven Central for resolving dependencies. 29 | mavenCentral() 30 | } 31 | 32 | dependencies { 33 | // Use the Kotlin JUnit 5 integration. 34 | testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") 35 | 36 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 37 | } 38 | 39 | // Add a source set for the functional test suite 40 | val functionalTestSourceSet = 41 | sourceSets.create("functionalTest") { 42 | } 43 | 44 | configurations["functionalTestImplementation"].extendsFrom(configurations["testImplementation"]) 45 | configurations["functionalTestRuntimeOnly"].extendsFrom(configurations["testRuntimeOnly"]) 46 | 47 | // Add a task to run the functional tests 48 | val functionalTest by tasks.registering(Test::class) { 49 | testClassesDirs = functionalTestSourceSet.output.classesDirs 50 | classpath = functionalTestSourceSet.runtimeClasspath 51 | useJUnitPlatform() 52 | } 53 | 54 | gradlePlugin.testSourceSets.add(functionalTestSourceSet) 55 | 56 | tasks.named("check") { 57 | // Run the functional tests as part of `check` 58 | dependsOn(functionalTest) 59 | } 60 | 61 | tasks.named("test") { 62 | // Use JUnit Jupiter for unit tests. 63 | useJUnitPlatform() 64 | } 65 | -------------------------------------------------------------------------------- /plugin/src/functionalTest/kotlin/io/github/dogacel/dsl/dockerfile/KotlinDockerfileDslPluginGroovyFunctionalTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.dogacel.dsl.dockerfile 2 | 3 | import org.gradle.testkit.runner.GradleRunner 4 | import org.junit.jupiter.api.io.TempDir 5 | import java.io.File 6 | import kotlin.test.Test 7 | import kotlin.test.assertTrue 8 | 9 | class KotlinDockerfileDslPluginGroovyFunctionalTest { 10 | @field:TempDir 11 | lateinit var projectDir: File 12 | 13 | private val buildFile by lazy { projectDir.resolve("build.gradle") } 14 | private val settingsFile by lazy { projectDir.resolve("settings.gradle") } 15 | 16 | @Test 17 | fun `can run kts`() { 18 | // Set up the test build 19 | settingsFile.writeText("") 20 | buildFile.writeText( 21 | """ 22 | plugins { 23 | id("io.github.dogacel.dsl.dockerfile") 24 | } 25 | 26 | dockerfiles { 27 | dockerfile { 28 | from("openjdk:8-jdk-alpine") 29 | } 30 | } 31 | """.trimIndent(), 32 | ) 33 | 34 | // Run the build 35 | val runner = GradleRunner.create() 36 | runner.forwardOutput() 37 | runner.withPluginClasspath() 38 | runner.withArguments("dockerfileGenerate") 39 | runner.withProjectDir(projectDir) 40 | val result = runner.build() 41 | 42 | // Verify the result 43 | assertTrue(result.output.contains("FROM openjdk:8-jdk-alpine")) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /plugin/src/functionalTest/kotlin/io/github/dogacel/dsl/dockerfile/KotlinDockerfileDslPluginKotlinFunctionalTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.dogacel.dsl.dockerfile 2 | 3 | import org.gradle.testkit.runner.GradleRunner 4 | import org.junit.jupiter.api.io.TempDir 5 | import java.io.File 6 | import kotlin.test.Test 7 | import kotlin.test.assertTrue 8 | 9 | class KotlinDockerfileDslPluginKotlinFunctionalTest { 10 | @field:TempDir 11 | lateinit var projectDir: File 12 | 13 | private val buildFile by lazy { projectDir.resolve("build.gradle.kts") } 14 | private val settingsFile by lazy { projectDir.resolve("settings.gradle.kts") } 15 | 16 | @Test 17 | fun `can run kts`() { 18 | // Set up the test build 19 | settingsFile.writeText("") 20 | buildFile.writeText( 21 | """ 22 | plugins { 23 | id("io.github.dogacel.dsl.dockerfile") 24 | } 25 | 26 | dockerfiles { 27 | dockerfile { 28 | from("openjdk:8-jdk-alpine") 29 | } 30 | } 31 | """.trimIndent(), 32 | ) 33 | 34 | // Run the build 35 | val runner = GradleRunner.create() 36 | runner.forwardOutput() 37 | runner.withPluginClasspath() 38 | runner.withArguments("dockerfileGenerate") 39 | runner.withProjectDir(projectDir) 40 | val result = runner.build() 41 | 42 | // Verify the result 43 | assertTrue(result.output.contains("FROM openjdk:8-jdk-alpine")) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/io/github/dogacel/dsl/dockerfile/DockerfileDsl.kt: -------------------------------------------------------------------------------- 1 | package io.github.dogacel.dsl.dockerfile 2 | 3 | import io.github.dogacel.dsl.dockerfile.Expose.Protocol 4 | import io.github.dogacel.dsl.dockerfile.Expose.Protocol.TCP 5 | import java.util.Locale 6 | import kotlin.time.Duration 7 | import kotlin.time.Duration.Companion.seconds 8 | 9 | @DslMarker 10 | annotation class DockerfileDsl 11 | 12 | @DockerfileDsl 13 | sealed class DockerfileStep( 14 | val markerName: String, 15 | ) { 16 | abstract fun getArgs(): List 17 | 18 | override fun toString(): String = "$markerName ${getArgs().joinToString(" ")}" 19 | } 20 | 21 | // Based on https://docs.docker.com/engine/reference/builder 22 | 23 | class Add : DockerfileStep("ADD") { 24 | var source: String? = null 25 | var sources: List = emptyList() 26 | var destination: String = "" 27 | 28 | var keepGitDir: Boolean = false 29 | var checksum: String? = null 30 | var chown: String? = null 31 | var chmod: String? = null 32 | var link: Boolean = false 33 | 34 | private var exclusionList: MutableList = mutableListOf() 35 | 36 | fun exclude(value: String) { 37 | exclusionList.add(value) 38 | } 39 | 40 | override fun getArgs(): List { 41 | val list = mutableListOf() 42 | 43 | list += listOfNotNull(source) 44 | list += sources 45 | 46 | if (keepGitDir) { 47 | list.add("--keep-git-dir") 48 | } 49 | 50 | if (checksum != null) { 51 | list.add("--checksum=$checksum") 52 | } 53 | 54 | if (chown != null) { 55 | list.add("--chown=$chown") 56 | } 57 | 58 | if (chmod != null) { 59 | list.add("--chmod=$chmod") 60 | } 61 | 62 | if (link) { 63 | list.add("--link") 64 | } 65 | 66 | list += destination 67 | 68 | return list 69 | } 70 | } 71 | 72 | class Arg : DockerfileStep("ARG") { 73 | var name: String = "" 74 | var value: String? = null 75 | 76 | override fun getArgs(): List { 77 | if (value == null) { 78 | return listOf(name) 79 | } 80 | 81 | return listOf("$name=$value") 82 | } 83 | } 84 | 85 | class Cmd : DockerfileStep("CMD") { 86 | var params: List = emptyList() 87 | 88 | override fun getArgs(): List = params 89 | } 90 | 91 | class Copy : DockerfileStep("COPY") { 92 | var source: String? = null 93 | var sources: List = emptyList() 94 | var destination: String = "" 95 | 96 | var from: String? = null 97 | var chown: String? = null 98 | var chmod: String? = null 99 | var link: Boolean = false 100 | var parents: Boolean = false 101 | 102 | private var exclusionList: MutableList = mutableListOf() 103 | 104 | fun exclude(value: String) { 105 | exclusionList.add(value) 106 | } 107 | 108 | override fun getArgs(): List { 109 | val list = mutableListOf() 110 | 111 | list += listOfNotNull(source) 112 | list += sources 113 | 114 | if (from != null) { 115 | list.add("--from=$from") 116 | } 117 | 118 | if (chown != null) { 119 | list.add("--chown=$chown") 120 | } 121 | 122 | if (chmod != null) { 123 | list.add("--chmod=$chmod") 124 | } 125 | 126 | if (link) { 127 | list.add("--link") 128 | } 129 | 130 | if (parents) { 131 | list.add("--parents") 132 | } 133 | 134 | list += destination 135 | 136 | return list 137 | } 138 | } 139 | 140 | class EntryPoint : DockerfileStep("ENTRYPOINT") { 141 | var params: List = emptyList() 142 | 143 | override fun getArgs(): List = params 144 | } 145 | 146 | class Env : DockerfileStep("ENV") { 147 | var name: String = "" 148 | var value: String = "" 149 | 150 | override fun getArgs(): List = listOf("$name=$value") 151 | } 152 | 153 | class Expose : DockerfileStep("EXPOSE") { 154 | enum class Protocol { 155 | TCP, 156 | UDP, 157 | } 158 | 159 | var port: Int = 0 160 | var protocol: Protocol = TCP 161 | 162 | override fun getArgs(): List = listOf("$port/${protocol.name.lowercase(Locale.US)}") 163 | } 164 | 165 | class From : DockerfileStep("FROM") { 166 | var platform: String? = null 167 | var image: String = "" 168 | var `as`: String? = null 169 | 170 | override fun getArgs(): List { 171 | val args = mutableListOf() 172 | 173 | if (platform != null) { 174 | args.add("--platform=$platform") 175 | } 176 | 177 | args.add(image) 178 | 179 | if (`as` != null) { 180 | args.add("AS") 181 | args.add(`as`!!) 182 | } 183 | 184 | return args 185 | } 186 | } 187 | 188 | class Healthcheck : DockerfileStep("HEALTHCHECK") { 189 | var interval: Duration = 30.seconds 190 | var timeout: Duration = 30.seconds 191 | var startPeriod: Duration = 0.seconds 192 | var startInterval: Duration = 5.seconds 193 | var retries: Int = 3 194 | 195 | private var __cmd = mutableListOf() 196 | 197 | fun cmd(vararg params: String) = __cmd.addAll(params) 198 | 199 | override fun getArgs(): List { 200 | val args = mutableListOf() 201 | 202 | args.add("--interval=${interval.inWholeSeconds}s") 203 | args.add("--timeout=${timeout.inWholeSeconds}s") 204 | args.add("--start-period=${startPeriod.inWholeSeconds}s") 205 | args.add("--start-interval=${startInterval.inWholeSeconds}s") 206 | args.add("--retries=$retries") 207 | 208 | args.add("CMD") 209 | args.addAll(__cmd) 210 | 211 | return args 212 | } 213 | } 214 | 215 | class Label : DockerfileStep("LABEL") { 216 | var labels: MutableList> = mutableListOf() 217 | 218 | override fun getArgs(): List = labels.map { (name, value) -> "$name=$value" } 219 | } 220 | 221 | class Maintainer : DockerfileStep("MAINTAINER") { 222 | var name: String = "" 223 | 224 | override fun getArgs(): List = listOf(name) 225 | } 226 | 227 | class Run : DockerfileStep("RUN") { 228 | var commands: List = emptyList() 229 | // TODO: Options 230 | 231 | override fun getArgs(): List = commands 232 | } 233 | 234 | class Shell : DockerfileStep("SHELL") { 235 | var commands: List = emptyList() 236 | 237 | override fun getArgs(): List = commands 238 | 239 | override fun toString(): String = """SHELL [${commands.joinToString(", ") { '"' + it + '"' }}]""" 240 | } 241 | 242 | class StopSignal : DockerfileStep("STOPSIGNAL") { 243 | var value: String = "" 244 | 245 | override fun getArgs(): List = listOf(value) 246 | } 247 | 248 | class User : DockerfileStep("USER") { 249 | var value: String = "" 250 | 251 | override fun getArgs(): List = listOf(value) 252 | } 253 | 254 | class Volume : DockerfileStep("VOLUME") { 255 | var volumes: List = emptyList() 256 | 257 | override fun getArgs(): List = volumes 258 | } 259 | 260 | class Workdir : DockerfileStep("WORKDIR") { 261 | var path: String = "" 262 | 263 | override fun getArgs(): List = listOf(path) 264 | } 265 | 266 | // Extensions 267 | 268 | class Comment : DockerfileStep("#") { 269 | var comment: String = "" 270 | 271 | override fun getArgs(): List = listOf(comment) 272 | } 273 | 274 | class Space : DockerfileStep("") { 275 | override fun getArgs(): List = emptyList() 276 | } 277 | 278 | @DockerfileDsl 279 | open class Dockerfile { 280 | var name = "Dockerfile" 281 | 282 | private var steps: MutableList = mutableListOf() 283 | 284 | // Begin base commands 285 | 286 | fun add(init: Add.() -> Unit) = steps.add(Add().apply(init)) 287 | 288 | fun add(closure: groovy.lang.Closure) = 289 | add { 290 | closure.delegate = this 291 | closure.call() 292 | } 293 | 294 | infix fun arg(name: String) = steps.add(Arg().apply { this.name = name }) 295 | 296 | fun arg( 297 | name: String, 298 | value: String, 299 | ) = steps.add( 300 | Arg().apply { 301 | this.name = name 302 | this.value = value 303 | }, 304 | ) 305 | 306 | fun cmd(vararg params: String) = steps.add(Cmd().apply { this.params = params.toList() }) 307 | 308 | fun copy(init: Copy.() -> Unit) = steps.add(Copy().apply(init)) 309 | 310 | fun copy(closure: groovy.lang.Closure) = 311 | copy { 312 | closure.delegate = this 313 | closure.call() 314 | } 315 | 316 | fun entryPoint(vararg params: String) = steps.add(EntryPoint().apply { this.params = params.toList() }) 317 | 318 | fun env( 319 | name: String, 320 | value: String, 321 | ) = steps.add( 322 | Env().apply { 323 | this.name = name 324 | this.value = value 325 | }, 326 | ) 327 | 328 | fun expose(port: Int) = expose(port, TCP) 329 | 330 | fun expose( 331 | port: Int, 332 | protocol: Protocol, 333 | ) = steps.add( 334 | Expose().apply { 335 | this.port = port 336 | this.protocol = protocol 337 | }, 338 | ) 339 | 340 | fun from(image: String) = from(image, null, null) 341 | 342 | fun from( 343 | image: String, 344 | platform: String? = null, 345 | `as`: String? = null, 346 | ) = steps.add( 347 | From().apply { 348 | this.image = image 349 | this.platform = platform 350 | this.`as` = `as` 351 | }, 352 | ) 353 | 354 | fun healthcheck(init: Healthcheck.() -> Unit) = steps.add(Healthcheck().apply(init)) 355 | 356 | fun healthcheck(closure: groovy.lang.Closure) = 357 | healthcheck { 358 | closure.delegate = this 359 | closure.call() 360 | } 361 | 362 | fun label( 363 | name: String, 364 | value: String, 365 | ) = steps.add(Label().apply { labels.add(name to value) }) 366 | 367 | fun maintainer(value: String) = steps.add(Maintainer().apply { this.name = value }) 368 | 369 | fun onBuild(init: Dockerfile.() -> Unit) = Dockerfile().apply(init) 370 | 371 | fun onBuild(closure: groovy.lang.Closure) = 372 | onBuild { 373 | closure.delegate = this 374 | closure.call() 375 | } 376 | 377 | fun run(vararg commands: String) = steps.add(Run().apply { this.commands = commands.toList() }) 378 | 379 | fun shell(vararg commands: String) = steps.add(Shell().apply { this.commands = commands.toList() }) 380 | 381 | fun stopSignal(value: String) = steps.add(StopSignal().apply { this.value = value }) 382 | 383 | fun user(value: String) = steps.add(User().apply { this.value = value }) 384 | 385 | fun volume(vararg volumes: String) = steps.add(Volume().apply { this.volumes = volumes.toList() }) 386 | 387 | fun workdir(value: String) = steps.add(Workdir().apply { this.path = value }) 388 | 389 | // End base commands 390 | // Begin extensions 391 | 392 | // Add comments 393 | 394 | fun comment(value: String) = steps.add(Comment().apply { this.comment = value }) 395 | 396 | operator fun String.unaryPlus() = comment(this) 397 | 398 | // Add spaces 399 | fun lines(count: Int) { 400 | repeat(count) { 401 | steps.add(Space()) 402 | } 403 | } 404 | // End extensions 405 | 406 | fun parse(): List = steps.map { it.toString() } 407 | } 408 | 409 | internal fun dockerfile(init: Dockerfile.() -> Unit) = Dockerfile().apply(init) 410 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/io/github/dogacel/dsl/dockerfile/DockerfileDslPlugin.kt: -------------------------------------------------------------------------------- 1 | package io.github.dogacel.dsl.dockerfile 2 | 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.Plugin 5 | import org.gradle.api.Project 6 | import org.gradle.api.tasks.Input 7 | import org.gradle.api.tasks.TaskAction 8 | import org.gradle.api.tasks.options.Option 9 | 10 | class DockerfileDslPlugin : Plugin { 11 | override fun apply(project: Project) { 12 | val extension = project.extensions.create("dockerfile", Dockerfile::class.java) 13 | 14 | project.tasks.register("dockerfileGenerate", DockerfileGenerateTask::class.java) { task -> 15 | task.group = "custom" 16 | task.dockerfile = extension 17 | } 18 | 19 | project.tasks.register("dockerfilePrint", DockerfilePrintTask::class.java) { task -> 20 | task.group = "custom" 21 | task.dockerfile = extension 22 | } 23 | } 24 | } 25 | 26 | abstract class DockerfileGenerateTask : DefaultTask() { 27 | @Input 28 | var dockerfile: Dockerfile? = null 29 | 30 | @get:Input 31 | @set:Option(option = "name", description = "Name of the dockerfile") 32 | var dockerFileName: String = "Dockerfile" 33 | 34 | @TaskAction 35 | fun executeTask() { 36 | val fileContent = dockerfile?.parse() 37 | 38 | if (fileContent == null) { 39 | logger.error("Dockerfile is empty.") 40 | return 41 | } 42 | 43 | val file = 44 | project.layout.projectDirectory 45 | .file(dockerFileName) 46 | .asFile 47 | 48 | file.createNewFile() 49 | file.writeText(fileContent.joinToString("\n")) 50 | 51 | logger.info("Generating Dockerfile") 52 | } 53 | } 54 | 55 | abstract class DockerfilePrintTask : DefaultTask() { 56 | @Input 57 | var dockerfile: Dockerfile? = null 58 | 59 | @TaskAction 60 | fun executeTask() { 61 | logger.info(dockerfile?.parse()?.joinToString("\n") ?: "Dockerfile is empty") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/io/github/dogacel/dsl/dockerfile/DockerfileDslPluginTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.dogacel.dsl.dockerfile 2 | 3 | import org.gradle.testfixtures.ProjectBuilder 4 | import kotlin.test.Test 5 | import kotlin.test.assertNotNull 6 | 7 | class DockerfileDslPluginTest { 8 | @Test 9 | fun `plugin registers task`() { 10 | // Create a test project and apply the plugin 11 | val project = ProjectBuilder.builder().build() 12 | project.plugins.apply("io.github.dogacel.dsl.dockerfile") 13 | 14 | // Verify the result 15 | assertNotNull(project.tasks.findByName("dockerfileGenerate")) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/io/github/dogacel/dsl/dockerfile/DockerfileDslTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.dogacel.dsl.dockerfile 2 | 3 | import io.github.dogacel.dsl.dockerfile.Expose.Protocol.UDP 4 | import kotlin.test.Test 5 | import kotlin.time.Duration.Companion.seconds 6 | 7 | class DockerfileDslTest { 8 | @Test 9 | fun syntax() { 10 | val file = 11 | dockerfile { 12 | add { 13 | source = "build/libs/*.jar" 14 | destination = "/app.jar" 15 | keepGitDir = true 16 | checksum = "sha256:1234567890" 17 | chown = "root:root" 18 | chmod = "755" 19 | link = true 20 | exclude("build/libs/*.jar") 21 | } 22 | 23 | arg("MODE") 24 | arg("JAR_FILE", "app.jar") 25 | 26 | cmd("java", "-jar", "\${JAR_FILE}") 27 | 28 | copy { 29 | source = "src/main/resources" 30 | destination = "/resources" 31 | from = "builder" 32 | chown = "root:root" 33 | chmod = "755" 34 | link = true 35 | parents = true 36 | exclude("src/main/resources/*.properties") 37 | } 38 | 39 | entryPoint("java", "-jar", "\${JAR_FILE}") 40 | 41 | env("JAVA_OPTS", "-Xmx512m") 42 | 43 | expose(80) 44 | expose(8080, UDP) 45 | 46 | from("openjdk:8-jdk-alpine") 47 | from("openjdk:8-jdk-alpine", "linux/amd64", "builder") 48 | 49 | healthcheck { 50 | interval = 10.seconds 51 | timeout = 5.seconds 52 | startPeriod = 0.seconds 53 | startInterval = 2.seconds 54 | retries = 5 55 | cmd("curl", "-f", "http://localhost:8080/health") 56 | } 57 | 58 | label("foo", "bar") 59 | 60 | maintainer("John Doe ") 61 | 62 | onBuild { 63 | add { 64 | source = "build/libs/*.jar" 65 | destination = "/app.jar" 66 | } 67 | } 68 | 69 | run("apk", "add", "curl") 70 | run("apk", "add", "curl", "wget") 71 | 72 | shell("/bin/bash", "-x") 73 | 74 | stopSignal("SIGTERM") 75 | 76 | user("root") 77 | 78 | volume("/data", "/logs") 79 | 80 | workdir("/app") 81 | } 82 | 83 | println(file.parse().joinToString("\n")) 84 | } 85 | 86 | @Test 87 | fun pythonSample() { 88 | val file = 89 | dockerfile { 90 | from("python:3.12") 91 | workdir("/usr/local/app") 92 | 93 | lines(1) 94 | +"Install the application dependencies" 95 | copy { 96 | source = "requirements.txt" 97 | destination = "./" 98 | } 99 | run("pip", "install", "--no-cache-dir", "-r", "requirements.txt") 100 | 101 | lines(1) 102 | +"Copy in the source code" 103 | copy { 104 | source = "src" 105 | destination = "./src" 106 | } 107 | expose(5000) 108 | 109 | lines(1) 110 | +"Setup an app user so the container doesn't run as the root user" 111 | run("useradd", "app") 112 | user("app") 113 | 114 | lines(1) 115 | cmd("uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080") 116 | } 117 | 118 | println(file.parse().joinToString("\n")) 119 | } 120 | 121 | @Test 122 | fun readme() { 123 | dockerfile { 124 | from("openjdk:21-jdk-slim") 125 | workdir("/app") 126 | 127 | +"Copy the JAR file into the Docker Image" 128 | copy { 129 | source = "app.jar" 130 | destination = "/app/app.jar" 131 | } 132 | 133 | +"Download assets" 134 | val assetPath = 135 | when (System.getenv("MODE")) { 136 | "local" -> "/first_100_assets.zip" 137 | "staging" -> "/compressed_assets.zip" 138 | else -> "/full_assets.zip" 139 | } 140 | 141 | run("curl", "-o", "assets.zip", "https://example.com/" + assetPath) 142 | run("unzip", "assets.zip", "-d", "/app/assets", "&&", "rm", "assets.zip") 143 | 144 | when (System.getenv("MODE")) { 145 | "local" -> expose(80) 146 | else -> {} 147 | } 148 | 149 | expose(443) 150 | 151 | // Enable dev tools 152 | when (System.getenv("MODE")) { 153 | "local" -> { 154 | env("ENABLE_GRPC_REFLECTION", "true") 155 | env("ENABLE_DOC_SERVICE", "true") 156 | } 157 | 158 | else -> {} 159 | } 160 | 161 | cmd("java", "-jar", "app.jar") 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "dockerfile-kotlin-dsl" 2 | 3 | // Include the plugin build 4 | includeBuild("plugin") 5 | 6 | // Include the integration test builds 7 | includeBuild("integration-test-kotlin") 8 | includeBuild("integration-test-groovy") 9 | --------------------------------------------------------------------------------