Skip to content
Permalink
Browse files
feat: support variable create / update methods and text attribute (#17)
This PR adds support for variable create and update methods.

The create method has been implemented in the Variable class instead of Config (which would have given symmetry with get_variable) for a couple of reasons:

it allows a more natural workflow of calling config.variable(), setting the desired attribute, then saving
it updates the created variable in place via _set_properties like other variable methods
This also adds support for the text attribute, which can now be used in alternative to the previously supported value. The create and update methods enforce mutual exclusivity of the two attributes.

Fixes #1
  • Loading branch information
ludoo committed Jun 5, 2020
1 parent 6b29562 commit 84a50ad6cd0765bd86a4ed7c338aec2612e5e91c
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 1 deletion.
@@ -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."""
@@ -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
@@ -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

0 comments on commit 84a50ad

Please sign in to comment.