Skip to content

Commit bf5ddeb

Browse files
author
Mitchell
committed
feat: add CreateLocalDeployment IPC support (C + C++ + Rust)
Add CreateLocalDeployment operation to the C, C++, and Rust SDK, enabling components to trigger local deployments via IPC without shelling out to greengrass-cli. Changes since initial review: - Full schema: GgCreateLocalDeploymentArgs struct exposes all 6 fields (componentToConfiguration, rootComponentVersionsToAdd, rootComponentsToRemove, recipeDirectoryPath, artifactsDirectoryPath, failureHandlingPolicy) - C++ wrapper: Client::create_local_deployment() added - README: CreateLocalDeployment section with C/C++/Rust examples - Mock packets: renamed ACCEPTED_HEADERS to RESPONSE_HEADERS per real IPC traces captured via GGLite-IPC-EventStream-Sniffer against Classic Nucleus v2.17.0 - Doc comments: clarified Classic vs Lite behavior (Lite has native support via ggdeploymentd, no Cli dep or ACL required) Testing: - nix flake check: 17/17 checks pass (formatting, namespacing, spelling, IWYU, C tests 45/45, Rust tests 28/28, cross-compile x86_64 + aarch64 + armv7l) - Integration: cache-proxy-bridge/server/common all compile and pass 172 tests with the updated SDK vendored in
1 parent 494a597 commit bf5ddeb

13 files changed

