├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── pyproject.toml ├── rancher_gitlab_deploy ├── __init__.py └── cli.py └── setup.py /.dockerignore: -------------------------------------------------------------------------------- 1 | *.egg 2 | *.pyc 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # IDEA stuff 92 | .idea -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine 2 | 3 | COPY . /rancher-gitlab-deploy/ 4 | 5 | WORKDIR /rancher-gitlab-deploy 6 | RUN python /rancher-gitlab-deploy/setup.py install 7 | RUN ln -s /usr/local/bin/rancher-gitlab-deploy /usr/local/bin/upgrade 8 | 9 | CMD rancher-gitlab-deploy -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 cdrx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | publish: 2 | pip install wheel 3 | python setup.py sdist bdist_wheel 4 | 5 | upload: 6 | pip install twine 7 | python -m twine upload dist/* 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rancher GitLab Deployment Tool 2 | 3 | **rancher-gitlab-deploy** is a tool for deploying containers built with GitLab CI onto your Rancher infrastructure. 4 | 5 | It fits neatly into the `gitlab-ci.yml` workflow and requires minimal configuration. It will upgrade existing services as part of your CI workflow. 6 | 7 | Both GitLab's built in Docker registry and external Docker registries are supported. 8 | 9 | `rancher-gitlab-deploy` will pick as much of its configuration up as possible from environment variables set by the GitLab CI runner. 10 | 11 | This tool is not suitable if your services are not already created in Rancher. It will upgrade existing services, but will not create new ones. If you need to create services you should use `rancher-compose` in your CI workflow, but that means storing any secret environment variables in your Git repo. 12 | 13 | ## Installation 14 | 15 | I recommend you use the pre-built container: 16 | 17 | https://hub.docker.com/r/cdrx/rancher-gitlab-deploy/ 18 | 19 | But you can install the command locally, with `pip`, if you prefer: 20 | 21 | ``` 22 | pip install rancher-gitlab-deploy 23 | ``` 24 | 25 | ## Usage 26 | 27 | You will need to create a set of API keys in Rancher and save them as secret variables in GitLab for your project. 28 | 29 | Three secret variables are required: 30 | 31 | `RANCHER_URL` (eg `https://rancher.example.com`) 32 | 33 | `RANCHER_ACCESS_KEY` 34 | 35 | `RANCHER_SECRET_KEY` 36 | 37 | Rancher supports two kind of API keys: environment and account. You can use either with this tool, but if your account key has access to more than one environment you'll need to specify the name of the environment with the --environment flag. This is so that the tool can upgrade find the service in the right place. For example, in your `gitlab-ci.yml`: 38 | 39 | ``` 40 | deploy: 41 | stage: deploy 42 | image: cdrx/rancher-gitlab-deploy 43 | script: 44 | - upgrade --environment production 45 | ``` 46 | 47 | `rancher-gitlab-deploy` will use the GitLab group and project name as the stack and service name by default. For example, the project: 48 | 49 | `http://gitlab.example.com/acme/webservice` 50 | 51 | will upgrade the service called `webservice` in the stack called `acme`. 52 | 53 | If the names of your services don't match your repos in GitLab 1:1, you can change the service that gets upgraded with the `--stack` and `--service` flags: 54 | 55 | ``` 56 | deploy: 57 | stage: deploy 58 | image: cdrx/rancher-gitlab-deploy 59 | script: 60 | - upgrade --stack acmeinc --service website 61 | ``` 62 | 63 | You can change the image (or :tag) used to deploy the upgraded containers with the `--new-image` option: 64 | 65 | ``` 66 | deploy: 67 | stage: deploy 68 | image: cdrx/rancher-gitlab-deploy 69 | script: 70 | - upgrade --new-image registry.example.com/acme/widget:1.2 71 | ``` 72 | 73 | You may use this with the `$CI_BUILD_TAG` environment variable that GitLab sets. 74 | 75 | `rancher-gitlab-deploy`'s default upgrade strategy is to upgrade containers one at time, waiting 2s between each one. It will start new containers after shutting down existing ones, to avoid issues with multiple containers trying to bind to the same port on a host. It will wait for the upgrade to complete in Rancher, then mark it as finished. The upgrade strategy can be adjusted with the flags in `--help` (see below). 76 | 77 | ## GitLab CI Example 78 | 79 | Complete gitlab-ci.yml: 80 | 81 | ``` 82 | image: docker:latest 83 | services: 84 | - docker:dind 85 | 86 | stages: 87 | - build 88 | - deploy 89 | 90 | build: 91 | stage: build 92 | script: 93 | - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com 94 | - docker build -t registry.example.com/group/project . 95 | - docker push registry.example.com/group/project 96 | 97 | deploy: 98 | stage: deploy 99 | image: cdrx/rancher-gitlab-deploy 100 | script: 101 | - upgrade 102 | ``` 103 | 104 | A more complex example: 105 | 106 | ``` 107 | deploy: 108 | stage: deploy 109 | image: cdrx/rancher-gitlab-deploy 110 | script: 111 | - upgrade --environment production --stack acme --service web --new-image alpine:3.4 --no-finish-upgrade 112 | ``` 113 | 114 | ## Help 115 | 116 | ``` 117 | $ rancher-gitlab-deploy --help 118 | 119 | Usage: rancher-gitlab-deploy [OPTIONS] 120 | 121 | Performs an in service upgrade of the service specified on the command 122 | line 123 | 124 | Options: 125 | --rancher-url TEXT The URL for your Rancher server, eg: 126 | http://rancher:8000 [required] 127 | --rancher-key TEXT The environment or account API key 128 | [required] 129 | --rancher-secret TEXT The secret for the access API key 130 | [required] 131 | --rancher-label-separator TEXT Where the default separator (',') could 132 | cause issues 133 | --environment TEXT The name of the environment to add the host 134 | into (only needed if you are using an 135 | account API key instead of an environment 136 | API key) 137 | --stack TEXT The name of the stack in Rancher (defaults 138 | to the name of the group in GitLab) 139 | [required] 140 | --service TEXT The name of the service in Rancher to 141 | upgrade (defaults to the name of the service 142 | in GitLab) [required] 143 | --start-before-stopping / --no-start-before-stopping 144 | Should Rancher start new containers before 145 | stopping the old ones? 146 | --batch-size INTEGER Number of containers to upgrade at once 147 | --batch-interval INTEGER Number of seconds to wait between upgrade 148 | batches 149 | --upgrade-timeout INTEGER How long to wait, in seconds, for the 150 | upgrade to finish before exiting. To skip 151 | the wait, pass the --no-wait-for-upgrade-to- 152 | finish option. 153 | --wait-for-upgrade-to-finish / --no-wait-for-upgrade-to-finish 154 | Wait for Rancher to finish the upgrade 155 | before this tool exits 156 | --rollback-on-error / --no-rollback-on-error 157 | Rollback the upgrade if an error occured. 158 | The rollback will be performed only if the 159 | option --wait-for-upgrade-to-finish is 160 | passed 161 | --new-image TEXT If specified, replace the image (and :tag) 162 | with this one during the upgrade 163 | --finish-upgrade / --no-finish-upgrade 164 | Mark the upgrade as finished after it 165 | completes 166 | --sidekicks / --no-sidekicks Upgrade service sidekicks at the same time 167 | --new-sidekick-image ... 168 | If specified, replace the sidekick image 169 | (and :tag) with this one during the upgrade 170 | --create / --no-create If specified, create Rancher stack and 171 | service if they don't exist 172 | --labels TEXT If specified, add a comma separated list of 173 | key=values to add to the service 174 | --label ... If specified, add a Rancher label to the 175 | service 176 | --variables TEXT If specified, add a comma separated list of 177 | key=values to add to the service 178 | --variable ... If specified, add a environment variable to 179 | the service 180 | --service-links TEXT If specified, add a comma separated list of 181 | key=values to add to the service 182 | --service-link ... If specified, add a service link to the 183 | service 184 | --host-id TEXT If specified, service will be deployed on 185 | requested host 186 | --debug / --no-debug Enable HTTP Debugging 187 | --ssl-verify / --no-ssl-verify Disable certificate checks. Use this to 188 | allow connecting to a HTTPS Rancher server 189 | using an self-signed certificate 190 | --secrets TEXT If specified, add a comma separated list of 191 | secrets to the service 192 | --secret TEXT If specified add a secret to the service 193 | --help Show this message and exit. 194 | ``` 195 | 196 | ## History 197 | 198 | #### [1.7] - 2020-12-16 199 | Fixed a bug when updating variables on existing service, thanks to @ffouchier 200 | Added the --rancher-label-separator option, thanks to @NigelGreenway for the PR 201 | Added the --service-links option, thanks to @mrpolman for the PR 202 | Added the support for secrets with the --secret option, thanks to @earzur for the PR 203 | 204 | #### [1.6] - 2018-09-09 205 | Added the --rollback-on-error option, thanks to @TZK- for the PR 206 | Added the --label, --variables, --variable options, thankls to @tsteenkamp for the PR 207 | 208 | #### [1.5] - 2017-11-25 209 | Fixed UnicodeError bug with authentication, thank you to @evilmind for the fix 210 | 211 | #### [1.4] - 2017-07-18 212 | Fixed some bug to do with error and sidekick handling and made `--no-start-before-stopping` the default behaviour 213 | 214 | #### [1.3] - 2017-03-16 215 | Added the --new-sidekick-image flag to change sidekick images while upgrading, thank you @kariae for the PR 216 | 217 | #### [1.2] - 2017-01-03 218 | Added the --sidekicks flag to upgrade sidekicks at the same time, thank you @kiesiu for the PR 219 | 220 | #### [1.1] - 2016-09-29 221 | Fixed a bug that caused a crash when using --environment, thank you @mvriel for the PR 222 | 223 | #### [1.0] - 2016-09-14 224 | First release, works. 225 | 226 | ## License 227 | 228 | MIT 229 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 125 3 | target-version = ['py38'] 4 | 5 | [tool.isort] 6 | profile = "black" 7 | known_first_party = "rancher_gitlab_deploy" 8 | sections = "FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER" 9 | line_length = 125 10 | multi_line_output = 3 11 | force_single_line = "true" 12 | -------------------------------------------------------------------------------- /rancher_gitlab_deploy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdrx/rancher-gitlab-deploy/dbe9cb1c74a5bc897549c3436ba5125aa5df3859/rancher_gitlab_deploy/__init__.py -------------------------------------------------------------------------------- /rancher_gitlab_deploy/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import logging 3 | import sys 4 | import time 5 | 6 | import click 7 | import requests 8 | from requests import HTTPError 9 | 10 | try: 11 | from httplib import HTTPConnection # py2 12 | except ImportError: 13 | from http.client import HTTPConnection # py3 14 | 15 | 16 | @click.command() 17 | @click.option( 18 | "--rancher-url", 19 | envvar="RANCHER_URL", 20 | required=True, 21 | help="The URL for your Rancher server, eg: http://rancher:8000", 22 | ) 23 | @click.option( 24 | "--rancher-key", 25 | envvar="RANCHER_ACCESS_KEY", 26 | required=True, 27 | help="The environment or account API key", 28 | ) 29 | @click.option( 30 | "--rancher-secret", 31 | envvar="RANCHER_SECRET_KEY", 32 | required=True, 33 | help="The secret for the access API key", 34 | ) 35 | @click.option( 36 | "--rancher-label-separator", 37 | envvar="RANCHER_LABEL_SEPARATOR", 38 | required=False, 39 | default=",", 40 | help="Where the default separator (',') could cause issues", 41 | ) 42 | @click.option( 43 | "--environment", 44 | default=None, 45 | help="The name of the environment to add the host into " 46 | + "(only needed if you are using an account API key instead of an environment API key)", 47 | ) 48 | @click.option( 49 | "--stack", 50 | envvar="CI_PROJECT_NAMESPACE", 51 | default=None, 52 | required=True, 53 | help="The name of the stack in Rancher (defaults to the name of the group in GitLab)", 54 | ) 55 | @click.option( 56 | "--service", 57 | envvar="CI_PROJECT_NAME", 58 | default=None, 59 | required=True, 60 | help="The name of the service in Rancher to upgrade (defaults to the name of the service in GitLab)", 61 | ) 62 | @click.option( 63 | "--start-before-stopping/--no-start-before-stopping", 64 | default=False, 65 | help="Should Rancher start new containers before stopping the old ones?", 66 | ) 67 | @click.option( 68 | "--batch-size", 69 | default=1, 70 | help="Number of containers to upgrade at once", 71 | ) 72 | @click.option( 73 | "--batch-interval", 74 | default=2, 75 | help="Number of seconds to wait between upgrade batches", 76 | ) 77 | @click.option( 78 | "--upgrade-timeout", 79 | default=5 * 60, 80 | help="How long to wait, in seconds, for the upgrade to finish before exiting. To skip the wait, pass the --no-wait-for-upgrade-to-finish option.", 81 | ) 82 | @click.option( 83 | "--wait-for-upgrade-to-finish/--no-wait-for-upgrade-to-finish", 84 | default=True, 85 | help="Wait for Rancher to finish the upgrade before this tool exits", 86 | ) 87 | @click.option( 88 | "--rollback-on-error/--no-rollback-on-error", 89 | default=False, 90 | help="Rollback the upgrade if an error occured. The rollback will be performed only if the option --wait-for-upgrade-to-finish is passed", 91 | ) 92 | @click.option( 93 | "--new-image", 94 | default=None, 95 | help="If specified, replace the image (and :tag) with this one during the upgrade", 96 | ) 97 | @click.option( 98 | "--finish-upgrade/--no-finish-upgrade", 99 | default=True, 100 | help="Mark the upgrade as finished after it completes", 101 | ) 102 | @click.option( 103 | "--sidekicks/--no-sidekicks", 104 | default=False, 105 | help="Upgrade service sidekicks at the same time", 106 | ) 107 | @click.option( 108 | "--new-sidekick-image", 109 | default=None, 110 | multiple=True, 111 | help="If specified, replace the sidekick image (and :tag) with this one during the upgrade", 112 | type=(str, str), 113 | ) 114 | @click.option( 115 | "--create/--no-create", 116 | default=False, 117 | help="If specified, create Rancher stack and service if they don't exist", 118 | ) 119 | @click.option( 120 | "--labels", 121 | default=None, 122 | help="If specified, add a comma separated list of key=values to add to the service", 123 | ) 124 | @click.option( 125 | "--label", 126 | default=None, 127 | multiple=True, 128 | help="If specified, add a Rancher label to the service", 129 | type=(str, str), 130 | ) 131 | @click.option( 132 | "--variables", 133 | default=None, 134 | help="If specified, add a comma separated list of key=values to add to the service", 135 | ) 136 | @click.option( 137 | "--variable", 138 | default=None, 139 | multiple=True, 140 | help="If specified, add a environment variable to the service", 141 | type=(str, str), 142 | ) 143 | @click.option( 144 | "--service-links", 145 | default=None, 146 | help="If specified, add a comma separated list of key=values to add to the service", 147 | ) 148 | @click.option( 149 | "--service-link", 150 | default=None, 151 | multiple=True, 152 | help="If specified, add a service link to the service", 153 | type=(str, str), 154 | ) 155 | @click.option( 156 | "--host-id", 157 | default=None, 158 | help="If specified, service will be deployed on requested host", 159 | ) 160 | @click.option( 161 | "--debug/--no-debug", 162 | default=False, 163 | help="Enable HTTP Debugging", 164 | ) 165 | @click.option( 166 | "--ssl-verify/--no-ssl-verify", 167 | default=True, 168 | help="Disable certificate checks. Use this to allow connecting to a HTTPS Rancher server using an self-signed certificate", 169 | ) 170 | @click.option( 171 | "--secrets", 172 | default=None, 173 | help="If specified, add a comma separated list of secrets to the service", 174 | ) 175 | @click.option( 176 | "--secret", 177 | default=None, 178 | multiple=True, 179 | help="If specified add a secret to the service", 180 | ) 181 | def main( 182 | rancher_url, 183 | rancher_key, 184 | rancher_secret, 185 | rancher_label_separator, 186 | environment, 187 | stack, 188 | service, 189 | new_image, 190 | batch_size, 191 | batch_interval, 192 | start_before_stopping, 193 | upgrade_timeout, 194 | wait_for_upgrade_to_finish, 195 | rollback_on_error, 196 | finish_upgrade, 197 | sidekicks, 198 | new_sidekick_image, 199 | create, 200 | labels, 201 | label, 202 | variables, 203 | variable, 204 | service_links, 205 | service_link, 206 | host_id, 207 | debug, 208 | ssl_verify, 209 | secrets, 210 | secret, 211 | ): 212 | """Performs an in service upgrade of the service specified on the command line""" 213 | if debug: 214 | debug_requests_on() 215 | 216 | # split url to protocol and host 217 | if "://" not in rancher_url: 218 | bail("The Rancher URL doesn't look right") 219 | 220 | proto, host = rancher_url.split("://") 221 | api = "%s://%s/v1" % (proto, host) 222 | apiv2 = "%s://%s/v2-beta" % (proto, host) 223 | 224 | stack = stack.replace(".", "-") 225 | service = service.replace(".", "-") 226 | 227 | session = requests.Session() 228 | 229 | # Set verify based on --ssl-verify/--no-ssl-verify option 230 | session.verify = ssl_verify 231 | 232 | # 0 -> Authenticate all future requests 233 | session.auth = (rancher_key, rancher_secret) 234 | 235 | # Check for labels and environment variables to set 236 | defined_labels = {} 237 | 238 | if labels is not None: 239 | labels_as_array = labels.split(rancher_label_separator) 240 | 241 | for label_item in labels_as_array: 242 | key, value = label_item.split("=", 1) 243 | defined_labels[key] = value 244 | 245 | if label: 246 | for item in label: 247 | key = item[0] 248 | value = item[1] 249 | defined_labels[key] = value 250 | 251 | defined_environment_variables = {} 252 | 253 | if variables is not None: 254 | variables_as_array = variables.split(",") 255 | 256 | for variable_item in variables_as_array: 257 | key, value = variable_item.split("=", 1) 258 | defined_environment_variables[key] = value 259 | 260 | if variable: 261 | for item in variable: 262 | key = item[0] 263 | value = item[1] 264 | defined_environment_variables[key] = value 265 | 266 | defined_secrets = [] 267 | 268 | if secrets is not None: 269 | secrets_as_array = secrets.split(",") 270 | 271 | for secret_item in secrets_as_array: 272 | key, value = secret_item.split("=", 1) 273 | defined_secrets.append({"type": "secretReference", "name": key}) 274 | 275 | if secret: 276 | for item in secret: 277 | defined_secrets.append({"type": "secretReference", "name": item}) 278 | 279 | # 1 -> Find the environment id in Rancher 280 | try: 281 | r = session.get("%s/projects?limit=1000" % api) 282 | r.raise_for_status() 283 | except HTTPError: 284 | bail("Unable to connect to Rancher at %s - is the URL and API key right?" % host) 285 | else: 286 | environments = r.json()["data"] 287 | 288 | environment_id = None 289 | if environment is None: 290 | environment_id = environments[0]["id"] 291 | environment_name = environments[0]["name"] 292 | else: 293 | for e in environments: 294 | if e["id"].lower() == environment.lower() or e["name"].lower() == environment.lower(): 295 | environment_id = e["id"] 296 | environment_name = e["name"] 297 | 298 | if not environment_id: 299 | if environment: 300 | bail( 301 | "The '%s' environment doesn't exist in Rancher, or your API credentials don't have access to it" 302 | % environment 303 | ) 304 | else: 305 | bail("No environment in Rancher matches your request") 306 | 307 | # map secrets to id (checking if passed secrets are defined in the environment) 308 | for def_secret in defined_secrets: 309 | # try to retrieve secret by name 310 | try: 311 | r = session.get("%s/projects/%s/secrets?name=%s" % (apiv2, environment_id, def_secret["name"])) 312 | r.raise_for_status() 313 | except HTTPError: 314 | bail( 315 | "Unable to connect to Rancher at %s to secret id for %s - are the URL and API key right? does %s exist ?" 316 | % host, 317 | def_secret["name"], 318 | def_secret["name"], 319 | ) 320 | else: 321 | result = r.json()["data"] 322 | if debug: 323 | msg("secret query result: %s" % result) 324 | if len(result) > 0: 325 | def_secret["secretId"] = result[0]["id"] 326 | else: 327 | bail("Cannot find secret %s in environment %s ?!" % (def_secret["name"], environment_id)) 328 | 329 | # 2 -> Find the stack in the environment 330 | 331 | try: 332 | r = session.get("%s/projects/%s/environments?limit=1000" % (api, environment_id)) 333 | r.raise_for_status() 334 | except HTTPError: 335 | bail("Unable to fetch a list of stacks in the environment '%s'" % environment_name) 336 | else: 337 | stacks = r.json()["data"] 338 | 339 | for s in stacks: 340 | if s["name"].lower() == stack.lower(): 341 | stack = s 342 | break 343 | else: 344 | if create: 345 | new_stack = {"name": stack.lower()} 346 | try: 347 | msg("Creating stack %s in environment %s..." % (new_stack["name"], environment_name)) 348 | r = session.post("%s/projects/%s/environments" % (api, environment_id), json=new_stack) 349 | r.raise_for_status() 350 | stack = r.json() 351 | except HTTPError: 352 | bail("Unable to create missing stack") 353 | else: 354 | bail("Unable to find a stack called '%s'. Does it exist in the '%s' environment?" % (stack, environment_name)) 355 | 356 | # 3 -> Find the service in the stack 357 | 358 | try: 359 | r = session.get("%s/projects/%s/environments/%s/services?limit=1000" % (api, environment_id, stack["id"])) 360 | r.raise_for_status() 361 | except HTTPError: 362 | bail("Unable to fetch a list of services in the stack. Does your API key have the right permissions?") 363 | else: 364 | services = r.json()["data"] 365 | 366 | for s in services: 367 | if s["name"].lower() == service.lower(): 368 | service = s 369 | break 370 | else: 371 | 372 | if create: 373 | new_service = { 374 | "name": service.lower(), 375 | "stackId": stack["id"], 376 | "startOnCreate": True, 377 | "launchConfig": { 378 | "imageUuid": ("docker:%s" % new_image), 379 | "labels": defined_labels, 380 | "environment": defined_environment_variables, 381 | "secrets": defined_secrets, 382 | }, 383 | } 384 | 385 | if host_id is not None: 386 | msg("Scheduled host %s" % host_id) 387 | new_service["launchConfig"]["requestedHostId"] = host_id 388 | 389 | try: 390 | msg( 391 | "Creating service %s in environment %s with image %s..." 392 | % (new_service["name"], environment_name, new_image) 393 | ) 394 | r = session.post("%s/projects/%s/services" % (apiv2, environment_id), json=new_service) 395 | r.raise_for_status() 396 | service = r.json() 397 | 398 | defined_service_links = [] 399 | 400 | if service_links is not None: 401 | service_links_as_array = service_links.split(",") 402 | 403 | for service_link_item in service_links_as_array: 404 | name, reference = service_link_item.split("=", 1) 405 | serviceId = None 406 | 407 | for s in services: 408 | if s["name"].lower() == reference.lower(): 409 | serviceId = s["id"] 410 | break 411 | 412 | if serviceId: 413 | defined_service_links.append({"name": name, "serviceId": serviceId}) 414 | 415 | if service_link: 416 | for name, reference in service_link: 417 | serviceId = None 418 | 419 | for s in services: 420 | if s["name"].lower() == reference.lower(): 421 | serviceId = s["id"] 422 | break 423 | 424 | if serviceId: 425 | defined_service_links.append({"name": name, "serviceId": serviceId}) 426 | 427 | if defined_service_links: 428 | msg( 429 | "Setting service links for service %s in environment %s with image %s..." 430 | % (new_service["name"], environment_name, new_image) 431 | ) 432 | r = session.post(service["actions"]["setservicelinks"], json={"serviceLinks": defined_service_links}) 433 | r.raise_for_status() 434 | service = r.json() 435 | msg("Service links set") 436 | 437 | msg("Creation finished") 438 | sys.exit(0) 439 | except HTTPError: 440 | bail("Unable to create missing service") 441 | else: 442 | bail("Unable to find a service called '%s', does it exist in Rancher?" % service) 443 | 444 | # 4 -> Is the service elligible for upgrade? 445 | 446 | if service["state"] == "upgraded": 447 | warn( 448 | "The current service state is 'upgraded', marking the previous upgrade as finished before starting a new upgrade..." 449 | ) 450 | 451 | try: 452 | r = session.post("%s/projects/%s/services/%s/?action=finishupgrade" % (api, environment_id, service["id"])) 453 | r.raise_for_status() 454 | except HTTPError: 455 | bail("Unable to finish the previous upgrade in Rancher") 456 | 457 | attempts = 0 458 | while service["state"] != "active": 459 | time.sleep(2) 460 | attempts += 2 461 | if attempts > upgrade_timeout: 462 | bail("A timeout occured while waiting for Rancher to finish the previous upgrade") 463 | try: 464 | r = session.get("%s/projects/%s/services/%s" % (api, environment_id, service["id"])) 465 | r.raise_for_status() 466 | except HTTPError: 467 | bail("Unable to request the service status from the Rancher API") 468 | else: 469 | service = r.json() 470 | 471 | if service["state"] != "active": 472 | bail("Unable to start upgrade: current service state '%s', but it needs to be 'active'" % service["state"]) 473 | 474 | msg("Upgrading %s/%s in environment %s..." % (stack["name"], service["name"], environment_name)) 475 | 476 | upgrade = { 477 | "inServiceStrategy": { 478 | "batchSize": batch_size, 479 | "intervalMillis": batch_interval * 1000, # rancher expects miliseconds 480 | "startFirst": start_before_stopping, 481 | "launchConfig": {}, 482 | "secondaryLaunchConfigs": [], 483 | } 484 | } 485 | # copy over the existing config 486 | upgrade["inServiceStrategy"]["launchConfig"] = service["launchConfig"] 487 | 488 | if defined_labels: 489 | upgrade["inServiceStrategy"]["launchConfig"]["labels"] = defined_labels 490 | 491 | if defined_environment_variables: 492 | upgrade["inServiceStrategy"]["launchConfig"]["environment"] = defined_environment_variables 493 | 494 | # new_sidekick_image parameter needs secondaryLaunchConfigs loaded 495 | if sidekicks or new_sidekick_image: 496 | # copy over existing sidekicks config 497 | upgrade["inServiceStrategy"]["secondaryLaunchConfigs"] = service["secondaryLaunchConfigs"] 498 | 499 | if new_image: 500 | # place new image into config 501 | upgrade["inServiceStrategy"]["launchConfig"]["imageUuid"] = "docker:%s" % new_image 502 | 503 | if new_sidekick_image: 504 | new_sidekick_image = dict(new_sidekick_image) 505 | 506 | for idx, secondaryLaunchConfigs in enumerate(service["secondaryLaunchConfigs"]): 507 | if secondaryLaunchConfigs["name"] in new_sidekick_image: 508 | upgrade["inServiceStrategy"]["secondaryLaunchConfigs"][idx]["imageUuid"] = ( 509 | "docker:%s" % new_sidekick_image[secondaryLaunchConfigs["name"]] 510 | ) 511 | 512 | # 5 -> Start the upgrade 513 | 514 | try: 515 | r = session.post("%s/projects/%s/services/%s/?action=upgrade" % (api, environment_id, service["id"]), json=upgrade) 516 | r.raise_for_status() 517 | except HTTPError: 518 | bail("Unable to request an upgrade on Rancher") 519 | 520 | # 6 -> Wait for the upgrade to finish 521 | 522 | if not wait_for_upgrade_to_finish: 523 | msg("Upgrade started") 524 | else: 525 | msg("Upgrade started, waiting for upgrade to complete...") 526 | attempts = 0 527 | while service["state"] != "upgraded": 528 | time.sleep(2) 529 | attempts += 2 530 | if attempts > upgrade_timeout: 531 | message = "A timeout occured while waiting for Rancher to complete the upgrade" 532 | if rollback_on_error: 533 | bail(message, exit=False) 534 | warn("Processing image rollback...") 535 | 536 | try: 537 | r = session.post( 538 | "%s/projects/%s/services/%s/?action=rollback" % (api, environment_id, service["id"]) 539 | ) 540 | r.raise_for_status() 541 | except HTTPError: 542 | bail("Unable to request a rollback on Rancher") 543 | 544 | attempts = 0 545 | while service["state"] != "active": 546 | time.sleep(2) 547 | attempts += 2 548 | if attempts > upgrade_timeout: 549 | bail( 550 | "A timeout occured while waiting for Rancher to rollback the upgrade to its latest running state" 551 | ) 552 | try: 553 | r = session.get("%s/projects/%s/services/%s" % (api, environment_id, service["id"])) 554 | r.raise_for_status() 555 | except HTTPError: 556 | bail("Unable to request the service status from the Rancher API") 557 | else: 558 | service = r.json() 559 | 560 | warn("Service sucessfully rolled back") 561 | sys.exit(1) 562 | else: 563 | bail(message) 564 | try: 565 | r = session.get("%s/projects/%s/services/%s" % (api, environment_id, service["id"])) 566 | r.raise_for_status() 567 | except HTTPError: 568 | bail("Unable to fetch the service status from the Rancher API") 569 | else: 570 | service = r.json() 571 | 572 | if not finish_upgrade: 573 | msg("Service upgraded") 574 | sys.exit(0) 575 | else: 576 | msg("Finishing upgrade...") 577 | try: 578 | r = session.post("%s/projects/%s/services/%s/?action=finishupgrade" % (api, environment_id, service["id"])) 579 | r.raise_for_status() 580 | except HTTPError: 581 | bail("Unable to finish the upgrade in Rancher") 582 | 583 | attempts = 0 584 | while service["state"] != "active": 585 | time.sleep(2) 586 | attempts += 2 587 | if attempts > upgrade_timeout: 588 | bail("A timeout occured while waiting for Rancher to finish the previous upgrade") 589 | try: 590 | r = session.get("%s/projects/%s/services/%s" % (api, environment_id, service["id"])) 591 | r.raise_for_status() 592 | except HTTPError: 593 | bail("Unable to request the service status from the Rancher API") 594 | else: 595 | service = r.json() 596 | 597 | msg("Upgrade finished") 598 | 599 | sys.exit(0) 600 | 601 | 602 | def msg(message): 603 | click.echo(click.style(message, fg="green")) 604 | 605 | 606 | def warn(message): 607 | click.echo(click.style(message, fg="yellow")) 608 | 609 | 610 | def bail(message, exit=True): 611 | click.echo(click.style("Error: " + message, fg="red")) 612 | if exit: 613 | sys.exit(1) 614 | 615 | 616 | def debug_requests_on(): 617 | """Switches on logging of the requests module.""" 618 | HTTPConnection.debuglevel = 1 619 | 620 | logging.basicConfig() 621 | logging.getLogger().setLevel(logging.DEBUG) 622 | requests_log = logging.getLogger("requests.packages.urllib3") 623 | requests_log.setLevel(logging.DEBUG) 624 | requests_log.propagate = True 625 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="rancher-gitlab-deploy", 5 | version="1.7", 6 | description="Command line tool to ease updating services in Rancher from your GitLab CI pipeline", 7 | url="https://github.com/cdrx/rancher-gitlab-deploy", 8 | author="cdrx", 9 | license="MIT", 10 | packages=["rancher_gitlab_deploy"], 11 | zip_safe=False, 12 | install_requires=[ 13 | "click", 14 | "requests", 15 | "colorama", 16 | ], 17 | entry_points={ 18 | "console_scripts": [ 19 | "rancher-gitlab-deploy=rancher_gitlab_deploy.cli:main", 20 | ], 21 | }, 22 | ) 23 | --------------------------------------------------------------------------------