From 18ebf26ac9c584c006c2d4ff860bab17b7d19f9f Mon Sep 17 00:00:00 2001 From: Rudi Schlatte Date: Thu, 7 Dec 2023 17:35:40 +0100 Subject: [PATCH] Emit AMPL code when called with json app creation message in a file Validate and use variable mappings, including data types and bounds. Currently does not handle array (mapping) parameters in KubeVela. Change-Id: I5864d3ac1e0e29bd1df393bc95e049a957a4e55a --- .gitignore | 6 + optimiser-controller/build.gradle | 3 + .../optimiser/controller/AppParser.java | 34 ++- .../optimiser/controller/ExnConnector.java | 3 +- .../optimiser/controller/Main.java | 21 +- .../optimiser/controller/NebulousApp.java | 85 +++++-- .../optimiser/controller/AppParserTest.java | 12 +- .../vela-deployment-app-message.json | 221 ++++++++++++++++++ .../resources/vela-deployment-parameters.yaml | 15 ++ 9 files changed, 356 insertions(+), 44 deletions(-) create mode 100644 optimiser-controller/src/test/resources/vela-deployment-app-message.json diff --git a/.gitignore b/.gitignore index dc4aaef..8bcd7cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ __pycache__/ .nox/ +.DS_Store + # Gradle project-specific cache directory .gradle # Gradle build output directory @@ -15,3 +17,7 @@ __pycache__/ # IntelliJ IDEA configuration files /.idea/ + +# Visual Studio Code files +/.vscode/ +/.dir-locals.el diff --git a/optimiser-controller/build.gradle b/optimiser-controller/build.gradle index 4f9fcef..e24a6a9 100644 --- a/optimiser-controller/build.gradle +++ b/optimiser-controller/build.gradle @@ -37,6 +37,9 @@ dependencies { // YAML parsing: https://github.com/decorators-squad/eo-yaml/tree/master implementation 'com.amihaiemil.web:eo-yaml:7.0.10' + // JSON parsing: https://github.com/stleary/JSON-java + implementation 'org.json:json:20231013' + // Command-line parsing: https://picocli.info implementation 'info.picocli:picocli:4.7.5' diff --git a/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/AppParser.java b/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/AppParser.java index 18f05dd..682bf8b 100644 --- a/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/AppParser.java +++ b/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/AppParser.java @@ -4,6 +4,9 @@ import java.io.IOException; +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONPointer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,19 +15,34 @@ public class AppParser { private static final Logger log = LoggerFactory.getLogger(AppParser.class); /** - * Parse a KubeVela file and mapping file. + * Location of the kubevela yaml file in the app creation message. Should + * point to a string. + */ + private static final JSONPointer kubevela_path + = JSONPointer.builder().append("kubevela").append("original").build(); + + /** + * Location of the modifiable locations of the kubevela file in the app + * creation message. Should point to an array of objects. + */ + private static final JSONPointer variables_path + = JSONPointer.builder().append("kubevela").append("variables").build(); + + /** + * Parse an incoming app creation message in json format. * - * @param kubevela a deployable KubeVela file - * @param mappings parameter mappings for the KubeVela file + * @param json_message the app creation message * @return a {@code NebulousApp} instance, or {@code null} if there was an - * error parsing the app creation message + * error parsing the app creation message */ - public static NebulousApp parseAppCreationMessage(String kubevela, String mappings) { + public static NebulousApp parseAppCreationMessage(JSONObject json_message) { + String kubevela_string = (String)kubevela_path.queryFrom(json_message); + JSONArray parameters = (JSONArray)variables_path.queryFrom(json_message); try { - return new NebulousApp(Yaml.createYamlInput(kubevela).readYamlMapping(), - Yaml.createYamlInput(mappings).readYamlMapping()); + return new NebulousApp(Yaml.createYamlInput(kubevela_string).readYamlMapping(), + parameters); } catch (IOException e) { - log.error("Could not read app creation data: ", e); + log.error("Could not read app creation message: ", e); return null; } } diff --git a/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/ExnConnector.java b/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/ExnConnector.java index 1d313bc..574947b 100644 --- a/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/ExnConnector.java +++ b/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/ExnConnector.java @@ -91,8 +91,7 @@ private class MyConsumerHandler extends Handler { // `body` is of type `Map` by default, so can be // handled by various JSON libraries directly. @Override - public void onMessage(String key, String address, Map body, Message message, - AtomicReference context) + public void onMessage(String key, String address, Map body, Message message, Context context) { log.info("Message delivered for key {} => {} ({}) = {}", key, address, body, message); diff --git a/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/Main.java b/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/Main.java index 075834d..be5e05c 100644 --- a/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/Main.java +++ b/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/Main.java @@ -10,6 +10,7 @@ import eu.nebulouscloud.exn.core.Context; import eu.nebulouscloud.exn.handlers.ConnectorHandler; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import picocli.CommandLine; @@ -31,7 +32,7 @@ public class Main implements Callable { @Option(names = {"-s", "--sal-url"}, description = "The URL of the SAL server (including URL scheme http:// or https://). Can also be set via the @|bold SAL_URL|@ environment variable.", paramLabel = "SAL_URL", - defaultValue = "${SAL_URL:-http://158.37.63.90:8880/}") + defaultValue = "${SAL_URL:-http://localhost:8880/}") private java.net.URI sal_uri; @Option(names = {"--sal-user"}, @@ -70,13 +71,9 @@ public class Main implements Callable { defaultValue = "${ACTIVEMQ_PASSWORD}") private String activemq_password; - @Option(names = {"--kubevela-file"}, - description = "The name of a deployable KubeVela yaml file (used for testing purposes)") - private Path kubevela_file; - - @Option(names = {"--kubevela-parameters"}, - description = "The name of a parameter file referencing the deployable model (used for testing purposes)") - private Path kubevela_parameters; + @Option(names = {"--app-creation-message-file", "-f"}, + description = "The name of a file containing a JSON app creation message (used for testing purposes)") + private Path json_app_creation_file; private static final Logger log = LoggerFactory.getLogger(Main.class); @@ -97,11 +94,11 @@ public Integer call() { connector.connect(sal_user, sal_password); } - if (kubevela_file != null && kubevela_parameters!= null) { + if (json_app_creation_file != null) { try { - NebulousApp app - = AppParser.parseAppCreationMessage(Files.readString(kubevela_file, StandardCharsets.UTF_8), - Files.readString(kubevela_parameters, StandardCharsets.UTF_8)); + JSONObject msg = new JSONObject(Files.readString(json_app_creation_file, StandardCharsets.UTF_8)); + NebulousApp app = AppParser.parseAppCreationMessage(msg); + app.printAMPL(); } catch (IOException e) { log.error("Could not read an input file: ", e); success = 1; diff --git a/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/NebulousApp.java b/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/NebulousApp.java index 9b934cc..f632ce7 100644 --- a/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/NebulousApp.java +++ b/optimiser-controller/src/main/java/eu/nebulouscloud/optimiser/controller/NebulousApp.java @@ -1,42 +1,47 @@ package eu.nebulouscloud.optimiser.controller; import com.amihaiemil.eoyaml.*; +import org.json.JSONArray; +import org.json.JSONObject; /** * Internal representation of a NebulOus app. */ public class NebulousApp { private YamlMapping original_kubevela; - private YamlMapping parameters; + private JSONArray parameters; /** * Creates a NebulousApp object. * - * Example KubeVela and parameter files can be found below {@code - * optimiser-controller/src/test/resources} - * * @param kubevela A parsed representation of the deployable KubeVela App model - * @param parameters A parameter mapping + * @param parameters A parameter mapping as a sequence of JSON objects. */ - public NebulousApp(YamlMapping kubevela, YamlMapping parameters) { + // Note that example KubeVela and parameter files can be found at + // optimiser-controller/src/test/resources/ + public NebulousApp(YamlMapping kubevela, JSONArray parameters) { this.original_kubevela = kubevela; this.parameters = parameters; } /** - * Check that the target paths of all parameters can be found in the - * original KubeVela file. + * Check that all parameters have a name, type and path, and that the + * target path can be found in the original KubeVela file. * - * @return true if all parameter paths match, false otherwise + * @return true if all requirements hold, false otherwise */ - public boolean validateMapping() { - YamlMapping params = parameters.yamlMapping("nebulous_metadata").yamlMapping("optimisation_variables"); - for (final YamlNode param : params.keys()) { - YamlMapping param_data = params.yamlMapping(param); - String param_name = param.asScalar().value(); - String target_path = param_data.value("target").asScalar().value(); + public boolean validatePaths() { + for (final Object p : parameters) { + JSONObject param = (JSONObject) p; + String param_name = param.optString("key"); + if (param_name.equals("")) return false; + String param_type = param.optString("type"); + if (param_type.equals("")) return false; + // TODO: also validate types, upper and lower bounds, etc. + String target_path = param.optString("path"); + if (target_path.equals("")) return false; YamlNode target = findPathInKubevela(target_path); - if (target == null) return false; + if (target == null) return false; // must exist } return true; } @@ -75,4 +80,52 @@ private YamlNode findPathInKubevela(String path) { return currentNode; } + /** + * Print AMPL code for the app, based on the parameter definition(s). + */ + public void printAMPL() { + for (final Object p : parameters) { + JSONObject param = (JSONObject) p; + String param_name = param.getString("key"); + String param_type = param.getString("type"); + JSONObject value = param.optJSONObject("value"); + if (param_type.equals("float")) { + System.out.format("var %s", param_name); + if (value != null) { + String separator = ""; + double lower = value.optDouble("lower_bound"); + double upper = value.optDouble("upper_bound"); + if (!Double.isNaN(lower)) { + System.out.format (" >= %s", lower); + separator = ", "; + } + if (!Double.isNaN(upper)) { + System.out.format("%s<= %s", separator, upper); + } + } + System.out.println(";"); + } else if (param_type.equals("int")) { + System.out.format("var %s integer", param_name); + if (value != null) { + String separator = ""; + Integer lower = value.optIntegerObject("lower_bound", null); + Integer upper = value.optIntegerObject("upper_bound", null); + if (lower != null) { + System.out.format (" >= %s", lower); + separator = ", "; + } + if (upper != null) { + System.out.format("%s<= %s", separator, upper); + } + } + System.out.println(";"); + } else if (param_type.equals("string")) { + System.out.println("# TODO not sure how to specify a string variable"); + System.out.format("var %s symbolic;%n"); + } else if (param_type.equals("array")) { + System.out.format("# TODO generate entries for map '%s'%n", param_name); + } + } + } + } diff --git a/optimiser-controller/src/test/java/eu/nebulouscloud/optimiser/controller/AppParserTest.java b/optimiser-controller/src/test/java/eu/nebulouscloud/optimiser/controller/AppParserTest.java index 6c0f8ec..9e47843 100644 --- a/optimiser-controller/src/test/java/eu/nebulouscloud/optimiser/controller/AppParserTest.java +++ b/optimiser-controller/src/test/java/eu/nebulouscloud/optimiser/controller/AppParserTest.java @@ -10,6 +10,7 @@ import java.nio.file.Path; import java.nio.file.Paths; +import org.json.JSONObject; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -22,12 +23,11 @@ private Path getResourcePath(String name) throws URISyntaxException { @Test void readValidAppCreationMessage() throws URISyntaxException, IOException { - String kubevela = Files.readString(getResourcePath("vela-deployment.yaml"), - StandardCharsets.UTF_8); - String parameters = Files.readString(getResourcePath("vela-deployment-parameters.yaml"), - StandardCharsets.UTF_8); - NebulousApp app = AppParser.parseAppCreationMessage(kubevela, parameters); + String app_message_string = Files.readString(getResourcePath("vela-deployment-app-message.json"), + StandardCharsets.UTF_8); + JSONObject msg = new JSONObject(app_message_string); + NebulousApp app = AppParser.parseAppCreationMessage(msg); assertNotNull(app); - assertTrue(app.validateMapping()); + assertTrue(app.validatePaths()); } } diff --git a/optimiser-controller/src/test/resources/vela-deployment-app-message.json b/optimiser-controller/src/test/resources/vela-deployment-app-message.json new file mode 100644 index 0000000..02e9247 --- /dev/null +++ b/optimiser-controller/src/test/resources/vela-deployment-app-message.json @@ -0,0 +1,221 @@ +{ + "application": { + "name": "This is the application name", + "uuid": "f81ee-b42a8-a13d56-e28ec9-2f5578" + }, + "kubevela": { + "original": "apiVersion: core.oam.dev/v1beta1\nkind: Application\nmetadata:\n name: surveillance-demo\n namespace: default\nspec:\n components:\n - name: kafka-server\n type: webservice\n properties:\n image: confluentinc/cp-kafka:7.2.1\n hostname: kafka-server\n ports:\n - port: 9092\n expose: true\n - port: 9093\n expose: true\n - port: 29092\n expose: true\n cpu: \"1\"\n memory: \"2000Mi\"\n cmd: [ \"/bin/bash\", \"/tmp/run_workaround.sh\" ]\n env:\n - name: KAFKA_NODE_ID\n value: \"1\"\n - name: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP\n value: \"CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT\"\n - name: KAFKA_LISTENERS\n value: \"PLAINTEXT://0.0.0.0:9092,PLAINTEXT_HOST://0.0.0.0:29092,CONTROLLER://0.0.0.0:9093\"\n - name: KAFKA_ADVERTISED_LISTENERS\n value: \"PLAINTEXT://kafka-server:9092,PLAINTEXT_HOST://212.101.173.161:29092\"\n - name: KAFKA_CONTROLLER_LISTENER_NAMES\n value: \"CONTROLLER\"\n - name: KAFKA_CONTROLLER_QUORUM_VOTERS\n value: \"1@0.0.0.0:9093\"\n - name: KAFKA_PROCESS_ROLES\n value: \"broker,controller\"\n# volumeMounts:\n# configMap:\n# - name: configmap-example-1\n# mountPath: /tmp\n# cmName: configmap-example-1\n# defaultMod: 777\n traits:\n - type: storage\n properties:\n configMap:\n - name: kafka-init\n mountPath: /tmp\n data:\n run_workaround.sh: |-\n #!/bin/sh\n sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure\n sed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure\n echo \"kafka-storage format --ignore-formatted -t NqnEdODVKkiLTfJvqd1uqQ== -c /etc/kafka/kafka.properties\" >> /etc/confluent/docker/ensure\n /etc/confluent/docker/run\n\n - name: kafka-ui\n type: webservice\n properties:\n image: provectuslabs/kafka-ui:cd9bc43d2e91ef43201494c4424c54347136d9c0\n exposeType: NodePort\n ports:\n - port: 8080\n expose: true\n nodePort: 30001\n cpu: \"0.3\"\n memory: \"512Mi\"\n env:\n - name: KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS\n value: \"kafka-server:9092\"\n\n - name: video-capture\n type: webservice\n properties:\n image: registry.ubitech.eu/nebulous/use-cases/surveillance-dsl-demo/video-capture:1.1.0\n cpu: \"0.2\"\n memory: \"100Mi\"\n env:\n - name: KAFKA_URL\n value: \"kafka-server:9092\"\n - name: KAFKA_DETECTION_TOPIC\n value: \"surveillance\"\n - name: CAPTURE_VIDEO\n value: \"False\"\n - name: CAPTURE_DEVICE\n value: \"/dev/video0\"\n - name: DEBUG\n value: \"True\"\n - name: HOSTNAME\n value: \"docker-capture\"\n volumeMounts:\n hostPath:\n - name: video\n mountPath: \"/dev/video1\"\n path: \"/dev/video0\"\n traits:\n - type: affinity\n properties:\n nodeAffinity:\n required:\n nodeSelectorTerms:\n - matchExpressions:\n - key: \"kubernetes.io/hostname\"\n operator: \"In\"\n values: [\"nebulousk8s-worker-1\"]\n\n\n# devices:\n# - /dev/video0:/dev/video0\n\n - name: face-detection\n type: webservice\n properties:\n image: registry.ubitech.eu/nebulous/use-cases/surveillance-dsl-demo/face-detection:1.2.0\n edge:\n cpu: \"1.2\"\n memory: \"512Mi\"\n env:\n - name: KAFKA_URL\n value: \"kafka-server:9092\"\n - name: KAFKA_DETECTION_TOPIC\n value: \"surveillance\"\n - name: THREADS_COUNT\n value: \"1\"\n - name: STORE_METRIC\n value: \"False\"\n - name: DEBUG\n value: \"True\"\n cloud:\n cpu: \"1.2\"\n memory: \"512Mi\"\n env:\n - name: KAFKA_URL\n value: \"kafka-server:9092\"\n - name: KAFKA_DETECTION_TOPIC\n value: \"surveillance\"\n - name: THREADS_COUNT\n value: \"1\"\n - name: STORE_METRIC\n value: \"False\"\n - name: DEBUG\n value: \"True\"\n traits:\n - type: affinity\n properties:\n podAntiAffinity:\n required:\n - labelSelector:\n matchExpressions:\n - key: \"app.oam.dev/component\"\n operator: \"In\"\n values: [ \"video-capture\" ]\n topologyKey: \"test\"\n - type: nodePlacement\n properties:\n cloudWorkers:\n count: 6\n nodeSelector:\n - name: node1\n value: 2\n - name: node2\n value: 1\n - name: node3\n value: 3\n edgeWorkers:\n count: 3\n nodeSelector:\n - name: node4\n value: 2\n - name: node5\n value: 1\n - type: geoLocation\n properties:\n affinity:\n required:\n - labelSelector:\n - key: \"continent\"\n operator: \"In\"\n values: [\"Europe\"]\n\n - name: video-player\n type: webservice\n properties:\n image: registry.ubitech.eu/nebulous/use-cases/surveillance-dsl-demo/video-player:1.1.0\n exposeType: NodePort\n env:\n - name: KAFKA_URL\n value: \"kafka-server:9092\"\n - name: DEBUG\n value: \"True\"\n - name: SERVER_PORT\n value: \"8081\"\n ports:\n - port: 8081\n expose: true\n nodePort: 30002\n", + "variables": [ + { + "key": "face_detection_edge_worker_cpu", + "path": ".spec.components[3].properties.edge.cpu", + "meaning": "cpu", + "type": "float", + "value": { + "lower_bound": 1.2, + "upper_bound": 3.0 + }, + "is_constant": false + }, + { + "key": "face_detection_edge_worker_memory", + "path": ".spec.components[3].properties.edge.memory", + "meaning": "memory", + "type": "float", + "value": { + "lower_bound": 250, + "upper_bound": 1000 + }, + "is_constant": false + }, + { + "key": "face_detection_edge_worker_count", + "path": ".spec.components[3].traits[1].properties.edgeWorkers.count", + "type": "int", + "value": { + "lower_bound": 0, + "upper_bound": 5 + } + }, + { + "key": "face_detection_edge_workers", + "path": ".spec.components[3].traits[1].properties.edgeWorkers.nodeSelector", + "type": "array", + "entry": { + "type": "kv", + "members": [ + { + "key": "nodename", + "type": "string" + }, + { + "key": "count", + "type": "int" + } + ] + } + }, + { + "key": "face_detection_cloud_worker_cpu", + "path": ".spec.components[3].properties.cloud.cpu", + "meaning": "cpu", + "type": "float", + "value": { + "lower_bound": 3.0, + "upper_bound": 6.0 + }, + "is_constant": false + }, + { + "key": "face_detection_cloud_worker_memory", + "path": ".spec.components[3].properties.cloud.memory", + "meaning": "memory", + "type": "float", + "value": { + "lower_bound": 1000, + "upper_bound": 4000 + }, + "is_constant": false + }, + { + "key": "face_detection_cloud_worker_count", + "path": ".spec.components[3].traits[1].properties.cloudWorkers.count", + "type": "int", + "value": { + "lower_bound": 2, + "upper_bound": 10 + } + }, + { + "key": "face_detection_cloud_workers", + "path": ".spec.components[3].traits[1].properties.cloudWorkers.nodeSelector", + "type": "array", + "entry": { + "type": "kv", + "members": [ + { + "key": "nodename", + "type": "string" + }, + { + "key": "count", + "type": "int" + } + ] + } + } + ] + }, + "cloud_providers": [ + { + "type": "aws", + "sal_key": "2342342342asdfsadf" + }, + { + "type": "gce", + "sal_key": "fseae2$@$@#aAfadadsf" + } + ], + "metrics": [ + { + "type": "composite", + "@comment": "// composite | raw", + "key": "some_composite_metric_name", + "name": "Some composite metric name", + "formula": "A * B * C - E", + "window": { + "@comment": "// this can be empty" + } + }, + { + "type": "composite", + "@comment": "// composite | raw", + "key": "cpu_util_prtc", + "name": "cpu_util_prtc", + "formula": "A * B * C - E", + "window": { + "input": { + "type": "all", + "interval": 30, + "unit": "sec", + "@comment": "// this can ms / sec / min / hour" + }, + "output": { + "type": "all", + "interval": 30, + "unit": "ms" + } + } + }, + { + "type": "raw", + "@comment": "// composite | raw", + "name": "cpu_util_prtc #2", + "key": "cpu_util_prtc_2", + "sensor": "sensor_camery", + "config": { + "ipAddres": "0.0.0.0", + "location": "europe", + "timezone": "Europe/Athens" + } + } + ], + "slo": { + "operator": "and", + "children": [ + { + "operator": "or", + "type": "composite", + "children": [ + { + "condition": { + "not": true, + "key": "cpu_util_prtc_2", + "operand": ">", + "value": 2 + } + }, + { + "condition": { + "key": "cpu_util_prtc", + "operand": "<", + "value": 100 + } + } + ] + }, + { + "type": "simple", + "condition": { + "key": "cpu_util_prtc_2", + "operand": ">", + "value": 2, + "type": "float" + } + } + ] + }, + "utility_functions": [ + { + "key": "utility_function_1", + "name": "Utility Function 1", + "type": "maximize", + "@comment": "// maximize | minimize", + "formula": "A * B * C - E", + "mapping": { + "A": "cpu_util_prtc", + "B": "component_facedetection_1_cpu_2" + } + }, + { + "key": "utility_function_2", + "name": "Utility Function 2", + "type": "minimize", + "@comment": "// maximize | minimize", + "formula": "A", + "mapping": { + "A": "bacdafd" + } + } + ] +} diff --git a/optimiser-controller/src/test/resources/vela-deployment-parameters.yaml b/optimiser-controller/src/test/resources/vela-deployment-parameters.yaml index 36f814f..e723bcd 100644 --- a/optimiser-controller/src/test/resources/vela-deployment-parameters.yaml +++ b/optimiser-controller/src/test/resources/vela-deployment-parameters.yaml @@ -3,11 +3,18 @@ nebulous_metadata: face_detection_edge_worker_cpu: target: .spec.components[3].properties.edge.cpu type: floating + lower_bound: 1.2 + upper_bound: 3.0 face_detection_edge_worker_memory: target: .spec.components[3].properties.edge.memory type: floating + lower_bound: 250 + upper_bound: 1000 face_detection_edge_worker_count: target: .spec.components[3].traits[1].properties.edgeWorkers.count + type: integer + lower_bound: 0 + upper_bound: 5 face_detection_edge_workers: target: .spec.components[3].traits[1].properties.edgeWorkers.nodeSelector type: array @@ -20,11 +27,19 @@ nebulous_metadata: type: integer face_detection_cloud_worker_cpu: target: .spec.components[3].properties.cloud.cpu + type: floating + lower_bound: 3 + upper_bound: 6 face_detection_cloud_worker_memory: target: .spec.components[3].properties.cloud.memory type: floating + lower_bound: 1000 + upper_bound: 4000 face_detection_cloud_worker_count: target: .spec.components[3].traits[1].properties.cloudWorkers.count + type: integer + lower_bound: 2 + upper_bound: 10 face_detection_cloud_workers: target: .spec.components[3].traits[1].properties.cloudWorkers.nodeSelector type: array