def test_get_pullspecs_some_locations(self, rel_images, rel_envs, containers, annotations, init_rel_envs, init_containers): data = ORIGINAL.data expected = {p.value for p in PULLSPECS.values()} if not rel_images: expected -= {RI1.value, RI2.value} del data["spec"]["relatedImages"] deployments = chain_get(data, ["spec", "install", "spec", "deployments"]) if not rel_envs: expected -= {CE1.value} for d in deployments: for c in chain_get(d, ["spec", "template", "spec", "containers"]): c.pop("env", None) if not containers: expected -= {C1.value, C2.value, C3.value} for d in deployments: del d["spec"]["template"]["spec"]["containers"] if not annotations: expected -= {AN1.value, AN2.value, AN3.value, AN4.value, AN5.value, AN6.value, AN7.value} delete_all_annotations(data) if not init_rel_envs: expected -= {ICE1.value} for d in deployments: for c in chain_get(d, ["spec", "template", "spec", "initContainers"], default=[]): c.pop("env", None) if not init_containers: expected -= {IC1.value} for d in deployments: d["spec"]["template"]["spec"].pop("initContainers", None) self._mock_check_csv() csv = OperatorCSV("x.yaml", data) assert csv.get_pullspecs() == expected
def test_get_pullspecs(self, caplog): csv = OperatorCSV("original.yaml", ORIGINAL.data) pullspecs = csv.get_pullspecs() assert pullspecs == self._original_pullspecs expected_logs = [ "original.yaml - Found pullspec for relatedImage ri1: {ri1}", "original.yaml - Found pullspec for relatedImage ri2: {ri2}", "original.yaml - Found pullspec for RELATED_IMAGE_CE1 var: {ce1}", "original.yaml - Found pullspec for RELATED_IMAGE_ICE1 var: {ice1}", "original.yaml - Found pullspec for container c1: {c1}", "original.yaml - Found pullspec for container c2: {c2}", "original.yaml - Found pullspec for container c3: {c3}", "original.yaml - Found pullspec for initContainer ic1: {ic1}", "original.yaml - Found pullspec for {an1.key} annotation: {an1}", "original.yaml - Found pullspec for {an2.key} annotation: {an2}", "original.yaml - Found pullspec for {an2.key} annotation: {an2}", "original.yaml - Found pullspec for {an3.key} annotation: {an3}", "original.yaml - Found pullspec for {an4.key} annotation: {an4}", "original.yaml - Found pullspec for {an5.key} annotation: {an5}", "original.yaml - Found pullspec for {an6.key} annotation: {an6}", "original.yaml - Found pullspec for {an7.key} annotation: {an7}", ] for log in expected_logs: assert log.format(**PULLSPECS) in caplog.text
def test_ignored_annotations(self): data = { 'kind': 'ClusterServiceVersion', 'metadata': { 'annotations': { 'some_text': 'abcdef', 'some_number': 123, 'some_array': [], 'some_object': {}, 'metadata': { 'annotations': { 'pullspec': 'metadata.annotations/nested.in:metadata.annotations' } } }, 'not_an_annotation': 'something.that/looks-like:a-pullspec', 'not_annotations': { 'also_not_an_annotation': 'other.pullspec/lookalike:thing' }, 'metadata': { 'annotations': { 'pullspec': 'metadata.annotations/nested.in:metadata' } } } } self._mock_check_csv() csv = OperatorCSV("original.yaml", data) assert csv.get_pullspecs() == set()
def test_known_vs_other_annotations(self): # All annotation must be found and replaced exactly once, heuristic # must not look in keys that are known pullspec sources data = { 'kind': 'ClusterServiceVersion', 'metadata': { 'annotations': { 'containerImage': 'a.b/c:1', 'notContainerImage': 'a.b/c:1' } }, 'spec': { 'metadata': { 'annotations': { 'containerImage': 'a.b/c:1', 'notContainerImage': 'a.b/c:1' } } } } replacements = { ImageName.parse(old): ImageName.parse(new) for old, new in [ ('a.b/c:1', 'd.e/f:1'), ('d.e/f:1', 'g.h/i:1'), ] } self._mock_check_csv() csv = OperatorCSV("original.yaml", data) csv.replace_pullspecs(replacements) assert csv.data["metadata"]["annotations"]["containerImage"] == 'd.e/f:1' assert csv.data["metadata"]["annotations"]["notContainerImage"] == 'd.e/f:1' assert csv.data["spec"]["metadata"]["annotations"]["containerImage"] == 'd.e/f:1' assert csv.data["spec"]["metadata"]["annotations"]["notContainerImage"] == 'd.e/f:1'
def test_has_related_image_envs(self, var, does_have): data = { 'kind': 'ClusterServiceVersion', 'spec': { 'install': { 'spec': { 'deployments': [ { 'spec': { 'template': { 'spec': { 'containers': [ {'name': 'spam', 'image': 'eggs', 'env': []} ] } } } } ] } } } } if var is not None: deployment = data['spec']['install']['spec']['deployments'][0] deployment['spec']['template']['spec']['containers'][0]['env'].append(var) self._mock_check_csv() csv = OperatorCSV('original.yaml', data) assert csv.has_related_image_envs() == does_have
def test_replace_pullspecs_everywhere(self, caplog): csv = OperatorCSV("original.yaml", ORIGINAL.data) csv.replace_pullspecs_everywhere(self._replacement_pullspecs) assert csv.data == REPLACED_EVERYWHERE.data expected_logs = { "{file} - Replaced pullspec: {ri1} -> {ri1.replace}": 3, "{file} - Replaced pullspec: {ri2} -> {ri2.replace}": 3, "{file} - Replaced pullspec: {ce1} -> {ce1.replace}": 4, "{file} - Replaced pullspec: {c1} -> {c1.replace}": 3, "{file} - Replaced pullspec: {c2} -> {c2.replace}": 3, "{file} - Replaced pullspec: {c3} -> {c3.replace}": 3, "{file} - Replaced pullspec: {an1} -> {an1.replace}": 2, "{file} - Replaced pullspec: {ic1} -> {ic1.replace}": 3, "{file} - Replaced pullspec: {ice1} -> {ice1.replace}": 3, "{file} - Replaced pullspec for {an1.key} annotation: {an1} -> {an1.replace}": 1, "{file} - Replaced pullspec for {an2.key} annotation: {an2} -> {an2.replace}": 1, "{file} - Replaced pullspec for {an3.key} annotation: {an3} -> {an3.replace}": 1, "{file} - Replaced pullspec for {an4.key} annotation: {an4} -> {an4.replace}": 1, "{file} - Replaced pullspec for {an5.key} annotation: {an5} -> {an5.replace}": 1, "{file} - Replaced pullspec for {an6.key} annotation: {an6} -> {an6.replace}": 2, "{file} - Replaced pullspec for {an7.key} annotation: {an7} -> {an7.replace}": 2, } # NOTE: an1 gets replaced once as an annotation and twice in other places # an6 and an7 both get replaced twice in the same annotation for log, count in expected_logs.items(): assert caplog.text.count(log.format(file="original.yaml", **PULLSPECS)) == count
def test_get_pullspecs_some_locations(self, rel_images, rel_envs, containers, annotations): data = ORIGINAL.data expected = {p.value for p in PULLSPECS.values()} if not rel_images: expected -= {FOO.value, BAR.value} del data["spec"]["relatedImages"] deployments = chain_get(data, ["spec", "install", "spec", "deployments"]) if not rel_envs: expected -= {EGGS.value} for d in deployments: for c in chain_get(d, ["spec", "template", "spec", "containers"]): c.pop("env", None) if not containers: expected -= {SPAM.value, HAM.value, JAM.value} for d in deployments: del d["spec"]["template"]["spec"]["containers"] if not annotations: expected -= {BAZ.value} del data["metadata"]["annotations"] csv = OperatorCSV("x.yaml", data) assert csv.get_pullspecs() == expected
def test_has_related_images(self, pullspecs, does_have): data = { 'kind': 'ClusterServiceVersion', 'spec': {} } if pullspecs is not None: data['spec']['relatedImages'] = pullspecs csv = OperatorCSV('original.yaml', data) assert csv.has_related_images() == does_have
def test_dump(self, tmpdir): path = tmpdir.join("original.yaml") csv = OperatorCSV(str(path), ORIGINAL.data) csv.dump() content = path.read() # Formatting does not necessarily have to match, at least check the data... assert yaml.load(content) == csv.data # ...and that the comment was preserved assert content.startswith('# A meaningful comment')
def test_valuefrom_references_not_allowed(self): data = ORIGINAL.data env_path = CE1.path[:-1] env = chain_get(data, env_path) env["valueFrom"] = "somewhere" csv = OperatorCSV("original.yaml", data) with pytest.raises(RuntimeError) as exc_info: csv.get_pullspecs() assert '"valueFrom" references are not supported' in str(exc_info.value)
def test_set_related_images(self, caplog): data = ORIGINAL.data csv = OperatorCSV("original.yaml", data) csv.set_related_images() # the order is: # 1. existing relatedImages # 2. known annotations # 3. containers # 4. initContainers # 5. container env vars # 6. initContainer env vars # 7. other annotations (in reverse order - quirky, I know) expected_related_images = [ CommentedMap([("name", name), ("image", pullspec.value.to_str())]) for name, pullspec in [ ("ri1", RI1), ("ri2", RI2), ("baz-latest-annotation", AN1), ("c1", C1), ("c2", C2), ("c3", C3), ("ic1", IC1), ("ce1", CE1), ("ice1", ICE1), ("an7-1-annotation", AN7), ("an6-1-annotation", AN6), ("an5-1-annotation", AN5), ("an4-1-annotation", AN4), ("an3-1-annotation", AN3), ("an2-1-annotation", AN2), ] ] assert csv.data["spec"]["relatedImages"] == expected_related_images expected_logs = [ "{path} - Set relatedImage ri1 (from relatedImage ri1): {ri1}", "{path} - Set relatedImage ri2 (from relatedImage ri2): {ri2}", "{path} - Set relatedImage baz-latest-annotation (from {an1.key} annotation): {an1}", "{path} - Set relatedImage c1 (from container c1): {c1}", "{path} - Set relatedImage c2 (from container c2): {c2}", "{path} - Set relatedImage c3 (from container c3): {c3}", "{path} - Set relatedImage ic1 (from initContainer ic1): {ic1}", "{path} - Set relatedImage ce1 (from RELATED_IMAGE_CE1 var): {ce1}", "{path} - Set relatedImage ice1 (from RELATED_IMAGE_ICE1 var): {ice1}", "{path} - Set relatedImage an2-1-annotation (from {an2.key} annotation): {an2}", "{path} - Set relatedImage an3-1-annotation (from {an3.key} annotation): {an3}", "{path} - Set relatedImage an4-1-annotation (from {an4.key} annotation): {an4}", "{path} - Set relatedImage an5-1-annotation (from {an5.key} annotation): {an5}", "{path} - Set relatedImage an6-1-annotation (from {an6.key} annotation): {an6}", "{path} - Set relatedImage an7-1-annotation (from {an7.key} annotation): {an7}", ] for log in expected_logs: assert log.format(path="original.yaml", **PULLSPECS) in caplog.text
def test_wrong_kind(self): data = ORIGINAL.data del data["kind"] with pytest.raises(NotOperatorCSV) as exc_info: OperatorCSV("original.yaml", data) assert str(exc_info.value) == "Not a ClusterServiceVersion" data["kind"] = "ClusterResourceDefinition" with pytest.raises(NotOperatorCSV) as exc_info: OperatorCSV("original.yaml", data) assert str(exc_info.value) == "Not a ClusterServiceVersion"
def test_related_annotation_names(self, pullspec, name): data = { 'kind': 'ClusterServiceVersion', 'metadata': { 'annotations': { 'foo': pullspec } } } self._mock_check_csv() csv = OperatorCSV("original.yaml", data) csv.set_related_images() generated_name = csv.data["spec"]["relatedImages"][0]["name"] assert generated_name == name
def test_related_images_no_pullspecs(self, caplog): """If no pullspecs are detected, skip creation of relatedImages""" data = { 'kind': 'ClusterServiceVersion', 'metadata': { 'annotations': { 'foo': 'test' } } } self._mock_check_csv() csv = OperatorCSV("original.yaml", data) csv.set_related_images() assert "" in caplog.text assert 'relatedImages' not in csv.data.get("spec", {})
def test_from_file(self, tmpdir): path = tmpdir.join("original.yaml") path.write(ORIGINAL.content) csv = OperatorCSV.from_file(str(path)) assert csv.path == str(path) assert csv.data == ORIGINAL.data
def test_replace_pullspecs(self, caplog): csv = OperatorCSV("original.yaml", ORIGINAL.data) csv.replace_pullspecs(self._replacement_pullspecs) assert csv.data == REPLACED.data expected_logs = [ "{file} - Replaced pullspec for related image foo: {foo} -> {foo.replace}", "{file} - Replaced pullspec for related image bar: {bar} -> {bar.replace}", "{file} - Replaced pullspec in RELATED_IMAGE_EGGS var: {eggs} -> {eggs.replace}", "{file} - Replaced pullspec for container spam: {spam} -> {spam.replace}", "{file} - Replaced pullspec for container ham: {ham} -> {ham.replace}", "{file} - Replaced pullspec for container jam: {jam} -> {jam.replace}", "{file} - Replaced pullspec in annotations: {baz} -> {baz.replace}", ] for log in expected_logs: assert log.format(file="original.yaml", **PULLSPECS) in caplog.text
def test_replace_pullspecs_everywhere(self, caplog): csv = OperatorCSV("original.yaml", ORIGINAL.data) csv.replace_pullspecs_everywhere(self._replacement_pullspecs) assert csv.data == REPLACED_EVERYWHERE.data expected_logs = { "original.yaml - Replaced pullspec: {foo} -> {foo.replace}": 3, "original.yaml - Replaced pullspec: {bar} -> {bar.replace}": 3, "original.yaml - Replaced pullspec: {eggs} -> {eggs.replace}": 4, "original.yaml - Replaced pullspec: {spam} -> {spam.replace}": 3, "original.yaml - Replaced pullspec: {ham} -> {ham.replace}": 3, "original.yaml - Replaced pullspec: {jam} -> {jam.replace}": 3, "original.yaml - Replaced pullspec: {baz} -> {baz.replace}": 3, } for log, count in expected_logs.items(): assert caplog.text.count(log.format(**PULLSPECS)) == count
def test_get_pullspecs(self, caplog): csv = OperatorCSV("original.yaml", ORIGINAL.data) pullspecs = csv.get_pullspecs() assert pullspecs == self._original_pullspecs expected_logs = [ "original.yaml - Found pullspec for related image foo: {foo}", "original.yaml - Found pullspec for related image bar: {bar}", "original.yaml - Found pullspec in RELATED_IMAGE_EGGS var: {eggs}", "original.yaml - Found pullspec for container spam: {spam}", "original.yaml - Found pullspec for container ham: {ham}", "original.yaml - Found pullspec for container jam: {jam}", "original.yaml - Found pullspec in annotations: {baz}", ] for log in expected_logs: assert log.format(**PULLSPECS) in caplog.text
def test_tricky_annotation_replacements(self, pullspecs, replacements, expected): replacements = { ImageName.parse(old): ImageName.parse(new) for old, new in replacements.items() } data = { 'kind': 'ClusterServiceVersion', 'metadata': { 'annotations': { 'foo': ", ".join(pullspecs) } } } self._mock_check_csv() csv = OperatorCSV("original.yaml", data) csv.replace_pullspecs(replacements) assert csv.data['metadata']['annotations']['foo'] == ", ".join(expected)
def test_replace_only_some_pullspecs(self, caplog): replacement_pullspecs = self._replacement_pullspecs.copy() # ri1 won't be replaced because replacement is identical replacement_pullspecs[RI1.value] = RI1.value # ri2 won't be replaced because no replacement available del replacement_pullspecs[RI2.value] csv = OperatorCSV("original.yaml", ORIGINAL.data) csv.replace_pullspecs(replacement_pullspecs) assert RI1.find_in_data(csv.data) == RI1.value assert RI2.find_in_data(csv.data) == RI2.value ri1_log = "original.yaml - Replaced pullspec for relatedImage ri1: {ri1}" ri2_log = "original.yaml - Replaced pullspec for relatedImage ri2: {ri2}" assert ri1_log.format(ri1=RI1) not in caplog.text assert ri2_log.format(ri2=RI2) not in caplog.text
def test_replace_only_some_pullspecs(self, caplog): replacement_pullspecs = self._replacement_pullspecs.copy() # Foo won't be replaced because replacement is identical replacement_pullspecs[FOO.value] = FOO.value # Bar won't be replaced because no replacement available del replacement_pullspecs[BAR.value] csv = OperatorCSV("original.yaml", ORIGINAL.data) csv.replace_pullspecs(replacement_pullspecs) assert FOO.find_in_data(csv.data) == FOO.value assert BAR.find_in_data(csv.data) == BAR.value foo_log = "original.yaml - Replaced pullspec for related image foo: {foo}" bar_log = "original.yaml - Replaced pullspec for related image bar: {bar}" assert foo_log.format(foo=FOO) not in caplog.text assert bar_log.format(bar=BAR) not in caplog.text
def test_set_related_images(self, caplog): data = ORIGINAL.data csv = OperatorCSV("original.yaml", data) csv.set_related_images() # the order is: # 1. existing relatedImages # 2. annotations # 3. containers # 4. initContainers # 5. container env vars # 6. initContainer env vars expected_related_images = [ CommentedMap([("name", name), ("image", pullspec.value.to_str())]) for name, pullspec in [ ("foo", FOO), ("bar", BAR), ("baz-annotation", BAZ), ("spam", SPAM), ("ham", HAM), ("jam", JAM), ("p1", P1), ("eggs", EGGS), ("p2", P2), ] ] assert csv.data["spec"]["relatedImages"] == expected_related_images expected_logs = [ "{path} - Set relatedImage foo (from relatedImage foo): {foo}", "{path} - Set relatedImage bar (from relatedImage bar): {bar}", "{path} - Set relatedImage baz-annotation (from containerImage annotation): {baz}", "{path} - Set relatedImage spam (from container spam): {spam}", "{path} - Set relatedImage ham (from container ham): {ham}", "{path} - Set relatedImage jam (from container jam): {jam}", "{path} - Set relatedImage p1 (from initContainer p1): {p1}", "{path} - Set relatedImage eggs (from RELATED_IMAGE_EGGS var): {eggs}", "{path} - Set relatedImage p2 (from RELATED_IMAGE_P2 var): {p2}", ] for log in expected_logs: assert log.format(path="original.yaml", **PULLSPECS) in caplog.text
def test_set_related_images_conflicts(self, related_images, containers, err_msg): data = { "kind": "ClusterServiceVersion", "spec": { "relatedImages": related_images, "install": { "spec": { "deployments": [ { "spec": { "template": { "spec": { "containers": containers } } } } ] } } } } self._mock_check_csv() csv = OperatorCSV("original.yaml", data) if err_msg is not None: with pytest.raises(RuntimeError) as exc_info: csv.set_related_images() assert str(exc_info.value) == err_msg.format(path="original.yaml") else: csv.set_related_images() updated_counts = Counter(x['name'] for x in csv.data['spec']['relatedImages']) # check that there are no duplicates in .spec.relatedImages for name, count in updated_counts.items(): assert count == 1, 'Duplicate in relatedImages: {}'.format(name)
def test_replace_pullspecs(self, caplog): csv = OperatorCSV("original.yaml", ORIGINAL.data) csv.replace_pullspecs(self._replacement_pullspecs) assert csv.data == REPLACED.data expected_logs = [ "{file} - Replaced pullspec for relatedImage ri1: {ri1} -> {ri1.replace}", "{file} - Replaced pullspec for relatedImage ri2: {ri2} -> {ri2.replace}", "{file} - Replaced pullspec for RELATED_IMAGE_CE1 var: {ce1} -> {ce1.replace}", "{file} - Replaced pullspec for RELATED_IMAGE_ICE1 var: {ice1} -> {ice1.replace}", "{file} - Replaced pullspec for container c1: {c1} -> {c1.replace}", "{file} - Replaced pullspec for container c2: {c2} -> {c2.replace}", "{file} - Replaced pullspec for container c3: {c3} -> {c3.replace}", "{file} - Replaced pullspec for initContainer ic1: {ic1} -> {ic1.replace}", "{file} - Replaced pullspec for {an1.key} annotation: {an1} -> {an1.replace}", "{file} - Replaced pullspec for {an2.key} annotation: {an2} -> {an2.replace}", "{file} - Replaced pullspec for {an3.key} annotation: {an3} -> {an3.replace}", "{file} - Replaced pullspec for {an4.key} annotation: {an4} -> {an4.replace}", "{file} - Replaced pullspec for {an5.key} annotation: {an5} -> {an5.replace}", "{file} - Replaced pullspec for {an6.key} annotation: {an6} -> {an6.replace}", "{file} - Replaced pullspec for {an7.key} annotation: {an7} -> {an7.replace}", ] for log in expected_logs: assert log.format(file="original.yaml", **PULLSPECS) in caplog.text
def test_related_annotation_name_conflicts(self, p1, p2, should_fail): data = { 'kind': 'ClusterServiceVersion', 'metadata': { 'annotations': { 'foo': "{}, {}".format(p1, p2) } } } self._mock_check_csv() csv = OperatorCSV("original.yaml", data) if should_fail: with pytest.raises(RuntimeError) as exc_info: csv.set_related_images() msg = ("original.yaml - Found conflicts when setting relatedImages:\n" "foo annotation: {} X foo annotation: {}".format(p2, p1)) assert str(exc_info.value) == msg else: csv.set_related_images()
def test_yaml_not_object(self): data = YAML_LIST.data with pytest.raises(NotOperatorCSV) as exc_info: OperatorCSV("original.yaml", data) assert str(exc_info.value) == "File does not contain a YAML object"
def test_get_related_image_pullsepcs(self, csv_content, expected_pullspecs): self._mock_check_csv() csv = OperatorCSV('csv.yaml', csv_content) assert expected_pullspecs == csv.get_related_image_pullspecs()