Add command to rollback release to CLI and API

This adds a command to the CLI and API to rollback a release name to a
specified version.

Change-Id: Ie1434da42ccc75c658b7bde7164b3f4c909be7c4
This commit is contained in:
Sean Eagan 2018-05-24 13:36:19 -05:00
parent 65ce95f3a4
commit 571c0b77f9
20 changed files with 562 additions and 15 deletions

View File

@ -0,0 +1,64 @@
# Copyright 2018 The Armada Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import falcon
from oslo_config import cfg
from armada import api
from armada.common import policy
from armada.handlers.tiller import Tiller
CONF = cfg.CONF
class Rollback(api.BaseResource):
"""Controller for performing a rollback of a release
"""
@policy.enforce('armada:rollback_release')
def on_post(self, req, resp, release):
try:
dry_run = req.get_param_as_bool('dry_run')
tiller = Tiller(
tiller_host=req.get_param('tiller_host'),
tiller_port=req.get_param_as_int(
'tiller_port') or CONF.tiller_port,
tiller_namespace=req.get_param(
'tiller_namespace', default=CONF.tiller_namespace),
dry_run=dry_run)
tiller.rollback_release(
release,
req.get_param_as_int('version') or 0,
wait=req.get_param_as_bool('wait'),
timeout=req.get_param_as_int('timeout') or 0)
resp.body = json.dumps(
{
'message': ('(dry run) ' if dry_run else '') +
'Rollback of {} complete.'.format(release),
}
)
resp.content_type = 'application/json'
resp.status = falcon.HTTP_200
except Exception as e:
self.logger.exception('Caught unexpected exception')
err_message = 'Failed to rollback release: {}'.format(e)
self.error(req.context, err_message)
self.return_error(
resp, falcon.HTTP_500, message=err_message)

View File

@ -23,6 +23,7 @@ from armada.api.controller.armada import Apply
from armada.api.middleware import AuthMiddleware
from armada.api.middleware import ContextMiddleware
from armada.api.middleware import LoggingMiddleware
from armada.api.controller.rollback import Rollback
from armada.api.controller.test import TestReleasesReleaseNameController
from armada.api.controller.test import TestReleasesManifestController
from armada.api.controller.health import Health
@ -61,6 +62,7 @@ def create(enable_middleware=CONF.middleware):
('health', Health()),
('apply', Apply()),
('releases', Release()),
('rollback/{release}', Rollback()),
('status', Status()),
('tests', TestReleasesManifestController()),
('test/{release}', TestReleasesReleaseNameController()),

124
armada/cli/rollback.py Normal file
View File

@ -0,0 +1,124 @@
# Copyright 2018 The Armada Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import click
from oslo_config import cfg
from armada.cli import CliAction
from armada.handlers.tiller import Tiller
CONF = cfg.CONF
@click.group()
def rollback():
""" Rollback a helm release
"""
DESC = """
This command performs a rollback on the specified release.
To rollback a release, run:
\b
$ armada rollback --release my_release
"""
SHORT_DESC = "Command performs a release rollback."
@rollback.command(name='rollback',
help=DESC,
short_help=SHORT_DESC)
@click.option('--release',
help="Release to rollback.",
type=str)
@click.option('--version',
help="Version of release to rollback to. 0 represents the "
"previous release",
type=int,
default=0)
@click.option('--dry-run',
help="Perform a dry-run rollback.",
is_flag=True)
@click.option('--tiller-host',
help="Tiller host IP.",
default=None)
@click.option('--tiller-port',
help="Tiller host port.",
type=int,
default=CONF.tiller_port)
@click.option('--tiller-namespace', '-tn',
help="Tiller namespace.",
type=str,
default=CONF.tiller_namespace)
@click.option('--timeout',
help="Specifies time to wait for rollback to complete.",
type=int,
default=0)
@click.option('--wait',
help=("Wait until rollback is complete before returning."),
is_flag=True)
@click.option('--debug',
help="Enable debug logging.",
is_flag=True)
@click.pass_context
def rollback_charts(ctx, release, version, dry_run, tiller_host, tiller_port,
tiller_namespace, timeout, wait, debug):
CONF.debug = debug
Rollback(ctx, release, version, dry_run, tiller_host, tiller_port,
tiller_namespace, timeout, wait).safe_invoke()
class Rollback(CliAction):
def __init__(self,
ctx,
release,
version,
dry_run,
tiller_host,
tiller_port,
tiller_namespace,
timeout,
wait):
super(Rollback, self).__init__()
self.ctx = ctx
self.release = release
self.version = version
self.dry_run = dry_run
self.tiller_host = tiller_host
self.tiller_port = tiller_port
self.tiller_namespace = tiller_namespace
self.timeout = timeout
self.wait = wait
def invoke(self):
tiller = Tiller(
tiller_host=self.tiller_host, tiller_port=self.tiller_port,
tiller_namespace=self.tiller_namespace, dry_run=self.dry_run)
response = tiller.rollback_release(
self.release,
self.version,
wait=self.wait,
timeout=self.timeout)
self.output(response)
def output(self, response):
self.logger.info(('(dry run) ' if self.dry_run else '') +
'Rollback of %s complete.', self.release)

