Skip to content

Commit bda45f6

Browse files
author
Mitchell
committed
feat: add CreateLocalDeployment IPC support (C + Rust)
Adds CreateLocalDeployment operation to the C and Rust SDK, enabling components to trigger local deployments via IPC without shelling out to greengrass-cli. Also fixes mock test data: renamed GG_IPC_ACCEPTED_HEADERS to GG_IPC_RESPONSE_HEADERS — Classic Nucleus sends 'Response' suffix (verified via GGLite-IPC-EventStream-Sniffer packet captures against Nucleus v2.17.0), not 'Accepted' as the mock previously used.
1 parent 494a597 commit bda45f6

13 files changed

Lines changed: 442 additions & 25 deletions

include/gg/ipc/client.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,35 @@ GgError ggipc_update_state(GgComponentState state);
116116
/// <https://docs.aws.amazon.com/greengrass/v2/developerguide/ipc-local-deployments-components.html#ipc-operation-restartcomponent>
117117
GgError ggipc_restart_component(GgBuffer component_name);
118118

119+
/// Create a local deployment on the core device.
120+
/// Applies a component configuration merge (and optionally adds/removes root
121+
/// components). Currently this wrapper only exposes the
122+
/// `componentToConfiguration` field of the IPC request, which is sufficient for
123+
/// merging configuration into an already-deployed component (including
124+
/// `aws.greengrass.Nucleus`). The other fields of the request
125+
/// (`rootComponentVersionsToAdd`, `rootComponentsToRemove`,
126+
/// `recipeDirectoryPath`, `artifactsDirectoryPath`, `failureHandlingPolicy`)
127+
/// can be added in follow-up revisions as needed.
128+
///
129+
/// `component_to_configuration` MUST be a `GgMap` whose keys are component
130+
/// names (as `GgBuffer`) and whose values are `GgMap` of config to merge.
131+
/// If `deployment_id` is not NULL, on success it is populated with a view into
132+
/// `deployment_id_mem` holding the returned deployment id.
133+
/// `deployment_id_mem` must remain valid and unmodified while
134+
/// `deployment_id` is used.
135+
///
136+
/// Requires a dependency on the `aws.greengrass.Cli` component and an
137+
/// `aws.greengrass.Cli` access-control policy allowing
138+
/// `aws.greengrass#CreateLocalDeployment`.
139+
/// See:
140+
/// <https://docs.aws.amazon.com/greengrass/v2/developerguide/ipc-local-deployments-components.html#ipc-operation-createlocaldeployment>
141+
ACCESS(read_write, 3)
142+
GgError ggipc_create_local_deployment(
143+
GgObject component_to_configuration,
144+
GgBuffer deployment_id_mem,
145+
GgBuffer *deployment_id
146+
);
147+
119148
/// Get component configuration value.
120149
/// Retrieves configuration for the specified key path.
121150
/// Pass empty list for complete config.

misc/dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ ffat
1010
fileb
1111
flto
1212
fstrict
13+
ggdeploymentd
1314
ggipc
1415
greengrassv2
1516
idents

mock/gg/ipc/packet_sequences.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ GgipcPacketSequence gg_test_restart_component_error_sequence(
8484
int32_t stream_id, GgBuffer component_name
8585
);
8686

87+
GgipcPacketSequence gg_test_create_local_deployment_accepted_sequence(
88+
int32_t stream_id,
89+
GgObject component_to_configuration,
90+
GgBuffer deployment_id
91+
);
92+
93+
GgipcPacketSequence gg_test_create_local_deployment_error_sequence(
94+
int32_t stream_id, GgObject component_to_configuration
95+
);
96+
8797
// Shadow sequences
8898

