diff --git a/google/cloud/runtimeconfig/exceptions.py b/google/cloud/runtimeconfig/exceptions.py new file mode 100644 index 0000000..665b71e --- /dev/null +++ b/google/cloud/runtimeconfig/exceptions.py @@ -0,0 +1,19 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Exceptions used in the Google RuntimeConfig client.""" + + +class Error(Exception): + """Exception for all non-warning RuntimeConfig errors.""" diff --git a/google/cloud/runtimeconfig/variable.py b/google/cloud/runtimeconfig/variable.py index e7974c7..d68378b 100644 --- a/google/cloud/runtimeconfig/variable.py +++ b/google/cloud/runtimeconfig/variable.py @@ -41,8 +41,9 @@ import pytz from google.api_core import datetime_helpers -from google.cloud.exceptions import NotFound +from google.cloud.exceptions import Conflict, NotFound from google.cloud.runtimeconfig._helpers import variable_name_from_full_name +from google.cloud.runtimeconfig.exceptions import Error STATE_UNSPECIFIED = "VARIABLE_STATE_UNSPECIFIED" @@ -117,6 +118,34 @@ def client(self): """The client bound to this variable.""" return self.config.client + @property + def text(self): + """Text of the variable, as string. + + See + https://cloud.google.com/deployment-manager/runtime-configurator/reference/rest/v1beta1/projects.configs.variables + + :rtype: str or ``NoneType`` + :returns: The text of the variable or ``None`` if the property + is not set locally. + """ + return self._properties.get("text") + + @text.setter + def text(self, value): + """Set text property. + + If the variable is already using value, this will raise + exceptions.Error since text and value are mutually exclusive. + To persist the change, call create() or update(). + + :type value: str + :param value: The new value for the text property. + """ + if "value" in self._properties: + raise Error("Value and text are mutually exclusive.") + self._properties["text"] = value + @property def value(self): """Value of the variable, as bytes. @@ -133,6 +162,21 @@ def value(self): value = base64.b64decode(value) return value + @value.setter + def value(self, value): + """Set value property. + + If the variable is already using text, this will raise exceptions.Error + since text and value are mutually exclusive. + To persist the change, call create() or update(). + + :type value: bytes + :param value: The new value for the value property. + """ + if "text" in self._properties: + raise Error("Value and text are mutually exclusive.") + self._properties["value"] = value + @property def state(self): """Retrieve the state of the variable. @@ -204,6 +248,71 @@ def _set_properties(self, resource): self.name = variable_name_from_full_name(cleaned.pop("name")) self._properties.update(cleaned) + def _get_payload(self): + """Return the payload for create and update operations + + :rtype: dict + :returns: payload for API call with name and text or value attributes + """ + data = {"name": self.full_name} + if "text" in self._properties: + data["text"] = self._properties["text"] + elif "value" in self._properties: + value = self._properties["value"] + data["value"] = base64.b64encode(value).decode("utf-8") + else: + raise Error("No text or value set.") + return data + + def create(self, client=None): + """API call: create the variable via a POST request + + See + https://cloud.google.com/deployment-manager/runtime-configurator/reference/rest/v1beta1/projects.configs.variables/create + + :type client: :class:`~google.cloud.runtimeconfig.client.Client` + :param client: + (Optional) The client to use. If not passed, falls back to the + ``client`` stored on the variable's config. + + :rtype: bool + :returns: True if the variable has been created, False on error. + """ + client = self._require_client(client) + path = "%s/variables" % self.config.path + data = self._get_payload() + try: + resp = client._connection.api_request(method="POST", path=path, data=data) + except Conflict: + return False + self._set_properties(resp) + return True + + def update(self, client=None): + """API call: update the variable via a PUT request + + See + https://cloud.google.com/deployment-manager/runtime-configurator/reference/rest/v1beta1/projects.configs.variables/update + + :type client: :class:`~google.cloud.runtimeconfig.client.Client` + :param client: + (Optional) The client to use. If not passed, falls back to the + ``client`` stored on the variable's config. + + :rtype: bool + :returns: True if the variable has been created, False on error. + """ + client = self._require_client(client) + data = self._get_payload() + try: + resp = client._connection.api_request( + method="PUT", path=self.path, data=data + ) + except NotFound: + return False + self._set_properties(resp) + return True + def exists(self, client=None): """API call: test for the existence of the variable via a GET request diff --git a/tests/unit/test_variable.py b/tests/unit/test_variable.py index 3044a23..c4c3cd0 100644 --- a/tests/unit/test_variable.py +++ b/tests/unit/test_variable.py @@ -42,6 +42,11 @@ def _verifyResourceProperties(self, variable, resource): else: self.assertIsNone(variable.value) + if "text" in resource: + self.assertEqual(variable.text, resource["text"]) + else: + self.assertIsNone(variable.text) + if "state" in resource: self.assertEqual(variable.state, resource["state"]) @@ -112,6 +117,154 @@ def test_exists_hit_w_alternate_client(self): self.assertEqual(req["path"], "/%s" % (self.PATH,)) self.assertEqual(req["query_params"], {"fields": "name"}) + def test_create_no_data(self): + from google.cloud.runtimeconfig.config import Config + from google.cloud.runtimeconfig.exceptions import Error + + conn = _Connection() + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.variable(self.VARIABLE_NAME) + with self.assertRaises(Error) as ctx: + variable.create() + self.assertEqual("No text or value set.", str(ctx.exception)) + + def test_create_conflict(self): + from google.cloud.exceptions import Conflict + from google.cloud.runtimeconfig.config import Config + + conn = _Connection(Conflict("test")) + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.variable(self.VARIABLE_NAME) + variable.text = "foo" + self.assertFalse(variable.create()) + + def test_create_text(self): + from google.cloud.runtimeconfig.config import Config + + RESOURCE = { + "name": self.PATH, + "text": "foo", + "updateTime": "2016-04-14T21:21:54.5000Z", + "state": "UPDATED", + } + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.variable(self.VARIABLE_NAME) + variable.text = "foo" + result = variable.create() + self.assertTrue(result) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req["method"], "POST") + self.assertEqual( + req["path"], + "/projects/%s/configs/%s/variables" % (self.PROJECT, self.CONFIG_NAME), + ) + self._verifyResourceProperties(variable, RESOURCE) + + def test_create_value(self): + from google.cloud.runtimeconfig.config import Config + + RESOURCE = { + "name": self.PATH, + "value": "bXktdmFyaWFibGUtdmFsdWU=", # base64 my-variable-value + "updateTime": "2016-04-14T21:21:54.5000Z", + "state": "UPDATED", + } + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.variable(self.VARIABLE_NAME) + variable.value = b"my-variable-value" + result = variable.create() + self.assertTrue(result) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req["method"], "POST") + self.assertEqual( + req["path"], + "/projects/%s/configs/%s/variables" % (self.PROJECT, self.CONFIG_NAME), + ) + self._verifyResourceProperties(variable, RESOURCE) + + def test_update_text_conflict(self): + from google.cloud.runtimeconfig.config import Config + from google.cloud.runtimeconfig.exceptions import Error + + RESOURCE = { + "name": self.PATH, + "value": "bXktdmFyaWFibGUtdmFsdWU=", # base64 my-variable-value + "updateTime": "2016-04-14T21:21:54.5000Z", + "state": "UPDATED", + } + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.get_variable(self.VARIABLE_NAME) + with self.assertRaises(Error) as ctx: + variable.text = "bar" + self.assertEqual("Value and text are mutually exclusive.", str(ctx.exception)) + + def test_update_value_conflict(self): + from google.cloud.runtimeconfig.config import Config + from google.cloud.runtimeconfig.exceptions import Error + + RESOURCE = { + "name": self.PATH, + "text": "foo", + "updateTime": "2016-04-14T21:21:54.5000Z", + "state": "UPDATED", + } + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.get_variable(self.VARIABLE_NAME) + with self.assertRaises(Error) as ctx: + variable.value = b"bar" + self.assertEqual("Value and text are mutually exclusive.", str(ctx.exception)) + + def test_update_not_found(self): + from google.cloud.runtimeconfig.config import Config + + RESOURCE = { + "name": self.PATH, + "text": "foo", + "updateTime": "2016-04-14T21:21:54.5000Z", + "state": "UPDATED", + } + conn = _Connection(RESOURCE) + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.get_variable(self.VARIABLE_NAME) + self.assertFalse(variable.update()) + + def test_update_text(self): + from google.cloud.runtimeconfig.config import Config + + RESOURCE = { + "name": self.PATH, + "text": "foo", + "updateTime": "2016-04-14T21:21:54.5000Z", + "state": "UPDATED", + } + RESOURCE_UPD = RESOURCE.copy() + RESOURCE_UPD["text"] = "bar" + conn = _Connection(RESOURCE, RESOURCE_UPD) + client = _Client(project=self.PROJECT, connection=conn) + config = Config(name=self.CONFIG_NAME, client=client) + variable = config.get_variable(self.VARIABLE_NAME) + variable.text = "bar" + result = variable.update() + self.assertTrue(result) + self.assertEqual(len(conn._requested), 2) + req = conn._requested[1] + self.assertEqual(req["method"], "PUT") + self.assertEqual(req["path"], "/%s" % self.PATH) + self._verifyResourceProperties(variable, RESOURCE_UPD) + def test_reload_w_bound_client(self): from google.cloud.runtimeconfig.config import Config @@ -226,4 +379,6 @@ def api_request(self, **kw): except IndexError: raise NotFound("miss") else: + if issubclass(type(response), Exception): + raise response return response