diff --git a/README.md b/README.md index 647eb73..1743a64 100755 --- a/README.md +++ b/README.md @@ -42,6 +42,21 @@ You can also use dynamic values from the datastore. See the is serialized as JSON. * ``kv.grep_object`` - Find datastore items which name matches the provided query amd deserialize their values from JSON serialized objects. + +* ``kv.entry.get`` - Retrieve entry in a JSON serialized object from the + datastore. An entry is a standard JSON object with properties. + Fails if the datastore key does not exist. +* ``kv.entry.set_property`` - Set a property in the named entry + of a JSON serialized object. Then, serialize and store the updated object. + The property's value may be any json-serializable type. + Fails if the datastore key does not exist. A coordination backend is recommended. +* ``kv.entry.append_property`` - Add the value to the end of an array property + in the named entry of a JSON serialized object. Then, serialize and store the + updated object. The property must be an array. The value to append can be anything. + Fails if the datastore key does not exist. A coordination backend is recommended. +* ``kv.entry.delete_property`` - Delete a property from a named entry of a JSON + serialized object in the datastore. If the entry is empty, delete it as well. + Fails if the datastore key does not exist. A coordination backend is recommended. Note: ``kv.set`` and ``kv.get`` actions support compressing value before storing it in a datastore and decompressing it when retrieving it from diff --git a/actions/kv_entry_append_property.py b/actions/kv_entry_append_property.py new file mode 100755 index 0000000..234654e --- /dev/null +++ b/actions/kv_entry_append_property.py @@ -0,0 +1,57 @@ +import json + +from st2common.services import coordination as coordination_service + +from lib.action import St2BaseAction + +__all__ = ["St2KVPEntryAppendPropertyAction"] + + +class St2KVPEntryAppendPropertyAction(St2BaseAction): + # noinspection PyShadowingBuiltins + def run(self, key, entry, property, value): + with coordination_service.get_coordinator().get_lock("st2.kv_entry." + key): + # get and deserialize object or fail. + _key = self.client.keys.get_by_name(key, decrypt=False) + + if not _key: + raise Exception("Key does not exist in datastore") + + # optimistically try to decode a json value + try: + value = json.loads(value) + except (TypeError, ValueError): + # assume it is either already decoded (TypeError) + # or it is a plain string (ValueError) + # (malformed JSON objects/arrays will be strings) + pass + + deserialized = json.loads(_key.value) + + # update or insert object.entry.property + _entry = deserialized.get(entry, {}) + _property = _entry.get(property, []) + try: + _property.append(value) + except AttributeError: + raise Exception( + "Cannot append. Property {}.{}.{} is not an array!".format( + key, entry, property + ) + ) + + _entry[property] = _property + deserialized[entry] = _entry + + # re-serialize and save + serialized = json.dumps(deserialized) + kvp = self._kvp(name=key, value=serialized) + kvp.id = key + + self.client.keys.update(kvp) + response = { + "key": key, + "entry_name": entry, + "entry": _entry, + } + return response diff --git a/actions/kv_entry_append_property.yaml b/actions/kv_entry_append_property.yaml new file mode 100755 index 0000000..e2b57e7 --- /dev/null +++ b/actions/kv_entry_append_property.yaml @@ -0,0 +1,32 @@ +--- +name: 'kv.entry.append_property' +enabled: true +description: | + Add a value to the end of an array property under a named entry (sub-object) + of a serialized object in kv datastore. + Fails if the key does not exist in the datastore, as a property inside it can't be appended to yet. + The kv object uses this structure: { entry: { property: [value, value], ...}, ...} + This action does not support encrypted datastore objects. + +runner_type: python-script +entry_point: kv_entry_append_property.py +parameters: + key: + required: true + type: string + position: 0 + entry: + description: name of the key's entry (or fallback entry) + required: true + type: string + position: 1 + property: + description: name of the entry's property to append to (property should have an array value) + required: true + type: string + position: 2 + value: + description: the value may be any json-serializable type (string, number, array, object) + required: true + # type: any + position: 3 diff --git a/actions/kv_entry_delete_property.py b/actions/kv_entry_delete_property.py new file mode 100755 index 0000000..deb9208 --- /dev/null +++ b/actions/kv_entry_delete_property.py @@ -0,0 +1,48 @@ +import json + +from st2common.services import coordination as coordination_service + +from lib.action import St2BaseAction + +__all__ = ["St2KVPEntryDeletePropertyAction"] + + +class St2KVPEntryDeletePropertyAction(St2BaseAction): + # noinspection PyShadowingBuiltins + def run(self, key, entry, property): + with coordination_service.get_coordinator().get_lock("st2.kv_entry." + key): + # get and deserialize object or fail. + _key = self.client.keys.get_by_name(key, decrypt=False) + + if not _key: + raise Exception("Key does not exist in datastore") + + deserialized = json.loads(_key.value) + + # delete object.entry.property + _entry = deserialized.get(entry, {}) + try: + del _entry[property] + except KeyError: + pass + + # delete object.entry if entry is empty + if not _entry: + try: + del deserialized[entry] + except KeyError: + pass + + # re-serialize and save + serialized = json.dumps(deserialized) + kvp = self._kvp(name=key, value=serialized) + kvp.id = key + + self.client.keys.update(kvp) + response = { + "key": key, + "entry_name": entry, + "entry": _entry, + "entry_deleted": not _entry, + } + return response diff --git a/actions/kv_entry_delete_property.yaml b/actions/kv_entry_delete_property.yaml new file mode 100755 index 0000000..8e60030 --- /dev/null +++ b/actions/kv_entry_delete_property.yaml @@ -0,0 +1,28 @@ +--- +name: 'kv.entry.delete_property' +enabled: true +description: | + Delete a property (if it exists) under a named entry (sub-object) of a + serialized object in kv datastore. If the entry is empty after deleting + the property, this deletes the entry as well. + Fails if the key does not exist in the datastore. + The kv object uses this structure: { entry: { property: value, ... }, entry: {}, ...} + This action does not support encrypted datastore objects. + +runner_type: python-script +entry_point: kv_entry_delete_property.py +parameters: + key: + required: true + type: string + position: 0 + entry: + description: name of the key's entry (or fallback entry) + required: true + type: string + position: 1 + property: + description: name of the entry's property to delete + required: true + type: string + position: 2 diff --git a/actions/kv_entry_get.py b/actions/kv_entry_get.py new file mode 100755 index 0000000..1ac5b3e --- /dev/null +++ b/actions/kv_entry_get.py @@ -0,0 +1,23 @@ +import json + +from lib.action import St2BaseAction + +__all__ = ["St2KVPEntryGetAction"] + + +class St2KVPEntryGetAction(St2BaseAction): + # noinspection PyShadowingBuiltins + def run(self, key, entry, fallback): + # get and deserialize object or fail. + _key = self.client.keys.get_by_name(key, decrypt=False) + + if not _key: + raise Exception("Key does not exist in datastore") + + deserialized = json.loads(_key.value) + + # try get object.entry.property + _entry = deserialized.get(fallback, {}) + _entry.update(deserialized.get(entry, {})) + + return _entry diff --git a/actions/kv_entry_get.yaml b/actions/kv_entry_get.yaml new file mode 100755 index 0000000..1121ce4 --- /dev/null +++ b/actions/kv_entry_get.yaml @@ -0,0 +1,29 @@ +--- +name: 'kv.entry.get' +enabled: true +description: | + Get a named entry (sub-object) of a serialized object in kv datastore + merged on top of the fallback entry (ie the fallback provides defaults). + Fails if the key does not exist in the datastore. + Does not fail if the entry or fallback is missing. + The kv object uses this structure: { fallback: { property: value, ... }, entry: {}, ...} + This action does not support encrypted datastore objects. + +runner_type: python-script +entry_point: kv_entry_get.py +parameters: + key: + required: true + type: string + position: 0 + entry: + description: name of the key's entry + required: true + type: string + position: 1 + fallback: + description: name of the key's fallback entry + required: false + type: string + default: default + position: 2 diff --git a/actions/kv_entry_set_property.py b/actions/kv_entry_set_property.py new file mode 100755 index 0000000..a6b5000 --- /dev/null +++ b/actions/kv_entry_set_property.py @@ -0,0 +1,47 @@ +import json + +from st2common.services import coordination as coordination_service + +from lib.action import St2BaseAction + +__all__ = ["St2KVPEntrySetPropertyAction"] + + +class St2KVPEntrySetPropertyAction(St2BaseAction): + # noinspection PyShadowingBuiltins + def run(self, key, entry, property, value): + with coordination_service.get_coordinator().get_lock("st2.kv_entry." + key): + # get and deserialize object or fail. + _key = self.client.keys.get_by_name(key, decrypt=False) + + if not _key: + raise Exception("Key does not exist in datastore") + + # optimistically try to decode a json value + try: + value = json.loads(value) + except (TypeError, ValueError): + # assume it is either already decoded (TypeError) + # or it is a plain string (ValueError) + # (malformed JSON objects/arrays will be strings) + pass + + deserialized = json.loads(_key.value) + + # update or insert object.entry.property + _entry = deserialized.get(entry, {}) + _entry[property] = value + deserialized[entry] = _entry + + # re-serialize and save + serialized = json.dumps(deserialized) + kvp = self._kvp(name=key, value=serialized) + kvp.id = key + + self.client.keys.update(kvp) + response = { + "key": key, + "entry_name": entry, + "entry": _entry, + } + return response diff --git a/actions/kv_entry_set_property.yaml b/actions/kv_entry_set_property.yaml new file mode 100755 index 0000000..4c64c01 --- /dev/null +++ b/actions/kv_entry_set_property.yaml @@ -0,0 +1,31 @@ +--- +name: 'kv.entry.set_property' +enabled: true +description: | + Insert or update a property under a named entry (sub-object) of a serialized object in kv datastore. + Fails if the key does not exist in the datastore, as a property inside it can't be inserted/updated yet. + The kv object uses this structure: { entry: { property: value, ... }, entry: {}, ...} + This action does not support encrypted datastore objects. + +runner_type: python-script +entry_point: kv_entry_set_property.py +parameters: + key: + required: true + type: string + position: 0 + entry: + description: name of the key's entry (or fallback entry) + required: true + type: string + position: 1 + property: + description: name of the entry's property to set + required: true + type: string + position: 2 + value: + description: the property value may be any json-serializable type (string, number, array, object) + required: true + # type: any + position: 3 diff --git a/pack.yaml b/pack.yaml index d88c4cc..8925815 100755 --- a/pack.yaml +++ b/pack.yaml @@ -2,7 +2,7 @@ ref: st2 name: st2 description: StackStorm utility actions and aliases -version: 1.3.1 +version: 1.4.0 python_versions: - "2" - "3"