Skip to content

Commit 4f4bff3

Browse files
authored
Support managed updates (#349)
A few notes: - We handle v2 deployer in this PR. In this updated schema, the images are declared explicitly under `x-google-marketplace.images`. We will use a `Secret` for the parameters, instead of `ConfigMap`. The properties that receive images will be generated by the deployer. - To __additionally__ indicate that an app supports KALM in a v2 deployer, the following entry in `schema.yaml` v2 is added: ```yaml x-google-marketplace: schemaVersion: v2 managedUpdates: kalmSupported: true ``` Note that it's possible to use v2 deployer without supporting managed updates. In fact, v2 will be the desired implementation for new partners.
1 parent a47e38b commit 4f4bff3

File tree

12 files changed

+526
-96
lines changed

12 files changed

+526
-96
lines changed

marketplace/deployer_util/clean_iam_resources.sh

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,11 @@ set -eox pipefail
1919
[[ -z "$NAME" ]] && echo "NAME must be set" && exit 1
2020
[[ -z "$NAMESPACE" ]] && echo "NAMESPACE must be set" && exit 1
2121

22-
# Clean up IAM resources.
23-
kubectl delete --namespace="$NAMESPACE" --filename=- <<EOF
24-
apiVersion: v1
25-
kind: ServiceAccount
26-
metadata:
27-
name: "${NAME}-deployer-sa"
28-
namespace: "${NAMESPACE}"
29-
---
30-
apiVersion: rbac.authorization.k8s.io/v1
31-
kind: RoleBinding
32-
metadata:
33-
name: "${NAME}-deployer-rb"
34-
namespace: "${NAMESPACE}"
35-
EOF
22+
# Delete the service account and RBAC objects by labels.
23+
# Note that only resources of a one-shot deployer have this label.
24+
# Resources of a KALM-managed deployer have
25+
# app.kubernetes.io/component=kalm.marketplace.cloud.google.com label.
26+
kubectl delete --namespace="$NAMESPACE" \
27+
ServiceAccount,RoleBinding \
28+
-l 'app.kubernetes.io/component'='deployer.marketplace.cloud.google.com','app.kubernetes.io/name'="$NAME" \
29+
--ignore-not-found

marketplace/deployer_util/config_helper.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,9 @@ def __init__(self, dictionary):
217217
dictionary, 'publishedVersionMetadata', lambda v: SchemaVersionMeta(v),
218218
'x-google-marketplace.publishedVersionMetadata is required')
219219

220+
self._managed_updates = SchemaManagedUpdates(
221+
dictionary.get('managedUpdates', {}))
222+
220223
images = _must_get(dictionary, 'images',
221224
'x-google-marketplace.images is required')
222225
self._images = {k: SchemaImage(k, v) for k, v in images.iteritems()}
@@ -244,10 +247,25 @@ def published_version_meta(self):
244247
def images(self):
245248
return self._images
246249

250+
@property
251+
def managed_updates(self):
252+
return self._managed_updates
253+
247254
def is_v2(self):
248255
return self._schema_version == _SCHEMA_VERSION_2
249256

250257

258+
class SchemaManagedUpdates:
259+
"""Accesses managedUpdates."""
260+
261+
def __init__(self, dictionary):
262+
self._kalm_supported = dictionary.get('kalmSupported', False)
263+
264+
@property
265+
def kalm_supported(self):
266+
return self._kalm_supported
267+
268+
251269
class SchemaClusterConstraints:
252270
"""Accesses top level clusterConstraints."""
253271

marketplace/deployer_util/config_helper_test.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,9 @@ def test_v2_fields(self):
648648
- BUG_FIX
649649
recommended: true
650650
651+
managedUpdates:
652+
kalmSupported: true
653+
651654
images:
652655
main:
653656
properties:
@@ -690,6 +693,9 @@ def test_v2_fields(self):
690693
'db.image.tag')
691694
self.assertEqual(images['db'].properties['db.image.tag'].part_type, 'TAG')
692695