8999
GgipcPacketSequence gg_test_shadow_update_accepted_sequence(

mock/packets/component_packet_sequences.c

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ GgipcPacket gg_test_update_state_request_packet(
3030
GgipcPacket gg_test_update_state_response_packet(int32_t stream_id) {
3131
return (GgipcPacket) { .direction = SERVER_TO_CLIENT,
3232
.has_payload = false,
33-
.headers = GG_IPC_ACCEPTED_HEADERS(
33+
.headers = GG_IPC_RESPONSE_HEADERS(
3434
stream_id, "aws.greengrass#UpdateStateAccepted"
3535
),
36-
.header_count = GG_IPC_ACCEPTED_HEADERS_COUNT };
36+
.header_count = GG_IPC_RESPONSE_HEADERS_COUNT };
3737
}
3838

3939
GgipcPacketSequence gg_test_update_state_accepted_sequence(
@@ -84,10 +84,10 @@ GgipcPacket gg_test_restart_component_response_packet(
8484
.direction = SERVER_TO_CLIENT,
8585
.has_payload = true,
8686
.payload = gg_obj_map((GgMap) { .pairs = payload, .len = payload_len }),
87-
.headers = GG_IPC_ACCEPTED_HEADERS(
87+
.headers = GG_IPC_RESPONSE_HEADERS(
8888
stream_id, "aws.greengrass#RestartComponentAccepted"
8989
),
90-
.header_count = GG_IPC_ACCEPTED_HEADERS_COUNT
90+
.header_count = GG_IPC_RESPONSE_HEADERS_COUNT
9191
};
9292
}
9393

@@ -114,3 +114,66 @@ GgipcPacketSequence gg_test_restart_component_error_sequence(
114114
.len = 2
115115
};
116116
}
117+
118+
GgipcPacket gg_test_create_local_deployment_request_packet(
119+
int32_t stream_id, GgObject component_to_configuration
120+
) {
121+
static GgKV payload[1];
122+
payload[0]
123+
= gg_kv(GG_STR("componentToConfiguration"), component_to_configuration);
124+
size_t payload_len = sizeof(payload) / sizeof(payload[0]);
125+
126+
return (GgipcPacket) { .direction = CLIENT_TO_SERVER,
127+
.has_payload = true,
128+
.payload = gg_obj_map((GgMap) {
129+
.pairs = payload, .len = payload_len }),
130+
.headers = GG_IPC_REQUEST_HEADERS(
131+
stream_id, "aws.greengrass#CreateLocalDeployment"
132+
),
133+
.header_count = GG_IPC_REQUEST_HEADERS_COUNT };
134+
}
135+
136+
GgipcPacket gg_test_create_local_deployment_response_packet(
137+
int32_t stream_id, GgBuffer deployment_id
138+
) {
139+
static GgKV payload[1];
140+
payload[0] = gg_kv(GG_STR("deploymentId"), gg_obj_buf(deployment_id));
141+
size_t payload_len = sizeof(payload) / sizeof(payload[0]);
142+
143+
return (GgipcPacket) { .direction = SERVER_TO_CLIENT,
144+
.has_payload = true,
145+
.payload = gg_obj_map((GgMap) {
146+
.pairs = payload, .len = payload_len }),
147+
.headers = GG_IPC_RESPONSE_HEADERS(
148+
stream_id, "aws.greengrass#CreateLocalDeployment"
149+
),
150+
.header_count = GG_IPC_RESPONSE_HEADERS_COUNT };
151+
}
152+
153+
GgipcPacketSequence gg_test_create_local_deployment_accepted_sequence(
154+
int32_t stream_id,
155+
GgObject component_to_configuration,
156+
GgBuffer deployment_id
157+
) {
158+
return (GgipcPacketSequence) {
159+
.packets = { gg_test_create_local_deployment_request_packet(
160+
stream_id, component_to_configuration
161+
),
162+
gg_test_create_local_deployment_response_packet(
163+
stream_id, deployment_id
164+
) },
165+
.len = 2
166+
};
167+
}
168+
169+
GgipcPacketSequence gg_test_create_local_deployment_error_sequence(
170+
int32_t stream_id, GgObject component_to_configuration
171+
) {
172+
return (GgipcPacketSequence) {
173+
.packets = { gg_test_create_local_deployment_request_packet(
174+
stream_id, component_to_configuration
175+
),
176+
gg_test_ipc_permissions_error_packet(stream_id) },
177+
.len = 2
178+
};
179+
}

mock/packets/config_packet_sequences.c

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ GgipcPacket gg_test_config_get_object_accepted_packet(
7979
.has_payload = true,
8080
.payload = gg_obj_map((GgMap) { .pairs = payload, .len = payload_len }),
8181
.headers
82-
= GG_IPC_ACCEPTED_HEADERS(stream_id, "aws.greengrass#GetConfiguration"),
83-
.header_count = GG_IPC_ACCEPTED_HEADERS_COUNT
82+
= GG_IPC_RESPONSE_HEADERS(stream_id, "aws.greengrass#GetConfiguration"),
83+
.header_count = GG_IPC_RESPONSE_HEADERS_COUNT
8484
};
8585
}
8686

@@ -124,10 +124,10 @@ static GgipcPacket config_bad_empty_response(int32_t stream_id) {
124124
return (GgipcPacket) { .direction = SERVER_TO_CLIENT,
125125
.has_payload = true,
126126
.payload = gg_obj_map((GgMap) {}),
127-
.headers = GG_IPC_ACCEPTED_HEADERS(
127+
.headers = GG_IPC_RESPONSE_HEADERS(
128128
stream_id, "aws.greengrass#GetConfiguration"
129129
),
130-
.header_count = GG_IPC_ACCEPTED_HEADERS_COUNT };
130+
.header_count = GG_IPC_RESPONSE_HEADERS_COUNT };
131131
}
132132

133133
GgipcPacketSequence gg_test_config_bad_server_response_sequence(
@@ -170,10 +170,10 @@ static GgipcPacket gg_test_config_update_request_packet(
170170
static GgipcPacket gg_test_config_update_response_packet(int32_t stream_id) {
171171
return (GgipcPacket) { .has_payload = false,
172172
.direction = SERVER_TO_CLIENT,
173-
.headers = GG_IPC_ACCEPTED_HEADERS(
173+
.headers = GG_IPC_RESPONSE_HEADERS(
174174
stream_id, "aws.greengrass#UpdateConfiguration"
175175
),
176-
.header_count = GG_IPC_ACCEPTED_HEADERS_COUNT };
176+
.header_count = GG_IPC_RESPONSE_HEADERS_COUNT };
177177
}
178178

179179
GgipcPacketSequence gg_test_config_update_sequence(

mock/packets/mqtt_packet_sequences.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ GgipcPacket gg_test_mqtt_publish_request_packet(
3131
GgipcPacket gg_test_mqtt_publish_accepted_packet(int32_t stream_id) {
3232
return (GgipcPacket) { .direction = SERVER_TO_CLIENT,
3333
.has_payload = false,
34-
.headers = GG_IPC_ACCEPTED_HEADERS(
34+
.headers = GG_IPC_RESPONSE_HEADERS(
3535
stream_id, "aws.greengrass#PublishToIoTCore"
3636
),
37-
.header_count = GG_IPC_ACCEPTED_HEADERS_COUNT };
37+
.header_count = GG_IPC_RESPONSE_HEADERS_COUNT };
3838
}
3939

4040
GgipcPacketSequence gg_test_mqtt_publish_accepted_sequence(

mock/packets/packets.h

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
#define GG_IPC_REQUEST_HEADERS_COUNT 5
2929

30-
#define GG_IPC_ACCEPTED_HEADERS(stream_id, operation) \
30+
#define GG_IPC_RESPONSE_HEADERS(stream_id, operation) \
3131
{ \
3232
{ GG_STR(":message-type"), \
3333
{ EVENTSTREAM_INT32, .int32 = EVENTSTREAM_APPLICATION_MESSAGE } }, \
@@ -39,12 +39,12 @@
3939
{ EVENTSTREAM_STRING, .string = GG_STR("application/json") } }, \
4040
{ \
4141
GG_STR("service-model-type"), { \
42-
EVENTSTREAM_STRING, .string = GG_STR(operation "Accepted") \
42+
EVENTSTREAM_STRING, .string = GG_STR(operation "Response") \
4343
} \
4444
} \
4545
}
4646

47-
#define GG_IPC_ACCEPTED_HEADERS_COUNT 5
47+
#define GG_IPC_RESPONSE_HEADERS_COUNT 5
4848

4949
#define GG_IPC_SUBSCRIBE_MESSAGE_HEADERS(stream_id, service_model) \
5050
{ \
@@ -165,6 +165,14 @@ GgipcPacket gg_test_restart_component_response_packet(
165165
int32_t stream_id, GgBuffer restart_status
166166
);
167167

168+
GgipcPacket gg_test_create_local_deployment_request_packet(
169+
int32_t stream_id, GgObject component_to_configuration
170+
);
171+
172+
GgipcPacket gg_test_create_local_deployment_response_packet(
173+
int32_t stream_id, GgBuffer deployment_id
174+
);
175+
168176
// Shadow operations
169177

170178
GgipcPacket gg_test_shadow_update_request_packet(

mock/packets/pubsub_packet_sequences.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ GgipcPacket gg_test_local_publish_request_packet(
5252
GgipcPacket gg_test_local_publish_accepted_packet(int32_t stream_id) {
5353
return (GgipcPacket) { .direction = SERVER_TO_CLIENT,
5454
.has_payload = false,
55-
.headers = GG_IPC_ACCEPTED_HEADERS(
55+
.headers = GG_IPC_RESPONSE_HEADERS(
5656
stream_id, "aws.greengrass#PublishToTopic"
5757
),
58-
.header_count = GG_IPC_ACCEPTED_HEADERS_COUNT };
58+
.header_count = GG_IPC_RESPONSE_HEADERS_COUNT };
5959
}
6060

6161
GgipcPacketSequence gg_test_local_publish_accepted_sequence(

mock/packets/shadow_packet_sequences.c

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ GgipcPacket gg_test_shadow_update_response_packet(
4242
.has_payload = true,
4343
.payload = gg_obj_map((GgMap) { .pairs = pairs,
4444
.len = pairs_len }),
45-
.headers = GG_IPC_ACCEPTED_HEADERS(
45+
.headers = GG_IPC_RESPONSE_HEADERS(
4646
stream_id, "aws.greengrass#UpdateThingShadow"
4747
),
48-
.header_count = GG_IPC_ACCEPTED_HEADERS_COUNT };
48+
.header_count = GG_IPC_RESPONSE_HEADERS_COUNT };
4949
}
5050

5151
GgipcPacketSequence gg_test_shadow_update_accepted_sequence(
@@ -113,8 +113,8 @@ GgipcPacket gg_test_shadow_get_response_packet(
113113
.has_payload = true,
114114
.payload = gg_obj_map((GgMap) { .pairs = pairs, .len = pairs_len }),
115115
.headers
116-
= GG_IPC_ACCEPTED_HEADERS(stream_id, "aws.greengrass#GetThingShadow"),
117-
.header_count = GG_IPC_ACCEPTED_HEADERS_COUNT
116+
= GG_IPC_RESPONSE_HEADERS(stream_id, "aws.greengrass#GetThingShadow"),
117+
.header_count = GG_IPC_RESPONSE_HEADERS_COUNT
118118
};
119119
}
120120

@@ -177,10 +177,10 @@ GgipcPacket gg_test_shadow_delete_response_packet(
177177
.has_payload = true,
178178
.payload = gg_obj_map((GgMap) { .pairs = pairs,
179179
.len = pairs_len }),
180-
.headers = GG_IPC_ACCEPTED_HEADERS(
180+
.headers = GG_IPC_RESPONSE_HEADERS(
181181
stream_id, "aws.greengrass#DeleteThingShadow"
182182
),
183-
.header_count = GG_IPC_ACCEPTED_HEADERS_COUNT };
183+
.header_count = GG_IPC_RESPONSE_HEADERS_COUNT };
184184
}
185185

186186
GgipcPacketSequence gg_test_shadow_delete_accepted_sequence(
@@ -251,10 +251,10 @@ GgipcPacket gg_test_shadow_list_response_packet(
251251
.direction = SERVER_TO_CLIENT,
252252
.has_payload = true,
253253
.payload = gg_obj_map((GgMap) { .pairs = pairs, .len = pairs_len }),
254-
.headers = GG_IPC_ACCEPTED_HEADERS(
254+
.headers = GG_IPC_RESPONSE_HEADERS(
255255
stream_id, "aws.greengrass#ListNamedShadowsForThing"
256256
),
257-
.header_count = GG_IPC_ACCEPTED_HEADERS_COUNT
257+
.header_count = GG_IPC_RESPONSE_HEADERS_COUNT
258258
};
259259
}
260260

rust/src/ipc.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,50 @@ impl Sdk {
369369
Result::from(unsafe { c::ggipc_restart_component(component_name.into()) })
370370
}
371371

372+
/// Create a local deployment that merges configuration into one or more
373+
/// already-deployed components.
374+
///
375+
/// `component_to_configuration` must be a map (`Object::from(...)` built from
376+
/// a `Map` / `KVList`) whose keys are component names and whose values are
377+
/// maps of config keys to merge. Nested maps are supported.
378+
///
379+
/// On success, the returned `&str` is a view into `deployment_id_mem`
380+
/// holding the deployment id returned by the nucleus.
381+
///
382+
/// On Classic Nucleus, this operation is served by the `aws.greengrass.Cli`
383+
/// plugin component; the caller must declare a dependency on
384+
/// `aws.greengrass.Cli` and an access-control policy allowing
385+
/// `aws.greengrass#CreateLocalDeployment`.
386+
///
387+
/// On Nucleus Lite, this operation is implemented natively by
388+
/// `ggdeploymentd` and requires no component dependency or ACL.
389+
///
390+
/// See: <https://docs.aws.amazon.com/greengrass/v2/developerguide/ipc-local-deployments-components.html#ipc-operation-createlocaldeployment>
391+
///
392+
/// # Errors
393+
/// Returns error if the IPC call fails or the nucleus rejects the deployment.
394+
pub fn create_local_deployment<'a, 'b>(
395+
&self,
396+
component_to_configuration: impl Into<Object<'a>>,
397+
deployment_id_mem: &'b mut [MaybeUninit<u8>],
398+
) -> Result<&'b str> {
399+
let mut value = c::GgBuffer {
400+
data: deployment_id_mem.as_mut_ptr().cast::<u8>(),
401+
len: deployment_id_mem.len(),
402+
};
403+
let obj: Object<'a> = component_to_configuration.into();
404+
Result::from(unsafe {
405+
c::ggipc_create_local_deployment(
406+
*ptr::from_ref(&obj).cast::<c::GgObject>(),
407+
value,
408+
&raw mut value,
409+
)
410+
})?;
411+
Ok(unsafe {
412+
str::from_utf8_unchecked(slice::from_raw_parts(value.data, value.len))
413+
})
414+
}
415+
372416
/// Get component configuration value.
373417
///
374418
/// Retrieves configuration for the specified key path. Pass empty slice for complete config.

0 commit comments

Comments
 (0)