diff --git a/.gitignore b/.gitignore index 7de70d75..1082a70b 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,4 @@ ansible.cfg *.retry update_description_utils/swaggers +reset.sh diff --git a/docs/api/container-registry/registry.md b/docs/api/container-registry/registry.md index 477c0310..9cb36ecb 100644 --- a/docs/api/container-registry/registry.md +++ b/docs/api/container-registry/registry.md @@ -8,17 +8,20 @@ This is a module that supports creating, updating or destroying Registries ```yaml - name: Create Registry registry: - name: test_registry + name: testregistry location: de/fra garbage_collection_schedule: days: - Wednesday time: 04:17:00+00:00 + features: + vulnerability_scanning: + enabled: false register: registry_response - name: Update Registry registry: - registry: test_registry + registry: testregistry name: test_registry_update garbage_collection_schedule: days: @@ -28,7 +31,7 @@ This is a module that supports creating, updating or destroying Registries - name: Delete Registry registry: - registry: test_registry + registry: testregistry wait: true state: absent @@ -82,18 +85,22 @@ This is a module that supports creating, updating or destroying Registries ## Parameters that can trigger a resource replacement: * name * location + * features (changing features.vulnerability_scanning.enabled from true to false will trigger a resource replacement)   # state: **present** ```yaml - name: Create Registry registry: - name: test_registry + name: testregistry location: de/fra garbage_collection_schedule: days: - Wednesday time: 04:17:00+00:00 + features: + vulnerability_scanning: + enabled: false register: registry_response ``` @@ -118,6 +125,12 @@ This is a module that supports creating, updating or destroying Registries location
str True The location of your registry + + + features
dict + False + Optional registry features. Format: 'vulnerability_scanning' key having a dict for value containing the 'enabled' key with a boolean value + Note: Vulnerability scanning for images is enabled by default. This is a paid add-on, please make sure you specify if you do not want it enabled name
str @@ -127,7 +140,7 @@ This is a module that supports creating, updating or destroying Registries allow_replace
bool False - Boolean indincating if the resource should be recreated when the state cannot be reached in another way. This may be used to prevent resources from being deleted from specifying a different value to an immutable property. An error will be thrown instead
Default: False + Boolean indicating if the resource should be recreated when the state cannot be reached in another way. This may be used to prevent resources from being deleted from specifying a different value to an immutable property. An error will be thrown instead
Default: False api_url
str @@ -174,7 +187,7 @@ This is a module that supports creating, updating or destroying Registries ```yaml - name: Delete Registry registry: - registry: test_registry + registry: testregistry wait: true state: absent @@ -241,7 +254,7 @@ This is a module that supports creating, updating or destroying Registries ```yaml - name: Update Registry registry: - registry: test_registry + registry: testregistry name: test_registry_update garbage_collection_schedule: days: @@ -271,6 +284,12 @@ This is a module that supports creating, updating or destroying Registries location
str False The location of your registry + + + features
dict + False + Optional registry features. Format: 'vulnerability_scanning' key having a dict for value containing the 'enabled' key with a boolean value + Note: Vulnerability scanning for images is enabled by default. This is a paid add-on, please make sure you specify if you do not want it enabled name
str @@ -285,7 +304,7 @@ This is a module that supports creating, updating or destroying Registries allow_replace
bool False - Boolean indincating if the resource should be recreated when the state cannot be reached in another way. This may be used to prevent resources from being deleted from specifying a different value to an immutable property. An error will be thrown instead
Default: False + Boolean indicating if the resource should be recreated when the state cannot be reached in another way. This may be used to prevent resources from being deleted from specifying a different value to an immutable property. An error will be thrown instead
Default: False api_url
str diff --git a/docs/api/container-registry/registry_artifact_info.md b/docs/api/container-registry/registry_artifact_info.md new file mode 100644 index 00000000..3a5e631e --- /dev/null +++ b/docs/api/container-registry/registry_artifact_info.md @@ -0,0 +1,127 @@ +# registry_artifact_info + +This is a simple module that supports listing existing Artifacts + +## Example Syntax + + +```yaml + + - name: List Artifacts + registry_artifact_info: + registry: "RegistryName" + repository: "repositoryName" + register: artifacts_response + + + - name: Show Artifacts + debug: + var: artifacts_response.result + +``` + +  + +  +## Returned object +```json +{ + "href": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/artifacts", + "id": "artifacts", + "items": [ + { + "href": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/repositories/image-test/artifacts/", + "id": "", + "metadata": { + "created_by": null, + "created_by_user_id": null, + "created_date": null, + "last_modified_by": null, + "last_modified_by_user_id": null, + "last_modified_date": null, + "last_pulled_at": null, + "last_pushed_at": "", + "last_scanned_at": "", + "pull_count": 0, + "push_count": 1, + "resource_urn": null, + "vuln_fixable_count": 45, + "vuln_max_severity": "critical", + "vuln_total_count": 57, + "vuln_total_score": 389.39993 + }, + "properties": { + "digest": "", + "media_type": "application/vnd.docker.distribution.manifest.v2+json", + "repository_name": "image-test", + "tags": [ + "latest" + ] + }, + "type": "artifact" + } + ], + "limit": 100, + "links": { + "next": null, + "prev": null, + "var_self": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/artifacts?limit=100&offset=100&orderBy=-pullCount" + }, + "offset": 0, + "type": "collection" +} + +``` + +  + +  +### Available parameters: +  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameRequiredDescription
filters
dict
FalseFilter that can be used to list only objects which have a certain set of propeties. Filters should be a dict with a key containing keys and value pair in the following format:'properties.name': 'server_name'
registry
str
TrueThe ID or name of an existing Registry.
repository
str
FalseThe name of an existing Repository.
api_url
str
FalseThe Ionos API base URL.
username
str
FalseThe Ionos username. Overrides the IONOS_USERNAME environment variable.
password
str
FalseThe Ionos password. Overrides the IONOS_PASSWORD environment variable.
token
str
FalseThe Ionos token. Overrides the IONOS_TOKEN environment variable.
diff --git a/docs/api/container-registry/registry_repository.md b/docs/api/container-registry/registry_repository.md new file mode 100644 index 00000000..1855bcc3 --- /dev/null +++ b/docs/api/container-registry/registry_repository.md @@ -0,0 +1,93 @@ +# registry_repository + +This is a module that supports creating, updating or destroying Repositories + +## Example Syntax + + +```yaml +- name: Delete Repository + registry_repository: + registry: RegistryName + repository: testRepository + state: absent + +``` + + +  + +  + +# state: **absent** +```yaml + - name: Delete Repository + registry_repository: + registry: RegistryName + repository: testRepository + state: absent + +``` +### Available parameters for state **absent**: +  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameRequiredDescription
repository
str
TrueThe name of an existing repository.
registry
str
TrueThe ID or name of an existing Registry.
api_url
str
FalseThe Ionos API base URL.
username
str
FalseThe Ionos username. Overrides the IONOS_USERNAME environment variable.
password
str
FalseThe Ionos password. Overrides the IONOS_PASSWORD environment variable.
token
str
FalseThe Ionos token. Overrides the IONOS_TOKEN environment variable.
wait
bool
FalseWait for the resource to be created before returning.
Default: True
Options: [True, False]
wait_timeout
int
FalseHow long before wait gives up, in seconds.
Default: 600
state
str
FalseIndicate desired state of the resource.
Default: present
Options: ['absent']
+ +  + +  diff --git a/docs/api/container-registry/registry_repository_info.md b/docs/api/container-registry/registry_repository_info.md new file mode 100644 index 00000000..d48456dc --- /dev/null +++ b/docs/api/container-registry/registry_repository_info.md @@ -0,0 +1,113 @@ +# registry_repository_info + +This is a simple module that supports listing existing Repositories + +## Example Syntax + + +```yaml + + - name: List Repositories + registry_repository_info: + registry: "RegistryName" + register: repositories_response + + + - name: Show Repositories + debug: + var: repositories_response.result + +``` + +  + +  +## Returned object +```json +{ + "href": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/repositories", + "id": "repositories", + "items": [ + { + "href": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/repositories/image-test", + "id": "image-test", + "metadata": { + "artifact_count": 1, + "created_by": null, + "created_by_user_id": null, + "created_date": null, + "last_modified_by": null, + "last_modified_by_user_id": null, + "last_modified_date": null, + "last_pulled_at": null, + "last_pushed_at": "", + "last_severity": "critical", + "pull_count": 0, + "push_count": 1, + "resource_urn": null + }, + "properties": { + "name": "image-test" + }, + "type": "repository" + } + ], + "limit": 100, + "links": { + "next": null, + "prev": null, + "var_self": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/repositories?limit=100&offset=100&orderBy=-lastPush" + }, + "offset": 0, + "type": "collection" +} + +``` + +  + +  +### Available parameters: +  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameRequiredDescription
filters
dict
FalseFilter that can be used to list only objects which have a certain set of propeties. Filters should be a dict with a key containing keys and value pair in the following format:'properties.name': 'server_name'
registry
str
TrueThe ID or name of an existing Registry.
api_url
str
FalseThe Ionos API base URL.
username
str
FalseThe Ionos username. Overrides the IONOS_USERNAME environment variable.
password
str
FalseThe Ionos password. Overrides the IONOS_PASSWORD environment variable.
token
str
FalseThe Ionos token. Overrides the IONOS_TOKEN environment variable.
diff --git a/docs/api/container-registry/registry_vulnerability_info.md b/docs/api/container-registry/registry_vulnerability_info.md new file mode 100644 index 00000000..4158d59b --- /dev/null +++ b/docs/api/container-registry/registry_vulnerability_info.md @@ -0,0 +1,137 @@ +# registry_vulnerability_info + +This is a simple module that supports listing existing Vulnerabilities + +## Example Syntax + + +```yaml + + - name: List Vulnerabilities + registry_vulnerability_info: + registry: "RegistryName" + repository: "repositoryName" + arifact: "" + register: vulnerabilities_response + + + - name: Show Vulnerabilities + debug: + var: vulnerabilities_response.result + +``` + +  + +  +## Returned object +```json +{ + "href": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/repositories/image-test/artifacts/", + "id": "vulnerabilities", + "items": [ + { + "href": "/vulnerabilities/", + "id": "", + "metadata": { + "publishedAt": "", + "updatedAt": "" + }, + "properties": { + "affects": [ + { + "name": "libc-bin", + "type": "deb", + "version": "2.31-0ubuntu9.2" + }, + { + "name": "libc6", + "type": "deb", + "version": "2.31-0ubuntu9.2" + } + ], + "dataSource": { + "id": null, + "url": null + }, + "description": "", + "fixable": true, + "recommendations": "", + "references": [ + "" + ], + "score": 2.5, + "severity": "medium" + }, + "type": "vulnerability" + } + ], + "limit": 100, + "links": { + "next": null, + "prev": null, + "varSelf": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/repositories/image-test/artifacts/?limit=100&offset=100&orderBy=-score" + }, + "offset": 0, + "type": "collection" +} + +``` + +  + +  +### Available parameters: +  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameRequiredDescription
filters
dict
FalseFilter that can be used to list only objects which have a certain set of propeties. Filters should be a dict with a key containing keys and value pair in the following format:'properties.name': 'server_name'
registry
str
TrueThe ID or name of an existing Registry.
repository
str
TrueThe name of an existing Repository.
artifact
str
TrueThe digest of an existing Artifact.
api_url
str
FalseThe Ionos API base URL.
username
str
FalseThe Ionos username. Overrides the IONOS_USERNAME environment variable.
password
str
FalseThe Ionos password. Overrides the IONOS_PASSWORD environment variable.
token
str
FalseThe Ionos token. Overrides the IONOS_TOKEN environment variable.
diff --git a/docs/returned_object_examples/registry_artifact_info.json b/docs/returned_object_examples/registry_artifact_info.json new file mode 100644 index 00000000..bda6962f --- /dev/null +++ b/docs/returned_object_examples/registry_artifact_info.json @@ -0,0 +1,45 @@ +{ + "href": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/artifacts", + "id": "artifacts", + "items": [ + { + "href": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/repositories/image-test/artifacts/", + "id": "", + "metadata": { + "created_by": null, + "created_by_user_id": null, + "created_date": null, + "last_modified_by": null, + "last_modified_by_user_id": null, + "last_modified_date": null, + "last_pulled_at": null, + "last_pushed_at": "", + "last_scanned_at": "", + "pull_count": 0, + "push_count": 1, + "resource_urn": null, + "vuln_fixable_count": 45, + "vuln_max_severity": "critical", + "vuln_total_count": 57, + "vuln_total_score": 389.39993 + }, + "properties": { + "digest": "", + "media_type": "application/vnd.docker.distribution.manifest.v2+json", + "repository_name": "image-test", + "tags": [ + "latest" + ] + }, + "type": "artifact" + } + ], + "limit": 100, + "links": { + "next": null, + "prev": null, + "var_self": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/artifacts?limit=100&offset=100&orderBy=-pullCount" + }, + "offset": 0, + "type": "collection" +} diff --git a/docs/returned_object_examples/registry_repository_info.json b/docs/returned_object_examples/registry_repository_info.json new file mode 100644 index 00000000..de849e1d --- /dev/null +++ b/docs/returned_object_examples/registry_repository_info.json @@ -0,0 +1,37 @@ +{ + "href": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/repositories", + "id": "repositories", + "items": [ + { + "href": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/repositories/image-test", + "id": "image-test", + "metadata": { + "artifact_count": 1, + "created_by": null, + "created_by_user_id": null, + "created_date": null, + "last_modified_by": null, + "last_modified_by_user_id": null, + "last_modified_date": null, + "last_pulled_at": null, + "last_pushed_at": "", + "last_severity": "critical", + "pull_count": 0, + "push_count": 1, + "resource_urn": null + }, + "properties": { + "name": "image-test" + }, + "type": "repository" + } + ], + "limit": 100, + "links": { + "next": null, + "prev": null, + "var_self": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/repositories?limit=100&offset=100&orderBy=-lastPush" + }, + "offset": 0, + "type": "collection" +} diff --git a/docs/returned_object_examples/registry_vulnerability_info.json b/docs/returned_object_examples/registry_vulnerability_info.json new file mode 100644 index 00000000..da26188f --- /dev/null +++ b/docs/returned_object_examples/registry_vulnerability_info.json @@ -0,0 +1,49 @@ +{ + "href": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/repositories/image-test/artifacts/", + "id": "vulnerabilities", + "items": [ + { + "href": "/vulnerabilities/", + "id": "", + "metadata": { + "publishedAt": "", + "updatedAt": "" + }, + "properties": { + "affects": [ + { + "name": "libc-bin", + "type": "deb", + "version": "2.31-0ubuntu9.2" + }, + { + "name": "libc6", + "type": "deb", + "version": "2.31-0ubuntu9.2" + } + ], + "dataSource": { + "id": null, + "url": null + }, + "description": "", + "fixable": true, + "recommendations": "", + "references": [ + "" + ], + "score": 2.5, + "severity": "medium" + }, + "type": "vulnerability" + } + ], + "limit": 100, + "links": { + "next": null, + "prev": null, + "varSelf": "/registries/0d6fd999-9bf9-462c-a148-951198ebca8f/repositories/image-test/artifacts/?limit=100&offset=100&orderBy=-score" + }, + "offset": 0, + "type": "collection" +} diff --git a/docs/summary.md b/docs/summary.md index 5b34bef0..ca0ca394 100644 --- a/docs/summary.md +++ b/docs/summary.md @@ -58,9 +58,13 @@ * Modules * [Registry](api/container-registry/registry.md) * [Registry Token](api/container-registry/registry_token.md) + * [Repository](api/container-registry/registry_repository.md) * Info Modules * [Registries](api/container-registry/registry_info.md) * [Registry Tokens](api/container-registry/registry_token_info.md) + * [Artifacts](api/container-registry/registry_artifact_info.md) + * [Repositories](api/container-registry/registry_repository_info.md) + * [Vulnerabilities](api/container-registry/registry_vulnerability_info.md) * DBaaS Postgres * Modules * [Postgres Cluster](api/dbaas-postgres/postgres_cluster.md) diff --git a/docs_generator.py b/docs_generator.py index e9933614..1165289e 100644 --- a/docs_generator.py +++ b/docs_generator.py @@ -129,6 +129,10 @@ def available_in_state(option): 'registry_info', 'registry_token', 'registry_token_info', + 'registry_artifact_info', + 'registry_repository_info', + 'registry_repository', + 'registry_vulnerability_info', 'postgres_cluster', 'postgres_backup_info', 'postgres_cluster_info', diff --git a/plugins/modules/registry.py b/plugins/modules/registry.py index 4f3bcc0b..5cd94b33 100644 --- a/plugins/modules/registry.py +++ b/plugins/modules/registry.py @@ -43,6 +43,11 @@ 'required': ['present'], 'type': 'str', }, + 'features': { + 'description': ["Optional registry features. Format: 'vulnerability_scanning' key having a dict for value containing the 'enabled' key with a boolean value\n Note: Vulnerability scanning for images is enabled by default. This is a paid add-on, please make sure you specify if you do not want it enabled"], + 'available': ['present', 'update'], + 'type': 'dict', + }, 'name': { 'description': ['The name of your registry.'], 'available': ['present', 'update'], @@ -57,7 +62,7 @@ }, 'allow_replace': { 'description': [ - 'Boolean indincating if the resource should be recreated when the state cannot be reached in ' + 'Boolean indicating if the resource should be recreated when the state cannot be reached in ' 'another way. This may be used to prevent resources from being deleted from specifying a different ' 'value to an immutable property. An error will be thrown instead', ], @@ -122,6 +127,7 @@ IMMUTABLE_OPTIONS = [ { "name": "name", "note": "" }, { "name": "location", "note": "" }, + { "name": "features", "note": "changing features.vulnerability_scanning.enabled from true to false will trigger a resource replacement" }, ] def transform_for_documentation(val): @@ -153,17 +159,20 @@ def transform_for_documentation(val): EXAMPLE_PER_STATE = { 'present': '''- name: Create Registry registry: - name: test_registry + name: testregistry location: de/fra garbage_collection_schedule: days: - Wednesday time: 04:17:00+00:00 + features: + vulnerability_scanning: + enabled: false register: registry_response ''', 'update': '''- name: Update Registry registry: - registry: test_registry + registry: testregistry name: test_registry_update garbage_collection_schedule: days: @@ -173,7 +182,7 @@ def transform_for_documentation(val): ''', 'absent': '''- name: Delete Registry registry: - registry: test_registry + registry: testregistry wait: true state: absent ''', @@ -222,16 +231,21 @@ def get_resource_id(module, resource_list, identity, identity_paths=None): def _should_replace_object(module, existing_object): + features = module.params.get('features') return ( module.params.get('location') is not None and existing_object.properties.location != module.params.get('location') or module.params.get('name') is not None and existing_object.properties.name != module.params.get('name') + or features is not None + and existing_object.properties.features.vulnerability_scanning.enabled == True + and features.get('vulnerability_scanning', {}).get('enabled') == False ) def _should_update_object(module, existing_object): gc_schedule = module.params.get('garbage_collection_schedule') + features = module.params.get('features') return ( gc_schedule is not None and ( @@ -240,6 +254,9 @@ def _should_update_object(module, existing_object): or gc_schedule.get('time') is not None and existing_object.properties.garbage_collection_schedule.time != gc_schedule.get('time') ) + or features.get('enabled') is not None + and existing_object.properties.features.vulnerability_scanning.enabled == False + and features.get('vulnerability_scanning', {}).get('enabled') == True ) @@ -256,12 +273,20 @@ def _get_object_identifier(module): def _create_object(module, client, existing_object=None): + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) gc_schedule = module.params.get('garbage_collection_schedule') + features = module.params.get('features') + vulnerability_scanning_feature = None if gc_schedule: gc_schedule = ionoscloud_container_registry.WeeklySchedule( days=gc_schedule.get('days'), time=gc_schedule.get('time'), ) + if features: + vulnerability_scanning_feature = ionoscloud_container_registry.FeatureVulnerabilityScanning( + enabled=features.get('vulnerability_scanning').get('enabled'), + ) name = module.params.get('name') location = module.params.get('location') if existing_object is not None: @@ -275,29 +300,52 @@ def _create_object(module, client, existing_object=None): name=name, location=location, garbage_collection_schedule=gc_schedule, + features=ionoscloud_container_registry.RegistryFeatures( + vulnerability_scanning=vulnerability_scanning_feature, + ), ) registry = ionoscloud_container_registry.PostRegistryInput(properties=registry_properties) try: registry = registries_api.registries_post(registry) + + if wait: + client.wait_for( + fn_request=lambda: registries_api.registries_find_by_id(registry.id).metadata.state, + fn_check=lambda r: r == 'Running', + scaleup=10000, + timeout=wait_timeout, + ) + registry = registries_api.registries_find_by_id(registry.id) except ionoscloud_container_registry.ApiException as e: module.fail_json(msg="failed to create the new Registry: %s" % to_native(e)) return registry def _update_object(module, client, existing_object): + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) gc_schedule = module.params.get('garbage_collection_schedule') + features = module.params.get('features') + vulnerability_scanning_feature = None if gc_schedule: gc_schedule = ionoscloud_container_registry.WeeklySchedule( days=gc_schedule.get('days'), time=gc_schedule.get('time'), ) + if features: + vulnerability_scanning_feature = ionoscloud_container_registry.FeatureVulnerabilityScanning( + enabled=features.get('vulnerability_scanning').get('enabled'), + ) registries_api = ionoscloud_container_registry.RegistriesApi(client) registry_properties = ionoscloud_container_registry.PatchRegistryInput( garbage_collection_schedule=gc_schedule, + features=ionoscloud_container_registry.RegistryFeatures( + vulnerability_scanning=vulnerability_scanning_feature, + ), ) try: @@ -306,6 +354,15 @@ def _update_object(module, client, existing_object): patch_registry_input=registry_properties, ) + if wait: + client.wait_for( + fn_request=lambda: registries_api.registries_find_by_id(registry.id).metadata.state, + fn_check=lambda r: r == 'Running', + scaleup=10000, + timeout=wait_timeout, + ) + registry = registries_api.registries_find_by_id(existing_object.id) + return registry except ionoscloud_container_registry.ApiException as e: module.fail_json(msg="failed to update the Registry: %s" % to_native(e)) diff --git a/plugins/modules/registry_artifact_info.py b/plugins/modules/registry_artifact_info.py new file mode 100644 index 00000000..7a074a47 --- /dev/null +++ b/plugins/modules/registry_artifact_info.py @@ -0,0 +1,315 @@ +import copy +import yaml + +from ansible import __version__ +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible.module_utils._text import to_native + +HAS_SDK = True +try: + import ionoscloud_container_registry +except ImportError: + HAS_SDK = False + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community', +} +CONTAINER_REGISTRY_USER_AGENT = 'ansible-module/%s_ionos-cloud-sdk-python-container-registry/%s' % ( +__version__, ionoscloud_container_registry.__version__) +DOC_DIRECTORY = 'container-registry' +STATES = ['info'] +OBJECT_NAME = 'Artifacts' +RETURNED_KEY = 'artifacts' + +OPTIONS = { + 'filters': { + 'description': [ + 'Filter that can be used to list only objects which have a certain set of propeties. Filters ' + 'should be a dict with a key containing keys and value pair in the following format:' + "'properties.name': 'server_name'" + ], + 'available': STATES, + 'type': 'dict', + }, + 'registry': { + 'description': ['The ID or name of an existing Registry.'], + 'available': STATES, + 'required': STATES, + 'type': 'str', + }, + 'repository': { + 'description': ['The name of an existing Repository.'], + 'available': STATES, + 'type': 'str', + }, + 'api_url': { + 'description': ['The Ionos API base URL.'], + 'version_added': '2.4', + 'env_fallback': 'IONOS_API_URL', + 'available': STATES, + 'type': 'str', + }, + 'username': { + # Required if no token, checked manually + 'description': ['The Ionos username. Overrides the IONOS_USERNAME environment variable.'], + 'aliases': ['subscription_user'], + 'env_fallback': 'IONOS_USERNAME', + 'available': STATES, + 'type': 'str', + }, + 'password': { + # Required if no token, checked manually + 'description': ['The Ionos password. Overrides the IONOS_PASSWORD environment variable.'], + 'aliases': ['subscription_password'], + 'available': STATES, + 'no_log': True, + 'env_fallback': 'IONOS_PASSWORD', + 'type': 'str', + }, + 'token': { + # If provided, then username and password no longer required + 'description': ['The Ionos token. Overrides the IONOS_TOKEN environment variable.'], + 'available': STATES, + 'no_log': True, + 'env_fallback': 'IONOS_TOKEN', + 'type': 'str', + }, +} + + +def transform_for_documentation(val): + val['required'] = len(val.get('required', [])) == len(STATES) + del val['available'] + del val['type'] + return val + + +DOCUMENTATION = ''' +--- +module: registry_artifact_info +short_description: List Artifacts +description: + - This is a simple module that supports listing existing Artifacts +version_added: "2.0" +options: +''' + ' ' + yaml.dump( + yaml.safe_load(str({k: transform_for_documentation(v) for k, v in copy.deepcopy(OPTIONS).items()})), + default_flow_style=False).replace('\n', '\n ') + ''' +requirements: + - "python >= 2.6" + - "ionoscloud-container-registry >= 1.0.0" +author: + - "IONOS Cloud SDK Team " +''' + +EXAMPLES = ''' + - name: List Artifacts + registry_artifact_info: + registry: "RegistryName" + repository: "repositoryName" + register: artifacts_response + + + - name: Show Artifacts + debug: + var: artifacts_response.result +''' + +def _get_matched_resources(resource_list, identity, identity_paths=None): + """ + Fetch and return a resource based on an identity supplied for it, if none or more than one matches + are found an error is printed and None is returned. + """ + + if identity_paths is None: + identity_paths = [['id'], ['properties', 'name']] + + def check_identity_method(resource): + resource_identity = [] + + for identity_path in identity_paths: + current = resource + for el in identity_path: + current = getattr(current, el) + resource_identity.append(current) + + return identity in resource_identity + + return list(filter(check_identity_method, resource_list.items)) + + +def get_resource(module, resource_list, identity, identity_paths=None): + matched_resources = _get_matched_resources(resource_list, identity, identity_paths) + + if len(matched_resources) == 1: + return matched_resources[0] + elif len(matched_resources) > 1: + module.fail_json(msg="found more resources of type {} for '{}'".format(resource_list.id, identity)) + else: + return None + + +def get_resource_id(module, resource_list, identity, identity_paths=None): + resource = get_resource(module, resource_list, identity, identity_paths) + return resource.id if resource is not None else None + + +def get_method_from_filter(filter): + ''' + Returns the method which check a filter for one object. Such a method would work in the following way: + for filter = ('properties.name', 'server_name') the resulting method would be + def method(item): + return item.properties.name == 'server_name' + Parameters: + filter (touple): Key, value pair representing the filter. + Returns: + the wanted method + ''' + key, value = filter + def method(item): + current = item + for key_part in key.split('.'): + current = getattr(current, key_part) + return current == value + return method + + +def get_method_to_apply_filters_to_item(filter_list): + ''' + Returns the method which applies a list of filtering methods obtained using get_method_from_filter to + one object and returns true if all the filters return true + Parameters: + filter_list (list): List of filtering methods + Returns: + the wanted method + ''' + def f(item): + return all([f(item) for f in filter_list]) + return f + + +def apply_filters(module, item_list): + ''' + Creates a list of filtering methods from the filters module parameter, filters item_list to keep only the + items for which every filter matches using get_method_to_apply_filters_to_item to make that check and returns + those items + Parameters: + module: The current Ansible module + item_list (list): List of items to be filtered + Returns: + List of items which match the filters + ''' + filters = module.params.get('filters') + if not filters: + return item_list + filter_methods = list(map(get_method_from_filter, filters.items())) + + return filter(get_method_to_apply_filters_to_item(filter_methods), item_list) + + + +def get_module_arguments(): + arguments = {} + + for option_name, option in OPTIONS.items(): + arguments[option_name] = { + 'type': option['type'], + } + for key in ['choices', 'default', 'aliases', 'no_log', 'elements']: + if option.get(key) is not None: + arguments[option_name][key] = option.get(key) + + if option.get('env_fallback'): + arguments[option_name]['fallback'] = (env_fallback, [option['env_fallback']]) + + if len(option.get('required', [])) == len(STATES): + arguments[option_name]['required'] = True + + return arguments + + +def get_sdk_config(module, sdk): + username = module.params.get('username') + password = module.params.get('password') + token = module.params.get('token') + api_url = module.params.get('api_url') + + if token is not None: + # use the token instead of username & password + conf = { + 'token': token + } + else: + # use the username & password + conf = { + 'username': username, + 'password': password, + } + + if api_url is not None: + conf['host'] = api_url + conf['server_index'] = None + + return sdk.Configuration(**conf) + + +def check_required_arguments(module, object_name): + # manually checking if token or username & password provided + if ( + not module.params.get("token") + and not (module.params.get("username") and module.params.get("password")) + ): + module.fail_json( + msg='Token or username & password are required for {object_name}'.format( + object_name=object_name, + ), + ) + for option_name, option in OPTIONS.items(): + if 'info' in option.get('required', []) and not module.params.get(option_name): + module.fail_json( + msg='{option_name} parameter is required for retrieving {object_name}'.format( + option_name=option_name, + object_name=object_name, + ), + ) + + +def main(): + module = AnsibleModule(argument_spec=get_module_arguments(), supports_check_mode=True) + + if not HAS_SDK: + module.fail_json( + msg='ionoscloud_container_registry is required for this module, run `pip install ionoscloud_container_registry`') + + + client = ionoscloud_container_registry.ApiClient(get_sdk_config(module, ionoscloud_container_registry)) + client.user_agent = CONTAINER_REGISTRY_USER_AGENT + + check_required_arguments(module, OBJECT_NAME) + + try: + registry_id = get_resource_id( + module, + ionoscloud_container_registry.RegistriesApi(client).registries_get(), + module.params.get('registry'), + ) + if module.params.get('repository'): + artifacts = ionoscloud_container_registry.ArtifactsApi(client).registries_repositories_artifacts_get( + registry_id, + module.params.get('repository'), + ) + else: + artifacts = ionoscloud_container_registry.ArtifactsApi(client).registries_artifacts_get( + registry_id, + ) + results = list(map(lambda x: x.to_dict(), apply_filters(module, artifacts.items))) + module.exit_json(**{RETURNED_KEY:results}) + except Exception as e: + module.fail_json( + msg='failed to retrieve {object_name}: {error}'.format(object_name=OBJECT_NAME, error=to_native(e))) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/registry_repository.py b/plugins/modules/registry_repository.py new file mode 100644 index 00000000..d01cf4f5 --- /dev/null +++ b/plugins/modules/registry_repository.py @@ -0,0 +1,355 @@ +import copy +from distutils.command.config import config +from operator import mod +import yaml + +from ansible import __version__ +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible.module_utils._text import to_native +import re + +HAS_SDK = True +try: + import ionoscloud_container_registry +except ImportError: + HAS_SDK = False + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community', +} + +CONTAINER_REGISTRY_USER_AGENT = 'ansible-module/%s_ionos-cloud-sdk-python-container-registry/%s'% ( + __version__, ionoscloud_container_registry.__version__, +) +DOC_DIRECTORY = 'container-registry' +STATES = ['absent'] +OBJECT_NAME = 'Repository' +RETURNED_KEY = 'repository' + + +OPTIONS = { + 'repository': { + 'description': ['The name of an existing repository.'], + 'available': STATES, + 'required': STATES, + 'type': 'str', + }, + 'registry': { + 'description': ['The ID or name of an existing Registry.'], + 'available': STATES, + 'required': STATES, + 'type': 'str', + }, + 'allow_replace': { + 'description': [ + 'Boolean indincating if the resource should be recreated when the state cannot be reached in ' + 'another way. This may be used to prevent resources from being deleted from specifying a different ' + 'value to an immutable property. An error will be thrown instead', + ], + 'available': ['present', 'update'], + 'default': False, + 'type': 'bool', + }, + 'api_url': { + 'description': ['The Ionos API base URL.'], + 'version_added': '2.4', + 'env_fallback': 'IONOS_API_URL', + 'available': STATES, + 'type': 'str', + }, + 'username': { + # Required if no token, checked manually + 'description': ['The Ionos username. Overrides the IONOS_USERNAME environment variable.'], + 'aliases': ['subscription_user'], + 'env_fallback': 'IONOS_USERNAME', + 'available': STATES, + 'type': 'str', + }, + 'password': { + # Required if no token, checked manually + 'description': ['The Ionos password. Overrides the IONOS_PASSWORD environment variable.'], + 'aliases': ['subscription_password'], + 'available': STATES, + 'no_log': True, + 'env_fallback': 'IONOS_PASSWORD', + 'type': 'str', + }, + 'token': { + # If provided, then username and password no longer required + 'description': ['The Ionos token. Overrides the IONOS_TOKEN environment variable.'], + 'available': STATES, + 'no_log': True, + 'env_fallback': 'IONOS_TOKEN', + 'type': 'str', + }, + 'wait': { + 'description': ['Wait for the resource to be created before returning.'], + 'default': True, + 'available': STATES, + 'choices': [True, False], + 'type': 'bool', + }, + 'wait_timeout': { + 'description': ['How long before wait gives up, in seconds.'], + 'default': 600, + 'available': STATES, + 'type': 'int', + }, + 'state': { + 'description': ['Indicate desired state of the resource.'], + 'default': 'present', + 'choices': STATES, + 'available': STATES, + 'type': 'str', + }, +} + + +def transform_for_documentation(val): + val['required'] = len(val.get('required', [])) == len(STATES) + del val['available'] + del val['type'] + return val + + +DOCUMENTATION = ''' +--- +module: registry_repository +short_description: Allows operations with Repositories. +description: + - This is a module that supports creating, updating or destroying Repositories +version_added: "2.0" +options: +''' + ' ' + yaml.dump( + yaml.safe_load(str({k: transform_for_documentation(v) for k, v in copy.deepcopy(OPTIONS).items()})), + default_flow_style=False).replace('\n', '\n ') + ''' +requirements: + - "python >= 2.6" + - "ionoscloud >= 6.0.2" + - "ionoscloud-container-registry >= 1.0.1" +author: + - "IONOS Cloud SDK Team " +''' + +EXAMPLE_PER_STATE = { + 'absent': '''- name: Delete Repository + registry_repository: + registry: RegistryName + repository: testRepository + state: absent + ''', +} + +EXAMPLES = '\n'.join(EXAMPLE_PER_STATE.values()) + + +def _get_matched_resources(resource_list, identity, identity_paths=None): + """ + Fetch and return a resource based on an identity supplied for it, if none or more than one matches + are found an error is printed and None is returned. + """ + + if identity_paths is None: + identity_paths = [['id'], ['properties', 'name']] + + def check_identity_method(resource): + resource_identity = [] + + for identity_path in identity_paths: + current = resource + for el in identity_path: + current = getattr(current, el) + resource_identity.append(current) + + return identity in resource_identity + + return list(filter(check_identity_method, resource_list.items)) + + +def get_resource(module, resource_list, identity, identity_paths=None): + matched_resources = _get_matched_resources(resource_list, identity, identity_paths) + + if len(matched_resources) == 1: + return matched_resources[0] + elif len(matched_resources) > 1: + module.fail_json(msg="found more resources of type {} for '{}'".format(resource_list.id, identity)) + else: + return None + + +def get_resource_id(module, resource_list, identity, identity_paths=None): + resource = get_resource(module, resource_list, identity, identity_paths) + return resource.id if resource is not None else None + + +def _should_replace_object(module, existing_object): + return False + + +def _should_update_object(module, existing_object): + return False + + +def _get_object_list(module, client): + registry_id = get_resource_id( + module, + ionoscloud_container_registry.RegistriesApi(client).registries_get(), + module.params.get('registry'), + ) + return ionoscloud_container_registry.RepositoriesApi(client).registries_repositories_get(registry_id) + + +def _get_object_name(module): + return module.params.get('name') + + +def _get_object_identifier(module): + return module.params.get('repository') + + +def _create_object(module, client, existing_object=None): + pass + + +def _update_object(module, client, existing_object): + pass + + +def _remove_object(module, client, existing_object): + registry_id = get_resource_id( + module, + ionoscloud_container_registry.RegistriesApi(client).registries_get(), + module.params.get('registry'), + ) + repositories_api = ionoscloud_container_registry.RepositoriesApi(client) + + try: + repositories_api.registries_repositories_delete(registry_id, existing_object.id) + except ionoscloud_container_registry.ApiException as e: + module.fail_json(msg="failed to remove the Repository: %s" % to_native(e)) + + +def update_replace_object(module, client, existing_object): + pass + + +def create_object(module, client): + pass + + +def update_object(module, client): + pass + + +def remove_object(module, client): + existing_object = get_resource(module, _get_object_list(module, client), _get_object_identifier(module)) + + if existing_object is None: + module.exit_json(changed=False) + return + + _remove_object(module, client, existing_object) + + return { + 'action': 'delete', + 'changed': True, + 'id': existing_object.id, + } + + +def get_module_arguments(): + arguments = {} + + for option_name, option in OPTIONS.items(): + arguments[option_name] = { + 'type': option['type'], + } + for key in ['choices', 'default', 'aliases', 'no_log', 'elements']: + if option.get(key) is not None: + arguments[option_name][key] = option.get(key) + + if option.get('env_fallback'): + arguments[option_name]['fallback'] = (env_fallback, [option['env_fallback']]) + + if len(option.get('required', [])) == len(STATES): + arguments[option_name]['required'] = True + + return arguments + + +def get_sdk_config(module, sdk): + username = module.params.get('username') + password = module.params.get('password') + token = module.params.get('token') + api_url = module.params.get('api_url') + + if token is not None: + # use the token instead of username & password + conf = { + 'token': token + } + else: + # use the username & password + conf = { + 'username': username, + 'password': password, + } + + if api_url is not None: + conf['host'] = api_url + conf['server_index'] = None + + return sdk.Configuration(**conf) + + +def check_required_arguments(module, state, object_name): + # manually checking if token or username & password provided + if ( + not module.params.get("token") + and not (module.params.get("username") and module.params.get("password")) + ): + module.fail_json( + msg='Token or username & password are required for {object_name}'.format( + object_name=object_name, + ), + ) + for option_name, option in OPTIONS.items(): + if state in option.get('required', []) and not module.params.get(option_name): + module.fail_json( + msg='{option_name} parameter is required for {object_name} state {state}'.format( + option_name=option_name, + object_name=object_name, + state=state, + ), + ) + + +def main(): + module = AnsibleModule(argument_spec=get_module_arguments(), supports_check_mode=True) + + if not HAS_SDK: + module.fail_json(msg='ionoscloud_container_registry is required for this module, ' + 'run `pip install ionoscloud_container_registry`') + + + client = ionoscloud_container_registry.ApiClient(get_sdk_config(module, ionoscloud_container_registry)) + client.user_agent = CONTAINER_REGISTRY_USER_AGENT + + state = module.params.get('state') + + check_required_arguments(module, state, OBJECT_NAME) + + try: + if state == 'absent': + module.exit_json(**remove_object(module, client)) + except Exception as e: + module.fail_json( + msg='failed to set {object_name} state {state}: {error}'.format( + object_name=OBJECT_NAME, error=to_native(e), state=state, + )) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/registry_repository_info.py b/plugins/modules/registry_repository_info.py new file mode 100644 index 00000000..9eedc08b --- /dev/null +++ b/plugins/modules/registry_repository_info.py @@ -0,0 +1,301 @@ +import copy +import yaml + +from ansible import __version__ +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible.module_utils._text import to_native + +HAS_SDK = True +try: + import ionoscloud_container_registry +except ImportError: + HAS_SDK = False + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community', +} +CONTAINER_REGISTRY_USER_AGENT = 'ansible-module/%s_ionos-cloud-sdk-python-container-registry/%s' % ( +__version__, ionoscloud_container_registry.__version__) +DOC_DIRECTORY = 'container-registry' +STATES = ['info'] +OBJECT_NAME = 'Repositories' +RETURNED_KEY = 'repositories' + +OPTIONS = { + 'filters': { + 'description': [ + 'Filter that can be used to list only objects which have a certain set of propeties. Filters ' + 'should be a dict with a key containing keys and value pair in the following format:' + "'properties.name': 'server_name'" + ], + 'available': STATES, + 'type': 'dict', + }, + 'registry': { + 'description': ['The ID or name of an existing Registry.'], + 'available': STATES, + 'required': STATES, + 'type': 'str', + }, + 'api_url': { + 'description': ['The Ionos API base URL.'], + 'version_added': '2.4', + 'env_fallback': 'IONOS_API_URL', + 'available': STATES, + 'type': 'str', + }, + 'username': { + # Required if no token, checked manually + 'description': ['The Ionos username. Overrides the IONOS_USERNAME environment variable.'], + 'aliases': ['subscription_user'], + 'env_fallback': 'IONOS_USERNAME', + 'available': STATES, + 'type': 'str', + }, + 'password': { + # Required if no token, checked manually + 'description': ['The Ionos password. Overrides the IONOS_PASSWORD environment variable.'], + 'aliases': ['subscription_password'], + 'available': STATES, + 'no_log': True, + 'env_fallback': 'IONOS_PASSWORD', + 'type': 'str', + }, + 'token': { + # If provided, then username and password no longer required + 'description': ['The Ionos token. Overrides the IONOS_TOKEN environment variable.'], + 'available': STATES, + 'no_log': True, + 'env_fallback': 'IONOS_TOKEN', + 'type': 'str', + }, +} + + +def transform_for_documentation(val): + val['required'] = len(val.get('required', [])) == len(STATES) + del val['available'] + del val['type'] + return val + + +DOCUMENTATION = ''' +--- +module: registry_repository_info +short_description: List Repositories +description: + - This is a simple module that supports listing existing Repositories +version_added: "2.0" +options: +''' + ' ' + yaml.dump( + yaml.safe_load(str({k: transform_for_documentation(v) for k, v in copy.deepcopy(OPTIONS).items()})), + default_flow_style=False).replace('\n', '\n ') + ''' +requirements: + - "python >= 2.6" + - "ionoscloud-container-registry >= 1.0.0" +author: + - "IONOS Cloud SDK Team " +''' + +EXAMPLES = ''' + - name: List Repositories + registry_repository_info: + registry: "RegistryName" + register: repositories_response + + + - name: Show Repositories + debug: + var: repositories_response.result +''' + +def _get_matched_resources(resource_list, identity, identity_paths=None): + """ + Fetch and return a resource based on an identity supplied for it, if none or more than one matches + are found an error is printed and None is returned. + """ + + if identity_paths is None: + identity_paths = [['id'], ['properties', 'name']] + + def check_identity_method(resource): + resource_identity = [] + + for identity_path in identity_paths: + current = resource + for el in identity_path: + current = getattr(current, el) + resource_identity.append(current) + + return identity in resource_identity + + return list(filter(check_identity_method, resource_list.items)) + + +def get_resource(module, resource_list, identity, identity_paths=None): + matched_resources = _get_matched_resources(resource_list, identity, identity_paths) + + if len(matched_resources) == 1: + return matched_resources[0] + elif len(matched_resources) > 1: + module.fail_json(msg="found more resources of type {} for '{}'".format(resource_list.id, identity)) + else: + return None + + +def get_resource_id(module, resource_list, identity, identity_paths=None): + resource = get_resource(module, resource_list, identity, identity_paths) + return resource.id if resource is not None else None + + +def get_method_from_filter(filter): + ''' + Returns the method which check a filter for one object. Such a method would work in the following way: + for filter = ('properties.name', 'server_name') the resulting method would be + def method(item): + return item.properties.name == 'server_name' + Parameters: + filter (touple): Key, value pair representing the filter. + Returns: + the wanted method + ''' + key, value = filter + def method(item): + current = item + for key_part in key.split('.'): + current = getattr(current, key_part) + return current == value + return method + + +def get_method_to_apply_filters_to_item(filter_list): + ''' + Returns the method which applies a list of filtering methods obtained using get_method_from_filter to + one object and returns true if all the filters return true + Parameters: + filter_list (list): List of filtering methods + Returns: + the wanted method + ''' + def f(item): + return all([f(item) for f in filter_list]) + return f + + +def apply_filters(module, item_list): + ''' + Creates a list of filtering methods from the filters module parameter, filters item_list to keep only the + items for which every filter matches using get_method_to_apply_filters_to_item to make that check and returns + those items + Parameters: + module: The current Ansible module + item_list (list): List of items to be filtered + Returns: + List of items which match the filters + ''' + filters = module.params.get('filters') + if not filters: + return item_list + filter_methods = list(map(get_method_from_filter, filters.items())) + + return filter(get_method_to_apply_filters_to_item(filter_methods), item_list) + + + +def get_module_arguments(): + arguments = {} + + for option_name, option in OPTIONS.items(): + arguments[option_name] = { + 'type': option['type'], + } + for key in ['choices', 'default', 'aliases', 'no_log', 'elements']: + if option.get(key) is not None: + arguments[option_name][key] = option.get(key) + + if option.get('env_fallback'): + arguments[option_name]['fallback'] = (env_fallback, [option['env_fallback']]) + + if len(option.get('required', [])) == len(STATES): + arguments[option_name]['required'] = True + + return arguments + + +def get_sdk_config(module, sdk): + username = module.params.get('username') + password = module.params.get('password') + token = module.params.get('token') + api_url = module.params.get('api_url') + + if token is not None: + # use the token instead of username & password + conf = { + 'token': token + } + else: + # use the username & password + conf = { + 'username': username, + 'password': password, + } + + if api_url is not None: + conf['host'] = api_url + conf['server_index'] = None + + return sdk.Configuration(**conf) + + +def check_required_arguments(module, object_name): + # manually checking if token or username & password provided + if ( + not module.params.get("token") + and not (module.params.get("username") and module.params.get("password")) + ): + module.fail_json( + msg='Token or username & password are required for {object_name}'.format( + object_name=object_name, + ), + ) + for option_name, option in OPTIONS.items(): + if 'info' in option.get('required', []) and not module.params.get(option_name): + module.fail_json( + msg='{option_name} parameter is required for retrieving {object_name}'.format( + option_name=option_name, + object_name=object_name, + ), + ) + + +def main(): + module = AnsibleModule(argument_spec=get_module_arguments(), supports_check_mode=True) + + if not HAS_SDK: + module.fail_json( + msg='ionoscloud_container_registry is required for this module, run `pip install ionoscloud_container_registry`') + + + client = ionoscloud_container_registry.ApiClient(get_sdk_config(module, ionoscloud_container_registry)) + client.user_agent = CONTAINER_REGISTRY_USER_AGENT + + check_required_arguments(module, OBJECT_NAME) + + try: + registry_id = get_resource_id( + module, + ionoscloud_container_registry.RegistriesApi(client).registries_get(), + module.params.get('registry'), + ) + artifacts = ionoscloud_container_registry.RepositoriesApi(client).registries_repositories_get(registry_id) + results = list(map(lambda x: x.to_dict(), apply_filters(module, artifacts.items))) + module.exit_json(**{RETURNED_KEY:results}) + except Exception as e: + module.fail_json( + msg='failed to retrieve {object_name}: {error}'.format(object_name=OBJECT_NAME, error=to_native(e))) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/registry_vulnerability_info.py b/plugins/modules/registry_vulnerability_info.py new file mode 100644 index 00000000..d52fff4d --- /dev/null +++ b/plugins/modules/registry_vulnerability_info.py @@ -0,0 +1,320 @@ +import copy +import yaml + +from ansible import __version__ +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible.module_utils._text import to_native + +HAS_SDK = True +try: + import ionoscloud_container_registry +except ImportError: + HAS_SDK = False + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community', +} +CONTAINER_REGISTRY_USER_AGENT = 'ansible-module/%s_ionos-cloud-sdk-python-container-registry/%s' % ( +__version__, ionoscloud_container_registry.__version__) +DOC_DIRECTORY = 'container-registry' +STATES = ['info'] +OBJECT_NAME = 'Vulnerabilities' +RETURNED_KEY = 'vulnerabilities' + +OPTIONS = { + 'filters': { + 'description': [ + 'Filter that can be used to list only objects which have a certain set of propeties. Filters ' + 'should be a dict with a key containing keys and value pair in the following format:' + "'properties.name': 'server_name'" + ], + 'available': STATES, + 'type': 'dict', + }, + 'registry': { + 'description': ['The ID or name of an existing Registry.'], + 'available': STATES, + 'required': STATES, + 'type': 'str', + }, + 'repository': { + 'description': ['The name of an existing Repository.'], + 'available': STATES, + 'required': STATES, + 'type': 'str', + }, + 'artifact': { + 'description': ['The digest of an existing Artifact.'], + 'available': STATES, + 'required': STATES, + 'type': 'str', + }, + 'api_url': { + 'description': ['The Ionos API base URL.'], + 'version_added': '2.4', + 'env_fallback': 'IONOS_API_URL', + 'available': STATES, + 'type': 'str', + }, + 'username': { + # Required if no token, checked manually + 'description': ['The Ionos username. Overrides the IONOS_USERNAME environment variable.'], + 'aliases': ['subscription_user'], + 'env_fallback': 'IONOS_USERNAME', + 'available': STATES, + 'type': 'str', + }, + 'password': { + # Required if no token, checked manually + 'description': ['The Ionos password. Overrides the IONOS_PASSWORD environment variable.'], + 'aliases': ['subscription_password'], + 'available': STATES, + 'no_log': True, + 'env_fallback': 'IONOS_PASSWORD', + 'type': 'str', + }, + 'token': { + # If provided, then username and password no longer required + 'description': ['The Ionos token. Overrides the IONOS_TOKEN environment variable.'], + 'available': STATES, + 'no_log': True, + 'env_fallback': 'IONOS_TOKEN', + 'type': 'str', + }, +} + + +def transform_for_documentation(val): + val['required'] = len(val.get('required', [])) == len(STATES) + del val['available'] + del val['type'] + return val + + +DOCUMENTATION = ''' +--- +module: registry_vulnerability_info +short_description: List Vulnerabilities +description: + - This is a simple module that supports listing existing Vulnerabilities +version_added: "2.0" +options: +''' + ' ' + yaml.dump( + yaml.safe_load(str({k: transform_for_documentation(v) for k, v in copy.deepcopy(OPTIONS).items()})), + default_flow_style=False).replace('\n', '\n ') + ''' +requirements: + - "python >= 2.6" + - "ionoscloud-container-registry >= 1.0.0" +author: + - "IONOS Cloud SDK Team " +''' + +EXAMPLES = ''' + - name: List Vulnerabilities + registry_vulnerability_info: + registry: "RegistryName" + repository: "repositoryName" + arifact: "" + register: vulnerabilities_response + + + - name: Show Vulnerabilities + debug: + var: vulnerabilities_response.result +''' + +def _get_matched_resources(resource_list, identity, identity_paths=None): + """ + Fetch and return a resource based on an identity supplied for it, if none or more than one matches + are found an error is printed and None is returned. + """ + + if identity_paths is None: + identity_paths = [['id'], ['properties', 'name']] + + def check_identity_method(resource): + resource_identity = [] + + for identity_path in identity_paths: + current = resource + for el in identity_path: + current = getattr(current, el) + resource_identity.append(current) + + return identity in resource_identity + + return list(filter(check_identity_method, resource_list.items)) + + +def get_resource(module, resource_list, identity, identity_paths=None): + matched_resources = _get_matched_resources(resource_list, identity, identity_paths) + + if len(matched_resources) == 1: + return matched_resources[0] + elif len(matched_resources) > 1: + module.fail_json(msg="found more resources of type {} for '{}'".format(resource_list.id, identity)) + else: + return None + + +def get_resource_id(module, resource_list, identity, identity_paths=None): + resource = get_resource(module, resource_list, identity, identity_paths) + return resource.id if resource is not None else None + + +def get_method_from_filter(filter): + ''' + Returns the method which check a filter for one object. Such a method would work in the following way: + for filter = ('properties.name', 'server_name') the resulting method would be + def method(item): + return item.properties.name == 'server_name' + Parameters: + filter (touple): Key, value pair representing the filter. + Returns: + the wanted method + ''' + key, value = filter + def method(item): + current = item + for key_part in key.split('.'): + current = getattr(current, key_part) + return current == value + return method + + +def get_method_to_apply_filters_to_item(filter_list): + ''' + Returns the method which applies a list of filtering methods obtained using get_method_from_filter to + one object and returns true if all the filters return true + Parameters: + filter_list (list): List of filtering methods + Returns: + the wanted method + ''' + def f(item): + return all([f(item) for f in filter_list]) + return f + + +def apply_filters(module, item_list): + ''' + Creates a list of filtering methods from the filters module parameter, filters item_list to keep only the + items for which every filter matches using get_method_to_apply_filters_to_item to make that check and returns + those items + Parameters: + module: The current Ansible module + item_list (list): List of items to be filtered + Returns: + List of items which match the filters + ''' + filters = module.params.get('filters') + if not filters: + return item_list + filter_methods = list(map(get_method_from_filter, filters.items())) + + return filter(get_method_to_apply_filters_to_item(filter_methods), item_list) + + + +def get_module_arguments(): + arguments = {} + + for option_name, option in OPTIONS.items(): + arguments[option_name] = { + 'type': option['type'], + } + for key in ['choices', 'default', 'aliases', 'no_log', 'elements']: + if option.get(key) is not None: + arguments[option_name][key] = option.get(key) + + if option.get('env_fallback'): + arguments[option_name]['fallback'] = (env_fallback, [option['env_fallback']]) + + if len(option.get('required', [])) == len(STATES): + arguments[option_name]['required'] = True + + return arguments + + +def get_sdk_config(module, sdk): + username = module.params.get('username') + password = module.params.get('password') + token = module.params.get('token') + api_url = module.params.get('api_url') + + if token is not None: + # use the token instead of username & password + conf = { + 'token': token + } + else: + # use the username & password + conf = { + 'username': username, + 'password': password, + } + + if api_url is not None: + conf['host'] = api_url + conf['server_index'] = None + + return sdk.Configuration(**conf) + + +def check_required_arguments(module, object_name): + # manually checking if token or username & password provided + if ( + not module.params.get("token") + and not (module.params.get("username") and module.params.get("password")) + ): + module.fail_json( + msg='Token or username & password are required for {object_name}'.format( + object_name=object_name, + ), + ) + for option_name, option in OPTIONS.items(): + if 'info' in option.get('required', []) and not module.params.get(option_name): + module.fail_json( + msg='{option_name} parameter is required for retrieving {object_name}'.format( + option_name=option_name, + object_name=object_name, + ), + ) + + +def main(): + module = AnsibleModule(argument_spec=get_module_arguments(), supports_check_mode=True) + + if not HAS_SDK: + module.fail_json( + msg='ionoscloud_container_registry is required for this module, run `pip install ionoscloud_container_registry`') + + + client = ionoscloud_container_registry.ApiClient(get_sdk_config(module, ionoscloud_container_registry)) + client.user_agent = CONTAINER_REGISTRY_USER_AGENT + + check_required_arguments(module, OBJECT_NAME) + + try: + registry_id = get_resource_id( + module, + ionoscloud_container_registry.RegistriesApi(client).registries_get(), + module.params.get('registry'), + ) + artifacts_api = ionoscloud_container_registry.ArtifactsApi(client) + vulnerabilities = artifacts_api.registries_repositories_artifacts_vulnerabilities_get( + registry_id, + module.params.get('repository'), + module.params.get('artifact'), + ) + results = list(map(lambda x: x.to_dict(), apply_filters(module, vulnerabilities.items))) + module.exit_json(**{RETURNED_KEY:results}) + except Exception as e: + module.fail_json( + msg='failed to retrieve {object_name}: {error}'.format(object_name=OBJECT_NAME, error=to_native(e))) + + +if __name__ == '__main__': + main() diff --git a/tests/container-registry/registry-test.yml b/tests/container-registry/registry-test.yml index 867341b4..005052ef 100644 --- a/tests/container-registry/registry-test.yml +++ b/tests/container-registry/registry-test.yml @@ -15,6 +15,10 @@ days: - Wednesday time: 04:17:00+00:00 + features: + vulnerability_scanning: + enabled: false + wait: true register: registry_response - name: List Registries @@ -23,16 +27,43 @@ - name: Show Registries debug: - var: registries_response.result + var: registries_response + + - name: List Repositories + registry_repository_info: + registry: "{{ registry_response.registry.properties.name }}" + register: repositories_response + + - name: Show Repositories + debug: + var: repositories_response + + - name: Ensure Repository does not exist + registry_repository: + registry: "{{ registry_response.registry.properties.name }}" + repository: repo-name + state: absent + + - name: List artifacts + registry_artifact_info: + registry: "{{ registry_response.registry.properties.name }}" + register: artifacts_response + + - name: Show artifacts + debug: + var: artifacts_response - name: Update Registry registry: registry: "{{ registry_response.registry.properties.name }}" garbage_collection_schedule: days: - - Wednesday - - Sunday + - Wednesday + - Sunday time: 06:17:00+00:00 + features: + vulnerability_scanning: + enabled: true allow_replace: False state: update register: updated_registry_response @@ -42,8 +73,11 @@ registry: "{{ registry_response.registry.properties.name }}" garbage_collection_schedule: days: - - Wednesday - - Sunday + - Wednesday + - Sunday + features: + vulnerability_scanning: + enabled: true allow_replace: False state: update register: updated_registry_response @@ -54,8 +88,28 @@ - updated_registry_response.changed == false msg: "Changed should be false" - - name: Delete Registry + - name: Replace Registry registry: registry: "{{ registry_response.registry.properties.name }}" + name: "{{ registry_response.registry.properties.name }}2" + garbage_collection_schedule: + days: + - Wednesday + - Sunday + time: 06:17:00+00:00 + features: + vulnerability_scanning: + enabled: false + allow_replace: true + state: update + register: updated_registry_response + + - name: Show Registry + debug: + var: updated_registry_response + + - name: Delete Registry + registry: + registry: "{{ updated_registry_response.registry.properties.name }}" wait: true state: absent diff --git a/tests/container-registry/registry-token-test.yml b/tests/container-registry/registry-token-test.yml index 5b826bb2..8a9ed704 100644 --- a/tests/container-registry/registry-token-test.yml +++ b/tests/container-registry/registry-token-test.yml @@ -40,7 +40,7 @@ - pull - push name: nume - type: repo + type: repository status: enabled register: registry_token_response