Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support variable create / update methods and text attribute #17

Merged
merged 12 commits into from Jun 5, 2020
19 changes: 19 additions & 0 deletions 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."""
111 changes: 110 additions & 1 deletion google/cloud/runtimeconfig/variable.py
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand Down
155 changes: 155 additions & 0 deletions tests/unit/test_variable.py
Expand Up @@ -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"])

Expand Down Expand Up @@ -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))
ludoo marked this conversation as resolved.
Show resolved Hide resolved

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

Expand Down Expand Up @@ -226,4 +379,6 @@ def api_request(self, **kw):
except IndexError:
raise NotFound("miss")
else:
if issubclass(type(response), Exception):
raise response
return response