Skip to content

Commit 07dbfd2

Browse files
author
Gabriel Monroy
committed
Merge pull request #1665 from gabrtv/tags
Application tags for scheduling to specific hosts
2 parents b941ad7 + 59a5796 commit 07dbfd2

11 files changed

Lines changed: 641 additions & 7 deletions

File tree

client/deis.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
domains manage and assign domain names to your applications
2020
builds manage builds created using `git push`
2121
limits manage resource limits for your application
22+
tags manage tags for application containers
2223
releases manage releases of an application
2324
2425
keys manage ssh keys used for `git push` deployments
@@ -1591,6 +1592,127 @@ def ps_scale(self, args):
15911592
else:
15921593
raise ResponseError(response)
15931594

1595+
def tags(self, args):
1596+
"""
1597+
Valid commands for tags:
1598+
1599+
tags:list list tags for an app
1600+
tags:set set tags for an app
1601+
tags:unset unset tags for an app
1602+
1603+
Use `deis help [command]` to learn more.
1604+
"""
1605+
sys.argv[1] = 'tags:list'
1606+
args = docopt(self.tags_list.__doc__)
1607+
return self.tags_list(args)
1608+
1609+
def tags_list(self, args):
1610+
"""
1611+
Lists tags for an application.
1612+
1613+
Usage: deis tags:list [options]
1614+
1615+
Options:
1616+
-a --app=<app>
1617+
the uniquely identifiable name of the application.
1618+
"""
1619+
app = args.get('--app')
1620+
if not app:
1621+
app = self._session.app
1622+
response = self._dispatch('get', "/api/apps/{}/config".format(app))
1623+
if response.status_code == requests.codes.ok: # @UndefinedVariable
1624+
self._print_tags(app, response.json())
1625+
else:
1626+
raise ResponseError(response)
1627+
1628+
def tags_set(self, args):
1629+
"""
1630+
Sets tags for an application.
1631+
1632+
A tag is a key/value pair used to tag an application's containers.
1633+
This is often used to restrict workloads to specific hosts.
1634+
1635+
Usage: deis tags:set [options] <key>=<value>...
1636+
1637+
Arguments:
1638+
<key> the tag key, for example: "environ" or "rack"
1639+
<value> the tag value, for example: "prod" or "1"
1640+
1641+
Options:
1642+
-a --app=<app>
1643+
the uniquely identifiable name for the application.
1644+
"""
1645+
app = args.get('--app')
1646+
if not app:
1647+
app = self._session.app
1648+
body = {}
1649+
body['tags'] = json.dumps(dictify(args['<key>=<value>']))
1650+
sys.stdout.write('Applying tags... ')
1651+
sys.stdout.flush()
1652+
try:
1653+
progress = TextProgress()
1654+
progress.start()
1655+
response = self._dispatch('post', "/api/apps/{}/config".format(app), json.dumps(body))
1656+
finally:
1657+
progress.cancel()
1658+
progress.join()
1659+
if response.status_code == requests.codes.created: # @UndefinedVariable
1660+
version = response.headers['x-deis-release']
1661+
print("done, v{}\n".format(version))
1662+
1663+
self._print_tags(app, response.json())
1664+
else:
1665+
raise ResponseError(response)
1666+
1667+
def tags_unset(self, args):
1668+
"""
1669+
Unsets tags for an application.
1670+
1671+
Usage: deis tags:unset [options] <key>...
1672+
1673+
Arguments:
1674+
<key> the tag key to unset, for example: "environ" or "rack"
1675+
1676+
Options:
1677+
-a --app=<app>
1678+
the uniquely identifiable name for the application.
1679+
"""
1680+
app = args.get('--app')
1681+
if not app:
1682+
app = self._session.app
1683+
values = {}
1684+
for k in args.get('<key>'):
1685+
values[k] = None
1686+
body = {}
1687+
body['tags'] = json.dumps(values)
1688+
sys.stdout.write('Applying tags... ')
1689+
sys.stdout.flush()
1690+
try:
1691+
progress = TextProgress()
1692+
progress.start()
1693+
response = self._dispatch('post', "/api/apps/{}/config".format(app), json.dumps(body))
1694+
finally:
1695+
progress.cancel()
1696+
progress.join()
1697+
if response.status_code == requests.codes.created: # @UndefinedVariable
1698+
version = response.headers['x-deis-release']
1699+
print("done, v{}\n".format(version))
1700+
self._print_tags(app, response.json())
1701+
else:
1702+
raise ResponseError(response)
1703+
1704+
def _print_tags(self, app, config):
1705+
items = json.loads(config['tags'])
1706+
print("=== {} Tags".format(app))
1707+
if len(items) == 0:
1708+
print('No tags defined')
1709+
return
1710+
keys = sorted(items)
1711+
width = max(map(len, keys)) + 5
1712+
for k in keys:
1713+
v = items[k]
1714+
print(("{k:<" + str(width) + "} {v}").format(**locals()))
1715+
15941716
def keys(self, args):
15951717
"""
15961718
Valid commands for SSH keys:

controller/api/models.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,8 @@ def _command_announceable(self):
337337
def create(self):
338338
image = self.release.image
339339
kwargs = {'memory': self.release.config.memory,
340-
'cpu': self.release.config.cpu}
340+
'cpu': self.release.config.cpu,
341+
'tags': self.release.config.tags}
341342
self._scheduler.create(name=self._job_id,
342343
image=image,
343344
command=self._command,
@@ -366,7 +367,8 @@ def deploy(self, release):
366367
image = self.release.image
367368
c_type = self.type
368369
kwargs = {'memory': self.release.config.memory,
369-
'cpu': self.release.config.cpu}
370+
'cpu': self.release.config.cpu,
371+
'tags': self.release.config.tags}
370372
self._scheduler.create(name=new_job_id,
371373
image=image,
372374
command=self._command.format(**locals()),
@@ -459,6 +461,7 @@ class Config(UuidAuditedModel):
459461
values = JSONField(default={}, blank=True)
460462
memory = JSONField(default={}, blank=True)
461463
cpu = JSONField(default={}, blank=True)
464+
tags = JSONField(default={}, blank=True)
462465

463466
class Meta:
464467
get_latest_by = 'created'
@@ -595,6 +598,22 @@ def save(self, *args, **kwargs): # noqa
595598
if changes:
596599
changes = 'changed limits for '+', '.join(changes)
597600
self.summary += "{} {}".format(self.config.owner, changes)
601+
# if the tags changed, log the dict diff
602+
changes = []
603+
old_tags = old_config.tags if old_config else {}
604+
diff = dict_diff(self.config.tags, old_tags)
605+
# try to be as succinct as possible
606+
added = ', '.join(k for k in diff.get('added', {}))
607+
added = 'added tag ' + added if added else ''
608+
changed = ', '.join(k for k in diff.get('changed', {}))
609+
changed = 'changed tag ' + changed if changed else ''
610+
deleted = ', '.join(k for k in diff.get('deleted', {}))
611+
deleted = 'deleted tag ' + deleted if deleted else ''
612+
changes = ', '.join(i for i in (added, changed, deleted) if i)
613+
if changes:
614+
if self.summary:
615+
self.summary += ' and '
616+
self.summary += "{} {}".format(self.config.owner, changes)
598617
if not self.summary:
599618
if self.version == 1:
600619
self.summary = "{} created the initial release".format(self.owner)

controller/api/serializers.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
PROCTYPE_MATCH = re.compile(r'^(?P<type>[a-z]+)')
1818
MEMLIMIT_MATCH = re.compile(r'^(?P<mem>[0-9]+[BbKkMmGg])$')
1919
CPUSHARE_MATCH = re.compile(r'^(?P<cpu>[0-9]+)$')
20+
TAGKEY_MATCH = re.compile(r'^[a-z]+$')
21+
TAGVAL_MATCH = re.compile(r'^\w+$')
2022

2123

2224
class OwnerSlugRelatedField(serializers.SlugRelatedField):
@@ -114,6 +116,8 @@ class ConfigSerializer(serializers.ModelSerializer):
114116
model_field=models.Config()._meta.get_field('memory'), required=False)
115117
cpu = serializers.ModelField(
116118
model_field=models.Config()._meta.get_field('cpu'), required=False)
119+
tags = serializers.ModelField(
120+
model_field=models.Config()._meta.get_field('tags'), required=False)
117121

118122
class Meta:
119123
"""Metadata options for a :class:`ConfigSerializer`."""
@@ -149,6 +153,16 @@ def validate_cpu(self, attrs, source):
149153
raise serializers.ValidationError("CPU shares must be between 0 and 1024")
150154
return attrs
151155

156+
def validate_tags(self, attrs, source):
157+
for k, v in attrs.get(source, {}).items():
158+
if v is None: # use NoneType to unset a value
159+
continue
160+
if not re.match(TAGKEY_MATCH, k):
161+
raise serializers.ValidationError("Tag keys can only contain [a-z]")
162+
if not re.match(TAGVAL_MATCH, str(v)):
163+
raise serializers.ValidationError("Invalid tag value")
164+
return attrs
165+
152166

153167
class ReleaseSerializer(serializers.ModelSerializer):
154168
"""Serialize a :class:`~api.models.Release` model."""

0 commit comments

Comments
 (0)