View File

@ -130,6 +130,15 @@ class ArmadaClient(object):
return resp.json()
def post_rollback_release(self, release, query=None, timeout=None):
endpoint = self._set_endpoint('1.0', 'rollback/{}'.format(release))
resp = self.session.get(endpoint, query=query, timeout=timeout)
self._check_response(resp)
return resp.json()
def get_test_release(self, release=None, query=None, timeout=None):
endpoint = self._set_endpoint('1.0', 'test/{}'.format(release))

View File

@ -36,6 +36,12 @@ armada_policies = [
check_str=base.RULE_ADMIN_REQUIRED,
description='Test manifest',
operations=[{'path': '/api/v1.0/tests/', 'method': 'POST'}]),
policy.DocumentedRuleDefault(
name=base.ARMADA % 'rollback_release',
check_str=base.RULE_ADMIN_REQUIRED,
description='Rollback release',
operations=[{'path': '/api/v1.0/rollback/{release}', 'method': 'POST'}]
),
]

View File

@ -154,6 +154,16 @@ class GetReleaseContentException(TillerException):
super(GetReleaseContentException, self).__init__(message)
class RollbackReleaseException(TillerException):
'''Exception that occurs during a failed Release Rollback'''
def __init__(self, release, version):
message = 'Failed to rollback release {} to version {}'.format(
release, version)
super(RollbackReleaseException, self).__init__(message)
class TillerPodNotFoundException(TillerException):
'''
Exception that occurs when a tiller pod cannot be found using the labels

View File

@ -93,6 +93,8 @@ class Armada(object):
self.dry_run = dry_run
self.force_wait = force_wait
self.timeout = timeout
# TODO: Use dependency injection i.e. pass in a Tiller instead of
# creating it here.
self.tiller = Tiller(
tiller_host=tiller_host, tiller_port=tiller_port,
tiller_namespace=tiller_namespace, dry_run=dry_run)

View File

@ -22,6 +22,7 @@ from hapi.services.tiller_pb2 import GetVersionRequest
from hapi.services.tiller_pb2 import InstallReleaseRequest
from hapi.services.tiller_pb2 import ListReleasesRequest
from hapi.services.tiller_pb2_grpc import ReleaseServiceStub
from hapi.services.tiller_pb2 import RollbackReleaseRequest
from hapi.services.tiller_pb2 import TestReleaseRequest
from hapi.services.tiller_pb2 import UninstallReleaseRequest
from hapi.services.tiller_pb2 import UpdateReleaseRequest
@ -322,7 +323,6 @@ class Tiller(object):
Update a Helm Release
'''
timeout = self._check_timeout(wait, timeout)
rel_timeout = self.timeout if not timeout else timeout
LOG.info('Helm update release%s: wait=%s, timeout=%s',
(' (dry run)' if self.dry_run else ''),
@ -349,7 +349,7 @@ class Tiller(object):
timeout=timeout)
update_msg = stub.UpdateRelease(
release_request, rel_timeout + GRPC_EPSILON,
release_request, timeout + GRPC_EPSILON,
metadata=self.metadata)
tiller_result = TillerResult(
@ -376,7 +376,6 @@ class Tiller(object):
Create a Helm Release
'''
timeout = self._check_timeout(wait, timeout)
rel_timeout = self.timeout if not timeout else timeout
LOG.info('Helm install release%s: wait=%s, timeout=%s',
(' (dry run)' if self.dry_run else ''),
@ -400,7 +399,7 @@ class Tiller(object):
timeout=timeout)
install_msg = stub.InstallRelease(
release_request, rel_timeout + GRPC_EPSILON,
release_request, timeout + GRPC_EPSILON,
metadata=self.metadata)
tiller_result = TillerResult(
@ -683,9 +682,44 @@ class Tiller(object):
else:
LOG.error("Unable to exectue name: % type: %s", name, action_type)
def rollback_release(self,
release_name,
version,
wait=False,
timeout=None):
'''
Rollback a helm release.
'''
timeout = self._check_timeout(wait, timeout)
LOG.debug('Helm rollback%s of release=%s, version=%s, '
'wait=%s, timeout=%s',
(' (dry run)' if self.dry_run else ''),
release_name, version, wait, timeout)
try:
stub = ReleaseServiceStub(self.channel)
rollback_request = RollbackReleaseRequest(
name=release_name,
version=version,
dry_run=self.dry_run,
wait=wait,
timeout=timeout)
rollback_msg = stub.RollbackRelease(
rollback_request,
timeout + GRPC_EPSILON,
metadata=self.metadata)
LOG.debug('RollbackRelease= %s', rollback_msg)
return
except Exception:
raise ex.RollbackReleaseException(release_name, version)
def _check_timeout(self, wait, timeout):
if wait and timeout <= 0:
LOG.warn('Tiller timeout is invalid or unspecified, '
'using default %ss.', const.DEFAULT_TILLER_TIMEOUT)
timeout = const.DEFAULT_TILLER_TIMEOUT
if timeout is None or timeout <= 0:
if wait:
LOG.warn('Tiller timeout is invalid or unspecified, '
'using default %ss.', self.timeout)
timeout = self.timeout
return timeout

View File

@ -20,6 +20,7 @@ from oslo_log import log
from armada.cli.apply import apply_create
from armada.cli.delete import delete_charts
from armada.cli.rollback import rollback_charts
from armada.cli.test import test_charts
from armada.cli.tiller import tiller_service
from armada.cli.validate import validate_manifest
@ -54,6 +55,7 @@ def main(ctx, debug, api, url, token):
\b
$ armada apply
$ armada delete
$ armada rollback
$ armada test
$ armada tiller
$ armada validate
@ -93,6 +95,7 @@ def main(ctx, debug, api, url, token):
main.add_command(apply_create)
main.add_command(delete_charts)
main.add_command(rollback_charts)
main.add_command(test_charts)
main.add_command(tiller_service)
main.add_command(validate_manifest)

View File

@ -0,0 +1,65 @@
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import mock
from armada.api.controller import rollback
from armada.common.policies import base as policy_base
from armada.tests import test_utils
from armada.tests.unit.api import base
class RollbackReleaseControllerTest(base.BaseControllerTest):
@mock.patch.object(rollback, 'Tiller')
def test_rollback_controller_pass(self, mock_tiller):
rules = {'armada:rollback_release': '@'}
self.policy.set_rules(rules)
rollback_release = mock_tiller.return_value.rollback_release
rollback_release.return_value = None
resp = self.app.simulate_post('/api/v1.0/rollback/test-release')
self.assertEqual(200, resp.status_code)
self.assertEqual('Rollback of test-release complete.',
json.loads(resp.text)['message'])
@test_utils.attr(type=['negative'])
class RollbackReleaseControllerNegativeTest(base.BaseControllerTest):
@mock.patch.object(rollback, 'Tiller')
def test_rollback_controller_tiller_exc_return_500(self, mock_tiller):
rules = {'armada:rollback_release': '@'}
self.policy.set_rules(rules)
mock_tiller.side_effect = Exception
resp = self.app.simulate_post('/api/v1.0/rollback/fake-release')
self.assertEqual(500, resp.status_code)
@test_utils.attr(type=['negative'])
class RollbackReleaseControllerNegativeRbacTest(base.BaseControllerTest):
def test_rollback_release_insufficient_permissions(self):
"""Tests the GET /api/v1.0/rollback/{release} endpoint returns 403
following failed authorization.
"""
rules = {'armada:rollback_release': policy_base.RULE_ADMIN_REQUIRED}
self.policy.set_rules(rules)
resp = self.app.simulate_post('/api/v1.0/rollback/fake-release')
self.assertEqual(403, resp.status_code)

View File

@ -19,6 +19,7 @@ policy_data = """
"armada:validate_manifest": "rule:admin_required"
"armada:test_release": "rule:admin_required"
"armada:test_manifest": "rule:admin_required"
"armada:rollback_release": "rule:admin_required"
"tiller:get_status": "rule:admin_required"
"tiller:get_release": "rule:admin_required"
"""

View File

@ -279,3 +279,26 @@ class TillerTestCase(base.ArmadaTestCase):
uninstall_release_stub.assert_called_once_with(
mock_uninstall_release_request.return_value, tiller_obj.timeout,
metadata=tiller_obj.metadata)
@mock.patch('armada.handlers.tiller.K8s')
@mock.patch('armada.handlers.tiller.grpc')
@mock.patch.object(tiller, 'RollbackReleaseRequest')
@mock.patch.object(tiller, 'ReleaseServiceStub')
def test_rollback_release(self, mock_release_service_stub,
mock_rollback_release_request, _, __):
mock_release_service_stub.return_value.RollbackRelease\
.return_value = {}
tiller_obj = tiller.Tiller('host', '8080', None)
self.assertIsNone(tiller_obj.rollback_release('release', 0))
mock_release_service_stub.assert_called_once_with(
tiller_obj.channel)
rollback_release_stub = mock_release_service_stub.return_value. \
RollbackRelease
rollback_release_stub.assert_called_once_with(
mock_rollback_release_request.return_value, tiller_obj.timeout +
tiller.GRPC_EPSILON,
metadata=tiller_obj.metadata)

View File

@ -176,6 +176,7 @@ conf:
policy:
admin_required: 'role:admin'
'armada:create_endpoints': 'rule:admin_required'
'armada:rollback_release': 'rule:admin_required'
'armada:test_manifest': 'rule:admin_required'
'armada:test_release': 'rule:admin_required'
'armada:validate_manifest': 'rule:admin_required'

View File

@ -11,6 +11,7 @@ Commands Guide
:caption: Contents:
apply.rst
rollback.rst
test.rst
tiller.rst
validate.rst

View File

@ -0,0 +1,32 @@
Armada - Rollback
=================
Commands
--------
.. code:: bash
Usage: armada rollback [OPTIONS]
This command performs a rollback on the specified release.
To rollback a release, run:
$ armada rollback --release my_release
Options:
--dry-run Perform a dry-run rollback.
--release TEXT Release to rollback.
--tiller-host TEXT Tiller Host IP
--tiller-port INTEGER Tiller Host Port
-tn, --tiller-namespace TEXT Tiller Namespace
--timeout INTEGER Tiller Host IP
--version INTEGER Version of release to rollback to. 0 represents the previous release
--wait Version of release to rollback to. 0 represents the previous release
--help Show this message and exit.
Synopsis
--------
The rollback command will perform helm rollback on the release.

View File

@ -230,6 +230,63 @@ Successful installation/update of manifest
Unable to Authorize or Permission
**405**
^^^^^^^
Failed to perform action
POST ``/rollback/{release}``
----------------------------
Summary
+++++++
Rollback release name
Parameters
++++++++++
.. csv-table::
:delim: |
:header: "Name", "Located in", "Required", "Type", "Format", "Properties", "Description"
:widths: 20, 15, 10, 10, 10, 20, 30
release | path | Yes | string | | | name of the release to rollback
version | query | No | integer | | | version of the release to rollback to
dry_run | query | No | boolean | | | perform dry run
wait | query | No | boolean | | | whether to wait for rollback to complete before returning
timeout | query | No | integer | | | time to wait for rollback to complete before timing out
Request
+++++++
Responses
+++++++++
**200**
^^^^^^^
Succesfully Test release response
**Example:**
.. code-block:: javascript
{
"message": {
"message": "Rollback of release xyz complete"
}
}
**403**
^^^^^^^
Unable to Authorize or Permission
**405**
^^^^^^^

View File

@ -108,6 +108,12 @@ b. Helm Install
docker exec armada armada test --file=/examples/openstack-helm.yaml
8. Rolling back Releases:
.. code:: bash
docker exec armada armada rollback --release=armada-keystone
Overriding Manifest Values
--------------------------
It is possible to override manifest values from the command line using the

View File

@ -11,6 +11,10 @@
# POST api/v1.0/apply/
#"armada:create_endpoints": "rule:admin_required"
# rollback release
# POST api/v1.0/rollback/{release}
#"armada:rollback_release": "rule:admin_required"
# validate installed manifest
# POST /api/v1.0/validate/
#"armada:validate_manifest": "rule:admin_required"

View File

@ -153,7 +153,7 @@ paths:
/api/v1.0/test/{release_name}:
post:
description: Test specified release name
operationId: postReleaseName
operationId: postTestReleaseName
parameters:
- name: release_name
in: path
@ -173,6 +173,39 @@ paths:
$ref: "#/responses/err-forbidden"
'500':
$ref: "#/responses/err-server-error"
/api/v1.0/rollback/{release_name}:
post:
description: Rollback the specified release name
operationId: postRollbackReleaseName
parameters:
- name: release_name
in: path
required: true
description: Name of the release to be rolled back
type: string
- name: version
in: query
required: false
type: integer
description: Version number of release to rollback to. 0 represents
the previous version
default: 0
- $ref: "#/parameters/x-auth-token"
- $ref: "#/parameters/tiller-host"
- $ref: "#/parameters/tiller-port"
- $ref: "#/parameters/tiller-namespace"
- $ref: "#/parameters/dry-run"
- $ref: "#/parameters/wait"
- $ref: "#/parameters/timeout"
responses:
'200':
$ref: "#/responses/response-post-rollback-release"
'401':
$ref: "#/responses/err-no-auth"
'403':
$ref: "#/responses/err-forbidden"
'500':
$ref: "#/responses/err-server-error"
/api/v1.0/validatedesign:
post:
description: Validate a design
@ -257,20 +290,22 @@ parameters:
name: dry_run
required: false
type: boolean
description: Flag to simulate an install if set to True
description: Flag to simulate an action if set to True
default: False
wait:
in: query
name: wait
required: false
type: boolean
description: Specifies whether Tiller should wait until all charts are deployed.
description: Specifies whether Tiller should wait until the action is
complete before returning.
timeout:
in: query
name: timeout
required: false
type: integer
description: Specifies time in seconds Tiller should wait for charts to deploy until timing out.
description: Specifies time in seconds Tiller should wait for the action to
complete before timing out.
default: 3600
responses:
# HTTP error responses
@ -297,6 +332,13 @@ responses:
schema:
allOf:
- $ref: "#/definitions/applyresult"
response-post-rollback-release:
description: Response of a rollback of a specified release name
schema:
allOf:
- $ref: "#/definitions/rollbackresult"
example:
message: "Rollback of release xyz complete"
response-post-test-release:
description: Response of a test of a specified release name
schema:
@ -405,6 +447,11 @@ definitions:
type: object
additionalProperties:
type: string
rollbackresult:
type: object
properties:
message:
type: string
testresult:
type: object
properties:

View File

@ -180,7 +180,7 @@ paths:
tags:
- Tests
description: Test specified release name
operationId: postReleaseName
operationId: postTestReleaseName
parameters:
- $ref: "#/components/parameters/release-name"
- $ref: "#/components/parameters/x-auth-token"
@ -199,7 +199,43 @@ paths:
options:
tags:
- Tests
operationId: optReleaseName
operationId: optTestReleaseName
parameters:
- $ref: "#/components/parameters/release-name"
responses:
'200':
$ref: "#/components/responses/response-options"
/api/v1.0/rollback/{release_name}:
post:
tags:
- Rollback
description: Rollback the specified release name
operationId: postRollbackReleaseName
parameters:
- $ref: "#/components/parameters/release-name"
- $ref: "#/components/parameters/release-version"
- $ref: "#/components/parameters/x-auth-token"
- $ref: "#/components/parameters/tiller-host"
- $ref: "#/components/parameters/tiller-port"
- $ref: "#/components/parameters/tiller-namespace"
- $ref: "#/components/parameters/dry-run"
- $ref: "#/components/parameters/wait"
- $ref: "#/components/parameters/timeout"
responses:
'200':
$ref: "#/components/responses/response-post-rollback-release"
'401':
$ref: "#/components/responses/err-no-auth"
'403':
$ref: "#/components/responses/err-forbidden"
'500':
$ref: "#/components/responses/err-server-error"
options:
tags:
- Rollback
operationId: optRollbackReleaseName
parameters:
- $ref: "#/components/parameters/release-name"
responses:
'200':
$ref: "#/components/responses/response-options"
@ -242,9 +278,16 @@ components:
in: path
name: release_name
required: true
description: Name of the release to be tested
description: Name of the release to be acted upon
schema:
type: string
release-version:
in: query
name: version
required: false
description: "Version number of release to rollback to. 0 represents the previous version. Default: `0`"
schema:
type: integer
tiller-host:
in: query
name: tiller_host
@ -369,6 +412,12 @@ components:
application/json:
schema:
$ref: "#/components/schemas/result-apply"
response-post-rollback-release:
description: Response of a rollback of a specified release name
content:
application/json:
schema:
$ref: "#/components/schemas/result-rollback"
response-post-test-release:
description: Response of a test of a specified release name
content:
@ -488,6 +537,13 @@ components:
diff:
key1: val1
key2: val2
result-rollback:
type: object
properties:
message:
type: string
example:
message: "Rollback of release xyz complete"
result-test:
type: object
properties: