├── .dockerignore ├── .github └── workflows │ ├── build_and_push.yml │ └── continuous_integration.yml ├── .gitignore ├── Dockerfile ├── README.md ├── action.yaml ├── composer.json ├── deeployer.libsonnet ├── deeployer.schema.json ├── environments └── default │ ├── main.jsonnet │ └── spec.json ├── jsonnetfile.json ├── jsonnetfile.lock.json ├── lib ├── deeployer │ ├── docker-compose-generator.libsonnet │ └── resource_generator.libsonnet ├── k.libsonnet └── php │ ├── commands │ ├── DownCommand.php │ ├── ExportCommand.php │ └── UpCommand.php │ └── utils │ ├── ComposeFileGenerator.php │ ├── ConfigFileFinder.php │ ├── ConfigGenerator.php │ ├── EnvironmentFetcher.php │ ├── EnvironmentFetcherInterface.php │ └── Executor.php ├── lint.sh ├── phpstan.neon ├── phpunit.xml.dist ├── scripts ├── deeployer-compose.php ├── deeployer-k8s ├── docker-compose.jsonnet ├── main-compose.jsonnet ├── main.jsonnet ├── parseConfigForCredentials.php ├── runParseConfigForCredentials.php └── spec.json ├── tests ├── docker-compose-prod │ ├── host.json │ ├── host_with_dockerCompose_commands.json │ ├── host_with_dockerCompose_labels.json │ └── no_host.json ├── host.json ├── host_with_container_port.json ├── host_with_https.json ├── host_with_https_without_mail.json ├── host_with_low_version.json ├── host_without_port.json ├── k8sextension.libsonnet ├── php │ ├── ComposeFileGeneratorTest.php │ ├── ConfigGeneratorTest.php │ ├── ParseConfigForCredentialsTest.php │ └── json │ │ └── host.json ├── registryCredentials.json ├── replicas.json ├── run_tests.sh ├── schema │ ├── invalid_container_definition_with_unknown_properties.json │ ├── invalid_container_definition_without_image.json │ ├── invalid_container_with_wrong_declared_envVars.json │ ├── invalid_properties_definition_with_emptyString_in_image.json │ ├── invalid_properties_definition_with_emptyString_in_max_cpu.json │ ├── invalid_properties_definition_with_emptyString_in_max_memory.json │ ├── invalid_properties_definition_with_emptyString_in_min_cpu.json │ ├── invalid_properties_definition_with_emptyString_in_min_memory.json │ ├── invalid_test_testing_envVars_with_a_specialObject.json │ ├── invalid_test_testing_envVars_with_nonStringValue.json │ ├── invalid_with_wrong_version.json │ ├── invalid_without_version.json │ └── valid.json ├── test_functions.sh └── volume.json └── validator.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | /.github 2 | /Docker 3 | /environments 4 | /vendor 5 | /tests 6 | Dockerfile 7 | -------------------------------------------------------------------------------- /.github/workflows/build_and_push.yml: -------------------------------------------------------------------------------- 1 | name: Build and push Docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '*' 9 | 10 | # Enables BuildKit 11 | env: 12 | DOCKER_BUILDKIT: 1 13 | 14 | jobs: 15 | 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | 22 | - name: Checkout 23 | uses: actions/checkout@v2 24 | 25 | - name: "Build and push image" 26 | uses: docker/build-push-action@v1 27 | with: 28 | username: ${{ secrets.DOCKER_USERNAME }} 29 | password: ${{ secrets.DOCKER_PASSWORD }} 30 | repository: thecodingmachine/deeployer 31 | tag_with_ref: true 32 | add_git_labels: true 33 | -------------------------------------------------------------------------------- /.github/workflows/continuous_integration.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/categories/automating-your-workflow-with-github-actions 2 | 3 | name: "Continuous Integration" 4 | 5 | on: 6 | - "pull_request" 7 | - "push" 8 | 9 | jobs: 10 | 11 | continuous-integration: 12 | name: "Continuous Integration" 13 | 14 | runs-on: "ubuntu-latest" 15 | 16 | steps: 17 | - name: "Checkout" 18 | uses: "actions/checkout@v2.0.0" 19 | 20 | - name: "Install PHP with extensions" 21 | uses: "shivammathur/setup-php@v2" 22 | with: 23 | coverage: "pcov" 24 | php-version: "7.2" 25 | 26 | - name: "Download JSONNET" 27 | run: | 28 | curl -o jsonnet.tar.gz -L https://github.com/google/jsonnet/releases/download/v0.15.0/jsonnet-bin-v0.15.0-linux.tar.gz 29 | tar xvzf jsonnet.tar.gz 30 | chmod +x jsonnet 31 | chmod +x jsonnetfmt 32 | sudo mv jsonnet /usr/local/bin/jsonnet 33 | sudo mv jsonnetfmt /usr/local/bin/jsonnetfmt 34 | rm jsonnet.tar.gz 35 | 36 | - name: "Install Tanka" 37 | run: | 38 | sudo curl -fSL -o "/usr/local/bin/tk" "https://github.com/grafana/tanka/releases/download/v0.9.0/tk-linux-amd64" 39 | sudo chmod a+x "/usr/local/bin/tk" 40 | 41 | - name: "Install Kubeval" 42 | run: | 43 | curl -fSL -o kubeval-linux-amd64.tar.gz https://github.com/instrumenta/kubeval/releases/latest/download/kubeval-linux-amd64.tar.gz 44 | tar xf kubeval-linux-amd64.tar.gz 45 | sudo cp kubeval /usr/local/bin 46 | 47 | - name: "Install node" 48 | uses: actions/setup-node@v1 49 | with: 50 | node-version: '12.14.0' 51 | 52 | - name: "Install ajv" 53 | run: npm install -g ajv-cli@^5 54 | 55 | - name: "Install ajv-formats" 56 | run: npm install -g ajv-formats@^2.1.1 57 | 58 | - name: "Install jsonlint" 59 | run: npm install jsonlint -g 60 | 61 | - name: "Download JB" 62 | run: | 63 | curl -o jb -L https://github.com/jsonnet-bundler/jsonnet-bundler/releases/download/v0.3.1/jb-linux-amd64 64 | chmod +x jb 65 | sudo mv jb /usr/local/bin/jb 66 | 67 | - name: "Install dependencies" 68 | run: jb install 69 | 70 | - name: "Install php dependencies" 71 | run: composer install 72 | 73 | - name: "Run tests" 74 | run: ./run_tests.sh 75 | working-directory: "tests" 76 | 77 | - name: "Run phpstan" 78 | run: composer phpstan 79 | 80 | - name: "Run phpunit tests" 81 | run: composer phpunit 82 | 83 | - name: "Validating JSON & *SONNET files" 84 | run: ./validator.sh 85 | 86 | - name: "Check coding style in deeployer lib" 87 | run: jsonnetfmt --test *.*sonnet 88 | working-directory: "lib/deeployer" 89 | 90 | - name: "Archive code coverage results" 91 | uses: "actions/upload-artifact@v1" 92 | with: 93 | name: "build" 94 | path: "build" 95 | 96 | - name: "upload the coverage to codecov" 97 | uses: codecov/codecov-action@v1 # 98 | with: 99 | fail_ci_if_error: true # optional (default = false) 100 | 101 | build-and-test: 102 | name: "Build and test" 103 | 104 | runs-on: "ubuntu-latest" 105 | 106 | steps: 107 | - name: "Checkout" 108 | uses: "actions/checkout@v2.0.0" 109 | 110 | - name: "Build" 111 | run: docker build -t thecodingmachine/deeployer:latest . 112 | 113 | - name: "Test image" 114 | run: docker run --rm -e "JSON_ENV={}" -v $(pwd):/var/app thecodingmachine/deeployer:latest deeployer-k8s show tests/host.json 115 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib/deeployer/test_tanka.libsonnet.dist 2 | /vendor 3 | composer.lock 4 | .phpunit.result.cache 5 | /.vscode 6 | build/ 7 | .idea/ 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM thecodingmachine/k8s_gitlabci:2.0.5 2 | 3 | ENV SILENT_WARNINGS=1 4 | RUN mkdir /var/app 5 | 6 | 7 | RUN curl -fSL -o "/usr/local/bin/tk" "https://github.com/grafana/tanka/releases/download/v0.9.0/tk-linux-amd64" && chmod a+x "/usr/local/bin/tk" 8 | RUN mkdir jsonnetdownload && cd jsonnetdownload && curl -fSL -o jsonnet.tar.gz https://github.com/google/jsonnet/releases/download/v0.15.0/jsonnet-bin-v0.15.0-linux.tar.gz && \ 9 | tar xzf jsonnet.tar.gz && \ 10 | mv jsonnet /usr/local/bin && \ 11 | mv jsonnetfmt /usr/local/bin && \ 12 | cd .. && \ 13 | rm -rf jsonnetdownload 14 | 15 | RUN curl -fSL -o "/usr/local/bin/jb" "https://github.com/jsonnet-bundler/jsonnet-bundler/releases/download/v0.3.1/jb-linux-amd64" && chmod a+x "/usr/local/bin/jb" 16 | 17 | # install NodeJS and jq 18 | RUN apt-get update &&\ 19 | apt-get install -y --no-install-recommends gnupg &&\ 20 | curl -sL https://deb.nodesource.com/setup_12.x | bash - &&\ 21 | apt-get update &&\ 22 | apt-get install -y --no-install-recommends nodejs jq docker-compose curl php-dom php-mbstring php-zip php-curl unzip 23 | 24 | RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" &&\ 25 | php composer-setup.php --install-dir=bin --filename=composer &&\ 26 | php -r "unlink('composer-setup.php');" 27 | 28 | 29 | # install AJV for schema validation 30 | RUN npm install -g ajv-cli@^5 31 | RUN npm install -g ajv-formats@^2.1.1 32 | 33 | COPY . /deeployer 34 | 35 | RUN cd /deeployer && jb install 36 | 37 | RUN cd /deeployer && composer install 38 | 39 | RUN ln -s /deeployer/scripts/deeployer-k8s /usr/local/bin/deeployer-k8s 40 | RUN ln -s /deeployer/scripts/deeployer-compose.php /usr/local/bin/deeployer-compose 41 | 42 | WORKDIR /var/app 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deeployer 2 | 3 | ## WORK IN PROGRESS 4 | 5 | Deeployer's goal is to allow you to describe an environment (be it a dev or a prod environment) in a simple JSON file. 6 | 7 | You write a single "deeployer.json" file and you can deploy easily to a Kubernetes cluster, or to a single server with docker-compose. 8 | 9 | Deeployer's goal is not to be 100% flexible (you have Kubernetes for that), but rather to ease the deployment process for developers 10 | that do not necessarily master the intricacies of Kubernetes. 11 | 12 | It aims to automate a number of processes, including easy backup setup, easy reverse proxy declaration, etc... 13 | 14 | ## The Deeployer config file 15 | 16 | The Deeployer config file contains the list of containers that makes your environment: 17 | 18 | **deeployer.json** 19 | ```json 20 | { 21 | "$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json", 22 | "version": "1.0", 23 | "containers": { 24 | "mysql": { 25 | "image": "mysql:8.0", 26 | "ports": [3306], 27 | "env": { 28 | "MYSQL_ROOT_PASSWORD": "secret" 29 | } 30 | }, 31 | "phpmyadmin": { 32 | "image": "phpmyadmin/phpmyadmin:5.0", 33 | "host": { 34 | "url": "phpmyadmin.myapp.localhost", 35 | "containerPort": 80 36 | }, 37 | "env": { 38 | "PMA_HOST": "mysql", 39 | "MYSQL_ROOT_PASSWORD": "secret" 40 | } 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | TODO: add volumes when ready 47 | 48 | Let's have a closer look at this file. 49 | 50 | The first line is optional: 51 | 52 | ``` 53 | "$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json", 54 | ``` 55 | (TODO: migrate the URL to a static website) 56 | 57 | It declares the JsonSchema. We highly recommend to keep this line. Indeed, if you are using an IDE like Visual Studio 58 | Code or a JetBrain's IDE, you will get auto-completion and validation of the structure of the file right in your IDE! 59 | 60 | Then, the "containers" section contains the list of containers for your environment. 61 | In the example above, we declare 2 containers: "mysql" and "phpmyadmin". 62 | Just like in "docker-compose", the name of the container is also an internal DNS record. So from any container of your 63 | environment, the "mysql" container is reachable at the "mysql" domain name. 64 | 65 | For each container, you need to pass: 66 | 67 | - "image": the Docker image for the container 68 | - "ports": a list of ports this image requires. Warning! Unlike in `docker-compose`, this is not a list of ports that 69 | will be shared with the host. This is simply a list of ports this image opens. This is particularly important if you 70 | do deployments in Kubernetes (each port will be turned into a K8S service). 71 | 72 | You can pass environment variables using the "env" key: 73 | 74 | ```json 75 | "env": { 76 | "MYSQL_ROOT_PASSWORD": "secret" 77 | } 78 | ``` 79 | 80 | We will see later how to manage those secrets without storing them in full text. (TODO) 81 | 82 | 83 | ## Using Jsonnet 84 | 85 | JSON is not the only format supported for the "Deeployer" config file. You can also write the file in [Jsonnet](https://jsonnet.org/learning/tutorial.html). 86 | 87 | Jsonnet? This is a very powerful data templating language for JSON. 88 | 89 | By convention, you should name your Deeployer file `deeployer.libsonnet`. (TODO: switch to `deeployer.jsonnnet`) 90 | 91 | Here is a sample file: 92 | 93 | **deeployer.libsonnet** 94 | ```jsonnnet 95 | { 96 | local mySqlPassword = "secret", 97 | local baseUrl = "myapp.localhost", 98 | "$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json", 99 | "version": "1.0", 100 | "containers": { 101 | "mysql": { 102 | "image": "mysql:8.0", 103 | "ports": [3306], 104 | "env": { 105 | "MYSQL_ROOT_PASSWORD": mySqlPassword 106 | } 107 | }, 108 | "phpmyadmin": { 109 | "image": "phpmyadmin/phpmyadmin:5.0", 110 | "host": { 111 | "url": "phpmyadmin."+baseUrl 112 | "containerPort": 80 113 | }, 114 | "env": { 115 | "PMA_HOST": "mysql", 116 | "MYSQL_ROOT_PASSWORD": mySqlPassword 117 | } 118 | } 119 | } 120 | } 121 | ``` 122 | 123 | In the example above, we declare 2 variables and use these variables in the config file. See how the `mySqlPassword` 124 | variable is used twice? Jsonnet allows us to avoid duplicating configuration code in all containers. 125 | 126 | But there is even better! Let's assume you have a staging and a production environment. Maybe you want PhpMyAdmin on the 127 | staging environment (for testing purpose) but not on the production environment. Using Jsonnet, we can do this easily 128 | using 2 files: 129 | 130 | **deeployer.libsonnet** 131 | ```jsonnnet 132 | { 133 | local mySqlPassword = "secret", 134 | "$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json", 135 | "version": "1.0", 136 | "containers": { 137 | "mysql": { 138 | "image": "mysql:8.0", 139 | "ports": [3306], 140 | "env": { 141 | "MYSQL_ROOT_PASSWORD": mySqlPassword 142 | } 143 | } 144 | } 145 | } 146 | ``` 147 | 148 | **deeployer-dev.libsonnet** 149 | ```jsonnnet 150 | local prod = import "deeployer.libsonnet"; 151 | local baseUrl = "myapp.localhost"; 152 | prod + { 153 | "containers"+: { 154 | "phpmyadmin": { 155 | "image": "phpmyadmin/phpmyadmin:5.0", 156 | "host": { 157 | "url": "phpmyadmin."+baseUrl, 158 | "containerPort": 80 159 | }, 160 | "env": { 161 | "PMA_HOST": "mysql", 162 | "MYSQL_ROOT_PASSWORD": prod.containers.mysql.env.MYSQL_ROOT_PASSWORD 163 | } 164 | } 165 | } 166 | } 167 | ``` 168 | 169 | TODO: test this. 170 | 171 | 172 | ## Referencing environment variables in the Deeployer config file 173 | 174 | When doing continuous deployment, it is common to put environment dependant variables and secrets in environment 175 | variables. Deeployer can access environment variables using the Jsonnet "env" external variable: 176 | 177 | **deeployer.libsonnet** 178 | ```jsonnnet 179 | local env = std.extVar("env"); 180 | { 181 | local mySqlPassword = "secret", 182 | "version": "1.0", 183 | "containers": { 184 | "mysql": { 185 | "image": "mysql:8.0", 186 | "ports": [3306], 187 | "env": { 188 | "MYSQL_ROOT_PASSWORD": env.MYSQL_PASSWORD 189 | } 190 | } 191 | } 192 | } 193 | ``` 194 | 195 | The first line is putting all environments variables in the `env` local variable: 196 | 197 | ```jsonnet 198 | local env = std.extVar("env"); 199 | ``` 200 | 201 | Then, you can access all environment variables from the machine running Deeployer using `env.ENV_VARIABLE_NAME`. 202 | 203 | Beware! If the environment variable is not set, Jsonnet will throw an error! 204 | 205 | ### Enabling HTTPS 206 | 207 | Deeployer offers HTTPS support out of the box using Let's encrypt. 208 | 209 | **deeployer.json** 210 | ```json 211 | { 212 | "version": "1.0", 213 | "$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json", 214 | "containers": { 215 | "phpmyadmin": { 216 | "image": "phpmyadmin/phpmyadmin:5.0", 217 | "host": { 218 | "url": "phpmyadmin.myapp.localhost", 219 | "containerPort": 80, 220 | "https": "enable" 221 | }, 222 | "env": { 223 | "PMA_HOST": "mysql" 224 | "MYSQL_ROOT_PASSWORD": "secret" 225 | } 226 | } 227 | }, 228 | "config": { 229 | "https": { 230 | "mail": "mymail@example.com" 231 | } 232 | } 233 | } 234 | ``` 235 | 236 | In order to automatically get a certificate for your HTTPS website, you need to: 237 | 238 | - Add `"https": "enable"` in your `host` section 239 | - At the bottom of the `deeployer.json` file, add a "config.https.mail" entry specifying a mail address. This mail address 240 | will be used to warn you, should something goes wrong with the certificate (for instance if the certificate is going 241 | to expire soon) 242 | 243 | Please note that if you are using Kubernetes, you will need in addition to install CertManager in your cluster. 244 | [See the relevant Kubernetes documentation below](#configuring-your-kubernetes-cluster-to-support-https) 245 | 246 | ### Customizing Kubernetes resources 247 | 248 | Deeployer's goal is to allow you to describe a complete environment in a simple JSON file. It simplifies a lot the 249 | configuration by making a set of common assumptions on your configuration. Of course, the JSON config file does not 250 | let you express everything you can in a raw Kubernetes environment. This is by design. 251 | 252 | However, there are times when you might need a very specific K8S feature. In this case, you can use JSONNET functions 253 | to dynamically alter the generated K8S configuration files. 254 | 255 | To do this, you will need to use a `deeployer.libsonnet` configuration file instead of a `deeployer.json` configuration 256 | file. 257 | 258 | You can then use the hidden `config.k8sextension` field to alter the generated configuration. 259 | In the example below, we are adding 2 annotations to the container of the deployment: 260 | 261 | ```libsonnet 262 | { 263 | "version": "1.0", 264 | "containers": { 265 | "phpmyadmin": { 266 | "image": "phpmyadmin", 267 | "ports": [ 268 | 80 269 | ], 270 | "host": { 271 | "url": "myhost.com" 272 | } 273 | } 274 | }, 275 | "config": { 276 | k8sextension(k8sConf):: 277 | k8sConf + { 278 | phpmyadmin+: { 279 | deployment+: { 280 | spec+: { 281 | template+: { 282 | metadata+: { 283 | annotations+: { 284 | "prometheus.io/port": "8080", 285 | "prometheus.io/scrape": "true" 286 | } 287 | } 288 | } 289 | } 290 | } 291 | } 292 | } 293 | } 294 | } 295 | ``` 296 | 297 | What is going on here? We are describing in the config a `k8sextension` function. 298 | This JSONNET function is passed a JSON object representing the complete list of all the Kubernetes resources. 299 | Using JSONNET, we extend that list to add annotations in one given container. 300 | 301 | Good to know: 302 | 303 | Resources stored in the JSON config object passed to `k8sextension` is on two levels. 304 | 305 | - The first level is the name of the container (`phpmyadmin` in the example above) 306 | - The second level is the name of the resource type we want to target (here, a `deployment`) 307 | 308 | ## Usage 309 | 310 | ### Deploying using Kubernetes 311 | 312 | View the list of Kubernetes resources that will be generated using `deeployer-k8s show` 313 | 314 | ```console 315 | $ deeployer-k8s show 316 | ``` 317 | 318 | By default, Deeployer will look for a `deeployer.libsonnet` or a `deeployer.json` file in the current working directory. 319 | 320 | You can specify an alternative name in the command: 321 | 322 | ```console 323 | $ deeployer-k8s show deeployer-dev.jsonnet 324 | ``` 325 | 326 | The "show" command is only used for debugging. In order to make an actual deployment, use the "apply" command: 327 | 328 | ```console 329 | $ deeployer-k8s apply --namespace target-namespace 330 | ``` 331 | 332 | Important: if you are using Deeployer locally, Deeployer will not use your Kubectl config by default. You need to pass 333 | the Kubectl configuration as an environment variable. 334 | 335 | Finally, you can delete a complete namespace using: 336 | 337 | ```console 338 | $ deeployer-k8s delete --namespace target-namespace 339 | ``` 340 | 341 | This is equivalent to using: 342 | 343 | ```console 344 | $ kubectl delete namespace target-namespace 345 | ``` 346 | 347 | #### Connecting to a "standard" environment 348 | 349 | If a "kubeconfig" file is enough to connect to your environement, you can connect to your cluster 350 | by setting the `KUBE_CONFIG_FILE` environment variable. 351 | 352 | - `KUBE_CONFIG_FILE` should contain the content of the *kubeconfig* file. 353 | 354 | #### Connecting to a GCloud environment 355 | 356 | You can connect to a GKE cluster by setting these environment variables: 357 | 358 | - `GCLOUD_SERVICE_KEY` 359 | - `GCLOUD_PROJECT` 360 | - `GCLOUD_ZONE` 361 | - `GKE_CLUSTER` 362 | 363 | #### Configuring your Kubernetes cluster to support HTTPS 364 | 365 | In order to have HTTPS support in Kubernetes, you need to install [Cert Manager](https://cert-manager.io/) in your Kubernetes cluster. 366 | Cert Manager is a certificate management tool that acts **cluster-wide**. Deeployer configures Cert Manager to generate 367 | certificates using [Let's encrypt](https://letsencrypt.org/). 368 | 369 | You can install Cert Manager using [their installation documentation](https://cert-manager.io/docs/installation/kubernetes/). 370 | You do not need to create a "cluster issuer" as Deeployer will come with its own issuer. 371 | 372 | You need to install Cert Manager v0.11+. 373 | 374 | #### Preventing automatic redeployment 375 | 376 | By default, in Kubernetes, all pods will be halted, and restarted. Even if the configuration did not change (Deeployer tries to redownload the latest version of the image). 377 | But in the case of some services (like a MySQL database or a Redis server), stopping and restarting the service will cause 378 | a disruption, for no good reason. 379 | 380 | You can tell Deeployer to not restart a pod automatically using the "redeploy": "onConfigChange" option. 381 | 382 | **deeployer.json** 383 | ```json 384 | { 385 | "version": "1.0", 386 | "$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json", 387 | "containers": { 388 | "mysql": { 389 | "image": "mysql:8.0", 390 | "ports": [3306], 391 | "env": { 392 | "MYSQL_ROOT_PASSWORD": "secret" 393 | }, 394 | "redeploy": "onConfigChange" 395 | } 396 | } 397 | } 398 | ``` 399 | 400 | 401 | With `"redeploy": "onConfigChange"`, your pod will be changed only if the configuration is changed. 402 | 403 | #### Redeployment strategy 404 | 405 | In Kubernetes, by default, Deeployer will stop and recreate the pod if there is only one pod (if you did not set the `replicas` property). 406 | If you configured `replicas` to a value greater than 1, Deeployer will use a "RollingUpdate" for this pod. 407 | 408 | ### Deploying using docker-compose 409 | 410 | To deploy with deeployer-compose, you need to setup the following alias on your local machine since deeployer in its first versions is only accessible thanks to its official docker-image : 411 | 412 | ``` 413 | alias deeployer-compose="docker run --rm -it -e \"JSON_ENV=\$(jq -n env)\" -v $(pwd):/var/app -v /var/run/docker.sock:/var/run/docker.sock thecodingmachine/deeployer:latest deeployer-compose" 414 | ``` 415 | 416 | 417 | ## Installing locally 418 | 419 | In order to use Deeployer locally, you need to install: 420 | 421 | - [Docker](https://docs.docker.com/get-docker/) 422 | - [jq](https://stedolan.github.io/jq/download/) 423 | 424 | Deeployer can be run via Docker. Installation is as easy as adding a few aliases to your `~/.bashrc` (if you are using Bash) 425 | 426 | `~/.bashrc` 427 | ```console 428 | alias deeployer-k8s='docker run --rm -it -e "JSON_ENV=\$\(jq -n env\)" -v $(pwd):/var/app thecodingmachine/deeployer:latest deeployer-k8s' 429 | alias deeployer-compose='docker run --rm -it -e "JSON_ENV=\$\(jq -n env\)" -v $(pwd):/var/app -v /var/run/docker.sock:/var/run/docker.sock thecodingmachine/deeployer:latest deeployer-compose' 430 | alias deeployer-self-update="docker pull thecodingmachine/deeployer:latest" 431 | ``` 432 | 433 | Deeployer is under heavy development. Do not forget to update the Docker image regularly: 434 | 435 | ```console 436 | $ deeployer-self-update 437 | ``` 438 | 439 | ## Usage in Gitlab CI 440 | 441 | To use deeployer in gitlab ci, you'll need to specify in your .gitlab-ci.yml file a job for the deployment like in the following example : 442 | ``` 443 | deeploy: 444 | image: thecodingmachine/deeployer:latest 445 | stage: deploy 446 | variables: 447 | KUBE_CONFIG_FILE: ${KUBE_CONFIG} 448 | script: 449 | - deeployer-k8s apply --namespace ${CI_PROJECT_PATH_SLUG}-${CI_COMMIT_REF_SLUG} 450 | - curl "https://bigbro.thecodingmachine.com/gitlab/call/start-environment?projectId=${CI_PROJECT_ID}&commitSha=${CI_COMMIT_SHA}&ref=${CI_COMMIT_REF_NAME}&name=${CI_PROJECT_PATH_SLUG}-${CI_COMMIT_REF_SLUG}" 451 | environment: 452 | name: review/$CI_COMMIT_REF_NAME 453 | url: https://bigbro.thecodingmachine.com/environment/${CI_PROJECT_PATH_SLUG}-${CI_COMMIT_REF_SLUG} 454 | when: manual 455 | only: 456 | - /^CD-.*$/ 457 | ``` 458 | the image thecodingmachine/deeployer:latest needs a variable named KUBE_CONFIG_FILE which contains the kubernetes config file that gives you access to the cluster. In the case of the example we setted it as a CI/CD variable of gitlab since the feature is still available. 459 | 460 | ``` 461 | variables: 462 | KUBE_CONFIG_FILE: ${KUBE_CONFIG} 463 | ``` 464 | 465 | Next, in the script section of the job we just use the command apply of deeployer-k8s with the mandatory option --namespace which in the case of the example is setted thanks to CI/CD variables. 466 | Since Deeployer is bundled as a Docker image, usage in Gitlab CI is very easy (assuming you are using a Docker based Gitlab CI runner, of course); 467 | 468 | **.gitlab-ci.yaml** 469 | ```yaml 470 | stages: 471 | # Your other stages ... 472 | - deploy 473 | - cleanup 474 | 475 | deploy_branches: 476 | image: thecodingmachine/deeployer:latest 477 | stage: deploy 478 | script: 479 | - deeployer-k8s apply --namespace ${CI_PROJECT_PATH_SLUG}-${CI_COMMIT_REF_SLUG} 480 | environment: 481 | name: review/$CI_COMMIT_REF_NAME 482 | url: https://${CI_COMMIT_REF_SLUG}.${CI_PROJECT_PATH_SLUG}.test.yourapp.com 483 | on_stop: cleanup_branches 484 | only: 485 | - branches 486 | 487 | cleanup_branches: 488 | stage: cleanup 489 | image: thecodingmachine/deeployer:latest 490 | variables: 491 | GIT_STRATEGY: none 492 | script: 493 | - deeployer-k8s delete --namespace ${CI_PROJECT_PATH_SLUG}-${CI_COMMIT_REF_SLUG} 494 | when: manual 495 | environment: 496 | name: review/$CI_COMMIT_REF_NAME 497 | action: stop 498 | only: 499 | - branches 500 | except: 501 | - master 502 | ``` 503 | 504 | For this to work, you will need to put the content of your Kubernetes configuration file in a `KUBE_CONFIG_FILE` environment variable in your project on Gitlab. 505 | 506 | ### Deploying in a Google cloud Kubernetes cluster using Gitlab CI 507 | 508 | If you are connecting to a Google Cloud cluster, instead of passing a `KUBE_CONFIG_FILE`, you will need to pass 509 | this set of environment variables: 510 | 511 | - `GCLOUD_SERVICE_KEY` 512 | - `GCLOUD_PROJECT` 513 | - `GCLOUD_ZONE` 514 | - `GKE_CLUSTER` 515 | 516 | ## Usage in Github actions 517 | 518 | Deeployer comes with a Github action. 519 | 520 | **deploy_workflow.yaml** 521 | ```yaml 522 | name: Deploy Docker image 523 | 524 | on: 525 | - push 526 | 527 | jobs: 528 | deeploy: 529 | runs-on: ubuntu-latest 530 | 531 | steps: 532 | - name: Checkout 533 | uses: actions/checkout@v2 534 | 535 | - name: Deploy 536 | uses: thecodingmachine/deeployer-action@master 537 | env: 538 | KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }} 539 | with: 540 | namespace: target-namespace 541 | ``` 542 | 543 | You will need to put the content of your Kubernetes configuration file in the `KUBE_CONFIG_FILE` secret on Github. 544 | 545 | ### Deploying using the Github action in a Google cloud Kubernetes cluster 546 | 547 | If you are connecting to a Google Cloud cluster, instead of passing a `KUBE_CONFIG_FILE`, you will need to pass 548 | this set of environment variables: 549 | 550 | - `GCLOUD_SERVICE_KEY` 551 | - `GCLOUD_PROJECT` 552 | - `GCLOUD_ZONE` 553 | - `GKE_CLUSTER` 554 | 555 | ## Passing registry credentials 556 | 557 | If you are using a private registry to store your Docker images, Deeployer needs the credentials to this registry in order 558 | to deploy images successfully. 559 | 560 | Put the credentials to your images in the "config" section: 561 | 562 | **deeployer.json** 563 | ``` 564 | { 565 | "config": { 566 | "registryCredentials": { 567 | "registry.example.com": { 568 | "user": "my_user", 569 | "password": "my_password" 570 | } 571 | } 572 | } 573 | } 574 | ``` 575 | 576 | Please note that the key of the "registryCredentials" object is the URL to your Docker private registry. 577 | 578 | These will be automatically passed to Kubernetes that will create a "registry secret". 579 | 580 | ## Contributing 581 | 582 | Download and install the Jsonnet Bundler: https://github.com/jsonnet-bundler/jsonnet-bundler/releases 583 | 584 | Install the dependencies: 585 | 586 | ```bash 587 | $ jb install 588 | ``` 589 | 590 | Download and install Tanka: https://github.com/grafana/tanka/releases 591 | 592 | Download and install Kubeval: https://kubeval.instrumenta.dev/installation/ 593 | 594 | Download and install AJV: 595 | 596 | ```bash 597 | $ sudo npm install -g ajv-cli@^5 598 | $ sudo npm install -g ajv-formats@^2.1.1 599 | ``` 600 | 601 | Download and install Jsonlint: 602 | 603 | ```bash 604 | $ sudo npm install -g jsonlint 605 | ``` 606 | 607 | Before submitting a PR: 608 | 609 | - run the tests: 610 | ```console 611 | cd tests/ 612 | ./run_tests.sh 613 | ``` 614 | - run the linter: 615 | ```console 616 | ./lint.sh 617 | ``` 618 | -------------------------------------------------------------------------------- /action.yaml: -------------------------------------------------------------------------------- 1 | name: 'Deploy with Deeployer' 2 | description: 'Deploy your application using Deeployer' 3 | inputs: 4 | deeployerFile: 5 | description: 'Deeployer file (by default, Deeployer will look for a deeployer.libsonnet or deeployer.json file)' 6 | required: false 7 | namespace: 8 | description: 'Kubernetes namespace to deploy to' 9 | required: true 10 | timeout: 11 | description: 'Maximum time to wait for deployment success before considering it failed (defaults to "600s")' 12 | required: false 13 | default: '600s' 14 | runs: 15 | using: 'docker' 16 | image: 'Dockerfile' 17 | args: 18 | - deeployer-k8s 19 | - apply 20 | - "--namespace" 21 | - ${{ inputs.namespace }} 22 | - "--timeout" 23 | - ${{ inputs.timeout }} 24 | - ${{ inputs.deeployerFile }} 25 | 26 | branding: 27 | icon: 'cloud' 28 | color: 'green' 29 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "symfony/console": "^5.1" 4 | }, 5 | "autoload": { 6 | "psr-4": { 7 | "App\\": "lib/php/" 8 | } 9 | }, 10 | "require-dev": { 11 | "phpunit/phpunit": "^8.0", 12 | "phpstan/phpstan": "^0.12.32" 13 | }, 14 | "autoload-dev": { 15 | "psr-4": { 16 | "App\\Tests\\": "tests/php" 17 | } 18 | }, 19 | "scripts": { 20 | "phpunit": "vendor/bin/phpunit", 21 | "phpstan": "phpstan analyse lib/php tests/php --level=max --no-progress -vvv --memory-limit=1024M" 22 | } 23 | } -------------------------------------------------------------------------------- /deeployer.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | version: '1.0', 3 | '$schema': '../deeployer.schema.json', 4 | containers: { 5 | mysql: { 6 | image: 'mysql:8', 7 | env: { 8 | MYSQL_ROOT_PASSWORD: 'ocs', 9 | }, 10 | volumes: { 11 | data: { 12 | diskSpace: '1G', 13 | mountPath: '/var/lib/mysql', 14 | }, 15 | }, 16 | }, 17 | phpmyadmin: { 18 | image: 'phpmyadmin/phpmyadmin', 19 | env: 20 | { 21 | PMA_HOST: 'mysql', 22 | }, 23 | host: { 24 | url: 'phpmyadmin.tcm-test.tk', 25 | https: 'enable', 26 | }, 27 | 28 | }, 29 | }, 30 | config: { 31 | https: { 32 | mail: 'test@thecodingmachine.com', 33 | }, 34 | dynamic: '\n composeExtension(composeConfig)::\n composeConfig + {\n services+: {\n mysql+: {\n "labels": ["testing_label"],\n "command": ["date"],\n },\n },\n },', 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /deeployer.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "version": { 5 | "type": "string", 6 | "pattern": "^[0-9]+[.][0-9]+$" 7 | }, 8 | "containers": { 9 | "type": "object", 10 | "additionalProperties": false, 11 | "patternProperties": { 12 | "^(?![0-9]+$)(?!.*-$)(?!-)[a-zA-Z0-9-]{1,63}$": { 13 | "type": "object", 14 | "additionalProperties": false, 15 | "properties": { 16 | "replicas": { 17 | "type": "integer" 18 | }, 19 | "image": { 20 | "type": "string", 21 | "minLength": 1 22 | }, 23 | "ports": { 24 | "type": "array" 25 | }, 26 | "env": { 27 | "type": "object", 28 | "additionalProperties": { 29 | "type": "string" 30 | } 31 | }, 32 | "redeploy": { 33 | "type": "string", 34 | "enum": [ 35 | "always", 36 | "onConfigChange" 37 | ] 38 | }, 39 | "host": { 40 | "type": "object", 41 | "properties": { 42 | "url": { 43 | "type": "string", 44 | "format": "hostname" 45 | }, 46 | "containerPort": { 47 | "type": "integer", 48 | "minimum": 1, 49 | "maximum": 65535 50 | }, 51 | "https": { 52 | "type": "string", 53 | "enum": [ 54 | "enable", 55 | "disable", 56 | "force" 57 | ] 58 | } 59 | }, 60 | "additionalProperties": false, 61 | "required": [ 62 | "url" 63 | ] 64 | }, 65 | "volumes": { 66 | "type": "object", 67 | "additionalProperties": false, 68 | "patternProperties": { 69 | "^(?![0-9]+$)(?!.*-$)(?!-)[a-zA-Z0-9-]{1,63}$": { 70 | "type": "object", 71 | "properties": { 72 | "mountPath": { 73 | "type": "string", 74 | "minLength": 1 75 | }, 76 | "diskSpace": { 77 | "type": "string", 78 | "minLength": 1 79 | } 80 | } 81 | } 82 | } 83 | }, 84 | "quotas": { 85 | "type": "object", 86 | "properties": { 87 | "min": { 88 | "type": "object", 89 | "properties": { 90 | "cpu": { 91 | "type": "string", 92 | "minLength": 1 93 | }, 94 | "memory": { 95 | "type": "string", 96 | "minLength": 1 97 | } 98 | } 99 | }, 100 | "max": { 101 | "type": "object", 102 | "properties": { 103 | "cpu": { 104 | "type": "string", 105 | "minLength": 1 106 | }, 107 | "memory": { 108 | "type": "string", 109 | "minLength": 1 110 | } 111 | } 112 | } 113 | } 114 | } 115 | }, 116 | "required": [ 117 | "image" 118 | ] 119 | } 120 | } 121 | }, 122 | "config": { 123 | "type": "object", 124 | "properties": { 125 | "https": { 126 | "type": "object", 127 | "properties": { 128 | "mail": { 129 | "type": "string", 130 | "format": "email" 131 | } 132 | }, 133 | "required": [ 134 | "mail" 135 | ] 136 | }, 137 | "registryCredentials": { 138 | "type":"object", 139 | "additionalProperties": true, 140 | "patternProperties": { 141 | ".*": { 142 | "type":"object", 143 | "additionalProperties": false, 144 | "properties": { 145 | "user": { 146 | "type": "string", 147 | "minLength": 1 148 | }, 149 | "password": { 150 | "type": "string", 151 | "minLength": 1 152 | } 153 | }, 154 | "required": [ 155 | "user", 156 | "password" 157 | ] 158 | } 159 | } 160 | }, 161 | "dynamic": { 162 | "type": "string" 163 | } 164 | } 165 | } 166 | }, 167 | "required": ["version"] 168 | } 169 | -------------------------------------------------------------------------------- /environments/default/main.jsonnet: -------------------------------------------------------------------------------- 1 | (import 'ksonnet-util/kausal.libsonnet') + 2 | (import 'deeployer/resource_generator.libsonnet') + 3 | 4 | { 5 | local config = import '../../deeployer.libsonnet', 6 | local deeployer = $.deeployer, 7 | 8 | 9 | generatedConf: deeployer.generateResources(config), 10 | 11 | 12 | } 13 | -------------------------------------------------------------------------------- /environments/default/spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "tanka.dev/v1alpha1", 3 | "kind": "Environment", 4 | "metadata": { 5 | "name": "deeployer" 6 | }, 7 | "spec": { 8 | "apiServer": "", 9 | "namespace": "deeployer" 10 | } 11 | } -------------------------------------------------------------------------------- /jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/grafana/jsonnet-libs", 8 | "subdir": "ksonnet-util" 9 | } 10 | }, 11 | "version": "master" 12 | }, 13 | { 14 | "source": { 15 | "git": { 16 | "remote": "https://github.com/ksonnet/ksonnet-lib", 17 | "subdir": "ksonnet.beta.4" 18 | } 19 | }, 20 | "version": "master" 21 | } 22 | ], 23 | "legacyImports": true 24 | } 25 | -------------------------------------------------------------------------------- /jsonnetfile.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/grafana/jsonnet-libs", 8 | "subdir": "ksonnet-util" 9 | } 10 | }, 11 | "version": "7ac7da1a0fe165b68cdb718b2521b560d51bd1f4", 12 | "sum": "LKsTTBcH8TXX5ANgRUu5I7Y1tf5le4nANFV3/W53I+c=" 13 | }, 14 | { 15 | "source": { 16 | "git": { 17 | "remote": "https://github.com/ksonnet/ksonnet-lib", 18 | "subdir": "ksonnet.beta.4" 19 | } 20 | }, 21 | "version": "0d2f82676817bbf9e4acf6495b2090205f323b9f", 22 | "sum": "ur22hPQq0JAPBxm8hNMcwjumj4MkozDwOKiGZvVMYh4=" 23 | } 24 | ], 25 | "legacyImports": false 26 | } 27 | -------------------------------------------------------------------------------- /lib/deeployer/docker-compose-generator.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | local generateContainer = function(deploymentName, data) 3 | { 4 | image: data.image, 5 | }, 6 | 7 | local hasHost = function(containers) std.length(std.filter 8 | ( 9 | function(deploymentName) 10 | std.objectHas(containers[deploymentName], 'host'), 11 | std.objectFields(containers) 12 | )) > 0, 13 | 14 | deeployer+:: { 15 | generateDockerCompose(config):: { 16 | version: '3', 17 | 18 | services: std.mapWithKey(generateContainer, config.containers) + 19 | if hasHost(config.containers) then { 20 | traefik: { 21 | image: 'traefik:2', 22 | command: [ 23 | // "--api.insecure=true", 24 | // "--api.dashboard=true", 25 | '--providers.docker', 26 | '--providers.docker.exposedByDefault=false', 27 | ], 28 | ports: ['80:80'], 29 | volumes: ['/var/run/docker.sock:/var/run/docker.sock'], 30 | }, 31 | } else {}, 32 | }, 33 | }, 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /lib/deeployer/resource_generator.libsonnet: -------------------------------------------------------------------------------- 1 | (import 'ksonnet-util/kausal.libsonnet') + 2 | 3 | { 4 | // declaring ressource types 5 | local deployment = $.apps.v1.deployment, 6 | local service = $.core.v1.service, 7 | local ingress = $.extensions.v1beta1.ingress, 8 | local ingressRule = ingress.mixin.spec.rulesType, 9 | local container = $.core.v1.container, 10 | local containerPort = $.core.v1.containerPort, 11 | local ImagePullSecret = $.apps.v1.deployment.mixin.spec.template.spec.imagePullSecretsType, 12 | local env = $.core.v1.container.envType, 13 | local envFrom = $.core.v1.container.envFromSource, 14 | local volumeMount = $.core.v1.container.volumeMountsType, 15 | local volume = $.apps.v1.deployment.mixin.spec.template.spec.volumesType, 16 | local pvc = $.core.v1.persistentVolumeClaim, 17 | local resources = $.core.v1.container.resourcesType, 18 | local httpIngressPath = ingressRule.mixin.http.pathsType, 19 | 20 | /** 21 | * Returns the list of ports to listen to by merging "ports" with the "containerPort" of the host section 22 | */ 23 | local getPorts = function(container) 24 | std.set( // ports are a "set" (an array of unique values) 25 | (if std.objectHas(container, 'ports') then container.ports else []) + 26 | // TODO: if host is defined and containerPort is not defined, put port 80 unless "ports" has only 1 element 27 | if std.objectHas(container, 'host') && std.objectHas(container.host, 'containerPort') then [container.host.containerPort] else [] 28 | ), 29 | 30 | /** 31 | * This function is written as a middleware to be easily plugged at the beginning of the script 32 | */ 33 | local checkVersionNumber = function(config, currentVersionNumber, next) 34 | if !(config.version == currentVersionNumber) then 35 | error 'Mismatch in version number' 36 | else 37 | next 38 | , 39 | 40 | local getHttpPort = function(container, deploymentName) 41 | if !std.objectHas(container, 'host') then 42 | error 'Unexpected call to getHttpPort if there is no host: ' + container 43 | else if std.objectHas(container.host, 'containerPort') then container.host.containerPort 44 | else 45 | if getPorts(container) == [] then error 'For container "' + deploymentName + '", host "' + container.host.url + '" needs a port to bind to. Please provide a containerPort in the "host" section.' 46 | else if std.length(getPorts(container)) > 1 then error ' For service "' + deploymentName + '", there is a host defined but several ports open. PleaseNouvelle liste provide a containerPort in the "host" section.' 47 | else getPorts(container)[0] 48 | , 49 | 50 | /** 51 | * Returns true of the container passed in parameter requires https, false otherwise 52 | */ 53 | local containerHasHttps = function(container) 54 | if std.objectHas(container, 'host') then 55 | if !std.objectHas(container.host, 'https') then 56 | false 57 | else 58 | if container.host.https == 'disable' then 59 | false 60 | else 61 | true 62 | else 63 | false, 64 | 65 | /** 66 | * Returns true if at least one container requires HTTPS 67 | */ 68 | local environmentRequiresHttps = function(containers) 69 | std.length(std.filter(function(containerName) containerHasHttps(containers[containerName]), std.objectFields(containers))) > 0, 70 | 71 | local f = function(config, deploymentName, data) 72 | { 73 | 74 | deployment: deployment.new( 75 | name=deploymentName, 76 | replicas=if (std.objectHas(data, 'replicas')) then data.replicas else 1, 77 | containers=[ 78 | container.new(deploymentName, data.image) + 79 | (if std.objectHas(data, 'ports') then container.withPorts([containerPort.new('p' + port, port) for port in data.ports]) else {}) 80 | + 81 | container.withImagePullPolicy('Always') + 82 | //container.withEnv([env.mixin.valueFrom.secretKeyRef.withName(key).withKey(data.envFrom.secretKeyRef[key]) for key in std.objectFields(data.envFrom.secretKeyRef) ],) + 83 | (if std.objectHas(data, 'env') then 84 | container.withEnv([env.new(key, data.env[key]) for key in std.objectFields(data.env)]) 85 | else {}) 86 | + 87 | (if std.objectHas(data, 'volumes') then 88 | container.withVolumeMounts([volumeMount.new(volumeName, mountPath=data.volumes[volumeName].mountPath, readOnly=false) for volumeName in std.objectFields(data.volumes)]) 89 | else {}) 90 | + 91 | (if std.objectHas(data, 'quotas') then 92 | container.mixin.resources.withRequests(data.quotas.min).withLimits(data.quotas.max) 93 | else {}), 94 | ] 95 | ) + 96 | ( 97 | if (std.objectHas(data, 'replicas') && data.replicas > 1) then 98 | deployment.mixin.spec.strategy.withType('RollingUpdate') 99 | else 100 | deployment.mixin.spec.strategy.withType('Recreate') 101 | ) + 102 | (if std.objectHas(config, 'config') && std.objectHas(config.config, 'registryCredentials') then 103 | deployment.mixin.spec.template.spec.withImagePullSecrets([(ImagePullSecret.new() + ImagePullSecret.withName('a' + std.md5(registryUrl))) for registryUrl in std.objectFields(config.config.registryCredentials)],) 104 | else {}) 105 | + 106 | // we add the current date to a random label to force a redeployment, even if the container name did not change, 107 | // unless we find redeploy: onConfigChange 108 | (if !std.objectHas(data, 'redeploy') || data.redeploy != 'onConfigChange' then 109 | deployment.mixin.spec.template.metadata.withLabelsMixin({ deeployerTimestamp: std.extVar('timestamp') }) 110 | else {}), 111 | 112 | //std.mapWithKey(fv, data.volumes), 113 | } + (if std.objectHas(data, 'volumes') then { 114 | deployment+: deployment.mixin.spec.template.spec.withVolumes([volume.fromPersistentVolumeClaim(volumeName, volumeName + '-pvc') for volumeName in std.objectFields(data.volumes)]), 115 | } else {}) 116 | + ( 117 | if std.objectHas(data, 'ports') then 118 | { service: $.util.serviceFor(self.deployment, ['deeployerTimestamp']) } 119 | else {} 120 | ) 121 | + ( 122 | if std.objectHas(data, 'host') then 123 | { 124 | service: $.util.serviceFor(self.deployment, ['deeployerTimestamp']), 125 | ingress: ingress.new() + 126 | ingress.mixin.metadata.withName('ingress-' + deploymentName) + 127 | { 128 | apiVersion: 'networking.k8s.io/v1', 129 | } + 130 | //ingress.mixin.metadata.withLabels(data.labels)+ 131 | (if containerHasHttps(data) then 132 | ingress.mixin.metadata.withAnnotations({ 133 | 'cert-manager.io/issuer': 'letsencrypt-prod', 134 | }) 135 | else 136 | {}) 137 | 138 | + 139 | 140 | ingress.mixin.spec.withRules([ingressRule.new() + 141 | ingressRule.withHost(data.host.url) + 142 | ingressRule.mixin.http.withPaths( 143 | httpIngressPath.new() + 144 | { 145 | backend: { 146 | service: { 147 | name: deploymentName, 148 | port: { 149 | number: getHttpPort(data, deploymentName), 150 | }, 151 | }, 152 | }, 153 | pathType: 'ImplementationSpecific', 154 | } 155 | )],) 156 | + if containerHasHttps(data) then 157 | { 158 | spec+: { 159 | tls: [{ 160 | hosts: [data.host.url], 161 | secretName: 'ingress-secret-' + deploymentName, 162 | }], 163 | }, 164 | } 165 | else 166 | {}, 167 | } 168 | 169 | else { service: $.util.serviceFor(self.deployment, ['deeployerTimestamp']) } 170 | 171 | ) + (if std.objectHas(data, 'volumes') then { 172 | pvcs: std.mapWithKey(function(pvcName, pvcData) { apiVersion: 'v1', kind: 'PersistentVolumeClaim' } + 173 | pvc.mixin.metadata.withName(pvcName + '-pvc') + 174 | pvc.mixin.spec.withAccessModes('ReadWriteOnce',) + 175 | pvc.mixin.spec.resources.withRequests({ storage: pvcData.diskSpace }), 176 | data.volumes), 177 | } else {}), 178 | 179 | local issuer = function(config) 180 | if environmentRequiresHttps(config.containers) then 181 | { 182 | issuer: { 183 | 184 | apiVersion: 'cert-manager.io/v1', 185 | kind: 'Issuer', 186 | metadata: { 187 | name: 'letsencrypt-prod', 188 | }, 189 | spec: { 190 | acme: { 191 | email: if std.objectHas(config, 'config') && std.objectHas(config.config, 'https') && std.objectHas(config.config.https, 'mail') then config.config.https.mail else error 'In order to have support for HTTPS, you need to provide an email address in the { "config": { "https": { "mail": "some@email.com" } } }', 192 | server: 'https://acme-v02.api.letsencrypt.org/directory', 193 | privateKeySecretRef: { 194 | name: 'letsencrypt-prod-secret', 195 | }, 196 | solvers: [ 197 | { 198 | http01: { 199 | ingress: { 200 | class: 'nginx', 201 | }, 202 | }, 203 | }, 204 | ], 205 | }, 206 | }, 207 | 208 | }, 209 | } 210 | else 211 | {} 212 | , 213 | deeployer:: { 214 | generateResourcesWithoutExtension(config):: 215 | local generateContainer = function(deploymentName, data) f(config, deploymentName, data); 216 | checkVersionNumber(config, '1.0', std.mapWithKey(generateContainer, config.containers) + issuer(config)), 217 | generateResources(config):: if std.objectHas(config, 'config') && std.objectHasAll(config.config, 'k8sextension') && std.isFunction(config.config.k8sextension) then 218 | config.config.k8sextension(self.generateResourcesWithoutExtension(config)) 219 | else 220 | self.generateResourcesWithoutExtension(config), 221 | }, 222 | 223 | } 224 | -------------------------------------------------------------------------------- /lib/k.libsonnet: -------------------------------------------------------------------------------- 1 | import '../vendor/ksonnet.beta.4/k.libsonnet' 2 | -------------------------------------------------------------------------------- /lib/php/commands/DownCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Todo the description') 20 | ; 21 | } 22 | 23 | protected function execute(InputInterface $input, OutputInterface $output): int 24 | { 25 | $message = 'Stopping the containers'; 26 | $tmpFilePath = ComposeFileGenerator::TmpFilePath; 27 | //todo: should we regenerate the temporary docker-compose file before shutting down the containers? 28 | $command = "docker-compose -f $tmpFilePath down"; 29 | $output->writeln($message); 30 | Executor::execute($command); 31 | return 0; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /lib/php/commands/ExportCommand.php: -------------------------------------------------------------------------------- 1 | configFileFinder = new ConfigFileFinder(); 34 | $this->configGenerator = new ConfigGenerator(); 35 | $this->composeFileGenerator = new ComposeFileGenerator(); 36 | } 37 | 38 | protected function configure(): void 39 | { 40 | $this->setDescription('Export your config in a docker-compose file'); 41 | } 42 | 43 | protected function execute(InputInterface $input, OutputInterface $output) 44 | { 45 | $output->writeln('Exporting your config in docker-compose format...'); 46 | $path = $this->configFileFinder->findFile(); 47 | $config = $this->configGenerator->getConfig($path); 48 | $dockerComposeConfig = $this->composeFileGenerator->createDockerComposeConfig($config); 49 | $code = file_put_contents('docker-compose.json', json_encode($dockerComposeConfig, JSON_PRETTY_PRINT)); 50 | if ($code === false) { 51 | throw new \RuntimeException('Error when creating the docker-compose file.'); 52 | } 53 | $output->writeln('Done! docker-compose.json was created.'); 54 | return 0; 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /lib/php/commands/UpCommand.php: -------------------------------------------------------------------------------- 1 | configFileFinder = new ConfigFileFinder(); 36 | $this->configGenerator = new ConfigGenerator(); 37 | $this->composeFileGenerator = new ComposeFileGenerator(); 38 | } 39 | 40 | protected function configure(): void 41 | { 42 | $this->setDescription('Todo the description') 43 | ->addOption( 44 | 'detach', 45 | 'd', 46 | InputOption::VALUE_NONE, 47 | 'Run the command in detach mode' 48 | ); 49 | } 50 | 51 | protected function execute(InputInterface $input, OutputInterface $output) 52 | { 53 | $path = $this->configFileFinder->findFile(); 54 | $config = $this->configGenerator->getConfig($path); 55 | $filePath = $this->composeFileGenerator->createFile($config); 56 | 57 | $detachMode = (bool) $input->getOption('detach'); 58 | 59 | $message = 'Starting the containers'; 60 | $comand = "docker-compose -f $filePath up"; 61 | if ($detachMode) { 62 | $message .= ' in detached mode'; 63 | $comand .= ' -d'; 64 | } 65 | $output->writeln($message); 66 | Executor::execute($comand); 67 | return 0; 68 | 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /lib/php/utils/ComposeFileGenerator.php: -------------------------------------------------------------------------------- 1 | createDockerComposeConfig($deeployerConfig); 16 | $returnCode = file_put_contents(self::TmpFilePath, json_encode($dockerFileConfig, JSON_UNESCAPED_SLASHES)); 17 | if ($returnCode === false) { 18 | throw new \RuntimeException('Error when trying to create the docker-compose file'); 19 | } 20 | if (isset($deeployerConfig['config']['dynamic'])) { 21 | $this->generateDynamicConfigFunction($deeployerConfig['config']['dynamic']); 22 | $this->editConfig(); 23 | 24 | } 25 | // exit; 26 | //dynamically edit config file with libsonnet 27 | 28 | return self::TmpFilePath; 29 | } 30 | 31 | public function editConfig(): void 32 | { 33 | $output = ""; 34 | $path = __DIR__.'/../../../scripts/main-compose.jsonnet'; 35 | exec ("jsonnet $path", $output); 36 | file_put_contents(self::TmpFilePath, $output); 37 | } 38 | 39 | private function generateDynamicConfigFunction(string $dynamicFunctionConfig): void 40 | { 41 | $dynamicFunction = "{\n$dynamicFunctionConfig\n}\n"; 42 | // $dynamicFunction = $dynamicFunctionConfig; 43 | file_put_contents("/tmp/dynamic-function.libsonnet", $dynamicFunction); 44 | } 45 | 46 | 47 | private function httpsChecker(array $deeployerConfig): bool 48 | { 49 | foreach ($deeployerConfig['containers'] as $serviceName => $service){ 50 | if (isset ($service['host']['https']) && $service['host']['https'] == true){ 51 | return true; 52 | } 53 | } 54 | return false; 55 | } 56 | 57 | private function httpChecker(array $deeployerConfig): bool 58 | { 59 | foreach ($deeployerConfig['containers'] as $serviceName => $service){ 60 | if (isset ($service['host']) ){ 61 | return true; 62 | } 63 | } 64 | return false; 65 | } 66 | 67 | public function createTraefikConf(array $deeployerConfig ): array 68 | { 69 | $HttpTraefikConfig=[ 70 | "image" => "traefik:2.0", 71 | "command" => [ 72 | "--global.sendAnonymousUsage=false", 73 | "--log.level=DEBUG", 74 | "--providers.docker=true", 75 | "--providers.docker.exposedbydefault=false", 76 | "--providers.docker.swarmMode=false", 77 | "--entrypoints.web.address=:80", 78 | // "--providers.docker.endpoint=\"unix:///var/run/docker.sock\"", 79 | ], 80 | "ports" => [ 81 | "80:80", 82 | ], 83 | "volumes" => [ 84 | "/var/run/docker.sock:/var/run/docker.sock", 85 | ] 86 | ]; 87 | 88 | if ( $this->httpsChecker($deeployerConfig) == true){ 89 | $HttpTraefikConfig['ports'][]= '443:443'; 90 | if (!isset ($deeployerConfig['config']['https']['mail'])) { 91 | throw new \RuntimeException('Error you need to set in the config section of your file the mail field'); 92 | } 93 | $HttpTraefikConfig['command'][]= "--entrypoints.websecured.address=:443"; 94 | $HttpTraefikConfig['command'][]= "--certificatesresolvers.letsencrypt.acme.email=".$deeployerConfig['config']['https']['mail']; 95 | $HttpTraefikConfig['command'][]= "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"; 96 | $HttpTraefikConfig['command'][]= "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"; 97 | $HttpTraefikConfig['command'][]= "--certificatesresolvers.letsencrypt.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory"; 98 | $HttpTraefikConfig['volumes'][]= "./conf/traefik/acme.json:/acme.json"; 99 | } 100 | return $HttpTraefikConfig; 101 | } 102 | 103 | public function createTraefikLabels(array $hostConfig, string $serviceName): array 104 | { 105 | $host = $hostConfig['url']; 106 | $httpLabels = [ 107 | 'traefik.enable=true', 108 | "traefik.http.routers.$serviceName.rule=Host(`$host`)" 109 | ]; 110 | if (isset($hostConfig['https']) && $hostConfig['https'] == "enable") { 111 | $httpLabels[] = "traefik.http.routers.$serviceName.entrypoints=websecured"; 112 | } 113 | return $httpLabels; 114 | } 115 | 116 | public function createServiceConfig(array $containerConfig): array 117 | { 118 | 119 | //What is the utility of the next 2 lines ?? 120 | $dockerComposeConfig = [ 121 | 'image' => $containerConfig['image'], 122 | // 'ports' => $containerConfig['ports'], 123 | // 'environment' => $containerConfig['env'], 124 | // 'volumes' => $containerConfig['volumes'] 125 | ]; 126 | 127 | //Added ports 128 | if (isset($containerConfig['ports'])) { 129 | $dockerComposeConfig['ports'] = []; 130 | foreach ($containerConfig['ports'] as $portsSet => $portsValue) { 131 | $dockerComposeConfig['ports'][$portsSet] = $portsValue; 132 | } 133 | } 134 | 135 | if (isset($containerConfig['env'])) { 136 | $dockerComposeConfig['environment'] = []; 137 | foreach ($containerConfig['env'] as $envVariableName => $envVariableValue) { 138 | $dockerComposeConfig['environment'][$envVariableName] = $envVariableValue; 139 | } 140 | } 141 | 142 | // Set volumes for container 143 | if (isset($containerConfig['volumes'])) { 144 | $dockerComposeConfig['volumes'] = []; 145 | foreach ($containerConfig['volumes'] as $volumeName => $volumeValue) { 146 | $mountPath = $volumeValue['mountPath']; 147 | $dockerComposeConfig['volumes'][] = $volumeName.":".$mountPath; 148 | } 149 | } 150 | 151 | // Setting command feature 152 | if (isset($containerConfig['command'])) { 153 | $dockerComposeConfig['command'] = []; 154 | foreach ($containerConfig['command'] as $commandValue) { 155 | $dockerComposeConfig['command'] = $commandValue; 156 | } 157 | } 158 | 159 | return $dockerComposeConfig; 160 | } 161 | 162 | // Create volumes 163 | public function createVolumeConfig( array $deeployerConfig ): array 164 | { 165 | $driver = ['driver' => 'local']; 166 | $volumesConfig = [] ; 167 | foreach ($deeployerConfig['containers'] as $serviceName => $service) { 168 | if (isset($service['volumes'])) { 169 | foreach ($service['volumes'] as $volumeName => $volume ) { 170 | $volumesConfig[$volumeName]= $driver; 171 | } 172 | } 173 | } 174 | return $volumesConfig; 175 | } 176 | 177 | public function createDockerComposeConfig(array $deeployerConfig ): array 178 | { 179 | $dockerComposeConfig = []; 180 | 181 | $dockerComposeConfig['version'] = "3.3"; 182 | 183 | if ($this->httpChecker($deeployerConfig)== true){ 184 | $dockerComposeConfig['services'] = [ 185 | 'traefik' => $this->createTraefikConf($deeployerConfig) 186 | ]; 187 | } 188 | 189 | foreach ($deeployerConfig['containers'] as $serviceName => $containerConfig) { 190 | $serviceConfig = $this->createServiceConfig($containerConfig); 191 | if ($this->httpChecker($deeployerConfig)== true){ 192 | if (isset ($containerConfig['host'])) { 193 | $serviceConfig['labels'] = $this->createTraefikLabels($containerConfig['host'], $serviceName); 194 | } 195 | } 196 | $dockerComposeConfig['services'][$serviceName] = $serviceConfig; 197 | } 198 | 199 | $volumesConfig = $this->createVolumeConfig($deeployerConfig); // Need to put this in a variable 200 | if (!empty($volumesConfig)){ 201 | $dockerComposeConfig['volumes'] = $volumesConfig; 202 | } 203 | 204 | return $dockerComposeConfig; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /lib/php/utils/ConfigFileFinder.php: -------------------------------------------------------------------------------- 1 | $tmpJsonFilePath"); 22 | $schemaFilePath = self::schemaFilePath; 23 | Executor::execute("ajv test -s $schemaFilePath -d $tmpJsonFilePath --valid -c ajv-formats"); 24 | 25 | $content = file_get_contents($tmpJsonFilePath); 26 | if ($content === false) { 27 | throw new \RuntimeException("Error when reading $tmpJsonFilePath"); 28 | } 29 | $json = json_decode($content, true); 30 | if ($json === false) { 31 | throw new \RuntimeException('Error when decoding json'); 32 | } 33 | return $json; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /lib/php/utils/EnvironmentFetcher.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | tests/php 17 | 18 | 19 | 20 | 21 | 22 | lib/php 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /scripts/deeployer-compose.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new ExportCommand()); 15 | $application->add(new UpCommand()); 16 | $application->add(new DownCommand()); 17 | 18 | $application->run(); 19 | -------------------------------------------------------------------------------- /scripts/deeployer-k8s: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | usage() 6 | { 7 | echo "usage: deeployer-k8s show [deeployer.json] | [-h]" 8 | echo " deeployer-k8s apply --namespace somenamespace [--no-wait] [--timeout 600s] [deeployer.json]" 9 | echo " deeployer-k8s delete --namespace somenamespace" 10 | } 11 | 12 | # Running this at the very beginning, before we declare any environment variable 13 | if [[ "$JSON_ENV" == "" ]]; then 14 | JSON_ENV=$(jq -n env) 15 | fi 16 | 17 | NAMESPACE= 18 | # A default timeout of 10 minutes should leave some room for cluster autoscaling if needed 19 | TIMEOUT=600s 20 | WAIT=1 21 | 22 | # DEEPLOYER_FILE contains ./deeployer.libsonnet (if this file exists), or ./deeployer.json (if this file exists) or empty 23 | DEEPLOYER_FILE=./deeployer.libsonnet 24 | DEEPLOYER_FILE_SPECIFIED=0 25 | 26 | if [ ! -f "$DEEPLOYER_FILE" ]; then 27 | DEEPLOYER_FILE=./deeployer.json 28 | if [ ! -f "$DEEPLOYER_FILE" ]; then 29 | DEEPLOYER_FILE= 30 | fi 31 | fi 32 | 33 | if [ "$1" == "" ]; then 34 | usage 35 | exit 1 36 | fi 37 | 38 | COMMAND="$1" 39 | shift 40 | 41 | # See http://linuxcommand.org/lc3_wss0120.php for explanation 42 | 43 | while [ "$1" != "" ]; do 44 | case $1 in 45 | -n | --namespace ) shift 46 | NAMESPACE=$1 47 | ;; 48 | -t | --timeout ) shift 49 | TIMEOUT=$1 50 | ;; 51 | --no-wait ) shift 52 | WAIT=0 53 | ;; 54 | -h | --help ) usage 55 | exit 56 | ;; 57 | * ) if [ "$DEEPLOYER_FILE_SPECIFIED" == "1" ]; then usage; exit; fi; 58 | DEEPLOYER_FILE_SPECIFIED=1 59 | DEEPLOYER_FILE=$1 60 | esac 61 | shift 62 | done 63 | 64 | if [[ "$COMMAND" != "delete" ]]; then 65 | if [[ "$DEEPLOYER_FILE" == "" ]]; then 66 | echo "Cannot find deeployer.libsonnet or deeployer.json in the current directory. Create those files or pass the name of the deeployer configuration file in argument to deeployer-k8s." 67 | exit 1; 68 | fi 69 | if [ ! -f "$DEEPLOYER_FILE" ]; then 70 | echo "Cannot find deeployer configuration file \"$DEEPLOYER_FILE\"" 71 | exit 1; 72 | fi 73 | 74 | # Get the directory of the script (resolving any symlink) 75 | DIR="$( cd "$( dirname $(realpath "${BASH_SOURCE[0]}") )" >/dev/null 2>&1 && pwd -P )" 76 | 77 | # Let's validate the Deeployer file 78 | jsonnet $DEEPLOYER_FILE --ext-code "env=$JSON_ENV" --ext-str timestamp="2020-05-05 00:00:00" > /tmp/deeployer.json 79 | set +e 80 | OUTPUT=`ajv test -s $DIR/../deeployer.schema.json -d /tmp/deeployer.json --valid -c ajv-formats` 81 | if [[ $? != 0 ]]; then 82 | echo "$OUTPUT" 83 | echo -e "\e[31m❌\e[39m Your configuration file '$DEEPLOYER_FILE' has an invalid format. See the message above for details." 84 | exit 1 85 | fi 86 | set -e 87 | 88 | config=$(cat "$DEEPLOYER_FILE") 89 | 90 | TIMESTAMP=$(date +%s) 91 | fi 92 | 93 | if [[ "$COMMAND" == "apply" ]]; then 94 | if [[ "$NAMESPACE" == "" ]]; then 95 | echo "When using 'apply', you must pass a namespace in parameter" 96 | echo " deeployer-k8s apply --namespace foobar" 97 | exit 1; 98 | fi 99 | 100 | # Let's connect (if we are in the Docker container) 101 | if [[ -f "/usr/local/bin/connect" ]]; then 102 | /usr/local/bin/connect 103 | fi 104 | 105 | # Let's get the current cluster 106 | CONTEXT_NAME=$(kubectl config current-context) 107 | 108 | tk env set --namespace "$NAMESPACE" --server-from-context "$CONTEXT_NAME" "$DIR" 109 | 110 | # Let's create the namespace if it does not exists 111 | kubectl create namespace "$NAMESPACE" || true 112 | 113 | php /deeployer/scripts/runParseConfigForCredentials.php /tmp/deeployer.json "$NAMESPACE" | bash 114 | 115 | tk apply --extCode "config=$config" --extCode "env=$JSON_ENV" --extVar "timestamp=$TIMESTAMP" --dangerous-auto-approve "$DIR" 116 | 117 | if [[ "$WAIT" == "1" ]]; then 118 | kubectl -n "$NAMESPACE" wait deployment --all=true --for=condition=Available --timeout=$TIMEOUT 119 | fi 120 | elif [[ "$COMMAND" == "delete" ]]; then 121 | if [[ "$NAMESPACE" == "" ]]; then 122 | echo "When using 'delete', you must pass a namespace in parameter" 123 | echo " deeployer-k8s delete --namespace foobar" 124 | exit 1; 125 | fi 126 | 127 | # Let's connect (if we are in the Docker container) 128 | if [[ -f "/usr/local/bin/connect" ]]; then 129 | /usr/local/bin/connect 130 | fi 131 | 132 | # Let's delete the namespace 133 | kubectl delete namespace "$NAMESPACE" 134 | elif [[ "$COMMAND" == "show" ]]; then 135 | # --dangerous-allow-redirect is needed for using the command in a Docker container 136 | tk show --extCode "config=$config" --extCode "env=$JSON_ENV" --extVar "timestamp=$TIMESTAMP" "$DIR" --dangerous-allow-redirect 137 | else 138 | tk $COMMAND --extCode "config=$config" --extCode "env=$JSON_ENV" --extVar "timestamp=$TIMESTAMP" "$DIR" 139 | fi 140 | -------------------------------------------------------------------------------- /scripts/docker-compose.jsonnet: -------------------------------------------------------------------------------- 1 | (import 'deeployer/docker-compose-generator.libsonnet') + 2 | 3 | { 4 | docker_compose: $.deeployer.generateDockerCompose(std.extVar('config')), 5 | } 6 | -------------------------------------------------------------------------------- /scripts/main-compose.jsonnet: -------------------------------------------------------------------------------- 1 | local generatedCompose = (import '/tmp/docker-compose.json'); 2 | local deeployer = (import '/tmp/dynamic-function.libsonnet'); 3 | 4 | 5 | deeployer.composeExtension(generatedCompose) 6 | -------------------------------------------------------------------------------- /scripts/main.jsonnet: -------------------------------------------------------------------------------- 1 | // environments/prom-grafana/prod 2 | (import 'ksonnet-util/kausal.libsonnet') + 3 | (import 'deeployer/resource_generator.libsonnet') + 4 | 5 | { 6 | local config = std.extVar('config'), 7 | local deeployer = $.deeployer, 8 | 9 | 10 | generatedConf: deeployer.generateResources(config), 11 | 12 | } 13 | -------------------------------------------------------------------------------- /scripts/parseConfigForCredentials.php: -------------------------------------------------------------------------------- 1 | $credentialsData) { 9 | $slugifiedName = 'a'.md5($url); 10 | $name = $credentialsData['user']; 11 | $password = $credentialsData['password']; 12 | 13 | yield "kubectl -n $namespace delete secret $slugifiedName\n"; 14 | yield "kubectl -n $namespace create secret docker-registry $slugifiedName --docker-server=$url --docker-username=$name --docker-password='$password' --docker-email=$name\n"; 15 | } 16 | 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /scripts/runParseConfigForCredentials.php: -------------------------------------------------------------------------------- 1 | '1.0', 17 | "containers" => [ 18 | "php" => [ 19 | "host" => [ 20 | 'url' => 'myhost.com', 21 | ], 22 | "image" => "thecodingmachine/php:7.4-v3-apache", 23 | ] 24 | ] 25 | ]; 26 | $result = $generator->createTraefikConf($config); 27 | $expected = [ 28 | "image" => "traefik:2.0", 29 | "command" => [ 30 | "--global.sendAnonymousUsage=false", 31 | "--log.level=DEBUG", 32 | "--providers.docker=true", 33 | "--providers.docker.exposedbydefault=false", 34 | "--providers.docker.swarmMode=false", 35 | "--entrypoints.web.address=:80", 36 | ], 37 | "ports" => [ 38 | "80:80" 39 | ], 40 | "volumes" => [ 41 | "/var/run/docker.sock:/var/run/docker.sock" 42 | ] 43 | ]; 44 | $this->assertEquals($expected, $result); 45 | } 46 | 47 | public function testTraefikConfigWithHttps(): void 48 | { 49 | $generator = new ComposeFileGenerator(); 50 | $config = [ 51 | "version" => '3.3', 52 | "containers" => [ 53 | "php" => [ 54 | "host" => [ 55 | 'url' => 'myhost.com', 56 | 'https' => 'enable' 57 | ], 58 | "image" => "thecodingmachine/php:7.4-v3-apache", 59 | ] 60 | ], 61 | "config" => [ 62 | "https" => [ 63 | "mail" => "dt@thecodingmachine.com" 64 | ] 65 | ] 66 | ]; 67 | $expected = [ 68 | "image" => "traefik:2.0", 69 | "command" => [ 70 | "--global.sendAnonymousUsage=false", 71 | "--log.level=DEBUG", 72 | "--providers.docker=true", 73 | "--providers.docker.exposedbydefault=false", 74 | "--providers.docker.swarmMode=false", 75 | "--entrypoints.web.address=:80", 76 | "--entrypoints.websecured.address=:443", 77 | "--certificatesresolvers.letsencrypt.acme.email=dt@thecodingmachine.com", 78 | "--certificatesresolvers.letsencrypt.acme.storage=/acme.json", 79 | "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web", 80 | "--certificatesresolvers.letsencrypt.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory" 81 | 82 | ], 83 | "ports" => [ 84 | "80:80", 85 | "443:443" 86 | ], 87 | "volumes" => [ 88 | "/var/run/docker.sock:/var/run/docker.sock", 89 | "./conf/traefik/acme.json:/acme.json" 90 | ] 91 | ]; 92 | $result = $generator->createTraefikConf($config); 93 | $this->assertEquals($expected, $result); 94 | } 95 | 96 | 97 | public function testServiceConfigWithoutLabel(): void 98 | { 99 | $generator = new ComposeFileGenerator(); 100 | $config = [ 101 | 'image' => 'myimage' 102 | ]; 103 | $result = $generator->createServiceConfig($config); 104 | $this->assertEquals([ 105 | 'image' => 'myimage' 106 | ], $result); 107 | } 108 | 109 | public function testTraefikLabelsConfigWithHttps(): void 110 | { 111 | $generator = new ComposeFileGenerator(); 112 | $Config = array ( 113 | 'version' => '1.0', 114 | '$schema' => '../deeployer.schema.json', 115 | 'containers' => 116 | array ( 117 | 'phpmyadmin' => 118 | array ( 119 | 'image' => 'phpmyadmin/phpmyadmin', 120 | 'host' => 121 | array ( 122 | 'url' => 'myhost.com', 123 | 'https' => 'true' 124 | ), 125 | ), 126 | ), 127 | ); 128 | $hostConfig = [ 129 | 'url' => 'myhost.com', 130 | 'https' => 'enable' 131 | ]; 132 | $result = $generator->createTraefikLabels($hostConfig, 'phpmyadmin'); 133 | $expected = [ 134 | "traefik.enable=true", 135 | "traefik.http.routers.phpmyadmin.rule=Host(`myhost.com`)", 136 | "traefik.http.routers.phpmyadmin.entrypoints=websecured" 137 | ]; 138 | $this->assertEquals($expected, $result); 139 | } 140 | 141 | public function testServiceConfigWithVolumes(): void 142 | { 143 | $generator = new ComposeFileGenerator(); 144 | $config = [ 145 | 'image' => 'myimage', 146 | 'volumes' => [ 147 | 'mysql_data' => [ 148 | 'diskSpace' => '1G', 149 | 'mountPath' => '/var/lib/mysql', 150 | ] 151 | ] 152 | ]; 153 | $result = $generator->createServiceConfig($config); 154 | $this->assertEquals([ 155 | 'image' => 'myimage', 156 | 'volumes' => [ 157 | 'mysql_data:/var/lib/mysql', 158 | ] 159 | ], $result); 160 | } 161 | 162 | public function testServiceConfigWithEnvVariables(): void 163 | { 164 | $generator = new ComposeFileGenerator(); 165 | $config = [ 166 | 'image' => 'myimage', 167 | 'env' => [ 168 | 'startup_command' => 'yarn build', 169 | 'dummy_variable' => 'dummy_value' 170 | ] 171 | ]; 172 | $result = $generator->createServiceConfig($config); 173 | $this->assertEquals([ 174 | 'image' => 'myimage', 175 | 'environment' => [ 176 | 'startup_command' => 'yarn build', 177 | 'dummy_variable' => 'dummy_value' 178 | ] 179 | ], $result); 180 | } 181 | 182 | public function testTraefikLabelsConfig(): void 183 | { 184 | $generator = new ComposeFileGenerator(); 185 | $hostConfig = [ 186 | 'url' => 'myhost.com' 187 | ]; 188 | $result = $generator->createTraefikLabels($hostConfig, 'mysql'); 189 | $expected = [ 190 | 'traefik.enable=true', 191 | 'traefik.http.routers.mysql.rule=Host(`myhost.com`)' 192 | ]; 193 | $this->assertEquals($expected, $result); 194 | } 195 | 196 | public function testCreatedConfigServicesNames(): void 197 | { 198 | $generator = new ComposeFileGenerator(); 199 | 200 | $config = [ 201 | "version" => '1.0', 202 | "containers" => [ 203 | "php" => [ 204 | "host" => [ 205 | 'url' => 'myhost.com', 206 | 'https' => 'true' 207 | ], 208 | "image" => "thecodingmachine/php:7.4-v3-apache", 209 | ], 210 | "mysql" => [ 211 | "image" => "mysql:5.8" 212 | ], 213 | "phpmyadmin" => [ 214 | "host" => [ 215 | 'url' => 'phpmyadmin.myhost.com' 216 | ], 217 | "image" => "phpmyadmin", 218 | "ports" => [ 219 | 0 => 80 220 | ] 221 | ] 222 | 223 | ], 224 | "config" => [ 225 | "https" => [ 226 | "mail" => "m.diallo@thecodingmachine.com" 227 | ] 228 | 229 | ] 230 | ]; 231 | $createdConfig = $generator->createDockerComposeConfig($config); 232 | $this->assertTrue(isset($createdConfig['services']['traefik'])); 233 | $this->assertTrue(isset($createdConfig['services']['php'])); 234 | $this->assertTrue(isset($createdConfig['services']['mysql'])); 235 | $this->assertTrue(isset($createdConfig['services']['phpmyadmin'])); 236 | 237 | } 238 | 239 | public function testHost(): void 240 | { 241 | $generator = new ComposeFileGenerator(); 242 | 243 | $config = [ 244 | "version" => '1.0', 245 | "containers" => [ 246 | "php" => [ 247 | "host" => [ 248 | 'url' => 'myhost.com' 249 | ], 250 | "image" => "thecodingmachine/php:7.4-v3-apache", 251 | ] 252 | ] 253 | ]; 254 | $createdConfig = $generator->createDockerComposeConfig($config); 255 | 256 | $this->assertArrayHasKey('traefik', $createdConfig['services']); 257 | $this->assertSame('traefik.enable=true', $createdConfig['services']['php']['labels'][0]); 258 | $this->assertSame('traefik.http.routers.php.rule=Host(`myhost.com`)', $createdConfig['services']['php']['labels'][1]); 259 | } 260 | 261 | public function testThereIsNoTraefikIfThereIsNoHost(): void 262 | { 263 | $generator = new ComposeFileGenerator(); 264 | 265 | $config = [ 266 | "version" => '3.3', 267 | "containers" => [ 268 | "mysql" => [ 269 | "image" => "mysql", 270 | ] 271 | ] 272 | ]; 273 | $createdConfig = $generator->createDockerComposeConfig($config); 274 | $this->assertArrayNotHasKey('traefik', $createdConfig['services']); 275 | } 276 | 277 | 278 | public function testVolumesCreated(): void 279 | { 280 | $generator = new ComposeFileGenerator(); 281 | 282 | $config = [ 283 | "version" => '1.0', 284 | "containers" => [ 285 | "mysql" => [ 286 | "image" => "mysql", 287 | "volumes" => [ 288 | "mysqldata" => [ 289 | "diskSpace" => "1G", 290 | "mountPath" => "/var/lib/mysql", 291 | ] 292 | ] 293 | ] 294 | ] 295 | ]; 296 | $createdConfig = $generator->createDockerComposeConfig($config); 297 | $this->assertEquals("local", $createdConfig['volumes']['mysqldata']['driver']); 298 | $this->assertEquals(["mysqldata:/var/lib/mysql"], $createdConfig['services']['mysql']['volumes']); 299 | } 300 | } -------------------------------------------------------------------------------- /tests/php/ConfigGeneratorTest.php: -------------------------------------------------------------------------------- 1 | getConfig(__DIR__.'/json/host.json'); 16 | $expected = [ 17 | "version" => '1.0', 18 | "containers" => [ 19 | "php" => [ 20 | "host" => [ 21 | 'url' => 'myhost.com' 22 | ], 23 | "image" => "thecodingmachine/php:7.4-v3-apache", 24 | ], 25 | "phpmyadmin" => [ 26 | "host" => [ 27 | 'url' => 'phpmyadmin.myhost.com' 28 | ], 29 | "image" => "phpmyadmin", 30 | "ports" => [ 31 | 0 => 80 32 | ] 33 | ] 34 | 35 | ] 36 | ]; 37 | $this->assertEquals($expected, $config); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /tests/php/ParseConfigForCredentialsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, $returnArray); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /tests/php/json/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "containers": { 4 | "php": { 5 | "image": "thecodingmachine/php:7.4-v3-apache", 6 | "host": { 7 | "url": "myhost.com" 8 | } 9 | }, 10 | "phpmyadmin": { 11 | "image": "phpmyadmin", 12 | "ports": [ 13 | 80 14 | ], 15 | "host": { 16 | "url": "phpmyadmin.myhost.com" 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /tests/registryCredentials.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "containers": { 4 | "phpmyadmin": { 5 | "image": "testUrl.com/foo/bar", 6 | "ports": [ 7 | 80 8 | ], 9 | "host": { 10 | "url": "myhost.com" 11 | } 12 | } 13 | }, 14 | "config": { 15 | "registryCredentials": { 16 | "testUrl.com" : { 17 | "user": "mika", 18 | "password": "secret" 19 | }, 20 | "testUrl2.com" : { 21 | "user": "mika", 22 | "password": "secret" 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /tests/replicas.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "containers": { 4 | "phpmyadmin": { 5 | "image": "phpmyadmin", 6 | "replicas": 3 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ./test_functions.sh 4 | 5 | set -e 6 | 7 | # Kubernetes tests 8 | echo "Starting Kubernetes tests" 9 | 10 | expectError "Testing creation of host with a low version number" "host_with_low_version.json" "Mismatch in version number" ../scripts/main.jsonnet 11 | 12 | expectError "Testing creation of host without a port" "host_without_port.json" "For container \"phpmyadmin\", host \"myhost.com\" needs a port to bind to. Please provide a containerPort in the \"host\" section." ../scripts/main.jsonnet 13 | expectValue "Testing creation of ingress when a host is added" "host.json" ".generatedConf.phpmyadmin.ingress.spec.rules[0].host" '"myhost.com"' ../scripts/main.jsonnet 14 | expectValue "Testing containerPort of ingress when a host is added" "host_with_container_port.json" ".generatedConf.phpmyadmin.ingress.spec.rules[0].host" '"myhost.com"' ../scripts/main.jsonnet 15 | expectValue "Testing https enable (metadata section)" "host_with_https.json" ".generatedConf.phpmyadmin.ingress.metadata.annotations[\"cert-manager.io/issuer\"]" '"letsencrypt-prod"' ../scripts/main.jsonnet 16 | expectValue "Testing https enable (tls section)" "host_with_https.json" ".generatedConf.phpmyadmin.ingress.spec.tls[0].hosts[0]" '"myhost.com"' ../scripts/main.jsonnet 17 | expectValue "Testing https enable (issuer)" "host_with_https.json" ".generatedConf.issuer.kind" '"Issuer"' ../scripts/main.jsonnet 18 | expectError "Testing https enable (missing mail)" "host_with_https_without_mail.json" "In order to have support for HTTPS, you need to provide an email address in the { \"config\": { \"https\": { \"mail\": \"some@email.com\" } } }" ../scripts/main.jsonnet 19 | expectValue "Testing the presence of a timestamp label to force reloading" "host.json" ".generatedConf.phpmyadmin.deployment.spec.template.metadata.labels.deeployerTimestamp" '"2020-05-05 00:00:00"' ../scripts/main.jsonnet 20 | expectValue "Testing the absence of a timestamp label in the service" "host.json" ".generatedConf.phpmyadmin.service.spec.selector.deeployerTimestamp" 'null' ../scripts/main.jsonnet 21 | expectValue "Testing the presence of a PVC" "volume.json" ".generatedConf.mysql.pvcs.data.spec.resources.requests.storage" '"1G"' ../scripts/main.jsonnet 22 | expectValue "Testing the RollingUpdate type when there are replicas" "replicas.json" ".generatedConf.phpmyadmin.deployment.spec.strategy.type" '"RollingUpdate"' ../scripts/main.jsonnet 23 | assertValidK8s "host.json" ../scripts/main.jsonnet 24 | assertValidK8s "volume.json" ../scripts/main.jsonnet 25 | expectValue "Testing the presence of a registry credential" "registryCredentials.json" ".generatedConf.phpmyadmin.deployment.spec.template.spec.imagePullSecrets[0].name" '"aa827ffc96199a7071140cc2267bc1b1a"' ../scripts/main.jsonnet 26 | 27 | 28 | # Docker-compose tests 29 | echo "Starting docker-compose tests" 30 | 31 | expectValue "Testing there is no Traefik if there is no container with a host" "docker-compose-prod/no_host.json" ".docker_compose.services.traefik" 'null' ../scripts/docker-compose.jsonnet 32 | expectValue "Testing creation of Traefik when a host is added" "docker-compose-prod/host.json" ".docker_compose.services.traefik.image" '"traefik:2"' ../scripts/docker-compose.jsonnet 33 | 34 | # Schema test 35 | echo "Starting JsonSchema tests" 36 | 37 | ajv test -s ../deeployer.schema.json -d schema/valid.json --valid -c ajv-formats 38 | ajv test -s ../deeployer.schema.json -d schema/invalid_without_version.json --invalid -c ajv-formats 39 | ajv test -s ../deeployer.schema.json -d schema/invalid_with_wrong_version.json --invalid -c ajv-formats 40 | ajv test -s ../deeployer.schema.json -d schema/invalid_container_definition_with_unknown_properties.json --invalid -c ajv-formats 41 | ajv test -s ../deeployer.schema.json -d schema/invalid_container_with_wrong_declared_envVars.json --invalid -c ajv-formats 42 | ajv test -s ../deeployer.schema.json -d schema/invalid_container_definition_without_image.json --invalid -c ajv-formats 43 | ajv test -s ../deeployer.schema.json -d schema/invalid_test_testing_envVars_with_a_specialObject.json --invalid -c ajv-formats 44 | ajv test -s ../deeployer.schema.json -d schema/invalid_test_testing_envVars_with_nonStringValue.json --invalid -c ajv-formats 45 | ajv test -s ../deeployer.schema.json -d schema/invalid_properties_definition_with_emptyString_in_image.json --invalid -c ajv-formats 46 | ajv test -s ../deeployer.schema.json -d schema/invalid_properties_definition_with_emptyString_in_max_cpu.json --invalid -c ajv-formats 47 | ajv test -s ../deeployer.schema.json -d schema/invalid_properties_definition_with_emptyString_in_min_cpu.json --invalid -c ajv-formats 48 | ajv test -s ../deeployer.schema.json -d schema/invalid_properties_definition_with_emptyString_in_max_memory.json --invalid -c ajv-formats 49 | ajv test -s ../deeployer.schema.json -d schema/invalid_properties_definition_with_emptyString_in_min_memory.json --invalid -c ajv-formats 50 | 51 | echo 52 | echo 53 | echo -e "\e[32m✓✓\e[39m All tests successful! \e[32m✓✓\e[39m" 54 | -------------------------------------------------------------------------------- /tests/schema/invalid_container_definition_with_unknown_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "$schema": "../../deeployer.schema.json", 4 | "containers": { 5 | "elasticsearch": { 6 | "image": "image_name", 7 | "foobar": "" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/schema/invalid_container_definition_without_image.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "$schema": "../../deeployer.schema.json", 4 | "containers": { 5 | "elasticsearch": {} 6 | } 7 | } -------------------------------------------------------------------------------- /tests/schema/invalid_container_with_wrong_declared_envVars.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "$schema": "../../deeployer.schema.json", 4 | "containers": { 5 | "elasticsearch": { 6 | "image": "image_name", 7 | "env": "app : mysql" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/schema/invalid_properties_definition_with_emptyString_in_image.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "$schema": "../../deeployer.schema.json", 4 | "containers": { 5 | "elasticsearch": { 6 | "image": "" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/schema/invalid_properties_definition_with_emptyString_in_max_cpu.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "$schema": "../../deeployer.schema.json", 4 | "containers": { 5 | "elasticsearch": { 6 | "image": "", 7 | "quotas": { 8 | "max": { 9 | "cpu": "" 10 | } 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/schema/invalid_properties_definition_with_emptyString_in_max_memory.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "$schema": "../../deeployer.schema.json", 4 | "containers": { 5 | "elasticsearch": { 6 | "image": "", 7 | "quotas": { 8 | "max": { 9 | "memory": "" 10 | } 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/schema/invalid_properties_definition_with_emptyString_in_min_cpu.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "$schema": "../../deeployer.schema.json", 4 | "containers": { 5 | "elasticsearch": { 6 | "image": "", 7 | "quotas": { 8 | "min": { 9 | "cpu": "" 10 | } 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/schema/invalid_properties_definition_with_emptyString_in_min_memory.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "$schema": "../../deeployer.schema.json", 4 | "containers": { 5 | "elasticsearch": { 6 | "image": "", 7 | "quotas": { 8 | "min": { 9 | "memory": "" 10 | } 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/schema/invalid_test_testing_envVars_with_a_specialObject.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "$schema": "../../deeployer.schema.json", 4 | "containers": { 5 | "elasticsearch": { 6 | "image": "image_name", 7 | "env": { 8 | "FOO": { 9 | "SOME": "SUBOBJECT" 10 | } 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/schema/invalid_test_testing_envVars_with_nonStringValue.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "$schema": "../../deeployer.schema.json", 4 | "containers": { 5 | "elasticsearch": { 6 | "image": "image_name", 7 | "env": { 8 | "FOO": 42 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/schema/invalid_with_wrong_version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "not a version", 3 | "$schema": "../../deeployer.schema.json", 4 | "containers": { 5 | "elasticsearch": { 6 | 7 | "image": "image_name", 8 | 9 | "replicas": 10 | 1, 11 | 12 | "ports": [80], 13 | 14 | 15 | "env" : { 16 | "FOO": "BAR" 17 | }, 18 | 19 | 20 | "host": { 21 | "url": "test.thecodingmachine.com" 22 | }, 23 | 24 | "volumes" : {}, 25 | 26 | 27 | "quotas" : { 28 | 29 | "min": { 30 | "cpu": "2", 31 | "memory" : "1" 32 | }, 33 | 34 | "max": { 35 | "cpu": "2", 36 | "memory": "1" 37 | } 38 | 39 | } 40 | 41 | 42 | 43 | 44 | 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /tests/schema/invalid_without_version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../deeployer.schema.json", 3 | "containers": { 4 | "elasticsearch": { 5 | 6 | "image": "image_name", 7 | 8 | "replicas": 9 | 1, 10 | 11 | "ports": [80], 12 | 13 | 14 | "env" : { 15 | "FOO": "BAR" 16 | }, 17 | 18 | 19 | "host": { 20 | "url": "test.thecodingmachine.com" 21 | }, 22 | 23 | "volumes" : {}, 24 | 25 | 26 | "quotas" : { 27 | 28 | "min": { 29 | "cpu": "2", 30 | "memory" : "1" 31 | }, 32 | 33 | "max": { 34 | "cpu": "2", 35 | "memory": "1" 36 | } 37 | 38 | } 39 | 40 | 41 | 42 | 43 | 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /tests/schema/valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "$schema": "../../deeployer.schema.json", 4 | "containers": { 5 | "elasticsearch": { 6 | 7 | "image": "image_name", 8 | 9 | "replicas": 10 | 1, 11 | 12 | "ports": [80], 13 | 14 | 15 | "env" : { 16 | "FOO": "BAR" 17 | }, 18 | 19 | 20 | "host": { 21 | "url": "test.thecodingmachine.com" 22 | }, 23 | 24 | "volumes" : {}, 25 | 26 | 27 | "quotas" : { 28 | 29 | "min": { 30 | "cpu": "2", 31 | "memory" : "1" 32 | }, 33 | 34 | "max": { 35 | "cpu": "2", 36 | "memory": "1" 37 | } 38 | 39 | } 40 | 41 | 42 | 43 | 44 | 45 | } 46 | }, 47 | "config": { 48 | "registryCredentials": { 49 | "foo.com:444": { 50 | "user": "bar", 51 | "password": "foo" 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/test_functions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export JSONNET_PATH=../vendor/:../lib/ 4 | 5 | # First parameter: Test name 6 | # Second parameter: libsonnet test file 7 | # Third parameter: Expected error message 8 | function expectError() { 9 | set +e 10 | echo " Running test: $1" 11 | 12 | config=$(cat "$2") 13 | 14 | OUTPUT=$(jsonnet ../scripts/main.jsonnet --ext-code config="$config" --ext-str timestamp="2020-05-05 00:00:00" 2>&1) 15 | if [[ $? == 0 ]]; then 16 | echo -e "\e[31m❌\e[39m Expected an error message" 17 | echo " Instead, got '$OUTPUT'" 18 | exit 1 19 | fi 20 | (echo $OUTPUT | grep "$3") > /dev/null 21 | if [[ $? != 0 ]]; then 22 | echo -e "\e[31m❌\e[39m Expected error message '$3'" 23 | echo " Instead, got '$OUTPUT'" 24 | exit 1 25 | fi 26 | 27 | echo -e "\e[32m✓\e[39m Successfully tested: $1" 28 | } 29 | 30 | # First parameter: Test name 31 | # Second parameter: json test file 32 | # Third parameter: JSON Path (as interpreted by jq) 33 | # Fourth parameter: Value expected 34 | # Fifth parameter: The test script to lanch (K8S or docker-compose) 35 | function expectValue() { 36 | set +e 37 | 38 | 39 | echo " Running test: $1" 40 | 41 | set -o pipefail 42 | 43 | config=$(cat "$2") 44 | 45 | OUTPUT=$(jsonnet $5 --ext-code config="$config" --ext-str timestamp="2020-05-05 00:00:00" | jq "$3") 46 | if [[ $? != 0 ]]; then 47 | echo -e "\e[31m❌\e[39m Jsonnet returned an error code" 48 | set -e 49 | exit 1 50 | fi 51 | set +o pipefail 52 | if [[ $OUTPUT != "$4" ]]; then 53 | echo -e "\e[31m❌\e[39m Expected '$4'" 54 | echo " Instead, got '$OUTPUT'" 55 | set -e 56 | exit 1 57 | fi 58 | 59 | echo -e "\e[32m✓\e[39m Successfully tested: $1" 60 | set -e 61 | } 62 | 63 | # Asserts that generated configuration file is a valid Kubernetes deployment 64 | # First parameter: libsonnet test file 65 | function assertValidK8s() { 66 | set +e 67 | 68 | 69 | echo " Testing K8S validity for: $1" 70 | 71 | set -o pipefail 72 | OUTPUT=$(../scripts/deeployer-k8s show "$1" | kubeval --ignore-missing-schemas) 73 | if [[ $? != 0 ]]; then 74 | echo -e "\e[31m❌\e[39m Kubeval returned an error code" 75 | echo "" 76 | echo " Tanka output:" 77 | echo "" 78 | ../scripts/deeployer-k8s show "$1" 79 | echo "" 80 | echo " Kubeval error message:" 81 | echo "" 82 | echo "$OUTPUT" 83 | set -e 84 | exit 1 85 | fi 86 | set +o pipefail 87 | 88 | echo -e "\e[32m✓\e[39m Successfully tested K8S validity for $1" 89 | set -e 90 | } 91 | -------------------------------------------------------------------------------- /tests/volume.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "$schema": "../deeployer.schema.json", 4 | "containers": { 5 | "mysql": { 6 | "image": "mysql:8", 7 | "volumes": { 8 | "data": { 9 | "diskSpace": "1G", 10 | "mountPath": "/var/lib/mysql" 11 | }, 12 | "mysql-conf": { 13 | "diskSpace": "10M", 14 | "mountPath": "/etc/mysql" 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /validator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd environments/default 4 | jsonlint *.json -q 5 | jsonnetfmt *.jsonnet >/dev/null 6 | 7 | cd ../../lib/ 8 | jsonnetfmt *.libsonnet >/dev/null 9 | 10 | cd deeployer 11 | jsonnetfmt docker-compose-generator.libsonnet >/dev/null 12 | jsonnetfmt resource_generator.libsonnet >/dev/null 13 | 14 | cd ../../scripts 15 | jsonlint *.json -q 16 | jsonnetfmt docker-compose.jsonnet >/dev/null 17 | jsonnetfmt main.jsonnet >/dev/null 18 | 19 | cd ../tests/docker-compose-prod 20 | jsonlint *.json -q 21 | 22 | cd ../schema 23 | jsonlint *.json -q 24 | 25 | cd .. #location : test/ 26 | jsonlint *.json -q 27 | 28 | cd .. #location : root/ 29 | jsonlint *.json -q 30 | 31 | --------------------------------------------------------------------------------