Lines changed: 709 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ The following Greengrass v2 IPC operations are currently supported by this SDK:
3131
- [UpdateThingShadow](https://docs.aws.amazon.com/greengrass/v2/developerguide/ipc-local-shadows.html#ipc-operation-updatethingshadow)
3232
- [DeleteThingShadow](https://docs.aws.amazon.com/greengrass/v2/developerguide/ipc-local-shadows.html#ipc-operation-deletethingshadow)
3333
- [ListNamedShadowsForThing](https://docs.aws.amazon.com/greengrass/v2/developerguide/ipc-local-shadows.html#ipc-operation-listnamedshadowsforthing)
34+
- [CreateLocalDeployment](https://docs.aws.amazon.com/greengrass/v2/developerguide/ipc-local-deployments-components.html#ipc-operation-createlocaldeployment)
3435

3536
## Sample Greengrass Components
3637

cpp/include/gg/ipc/client.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <utility>
2323

2424
extern "C" {
25+
#include <gg/ipc/client.h>
2526
#include <gg/ipc/types.h>
2627
}
2728

@@ -162,6 +163,18 @@ class Client {
162163
/// <https://docs.aws.amazon.com/greengrass/v2/developerguide/ipc-local-deployments-components.html#ipc-operation-restartcomponent>
163164
std::error_code restart_component(std::string_view component_name) noexcept;
164165

166+
/// Create a local deployment on the core device.
167+
/// Triggers a local deployment that can merge configuration into
168+
/// components, add/remove root components, and specify local
169+
/// recipe/artifact paths to overwrite the Greengrass recipe/artifact
170+
/// stores. See:
171+
/// <https://docs.aws.amazon.com/greengrass/v2/developerguide/ipc-local-deployments-components.html#ipc-operation-createlocaldeployment>
172+
std::error_code create_local_deployment(
173+
const GgCreateLocalDeploymentArgs &args,
174+
std::span<std::byte> deployment_id_mem = {},
175+
std::string_view *deployment_id = nullptr
176+
) noexcept;
177+
165178
/// Get component configuration value.
166179
/// Retrieves configuration for the specified key path.
167180
/// Pass empty span for complete config.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// aws-greengrass-component-sdk - Lightweight AWS IoT Greengrass SDK
2+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
#include <gg/ipc/client.hpp>
6+
#include <cstddef>
7+
#include <span>
8+
#include <string_view>
9+
#include <system_error>
10+
11+
extern "C" {
12+
#include <gg/ipc/client.h>
13+
}
14+
15+
namespace gg::ipc {
16+
17+
std::error_code Client::create_local_deployment(
18+
const GgCreateLocalDeploymentArgs &args,
19+
std::span<std::byte> deployment_id_mem,
20+
std::string_view *deployment_id
21+
) noexcept {
22+
GgBuffer id_buf = { reinterpret_cast<uint8_t *>(deployment_id_mem.data()),
23+
deployment_id_mem.size() };
24+
auto err = ggipc_create_local_deployment(
25+
&args, deployment_id != nullptr ? &id_buf : nullptr
26+
);
27+
if ((deployment_id != nullptr) && (err == GG_ERR_OK)) {
28+
*deployment_id
29+
= { reinterpret_cast<const char *>(id_buf.data), id_buf.len };
30+
}
31+
return err;
32+
}
33+
34+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// aws-greengrass-component-sdk - Lightweight AWS IoT Greengrass SDK
2+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
#include <gg/ipc/client.hpp>
6+
#include <gg/object.hpp>
7+
#include <cstddef>
8+
#include <cstdint>
9+
#include <array>
10+
#include <span>
11+
#include <string_view>
12+
#include <system_error>
13+
14+
extern "C" {
15+
#include <gg/buffer.h>
16+
#include <gg/ipc/mock.h>
17+
#include <gg/ipc/packet_sequences.h>
18+
#include <gg/map.h>
19+
#include <gg/object.h>
20+
#include <gg/test.h>
21+
#include <sys/types.h>
22+
#include <unistd.h>
23+
#include <unity.h>
24+
25+
GgError gg_process_wait(pid_t pid) noexcept;
26+
}
27+
28+
namespace {
29+
30+
// Build { "aws.greengrass.NucleusLite": { "merge": { merge_key: merge_val } } }
31+
// programmatically. C compound-literal macros (GG_MAP/gg_kv) don't compile
32+
// cleanly in C++, so we construct the nested structure with direct calls.
33+
GgMap build_nucleus_merge_map(
34+
GgBuffer merge_key,
35+
GgBuffer merge_val,
36+
GgKV *kv_storage,
37+
GgObject *obj_storage
38+
) {
39+
// Innermost: { merge_key: merge_val }
40+
kv_storage[0] = gg_kv(merge_key, gg_obj_buf(merge_val));
41+
obj_storage[0] = gg_obj_map(GgMap { .pairs = &kv_storage[0], .len = 1 });
42+
43+
// Middle: { "merge": <inner> }
44+
kv_storage[1] = gg_kv(GG_STR("merge"), obj_storage[0]);
45+
obj_storage[1] = gg_obj_map(GgMap { .pairs = &kv_storage[1], .len = 1 });
46+
47+
// Outer: { "aws.greengrass.NucleusLite": <middle> }
48+
kv_storage[2] = gg_kv(GG_STR("aws.greengrass.NucleusLite"), obj_storage[1]);
49+
return GgMap { .pairs = &kv_storage[2], .len = 1 };
50+
}
51+
52+
} // namespace
53+
54+
GG_TEST_DEFINE(cpp_create_local_deployment_okay) {
55+
GgBuffer expected_id = GG_STR("cpp-deploy-id-123");
56+
57+
GgKV kv_storage[3];
58+
GgObject obj_storage[2];
59+
GgMap component_to_config = build_nucleus_merge_map(
60+
GG_STR("url"), GG_STR("http://127.0.0.1:3129"), kv_storage, obj_storage
61+
);
62+
63+
std::array<std::byte, 64> id_mem {};
64+
65+
pid_t pid = fork();
66+
TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork failed");
67+
68+
if (pid == 0) {
69+
auto &client = gg::ipc::Client::get();
70+
GG_TEST_ASSERT_OK(client.connect().value());
71+
72+
GgCreateLocalDeploymentArgs args = {};
73+
args.component_to_configuration = component_to_config;
74+
75+
std::string_view deployment_id;
76+
GG_TEST_ASSERT_OK(client
77+
.create_local_deployment(
78+
args, std::span(id_mem), &deployment_id
79+
)
80+
.value());
81+
82+
TEST_ASSERT_EQUAL_STRING_LEN(
83+
"cpp-deploy-id-123", deployment_id.data(), deployment_id.size()
84+
);
85+
86+
TEST_PASS();
87+
}
88+
89+
GG_TEST_ASSERT_OK(gg_test_connect_request_disconnect_sequence(
90+
gg_test_create_local_deployment_accepted_sequence(
91+
1, gg_obj_map(component_to_config), expected_id
92+
),
93+
5
94+
));
95+
96+
GG_TEST_ASSERT_OK(gg_process_wait(pid));
97+
}
98+
99+
GG_TEST_DEFINE(cpp_create_local_deployment_no_output) {
100+
GgKV kv_storage[3];
101+
GgObject obj_storage[2];
102+
GgMap component_to_config = build_nucleus_merge_map(
103+
GG_STR("url"), GG_STR("http://127.0.0.1:3129"), kv_storage, obj_storage
104+
);
105+
106+
pid_t pid = fork();
107+
TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork failed");
108+
109+
if (pid == 0) {
110+
auto &client = gg::ipc::Client::get();
111+
GG_TEST_ASSERT_OK(client.connect().value());
112+
113+
GgCreateLocalDeploymentArgs args = {};
114+
args.component_to_configuration = component_to_config;
115+
116+
GG_TEST_ASSERT_OK(client.create_local_deployment(args).value());
117+
118+
TEST_PASS();
119+
}
120+
121+
GG_TEST_ASSERT_OK(gg_test_connect_request_disconnect_sequence(
122+
gg_test_create_local_deployment_accepted_sequence(
123+
1, gg_obj_map(component_to_config), GG_STR("any-id")
124+
),
125+
5
126+
));
127+
128+
GG_TEST_ASSERT_OK(gg_process_wait(pid));
129+
}

include/gg/ipc/client.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,41 @@ 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+
/// Arguments for ggipc_create_local_deployment.
120+
/// All fields are optional; zero-initialize the struct to omit all fields
121+
/// (zero-initialized maps/lists are valid empty maps/lists).
122+
typedef struct {
123+
/// Map of component name → configuration merge map.
124+
GgMap component_to_configuration;
125+
/// Map of component name → version string to add as root components.
126+
GgMap root_component_versions_to_add;
127+
/// List of component name buffers to remove from root components.
128+
GgList root_components_to_remove;
129+
/// Path to a directory containing local recipe files.
130+
GgBuffer recipe_directory_path;
131+
/// Path to a directory containing local artifact files.
132+
GgBuffer artifacts_directory_path;
133+
/// Failure handling policy: "ROLLBACK" or "DO_NOTHING".
134+
GgBuffer failure_handling_policy;
135+
} GgCreateLocalDeploymentArgs;
136+
137+
/// Create a local deployment on the core device.
138+
/// Triggers a local deployment that can merge configuration into components,
139+
/// add/remove root components, and specify local
140+
/// recipe/artifact paths to overwrite the Greengrass recipe/artifact stores.
141+
///
142+
/// If `deployment_id` is not NULL, its `data` must point to a writable buffer
143+
/// of at least `deployment_id->len` bytes. On success, `deployment_id->len` is
144+
/// updated to the actual length of the returned deployment id string, which is
145+
/// written into the caller-provided buffer.
146+
///
147+
/// See:
148+
/// <https://docs.aws.amazon.com/greengrass/v2/developerguide/ipc-local-deployments-components.html#ipc-operation-createlocaldeployment>
149+
ACCESS(read_write, 2)
150+
GgError ggipc_create_local_deployment(
151+
const GgCreateLocalDeploymentArgs *args, GgBuffer *deployment_id
152+
) NONNULL(1);
153+
119154
/// Get component configuration value.
120155
/// Retrieves configuration for the specified key path.
121156
/// 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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,22 @@ 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+
97+
GgipcPacketSequence gg_test_create_local_deployment_versions_accepted_sequence(
98+
int32_t stream_id,
99+
GgObject root_component_versions_to_add,
100+
GgBuffer deployment_id
101+
);
102+
87103
// Shadow sequences
88104

89105
GgipcPacketSequence gg_test_shadow_update_accepted_sequence(

mock/packets/component_packet_sequences.c

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,101 @@ 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_ACCEPTED_HEADERS(
148+
stream_id, "aws.greengrass#CreateLocalDeployment"
149+
),
150+
.header_count = GG_IPC_ACCEPTED_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+
}
180+
181+
GgipcPacket gg_test_create_local_deployment_versions_request_packet(
182+
int32_t stream_id, GgObject root_component_versions_to_add
183+
) {
184+
static GgKV payload[1];
185+
payload[0] = gg_kv(
186+
GG_STR("rootComponentVersionsToAdd"), root_component_versions_to_add
187+
);
188+
size_t payload_len = sizeof(payload) / sizeof(payload[0]);
189+
190+
return (GgipcPacket) { .direction = CLIENT_TO_SERVER,
191+
.has_payload = true,
192+
.payload = gg_obj_map((GgMap) {
193+
.pairs = payload, .len = payload_len }),
194+
.headers = GG_IPC_REQUEST_HEADERS(
195+
stream_id, "aws.greengrass#CreateLocalDeployment"
196+
),
197+
.header_count = GG_IPC_REQUEST_HEADERS_COUNT };
198+
}
199+
200+
GgipcPacketSequence gg_test_create_local_deployment_versions_accepted_sequence(
201+
int32_t stream_id,
202+
GgObject root_component_versions_to_add,
203+
GgBuffer deployment_id
204+
) {
205+
return (GgipcPacketSequence) {
206+
.packets = { gg_test_create_local_deployment_versions_request_packet(
207+
stream_id, root_component_versions_to_add
208+
),
209+
gg_test_create_local_deployment_response_packet(
210+
stream_id, deployment_id
211+
) },
212+
.len = 2
213+
};
214+
}

mock/packets/packets.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,18 @@ 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+
176+
GgipcPacket gg_test_create_local_deployment_versions_request_packet(
177+
int32_t stream_id, GgObject root_component_versions_to_add
178+
);
179+
168180
// Shadow operations
169181

170182
GgipcPacket gg_test_shadow_update_request_packet(

0 commit comments

Comments
 (0)