From a80b953d044a916303bc3ba31864edc6096f3d7c Mon Sep 17 00:00:00 2001 From: David Laine Date: Wed, 22 May 2024 15:40:05 -0500 Subject: [PATCH 01/25] CASMCMS-8022 - update dependencies. --- CHANGELOG.md | 33 ++++++++ README.md | 76 ++++++++++++++++- constraints.txt | 61 +++++++------- requirements-test.txt | 1 + src/server/__init__.py | 3 +- src/server/models/__init__.py | 9 ++- src/server/models/images.py | 25 +++--- src/server/models/jobs.py | 103 +++++++++++------------- src/server/models/publickeys.py | 14 ++-- src/server/models/recipes.py | 47 +++++------ src/server/models/remote_build_nodes.py | 4 +- src/server/v3/models/images.py | 12 +-- src/server/v3/models/public_keys.py | 12 +-- src/server/v3/models/recipes.py | 12 +-- tests/utils.py | 2 + tests/v2/test_v2_images.py | 16 ++-- tests/v2/test_v2_jobs.py | 6 +- tests/v2/test_v2_public_keys.py | 6 +- tests/v2/test_v2_recipes.py | 14 ++-- tests/v3/test_v3_deleted_images.py | 6 +- tests/v3/test_v3_deleted_public_keys.py | 6 +- tests/v3/test_v3_deleted_recipes.py | 6 +- tests/v3/test_v3_images.py | 16 ++-- tests/v3/test_v3_jobs.py | 6 +- tests/v3/test_v3_public_keys.py | 6 +- tests/v3/test_v3_recipes.py | 14 ++-- 26 files changed, 309 insertions(+), 207 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d8772..cc3d1ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,39 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Dependencies +- CASMCMS-8022 - update python dependencies to most recent versions. +| Package | From | To | +|-----------------------|------------|----------| +| `aniso8601` | 3.0.2 | 9.0.1 | +| `boto3` | 1.12.49 | 1.34.114 | +| `botocore` | 1.15.49 | 1.34.114 | +| `cachetools` | 3.0.0 | 5.3.3 | +| `certifi` | 2019.11.28 | 2024.2.2 | +| `chardet` | 3.0.4 | 5.2.0 | +| `click` | 6.7 | 8.1.7 | +| `docutils` | 0.14 | 0.21.2 | +| `Flask` | 1.1.4 | 3.0.3 | +| `flask-marshmallow` | 0.9.0 | 1.2.1 | +| `google-auth` | 1.6.3 | 2.29.0 | +| `gunicorn` | 19.10.0 | 22.0.0 | +| `idna` | 2.8 | 3.7 | +| `itsdangerous` | 0.24 | 2.2.0 | +| `Jinja2` | 2.10.3 | 3.1.4 | +| `jmespath` | 0.9.5 | 1.0.1 | +| `MarkupSafe` | 1.1.1 | 2.1.5 | +| `marshmallow` | 3.0.0b16 | 3.21.2 | +| `oauthlib` | 2.1.0 | 3.2.2 | +| `pyasn1` | 0.4.8 | 0.6.0 | +| `pyasn1-modules` | 0.2.8 | 0.4.0 | +| `pytz` | 2018.4 | 2024.1 | +| `requests` | 2.23.0 | 2.31.0 | +| `requests-oauthlib` | 1.0.0 | 1.3.1 | +| `rsa` | 4.7.2 | 4.9 | +| `s3transfer` | 0.3.7 | 0.10.1 | +| `urllib3` | 1.25.11 | 1.26.18 | +| `websocket-client` | 0.54.0 | 1.8.0 | +| `Werkzeug` | 0.15.6 | 3.0.3 | ## [3.15.0] - 2024-05-20 ### Dependencies diff --git a/README.md b/README.md index cedef62..7c32743 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ before trying to build: ``` $ docker login artifactory.algol60.net ``` -See more information on authetication here: +See more information on authentication here: https://rndwiki-pro.its.hpecorp.net/display/CSMTemp/Client+Authentication#ClientAuthentication-SecurityConsiderations ## Running Locally @@ -206,6 +206,8 @@ This will start the IMS server on `http://localhost:9100`. An S3 instance is required for the IMS server to do anything meaningful. See the [Configuration Options](#Configuration-Options) section for more information and further configuration possibilities. +Fetch recipes and images: + ``` $ curl http://127.0.0.1:9100/images [] @@ -213,9 +215,38 @@ $ curl http://127.0.0.1:9100/recipes [] ``` +Add a public key: + +``` +$ curl http://127.0.0.1:9100/public-keys -X POST -H "Content-Type: application/json" \ + --data '{"name":"test","public_key":"TEST_KEY_DATA"}' +``` + +Get a recipe description: + +``` +$ curl http://127.0.0.1:9100/recipes/RECIPE_ID +``` + +Patch an image record: + +``` +curl http://127.0.0.1:9100/images/IMAGE_ID -X PATCH -H "Content-Type: application/json" \ + --data '{"platform":"aarch64"}' +``` + +Create a job: + +``` +curl http://127.0.0.1:9100/jobs -X POST -H "Content-Type: application/json" \ + --data '{ "job_type":"create","require_dkms":"False","image_root_archive_name":"Test","artifact_id":"RECIPE_ID", \ + "public_key_id":"PUBLIC_KEY_ID"}' +``` + **NOTE:** To successfully post jobs to the IMS Jobs endpoint, you must be running under kubernetes as the IMS Service tries to launch a new K8S job. This is not expected to -work when running the IMS Service locally under Docker. +work when running the IMS Service locally under Docker. However running locally you +can watch the logs and verify the request posts correctly. ## Testing @@ -228,6 +259,47 @@ $ ./runCodeStyleCheck.sh $ ./runLint.sh ``` +### Setting up PyTest + +PyTest runs the unit tests directly, not inside a container. This means that it needs +some additional changes to get the correct configuration to run locally. + +The following are required beyond the development system setup: +* The Python requirements in requirements-test.txt + +1. Activate the virtual environment created for development + + ``` + $ . .env/bin/activate + ``` + +1. Install required python modules + + ``` + $ pip3 install -r requirements-test.txt + ``` + +1. Create pytest.ini file with env vars + + In the parent directory that IMS is cloned into, create the file 'pytest.ini' + and put the following contents in it: + + ``` + [pytest] + env = + FLASK_ENV=development + ``` + +1. Create a directory for the IMS data files + + This is the directory that the unit tests will use to create the + data file for IMS object records. + + ``` + cd ~ + mkdir -p ims/data + ``` + ### CT Tests See cms-tools repo for details on running CT tests for this service. diff --git a/constraints.txt b/constraints.txt index bb5ec03..8a0e880 100644 --- a/constraints.txt +++ b/constraints.txt @@ -1,37 +1,38 @@ -aniso8601==3.0.2 -boto3==1.12.49 -botocore==1.15.49 -cachetools==3.0.0 -certifi==2019.11.28 -chardet==3.0.4 -click==6.7 -docutils==0.14 +# Constraints updated 5/29/2024 +aniso8601==9.0.1 +boto3==1.34.114 +botocore==1.34.114 +cachetools==5.3.3 +certifi==2024.2.2 +chardet==5.2.0 +click==8.1.7 +docutils==0.21.2 fabric==3.2.2 -Flask==1.1.4 -flask-marshmallow==0.9.0 +Flask==3.0.3 +flask-marshmallow==1.2.1 Flask-RESTful==0.3.10 -google-auth==1.6.3 -gunicorn==19.10.0 +google-auth==2.29.0 +gunicorn==22.0.0 httpproblem==0.2.0 -idna==2.8 -itsdangerous==0.24 -Jinja2==2.10.3 -jmespath==0.9.5 +idna==3.7 +itsdangerous==2.2.0 +Jinja2==3.1.4 +jmespath==1.0.1 # CSM 1.6 uses Kubernetes 1.22, so use client v22.x to ensure compatability kubernetes==22.6.0 -MarkupSafe==1.1.1 -marshmallow==3.0.0b16 -oauthlib==2.1.0 -pyasn1==0.4.8 -pyasn1-modules==0.2.8 -pytest==8.1.1 +MarkupSafe==2.1.5 +marshmallow==3.21.2 +oauthlib==3.2.2 +pyasn1==0.6.0 # most recent: 1.6.1, pyasn1-modules 0.4.0 requires <0.7.0 +pyasn1-modules==0.4.0 python-dateutil==2.8.2 -pytz==2018.4 +pytest==8.1.1 +pytz==2024.1 PyYAML==6.0.1 -requests==2.23.0 -requests-oauthlib==1.0.0 -rsa==4.7.2 -s3transfer==0.3.7 -urllib3==1.25.11 -websocket-client==0.54.0 -Werkzeug==0.15.6 +requests==2.31.0 +requests-oauthlib==1.3.1 +rsa==4.9 +s3transfer==0.10.1 +urllib3==1.26.18 # most recent 2.2.1, botocore 1.34.114 requires <1.27.0 +websocket-client==1.8.0 +Werkzeug==3.0.3 diff --git a/requirements-test.txt b/requirements-test.txt index ff41b36..98ced5f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -5,6 +5,7 @@ testtools fixtures pytest pytest-cov +pytest-env pycodestyle pylint mock diff --git a/src/server/__init__.py b/src/server/__init__.py index c1be95b..01ee5a5 100644 --- a/src/server/__init__.py +++ b/src/server/__init__.py @@ -29,6 +29,7 @@ # TODO CASMCMS-1154 Get a real data store import os import os.path +from marshmallow import EXCLUDE class DataStoreHACK(collections.abc.MutableMapping): """ A dictionary that reads/writes to a file """ @@ -51,7 +52,7 @@ def _read(self): # Setting 'unknown="Exclude" allows downgrades by just dropping any data # fields that are no longer part of the current schema. with open(self.store_file, 'r') as data_file: - obj_data = self.schema.loads(data_file.read(), many=True, unknown="EXCLUDE") + obj_data = self.schema.loads(data_file.read(), many=True, unknown=EXCLUDE) self.store = {str(getattr(obj, self.key_field)): obj for obj in obj_data} def _write(self): diff --git a/src/server/models/__init__.py b/src/server/models/__init__.py index 18b3572..f423023 100644 --- a/src/server/models/__init__.py +++ b/src/server/models/__init__.py @@ -29,9 +29,10 @@ class ArtifactLink(Schema): """ A schema specifically for validating artifact links """ - path = fields.Str(required=True, description="URL or path to the artifact", - validate=Length(min=1, error="name field must not be blank")) - etag = fields.Str(required=False, default="", description="Artifact entity tag") + path = fields.Str(required=True, validate=Length(min=1, error="name field must not be blank"), + metadata={"metadata": {"description": "URL or path to the artifact"}}) + etag = fields.Str(required=False, dump_default="", load_default="", + metadata={"metadata": {"description": "Artifact entity tag"}}) type = fields.Str(required=True, allow_none=False, - description="The type of artifact link", + metadata={"metadata": {"description": "The type of artifact link"}}, validate=OneOf(ARTIFACT_LINK_TYPES, error="Type must be one of: {choices}.")) diff --git a/src/server/models/images.py b/src/server/models/images.py index eaeaf39..4b21407 100644 --- a/src/server/models/images.py +++ b/src/server/models/images.py @@ -56,15 +56,16 @@ def __repr__(self): class V2ImageRecordInputSchema(Schema): """ A schema specifically for defining and validating user input """ - name = fields.Str(required=True, description="the name of the image", - validate=Length(min=1, error="name field must not be blank")) + name = fields.Str(required=True, validate=Length(min=1, error="name field must not be blank"), + metadata={"metadata": {"description": "Name of the image"}}) link = fields.Nested(ArtifactLink, required=False, allow_none=True, - description="the location of the image manifest") - arch = fields.Str(required=False, default = ARCH_X86_64, description="Architecture of the image", - validate=OneOf([ARCH_ARM64,ARCH_X86_64]), load_default=True, dump_default=True) + metadata={"metadata": {"description": "Location of the image manifest"}}) + arch = fields.Str(required=False, validate=OneOf([ARCH_ARM64,ARCH_X86_64]), + load_default=ARCH_X86_64, dump_default=ARCH_X86_64, + metadata={"metadata": {"description": "Architecture of the image"}}) @post_load - def make_image(self, data): + def make_image(self, data, many, partial): """ Marshall an object out of the individual data components """ return V2ImageRecord(**data) @@ -79,8 +80,8 @@ class V2ImageRecordSchema(V2ImageRecordInputSchema): read in from a database. Builds upon the basic input fields in ImageRecordInputSchema. """ - id = fields.UUID(description="the unique id of the image") - created = fields.DateTime(description="the time the image record was created") + id = fields.UUID(metadata={"metadata": {"description": "Unique id of the image"}}) + created = fields.DateTime(metadata={"metadata": {"description": "Time the image record was created"}}) class V2ImageRecordPatchSchema(Schema): @@ -88,7 +89,7 @@ class V2ImageRecordPatchSchema(Schema): Schema for a updating an ImageRecord object. """ link = fields.Nested(ArtifactLink, required=False, allow_none=False, - description="the location of the image manifest") - arch = fields.Str(required=False, description="Architecture of the recipe", default=ARCH_X86_64, - validate=OneOf([ARCH_ARM64,ARCH_X86_64]), load_default=True, dump_default=True) - + metadata={"metadata": {"description": "Location of the image manifest"}}) + arch = fields.Str(required=False, validate=OneOf([ARCH_ARM64,ARCH_X86_64]), + load_default=ARCH_X86_64, dump_default=ARCH_X86_64, + metadata={"metadata": {"description": "Architecture of the recipe"}}) diff --git a/src/server/models/jobs.py b/src/server/models/jobs.py index 53a38c3..8a0642f 100644 --- a/src/server/models/jobs.py +++ b/src/server/models/jobs.py @@ -128,56 +128,50 @@ def __repr__(self): class SshContainerInputSchema(Schema): """ A schema specifically for defining and validating user input of SSH Containers """ - name = fields.String(description="SSH Container name") - jail = fields.Boolean(description="Whether to use an SSH jail to restrict access to the image root. " - "Default = False", default=False) + name = fields.String(metadata={"metadata": {"description": "SSH Container name"}}) + jail = fields.Boolean(metadata={"metadata": {"description": "Whether to use an SSH jail to restrict access to the image root. Default = False"}}, + load_default=False, dump_default=False) class V2JobRecordInputSchema(Schema): """ A schema specifically for defining and validating user input of Job requests """ artifact_id = fields.UUID(required=True, - description="IMS record id (either recipe or image depending on job_type)" - "for the source artifact") - public_key_id = fields.UUID(required=True, - description="IMS record id for the public_key record to use.") - job_type = fields.Str(required=True, - description="The type of job, either 'create' or 'customize'", + metadata={"metadata": {"description": "IMS record id (either recipe or image depending on job_type) for the source artifact"}},) + public_key_id = fields.UUID(required=True,metadata={"metadata": {"description": "IMS record id for the public_key record to use."}}) + job_type = fields.Str(required=True,metadata={"metadata": {"description": "The type of job, either 'create' or 'customize'"}}, validate=OneOf(JOB_TYPES, error="Job type must be one of: {choices}.")) - image_root_archive_name = fields.Str(required=True, - description="Name to be given to the image root artifact", - validate=Length(min=1, - error="image_root_archive_name field must not be blank")) - enable_debug = fields.Boolean(default=False, - Description="Whether to enable debugging of the job") - build_env_size = fields.Integer(Default=15, - Description="approximate disk size in GiB to reserve for the image build" - "environment (usually 2x final image size)", + image_root_archive_name = fields.Str(required=True, metadata={"metadata": {"description": "Name to be given to the image root artifact"}}, + validate=Length(min=1, error="image_root_archive_name field must not be blank")) + enable_debug = fields.Boolean(load_default=False,dump_default=False, + metadata={"metadata": {"description": "Whether to enable debugging of the job"}}) + build_env_size = fields.Integer(load_default=15,dump_default=15, + metadata={"metadata": {"description": "Approximate disk size in GiB to reserve for the image build environment (usually 2x final image size)"}}, validate=Range(min=1, error="build_env_size must be greater than or equal to 1")) - kernel_file_name = fields.Str(description="Name of the kernel file to extract and upload") + kernel_file_name = fields.Str(metadata={"metadata": {"description": "Name of the kernel file to extract and upload"}}) - initrd_file_name = fields.Str(default="initrd", - description="Name of the initrd file to extract and upload", + initrd_file_name = fields.Str(load_default="initrd", dump_default="initrd", + metadata={"metadata": {"description": "Name of the initrd file to extract and upload"}}, validate=Length(min=1, error="initrd_file_name field must not be blank")) kernel_parameters_file_name = \ - fields.Str(default="kernel-parameters", - description="Name of the kernel parameters file to extract and upload", + fields.Str(load_default="kernel-parameters", dump_default="kernel-parameters", + metadata={"metadata": {"description": "Name of the kernel parameters file to extract and upload"}}, validate=Length(min=1, error="kernel_parameters_file_name field must not be blank")) ssh_containers = fields.List(fields.Nested(SshContainerInputSchema()), allow_none=True) # v2.1 - require_dkms = fields.Boolean(required=False, default=False, load_default=True, dump_default=True, - description="Job requires the use of dkms") + require_dkms = fields.Boolean(required=False, load_default=False, dump_default=False, + metadata={"metadata": {"description": "Job requires the use of dkms"}}) # v2.2 - job_mem_size = fields.Integer(Default=1, - Description="approximate working memory in GiB to reserve for the build job " - "environment (loosely proporational to the final image size)", - validate=Range(min=1, error="build_env_size must be greater than or equal to 1")) + job_mem_size = fields.Integer(load_default=1, dump_default=1, + validate=Range(min=1, error="build_env_size must be greater than or equal to 1"), + metadata={"metadata": {"description": "Approximate working memory in GiB to reserve for the build job " + "environment (loosely proportional to the final image size)"}}) @post_load - def make_job(self, data): + def make_job(self, data, many, partial): """ Marshall an object out of the individual data components """ return V2JobRecord(**data) @@ -188,16 +182,16 @@ class Meta: # pylint: disable=missing-docstring class SshConnectionInfo(Schema): """ A schema specifically for validating SSH Container Connection Info """ - host = fields.String(description="Host ip or name to use to connect to the SSH container") - port = fields.Integer(description="Port number to use to connect to the SSH container") + host = fields.String(metadata={"metadata": {"description": "Host ip or name to use to connect to the SSH container"}}) + port = fields.Integer(metadata={"metadata": {"description": "Port number to use to connect to the SSH container"}}) class SshContainerSchema(SshContainerInputSchema): """ A schema specifically for validating SSH Containers """ - status = fields.String(description="SSH Container Status") - host = fields.String(description="Host ip or name to use to connect to the SSH container") - port = fields.Integer(description="Port number to use to connect to the SSH container") - connection_info = fields.Dict(key=fields.Str(), values=fields.Nested(SshConnectionInfo)) + status = fields.String(metadata={"metadata": {"description": "SSH Container Status"}}) + host = fields.String(metadata={"metadata": {"description": "Host ip or name to use to connect to the SSH container"}}) + port = fields.Integer(metadata={"metadata": {"description": "Port number to use to connect to the SSH container"}}) + connection_info = fields.Dict(keys=fields.Str(), values=fields.Nested(SshConnectionInfo)) class Meta: # pylint: disable=missing-docstring # host and port have been deprecated and are no longer used. @@ -210,38 +204,38 @@ class V2JobRecordSchema(V2JobRecordInputSchema): read in from a database. Builds upon the basic input fields in JobRecordInputSchema. """ - id = fields.UUID(description="the unique id of the job") - created = fields.DateTime(description="the time the job record was created") + id = fields.UUID(metadata={"metadata": {"description": "Unique id of the job"}}) + created = fields.DateTime(metadata={"metadata": {"description": "Time the job record was created"}}) kubernetes_job = fields.Str(allow_none=True, - description="Job name for the underlying Kubernetes job") + metadata={"metadata": {"description": "Job name for the underlying Kubernetes job"}}) kubernetes_service = fields.Str(allow_none=True, - description="Service name for the underlying Kubernetes service") + metadata={"metadata": {"description": "Service name for the underlying Kubernetes service"}}) kubernetes_configmap = fields.Str(allow_none=True, - description="ConfigMap name for the underlying Kubernetes configmap") - kubernetes_namespace = fields.Str(allow_none=True, default="default", - description="Kubernetes namespace where the IMS job resources were created") - status = fields.Str(allow_none=False, - description="state of the job request", + metadata={"metadata": {"description": "ConfigMap name for the underlying Kubernetes configmap"}}) + kubernetes_namespace = fields.Str(allow_none=True, load_default="default", dump_default="default", + metadata={"metadata": {"description": "Kubernetes namespace where the IMS job resources were created"}}) + status = fields.Str(allow_none=False,metadata={"metadata": {"description": "State of the job request"}}, validate=OneOf(STATUS_TYPES, error="Job state must be one of: {choices}.")) resultant_image_id = fields.UUID(allow_none=True, - description="the unique id of the resultant image record") + metadata={"metadata": {"description": "Unique id of the resultant image record"}}) ssh_containers = fields.List(fields.Nested(SshContainerSchema()), allow_none=True) # v2.1 - arch = fields.Str(description="Architecture of the job", default=ARCH_X86_64, - validate=OneOf([ARCH_ARM64,ARCH_X86_64]), load_default=True, dump_default=True) + arch = fields.Str(metadata={"metadata": {"description": "Architecture of the job"}}, + validate=OneOf([ARCH_ARM64,ARCH_X86_64]), + load_default=ARCH_X86_64, dump_default=ARCH_X86_64) # v2.2 kubernetes_pvc = fields.Str(allow_none=True, - description="PVC name for the underlying Kubernetes image pvc") + metadata={"metadata": {"description": "PVC name for the underlying Kubernetes image pvc"}}) # v2.3 - remote_build_node = fields.Str(allow_none=False, default="", - description="XName of remote job if running on a remote node") + remote_build_node = fields.Str(allow_none=False, load_default="", dump_default="", + metadata={"metadata": {"description": "XName of remote job if running on a remote node"}}) # after reading in the data, make sure there is an arch defined - default to x86 @post_load(pass_original=False) - def fill_arch(self, data, **kwargs): + def fill_arch(self, data, many, partial, **kwargs): if not "arch" in data or data["arch"] is None: data["arch"] = ARCH_X86_64 return data @@ -250,11 +244,10 @@ class V2JobRecordPatchSchema(Schema): """ Schema for a updating a JobRecord object. """ - status = fields.Str(required=False, - description="state of the job request", + status = fields.Str(required=False,metadata={"metadata": {"description": "State of the job request"}}, validate=OneOf(STATUS_TYPES, error="Job state must be one of: {choices}.")) resultant_image_id = fields.UUID(required=False, - description="the unique id of the resultant image record") + metadata={"metadata": {"description": "Unique id of the resultant image record"}}) #NOTE: this can't live in helper.py due to a circular dependency def find_remote_node_for_job(app, job: V2JobRecordSchema) -> str: diff --git a/src/server/models/publickeys.py b/src/server/models/publickeys.py index 27a6bad..73bd8c8 100644 --- a/src/server/models/publickeys.py +++ b/src/server/models/publickeys.py @@ -51,13 +51,13 @@ def __repr__(self): class V2PublicKeyRecordInputSchema(Schema): """ A schema specifically for defining and validating user input """ - name = fields.Str(required=True, description="A name to identify the public key", - validate=Length(min=1, error="name field must not be blank")) - public_key = fields.Str(required=True, description="The raw public key file contents", - validate=Length(min=1, error="public_key field must not be blank")) + name = fields.Str(required=True, validate=Length(min=1, error="name field must not be blank"), + metadata={"metadata": {"description": "Name to identify the public key"}}) + public_key = fields.Str(required=True, validate=Length(min=1, error="public_key field must not be blank"), + metadata={"metadata": {"description": "Raw public key file contents"}}) @post_load - def make_public_key(self, data): + def make_public_key(self, data, many, partial): """ Marshall an object out of the individual data components """ return V2PublicKeyRecord(**data) @@ -72,5 +72,5 @@ class V2PublicKeyRecordSchema(V2PublicKeyRecordInputSchema): read in from a database. Builds upon the basic input fields in PublicKeyRecordInputSchema. """ - id = fields.UUID(description="the unique id of the public key") - created = fields.DateTime(description="the time the public key9 record was created") + id = fields.UUID(metadata={"metadata": {"description": "Unique id of the public key"}}) + created = fields.DateTime(metadata={"metadata": {"description": "Time the public key9 record was created"}}) diff --git a/src/server/models/recipes.py b/src/server/models/recipes.py index d5182bd..1d49c96 100644 --- a/src/server/models/recipes.py +++ b/src/server/models/recipes.py @@ -47,8 +47,8 @@ class RecipeKeyValuePair(Schema): """ A schema specifically for defining and validating user input of SSH Containers """ - key = fields.String(description="Template Key", required=True) - value = fields.String(description="Template Value", required=True) + key = fields.String(metadata={"metadata": {"description": "Template Key"}}, required=True) + value = fields.String(metadata={"metadata": {"description": "Template Value"}}, required=True) class V2RecipeRecord: @@ -77,31 +77,27 @@ def __repr__(self): class V2RecipeRecordInputSchema(Schema): """ A schema specifically for defining and validating user input """ # v2.0 - name = fields.Str(required=True, description="the name of the recipe", - validate=Length(min=1, error="name field must not be blank")) + name = fields.Str(required=True, validate=Length(min=1, error="name field must not be blank"), + metadata={"metadata": {"description": "Name of the recipe"}}) link = fields.Nested(ArtifactLink, required=False, allow_none=True, - description="the location of the recipe archive") - recipe_type = fields.Str(required=True, - description="The type of recipe, currently '%s' is the only valid value" - % RECIPE_TYPE_KIWI_NG, - validate=OneOf(RECIPE_TYPES, error="Recipe type must be one of: {choices}.")) - linux_distribution = fields.Str(required=True, - description="The linux distributiobn of the recipe, either '%s' or '%s' or '%s'" - % (LINUX_DISTRIBUTION_SLES12, LINUX_DISTRIBUTION_SLES15, - LINUX_DISTRIBUTION_CENTOS), + metadata={"metadata": {"description": "Location of the recipe archive"}}) + recipe_type = fields.Str(required=True, validate=OneOf(RECIPE_TYPES, error="Recipe type must be one of: {choices}."), + metadata={"metadata": {"description": f"The type of recipe, currently '{RECIPE_TYPE_KIWI_NG}' is the only valid value"}}) + linux_distribution = fields.Str(required=True,metadata={"metadata": {"description": f"The linux distribution of the recipe, either " + f"'{LINUX_DISTRIBUTION_SLES12}' or '{LINUX_DISTRIBUTION_SLES15}' or '{LINUX_DISTRIBUTION_CENTOS}'"}}, validate=OneOf(LINUX_DISTRIBUTIONS, error="Recipe type must be one of: {choices}.")) # v2.1 template_dictionary = fields.List(fields.Nested(RecipeKeyValuePair()), required=False, allow_none=True) # v2.2 - require_dkms = fields.Boolean(required=False, default=False, load_default=True, dump_default=True, - description="Recipe requires the use of dkms") - arch = fields.Str(required=False, description="Architecture of the recipe", default=ARCH_X86_64, - validate=OneOf([ARCH_ARM64,ARCH_X86_64]), load_default=True, dump_default=True) + require_dkms = fields.Boolean(load_default=False, dump_default=False, + metadata={"metadata": {"description": "Recipe requires the use of dkms"}}) + arch = fields.Str(required=False, metadata={"metadata": {"description": "Architecture of the recipe"}}, + validate=OneOf([ARCH_ARM64,ARCH_X86_64]), load_default=ARCH_X86_64, dump_default=ARCH_X86_64) @post_load - def make_recipe(self, data): + def make_recipe(self, data, many, partial): """ Marshall an object out of the individual data components """ data['template_dictionary'] = data.get('template_dictionary', []) return V2RecipeRecord(**data) @@ -117,8 +113,8 @@ class V2RecipeRecordSchema(V2RecipeRecordInputSchema): read in from a database. Builds upon the basic input fields in RecipeRecordInputSchema. """ - id = fields.UUID(description="the unique id of the recipe") - created = fields.DateTime(description="the time the recipe record was created") + id = fields.UUID(metadata={"metadata": {"description": "Unique id of the recipe"}}) + created = fields.DateTime(metadata={"metadata": {"description": "Time the recipe record was created"}}) class V2RecipeRecordPatchSchema(Schema): @@ -126,9 +122,10 @@ class V2RecipeRecordPatchSchema(Schema): Schema for a updating a RecipeRecord object. """ link = fields.Nested(ArtifactLink, required=False, allow_none=False, - description="the location of the recipe archive") - arch = fields.Str(required=False, description="Architecture of the recipe", default=ARCH_X86_64, - validate=OneOf([ARCH_ARM64,ARCH_X86_64]), load_default=True, dump_default=True) - require_dkms = fields.Boolean(required=False, default=False, load_default=True, dump_default=True, - description="Recipe requires the use of dkms") + metadata={"metadata": {"description": "Location of the recipe archive"}}) + arch = fields.Str(required=False, validate=OneOf([ARCH_ARM64,ARCH_X86_64]), + load_default=ARCH_X86_64, dump_default=ARCH_X86_64, + metadata={"metadata": {"description": "Architecture of the recipe"}}) + require_dkms = fields.Boolean(required=False, load_default=False, dump_default=False, + metadata={"metadata": {"description": "Recipe requires the use of dkms"}}) template_dictionary = fields.List(fields.Nested(RecipeKeyValuePair()), required=False, allow_none=True) diff --git a/src/server/models/remote_build_nodes.py b/src/server/models/remote_build_nodes.py index 54d6cf6..7130250 100644 --- a/src/server/models/remote_build_nodes.py +++ b/src/server/models/remote_build_nodes.py @@ -144,11 +144,11 @@ def getStatus(self) -> (str, int): #(arch, current jobs) class V3RemoteBuildNodeRecordInputSchema(Schema): """ A schema specifically for defining and validating user input """ - xname = fields.Str(required=True, description="The XName of the remote build node", + xname = fields.Str(required=True, metadata={"metadata": {"description": "XName of the remote build node"}}, validate=Length(min=1, error="name field must not be blank")) @post_load - def make_remote_build_node(self, data): + def make_remote_build_node(self, data, many, partial): """ Marshall an object out of the individual data components """ return V3RemoteBuildNodeRecord(**data) diff --git a/src/server/v3/models/images.py b/src/server/v3/models/images.py index fba5540..65aa802 100644 --- a/src/server/v3/models/images.py +++ b/src/server/v3/models/images.py @@ -47,7 +47,7 @@ class V3DeletedImageRecordInputSchema(V2ImageRecordInputSchema): """ A schema specifically for defining and validating user input """ @post_load - def make_image(self, data): + def make_image(self, data, many, partial): """ Marshall an object out of the individual data components """ return V3DeletedImageRecord(**data) @@ -62,9 +62,9 @@ class V3DeletedImageRecordSchema(V3DeletedImageRecordInputSchema): read in from a database. Builds upon the basic input fields in ImageRecordInputSchema. """ - id = fields.UUID(description="the unique id of the image") - created = fields.DateTime(description="the time the image record was created") - deleted = fields.DateTime(description="the time the image record was deleted") + id = fields.UUID(metadata={"metadata": {"description": "Unique id of the image"}}) + created = fields.DateTime(metadata={"metadata": {"description": "Time the image record was created"}}) + deleted = fields.DateTime(metadata={"metadata": {"description": "Time the image record was deleted"}}) class V3DeletedImageRecordPatchSchema(Schema): @@ -72,6 +72,6 @@ class V3DeletedImageRecordPatchSchema(Schema): Schema for a updating an ImageRecord object. """ operation = fields.Str(required=True, - description='The operation or action that should be taken on the image record. ' - f'Supported operations are: { ", ".join(PATCH_OPERATIONS) }', + metadata={"metadata": {"description": "The operation or action that should be taken on the image record. " + f'Supported operations are: { ", ".join(PATCH_OPERATIONS) }'}}, validate=OneOf(PATCH_OPERATIONS, error="Recipe type must be one of: {choices}.")) diff --git a/src/server/v3/models/public_keys.py b/src/server/v3/models/public_keys.py index 8ed8e65..1ca770f 100644 --- a/src/server/v3/models/public_keys.py +++ b/src/server/v3/models/public_keys.py @@ -47,7 +47,7 @@ class V3DeletedPublicKeyRecordInputSchema(V2PublicKeyRecordInputSchema): """ A schema specifically for defining and validating user input """ @post_load - def make_public_key(self, data): + def make_public_key(self, data, many, partial): """ Marshall an object out of the individual data components """ return V3DeletedPublicKeyRecord(**data) @@ -62,9 +62,9 @@ class V3DeletedPublicKeyRecordSchema(V3DeletedPublicKeyRecordInputSchema): read in from a database. Builds upon the basic input fields in DeletedRecipeRecordInputSchema. """ - id = fields.UUID(description="the unique id of the public_key") - created = fields.DateTime(description="the time the public_key record was created") - deleted = fields.DateTime(description="the time the public_key record was deleted") + id = fields.UUID(metadata={"metadata": {"description": "Unique id of the public_key"}}) + created = fields.DateTime(metadata={"metadata": {"description": "Time the public_key record was created"}}) + deleted = fields.DateTime(metadata={"metadata": {"description": "Time the public_key record was deleted"}}) class V3DeletedPublicKeyRecordPatchSchema(Schema): @@ -72,6 +72,6 @@ class V3DeletedPublicKeyRecordPatchSchema(Schema): Schema for a updating an PublicKey object. """ operation = fields.Str(required=True, - description='The operation or action that should be taken on the recipe record. ' - f'Supported operations are: { ", ".join(PATCH_OPERATIONS) }', + metadata={"metadata": {"description": "The operation or action that should be taken on the recipe record. " + f'Supported operations are: { ", ".join(PATCH_OPERATIONS) }'}}, validate=OneOf(PATCH_OPERATIONS, error="Recipe type must be one of: {choices}.")) diff --git a/src/server/v3/models/recipes.py b/src/server/v3/models/recipes.py index 381eaba..87e481f 100644 --- a/src/server/v3/models/recipes.py +++ b/src/server/v3/models/recipes.py @@ -53,7 +53,7 @@ class V3DeletedRecipeRecordInputSchema(V2RecipeRecordInputSchema): """ A schema specifically for defining and validating user input """ @post_load - def make_recipe(self, data): + def make_recipe(self, data, many, partial): """ Marshall an object out of the individual data components """ return V3DeletedRecipeRecord(**data) @@ -68,9 +68,9 @@ class V3DeletedRecipeRecordSchema(V3DeletedRecipeRecordInputSchema): read in from a database. Builds upon the basic input fields in DeletedRecipeRecordInputSchema. """ - id = fields.UUID(description="the unique id of the recipe") - created = fields.DateTime(description="the time the recipe record was created") - deleted = fields.DateTime(description="the time the recipe record was deleted") + id = fields.UUID(metadata={"metadata": {"description": "Unique id of the recipe"}}) + created = fields.DateTime(metadata={"metadata": {"description": "Time the recipe record was created"}}) + deleted = fields.DateTime(metadata={"metadata": {"description": "Time the recipe record was deleted"}}) class V3DeletedRecipeRecordPatchSchema(Schema): @@ -78,6 +78,6 @@ class V3DeletedRecipeRecordPatchSchema(Schema): Schema for a updating an RecipeRecord object. """ operation = fields.Str(required=True, - description='The operation or action that should be taken on the recipe record. ' - f'Supported operations are: { ", ".join(PATCH_OPERATIONS) }', + metadata={"metadata": {"description": "The operation or action that should be taken on the recipe record. " + f'Supported operations are: { ", ".join(PATCH_OPERATIONS) }'}}, validate=OneOf(PATCH_OPERATIONS, error="Recipe type must be one of: {choices}.")) diff --git a/tests/utils.py b/tests/utils.py index 43694b1..b37fa61 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -25,6 +25,8 @@ Test Utilities """ +# Format for read/write of test data datetime strings +DATETIME_STRING = '%Y-%m-%dT%H:%M:%S' def check_error_responses(testcase, response, status_code, fields): """ diff --git a/tests/v2/test_v2_images.py b/tests/v2/test_v2_images.py index be15acb..35e00c0 100644 --- a/tests/v2/test_v2_images.py +++ b/tests/v2/test_v2_images.py @@ -37,7 +37,7 @@ from src.server import app from src.server.helper import S3Url -from tests.utils import check_error_responses +from tests.utils import check_error_responses, DATETIME_STRING from tests.v2.ims_fixtures import V2FlaskTestClientFixture, V2ImagesDataFixture @@ -122,7 +122,7 @@ def test_get(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_with_link[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.data_record_with_link[key], @@ -142,7 +142,7 @@ def test_get_link_none(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_link_none[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.data_record_link_none[key], @@ -162,7 +162,7 @@ def test_get_no_link(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_no_link[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) elif key == 'link': self.assertEqual(response_data[key], None, @@ -272,7 +272,7 @@ def test_patch(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_link_none[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) elif key == 'link': self.assertEqual(response_data[key], link_data['link'], @@ -331,7 +331,7 @@ def test_patch_same_link(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_with_link[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.data_record_with_link[key], @@ -355,7 +355,7 @@ def test_patch_change_arch(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_link_none[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) elif key == 'arch': self.assertEqual(response_data[key], patch_data['arch'], @@ -421,7 +421,7 @@ def test_get(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.data[key]) diff --git a/tests/v2/test_v2_jobs.py b/tests/v2/test_v2_jobs.py index 0de1ffd..3e35cdd 100644 --- a/tests/v2/test_v2_jobs.py +++ b/tests/v2/test_v2_jobs.py @@ -43,7 +43,7 @@ from src.server.helper import S3Url from src.server.models.jobs import (KERNEL_FILE_NAME_ARM, KERNEL_FILE_NAME_X86, STATUS_TYPES) -from tests.utils import check_error_responses +from tests.utils import check_error_responses, DATETIME_STRING from tests.v2.ims_fixtures import (V2FlaskTestClientFixture, V2ImagesDataFixture, V2JobsDataFixture, V2PublicKeysDataFixture, @@ -94,7 +94,7 @@ def test_get(self, client_mock, config_mock, utils_mock): self.assertAlmostEqual(datetime.datetime.strptime(self.data[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) elif key in self.data: self.assertEqual(response_data[key], self.data[key], @@ -376,7 +376,7 @@ def test_get(self, utils_mock, config_mock, client_mock): self.assertAlmostEqual(datetime.datetime.strptime(self.job_data[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.job_data[key]) diff --git a/tests/v2/test_v2_public_keys.py b/tests/v2/test_v2_public_keys.py index 124e122..6fa877a 100644 --- a/tests/v2/test_v2_public_keys.py +++ b/tests/v2/test_v2_public_keys.py @@ -33,7 +33,7 @@ from testtools.matchers import HasLength from tests.v2.ims_fixtures import V2FlaskTestClientFixture, V2PublicKeysDataFixture -from tests.utils import check_error_responses +from tests.utils import check_error_responses, DATETIME_STRING class TestV2PublicKeyEndpoint(TestCase): @@ -65,7 +65,7 @@ def test_get(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.data[key], @@ -117,7 +117,7 @@ def test_get(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.data[key]) diff --git a/tests/v2/test_v2_recipes.py b/tests/v2/test_v2_recipes.py index 6dfdd35..87a58b1 100644 --- a/tests/v2/test_v2_recipes.py +++ b/tests/v2/test_v2_recipes.py @@ -36,7 +36,7 @@ from src.server import app from src.server.helper import S3Url -from tests.utils import check_error_responses +from tests.utils import check_error_responses, DATETIME_STRING from tests.v2.ims_fixtures import V2FlaskTestClientFixture, V2RecipesDataFixture @@ -119,7 +119,7 @@ def test_get(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_with_link[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.data_record_with_link[key], @@ -190,7 +190,7 @@ def test_patch(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_link_none[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) elif key == 'link': self.assertEqual(response_data[key], link_data['link'], @@ -218,7 +218,7 @@ def test_patch_architecture(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_link_none[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) elif key == 'arch': self.assertEqual(response_data[key], arch_data['arch'], @@ -246,7 +246,7 @@ def test_patch_dkms(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_link_none[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) elif key == 'require_dkms': self.assertEqual(response_data[key], dkms_data['require_dkms'], @@ -305,7 +305,7 @@ def test_patch_same_link(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_with_link[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.data_record_with_link[key], @@ -356,7 +356,7 @@ def test_get(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.data[key]) diff --git a/tests/v3/test_v3_deleted_images.py b/tests/v3/test_v3_deleted_images.py index 8473379..fb1ddf9 100644 --- a/tests/v3/test_v3_deleted_images.py +++ b/tests/v3/test_v3_deleted_images.py @@ -33,7 +33,7 @@ from src.server import app from src.server.helper import S3Url, ARTIFACT_LINK_TYPE_S3 -from tests.utils import check_error_responses +from tests.utils import check_error_responses, DATETIME_STRING from tests.v3.ims_fixtures import V3FlaskTestClientFixture, V3ImagesDataFixture, V3DeletedImagesDataFixture @@ -171,7 +171,7 @@ def test_get(self): self.assertAlmostEqual(datetime.strptime(test_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.strptime(response_data[key], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=timedelta(seconds=1)) else: self.assertEqual(response_data[key], test_record[key], @@ -302,7 +302,7 @@ def test_get_all(self): self.assertAlmostEqual(datetime.strptime(source_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.strptime(response_record[key], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=timedelta(seconds=1)) else: self.assertEqual(source_record[key], response_record[key], diff --git a/tests/v3/test_v3_deleted_public_keys.py b/tests/v3/test_v3_deleted_public_keys.py index b0b80c2..d6f1bea 100644 --- a/tests/v3/test_v3_deleted_public_keys.py +++ b/tests/v3/test_v3_deleted_public_keys.py @@ -31,7 +31,7 @@ from src.server import app from src.server.helper import S3Url from tests.v3.ims_fixtures import V3FlaskTestClientFixture, V3PublicKeysDataFixture, V3DeletedPublicKeysDataFixture -from tests.utils import check_error_responses +from tests.utils import check_error_responses, DATETIME_STRING class TestV3DeletedPublicKeysBase(TestCase): @@ -81,7 +81,7 @@ def test_get(self): self.assertAlmostEqual(datetime.strptime(self.test_public_key_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.strptime(response_data[key], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=timedelta(seconds=1)) else: self.assertEqual(response_data[key], self.test_public_key_record[key], @@ -159,7 +159,7 @@ def test_get_all(self): self.assertAlmostEqual(datetime.strptime(source_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.strptime(response_record[key], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=timedelta(seconds=1)) else: self.assertEqual(source_record[key], response_record[key], diff --git a/tests/v3/test_v3_deleted_recipes.py b/tests/v3/test_v3_deleted_recipes.py index 100a5c7..85acbf4 100644 --- a/tests/v3/test_v3_deleted_recipes.py +++ b/tests/v3/test_v3_deleted_recipes.py @@ -33,7 +33,7 @@ from src.server import app from src.server.helper import S3Url, ARTIFACT_LINK_TYPE_S3 from tests.v3.ims_fixtures import V3FlaskTestClientFixture, V3RecipesDataFixture, V3DeletedRecipesDataFixture -from tests.utils import check_error_responses +from tests.utils import check_error_responses, DATETIME_STRING class TestV3DeletedRecipeBase(TestCase): @@ -174,7 +174,7 @@ def test_get(self): self.assertAlmostEqual(datetime.strptime(test_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.strptime(response_data[key], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=timedelta(seconds=1)) else: self.assertEqual(response_data[key], test_record[key], @@ -266,7 +266,7 @@ def test_get_all(self): self.assertAlmostEqual(datetime.strptime(source_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.strptime(response_record[key], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=timedelta(seconds=1)) else: self.assertEqual(source_record[key], response_record[key], diff --git a/tests/v3/test_v3_images.py b/tests/v3/test_v3_images.py index 9792c71..7e85d05 100644 --- a/tests/v3/test_v3_images.py +++ b/tests/v3/test_v3_images.py @@ -36,7 +36,7 @@ from src.server import app from src.server.helper import S3Url, ARTIFACT_LINK_TYPE_S3 -from tests.utils import check_error_responses +from tests.utils import check_error_responses, DATETIME_STRING from tests.v3.ims_fixtures import V3FlaskTestClientFixture, V3ImagesDataFixture, V3DeletedImagesDataFixture @@ -171,7 +171,7 @@ def test_get(self): self.assertAlmostEqual(datetime.datetime.strptime(self.test_with_link_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.test_with_link_record[key], @@ -191,7 +191,7 @@ def test_get_link_none(self): self.assertAlmostEqual(datetime.datetime.strptime(self.test_link_none_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.test_link_none_record[key], @@ -211,7 +211,7 @@ def test_get_no_link(self): self.assertAlmostEqual(datetime.datetime.strptime(self.test_no_link_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) elif key == 'link': self.assertEqual(response_data[key], None, @@ -349,7 +349,7 @@ def test_patch_change_architecture(self): self.assertAlmostEqual(datetime.datetime.strptime(self.test_link_none_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) elif key == 'arch': self.assertEqual(response_data[key], patch_data['arch'], @@ -403,7 +403,7 @@ def test_patch(self): self.assertAlmostEqual(datetime.datetime.strptime(self.test_link_none_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) elif key == 'link': self.assertEqual(response_data[key], link_data['link'], @@ -453,7 +453,7 @@ def test_patch_same_link(self): self.assertAlmostEqual(datetime.datetime.strptime(self.test_with_link_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.test_with_link_record[key], @@ -486,7 +486,7 @@ def test_get_all(self): self.assertAlmostEqual(datetime.datetime.strptime(source_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_record[key], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=1)) else: self.assertEqual(source_record[key], response_record[key]) diff --git a/tests/v3/test_v3_jobs.py b/tests/v3/test_v3_jobs.py index 0ec8906..7d181d8 100644 --- a/tests/v3/test_v3_jobs.py +++ b/tests/v3/test_v3_jobs.py @@ -43,7 +43,7 @@ from src.server.helper import ARTIFACT_LINK_TYPE_S3, S3Url from src.server.models.jobs import (KERNEL_FILE_NAME_ARM, KERNEL_FILE_NAME_X86, STATUS_TYPES) -from tests.utils import check_error_responses +from tests.utils import check_error_responses, DATETIME_STRING #from tests.v2.ims_fixtures import (V2FlaskTestClientFixture, # V2ImagesDataFixture, V2JobsDataFixture, # V2PublicKeysDataFixture, @@ -101,7 +101,7 @@ def test_get(self, client_mock, config_mock, utils_mock): self.assertAlmostEqual(datetime.datetime.strptime(self.data[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) elif key in self.data: self.assertEqual(response_data[key], self.data[key], @@ -382,7 +382,7 @@ def test_get(self, utils_mock, config_mock, client_mock): self.assertAlmostEqual(datetime.datetime.strptime(self.job_data[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.job_data[key]) diff --git a/tests/v3/test_v3_public_keys.py b/tests/v3/test_v3_public_keys.py index 748b310..0af3c45 100644 --- a/tests/v3/test_v3_public_keys.py +++ b/tests/v3/test_v3_public_keys.py @@ -33,7 +33,7 @@ from testtools.matchers import HasLength from tests.v3.ims_fixtures import V3FlaskTestClientFixture, V3PublicKeysDataFixture, V3DeletedPublicKeysDataFixture -from tests.utils import check_error_responses +from tests.utils import check_error_responses, DATETIME_STRING class TestV3PublicKeyBase(TestCase): @@ -80,7 +80,7 @@ def test_get(self): self.assertAlmostEqual(datetime.datetime.strptime(self.test_public_key_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data[key], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=1)) else: self.assertEqual(response_data[key], self.test_public_key_record[key], @@ -146,7 +146,7 @@ def test_get_all(self): self.assertAlmostEqual(datetime.datetime.strptime(source_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_record[key], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=1)) else: self.assertEqual(source_record[key], response_record[key]) diff --git a/tests/v3/test_v3_recipes.py b/tests/v3/test_v3_recipes.py index da4b9a3..556819e 100644 --- a/tests/v3/test_v3_recipes.py +++ b/tests/v3/test_v3_recipes.py @@ -36,7 +36,7 @@ from src.server import app from src.server.helper import S3Url, ARTIFACT_LINK_TYPE_S3 -from tests.utils import check_error_responses +from tests.utils import check_error_responses, DATETIME_STRING from tests.v3.ims_fixtures import V3FlaskTestClientFixture, V3RecipesDataFixture, V3DeletedRecipesDataFixture @@ -167,7 +167,7 @@ def test_get(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_with_link[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.data_record_with_link[key], @@ -251,7 +251,7 @@ def test_patch(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_link_none[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) elif key == 'link': self.assertEqual(response_data[key], link_data['link'], @@ -279,7 +279,7 @@ def test_patch_architecture(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_link_none[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) elif key == 'arch': self.assertEqual(response_data[key], arch_data['arch'], @@ -307,7 +307,7 @@ def test_patch_dkms(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_link_none[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) elif key == 'require_dkms': self.assertEqual(response_data[key], dkms_data['require_dkms'], @@ -357,7 +357,7 @@ def test_patch_same_link(self): self.assertAlmostEqual(datetime.datetime.strptime(self.data_record_with_link[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_data['created'], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=5)) else: self.assertEqual(response_data[key], self.data_record_with_link[key], @@ -387,7 +387,7 @@ def test_get_all(self): self.assertAlmostEqual(datetime.datetime.strptime(source_record[key], '%Y-%m-%dT%H:%M:%S'), datetime.datetime.strptime(response_record[key], - '%Y-%m-%dT%H:%M:%S+00:00'), + DATETIME_STRING), delta=datetime.timedelta(seconds=1)) else: self.assertEqual(source_record[key], response_record[key]) From e789dbdc90e2a43fc368f2999a9650f8294ca40e Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Tue, 4 Jun 2024 12:37:33 -0500 Subject: [PATCH 02/25] Merge conflict resolution --- .idea/ims.iml | 8 ++ .idea/inspectionProfiles/Project_Default.xml | 14 ++++ .../inspectionProfiles/profiles_settings.xml | 6 ++ .idea/modules.xml | 8 ++ CHANGELOG.md | 3 + api/openapi.yaml | 77 +++++++++++++++---- constraints.txt | 1 + src/server/models/images.py | 32 +++++++- src/server/v2/resources/images.py | 35 ++++++++- src/server/v3/resources/images.py | 34 +++++++- tests/ims_fixtures.py | 5 ++ tests/v2/ims_fixtures.py | 3 + tests/v2/test_v2_images.py | 23 +++++- 13 files changed, 227 insertions(+), 22 deletions(-) create mode 100644 .idea/ims.iml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/modules.xml diff --git a/.idea/ims.iml b/.idea/ims.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/.idea/ims.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..c662325 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2ba2cd2 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index cc3d1ee..90ff868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- CASMCMS-8915: IMS API features for tagging built images ### Dependencies - CASMCMS-8022 - update python dependencies to most recent versions. | Package | From | To | @@ -52,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - CASMCMS-8950: Fixed loading Kubernetes configuration data in the shasta_s3_creds module + ## [3.14.0] - 2024-03-01 ### Added - CASMCMS-8795 - add remote-build-nodes API. diff --git a/api/openapi.yaml b/api/openapi.yaml index 9809cc4..3f89a3d 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -2245,6 +2245,60 @@ components: type: array items: $ref: '#/components/schemas/RecipeKeyValuePair' + ImageMetadataAnnotationKeyValuePair: + description: Key/value pair used to further define an Image + type: object + required: + - key + - value + properties: + key: + description: Template variable to associate with the IMS image + example: includes_additional_packages + type: string + value: + description: Value variable to associate with the IMS image + example: "foo,bar,baz" + type: string + ImagePatchRecord: + description: Values used to update an existing IMS Image Record + type: object + properties: + link: + $ref: '#/components/schemas/ArtifactLinkRecord' + arch: + description: Target architecture for the recipe. + example: aarch64 + enum: + - aarch64 + - x86_64 + type: string + metadata: + description: A list of metadata change operations to apply to an existing Image Record + type: array + items: + description: An object which indicates a number of annotation patch operations relating to image tags. + type: object + required: + - operation + - key + properties: + operation: + description: How to update a given key within the context of a patch operation + type: string + enum: + - set + - remove + key: + description: The key to update for a given image + type: string + example: includes_additional_packages + value: + description: The value to associate with a key during a patch operation + type: string + example: "vim,emacs,man" + + ImageRecord: description: An Image Record type: object @@ -2277,6 +2331,11 @@ components: - aarch64 - x86_64 type: string + metadata: + description: An object of key/value associated with an Image + type: object + properties: + $ref: '#/components/schemas/ImageMetadataAnnotationKeyValuePair' DeletedImageRecord: description: A Deleted Image Record type: object @@ -2316,19 +2375,11 @@ components: - aarch64 - x86_64 type: string - ImagePatchRecord: - description: Values to update an ImageRecord with - type: object - properties: - link: - $ref: '#/components/schemas/ArtifactLinkRecord' - arch: - description: Target architecture for the recipe. - example: aarch64 - enum: - - aarch64 - - x86_64 - type: string + metadata: + description: List of key/value pairs to associate with an Image + type: object + properties: + $ref: '#/components/schemas/ImageMetadataAnnotationKeyValuePair' JobRecord: description: A Job Record type: object diff --git a/constraints.txt b/constraints.txt index 8a0e880..df25148 100644 --- a/constraints.txt +++ b/constraints.txt @@ -36,3 +36,4 @@ s3transfer==0.10.1 urllib3==1.26.18 # most recent 2.2.1, botocore 1.34.114 requires <1.27.0 websocket-client==1.8.0 Werkzeug==3.0.3 + diff --git a/src/server/models/images.py b/src/server/models/images.py index 4b21407..0804623 100644 --- a/src/server/models/images.py +++ b/src/server/models/images.py @@ -34,15 +34,25 @@ from src.server.models import ArtifactLink from src.server.helper import ARCH_X86_64, ARCH_ARM64 + +class ImageMetadata(Schema): + """ A schema specifically for validating existing image metadata instances. """ + key = fields.Str(required=True, description="An arbitrary key of metadata given for an image", + validate=Length(min=1, error="name field must not be blank")) + value = fields.Str(required=True, default="", + description="A value field given for an associated image metadata key.") + + class V2ImageRecord: """ The ImageRecord object """ # pylint: disable=W0622 - def __init__(self, name, link=None, id=None, created=None, arch=ARCH_X86_64): + def __init__(self, name, link=None, id=None, created=None, arch=ARCH_X86_64, metadata=None): # Supplied self.name = name self.link = link - + self.metadata = metadata if metadata else {} + # v2.1 self.arch = arch @@ -63,6 +73,10 @@ class V2ImageRecordInputSchema(Schema): arch = fields.Str(required=False, validate=OneOf([ARCH_ARM64,ARCH_X86_64]), load_default=ARCH_X86_64, dump_default=ARCH_X86_64, metadata={"metadata": {"description": "Architecture of the image"}}) + metadata = fields.Mapping(keys=fields.Str(required=True), + value=fields.Str(required=False), + desciption="User supplied additional information about an image", + default={}) @post_load def make_image(self, data, many, partial): @@ -84,12 +98,24 @@ class V2ImageRecordSchema(V2ImageRecordInputSchema): created = fields.DateTime(metadata={"metadata": {"description": "Time the image record was created"}}) +class V2ImageRecordMetadataPatchSchema(Schema): + operation = fields.Str(required=True, description="A method for how to change a metadata struct.", + validate=OneOf(['set', 'remove'])) + key = fields.Str(required=True, description="The metadata key that is to be affected.") + value = fields.Str(required=False, description="The value to store for the provided key.") + + class V2ImageRecordPatchSchema(Schema): """ - Schema for a updating an ImageRecord object. + Schema for updating an ImageRecord object. """ link = fields.Nested(ArtifactLink, required=False, allow_none=False, metadata={"metadata": {"description": "Location of the image manifest"}}) arch = fields.Str(required=False, validate=OneOf([ARCH_ARM64,ARCH_X86_64]), load_default=ARCH_X86_64, dump_default=ARCH_X86_64, metadata={"metadata": {"description": "Architecture of the recipe"}}) + metadata = fields.List(fields.Nested(V2ImageRecordMetadataPatchSchema()), + default=[], + required=False, + description="A list of change operations to perform on Image Metadata.") + diff --git a/src/server/v2/resources/images.py b/src/server/v2/resources/images.py index 9405ac7..61ce625 100644 --- a/src/server/v2/resources/images.py +++ b/src/server/v2/resources/images.py @@ -29,6 +29,7 @@ from flask import jsonify, request, current_app from flask_restful import Resource +from copy import deepcopy from src.server.errors import problemify, generate_missing_input_response, generate_data_validation_failure, \ generate_resource_not_found_response, generate_patch_conflict @@ -208,7 +209,8 @@ def patch(self, image_id): # Validate input errors = image_patch_input_schema.validate(json_data) if errors: - current_app.logger.info("%s There was a problem validating the PATCH data: %s", log_id, errors) + current_app.logger.info("%s There was a problem validating the PATCH data: %s, json_data: %s" + % (log_id, errors, json_data)) return generate_data_validation_failure(errors) image = current_app.data["images"][image_id] @@ -229,8 +231,37 @@ def patch(self, image_id): elif key == "arch": current_app.logger.info(f"Patching architecture with {value}") image.arch = value + elif key == 'metadata': + if not value: + current_app.logger.info("No metadata values to patch.") + continue + # Even though the API represents Image Metadata Annotations as a list internally, they behave like + # dictionaries. The ordered nature of the data should not matter, nor are they enforced. As such, + # converting the list of k:vs to a unified dictionary has performance advantages log(n) when doing + # multiple insertions or deletions. We will flatten this back out to a list before setting it within + # the image. + metadata_dict = deepcopy(image.metadata) + for changeset in value: + operation = changeset.get('operation') + if operation not in ['set', 'remove']: + current_app.logger.info(f"Unknown requested operation change '{operation}'.") + return generate_data_validation_failure(errors=[]) + annotation_key = changeset.get('key') + annotation_value = changeset.get('value', '') + + if operation == 'set': + metadata_dict[annotation_key] = annotation_value + elif operation == 'remove': + try: + del metadata_dict[annotation_key] + except KeyError: + current_app.logger.info("No-op when removing non-existent metadata from IMS record.") + pass + # With every change made to the image_annotation_dictionary, the last thing that is necessary is + # to convert the temporary dictionary back into a list of key:value pairs. + image.metadata = metadata_dict else: - current_app.logger.info(f"{log_id} Not able to patch record field {key} with value {value}") + current_app.logger.info(f"{log_id} Not able to patch record field '{key}' with value {value}") return generate_data_validation_failure(errors=[]) setattr(image, key, value) diff --git a/src/server/v3/resources/images.py b/src/server/v3/resources/images.py index 244475b..9e82242 100644 --- a/src/server/v3/resources/images.py +++ b/src/server/v3/resources/images.py @@ -28,6 +28,7 @@ from flask import jsonify, request, current_app from flask_restful import Resource +from copy import deepcopy from src.server.errors import problemify, generate_missing_input_response, generate_data_validation_failure, \ generate_resource_not_found_response, generate_patch_conflict @@ -348,6 +349,7 @@ def patch(self, image_id): # Validate input errors = image_patch_input_schema.validate(json_data) if errors: + current_app.logger.info("PATCH data: '%s'", json_data) current_app.logger.info("%s There was a problem validating the PATCH data: %s", log_id, errors) return generate_data_validation_failure(errors) @@ -376,8 +378,38 @@ def patch(self, image_id): elif key == "arch": current_app.logger.info(f"Patching architecture with {value}") image.arch = value + elif key == 'metadata': + if not value: + current_app.logger.info("No metadata values to patch.") + continue + # Even though the API represents Image Metadata Annotations as a list internally, they behave like + # dictionaries. The ordered nature of the data should not matter, nor are they enforced. As such, + # converting the list of k:vs to a unified dictionary has performance advantages log(n) when doing + # multiple insertions or deletions. We will flatten this back out to a list before setting it within + # the image. + metadata_dict = deepcopy(image.metadata) + for changeset in value: + operation = changeset.get('operation') + if operation not in ['set', 'remove']: + current_app.logger.info(f"Unknown requested operation change '{operation}'.") + return generate_data_validation_failure(errors=[]) + annotation_key = changeset.get('key') + annotation_value = changeset.get('value', '') + + if operation == 'set': + metadata_dict[annotation_key] = annotation_value + elif operation == 'remove': + try: + del metadata_dict[annotation_key] + except KeyError: + current_app.logger.info("No-op when removing non-existent metadata from IMS record.") + pass + # With every change made to the image_annotation_dictionary, the last thing that is necessary is + # to convert the temporary dictionary back into a list of key:value pairs. + image.metadata = metadata_dict else: - current_app.logger.info(f"{log_id} Not able to patch record field {key} with value {value}") + current_app.logger.info(f"{log_id} Not able to patch record field '{key}' with value {value}") + current_app.logger.info(f"{log_id}: '{key}', of type: %s. Is metadata? %s" % (type(key), key == 'metadata')) return generate_data_validation_failure(errors=[]) setattr(image, key, value) diff --git a/tests/ims_fixtures.py b/tests/ims_fixtures.py index 95bf9ef..eb320a6 100644 --- a/tests/ims_fixtures.py +++ b/tests/ims_fixtures.py @@ -25,10 +25,13 @@ Test Fixtures """ import os.path +import logging from fixtures import Fixture, TempDir +LOGGER = logging.getLogger(__name__) + class DataStoreFixture(Fixture): """ Test Fixture for preparing an empty data store """ @@ -51,6 +54,7 @@ class _GenericDataFixture(Fixture): def __init__(self, initial_data=None): super(_GenericDataFixture, self).__init__() + LOGGER.info("Initial Data: %s" % initial_data) self._initial_data = initial_data def _setUp(self): @@ -64,6 +68,7 @@ def _setUp(self): elif isinstance(self._initial_data, list): data = {} for record in self._initial_data: + LOGGER.info("Record: %s" % record) input_data = self.schema().load(record) data[record[self.id_field]] = input_data self.datastore.store = data diff --git a/tests/v2/ims_fixtures.py b/tests/v2/ims_fixtures.py index 4179f02..b5961cb 100644 --- a/tests/v2/ims_fixtures.py +++ b/tests/v2/ims_fixtures.py @@ -57,6 +57,9 @@ class V2ImagesDataFixture(_GenericDataFixture): class V2JobsDataFixture(_GenericDataFixture): + + + schema = V2JobRecordSchema datastore = app.data['jobs'] id_field = 'id' diff --git a/tests/v2/test_v2_images.py b/tests/v2/test_v2_images.py index 35e00c0..ea8758a 100644 --- a/tests/v2/test_v2_images.py +++ b/tests/v2/test_v2_images.py @@ -67,6 +67,7 @@ def setUp(self): 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), 'id': self.test_id, 'arch': self.test_arch, + 'metadata': {} } self.data_record_link_none = { 'name': self.getUniqueString(), @@ -74,17 +75,34 @@ def setUp(self): 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), 'id': self.test_id_link_none, 'arch': self.test_arch, + 'metadata': {} } self.data_record_no_link = { 'name': self.getUniqueString(), 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), 'id': self.test_id_no_link, 'arch': self.test_arch, + 'metadata': {} + } + self.data_record_with_metadata = { + 'name': self.getUniqueString(), + 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), + 'id': self.test_id_no_link, + 'arch': self.test_arch, + 'metadata': {'foo': 'bar'} + } + self.data_record_with_no_metadata = { + 'name': self.getUniqueString(), + 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), + 'id': self.test_id_no_link, + 'arch': self.test_arch } self.data = [ self.data_record_with_link, self.data_record_link_none, - self.data_record_no_link + self.data_record_no_link, + self.data_record_with_metadata, + self.data_record_with_no_metadata ] self.useFixture(V2ImagesDataFixture(initial_data=self.data)) @@ -261,8 +279,7 @@ def test_patch(self): self.stubber.activate() response = self.app.patch(self.test_uri_link_none, content_type='application/json', data=json.dumps(link_data)) self.stubber.deactivate() - - self.assertEqual(response.status_code, 200, 'status code was not 200') + self.assertEqual(response.status_code, 200, 'status code was not 200: data:%s response.data: %s' % (json.dumps(link_data), response.data)) response_data = json.loads(response.data) self.assertEqual(set(self.data_record_link_none.keys()).difference(response_data.keys()), set(), 'returned keys not the same') From eea4deaa82ec3e8844d615dfa1d1fba5e581091e Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Tue, 4 Jun 2024 13:25:33 -0500 Subject: [PATCH 03/25] Cleanup resolved debug command portions --- CHANGELOG.md | 3 +-- constraints.txt | 1 - src/server/models/images.py | 10 +--------- src/server/v2/resources/images.py | 5 ++--- src/server/v3/resources/images.py | 4 +--- tests/ims_fixtures.py | 5 ----- tests/v2/ims_fixtures.py | 3 --- 7 files changed, 5 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90ff868..2707d2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- CASMCMS-8915: IMS API features for tagging built images +- CASMCMS-8915: IMS API features for tagging built images. ### Dependencies - CASMCMS-8022 - update python dependencies to most recent versions. | Package | From | To | @@ -54,7 +54,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - CASMCMS-8950: Fixed loading Kubernetes configuration data in the shasta_s3_creds module - ## [3.14.0] - 2024-03-01 ### Added - CASMCMS-8795 - add remote-build-nodes API. diff --git a/constraints.txt b/constraints.txt index df25148..8a0e880 100644 --- a/constraints.txt +++ b/constraints.txt @@ -36,4 +36,3 @@ s3transfer==0.10.1 urllib3==1.26.18 # most recent 2.2.1, botocore 1.34.114 requires <1.27.0 websocket-client==1.8.0 Werkzeug==3.0.3 - diff --git a/src/server/models/images.py b/src/server/models/images.py index 0804623..acbf87c 100644 --- a/src/server/models/images.py +++ b/src/server/models/images.py @@ -35,14 +35,6 @@ from src.server.helper import ARCH_X86_64, ARCH_ARM64 -class ImageMetadata(Schema): - """ A schema specifically for validating existing image metadata instances. """ - key = fields.Str(required=True, description="An arbitrary key of metadata given for an image", - validate=Length(min=1, error="name field must not be blank")) - value = fields.Str(required=True, default="", - description="A value field given for an associated image metadata key.") - - class V2ImageRecord: """ The ImageRecord object """ @@ -74,7 +66,7 @@ class V2ImageRecordInputSchema(Schema): load_default=ARCH_X86_64, dump_default=ARCH_X86_64, metadata={"metadata": {"description": "Architecture of the image"}}) metadata = fields.Mapping(keys=fields.Str(required=True), - value=fields.Str(required=False), + value=fields.Str(required=False, default=''), desciption="User supplied additional information about an image", default={}) diff --git a/src/server/v2/resources/images.py b/src/server/v2/resources/images.py index 61ce625..94ef51e 100644 --- a/src/server/v2/resources/images.py +++ b/src/server/v2/resources/images.py @@ -209,8 +209,7 @@ def patch(self, image_id): # Validate input errors = image_patch_input_schema.validate(json_data) if errors: - current_app.logger.info("%s There was a problem validating the PATCH data: %s, json_data: %s" - % (log_id, errors, json_data)) + current_app.logger.info("%s There was a problem validating the PATCH data: %s", log_id, errors) return generate_data_validation_failure(errors) image = current_app.data["images"][image_id] @@ -261,7 +260,7 @@ def patch(self, image_id): # to convert the temporary dictionary back into a list of key:value pairs. image.metadata = metadata_dict else: - current_app.logger.info(f"{log_id} Not able to patch record field '{key}' with value {value}") + current_app.logger.info(f"{log_id} Not able to patch record field {key} with value {value}") return generate_data_validation_failure(errors=[]) setattr(image, key, value) diff --git a/src/server/v3/resources/images.py b/src/server/v3/resources/images.py index 9e82242..014a2e1 100644 --- a/src/server/v3/resources/images.py +++ b/src/server/v3/resources/images.py @@ -349,7 +349,6 @@ def patch(self, image_id): # Validate input errors = image_patch_input_schema.validate(json_data) if errors: - current_app.logger.info("PATCH data: '%s'", json_data) current_app.logger.info("%s There was a problem validating the PATCH data: %s", log_id, errors) return generate_data_validation_failure(errors) @@ -408,8 +407,7 @@ def patch(self, image_id): # to convert the temporary dictionary back into a list of key:value pairs. image.metadata = metadata_dict else: - current_app.logger.info(f"{log_id} Not able to patch record field '{key}' with value {value}") - current_app.logger.info(f"{log_id}: '{key}', of type: %s. Is metadata? %s" % (type(key), key == 'metadata')) + current_app.logger.info(f"{log_id} Not able to patch record field {key} with value {value}") return generate_data_validation_failure(errors=[]) setattr(image, key, value) diff --git a/tests/ims_fixtures.py b/tests/ims_fixtures.py index eb320a6..95bf9ef 100644 --- a/tests/ims_fixtures.py +++ b/tests/ims_fixtures.py @@ -25,13 +25,10 @@ Test Fixtures """ import os.path -import logging from fixtures import Fixture, TempDir -LOGGER = logging.getLogger(__name__) - class DataStoreFixture(Fixture): """ Test Fixture for preparing an empty data store """ @@ -54,7 +51,6 @@ class _GenericDataFixture(Fixture): def __init__(self, initial_data=None): super(_GenericDataFixture, self).__init__() - LOGGER.info("Initial Data: %s" % initial_data) self._initial_data = initial_data def _setUp(self): @@ -68,7 +64,6 @@ def _setUp(self): elif isinstance(self._initial_data, list): data = {} for record in self._initial_data: - LOGGER.info("Record: %s" % record) input_data = self.schema().load(record) data[record[self.id_field]] = input_data self.datastore.store = data diff --git a/tests/v2/ims_fixtures.py b/tests/v2/ims_fixtures.py index b5961cb..4179f02 100644 --- a/tests/v2/ims_fixtures.py +++ b/tests/v2/ims_fixtures.py @@ -57,9 +57,6 @@ class V2ImagesDataFixture(_GenericDataFixture): class V2JobsDataFixture(_GenericDataFixture): - - - schema = V2JobRecordSchema datastore = app.data['jobs'] id_field = 'id' From 077a9ff5a800ab82380a100a358c6776c72db5d6 Mon Sep 17 00:00:00 2001 From: David Laine Date: Tue, 4 Jun 2024 13:27:45 -0500 Subject: [PATCH 04/25] Fix TestV2ImageEndpoint tests. --- tests/v2/test_v2_images.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/v2/test_v2_images.py b/tests/v2/test_v2_images.py index ea8758a..dcd9c82 100644 --- a/tests/v2/test_v2_images.py +++ b/tests/v2/test_v2_images.py @@ -55,6 +55,8 @@ def setUp(self): self.test_id = str(uuid.uuid4()) self.test_id_link_none = str(uuid.uuid4()) self.test_id_no_link = str(uuid.uuid4()) + self.test_id_with_metadata = str(uuid.uuid4()) + self.test_id_without_metadata = str(uuid.uuid4()) self.test_arch = "x86_64" self.app = self.useFixture(V2FlaskTestClientFixture()).client self.data_record_with_link = { @@ -87,14 +89,14 @@ def setUp(self): self.data_record_with_metadata = { 'name': self.getUniqueString(), 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), - 'id': self.test_id_no_link, + 'id': self.test_id_with_metadata, 'arch': self.test_arch, 'metadata': {'foo': 'bar'} } self.data_record_with_no_metadata = { 'name': self.getUniqueString(), 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), - 'id': self.test_id_no_link, + 'id': self.test_id_without_metadata, 'arch': self.test_arch } self.data = [ @@ -171,6 +173,7 @@ def test_get_no_link(self): response = self.app.get(self.test_uri_no_link) self.assertEqual(response.status_code, 200, 'status code was not 200') response_data = json.loads(response.data) + print(f"{response_data}") self.assertEqual(set(self.data_record_no_link.keys()).difference(response_data.keys()), set(), 'returned keys not the same') self.assertEqual(response_data["link"], None) From 98e6ea58c17ecfb240cb4a0068c780f2f2bf3b45 Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Tue, 4 Jun 2024 13:41:53 -0500 Subject: [PATCH 05/25] Fix exptected post tests --- tests/v2/test_v2_images.py | 2 +- tests/v3/test_v3_images.py | 27 +++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/tests/v2/test_v2_images.py b/tests/v2/test_v2_images.py index dcd9c82..24c21ec 100644 --- a/tests/v2/test_v2_images.py +++ b/tests/v2/test_v2_images.py @@ -489,7 +489,7 @@ def test_post(self): r'[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\Z') self.assertIsNotNone(response_data['created'], 'image creation date/time was not set properly') self.assertItemsEqual(response_data.keys(), - ['created', 'name', 'link', 'id', 'arch'], + ['created', 'name', 'link', 'id', 'arch', 'metadata'], 'returned keys not the same') def test_post_link_none(self): diff --git a/tests/v3/test_v3_images.py b/tests/v3/test_v3_images.py index 7e85d05..ee230bd 100644 --- a/tests/v3/test_v3_images.py +++ b/tests/v3/test_v3_images.py @@ -49,6 +49,8 @@ def setUp(self): self.s3resource_stub = Stubber(app.app.s3resource.meta.client) self.test_with_link_id = str(uuid.uuid4()) + self.test_id_with_metadata = str(uuid.uuid4()) + self.test_id_without_metadata = str(uuid.uuid4()) self.test_arch = "x86_64" self.test_with_link_uri = '/v3/images/{}'.format(self.test_with_link_id) self.test_with_link_record = { @@ -61,6 +63,7 @@ def setUp(self): 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), 'id': self.test_with_link_id, 'arch': self.test_arch, + 'metadata': {} } self.test_link_none_id = str(uuid.uuid4()) @@ -71,6 +74,7 @@ def setUp(self): 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), 'id': self.test_link_none_id, 'arch': self.test_arch, + 'metadata': {} } self.test_no_link_id = str(uuid.uuid4()) @@ -80,12 +84,27 @@ def setUp(self): 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), 'id': self.test_no_link_id, 'arch': self.test_arch, + 'metadata': {} + } + self.data_record_with_metadata = { + 'name': self.getUniqueString(), + 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), + 'id': self.test_id_with_metadata, + 'arch': self.test_arch, + 'metadata': {'foo': 'bar'} + } + self.data_record_with_no_metadata = { + 'name': self.getUniqueString(), + 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), + 'id': self.test_id_without_metadata, + 'arch': self.test_arch } - self.data = [ - self.test_with_link_record, - self.test_link_none_record, - self.test_no_link_record + self.data_record_with_link, + self.data_record_link_none, + self.data_record_no_link, + self.data_record_with_metadata, + self.data_record_with_no_metadata ] self.app = self.useFixture(V3FlaskTestClientFixture()).client From ff6dd721fdd9402a78cbe93b5da812e052a9ad09 Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Tue, 4 Jun 2024 13:49:44 -0500 Subject: [PATCH 06/25] update tests for new metadata key --- tests/v2/test_v2_images.py | 4 ++-- tests/v3/test_v3_images.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/v2/test_v2_images.py b/tests/v2/test_v2_images.py index 24c21ec..9e2fd12 100644 --- a/tests/v2/test_v2_images.py +++ b/tests/v2/test_v2_images.py @@ -508,7 +508,7 @@ def test_post_link_none(self): r'[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\Z') self.assertIsNotNone(response_data['created'], 'image creation date/time was not set properly') self.assertItemsEqual(response_data.keys(), - ['created', 'name', 'link', 'id', 'arch'], + ['created', 'name', 'link', 'id', 'arch', 'metadata',], 'returned keys not the same') def test_post_no_link(self): @@ -526,7 +526,7 @@ def test_post_no_link(self): r'[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\Z') self.assertIsNotNone(response_data['created'], 'image creation date/time was not set properly') self.assertItemsEqual(response_data.keys(), - ['created', 'name', 'link', 'id', 'arch'], + ['created', 'name', 'link', 'id', 'arch', 'metadata'], 'returned keys not the same') def test_post_400_no_input(self): diff --git a/tests/v3/test_v3_images.py b/tests/v3/test_v3_images.py index ee230bd..207747f 100644 --- a/tests/v3/test_v3_images.py +++ b/tests/v3/test_v3_images.py @@ -574,7 +574,7 @@ def test_post_link_none(self): r'[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\Z') self.assertIsNotNone(response_data['created'], 'image creation date/time was not set properly') self.assertItemsEqual(response_data.keys(), - ['created', 'name', 'link', 'id', 'arch'], + ['created', 'name', 'link', 'id', 'arch', 'metadata',], 'returned keys not the same') def test_post_no_link(self): @@ -592,7 +592,7 @@ def test_post_no_link(self): r'[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\Z') self.assertIsNotNone(response_data['created'], 'image creation date/time was not set properly') self.assertItemsEqual(response_data.keys(), - ['created', 'name', 'link', 'id', 'arch'], + ['created', 'name', 'link', 'id', 'arch', 'metadata'], 'returned keys not the same') def test_post_400_no_input(self): From b7e7835ad428d52dbf5e3479b560ddb21a0e5bdb Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Tue, 4 Jun 2024 14:03:45 -0500 Subject: [PATCH 07/25] Add test cases and honor the format of the new test cases in v3 --- tests/v3/test_v3_images.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/v3/test_v3_images.py b/tests/v3/test_v3_images.py index 207747f..3283a20 100644 --- a/tests/v3/test_v3_images.py +++ b/tests/v3/test_v3_images.py @@ -49,8 +49,6 @@ def setUp(self): self.s3resource_stub = Stubber(app.app.s3resource.meta.client) self.test_with_link_id = str(uuid.uuid4()) - self.test_id_with_metadata = str(uuid.uuid4()) - self.test_id_without_metadata = str(uuid.uuid4()) self.test_arch = "x86_64" self.test_with_link_uri = '/v3/images/{}'.format(self.test_with_link_id) self.test_with_link_record = { @@ -86,14 +84,19 @@ def setUp(self): 'arch': self.test_arch, 'metadata': {} } - self.data_record_with_metadata = { + self.test_data_record_with_metadata_id = str(uuid.uuid4()) + self.test_data_record_with_metadata_uri = '/v3/images/{}'.format(self.test_data_record_with_metadata_id) + self.test_data_record_with_metadata_record = { 'name': self.getUniqueString(), 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), - 'id': self.test_id_with_metadata, + 'id': self.test_data_record_with_metadata_id, 'arch': self.test_arch, 'metadata': {'foo': 'bar'} } - self.data_record_with_no_metadata = { + + self.data_record_with_no_metadata_id = str(uuid.uuid4()) + self.data_record_with_no_metadata_uri = '/v3/images/{}'.format(self.data_record_with_no_metadata_id) + self.data_record_with_no_metadata_record = { 'name': self.getUniqueString(), 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), 'id': self.test_id_without_metadata, From 19f124ffef3eea7eb5e4886d2dfc35fdf0c6ae55 Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Tue, 4 Jun 2024 14:47:44 -0500 Subject: [PATCH 08/25] reconcile missing metadata from v3 tests --- tests/v2/test_v2_images.py | 1 - tests/v3/test_v3_deleted_images.py | 2 ++ tests/v3/test_v3_images.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/v2/test_v2_images.py b/tests/v2/test_v2_images.py index 9e2fd12..13a63af 100644 --- a/tests/v2/test_v2_images.py +++ b/tests/v2/test_v2_images.py @@ -173,7 +173,6 @@ def test_get_no_link(self): response = self.app.get(self.test_uri_no_link) self.assertEqual(response.status_code, 200, 'status code was not 200') response_data = json.loads(response.data) - print(f"{response_data}") self.assertEqual(set(self.data_record_no_link.keys()).difference(response_data.keys()), set(), 'returned keys not the same') self.assertEqual(response_data["link"], None) diff --git a/tests/v3/test_v3_deleted_images.py b/tests/v3/test_v3_deleted_images.py index fb1ddf9..7eb98ff 100644 --- a/tests/v3/test_v3_deleted_images.py +++ b/tests/v3/test_v3_deleted_images.py @@ -79,6 +79,7 @@ def setUp(self): 'id': self.test_with_link_id, 'name': self.getUniqueString(), 'arch': "x86_64", + 'metadata': {}, 'link': { 'path': 's3://boot-images/{}/manifest.json'.format(self.test_with_link_id), 'etag': self.getUniqueString(), @@ -93,6 +94,7 @@ def setUp(self): self.test_no_link_record = { 'id': self.test_no_link_id, 'name': self.getUniqueString(), + 'metadata': {}, 'arch': "x86_64", 'link': None, 'created': (datetime.now() - timedelta(days=77)).replace(microsecond=0).isoformat(), diff --git a/tests/v3/test_v3_images.py b/tests/v3/test_v3_images.py index 3283a20..ff15091 100644 --- a/tests/v3/test_v3_images.py +++ b/tests/v3/test_v3_images.py @@ -99,7 +99,7 @@ def setUp(self): self.data_record_with_no_metadata_record = { 'name': self.getUniqueString(), 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), - 'id': self.test_id_without_metadata, + 'id': self.data_record_with_no_metadata_id, 'arch': self.test_arch } self.data = [ From 7886312e4cf6471fa58ded42b05be5e15cfc7529 Mon Sep 17 00:00:00 2001 From: David Laine Date: Tue, 4 Jun 2024 14:52:06 -0500 Subject: [PATCH 09/25] Update v3 tests. --- src/server/v3/models/images.py | 4 ++-- tests/v3/test_v3_deleted_images.py | 1 + tests/v3/test_v3_images.py | 10 +++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/server/v3/models/images.py b/src/server/v3/models/images.py index 65aa802..753e2eb 100644 --- a/src/server/v3/models/images.py +++ b/src/server/v3/models/images.py @@ -34,10 +34,10 @@ class V3DeletedImageRecord(V2ImageRecord): """ The ImageRecord object """ # pylint: disable=W0622 - def __init__(self, name, link=None, id=None, created=None, deleted=None, arch="x86_64"): + def __init__(self, name, link=None, id=None, created=None, deleted=None, arch="x86_64", metadata=None): # Supplied self.deleted = deleted or datetime.datetime.now() - super().__init__(name, link=link, id=id, created=created, arch=arch) + super().__init__(name, link=link, id=id, created=created, arch=arch, metadata=metadata) def __repr__(self): return ''.format(self=self) diff --git a/tests/v3/test_v3_deleted_images.py b/tests/v3/test_v3_deleted_images.py index 7eb98ff..34e517a 100644 --- a/tests/v3/test_v3_deleted_images.py +++ b/tests/v3/test_v3_deleted_images.py @@ -96,6 +96,7 @@ def setUp(self): 'name': self.getUniqueString(), 'metadata': {}, 'arch': "x86_64", + 'metadata' : {}, 'link': None, 'created': (datetime.now() - timedelta(days=77)).replace(microsecond=0).isoformat(), 'deleted': datetime.now().replace(microsecond=0).isoformat(), diff --git a/tests/v3/test_v3_images.py b/tests/v3/test_v3_images.py index ff15091..39c1be9 100644 --- a/tests/v3/test_v3_images.py +++ b/tests/v3/test_v3_images.py @@ -103,11 +103,11 @@ def setUp(self): 'arch': self.test_arch } self.data = [ - self.data_record_with_link, - self.data_record_link_none, - self.data_record_no_link, - self.data_record_with_metadata, - self.data_record_with_no_metadata + self.test_with_link_record, + self.test_link_none_record, + self.test_no_link_record, + self.test_data_record_with_metadata_record, + self.data_record_with_no_metadata_record ] self.app = self.useFixture(V3FlaskTestClientFixture()).client From fb4d95666cccb72708d3753c1327e39145e32c1c Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Tue, 4 Jun 2024 14:56:57 -0500 Subject: [PATCH 10/25] passthrough metadata to v3 schema --- src/server/v3/models/images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/v3/models/images.py b/src/server/v3/models/images.py index 753e2eb..930e1f4 100644 --- a/src/server/v3/models/images.py +++ b/src/server/v3/models/images.py @@ -69,7 +69,7 @@ class V3DeletedImageRecordSchema(V3DeletedImageRecordInputSchema): class V3DeletedImageRecordPatchSchema(Schema): """ - Schema for a updating an ImageRecord object. + Schema for updating an ImageRecord object. """ operation = fields.Str(required=True, metadata={"metadata": {"description": "The operation or action that should be taken on the image record. " From aec0e1a0689813db5a53ecd870b21c4a0e057d76 Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Tue, 4 Jun 2024 15:00:30 -0500 Subject: [PATCH 11/25] add missing metadata field --- tests/v3/test_v3_images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/v3/test_v3_images.py b/tests/v3/test_v3_images.py index 39c1be9..2ff61bf 100644 --- a/tests/v3/test_v3_images.py +++ b/tests/v3/test_v3_images.py @@ -558,7 +558,7 @@ def test_post(self): r'[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\Z') self.assertIsNotNone(response_data['created'], 'image creation date/time was not set properly') self.assertItemsEqual(response_data.keys(), - ['created', 'name', 'link', 'id', 'arch'], + ['created', 'name', 'link', 'id', 'arch', 'metadata'], 'returned keys not the same') def test_post_link_none(self): From 971fd59116b63b58b1817953094cb38d3b95870b Mon Sep 17 00:00:00 2001 From: David Laine Date: Tue, 4 Jun 2024 15:46:33 -0500 Subject: [PATCH 12/25] Fix some deprecation warnings. --- src/server/models/images.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/server/models/images.py b/src/server/models/images.py index acbf87c..45a95ea 100644 --- a/src/server/models/images.py +++ b/src/server/models/images.py @@ -66,9 +66,9 @@ class V2ImageRecordInputSchema(Schema): load_default=ARCH_X86_64, dump_default=ARCH_X86_64, metadata={"metadata": {"description": "Architecture of the image"}}) metadata = fields.Mapping(keys=fields.Str(required=True), - value=fields.Str(required=False, default=''), - desciption="User supplied additional information about an image", - default={}) + value=fields.Str(required=False, dump_default='', load_default=''), + metadata={"metadata": {"description":"User supplied additional information about an image"}}, + dump_default={}, load_default={}) @post_load def make_image(self, data, many, partial): @@ -91,10 +91,10 @@ class V2ImageRecordSchema(V2ImageRecordInputSchema): class V2ImageRecordMetadataPatchSchema(Schema): - operation = fields.Str(required=True, description="A method for how to change a metadata struct.", + operation = fields.Str(required=True, metadata={"metadata": {"description":"A method for how to change a metadata struct."}}, validate=OneOf(['set', 'remove'])) - key = fields.Str(required=True, description="The metadata key that is to be affected.") - value = fields.Str(required=False, description="The value to store for the provided key.") + key = fields.Str(required=True, metadata={"metadata": {"description":"The metadata key that is to be affected."}}) + value = fields.Str(required=False, metadata={"metadata": {"description":"The value to store for the provided key."}}) class V2ImageRecordPatchSchema(Schema): From ccab3519714818b773cf1f47b6ebac2f1e6d705a Mon Sep 17 00:00:00 2001 From: David Laine Date: Tue, 4 Jun 2024 15:57:30 -0500 Subject: [PATCH 13/25] Fix last of the deprecation warnings. --- src/server/models/images.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/models/images.py b/src/server/models/images.py index 45a95ea..a4c7242 100644 --- a/src/server/models/images.py +++ b/src/server/models/images.py @@ -66,7 +66,7 @@ class V2ImageRecordInputSchema(Schema): load_default=ARCH_X86_64, dump_default=ARCH_X86_64, metadata={"metadata": {"description": "Architecture of the image"}}) metadata = fields.Mapping(keys=fields.Str(required=True), - value=fields.Str(required=False, dump_default='', load_default=''), + values=fields.Str(required=False, dump_default='', load_default=''), metadata={"metadata": {"description":"User supplied additional information about an image"}}, dump_default={}, load_default={}) @@ -107,7 +107,7 @@ class V2ImageRecordPatchSchema(Schema): load_default=ARCH_X86_64, dump_default=ARCH_X86_64, metadata={"metadata": {"description": "Architecture of the recipe"}}) metadata = fields.List(fields.Nested(V2ImageRecordMetadataPatchSchema()), - default=[], + load_default=[], dump_default=[], required=False, - description="A list of change operations to perform on Image Metadata.") + metadata={"metadata": {"description":"A list of change operations to perform on Image Metadata."}}) From 0f53de8183327b3115ed8f1147fa51ee4b8d8153 Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Fri, 7 Jun 2024 10:15:51 -0500 Subject: [PATCH 14/25] output raw fileformat in image get. --- src/server/models/images.py | 7 ++++--- src/server/v2/resources/images.py | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/server/models/images.py b/src/server/models/images.py index a4c7242..1c6c148 100644 --- a/src/server/models/images.py +++ b/src/server/models/images.py @@ -62,12 +62,12 @@ class V2ImageRecordInputSchema(Schema): metadata={"metadata": {"description": "Name of the image"}}) link = fields.Nested(ArtifactLink, required=False, allow_none=True, metadata={"metadata": {"description": "Location of the image manifest"}}) - arch = fields.Str(required=False, validate=OneOf([ARCH_ARM64,ARCH_X86_64]), + arch = fields.Str(required=False, validate=OneOf([ARCH_ARM64, ARCH_X86_64]), load_default=ARCH_X86_64, dump_default=ARCH_X86_64, metadata={"metadata": {"description": "Architecture of the image"}}) metadata = fields.Mapping(keys=fields.Str(required=True), values=fields.Str(required=False, dump_default='', load_default=''), - metadata={"metadata": {"description":"User supplied additional information about an image"}}, + metadata={"metadata": {"description": "User supplied additional information about an image"}}, dump_default={}, load_default={}) @post_load @@ -91,7 +91,8 @@ class V2ImageRecordSchema(V2ImageRecordInputSchema): class V2ImageRecordMetadataPatchSchema(Schema): - operation = fields.Str(required=True, metadata={"metadata": {"description":"A method for how to change a metadata struct."}}, + operation = fields.Str(required=True, + metadata={"metadata": {"description": "A method for how to change a metadata struct."}}, validate=OneOf(['set', 'remove'])) key = fields.Str(required=True, metadata={"metadata": {"description":"The metadata key that is to be affected."}}) value = fields.Str(required=False, metadata={"metadata": {"description":"The value to store for the provided key."}}) diff --git a/src/server/v2/resources/images.py b/src/server/v2/resources/images.py index 94ef51e..386867b 100644 --- a/src/server/v2/resources/images.py +++ b/src/server/v2/resources/images.py @@ -84,6 +84,7 @@ def get(self): """ retrieve a list/collection of images """ log_id = get_log_id() current_app.logger.info("%s ++ images.v2.GET", log_id) + current_app.logger.info("%s ++ images.v2.GET RAW:%s" % (log_id, current_app.data["images"].values())) return_json = image_schema.dump(iter(current_app.data["images"].values()), many=True) current_app.logger.info("%s Returning json response: %s", log_id, return_json) return jsonify(return_json) From c8bbae2470a1ca4508245f745b0a52e9ed0be867 Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Fri, 7 Jun 2024 10:16:13 -0500 Subject: [PATCH 15/25] Additional logging to discover how current_app.data stores image information --- src/server/v2/resources/images.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/v2/resources/images.py b/src/server/v2/resources/images.py index 386867b..8cf2f57 100644 --- a/src/server/v2/resources/images.py +++ b/src/server/v2/resources/images.py @@ -168,6 +168,7 @@ def get(self, image_id): current_app.logger.info("%s no IMS image record matches image_id=%s", log_id, image_id) return generate_resource_not_found_response() + current_app.logger.info("%s ++ images.v2.GET Raw: %s" % (log_id, current_app.data['images'][image_id])) return_json = image_schema.dump(current_app.data['images'][image_id]) current_app.logger.info("%s Returning json response: %s", log_id, return_json) return jsonify(return_json) From 8bf637c3e393412fc72070ed083ac542284d1924 Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Tue, 11 Jun 2024 14:47:04 -0500 Subject: [PATCH 16/25] Do not corrupt IMS image database on failed writes + tests --- src/server/models/images.py | 3 +- src/server/v2/resources/images.py | 35 +++++------ tests/v2/test_v2_images.py | 99 +++++++++++++++++++++++++------ 3 files changed, 97 insertions(+), 40 deletions(-) diff --git a/src/server/models/images.py b/src/server/models/images.py index 1c6c148..15f1d45 100644 --- a/src/server/models/images.py +++ b/src/server/models/images.py @@ -104,11 +104,10 @@ class V2ImageRecordPatchSchema(Schema): """ link = fields.Nested(ArtifactLink, required=False, allow_none=False, metadata={"metadata": {"description": "Location of the image manifest"}}) - arch = fields.Str(required=False, validate=OneOf([ARCH_ARM64,ARCH_X86_64]), + arch = fields.Str(required=False, validate=OneOf([ARCH_ARM64, ARCH_X86_64]), load_default=ARCH_X86_64, dump_default=ARCH_X86_64, metadata={"metadata": {"description": "Architecture of the recipe"}}) metadata = fields.List(fields.Nested(V2ImageRecordMetadataPatchSchema()), - load_default=[], dump_default=[], required=False, metadata={"metadata": {"description":"A list of change operations to perform on Image Metadata."}}) diff --git a/src/server/v2/resources/images.py b/src/server/v2/resources/images.py index 8cf2f57..352f393 100644 --- a/src/server/v2/resources/images.py +++ b/src/server/v2/resources/images.py @@ -29,7 +29,6 @@ from flask import jsonify, request, current_app from flask_restful import Resource -from copy import deepcopy from src.server.errors import problemify, generate_missing_input_response, generate_data_validation_failure, \ generate_resource_not_found_response, generate_patch_conflict @@ -209,6 +208,7 @@ def patch(self, image_id): return generate_missing_input_response() # Validate input + current_app.logger.info("%s image patch json_value: %s" % (log_id, json_data)) errors = image_patch_input_schema.validate(json_data) if errors: current_app.logger.info("%s There was a problem validating the PATCH data: %s", log_id, errors) @@ -229,43 +229,36 @@ def patch(self, image_id): if problem: current_app.logger.info("%s Could not validate link artifact or artifact doesn't exist", log_id) return problem + setattr(image, key, value) elif key == "arch": current_app.logger.info(f"Patching architecture with {value}") image.arch = value + setattr(image, key, value) elif key == 'metadata': - if not value: - current_app.logger.info("No metadata values to patch.") - continue - # Even though the API represents Image Metadata Annotations as a list internally, they behave like - # dictionaries. The ordered nature of the data should not matter, nor are they enforced. As such, - # converting the list of k:vs to a unified dictionary has performance advantages log(n) when doing - # multiple insertions or deletions. We will flatten this back out to a list before setting it within - # the image. - metadata_dict = deepcopy(image.metadata) + # The API represents metadata keys as a dictionary, but the patchset is provided as a list of + # changes that need to be applied to the metadata itself. for changeset in value: operation = changeset.get('operation') + annotation_key = changeset.get('key') + annotation_value = changeset.get('value', '') + current_app.logger.info("Image Patch changeset: Current: %s -> %s %s %s" + % (image.metadata, operation, annotation_key, annotation_value)) if operation not in ['set', 'remove']: current_app.logger.info(f"Unknown requested operation change '{operation}'.") return generate_data_validation_failure(errors=[]) - annotation_key = changeset.get('key') - annotation_value = changeset.get('value', '') - + current_app.logger.info("Image Patch changeset: %s %s %s" % (operation, annotation_key, annotation_value)) if operation == 'set': - metadata_dict[annotation_key] = annotation_value + image.metadata[annotation_key] = annotation_value elif operation == 'remove': try: - del metadata_dict[annotation_key] + del image.metadata[annotation_key] except KeyError: current_app.logger.info("No-op when removing non-existent metadata from IMS record.") - pass - # With every change made to the image_annotation_dictionary, the last thing that is necessary is - # to convert the temporary dictionary back into a list of key:value pairs. - image.metadata = metadata_dict + current_app.logger.info("Image metadata result: %s" %(image.metadata)) else: current_app.logger.info(f"{log_id} Not able to patch record field {key} with value {value}") return generate_data_validation_failure(errors=[]) - - setattr(image, key, value) + current_app.logger.info(f"{log_id} image metadata information dump: '%s'" % image.metadata) current_app.data['images'][image_id] = image return_json = image_schema.dump(current_app.data['images'][image_id]) diff --git a/tests/v2/test_v2_images.py b/tests/v2/test_v2_images.py index 13a63af..1bea31c 100644 --- a/tests/v2/test_v2_images.py +++ b/tests/v2/test_v2_images.py @@ -52,13 +52,12 @@ def setUpClass(cls): def setUp(self): super(TestV2ImageEndpoint, self).setUp() - self.test_id = str(uuid.uuid4()) - self.test_id_link_none = str(uuid.uuid4()) - self.test_id_no_link = str(uuid.uuid4()) - self.test_id_with_metadata = str(uuid.uuid4()) - self.test_id_without_metadata = str(uuid.uuid4()) self.test_arch = "x86_64" self.app = self.useFixture(V2FlaskTestClientFixture()).client + + # Default fixture + self.test_id = str(uuid.uuid4()) + self.test_uri_with_link = '/images/{}'.format(self.test_id) self.data_record_with_link = { 'name': self.getUniqueString(), 'link': { @@ -71,6 +70,9 @@ def setUp(self): 'arch': self.test_arch, 'metadata': {} } + # Fixture without a link + self.test_id_link_none = str(uuid.uuid4()) + self.test_uri_link_none = '/images/{}'.format(self.test_id_link_none) self.data_record_link_none = { 'name': self.getUniqueString(), 'link': None, @@ -79,6 +81,9 @@ def setUp(self): 'arch': self.test_arch, 'metadata': {} } + # Test Fixtures with no set link + self.test_id_no_link = str(uuid.uuid4()) + self.test_uri_no_link = '/images/{}'.format(self.test_id_no_link) self.data_record_no_link = { 'name': self.getUniqueString(), 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), @@ -86,19 +91,37 @@ def setUp(self): 'arch': self.test_arch, 'metadata': {} } + # with_metadata; allows for testing records with stored metadata information + self.test_id_with_metadata = str(uuid.uuid4()) + self.test_uri_with_metadata = '/images/{}'.format(self.test_id_no_link) self.data_record_with_metadata = { 'name': self.getUniqueString(), 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), 'id': self.test_id_with_metadata, 'arch': self.test_arch, - 'metadata': {'foo': 'bar'} + 'metadata': {'foo': 'bar'}, + 'link': { + 'path': 's3://boot-images/{}/manifest.json'.format(self.test_id_with_metadata), + 'etag': self.getUniqueString(), + 'type': 's3' + }, } + # with_no_metadata; allows for testing existing data structures on system + self.test_id_with_no_metadata = str(uuid.uuid4()) + self.test_uri_with_no_metadata = '/images/{}'.format(self.test_id_with_no_metadata) self.data_record_with_no_metadata = { 'name': self.getUniqueString(), 'created': datetime.datetime.now().replace(microsecond=0).isoformat(), - 'id': self.test_id_without_metadata, - 'arch': self.test_arch + 'id': self.test_id_with_no_metadata, + 'arch': self.test_arch, + 'link': { + 'path': 's3://boot-images/{}/manifest.json'.format(self.test_id_with_no_metadata), + 'etag': self.getUniqueString(), + 'type': 's3' + }, } + # Test Fixture for link cascades + self.test_uri_with_link_cascade_false = '/images/{}?cascade=False'.format(self.test_id) self.data = [ self.data_record_with_link, self.data_record_link_none, @@ -106,13 +129,7 @@ def setUp(self): self.data_record_with_metadata, self.data_record_with_no_metadata ] - self.useFixture(V2ImagesDataFixture(initial_data=self.data)) - self.test_uri_with_link = '/images/{}'.format(self.test_id) - self.test_uri_with_link_cascade_false = '/images/{}?cascade=False'.format(self.test_id) - self.test_uri_link_none = '/images/{}'.format(self.test_id_link_none) - self.test_uri_no_link = '/images/{}'.format(self.test_id_no_link) - self.s3_manifest_data = { "version": "1.0", "created": "2020-01-14 03:17:14", @@ -359,10 +376,12 @@ def test_patch_same_link(self): def test_patch_change_arch(self): """ Test that we're able to patch a record with a new architecture""" - test_archs = ['x86_64','aarch64','x86_64'] + test_archs = ['x86_64', 'aarch64', 'x86_64'] for arch in test_archs: patch_data = {'arch': arch} - response = self.app.patch(self.test_uri_link_none, content_type='application/json', data=json.dumps(patch_data)) + response = self.app.patch(self.test_uri_link_none, + content_type='application/json', + data=json.dumps(patch_data)) self.assertEqual(response.status_code, 200, 'status code was not 200') response_data = json.loads(response.data) @@ -380,9 +399,55 @@ def test_patch_change_arch(self): self.assertEqual(response_data[key], patch_data['arch'], 'resource field "{}" returned was not equal'.format(key)) else: - self.assertEqual(response_data[key], self.data_record_link_none[key], + self.assertEqual(response_data[key], + self.data_record_link_none[key], 'resource field "{}" returned was not equal'.format(key)) + def test_patch_set_metadata(self): + test_kv_pairs = [('foo', 'bar'), ('projected', 'image')] + for key_val, val_val in test_kv_pairs: + patch_data = {'metadata': [{'operation': 'set', 'key': key_val, 'value': val_val}]} + response = self.app.patch(self.test_uri_link_none, + content_type='application/json', + data=json.dumps(patch_data)) + self.assertEqual(response.status_code, 200, 'status code was not 200') + + def test_patch_remove_metadata(self): + patch_data = {'metadata': [{'operation': 'remove', 'key': 'key'}]} + response = self.app.patch(self.data_record_with_metadata, + content_type='application/json', + data=json.dumps(patch_data)) + self.assertEqual(response.status_code, 200, 'status code was not 200') + + def test_patch_remove_metadata_idempotent(self): + patch_data = {'metadata': [{'operation': 'remove', 'key': 'key'}]} + response = self.app.patch(self.data_record_with_no_metadata, + content_type='application/json', + data=json.dumps(patch_data)) + self.assertEqual(response.status_code, 200, 'status code was not 200') + + def test_patch_multiple_changesets_single_patch(self): + patch_data = {'metadata': []} + for operation in ('set', 'remove', 'set', 'remove', 'remove'): + patch_data['metadata'].append({'operation': operation, 'key': 'foo', 'value': 'bar'}) + for test_uri in (self.test_uri_no_link, + self.test_uri_with_link, + self.test_uri_link_none, +# self.test_uri_with_metadata, +# self.test_uri_with_no_metadata + ): + response = self.app.patch(test_uri, + content_type='application/json', + data=json.dumps(patch_data)) + self.assertEqual(response.status_code, 200, 'status code was not 200') + response = self.app.get(test_uri) + response_data = json.loads(response.data) + self.assertEqual(response.status_code, 200, 'unable to retrieve after a patch') + # We ended with a remove, so there should be no remaining metadata + self.assertEqual(response_data.metadata.keys(), 0, + 'Unable to remove set keys: %s' % (response_data.metadata.keys())) + + class TestV2ImagesCollectionEndpoint(TestCase): """ Test the images/ collection endpoint (ims.v2.resources.images.ImagesCollection) From a9234d98d8ce6b66372f9e96a8d77aee7f8bd3af Mon Sep 17 00:00:00 2001 From: David Laine Date: Tue, 11 Jun 2024 15:46:32 -0500 Subject: [PATCH 17/25] Fix tests. --- tests/v2/test_v2_images.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/v2/test_v2_images.py b/tests/v2/test_v2_images.py index 1bea31c..32ec17a 100644 --- a/tests/v2/test_v2_images.py +++ b/tests/v2/test_v2_images.py @@ -414,14 +414,14 @@ def test_patch_set_metadata(self): def test_patch_remove_metadata(self): patch_data = {'metadata': [{'operation': 'remove', 'key': 'key'}]} - response = self.app.patch(self.data_record_with_metadata, + response = self.app.patch(self.test_uri_with_metadata, content_type='application/json', data=json.dumps(patch_data)) self.assertEqual(response.status_code, 200, 'status code was not 200') def test_patch_remove_metadata_idempotent(self): patch_data = {'metadata': [{'operation': 'remove', 'key': 'key'}]} - response = self.app.patch(self.data_record_with_no_metadata, + response = self.app.patch(self.test_uri_with_no_metadata, content_type='application/json', data=json.dumps(patch_data)) self.assertEqual(response.status_code, 200, 'status code was not 200') @@ -433,8 +433,8 @@ def test_patch_multiple_changesets_single_patch(self): for test_uri in (self.test_uri_no_link, self.test_uri_with_link, self.test_uri_link_none, -# self.test_uri_with_metadata, -# self.test_uri_with_no_metadata + self.test_uri_with_metadata, + self.test_uri_with_no_metadata ): response = self.app.patch(test_uri, content_type='application/json', @@ -444,8 +444,8 @@ def test_patch_multiple_changesets_single_patch(self): response_data = json.loads(response.data) self.assertEqual(response.status_code, 200, 'unable to retrieve after a patch') # We ended with a remove, so there should be no remaining metadata - self.assertEqual(response_data.metadata.keys(), 0, - 'Unable to remove set keys: %s' % (response_data.metadata.keys())) + self.assertEqual(len(response_data['metadata'].keys()), 0, + 'Unable to remove set keys: %s' % (response_data['metadata'].keys())) class TestV2ImagesCollectionEndpoint(TestCase): From 5d02c69135a5c11fe7d1a3e131315862fa2b8d35 Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Wed, 12 Jun 2024 13:48:58 -0500 Subject: [PATCH 18/25] Clean up unit test verbosity and port to v3 images --- src/server/v2/resources/images.py | 8 +++---- src/server/v3/resources/images.py | 40 +++++++++++++------------------ tests/v3/test_v3_images.py | 4 ++-- 3 files changed, 23 insertions(+), 29 deletions(-) diff --git a/src/server/v2/resources/images.py b/src/server/v2/resources/images.py index 352f393..d3dbf41 100644 --- a/src/server/v2/resources/images.py +++ b/src/server/v2/resources/images.py @@ -241,12 +241,12 @@ def patch(self, image_id): operation = changeset.get('operation') annotation_key = changeset.get('key') annotation_value = changeset.get('value', '') - current_app.logger.info("Image Patch changeset: Current: %s -> %s %s %s" + current_app.logger.debug("Image Patch changeset: Current: %s -> %s %s %s" % (image.metadata, operation, annotation_key, annotation_value)) if operation not in ['set', 'remove']: current_app.logger.info(f"Unknown requested operation change '{operation}'.") return generate_data_validation_failure(errors=[]) - current_app.logger.info("Image Patch changeset: %s %s %s" % (operation, annotation_key, annotation_value)) + current_app.logger.debug("Image Patch changeset: %s %s %s" % (operation, annotation_key, annotation_value)) if operation == 'set': image.metadata[annotation_key] = annotation_value elif operation == 'remove': @@ -254,7 +254,7 @@ def patch(self, image_id): del image.metadata[annotation_key] except KeyError: current_app.logger.info("No-op when removing non-existent metadata from IMS record.") - current_app.logger.info("Image metadata result: %s" %(image.metadata)) + current_app.logger.debug("Image metadata result: %s" %(image.metadata)) else: current_app.logger.info(f"{log_id} Not able to patch record field {key} with value {value}") return generate_data_validation_failure(errors=[]) @@ -262,5 +262,5 @@ def patch(self, image_id): current_app.data['images'][image_id] = image return_json = image_schema.dump(current_app.data['images'][image_id]) - current_app.logger.info("%s Returning json response: %s", log_id, return_json) + current_app.logger.debug("%s Returning json response: %s", log_id, return_json) return jsonify(return_json) diff --git a/src/server/v3/resources/images.py b/src/server/v3/resources/images.py index 014a2e1..3f7dc88 100644 --- a/src/server/v3/resources/images.py +++ b/src/server/v3/resources/images.py @@ -374,47 +374,41 @@ def patch(self, image_id): current_app.logger.info(f"The artifact {value} is not in S3 and " f"was not soft-deleted. Ignoring") current_app.logger.info(str(exc)) + setattr(image, key, value) elif key == "arch": current_app.logger.info(f"Patching architecture with {value}") image.arch = value + setattr(image, key, value) elif key == 'metadata': - if not value: - current_app.logger.info("No metadata values to patch.") - continue - # Even though the API represents Image Metadata Annotations as a list internally, they behave like - # dictionaries. The ordered nature of the data should not matter, nor are they enforced. As such, - # converting the list of k:vs to a unified dictionary has performance advantages log(n) when doing - # multiple insertions or deletions. We will flatten this back out to a list before setting it within - # the image. - metadata_dict = deepcopy(image.metadata) + # The API represents metadata keys as a dictionary, but the patchset is provided as a list of + # changes that need to be applied to the metadata itself. for changeset in value: operation = changeset.get('operation') + annotation_key = changeset.get('key') + annotation_value = changeset.get('value', '') + current_app.logger.debug("Image Patch changeset: Current: %s -> %s %s %s" + % (image.metadata, operation, annotation_key, annotation_value)) if operation not in ['set', 'remove']: current_app.logger.info(f"Unknown requested operation change '{operation}'.") return generate_data_validation_failure(errors=[]) - annotation_key = changeset.get('key') - annotation_value = changeset.get('value', '') - + current_app.logger.debug( + "Image Patch changeset: %s %s %s" % (operation, annotation_key, annotation_value)) if operation == 'set': - metadata_dict[annotation_key] = annotation_value + image.metadata[annotation_key] = annotation_value elif operation == 'remove': try: - del metadata_dict[annotation_key] + del image.metadata[annotation_key] except KeyError: current_app.logger.info("No-op when removing non-existent metadata from IMS record.") - pass - # With every change made to the image_annotation_dictionary, the last thing that is necessary is - # to convert the temporary dictionary back into a list of key:value pairs. - image.metadata = metadata_dict + current_app.logger.debug("Image metadata result: %s" % (image.metadata)) else: current_app.logger.info(f"{log_id} Not able to patch record field {key} with value {value}") return generate_data_validation_failure(errors=[]) + current_app.logger.info(f"{log_id} image metadata information dump: '%s'" % image.metadata) + current_app.data['images'][image_id] = image - setattr(image, key, value) - current_app.data[self.images_table][image_id] = image - - return_json = image_schema.dump(current_app.data[self.images_table][image_id]) - current_app.logger.info("%s Returning json response: %s", log_id, return_json) + return_json = image_schema.dump(current_app.data['images'][image_id]) + current_app.logger.debug("%s Returning json response: %s", log_id, return_json) return jsonify(return_json) diff --git a/tests/v3/test_v3_images.py b/tests/v3/test_v3_images.py index 2ff61bf..2fdbf74 100644 --- a/tests/v3/test_v3_images.py +++ b/tests/v3/test_v3_images.py @@ -428,8 +428,8 @@ def test_patch(self): DATETIME_STRING), delta=datetime.timedelta(seconds=5)) elif key == 'link': - self.assertEqual(response_data[key], link_data['link'], - 'resource field "{}" returned was not equal'.format(key)) + self.assertEqual(response_data[key], link_data[key], + 'resource field "{}" returned was not equal: {} != {}'.format(key, response_data[key], link_data[key])) else: self.assertEqual(response_data[key], self.test_link_none_record[key], 'resource field "{}" returned was not equal'.format(key)) From 7ed5742d54c674ccc19861bdec19546fa4a2575d Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Wed, 12 Jun 2024 13:53:24 -0500 Subject: [PATCH 19/25] Remove IDE specific files; add them to git ignore --- .gitignore | 4 +++- .idea/ims.iml | 8 -------- .idea/inspectionProfiles/Project_Default.xml | 14 -------------- .idea/inspectionProfiles/profiles_settings.xml | 6 ------ .idea/modules.xml | 8 -------- 5 files changed, 3 insertions(+), 37 deletions(-) delete mode 100644 .idea/ims.iml delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml delete mode 100644 .idea/modules.xml diff --git a/.gitignore b/.gitignore index 3425e8d..4c581c4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ myimage.yaml *.pyc .version .env -.vscode \ No newline at end of file +.vscode +.idea/* +.idea/inspectionProfiles/* diff --git a/.idea/ims.iml b/.idea/ims.iml deleted file mode 100644 index d0876a7..0000000 --- a/.idea/ims.iml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index c662325..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 2ba2cd2..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file From 320e94c045474a9d6df68c5afd7a3f43b1623da2 Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Wed, 12 Jun 2024 13:58:22 -0500 Subject: [PATCH 20/25] Remove twice included 'metadata' key from test fixture --- tests/v3/test_v3_deleted_images.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/v3/test_v3_deleted_images.py b/tests/v3/test_v3_deleted_images.py index 34e517a..7eb98ff 100644 --- a/tests/v3/test_v3_deleted_images.py +++ b/tests/v3/test_v3_deleted_images.py @@ -96,7 +96,6 @@ def setUp(self): 'name': self.getUniqueString(), 'metadata': {}, 'arch': "x86_64", - 'metadata' : {}, 'link': None, 'created': (datetime.now() - timedelta(days=77)).replace(microsecond=0).isoformat(), 'deleted': datetime.now().replace(microsecond=0).isoformat(), From 29c7133b13da15c457858323c4b5a48af9a5b50c Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Wed, 12 Jun 2024 14:44:36 -0500 Subject: [PATCH 21/25] Bump copyright years --- api/openapi.yaml | 2 +- src/server/models/images.py | 2 +- src/server/v2/resources/images.py | 2 +- src/server/v3/resources/images.py | 2 +- tests/ims_fixtures.py | 2 +- tests/v2/ims_fixtures.py | 2 +- tests/v2/test_v2_images.py | 2 +- tests/v3/test_v3_deleted_images.py | 2 +- tests/v3/test_v3_images.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api/openapi.yaml b/api/openapi.yaml index 3f89a3d..114aef5 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2021-2023 Hewlett Packard Enterprise Development LP +# (C) Copyright 2021-2024 Hewlett Packard Enterprise Development LP # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), diff --git a/src/server/models/images.py b/src/server/models/images.py index 15f1d45..74185be 100644 --- a/src/server/models/images.py +++ b/src/server/models/images.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2018-2023 Hewlett Packard Enterprise Development LP +# (C) Copyright 2018-2024 Hewlett Packard Enterprise Development LP # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), diff --git a/src/server/v2/resources/images.py b/src/server/v2/resources/images.py index d3dbf41..a8f23df 100644 --- a/src/server/v2/resources/images.py +++ b/src/server/v2/resources/images.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2018-2023 Hewlett Packard Enterprise Development LP +# (C) Copyright 2018-2024 Hewlett Packard Enterprise Development LP # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), diff --git a/src/server/v3/resources/images.py b/src/server/v3/resources/images.py index 3f7dc88..c262bf6 100644 --- a/src/server/v3/resources/images.py +++ b/src/server/v3/resources/images.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2020-2023 Hewlett Packard Enterprise Development LP +# (C) Copyright 2020-2024 Hewlett Packard Enterprise Development LP # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), diff --git a/tests/ims_fixtures.py b/tests/ims_fixtures.py index 95bf9ef..390163f 100644 --- a/tests/ims_fixtures.py +++ b/tests/ims_fixtures.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2018-2019, 2021-2022 Hewlett Packard Enterprise Development LP +# (C) Copyright 2018-2019, 2021-2022, 2024 Hewlett Packard Enterprise Development LP # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), diff --git a/tests/v2/ims_fixtures.py b/tests/v2/ims_fixtures.py index 4179f02..42c5ab4 100644 --- a/tests/v2/ims_fixtures.py +++ b/tests/v2/ims_fixtures.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2018-2022 Hewlett Packard Enterprise Development LP +# (C) Copyright 2018-2024 Hewlett Packard Enterprise Development LP # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), diff --git a/tests/v2/test_v2_images.py b/tests/v2/test_v2_images.py index 32ec17a..5988e24 100644 --- a/tests/v2/test_v2_images.py +++ b/tests/v2/test_v2_images.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2018-2019, 2021-2022 Hewlett Packard Enterprise Development LP +# (C) Copyright 2018-2019, 2021-2022, 2024 Hewlett Packard Enterprise Development LP # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), diff --git a/tests/v3/test_v3_deleted_images.py b/tests/v3/test_v3_deleted_images.py index 7eb98ff..7e69474 100644 --- a/tests/v3/test_v3_deleted_images.py +++ b/tests/v3/test_v3_deleted_images.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2020-2023 Hewlett Packard Enterprise Development LP +# (C) Copyright 2020-2024 Hewlett Packard Enterprise Development LP # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), diff --git a/tests/v3/test_v3_images.py b/tests/v3/test_v3_images.py index 2fdbf74..dd61700 100644 --- a/tests/v3/test_v3_images.py +++ b/tests/v3/test_v3_images.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2020-2023 Hewlett Packard Enterprise Development LP +# (C) Copyright 2020-2024 Hewlett Packard Enterprise Development LP # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), From 171dc3da6e7ab4b6a72ed3d348b9f85a15df2325 Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Mon, 24 Jun 2024 10:40:07 -0500 Subject: [PATCH 22/25] Remove batch patch operations to simplify CLI --- api/openapi.yaml | 45 ++++++++++++++----------------- src/server/models/images.py | 6 ++--- src/server/v2/resources/images.py | 36 +++++++++++-------------- src/server/v3/resources/images.py | 37 +++++++++++-------------- tests/v2/test_v2_images.py | 21 --------------- 5 files changed, 55 insertions(+), 90 deletions(-) diff --git a/api/openapi.yaml b/api/openapi.yaml index 114aef5..c9cf074 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -2274,31 +2274,26 @@ components: - x86_64 type: string metadata: - description: A list of metadata change operations to apply to an existing Image Record - type: array - items: - description: An object which indicates a number of annotation patch operations relating to image tags. - type: object - required: - - operation - - key - properties: - operation: - description: How to update a given key within the context of a patch operation - type: string - enum: - - set - - remove - key: - description: The key to update for a given image - type: string - example: includes_additional_packages - value: - description: The value to associate with a key during a patch operation - type: string - example: "vim,emacs,man" - - + description: An object which indicates a number of annotation patch operations relating to image tags. + type: object + required: + - operation + - key + properties: + operation: + description: How to update a given key within the context of a patch operation + type: string + enum: + - set + - remove + key: + description: The key to update for a given image + type: string + example: includes_additional_packages + value: + description: The value to associate with a key during a patch operation + type: string + example: "vim,emacs,man" ImageRecord: description: An Image Record type: object diff --git a/src/server/models/images.py b/src/server/models/images.py index 74185be..3d8fc99 100644 --- a/src/server/models/images.py +++ b/src/server/models/images.py @@ -107,7 +107,7 @@ class V2ImageRecordPatchSchema(Schema): arch = fields.Str(required=False, validate=OneOf([ARCH_ARM64, ARCH_X86_64]), load_default=ARCH_X86_64, dump_default=ARCH_X86_64, metadata={"metadata": {"description": "Architecture of the recipe"}}) - metadata = fields.List(fields.Nested(V2ImageRecordMetadataPatchSchema()), - required=False, - metadata={"metadata": {"description":"A list of change operations to perform on Image Metadata."}}) + metadata = fields.Nested(V2ImageRecordMetadataPatchSchema(), + required=False, + metadata={"metadata": {"description": "An action to set or remove image metadata information."}}) diff --git a/src/server/v2/resources/images.py b/src/server/v2/resources/images.py index a8f23df..cf98b6d 100644 --- a/src/server/v2/resources/images.py +++ b/src/server/v2/resources/images.py @@ -235,26 +235,22 @@ def patch(self, image_id): image.arch = value setattr(image, key, value) elif key == 'metadata': - # The API represents metadata keys as a dictionary, but the patchset is provided as a list of - # changes that need to be applied to the metadata itself. - for changeset in value: - operation = changeset.get('operation') - annotation_key = changeset.get('key') - annotation_value = changeset.get('value', '') - current_app.logger.debug("Image Patch changeset: Current: %s -> %s %s %s" - % (image.metadata, operation, annotation_key, annotation_value)) - if operation not in ['set', 'remove']: - current_app.logger.info(f"Unknown requested operation change '{operation}'.") - return generate_data_validation_failure(errors=[]) - current_app.logger.debug("Image Patch changeset: %s %s %s" % (operation, annotation_key, annotation_value)) - if operation == 'set': - image.metadata[annotation_key] = annotation_value - elif operation == 'remove': - try: - del image.metadata[annotation_key] - except KeyError: - current_app.logger.info("No-op when removing non-existent metadata from IMS record.") - current_app.logger.debug("Image metadata result: %s" %(image.metadata)) + operation = value.get('operation') + annotation_key = value.get('key') + annotation_value = value.get('value', '') + current_app.logger.debug("Image Patch changeset: Current: %s -> %s %s %s" + % (image.metadata, operation, annotation_key, annotation_value)) + if operation not in ['set', 'remove']: + current_app.logger.info(f"Unknown requested operation change '{operation}'.") + return generate_data_validation_failure(errors=[]) + if operation == 'set': + image.metadata[annotation_key] = annotation_value + elif operation == 'remove': + try: + del image.metadata[annotation_key] + except KeyError: + current_app.logger.info("No-op when removing non-existent metadata from IMS record.") + current_app.logger.debug("Image metadata result: %s", image.metadata) else: current_app.logger.info(f"{log_id} Not able to patch record field {key} with value {value}") return generate_data_validation_failure(errors=[]) diff --git a/src/server/v3/resources/images.py b/src/server/v3/resources/images.py index c262bf6..b9895a7 100644 --- a/src/server/v3/resources/images.py +++ b/src/server/v3/resources/images.py @@ -380,27 +380,22 @@ def patch(self, image_id): image.arch = value setattr(image, key, value) elif key == 'metadata': - # The API represents metadata keys as a dictionary, but the patchset is provided as a list of - # changes that need to be applied to the metadata itself. - for changeset in value: - operation = changeset.get('operation') - annotation_key = changeset.get('key') - annotation_value = changeset.get('value', '') - current_app.logger.debug("Image Patch changeset: Current: %s -> %s %s %s" - % (image.metadata, operation, annotation_key, annotation_value)) - if operation not in ['set', 'remove']: - current_app.logger.info(f"Unknown requested operation change '{operation}'.") - return generate_data_validation_failure(errors=[]) - current_app.logger.debug( - "Image Patch changeset: %s %s %s" % (operation, annotation_key, annotation_value)) - if operation == 'set': - image.metadata[annotation_key] = annotation_value - elif operation == 'remove': - try: - del image.metadata[annotation_key] - except KeyError: - current_app.logger.info("No-op when removing non-existent metadata from IMS record.") - current_app.logger.debug("Image metadata result: %s" % (image.metadata)) + operation = value.get('operation') + annotation_key = value.get('key') + annotation_value = value.get('value', '') + current_app.logger.debug("Image Patch changeset: Current: %s -> %s %s %s" + % (image.metadata, operation, annotation_key, annotation_value)) + if operation not in ['set', 'remove']: + current_app.logger.info(f"Unknown requested operation change '{operation}'.") + return generate_data_validation_failure(errors=[]) + if operation == 'set': + image.metadata[annotation_key] = annotation_value + elif operation == 'remove': + try: + del image.metadata[annotation_key] + except KeyError: + current_app.logger.info("No-op when removing non-existent metadata from IMS record.") + current_app.logger.debug("Image metadata result: %s", image.metadata) else: current_app.logger.info(f"{log_id} Not able to patch record field {key} with value {value}") return generate_data_validation_failure(errors=[]) diff --git a/tests/v2/test_v2_images.py b/tests/v2/test_v2_images.py index 5988e24..00c6e7e 100644 --- a/tests/v2/test_v2_images.py +++ b/tests/v2/test_v2_images.py @@ -426,27 +426,6 @@ def test_patch_remove_metadata_idempotent(self): data=json.dumps(patch_data)) self.assertEqual(response.status_code, 200, 'status code was not 200') - def test_patch_multiple_changesets_single_patch(self): - patch_data = {'metadata': []} - for operation in ('set', 'remove', 'set', 'remove', 'remove'): - patch_data['metadata'].append({'operation': operation, 'key': 'foo', 'value': 'bar'}) - for test_uri in (self.test_uri_no_link, - self.test_uri_with_link, - self.test_uri_link_none, - self.test_uri_with_metadata, - self.test_uri_with_no_metadata - ): - response = self.app.patch(test_uri, - content_type='application/json', - data=json.dumps(patch_data)) - self.assertEqual(response.status_code, 200, 'status code was not 200') - response = self.app.get(test_uri) - response_data = json.loads(response.data) - self.assertEqual(response.status_code, 200, 'unable to retrieve after a patch') - # We ended with a remove, so there should be no remaining metadata - self.assertEqual(len(response_data['metadata'].keys()), 0, - 'Unable to remove set keys: %s' % (response_data['metadata'].keys())) - class TestV2ImagesCollectionEndpoint(TestCase): """ From ef98890c55f570bdc072349dee3ec7ffcaffbda9 Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Mon, 24 Jun 2024 10:43:31 -0500 Subject: [PATCH 23/25] Fix patch calls and reduce to a single patch change key --- tests/v2/test_v2_images.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/v2/test_v2_images.py b/tests/v2/test_v2_images.py index 00c6e7e..cc53b6b 100644 --- a/tests/v2/test_v2_images.py +++ b/tests/v2/test_v2_images.py @@ -406,21 +406,21 @@ def test_patch_change_arch(self): def test_patch_set_metadata(self): test_kv_pairs = [('foo', 'bar'), ('projected', 'image')] for key_val, val_val in test_kv_pairs: - patch_data = {'metadata': [{'operation': 'set', 'key': key_val, 'value': val_val}]} + patch_data = {'metadata': {'operation': 'set', 'key': key_val, 'value': val_val}} response = self.app.patch(self.test_uri_link_none, content_type='application/json', data=json.dumps(patch_data)) self.assertEqual(response.status_code, 200, 'status code was not 200') def test_patch_remove_metadata(self): - patch_data = {'metadata': [{'operation': 'remove', 'key': 'key'}]} + patch_data = {'metadata': {'operation': 'remove', 'key': 'key'}} response = self.app.patch(self.test_uri_with_metadata, content_type='application/json', data=json.dumps(patch_data)) self.assertEqual(response.status_code, 200, 'status code was not 200') def test_patch_remove_metadata_idempotent(self): - patch_data = {'metadata': [{'operation': 'remove', 'key': 'key'}]} + patch_data = {'metadata': {'operation': 'remove', 'key': 'key'}} response = self.app.patch(self.test_uri_with_no_metadata, content_type='application/json', data=json.dumps(patch_data)) From 49e3ded7d04e34743f54cdd1fdea305d240ab1a0 Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Wed, 26 Jun 2024 10:28:46 -0500 Subject: [PATCH 24/25] fix log level and remove copyright bumps for untouched files --- src/server/v2/resources/images.py | 2 +- tests/ims_fixtures.py | 2 +- tests/v2/ims_fixtures.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/v2/resources/images.py b/src/server/v2/resources/images.py index cf98b6d..9838040 100644 --- a/src/server/v2/resources/images.py +++ b/src/server/v2/resources/images.py @@ -254,7 +254,7 @@ def patch(self, image_id): else: current_app.logger.info(f"{log_id} Not able to patch record field {key} with value {value}") return generate_data_validation_failure(errors=[]) - current_app.logger.info(f"{log_id} image metadata information dump: '%s'" % image.metadata) + current_app.logger.debug(f"{log_id} image metadata information dump: '%s'" % image.metadata) current_app.data['images'][image_id] = image return_json = image_schema.dump(current_app.data['images'][image_id]) diff --git a/tests/ims_fixtures.py b/tests/ims_fixtures.py index 390163f..95bf9ef 100644 --- a/tests/ims_fixtures.py +++ b/tests/ims_fixtures.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2018-2019, 2021-2022, 2024 Hewlett Packard Enterprise Development LP +# (C) Copyright 2018-2019, 2021-2022 Hewlett Packard Enterprise Development LP # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), diff --git a/tests/v2/ims_fixtures.py b/tests/v2/ims_fixtures.py index 42c5ab4..4179f02 100644 --- a/tests/v2/ims_fixtures.py +++ b/tests/v2/ims_fixtures.py @@ -1,7 +1,7 @@ # # MIT License # -# (C) Copyright 2018-2024 Hewlett Packard Enterprise Development LP +# (C) Copyright 2018-2022 Hewlett Packard Enterprise Development LP # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), From a10aae712cc35d3ec0edfc19879f788f8b3b8f61 Mon Sep 17 00:00:00 2001 From: Joel Landsteiner Date: Wed, 26 Jun 2024 12:57:30 -0500 Subject: [PATCH 25/25] Version bump --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2707d2d..773c3ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [3.16.0] - 2024-06-26 ### Added - CASMCMS-8915: IMS API features for tagging built images. ### Dependencies