696+
self.assertEqual(schema.x_google_marketplace.managed_updates.kalm_supported,
697+
True)
698+
693699
def test_k8s_version_constraint(self):
694700
schema = config_helper.Schema.load_yaml("""
695701
applicationApiVersion: v1beta1

marketplace/deployer_util/expand_config.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
according to their schema.
3333
"""
3434

35+
_IMAGE_REPO_PREFIX_PROPERTY_NAME = '__image_repo_prefix__'
36+
3537

3638
class InvalidProperty(Exception):
3739
pass
@@ -41,6 +43,10 @@ class MissingRequiredProperty(Exception):
4143
pass
4244

4345

46+
class MissingRequiredValue(Exception):
47+
pass
48+
49+
4450
def main():
4551
parser = ArgumentParser(description=_PROG_HELP)
4652
schema_values_common.add_to_argument_parser(parser)
@@ -64,25 +70,32 @@ def expand(values_dict, schema, app_uid=''):
6470
"""Returns the expanded values according to schema."""
6571
schema.validate()
6672

73+
valid_property_names = set(schema.properties.keys() +
74+
[_IMAGE_REPO_PREFIX_PROPERTY_NAME])
6775
for k in values_dict:
68-
if k not in schema.properties:
76+
if k not in valid_property_names:
6977
raise InvalidProperty('No such property defined in schema: {}'.format(k))
7078

7179
# Captures the final property name-value mappings.
7280
# This has both properties directly specified under schema's `properties` and
7381
# generated properties. See below for details about generated properties.
7482
result = {}
75-
# Captures only the generated properties. These are not directly specified in the schema
76-
# under `properties`. Rather, their name are specified in special `generatedProperties` fields
77-
# under individual property `x-google-marketplace`.
83+
# Captures only the generated properties. These are not directly specified in
84+
# the schema under `properties`. Rather, their name are specified in special
85+
# `generatedProperties` fields under each property's `x-google-marketplace`.
7886
# Note that properties with generated values are NOT generated properties.
7987
generated = {}
8088

89+
if schema.is_v2():
90+
# Handles the images section of the schema.
91+
generate_v2_image_properties(schema, values_dict, generated)
92+
8193
# Copy explicitly specified values and generate values into result.
8294
for k, prop in schema.properties.iteritems():
8395
v = values_dict.get(k, None)
8496

85-
# The value is not explicitly specified and thus is eligible for auto-generation.
97+
# The value is not explicitly specified and
98+
# thus is eligible for auto-generation.
8699
if v is None:
87100
if prop.password:
88101
v = generate_password(prop.password)
@@ -106,7 +119,7 @@ def expand(values_dict, schema, app_uid=''):
106119
if not isinstance(v, str):
107120
raise InvalidProperty(
108121
'Invalid value for IMAGE property {}: {}'.format(k, v))
109-
generate_properties_for_image(prop, v, generated)
122+
generate_v1_properties_for_image(prop, v, generated)
110123
elif prop.string:
111124
if not isinstance(v, str):
112125
raise InvalidProperty(
@@ -118,13 +131,13 @@ def expand(values_dict, schema, app_uid=''):
118131
'Invalid value for TLS_CERTIFICATE property {}: {}'.format(k, v))
119132
generate_properties_for_tls_certificate(prop, v, generated)
120133

121-
# Copy generated properties into result, validating that there are no collisions.
122134
if v is not None:
123135
result[k] = v
124136

125137
validate_value_types(result, schema)
126138
validate_required_props(result, schema)
127139

140+
# Copy generated properties into result, validating no collisions.
128141
for k, v in generated.iteritems():
129142
if k in result:
130143
raise InvalidProperty(
@@ -155,7 +168,7 @@ def generate_properties_for_appuid(prop, value, result):
155168
result[prop.application_uid.application_create] = False if value else True
156169

157170

158-
def generate_properties_for_image(prop, value, result):
171+
def generate_v1_properties_for_image(prop, value, result):
159172
if prop.image.split_by_colon:
160173
before_name, after_name = prop.image.split_by_colon
161174
parts = value.split(':', 1)
@@ -185,6 +198,37 @@ def generate_properties_for_image(prop, value, result):
185198
result[tag_name] = tag_value
186199

187200

201+
def generate_v2_image_properties(schema, values_dict, result):
202+
repo_prefix = values_dict.get(_IMAGE_REPO_PREFIX_PROPERTY_NAME, None)
203+
if not repo_prefix:
204+
raise MissingRequiredValue('A valid value for __image_repo_prefix__ '
205+
'must be specified in values.yaml')
206+
tag = schema.x_google_marketplace.published_version
207+
for img in schema.x_google_marketplace.images.values():
208+
if img.name:
209+
# Allows an empty image name for legacy reason.
210+
registry_repo = '{}/{}'.format(repo_prefix, img.name)
211+
else:
212+
registry_repo = repo_prefix
213+
registry, repo = registry_repo.split('/', 1)
214+
full = '{}:{}'.format(registry_repo, tag)
215+
for prop in img.properties.values():
216+
if prop.part_type == config_helper.IMAGE_PROJECTION_TYPE_FULL:
217+
result[prop.name] = full
218+
elif prop.part_type == config_helper.IMAGE_PROJECTION_TYPE_REGISTRY:
219+
result[prop.name] = registry
220+
elif prop.part_type == config_helper.IMAGE_PROJECTION_TYPE_REGISTRY_REPO:
221+
result[prop.name] = registry_repo
222+
elif prop.part_type == config_helper.IMAGE_PROJECTION_TYPE_REPO:
223+
result[prop.name] = repo
224+
elif prop.part_type == config_helper.IMAGE_PROJECTION_TYPE_TAG:
225+
result[prop.name] = tag
226+
else:
227+
raise InvalidProperty(
228+
'Invalid type for images.properties.type: {}'.format(
229+
prop.part_type))
230+
231+
188232
def generate_properties_for_string(prop, value, result):
189233
if prop.string.base64_encoded:
190234
result[prop.string.base64_encoded] = base64.b64encode(value)

marketplace/deployer_util/expand_config_test.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def test_invalid_value_type(self):
4747
self.assertRaises(expand_config.InvalidProperty, lambda: expand_config.
4848
expand({'p1': 3}, schema))
4949

50-
def test_generate_properties_for_image_split_by_colon(self):
50+
def test_generate_properties_for_v1_image_split_by_colon(self):
5151
schema = config_helper.Schema.load_yaml("""
5252
applicationApiVersion: v1beta1
5353
properties:
@@ -69,7 +69,7 @@ def test_generate_properties_for_image_split_by_colon(self):
6969
'i1.after': 'bar',
7070
}, result)
7171

72-
def test_generate_properties_for_image_split_to_registry_repo_tag(self):
72+
def test_generate_properties_for_v1_image_split_to_registry_repo_tag(self):
7373
schema = config_helper.Schema.load_yaml("""
7474
applicationApiVersion: v1beta1
7575
properties:
@@ -93,6 +93,58 @@ def test_generate_properties_for_image_split_to_registry_repo_tag(self):
9393
'i1.tag': 'baz',
9494
}, result)
9595

96+
def test_generate_properties_for_v2_images(self):
97+
schema = config_helper.Schema.load_yaml("""
98+
x-google-marketplace:
99+
schemaVersion: v2
100+
applicationApiVersion: v1beta1
101+
publishedVersion: '0.1.1'
102+
publishedVersionMetadata:
103+
releaseNote: Release note for 0.1.1
104+
images:
105+
"":
106+
properties:
107+
image.full: {type: FULL}
108+
image.registry: {type: REGISTRY}
109+
image.registry_repo: {type: REPO_WITH_REGISTRY}
110+
image.repo: {type: REPO_WITHOUT_REGISTRY}
111+
image.tag: {type: TAG}
112+
i1:
113+
properties:
114+
image.i1.full: {type: FULL}
115+
image.i1.registry: {type: REGISTRY}
116+
image.i1.registry_repo: {type: REPO_WITH_REGISTRY}
117+
image.i1.repo: {type: REPO_WITHOUT_REGISTRY}
118+
image.i1.tag: {type: TAG}
119+
i2:
120+
properties:
121+
image.i2.full: {type: FULL}
122+
image.i2.registry: {type: REGISTRY}
123+
image.i2.registry_repo: {type: REPO_WITH_REGISTRY}
124+
image.i2.repo: {type: REPO_WITHOUT_REGISTRY}
125+
image.i2.tag: {type: TAG}
126+
""")
127+
result = expand_config.expand({'__image_repo_prefix__': 'gcr.io/app'},
128+
schema)
129+
self.assertEqual(
130+
{
131+
'image.full': 'gcr.io/app:0.1.1',
132+
'image.tag': '0.1.1',
133+
'image.registry': 'gcr.io',
134+
'image.registry_repo': 'gcr.io/app',
135+
'image.repo': 'app',
136+
'image.i1.full': 'gcr.io/app/i1:0.1.1',
137+
'image.i1.tag': '0.1.1',
138+
'image.i1.registry': 'gcr.io',
139+
'image.i1.registry_repo': 'gcr.io/app/i1',
140+
'image.i1.repo': 'app/i1',
141+
'image.i2.full': 'gcr.io/app/i2:0.1.1',
142+
'image.i2.tag': '0.1.1',
143+
'image.i2.registry': 'gcr.io',
144+
'image.i2.registry_repo': 'gcr.io/app/i2',
145+
'image.i2.repo': 'app/i2',
146+
}, result)
147+
96148
def test_generate_properties_for_string_base64_encoded(self):
97149
schema = config_helper.Schema.load_yaml("""
98150
applicationApiVersion: v1beta1

marketplace/deployer_util/print_version_metadata.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17+
import collections
1718
import datetime
1819
import sys
1920
import yaml
@@ -35,6 +36,10 @@ def main():
3536
'--deployer_image',
3637
required=True,
3738
help='The full deployer image, required')
39+
parser.add_argument(
40+
'--deployer_image_digest',
41+
required=True,
42+
help='The digest of the deployer image, required')
3843
args = parser.parse_args()
3944

4045
schema = schema_values_common.load_schema(args)
@@ -44,20 +49,34 @@ def main():
4449
raise Exception('schema.yaml must be in v2 version')
4550

4651
x = schema.x_google_marketplace
47-
meta = {
48-
'releaseNote': x.published_version_meta.release_note,
49-
'releaseDate': _utcnow_timestamp(),
50-
'url': args.deployer_image,
51-
}
52+
meta = collections.OrderedDict([
53+
('releaseDate', _utcnow_timestamp()),
54+
('url', args.deployer_image),
55+
('digest', args.deployer_image_digest),
56+
('releaseNote', x.published_version_meta.release_note),
57+
])
5258
if x.published_version_meta.release_types:
5359
meta['releaseTypes'] = x.published_version_meta.release_types
5460
if x.published_version_meta.recommended is not None:
5561
meta['recommended'] = x.published_version_meta.recommended
5662

57-
sys.stdout.write(yaml.safe_dump(meta, default_flow_style=False, indent=2))
63+
sys.stdout.write(_ordered_dump(meta, default_flow_style=False, indent=2))
5864
sys.stdout.flush()
5965

6066

67+
def _ordered_dump(data, stream=None, dumper=yaml.Dumper, **kwds):
68+
69+
class OrderedDumper(dumper):
70+
pass
71+
72+
def _dict_representer(dumper, data):
73+
return dumper.represent_mapping(
74+
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, data.items())
75+
76+
OrderedDumper.add_representer(collections.OrderedDict, _dict_representer)
77+
return yaml.dump(data, stream, OrderedDumper, **kwds)
78+
79+
6180
def _utcnow_timestamp():
6281
# Instructed to output timezone, python would outputs +00:00 instead of Z.
6382
# So we add that manually here.

0 commit comments

Comments
 (0)