Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions promise-types/json/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Promise type for manipulating `json` files
| Name | Type | Description |
|---------------|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
| `object` | `data container` | json object type. It can also be json arrays |
| `array` | `slist`, `rlist`, `ilist`, `data array` | json array type. `slist`, `rlist` and `ilist` will only create string arrays. To create array of other types, use `data array` |
| `array` | `data array` | json array type |
| `string` | `string` | json string type |
| `number` | `real`, `int` | json number type |
| `primitive` | `string` | Primitives are values that are either `"true"`, `"false"` or `"null"` in json |
Expand Down Expand Up @@ -64,9 +64,9 @@ And the content of `/tmp/oldfile.json` will become:

If the field doesn't exist, it is appended. If it already exists, its data will be overwritten.

### Writing types
### Writing arrays

In order to write compound type such as arrays containg booleans, numbers, etc... One has to use the `data container` type in the policy.
In order to write compound type such as arrays containg booleans, numbers, etc... One has to use the `data` type in the policy.

To see what happens if we use

Expand All @@ -84,12 +84,12 @@ bundle agent main

json:
"/tmp/example_1.json:json_data"
array => "$(json_data)";
array => "@(json_data)";

"/tmp/example_2.json:real_list"
array => "$(real_list)";
array => "@(real_list)";
"/tmp/example_2.json:bool_list"
array => "$(bool_list)";
array => "@(bool_list)";
}
```

Expand Down
84 changes: 52 additions & 32 deletions promise-types/json/json_promise_type.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os
import json
import tempfile
import shutil

from cfengine import PromiseModule, ValidationError, Result, AttributeObject

Expand Down Expand Up @@ -48,22 +50,23 @@ def validate_promise(self, promiser, attributes, metadata):
if present_types == 0:
raise ValidationError(
"The promiser '{}' is missing a type attribute. The possible types are {}".format(
promiser, str(self.types)
promiser, ", ".join(["'{}'".format(t) for t in self.types])
)
)
elif len(present_types) > 1:
raise ValidationError(
"The attributes {} cannot be together".format(str(self.types))
"The attributes {} cannot be together".format(
", ".join(["'{}'".format(t) for t in self.types])
)
)

filename, _, _ = promiser.partition(":")
if os.path.exists(filename) and not os.path.isfile(filename):
raise ValidationError(
"'{}' already exists and is not a file".format(filename)
)
filename, colon, field = promiser.partition(":")

if not filename.endswith(".json"):
raise ValidationError("'{}' is not a json file")
if not filename:
raise ValidationError("Invalid syntax: missing file name")

if colon and not field:
raise ValidationError("Invalid syntax: field specified but empty")

model = self.create_attribute_object(attributes)
if (
Expand All @@ -77,14 +80,16 @@ def validate_promise(self, promiser, attributes, metadata):

if model.array:
if isinstance(model.array, str):
if not is_json_serializable(model.array):
try:
array = json.loads(model.array)

except:
raise ValidationError(
"'{}' is not a valid list".format(model.array)
"'{}' cannot be serialized to a json array".format(model.array)
)

if not isinstance(json.loads(model.array), list):
if not isinstance(array, list):
raise ValidationError(
"'{}' is not a valid data".format(model.array)
"'{}' is not a valid data array".format(model.array)
)

elif not isinstance(model.array, list):
Expand All @@ -106,23 +111,18 @@ def evaluate_promise(self, promiser, attributes, metadata):
model = self.create_attribute_object(attributes)
filename, _, field = promiser.partition(":")

if os.path.exists(filename) and not os.path.isfile(filename):
self.log_error("'{}' already exists and is not a regular file".format(filename))
return Result.NOT_KEPT

# type conversion

datatype = next(t for t in self.types if t in attributes)

match datatype:
case "object" | "array":
data = (
json.loads(attributes[datatype])
if isinstance(attributes[datatype], str)
else attributes[datatype]
)
case "number":
data = float(model.number) if "." in model.number else int(model.number)
case "primitive":
data = None if model.primitive == "null" else model.primitive == "true"
case _: # strings
data = attributes[datatype]
if isinstance(attributes[datatype], str) and not model.string:
data = json.loads(attributes[datatype])
else:
data = attributes[datatype]

# json manipulation

Expand All @@ -131,21 +131,41 @@ def evaluate_promise(self, promiser, attributes, metadata):
content = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
content = {}
except Exception as e:
self.log_error("Failed to read '{}': {}".format(filename, e))
return Result.NOT_KEPT

if field:
if not isinstance(content, dict):
content = {}
self.log_warning(
"Tried to access '{}' in '{}' when the content is not subscriptable. Overwriting the file...".format(
field, filename
)
)

if field in content and content[field] == data:
Result.KEPT
self.log_info("'{}' is already up to date")
return Result.KEPT
content[field] = data
else:
if content == data:
Result.KEPT
self.log_info("'{}' is already up to date")
return Result.KEPT
content = data

with open(filename, "w") as f:
json.dump(content, f, indent=4)
fd, tmp = tempfile.mkstemp()
json_bytes = json.dumps(content, indent=4).encode("utf-8")
written = os.write(fd, json_bytes)
os.close(fd)
shutil.move(tmp, filename)

if (written != len(json_bytes)):
self.log_error("Couldn't write all the data to the file '{}'. Wrote {} out of {} bytes".format(filename, written, len(json_bytes)))
return Result.NOT_KEPT

self.log_info("Updated '{}'".format(filename))
Result.REPAIRED
return Result.REPAIRED


if __name__ == "__main__":
Expand Down