mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(workspace): stabilise the development stack (#2024)
This commit is contained in:
1 parent
cf1a7d7a8a
commit
5d2e5932a4
55 files changed
+1848
-3396
No files matched your search
@@ -59,7 +59,7 @@ env_services_config() ->
|
||||
|
||||
-spec env_gateway_config() -> map().
|
||||
env_gateway_config() ->
|
||||
maps:merge(env_gateway_base_config(), env_gateway_hotpatch_config()).
|
||||
env_gateway_base_config().
|
||||
|
||||
-spec env_gateway_base_config() -> map().
|
||||
env_gateway_base_config() ->
|
||||
@@ -107,36 +107,6 @@ env_gateway_base_config() ->
|
||||
)
|
||||
}.
|
||||
|
||||
-spec env_gateway_hotpatch_config() -> map().
|
||||
env_gateway_hotpatch_config() ->
|
||||
#{
|
||||
<<"hotpatch_enabled">> => env_bool("FLUXER_GATEWAY_HOTPATCH_ENABLED", false),
|
||||
<<"hotpatch_cassandra_hosts">> => env_optional_binary(
|
||||
"FLUXER_GATEWAY_HOTPATCH_CASSANDRA_HOSTS"
|
||||
),
|
||||
<<"hotpatch_cassandra_port">> => env_int(
|
||||
"FLUXER_GATEWAY_HOTPATCH_CASSANDRA_PORT", 9042
|
||||
),
|
||||
<<"hotpatch_cassandra_keyspace">> => env_binary(
|
||||
"FLUXER_GATEWAY_HOTPATCH_CASSANDRA_KEYSPACE", <<"fluxer">>
|
||||
),
|
||||
<<"hotpatch_cassandra_username">> => env_optional_binary(
|
||||
"FLUXER_GATEWAY_HOTPATCH_CASSANDRA_USERNAME"
|
||||
),
|
||||
<<"hotpatch_cassandra_password">> => env_optional_binary(
|
||||
"FLUXER_GATEWAY_HOTPATCH_CASSANDRA_PASSWORD"
|
||||
),
|
||||
<<"hotpatch_public_keys">> => env_optional_binary(
|
||||
"FLUXER_GATEWAY_HOTPATCH_PUBLIC_KEYS"
|
||||
),
|
||||
<<"hotpatch_poll_interval_ms">> => env_int(
|
||||
"FLUXER_GATEWAY_HOTPATCH_POLL_INTERVAL_MS", 5000
|
||||
),
|
||||
<<"hotpatch_startup_sync_timeout_ms">> => env_int(
|
||||
"FLUXER_GATEWAY_HOTPATCH_STARTUP_SYNC_TIMEOUT_MS", 30000
|
||||
)
|
||||
}.
|
||||
|
||||
-spec env_nats_config() -> map().
|
||||
env_nats_config() ->
|
||||
#{
|
||||
@@ -295,20 +265,6 @@ build_http_config(Service) ->
|
||||
),
|
||||
gateway_http_recovery_timeout_ms =>
|
||||
get_int(Service, <<"gateway_http_recovery_timeout_ms">>, 15000),
|
||||
hotpatch_enabled => get_bool(Service, <<"hotpatch_enabled">>, false),
|
||||
hotpatch_cassandra_hosts =>
|
||||
optional_string(get_optional_binary(Service, <<"hotpatch_cassandra_hosts">>)),
|
||||
hotpatch_cassandra_port => get_int(Service, <<"hotpatch_cassandra_port">>, 9042),
|
||||
hotpatch_cassandra_keyspace =>
|
||||
get_binary(Service, <<"hotpatch_cassandra_keyspace">>, <<"fluxer">>),
|
||||
hotpatch_cassandra_username =>
|
||||
get_optional_binary(Service, <<"hotpatch_cassandra_username">>),
|
||||
hotpatch_cassandra_password =>
|
||||
get_optional_binary(Service, <<"hotpatch_cassandra_password">>),
|
||||
hotpatch_public_keys => get_optional_binary(Service, <<"hotpatch_public_keys">>),
|
||||
hotpatch_poll_interval_ms => get_int(Service, <<"hotpatch_poll_interval_ms">>, 5000),
|
||||
hotpatch_startup_sync_timeout_ms =>
|
||||
get_int(Service, <<"hotpatch_startup_sync_timeout_ms">>, 30000),
|
||||
gateway_http_cleanup_interval_ms =>
|
||||
get_int(Service, <<"gateway_http_cleanup_interval_ms">>, 30000),
|
||||
gateway_http_cleanup_max_age_ms =>
|
||||
|
||||
@@ -38,8 +38,7 @@ common_children() ->
|
||||
child_spec(gateway_nats_pool, gateway_nats_pool),
|
||||
child_spec(gateway_event_pause, gateway_event_pause),
|
||||
child_spec(gateway_concurrency, gateway_concurrency),
|
||||
child_spec(gateway_rollout_config, gateway_rollout_config),
|
||||
child_spec(gateway_hotpatch_reconciler, gateway_hotpatch_reconciler)
|
||||
child_spec(gateway_rollout_config, gateway_rollout_config)
|
||||
] ++ cluster_children() ++
|
||||
[
|
||||
child_spec(gateway_dispatch_relay, gateway_dispatch_relay),
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
-module(gateway_hotpatch_bundle).
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([
|
||||
compress_term/1,
|
||||
decompress_term/1,
|
||||
bundle_hash/1,
|
||||
signing_payload/1,
|
||||
sign/2,
|
||||
verify_signature/4,
|
||||
parse_public_keys/1,
|
||||
decode_signed_event/2
|
||||
]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-endif.
|
||||
|
||||
-define(DOMAIN, <<"fluxer-gateway-hotpatch-v1">>).
|
||||
-define(ZSTD_LEVEL, 3).
|
||||
|
||||
-spec compress_term(term()) -> {ok, binary()} | {error, term()}.
|
||||
compress_term(Term) ->
|
||||
compress_binary(term_to_binary(Term, [deterministic])).
|
||||
|
||||
-spec decompress_term(binary()) -> {ok, term()} | {error, term()}.
|
||||
decompress_term(Compressed) when is_binary(Compressed) ->
|
||||
case decompress_binary(Compressed) of
|
||||
{ok, Binary} -> decode_term(Binary);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec decode_term(binary()) -> {ok, term()} | {error, term()}.
|
||||
decode_term(Binary) ->
|
||||
try
|
||||
{ok, binary_to_term(Binary, [safe])}
|
||||
catch
|
||||
Class:Reason -> {error, {invalid_term, Class, Reason}}
|
||||
end.
|
||||
|
||||
-spec bundle_hash(binary()) -> binary().
|
||||
bundle_hash(CompressedBundle) when is_binary(CompressedBundle) ->
|
||||
crypto:hash(sha256, CompressedBundle).
|
||||
|
||||
-spec signing_payload(binary()) -> binary().
|
||||
signing_payload(CompressedBundle) when is_binary(CompressedBundle) ->
|
||||
<<?DOMAIN/binary, 0, CompressedBundle/binary>>.
|
||||
|
||||
-spec sign(binary(), binary()) -> {ok, binary()} | {error, term()}.
|
||||
sign(CompressedBundle, PrivateKey) when is_binary(CompressedBundle), is_binary(PrivateKey) ->
|
||||
try
|
||||
{ok, crypto:sign(eddsa, none, signing_payload(CompressedBundle), [PrivateKey, ed25519])}
|
||||
catch
|
||||
Class:Reason -> {error, {sign_failed, Class, Reason}}
|
||||
end.
|
||||
|
||||
-spec verify_signature(binary(), binary(), binary(), #{binary() => binary()}) ->
|
||||
ok | {error, term()}.
|
||||
verify_signature(CompressedBundle, SignerKeyId, Signature, PublicKeys) when
|
||||
is_binary(CompressedBundle),
|
||||
is_binary(SignerKeyId),
|
||||
is_binary(Signature),
|
||||
is_map(PublicKeys)
|
||||
->
|
||||
case maps:get(SignerKeyId, PublicKeys, undefined) of
|
||||
undefined ->
|
||||
{error, {unknown_signer, SignerKeyId}};
|
||||
PublicKey ->
|
||||
verify_with_key(CompressedBundle, Signature, PublicKey)
|
||||
end.
|
||||
|
||||
-spec parse_public_keys(term()) ->
|
||||
{ok, #{binary() => binary()}} | {error, term()}.
|
||||
parse_public_keys(undefined) ->
|
||||
{ok, #{}};
|
||||
parse_public_keys(Value) when is_list(Value) ->
|
||||
parse_public_keys(type_conv:ensure_binary(Value));
|
||||
parse_public_keys(Value) when is_binary(Value) ->
|
||||
Tokens = [
|
||||
string:trim(Token)
|
||||
|| Token <- binary:split(Value, [<<",">>, <<"\n">>, <<";">>], [global]),
|
||||
string:trim(Token) =/= <<>>
|
||||
],
|
||||
parse_public_key_tokens(Tokens, #{});
|
||||
parse_public_keys(Value) ->
|
||||
{error, {invalid_public_keys_config, Value}}.
|
||||
|
||||
-spec decode_signed_event(map(), #{binary() => binary()}) -> {ok, map()} | {error, term()}.
|
||||
decode_signed_event(Event, PublicKeys) when is_map(Event), is_map(PublicKeys) ->
|
||||
Bundle = event_value(bundle, Event),
|
||||
SignerKeyId = event_value(signer_key_id, Event),
|
||||
Signature = event_value(signature, Event),
|
||||
BundleHash = event_value(bundle_sha256, Event),
|
||||
decode_event_payload(Bundle, SignerKeyId, Signature, BundleHash, PublicKeys).
|
||||
|
||||
-spec decode_event_payload(term(), term(), term(), term(), #{binary() => binary()}) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
decode_event_payload(Bundle, SignerKeyId, Signature, BundleHash, PublicKeys) when
|
||||
is_binary(Bundle), is_binary(SignerKeyId), is_binary(Signature), is_binary(BundleHash)
|
||||
->
|
||||
case validate_event_hash(Bundle, BundleHash) of
|
||||
ok -> decode_verified_event(Bundle, SignerKeyId, Signature, PublicKeys);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end;
|
||||
decode_event_payload(_Bundle, _SignerKeyId, _Signature, _BundleHash, _PublicKeys) ->
|
||||
{error, invalid_event_payload}.
|
||||
|
||||
-spec decode_verified_event(binary(), binary(), binary(), #{binary() => binary()}) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
decode_verified_event(Bundle, SignerKeyId, Signature, PublicKeys) ->
|
||||
case verify_signature(Bundle, SignerKeyId, Signature, PublicKeys) of
|
||||
ok -> decompress_bundle_map(Bundle);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec decompress_bundle_map(binary()) -> {ok, map()} | {error, term()}.
|
||||
decompress_bundle_map(Bundle) ->
|
||||
case decompress_term(Bundle) of
|
||||
{ok, Map} when is_map(Map) -> {ok, Map};
|
||||
{ok, Other} -> {error, {invalid_bundle_term, Other}};
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec compress_binary(binary()) -> {ok, binary()} | {error, term()}.
|
||||
compress_binary(Binary) ->
|
||||
try erlang:apply(ezstd, compress, [Binary, ?ZSTD_LEVEL]) of
|
||||
Compressed when is_binary(Compressed) -> {ok, Compressed};
|
||||
{error, Reason} -> {error, {compress_failed, Reason}};
|
||||
Other -> {error, {compress_failed, Other}}
|
||||
catch
|
||||
Class:Reason -> {error, {compress_failed, Class, Reason}}
|
||||
end.
|
||||
|
||||
-spec decompress_binary(binary()) -> {ok, binary()} | {error, term()}.
|
||||
decompress_binary(Binary) ->
|
||||
try erlang:apply(ezstd, decompress, [Binary]) of
|
||||
Decompressed when is_binary(Decompressed) -> {ok, Decompressed};
|
||||
Decompressed when is_list(Decompressed) -> {ok, iolist_to_binary(Decompressed)};
|
||||
{error, Reason} -> {error, {decompress_failed, Reason}};
|
||||
Other -> {error, {decompress_failed, Other}}
|
||||
catch
|
||||
Class:Reason -> {error, {decompress_failed, Class, Reason}}
|
||||
end.
|
||||
|
||||
-spec verify_with_key(binary(), binary(), binary()) -> ok | {error, term()}.
|
||||
verify_with_key(CompressedBundle, Signature, PublicKey) when byte_size(PublicKey) =:= 32 ->
|
||||
verify_with_valid_key(CompressedBundle, Signature, PublicKey);
|
||||
verify_with_key(_CompressedBundle, _Signature, PublicKey) ->
|
||||
{error, {invalid_public_key_size, byte_size(PublicKey)}}.
|
||||
|
||||
-spec verify_with_valid_key(binary(), binary(), binary()) -> ok | {error, term()}.
|
||||
verify_with_valid_key(CompressedBundle, Signature, PublicKey) ->
|
||||
try
|
||||
crypto:verify(eddsa, none, signing_payload(CompressedBundle), Signature, [
|
||||
PublicKey, ed25519
|
||||
])
|
||||
of
|
||||
true -> ok;
|
||||
false -> {error, invalid_signature}
|
||||
catch
|
||||
Class:Reason -> {error, {verify_failed, Class, Reason}}
|
||||
end.
|
||||
|
||||
-spec parse_public_key_tokens([binary()], #{binary() => binary()}) ->
|
||||
{ok, #{binary() => binary()}} | {error, term()}.
|
||||
parse_public_key_tokens([], Acc) ->
|
||||
{ok, Acc};
|
||||
parse_public_key_tokens([Token | Rest], Acc) ->
|
||||
case parse_public_key_token(Token) of
|
||||
{ok, KeyId, PublicKey} -> parse_public_key_tokens(Rest, Acc#{KeyId => PublicKey});
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec parse_public_key_token(binary()) -> {ok, binary(), binary()} | {error, term()}.
|
||||
parse_public_key_token(Token) ->
|
||||
case split_key_token(Token) of
|
||||
{ok, KeyId, Encoded} -> parse_public_key_material(KeyId, Encoded);
|
||||
error -> {error, {invalid_public_key_token, Token}}
|
||||
end.
|
||||
|
||||
-spec parse_public_key_material(binary(), binary()) ->
|
||||
{ok, binary(), binary()} | {error, term()}.
|
||||
parse_public_key_material(KeyId, Encoded) ->
|
||||
case decode_key_material(Encoded) of
|
||||
{ok, PublicKey} when byte_size(PublicKey) =:= 32 -> {ok, KeyId, PublicKey};
|
||||
{ok, PublicKey} -> {error, {invalid_public_key_size, KeyId, byte_size(PublicKey)}};
|
||||
{error, Reason} -> {error, {invalid_public_key, KeyId, Reason}}
|
||||
end.
|
||||
|
||||
-spec split_key_token(binary()) -> {ok, binary(), binary()} | error.
|
||||
split_key_token(Token) ->
|
||||
case binary:split(Token, <<":">>) of
|
||||
[KeyId, Encoded] -> {ok, string:trim(KeyId), string:trim(Encoded)};
|
||||
_ -> split_key_token_equals(Token)
|
||||
end.
|
||||
|
||||
-spec split_key_token_equals(binary()) -> {ok, binary(), binary()} | error.
|
||||
split_key_token_equals(Token) ->
|
||||
case binary:split(Token, <<"=">>) of
|
||||
[KeyId, Encoded] -> {ok, string:trim(KeyId), string:trim(Encoded)};
|
||||
_ -> error
|
||||
end.
|
||||
|
||||
-spec decode_key_material(binary()) -> {ok, binary()} | {error, term()}.
|
||||
decode_key_material(Encoded) ->
|
||||
case try_base64(Encoded) of
|
||||
{ok, Decoded} -> {ok, Decoded};
|
||||
{error, _} -> try_base64url(Encoded)
|
||||
end.
|
||||
|
||||
-spec try_base64(binary()) -> {ok, binary()} | {error, term()}.
|
||||
try_base64(Encoded) ->
|
||||
try
|
||||
{ok, base64:decode(Encoded)}
|
||||
catch
|
||||
Class:Reason -> {error, {Class, Reason}}
|
||||
end.
|
||||
|
||||
-spec try_base64url(binary()) -> {ok, binary()} | {error, term()}.
|
||||
try_base64url(Encoded) ->
|
||||
try
|
||||
{ok, base64url:decode(Encoded)}
|
||||
catch
|
||||
Class:Reason -> {error, {Class, Reason}}
|
||||
end.
|
||||
|
||||
-spec validate_event_hash(binary(), binary()) -> ok | {error, term()}.
|
||||
validate_event_hash(Bundle, BundleHash) ->
|
||||
case bundle_hash(Bundle) of
|
||||
BundleHash -> ok;
|
||||
Other -> {error, {bundle_hash_mismatch, Other, BundleHash}}
|
||||
end.
|
||||
|
||||
-spec event_value(atom(), map()) -> term().
|
||||
event_value(Key, Event) ->
|
||||
maps:get(Key, Event, maps:get(atom_to_binary(Key, utf8), Event, undefined)).
|
||||
|
||||
-ifdef(TEST).
|
||||
|
||||
compress_decompress_roundtrip_test() ->
|
||||
Term = #{
|
||||
version => 1,
|
||||
build_sha => <<"abc123">>,
|
||||
modules => [#{module => <<"session_lifecycle">>, expected_current_md5 => <<0:128>>}]
|
||||
},
|
||||
{ok, Compressed} = compress_term(Term),
|
||||
?assert(is_binary(Compressed)),
|
||||
?assertEqual({ok, Term}, decompress_term(Compressed)).
|
||||
|
||||
sign_and_verify_roundtrip_test() ->
|
||||
{PublicKey, PrivateKey} = ed25519_keypair(),
|
||||
{ok, Compressed} = compress_term(#{version => 1}),
|
||||
{ok, Signature} = sign(Compressed, PrivateKey),
|
||||
Keys = #{<<"ops">> => PublicKey},
|
||||
?assertEqual(ok, verify_signature(Compressed, <<"ops">>, Signature, Keys)),
|
||||
?assertEqual(
|
||||
{error, invalid_signature},
|
||||
verify_signature(<<Compressed/binary, 0>>, <<"ops">>, Signature, Keys)
|
||||
).
|
||||
|
||||
decode_signed_event_rejects_hash_mismatch_test() ->
|
||||
{PublicKey, PrivateKey} = ed25519_keypair(),
|
||||
{ok, Compressed} = compress_term(#{version => 1}),
|
||||
{ok, Signature} = sign(Compressed, PrivateKey),
|
||||
Event = #{
|
||||
signer_key_id => <<"ops">>,
|
||||
signature => Signature,
|
||||
bundle_sha256 => <<0:256>>,
|
||||
bundle => Compressed
|
||||
},
|
||||
?assertMatch(
|
||||
{error, {bundle_hash_mismatch, _, _}},
|
||||
decode_signed_event(Event, #{<<"ops">> => PublicKey})
|
||||
).
|
||||
|
||||
parse_public_keys_test() ->
|
||||
PublicKey = <<1:256>>,
|
||||
Encoded = base64:encode(PublicKey),
|
||||
?assertEqual(
|
||||
{ok, #{<<"ops">> => PublicKey}}, parse_public_keys(<<"ops:", Encoded/binary>>)
|
||||
).
|
||||
|
||||
ed25519_keypair() ->
|
||||
{PublicKey, PrivateKey} = crypto:generate_key(eddsa, ed25519),
|
||||
{require_binary(PublicKey), require_binary(PrivateKey)}.
|
||||
|
||||
require_binary(Value) when is_binary(Value) ->
|
||||
Value.
|
||||
|
||||
-endif.
|
||||
@@ -1,459 +0,0 @@
|
||||
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
-module(gateway_hotpatch_cli).
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([main/1, build_bundle/2, sign_bundle/3]).
|
||||
|
||||
-spec main([string()]) -> no_return().
|
||||
main(Args) ->
|
||||
halt(run_main(Args)).
|
||||
|
||||
-spec run_main([string()]) -> non_neg_integer().
|
||||
run_main(["bundle", BuildSha | ModuleOrder]) when ModuleOrder =/= [] ->
|
||||
write_json_result(build_bundle(type_conv:ensure_binary(BuildSha), ModuleOrder));
|
||||
run_main(["sign", SignerKeyId, PrivateKeyPath, BundlePath]) ->
|
||||
write_json_result(sign_bundle_file(SignerKeyId, PrivateKeyPath, BundlePath));
|
||||
run_main(["append", BuildSha, CreatedBy, EventPath]) ->
|
||||
Result = append_event(
|
||||
type_conv:ensure_binary(BuildSha), type_conv:ensure_binary(CreatedBy), EventPath
|
||||
),
|
||||
write_append_result(Result);
|
||||
run_main(_Args) ->
|
||||
write_usage(),
|
||||
64.
|
||||
|
||||
-spec build_bundle(binary(), [string()]) -> {ok, map()} | {error, term()}.
|
||||
build_bundle(BuildSha, ModuleSpecs) when is_binary(BuildSha), is_list(ModuleSpecs) ->
|
||||
build_bundle_modules(ModuleSpecs, fun(Modules) ->
|
||||
{ok, #{<<"version">> => 1, <<"build_sha">> => BuildSha, <<"modules">> => Modules}}
|
||||
end).
|
||||
|
||||
-spec sign_bundle(binary(), file:filename(), binary()) -> {ok, map()} | {error, term()}.
|
||||
sign_bundle(SignerKeyId, PrivateKeyPath, BundleJson) when
|
||||
is_binary(SignerKeyId), is_binary(BundleJson)
|
||||
->
|
||||
case decode_json(BundleJson) of
|
||||
{ok, Bundle} -> sign_decoded_bundle_with_key(SignerKeyId, PrivateKeyPath, Bundle);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec sign_decoded_bundle_with_key(binary(), file:filename(), term()) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
sign_decoded_bundle_with_key(SignerKeyId, PrivateKeyPath, Bundle) ->
|
||||
case read_private_key(PrivateKeyPath) of
|
||||
{ok, PrivateKey} -> sign_decoded_bundle(SignerKeyId, Bundle, PrivateKey);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec sign_bundle_file(string(), file:filename(), file:filename()) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
sign_bundle_file(SignerKeyId, PrivateKeyPath, BundlePath) ->
|
||||
case file:read_file(BundlePath) of
|
||||
{ok, BundleJson} ->
|
||||
sign_bundle(type_conv:ensure_binary(SignerKeyId), PrivateKeyPath, BundleJson);
|
||||
{error, Reason} ->
|
||||
{error, {read_bundle_failed, Reason}}
|
||||
end.
|
||||
|
||||
-spec sign_decoded_bundle(binary(), term(), binary()) -> {ok, map()} | {error, term()}.
|
||||
sign_decoded_bundle(SignerKeyId, BundleJson, PrivateKey) ->
|
||||
case json_to_bundle(BundleJson) of
|
||||
{ok, Bundle} -> sign_bundle_term(SignerKeyId, Bundle, PrivateKey);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec sign_bundle_term(binary(), map(), binary()) -> {ok, map()} | {error, term()}.
|
||||
sign_bundle_term(SignerKeyId, Bundle, PrivateKey) ->
|
||||
case gateway_hotpatch_bundle:compress_term(Bundle) of
|
||||
{ok, Compressed} -> sign_compressed_bundle(SignerKeyId, Compressed, PrivateKey);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec sign_compressed_bundle(binary(), binary(), binary()) -> {ok, map()} | {error, term()}.
|
||||
sign_compressed_bundle(SignerKeyId, Compressed, PrivateKey) ->
|
||||
case gateway_hotpatch_bundle:sign(Compressed, PrivateKey) of
|
||||
{ok, Signature} -> {ok, signed_event(SignerKeyId, Compressed, Signature)};
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec signed_event(binary(), binary(), binary()) -> map().
|
||||
signed_event(SignerKeyId, Compressed, Signature) ->
|
||||
#{
|
||||
<<"schema_version">> => 1,
|
||||
<<"kind">> => <<"beam_bundle">>,
|
||||
<<"created_by">> => gateway_hotpatch_runtime:node_name(),
|
||||
<<"signer_key_id">> => SignerKeyId,
|
||||
<<"bundle_sha256">> => encode_bytes(gateway_hotpatch_bundle:bundle_hash(Compressed)),
|
||||
<<"signature">> => encode_bytes(Signature),
|
||||
<<"bundle">> => encode_bytes(Compressed)
|
||||
}.
|
||||
|
||||
-spec append_event(binary(), binary(), file:filename()) -> {ok, binary()} | {error, term()}.
|
||||
append_event(BuildSha, CreatedBy, EventPath) ->
|
||||
case file:read_file(EventPath) of
|
||||
{ok, EventJson} -> append_event_json(BuildSha, CreatedBy, EventJson);
|
||||
{error, Reason} -> {error, {read_event_failed, Reason}}
|
||||
end.
|
||||
|
||||
-spec append_event_json(binary(), binary(), binary()) -> {ok, binary()} | {error, term()}.
|
||||
append_event_json(BuildSha, CreatedBy, EventJson) ->
|
||||
case decode_json(EventJson) of
|
||||
{ok, EventJsonTerm} -> append_event_term(BuildSha, CreatedBy, EventJsonTerm);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec append_event_term(binary(), binary(), term()) -> {ok, binary()} | {error, term()}.
|
||||
append_event_term(BuildSha, CreatedBy, EventJsonTerm) ->
|
||||
case json_to_event(EventJsonTerm) of
|
||||
{ok, Event} -> append_event_row(BuildSha, CreatedBy, Event);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec build_bundle_modules([string()], fun(([map()]) -> {ok, map()})) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
build_bundle_modules(ModuleSpecs, Fun) ->
|
||||
case build_bundle_module_list(ModuleSpecs, []) of
|
||||
{ok, Modules} -> Fun(Modules);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec build_bundle_module_list([string()], [map()]) -> {ok, [map()]} | {error, term()}.
|
||||
build_bundle_module_list([], Acc) ->
|
||||
{ok, lists:reverse(Acc)};
|
||||
build_bundle_module_list([ModuleSpec | Rest], Acc) ->
|
||||
case bundle_module(ModuleSpec) of
|
||||
{ok, Module} -> build_bundle_module_list(Rest, [Module | Acc]);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec bundle_module(string()) -> {ok, map()} | {error, term()}.
|
||||
bundle_module(ModuleSpec) ->
|
||||
case split_module_spec(ModuleSpec) of
|
||||
{ok, ModuleName, BeamPath0} -> bundle_named_module(ModuleName, BeamPath0);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec bundle_named_module(string(), file:filename() | undefined) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
bundle_named_module(ModuleName, BeamPath0) ->
|
||||
case existing_module(ModuleName) of
|
||||
{ok, Module} -> bundle_existing_module(Module, BeamPath0);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec bundle_existing_module(atom(), file:filename() | undefined) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
bundle_existing_module(Module, BeamPath0) ->
|
||||
case collect_bundle_parts(Module, BeamPath0) of
|
||||
{ok, CurrentMd5, TargetMd5, CompressedBeam} ->
|
||||
{ok, build_module_entry(Module, CurrentMd5, TargetMd5, CompressedBeam)};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec collect_bundle_parts(atom(), file:filename() | undefined) ->
|
||||
{ok, binary(), binary(), binary()} | {error, term()}.
|
||||
collect_bundle_parts(Module, BeamPath0) ->
|
||||
case gateway_hotpatch_loader:current_md5(Module) of
|
||||
{ok, CurrentMd5} -> collect_bundle_beam(Module, BeamPath0, CurrentMd5);
|
||||
{error, Reason} -> {error, {current_md5_failed, Module, Reason}}
|
||||
end.
|
||||
|
||||
-spec collect_bundle_beam(atom(), file:filename() | undefined, binary()) ->
|
||||
{ok, binary(), binary(), binary()} | {error, term()}.
|
||||
collect_bundle_beam(Module, BeamPath0, CurrentMd5) ->
|
||||
case resolve_beam_path(Module, BeamPath0) of
|
||||
{ok, BeamPath} -> read_bundle_beam(Module, BeamPath, CurrentMd5);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec read_bundle_beam(atom(), file:filename(), binary()) ->
|
||||
{ok, binary(), binary(), binary()} | {error, term()}.
|
||||
read_bundle_beam(Module, BeamPath, CurrentMd5) ->
|
||||
case file:read_file(BeamPath) of
|
||||
{ok, Beam} -> hash_bundle_beam(Module, CurrentMd5, Beam);
|
||||
{error, Reason} -> {error, {read_beam_failed, Module, BeamPath, Reason}}
|
||||
end.
|
||||
|
||||
-spec hash_bundle_beam(atom(), binary(), binary()) ->
|
||||
{ok, binary(), binary(), binary()} | {error, term()}.
|
||||
hash_bundle_beam(Module, CurrentMd5, Beam) ->
|
||||
case gateway_hotpatch_loader:beam_md5(Beam) of
|
||||
{ok, TargetMd5} -> compress_bundle_beam(Module, CurrentMd5, TargetMd5, Beam);
|
||||
{error, Reason} -> {error, {target_md5_failed, Module, Reason}}
|
||||
end.
|
||||
|
||||
-spec compress_bundle_beam(atom(), binary(), binary(), binary()) ->
|
||||
{ok, binary(), binary(), binary()} | {error, term()}.
|
||||
compress_bundle_beam(Module, CurrentMd5, TargetMd5, Beam) ->
|
||||
case compress_beam(Beam) of
|
||||
{ok, CompressedBeam} -> {ok, CurrentMd5, TargetMd5, CompressedBeam};
|
||||
{error, Reason} -> {error, {beam_compress_failed, Module, Reason}}
|
||||
end.
|
||||
|
||||
-spec build_module_entry(atom(), binary(), binary(), binary()) -> map().
|
||||
build_module_entry(Module, CurrentMd5, TargetMd5, CompressedBeam) ->
|
||||
#{
|
||||
<<"module">> => atom_to_binary(Module, utf8),
|
||||
<<"expected_current_md5">> => encode_bytes(CurrentMd5),
|
||||
<<"target_md5">> => encode_bytes(TargetMd5),
|
||||
<<"beam_zstd">> => encode_bytes(CompressedBeam)
|
||||
}.
|
||||
|
||||
-spec append_event_row(binary(), binary(), map()) -> {ok, binary()} | {error, term()}.
|
||||
append_event_row(BuildSha, CreatedBy, Event) ->
|
||||
_ = fluxer_gateway_env:load(),
|
||||
case gateway_hotpatch_store:connect() of
|
||||
ok ->
|
||||
gateway_hotpatch_store:append_event(
|
||||
BuildSha,
|
||||
CreatedBy,
|
||||
maps:get(signer_key_id, Event),
|
||||
maps:get(signature, Event),
|
||||
maps:get(bundle_sha256, Event),
|
||||
maps:get(bundle, Event)
|
||||
);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec split_module_spec(string()) ->
|
||||
{ok, string(), file:filename() | undefined} | {error, term()}.
|
||||
split_module_spec(ModuleSpec) ->
|
||||
case string:split(ModuleSpec, "=", leading) of
|
||||
[ModuleName, BeamPath] when ModuleName =/= "", BeamPath =/= "" ->
|
||||
{ok, ModuleName, BeamPath};
|
||||
[ModuleName] when ModuleName =/= "" ->
|
||||
{ok, ModuleName, undefined};
|
||||
_ ->
|
||||
{error, {invalid_module_spec, ModuleSpec}}
|
||||
end.
|
||||
|
||||
-spec existing_module(string()) -> {ok, atom()} | {error, term()}.
|
||||
existing_module(ModuleName) ->
|
||||
try
|
||||
{ok, list_to_existing_atom(ModuleName)}
|
||||
catch
|
||||
error:badarg -> {error, {unknown_module, ModuleName}}
|
||||
end.
|
||||
|
||||
-spec resolve_beam_path(atom(), file:filename() | undefined) ->
|
||||
{ok, file:filename()} | {error, term()}.
|
||||
resolve_beam_path(Module, undefined) ->
|
||||
case code:which(Module) of
|
||||
File when is_list(File) -> {ok, File};
|
||||
Other -> {error, {module_beam_not_found, Module, Other}}
|
||||
end;
|
||||
resolve_beam_path(_Module, BeamPath) ->
|
||||
{ok, BeamPath}.
|
||||
|
||||
-spec compress_beam(binary()) -> {ok, binary()} | {error, term()}.
|
||||
compress_beam(Beam) ->
|
||||
try erlang:apply(ezstd, compress, [Beam, 3]) of
|
||||
Compressed when is_binary(Compressed) -> {ok, Compressed};
|
||||
{error, Reason} -> {error, Reason};
|
||||
Other -> {error, Other}
|
||||
catch
|
||||
Class:Reason -> {error, {Class, Reason}}
|
||||
end.
|
||||
|
||||
-spec decode_json(binary()) -> {ok, term()} | {error, term()}.
|
||||
decode_json(Json) ->
|
||||
try
|
||||
{ok, json:decode(Json)}
|
||||
catch
|
||||
Class:Reason -> {error, {decode_json_failed, Class, Reason}}
|
||||
end.
|
||||
|
||||
-spec read_private_key(file:filename()) -> {ok, binary()} | {error, term()}.
|
||||
read_private_key(Path) ->
|
||||
case file:read_file(Path) of
|
||||
{ok, PrivateKey} when byte_size(PrivateKey) =:= 32 ->
|
||||
{ok, PrivateKey};
|
||||
{ok, PrivateKey} ->
|
||||
{error, {invalid_private_key_size, byte_size(PrivateKey)}};
|
||||
{error, Reason} ->
|
||||
{error, {read_private_key_failed, Reason}}
|
||||
end.
|
||||
|
||||
-spec json_to_bundle(term()) -> {ok, map()} | {error, term()}.
|
||||
json_to_bundle(#{
|
||||
<<"version">> := Version, <<"build_sha">> := BuildSha, <<"modules">> := Modules
|
||||
}) when is_list(Modules) ->
|
||||
case json_to_module_entries(Modules, []) of
|
||||
{ok, Entries} ->
|
||||
{ok, #{
|
||||
<<"version">> => Version,
|
||||
<<"build_sha">> => BuildSha,
|
||||
<<"modules">> => Entries
|
||||
}};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
json_to_bundle(Other) ->
|
||||
{error, {invalid_bundle_json, Other}}.
|
||||
|
||||
-spec json_to_module_entries([term()], [map()]) -> {ok, [map()]} | {error, term()}.
|
||||
json_to_module_entries([], Acc) ->
|
||||
{ok, lists:reverse(Acc)};
|
||||
json_to_module_entries([Module | Rest], Acc) ->
|
||||
case json_to_module_entry(Module) of
|
||||
{ok, Entry} -> json_to_module_entries(Rest, [Entry | Acc]);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec json_to_module_entry(term()) -> {ok, map()} | {error, term()}.
|
||||
json_to_module_entry(#{
|
||||
<<"module">> := Module,
|
||||
<<"expected_current_md5">> := ExpectedMd5,
|
||||
<<"target_md5">> := TargetMd5,
|
||||
<<"beam_zstd">> := BeamZstd
|
||||
}) ->
|
||||
decode_module_entry(Module, ExpectedMd5, TargetMd5, BeamZstd);
|
||||
json_to_module_entry(Other) ->
|
||||
{error, {invalid_module_entry_json, Other}}.
|
||||
|
||||
-spec decode_module_entry(term(), term(), term(), term()) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
decode_module_entry(Module, ExpectedMd5, TargetMd5, BeamZstd) when
|
||||
is_binary(Module), is_binary(ExpectedMd5), is_binary(TargetMd5), is_binary(BeamZstd)
|
||||
->
|
||||
case decode_bytes(ExpectedMd5) of
|
||||
{ok, ExpectedMd5Bytes} ->
|
||||
decode_module_entry_target(Module, ExpectedMd5Bytes, TargetMd5, BeamZstd);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
decode_module_entry(Module, ExpectedMd5, TargetMd5, BeamZstd) ->
|
||||
{error, {invalid_module_entry_json, {Module, ExpectedMd5, TargetMd5, BeamZstd}}}.
|
||||
|
||||
-spec decode_module_entry_target(binary(), binary(), binary(), binary()) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
decode_module_entry_target(Module, ExpectedMd5, TargetMd5, BeamZstd) ->
|
||||
case decode_bytes(TargetMd5) of
|
||||
{ok, TargetMd5Bytes} ->
|
||||
decode_module_entry_beam(Module, ExpectedMd5, TargetMd5Bytes, BeamZstd);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec decode_module_entry_beam(binary(), binary(), binary(), binary()) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
decode_module_entry_beam(Module, ExpectedMd5, TargetMd5, BeamZstd) ->
|
||||
case decode_bytes(BeamZstd) of
|
||||
{ok, BeamZstdBytes} ->
|
||||
{ok, #{
|
||||
<<"module">> => Module,
|
||||
<<"expected_current_md5">> => ExpectedMd5,
|
||||
<<"target_md5">> => TargetMd5,
|
||||
<<"beam_zstd">> => BeamZstdBytes
|
||||
}};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec json_to_event(term()) -> {ok, map()} | {error, term()}.
|
||||
json_to_event(#{
|
||||
<<"signer_key_id">> := SignerKeyId,
|
||||
<<"bundle_sha256">> := BundleSha256,
|
||||
<<"signature">> := Signature,
|
||||
<<"bundle">> := Bundle
|
||||
}) ->
|
||||
decode_event_bytes(SignerKeyId, BundleSha256, Signature, Bundle);
|
||||
json_to_event(Other) ->
|
||||
{error, {invalid_event_json, Other}}.
|
||||
|
||||
-spec decode_event_bytes(term(), term(), term(), term()) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
decode_event_bytes(SignerKeyId, BundleSha256, Signature, Bundle) when
|
||||
is_binary(SignerKeyId), is_binary(BundleSha256), is_binary(Signature), is_binary(Bundle)
|
||||
->
|
||||
case decode_bytes(BundleSha256) of
|
||||
{ok, BundleSha256Bytes} ->
|
||||
decode_event_signature(SignerKeyId, BundleSha256Bytes, Signature, Bundle);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
decode_event_bytes(SignerKeyId, BundleSha256, Signature, Bundle) ->
|
||||
{error, {invalid_event_json, {SignerKeyId, BundleSha256, Signature, Bundle}}}.
|
||||
|
||||
-spec decode_event_signature(binary(), binary(), binary(), binary()) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
decode_event_signature(SignerKeyId, BundleSha256, Signature, Bundle) ->
|
||||
case decode_bytes(Signature) of
|
||||
{ok, SignatureBytes} ->
|
||||
decode_event_bundle(SignerKeyId, BundleSha256, SignatureBytes, Bundle);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec decode_event_bundle(binary(), binary(), binary(), binary()) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
decode_event_bundle(SignerKeyId, BundleSha256, Signature, Bundle) ->
|
||||
case decode_bytes(Bundle) of
|
||||
{ok, BundleBytes} ->
|
||||
{ok, #{
|
||||
signer_key_id => SignerKeyId,
|
||||
bundle_sha256 => BundleSha256,
|
||||
signature => Signature,
|
||||
bundle => BundleBytes
|
||||
}};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec encode_bytes(binary()) -> binary().
|
||||
encode_bytes(Binary) ->
|
||||
base64:encode(Binary).
|
||||
|
||||
-spec decode_bytes(binary()) -> {ok, binary()} | {error, term()}.
|
||||
decode_bytes(Encoded) when is_binary(Encoded) ->
|
||||
try
|
||||
{ok, base64:decode(Encoded)}
|
||||
catch
|
||||
Class:Reason -> {error, {invalid_base64_bytes, Encoded, Class, Reason}}
|
||||
end.
|
||||
|
||||
-spec write_json_result({ok, map()} | {error, term()}) -> non_neg_integer().
|
||||
write_json_result({ok, Term}) ->
|
||||
write_stdout("~ts~n", [json:encode(Term)]),
|
||||
0;
|
||||
write_json_result({error, Reason}) ->
|
||||
write_error(Reason).
|
||||
|
||||
-spec write_append_result({ok, binary()} | {error, term()}) -> non_neg_integer().
|
||||
write_append_result({ok, EventId}) ->
|
||||
write_stdout("appended hotpatch event ~ts~n", [gateway_hotpatch_loader:hex(EventId)]),
|
||||
0;
|
||||
write_append_result({error, Reason}) ->
|
||||
write_error(Reason).
|
||||
|
||||
-spec write_error(term()) -> non_neg_integer().
|
||||
write_error(Reason) ->
|
||||
write_stderr("gateway hotpatch failed: ~0tp~n", [Reason]),
|
||||
1.
|
||||
|
||||
-spec write_usage() -> ok.
|
||||
write_usage() ->
|
||||
write_stderr(
|
||||
"usage: gateway_hotpatch_cli bundle BUILD_SHA module_a=/path/to/module_a.beam ...~n"
|
||||
" gateway_hotpatch_cli sign SIGNER_KEY_ID PRIVATE_KEY_RAW_FILE bundle.json~n"
|
||||
" gateway_hotpatch_cli append BUILD_SHA CREATED_BY signed-event.json~n",
|
||||
[]
|
||||
).
|
||||
|
||||
-spec write_stdout(io:format(), [term()]) -> ok.
|
||||
write_stdout(Format, Args) ->
|
||||
write_stream(standard_io, Format, Args).
|
||||
|
||||
-spec write_stderr(io:format(), [term()]) -> ok.
|
||||
write_stderr(Format, Args) ->
|
||||
write_stream(standard_error, Format, Args).
|
||||
|
||||
-spec write_stream(file:io_device() | standard_io | standard_error, io:format(), [term()]) ->
|
||||
ok.
|
||||
write_stream(Device, Format, Args) ->
|
||||
Output = iolist_to_binary(io_lib:format(Format, Args)),
|
||||
_ = file:write(Device, Output),
|
||||
ok.
|
||||
@@ -1,433 +0,0 @@
|
||||
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
-module(gateway_hotpatch_loader).
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([
|
||||
apply_bundle/1,
|
||||
current_md5/1,
|
||||
beam_module/1,
|
||||
beam_md5/1,
|
||||
hex/1
|
||||
]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-endif.
|
||||
|
||||
-type module_entry() :: map().
|
||||
-type apply_result() :: {ok, applied | skipped, atom(), binary()} | {error, term()}.
|
||||
|
||||
-spec apply_bundle(map()) -> {ok, map()} | {error, term()}.
|
||||
apply_bundle(Bundle) when is_map(Bundle) ->
|
||||
case
|
||||
{
|
||||
entry_value(version, Bundle),
|
||||
entry_value(build_sha, Bundle),
|
||||
entry_value(modules, Bundle)
|
||||
}
|
||||
of
|
||||
{1, BundleBuildSha, Modules} when is_list(Modules) ->
|
||||
apply_versioned_bundle(BundleBuildSha, Modules);
|
||||
_ ->
|
||||
{error, invalid_bundle}
|
||||
end;
|
||||
apply_bundle(_Bundle) ->
|
||||
{error, invalid_bundle}.
|
||||
|
||||
-spec apply_versioned_bundle(term(), [module_entry()]) -> {ok, map()} | {error, term()}.
|
||||
apply_versioned_bundle(BundleBuildSha, Modules) ->
|
||||
CurrentBuildSha = gateway_hotpatch_runtime:build_sha(),
|
||||
case normalize_binary(BundleBuildSha) of
|
||||
CurrentBuildSha -> apply_modules(Modules, #{applied => [], skipped => []});
|
||||
Other -> {error, {build_sha_mismatch, Other, CurrentBuildSha}}
|
||||
end.
|
||||
|
||||
-spec current_md5(atom()) -> {ok, binary()} | {error, term()}.
|
||||
current_md5(Module) when is_atom(Module) ->
|
||||
case hotpatch_loaded_md5(Module) of
|
||||
{ok, Md5} -> {ok, Md5};
|
||||
error -> current_md5_result(Module, current_md5_from_loaded_or_file(Module))
|
||||
end.
|
||||
|
||||
-spec current_md5_result(atom(), {ok, binary()} | {error, term()}) ->
|
||||
{ok, binary()} | {error, term()}.
|
||||
current_md5_result(_Module, {ok, Md5}) ->
|
||||
{ok, Md5};
|
||||
current_md5_result(Module, {error, Reason}) ->
|
||||
fallback_current_md5(Module, Reason).
|
||||
|
||||
-spec fallback_current_md5(atom(), term()) -> {ok, binary()} | {error, term()}.
|
||||
fallback_current_md5(Module, Reason) ->
|
||||
case hotpatch_loaded_md5(Module) of
|
||||
{ok, Md5} -> {ok, Md5};
|
||||
error -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec current_md5_from_loaded_or_file(atom()) -> {ok, binary()} | {error, term()}.
|
||||
current_md5_from_loaded_or_file(Module) ->
|
||||
case code:get_object_code(Module) of
|
||||
{Module, Beam, _File} when is_binary(Beam) -> beam_md5(Beam);
|
||||
error -> current_md5_from_file(Module)
|
||||
end.
|
||||
|
||||
-spec beam_module(binary()) -> {ok, atom()} | {error, term()}.
|
||||
beam_module(Beam) when is_binary(Beam) ->
|
||||
case beam_lib:info(Beam) of
|
||||
Info when is_list(Info) -> beam_module_from_info(Info);
|
||||
Other -> {error, {beam_info_failed, Other}}
|
||||
end.
|
||||
|
||||
-spec beam_module_from_info(list()) -> {ok, atom()} | {error, term()}.
|
||||
beam_module_from_info(Info) ->
|
||||
case lists:keyfind(module, 1, Info) of
|
||||
{module, Module} when is_atom(Module) -> {ok, Module};
|
||||
_ -> {error, module_not_found}
|
||||
end.
|
||||
|
||||
-spec beam_md5(binary()) -> {ok, binary()} | {error, term()}.
|
||||
beam_md5(Beam) when is_binary(Beam) ->
|
||||
case beam_lib:md5(Beam) of
|
||||
{ok, {_Module, Md5}} when is_binary(Md5) -> {ok, Md5};
|
||||
Error -> {error, {beam_md5_failed, Error}}
|
||||
end.
|
||||
|
||||
-spec hex(binary()) -> binary().
|
||||
hex(Binary) when is_binary(Binary) ->
|
||||
iolist_to_binary([[hex_nibble(High), hex_nibble(Low)] || <<High:4, Low:4>> <= Binary]).
|
||||
|
||||
-spec apply_modules([module_entry()], map()) -> {ok, map()} | {error, term()}.
|
||||
apply_modules([], Acc) ->
|
||||
{ok, Acc#{module_count => length(maps:get(applied, Acc)) + length(maps:get(skipped, Acc))}};
|
||||
apply_modules([Entry | Rest], Acc) ->
|
||||
case apply_module(Entry) of
|
||||
{ok, applied, Module, TargetMd5} ->
|
||||
apply_modules(
|
||||
Rest,
|
||||
Acc#{applied => [{Module, hex(TargetMd5)} | maps:get(applied, Acc)]}
|
||||
);
|
||||
{ok, skipped, Module, TargetMd5} ->
|
||||
apply_modules(
|
||||
Rest,
|
||||
Acc#{skipped => [{Module, hex(TargetMd5)} | maps:get(skipped, Acc)]}
|
||||
);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec apply_module(module_entry()) -> apply_result().
|
||||
apply_module(Entry) ->
|
||||
with_entry(Entry, fun apply_valid_entry/4).
|
||||
|
||||
-spec apply_valid_entry(atom(), binary(), binary(), binary()) -> apply_result().
|
||||
apply_valid_entry(Module, ExpectedMd5, TargetMd5, Beam) ->
|
||||
case current_md5(Module) of
|
||||
{ok, TargetMd5} ->
|
||||
{ok, skipped, Module, TargetMd5};
|
||||
{ok, ExpectedMd5} ->
|
||||
load_module(Module, Beam, TargetMd5);
|
||||
{ok, CurrentMd5} ->
|
||||
{error, {md5_mismatch, Module, hex(CurrentMd5), hex(ExpectedMd5)}};
|
||||
{error, Reason} ->
|
||||
{error, {current_md5_failed, Module, Reason}}
|
||||
end.
|
||||
|
||||
-spec with_entry(module_entry(), fun((atom(), binary(), binary(), binary()) -> apply_result())) ->
|
||||
apply_result().
|
||||
with_entry(Entry, Fun) when is_map(Entry), is_function(Fun, 4) ->
|
||||
case normalize_module(entry_value(module, Entry)) of
|
||||
{ok, Module} ->
|
||||
ExpectedMd5 = entry_value(expected_current_md5, Entry),
|
||||
TargetMd5 = entry_value(target_md5, Entry),
|
||||
BeamZstd = entry_value(beam_zstd, Entry),
|
||||
with_entry_beam(Module, ExpectedMd5, TargetMd5, BeamZstd, Fun);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
with_entry(_Entry, _Fun) ->
|
||||
{error, invalid_module_entry}.
|
||||
|
||||
-spec with_entry_beam(atom(), term(), term(), term(), fun(
|
||||
(atom(), binary(), binary(), binary()) -> apply_result()
|
||||
)) ->
|
||||
apply_result().
|
||||
with_entry_beam(Module, ExpectedMd5, TargetMd5, BeamZstd, Fun) when
|
||||
is_binary(ExpectedMd5),
|
||||
is_binary(TargetMd5),
|
||||
byte_size(ExpectedMd5) =:= 16,
|
||||
byte_size(TargetMd5) =:= 16,
|
||||
is_binary(BeamZstd)
|
||||
->
|
||||
case decompress_beam(BeamZstd) of
|
||||
{ok, Beam} -> validate_beam(Module, ExpectedMd5, TargetMd5, Beam, Fun);
|
||||
{error, Reason} -> {error, {beam_decompress_failed, Module, Reason}}
|
||||
end;
|
||||
with_entry_beam(Module, _ExpectedMd5, _TargetMd5, _BeamZstd, _Fun) ->
|
||||
{error, {invalid_module_entry, Module}}.
|
||||
|
||||
-spec validate_beam(atom(), binary(), binary(), binary(), fun(
|
||||
(atom(), binary(), binary(), binary()) -> apply_result()
|
||||
)) ->
|
||||
apply_result().
|
||||
validate_beam(Module, ExpectedMd5, TargetMd5, Beam, Fun) ->
|
||||
case beam_module(Beam) of
|
||||
{ok, Module} -> validate_beam_md5(Module, ExpectedMd5, TargetMd5, Beam, Fun);
|
||||
{ok, OtherModule} -> {error, {beam_module_mismatch, Module, OtherModule}};
|
||||
{error, Reason} -> {error, {beam_module_failed, Module, Reason}}
|
||||
end.
|
||||
|
||||
-spec validate_beam_md5(atom(), binary(), binary(), binary(), fun(
|
||||
(atom(), binary(), binary(), binary()) -> apply_result()
|
||||
)) ->
|
||||
apply_result().
|
||||
validate_beam_md5(Module, ExpectedMd5, TargetMd5, Beam, Fun) ->
|
||||
case beam_md5(Beam) of
|
||||
{ok, TargetMd5} ->
|
||||
Fun(Module, ExpectedMd5, TargetMd5, Beam);
|
||||
{ok, OtherMd5} ->
|
||||
{error, {target_md5_mismatch, Module, hex(OtherMd5), hex(TargetMd5)}};
|
||||
{error, Reason} ->
|
||||
{error, {target_md5_failed, Module, Reason}}
|
||||
end.
|
||||
|
||||
-spec load_module(atom(), binary(), binary()) -> apply_result().
|
||||
load_module(Module, Beam, TargetMd5) ->
|
||||
case code:soft_purge(Module) of
|
||||
true -> load_purged_module(Module, Beam, TargetMd5);
|
||||
false -> {error, {soft_purge_failed, Module}}
|
||||
end.
|
||||
|
||||
-spec load_purged_module(atom(), binary(), binary()) -> apply_result().
|
||||
load_purged_module(Module, Beam, TargetMd5) ->
|
||||
case code:load_binary(Module, atom_to_list(Module) ++ ".beam", Beam) of
|
||||
{module, Module} ->
|
||||
put_hotpatch_loaded_md5(Module, TargetMd5),
|
||||
verify_loaded_module(Module, TargetMd5);
|
||||
{error, Reason} ->
|
||||
{error, {load_binary_failed, Module, Reason}}
|
||||
end.
|
||||
|
||||
-spec verify_loaded_module(atom(), binary()) -> apply_result().
|
||||
verify_loaded_module(Module, TargetMd5) ->
|
||||
case current_md5(Module) of
|
||||
{ok, TargetMd5} ->
|
||||
{ok, applied, Module, TargetMd5};
|
||||
{ok, OtherMd5} ->
|
||||
{error, {post_load_md5_mismatch, Module, hex(OtherMd5), hex(TargetMd5)}};
|
||||
{error, Reason} ->
|
||||
{error, {post_load_md5_failed, Module, Reason}}
|
||||
end.
|
||||
|
||||
-spec current_md5_from_file(atom()) -> {ok, binary()} | {error, term()}.
|
||||
current_md5_from_file(Module) ->
|
||||
case code:which(Module) of
|
||||
File when is_list(File) -> current_md5_from_path(Module, File);
|
||||
preloaded -> {error, preloaded};
|
||||
non_existing -> {error, not_loaded};
|
||||
Other -> {error, {not_loadable, Other}}
|
||||
end.
|
||||
|
||||
-spec current_md5_from_path(atom(), file:filename()) -> {ok, binary()} | {error, term()}.
|
||||
current_md5_from_path(Module, File) ->
|
||||
case beam_lib:md5(File) of
|
||||
{ok, {Module, Md5}} when is_binary(Md5) -> {ok, Md5};
|
||||
Error -> {error, {beam_file_md5_failed, Error}}
|
||||
end.
|
||||
|
||||
-spec hotpatch_loaded_md5(atom()) -> {ok, binary()} | error.
|
||||
hotpatch_loaded_md5(Module) ->
|
||||
case persistent_term:get({?MODULE, loaded_md5, Module}, undefined) of
|
||||
Md5 when is_binary(Md5), byte_size(Md5) =:= 16 -> {ok, Md5};
|
||||
_ -> error
|
||||
end.
|
||||
|
||||
-spec put_hotpatch_loaded_md5(atom(), binary()) -> ok.
|
||||
put_hotpatch_loaded_md5(Module, Md5) ->
|
||||
persistent_term:put({?MODULE, loaded_md5, Module}, Md5).
|
||||
|
||||
-spec decompress_beam(binary()) -> {ok, binary()} | {error, term()}.
|
||||
decompress_beam(Compressed) ->
|
||||
try erlang:apply(ezstd, decompress, [Compressed]) of
|
||||
Beam when is_binary(Beam) -> {ok, Beam};
|
||||
Beam when is_list(Beam) -> {ok, iolist_to_binary(Beam)};
|
||||
{error, Reason} -> {error, Reason};
|
||||
Other -> {error, Other}
|
||||
catch
|
||||
Class:Reason -> {error, {Class, Reason}}
|
||||
end.
|
||||
|
||||
-spec normalize_module(term()) -> {ok, atom()} | {error, term()}.
|
||||
normalize_module(Module) when is_atom(Module) ->
|
||||
{ok, Module};
|
||||
normalize_module(Module) when is_binary(Module) ->
|
||||
try
|
||||
{ok, binary_to_existing_atom(Module, utf8)}
|
||||
catch
|
||||
error:badarg -> {error, {unknown_module, Module}}
|
||||
end;
|
||||
normalize_module(Module) when is_list(Module) ->
|
||||
normalize_module(type_conv:ensure_binary(Module));
|
||||
normalize_module(Module) ->
|
||||
{error, {invalid_module, Module}}.
|
||||
|
||||
-spec normalize_binary(term()) -> binary().
|
||||
normalize_binary(Bin) when is_binary(Bin) -> Bin;
|
||||
normalize_binary(List) when is_list(List) -> type_conv:ensure_binary(List);
|
||||
normalize_binary(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
|
||||
normalize_binary(Other) -> term_to_binary(Other).
|
||||
|
||||
-spec entry_value(atom(), map()) -> term().
|
||||
entry_value(Key, Entry) ->
|
||||
maps:get(Key, Entry, maps:get(atom_to_binary(Key, utf8), Entry, undefined)).
|
||||
|
||||
-spec hex_nibble(0..15) -> integer().
|
||||
hex_nibble(N) when N < 10 -> $0 + N;
|
||||
hex_nibble(N) -> $a + (N - 10).
|
||||
|
||||
-ifdef(TEST).
|
||||
|
||||
beam_module_and_md5_test() ->
|
||||
{module, ?MODULE} = code:ensure_loaded(?MODULE),
|
||||
{?MODULE, Binary, _File} = code:get_object_code(?MODULE),
|
||||
?assertEqual({ok, ?MODULE}, beam_module(Binary)),
|
||||
{ok, Md5} = beam_md5(Binary),
|
||||
?assertEqual(16, byte_size(Md5)).
|
||||
|
||||
apply_bundle_rejects_build_mismatch_test() ->
|
||||
Bundle = #{version => 1, build_sha => <<"definitely-not-this-build">>, modules => []},
|
||||
?assertMatch({error, {build_sha_mismatch, _, _}}, apply_bundle(Bundle)).
|
||||
|
||||
apply_bundle_accepts_binary_keys_test() ->
|
||||
Bundle = #{
|
||||
<<"version">> => 1,
|
||||
<<"build_sha">> => gateway_hotpatch_runtime:build_sha(),
|
||||
<<"modules">> => []
|
||||
},
|
||||
?assertEqual(
|
||||
{ok, #{applied => [], skipped => [], module_count => 0}}, apply_bundle(Bundle)
|
||||
).
|
||||
|
||||
apply_bundle_loads_new_beam_test() ->
|
||||
Module = gateway_hotpatch_loader_test_target,
|
||||
cleanup_test_module(Module),
|
||||
try
|
||||
Beam1 = compile_test_module(Module, 1),
|
||||
Beam2 = compile_test_module(Module, 2),
|
||||
{module, Module} = load_test_beam(Module, Beam1),
|
||||
?assertEqual(1, erlang:apply(Module, version, [])),
|
||||
{ok, ExpectedMd5} = current_md5(Module),
|
||||
{Entry, TargetMd5} = beam_entry(Module, ExpectedMd5, Beam2),
|
||||
Bundle = #{
|
||||
version => 1,
|
||||
build_sha => gateway_hotpatch_runtime:build_sha(),
|
||||
modules => [Entry]
|
||||
},
|
||||
?assertEqual(
|
||||
{ok, #{applied => [{Module, hex(TargetMd5)}], skipped => [], module_count => 1}},
|
||||
apply_bundle(Bundle)
|
||||
),
|
||||
?assertEqual(2, erlang:apply(Module, version, []))
|
||||
after
|
||||
cleanup_test_module(Module)
|
||||
end.
|
||||
|
||||
apply_bundle_leaves_prior_module_loaded_when_later_entry_fails_test() ->
|
||||
ModuleA = gateway_hotpatch_loader_test_partial_a,
|
||||
ModuleB = gateway_hotpatch_loader_test_partial_b,
|
||||
cleanup_test_module(ModuleA),
|
||||
cleanup_test_module(ModuleB),
|
||||
try
|
||||
BeamA1 = compile_test_module(ModuleA, 1),
|
||||
BeamA2 = compile_test_module(ModuleA, 2),
|
||||
BeamB1 = compile_test_module(ModuleB, 1),
|
||||
BeamB2 = compile_test_module(ModuleB, 2),
|
||||
{module, ModuleA} = load_test_beam(ModuleA, BeamA1),
|
||||
{module, ModuleB} = load_test_beam(ModuleB, BeamB1),
|
||||
{ok, Md5A1} = current_md5(ModuleA),
|
||||
WrongExpectedMd5 = <<0:128>>,
|
||||
{EntryA, _Md5A2} = beam_entry(ModuleA, Md5A1, BeamA2),
|
||||
{EntryB, _Md5B2} = beam_entry(ModuleB, WrongExpectedMd5, BeamB2),
|
||||
Bundle = #{
|
||||
version => 1,
|
||||
build_sha => gateway_hotpatch_runtime:build_sha(),
|
||||
modules => [EntryA, EntryB]
|
||||
},
|
||||
?assertMatch({error, {md5_mismatch, ModuleB, _, _}}, apply_bundle(Bundle)),
|
||||
?assertEqual(2, erlang:apply(ModuleA, version, [])),
|
||||
?assertEqual(1, erlang:apply(ModuleB, version, []))
|
||||
after
|
||||
cleanup_test_module(ModuleA),
|
||||
cleanup_test_module(ModuleB)
|
||||
end.
|
||||
|
||||
hex_test() ->
|
||||
?assertEqual(<<"0001020f10ff">>, hex(<<0, 1, 2, 15, 16, 255>>)).
|
||||
|
||||
compile_test_module(Module, Version) ->
|
||||
Dir = test_compile_dir(),
|
||||
Source = filename:join(Dir, atom_to_list(Module) ++ ".erl"),
|
||||
SourceText = io_lib:format(
|
||||
"-module(~p).~n-export([version/0]).~nversion() -> ~p.~n",
|
||||
[Module, Version]
|
||||
),
|
||||
ok = file:write_file(Source, SourceText),
|
||||
case compile:file(Source, [binary, return_errors, return_warnings]) of
|
||||
{ok, Module, Beam} -> Beam;
|
||||
{ok, Module, Beam, _Warnings} -> Beam;
|
||||
Error -> erlang:error({test_module_compile_failed, Module, Error})
|
||||
end.
|
||||
|
||||
test_compile_dir() ->
|
||||
Root =
|
||||
case os:getenv("TMPDIR") of
|
||||
false -> "/tmp";
|
||||
Value -> Value
|
||||
end,
|
||||
Dir = filename:join(Root, "fluxer_hotpatch_loader_tests"),
|
||||
ok = filelib:ensure_dir(filename:join(Dir, "placeholder")),
|
||||
Dir.
|
||||
|
||||
load_test_beam(Module, Beam) ->
|
||||
cleanup_test_module(Module),
|
||||
BeamPath = filename:join(test_compile_dir(), atom_to_list(Module) ++ ".beam"),
|
||||
ok = file:write_file(BeamPath, Beam),
|
||||
code:load_abs(filename:rootname(BeamPath)).
|
||||
|
||||
beam_entry(Module, ExpectedMd5, TargetBeam) ->
|
||||
{ok, TargetMd5} = beam_md5(TargetBeam),
|
||||
{ok, BeamZstd} = compress_test_beam(TargetBeam),
|
||||
{
|
||||
#{
|
||||
module => Module,
|
||||
expected_current_md5 => ExpectedMd5,
|
||||
target_md5 => TargetMd5,
|
||||
beam_zstd => BeamZstd
|
||||
},
|
||||
TargetMd5
|
||||
}.
|
||||
|
||||
compress_test_beam(Beam) ->
|
||||
try erlang:apply(ezstd, compress, [Beam, 3]) of
|
||||
Compressed when is_binary(Compressed) -> {ok, Compressed};
|
||||
{error, Reason} -> {error, Reason};
|
||||
Other -> {error, Other}
|
||||
catch
|
||||
Class:Reason -> {error, {Class, Reason}}
|
||||
end.
|
||||
|
||||
cleanup_test_module(Module) ->
|
||||
_ = code:soft_purge(Module),
|
||||
_ = code:purge(Module),
|
||||
_ = code:delete(Module),
|
||||
_ = code:purge(Module),
|
||||
erase_hotpatch_loaded_md5(Module),
|
||||
_ = file:delete(filename:join(test_compile_dir(), atom_to_list(Module) ++ ".erl")),
|
||||
_ = file:delete(filename:join(test_compile_dir(), atom_to_list(Module) ++ ".beam")),
|
||||
ok.
|
||||
|
||||
-spec erase_hotpatch_loaded_md5(atom()) -> ok.
|
||||
erase_hotpatch_loaded_md5(Module) ->
|
||||
_ = persistent_term:erase({?MODULE, loaded_md5, Module}),
|
||||
ok.
|
||||
|
||||
-endif.
|
||||
@@ -1,319 +0,0 @@
|
||||
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
-module(gateway_hotpatch_reconciler).
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
-export([start_link/0, is_ready/0, status/0, reconcile_async/0]).
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-record(state, {
|
||||
enabled = false :: boolean(),
|
||||
build_sha = <<"dev">> :: binary(),
|
||||
public_keys = #{} :: #{binary() => binary()},
|
||||
applied_event_ids = [] :: [term()],
|
||||
applied_count = 0 :: non_neg_integer(),
|
||||
poll_interval_ms = 5000 :: pos_integer(),
|
||||
last_error = undefined :: term()
|
||||
}).
|
||||
|
||||
-type state() :: #state{}.
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
-define(POLL, poll).
|
||||
-define(STARTUP_RECONCILE_RETRY_MS, 1000).
|
||||
|
||||
-spec start_link() -> gen_server:start_ret().
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||
|
||||
-spec is_ready() -> boolean().
|
||||
is_ready() ->
|
||||
gateway_hotpatch_runtime:is_ready().
|
||||
|
||||
-spec status() -> map().
|
||||
status() ->
|
||||
gateway_hotpatch_runtime:status().
|
||||
|
||||
-spec reconcile_async() -> ok.
|
||||
reconcile_async() ->
|
||||
gen_server:cast(?SERVER, reconcile).
|
||||
|
||||
-spec init([]) -> {ok, state()} | {stop, term()}.
|
||||
init([]) ->
|
||||
case gateway_hotpatch_runtime:is_enabled() of
|
||||
false -> init_disabled();
|
||||
true -> init_enabled()
|
||||
end.
|
||||
|
||||
-spec handle_call(term(), {pid(), term()}, state()) -> {reply, term(), state()}.
|
||||
handle_call(status, _From, State) ->
|
||||
{reply, state_status(State, gateway_hotpatch_runtime:is_ready()), State};
|
||||
handle_call(_Request, _From, State) ->
|
||||
{reply, {error, unsupported_call}, State}.
|
||||
|
||||
-spec handle_cast(term(), state()) -> {noreply, state()}.
|
||||
handle_cast(reconcile, State) ->
|
||||
{noreply, reconcile_and_publish(State)};
|
||||
handle_cast(_Request, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
-spec handle_info(term(), state()) -> {noreply, state()}.
|
||||
handle_info(?POLL, #state{enabled = true} = State) ->
|
||||
NewState = reconcile_and_publish(State),
|
||||
schedule_poll(NewState),
|
||||
{noreply, NewState};
|
||||
handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
-spec terminate(term(), state()) -> ok.
|
||||
terminate(_Reason, _State) ->
|
||||
ok.
|
||||
|
||||
-spec code_change(term(), state(), term()) -> {ok, state()}.
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
-spec init_disabled() -> {ok, state()}.
|
||||
init_disabled() ->
|
||||
BuildSha = gateway_hotpatch_runtime:build_sha(),
|
||||
State = #state{enabled = false, build_sha = BuildSha},
|
||||
gateway_hotpatch_runtime:put_ready(true),
|
||||
publish_status(State, true),
|
||||
{ok, State}.
|
||||
|
||||
-spec init_enabled() -> {ok, state()} | {stop, term()}.
|
||||
init_enabled() ->
|
||||
gateway_hotpatch_runtime:put_ready(false),
|
||||
BuildSha = gateway_hotpatch_runtime:build_sha(),
|
||||
PollIntervalMs = runtime_pos_integer(hotpatch_poll_interval_ms, 5000),
|
||||
PublicKeyConfig = gateway_hotpatch_runtime:get(hotpatch_public_keys, undefined),
|
||||
case gateway_hotpatch_bundle:parse_public_keys(PublicKeyConfig) of
|
||||
{ok, PublicKeys} when map_size(PublicKeys) > 0 ->
|
||||
State0 = #state{
|
||||
enabled = true,
|
||||
build_sha = BuildSha,
|
||||
public_keys = PublicKeys,
|
||||
poll_interval_ms = max(1000, PollIntervalMs)
|
||||
},
|
||||
publish_status(State0, false),
|
||||
init_enabled_connected(State0);
|
||||
{ok, _Empty} ->
|
||||
init_enabled_config_error(BuildSha, PollIntervalMs, missing_hotpatch_public_keys);
|
||||
{error, Reason} ->
|
||||
init_enabled_config_error(
|
||||
BuildSha, PollIntervalMs, {invalid_hotpatch_public_keys, Reason}
|
||||
)
|
||||
end.
|
||||
|
||||
-spec init_enabled_config_error(binary(), pos_integer(), term()) -> {ok, state()}.
|
||||
init_enabled_config_error(BuildSha, PollIntervalMs, Reason) ->
|
||||
State = #state{
|
||||
enabled = true,
|
||||
build_sha = BuildSha,
|
||||
poll_interval_ms = max(1000, PollIntervalMs),
|
||||
last_error = Reason
|
||||
},
|
||||
gateway_hotpatch_runtime:put_ready(false),
|
||||
publish_status(State, false),
|
||||
logger:error("Gateway hotpatch configuration invalid: ~0tp", [Reason]),
|
||||
schedule_poll(State),
|
||||
{ok, State}.
|
||||
|
||||
-spec init_enabled_connected(state()) -> {ok, state()}.
|
||||
init_enabled_connected(State0) ->
|
||||
finish_enabled_startup(State0).
|
||||
|
||||
-spec finish_enabled_startup(state()) -> {ok, state()}.
|
||||
finish_enabled_startup(State0) ->
|
||||
TimeoutMs = runtime_pos_integer(hotpatch_startup_sync_timeout_ms, 30000),
|
||||
DeadlineMs = erlang:monotonic_time(millisecond) + TimeoutMs,
|
||||
finish_startup_reconcile(State0, startup_reconcile(State0, DeadlineMs)).
|
||||
|
||||
-spec finish_startup_reconcile(state(), {ok, state()} | {error, term()}) -> {ok, state()}.
|
||||
finish_startup_reconcile(_State0, {ok, State}) ->
|
||||
gateway_hotpatch_runtime:put_ready(true),
|
||||
publish_status(State, true),
|
||||
schedule_poll(State),
|
||||
{ok, State};
|
||||
finish_startup_reconcile(State0, {error, Reason}) ->
|
||||
Error = {hotpatch_startup_sync_failed, Reason},
|
||||
State = State0#state{last_error = Error},
|
||||
gateway_hotpatch_runtime:put_ready(false),
|
||||
publish_status(State, false),
|
||||
logger:error("Gateway hotpatch startup sync failed; keeping node unready: ~0tp", [Error]),
|
||||
schedule_poll(State),
|
||||
{ok, State}.
|
||||
|
||||
-spec startup_reconcile(state(), integer()) -> {ok, state()} | {error, term()}.
|
||||
startup_reconcile(State, DeadlineMs) ->
|
||||
case reconcile_once(State) of
|
||||
{ok, NewState} ->
|
||||
{ok, NewState};
|
||||
{error, Reason} ->
|
||||
maybe_retry_startup_reconcile(State, DeadlineMs, Reason)
|
||||
end.
|
||||
|
||||
-spec maybe_retry_startup_reconcile(state(), integer(), term()) ->
|
||||
{ok, state()} | {error, term()}.
|
||||
maybe_retry_startup_reconcile(State, DeadlineMs, Reason) ->
|
||||
case erlang:monotonic_time(millisecond) >= DeadlineMs of
|
||||
true ->
|
||||
{error, Reason};
|
||||
false ->
|
||||
retry_startup_reconcile(State#state{last_error = Reason}, DeadlineMs, Reason)
|
||||
end.
|
||||
|
||||
-spec retry_startup_reconcile(state(), integer(), term()) -> {ok, state()} | {error, term()}.
|
||||
retry_startup_reconcile(State, DeadlineMs, Reason) ->
|
||||
case gateway_retry_timer:wait_until(?STARTUP_RECONCILE_RETRY_MS, DeadlineMs) of
|
||||
ok -> startup_reconcile(State, DeadlineMs);
|
||||
expired -> {error, Reason};
|
||||
{error, _InvalidDelay} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec reconcile_and_publish(state()) -> state().
|
||||
reconcile_and_publish(#state{enabled = false} = State) ->
|
||||
gateway_hotpatch_runtime:put_ready(true),
|
||||
publish_status(State, true),
|
||||
State;
|
||||
reconcile_and_publish(State) ->
|
||||
case reconcile_once(State) of
|
||||
{ok, NewState} ->
|
||||
gateway_hotpatch_runtime:put_ready(true),
|
||||
publish_status(NewState, true),
|
||||
NewState;
|
||||
{error, Reason} ->
|
||||
publish_reconcile_error(State, Reason)
|
||||
end.
|
||||
|
||||
-spec reconcile_once(state()) -> {ok, state()} | {error, term()}.
|
||||
reconcile_once(#state{build_sha = BuildSha} = State) ->
|
||||
case gateway_hotpatch_store:connect() of
|
||||
ok -> fetch_and_apply_events(BuildSha, State);
|
||||
{error, Reason} -> {error, {store_connect_failed, Reason}}
|
||||
end.
|
||||
|
||||
-spec fetch_and_apply_events(binary(), state()) -> {ok, state()} | {error, term()}.
|
||||
fetch_and_apply_events(BuildSha, State) ->
|
||||
case gateway_hotpatch_store:fetch_events(BuildSha) of
|
||||
{ok, Events} -> apply_events(Events, State);
|
||||
{error, Reason} -> {error, {fetch_events_failed, Reason}}
|
||||
end.
|
||||
|
||||
-spec publish_reconcile_error(state(), term()) -> state().
|
||||
publish_reconcile_error(State, {fetch_events_failed, _Reason} = Error) ->
|
||||
ErrorState = State#state{last_error = Error},
|
||||
Ready = gateway_hotpatch_runtime:is_ready(),
|
||||
gateway_hotpatch_runtime:put_ready(Ready),
|
||||
publish_status(ErrorState, Ready),
|
||||
logger:warning("Gateway hotpatch fetch failed; keeping current readiness: ~0tp", [Error]),
|
||||
ErrorState;
|
||||
publish_reconcile_error(State, Reason) ->
|
||||
ErrorState = State#state{last_error = Reason},
|
||||
gateway_hotpatch_runtime:put_ready(false),
|
||||
publish_status(ErrorState, false),
|
||||
logger:error("Gateway hotpatch reconciliation failed: ~0tp", [Reason]),
|
||||
ErrorState.
|
||||
|
||||
-spec apply_events([map()], state()) -> {ok, state()} | {error, term()}.
|
||||
apply_events([], State) ->
|
||||
{ok, State#state{last_error = undefined}};
|
||||
apply_events([Event | Rest], State) ->
|
||||
EventId = maps:get(event_id, Event, undefined),
|
||||
case lists:member(EventId, State#state.applied_event_ids) of
|
||||
true ->
|
||||
apply_events(Rest, State);
|
||||
false ->
|
||||
apply_new_event(EventId, Event, Rest, State)
|
||||
end.
|
||||
|
||||
-spec apply_new_event(term(), map(), [map()], state()) -> {ok, state()} | {error, term()}.
|
||||
apply_new_event(EventId, Event, Rest, State) ->
|
||||
case apply_event(Event, State) of
|
||||
{ok, NewState} -> apply_events(Rest, NewState);
|
||||
{error, Reason} -> {error, {event_apply_failed, EventId, Reason}}
|
||||
end.
|
||||
|
||||
-spec apply_event(map(), state()) -> {ok, state()} | {error, term()}.
|
||||
apply_event(Event, #state{public_keys = PublicKeys, build_sha = BuildSha} = State) ->
|
||||
EventId = maps:get(event_id, Event, undefined),
|
||||
BundleHash = maps:get(bundle_sha256, Event, <<>>),
|
||||
_ = code:ensure_loaded(gateway_hotpatch_loader),
|
||||
case gateway_hotpatch_bundle:decode_signed_event(Event, PublicKeys) of
|
||||
{ok, Bundle} ->
|
||||
apply_decoded_event(BuildSha, EventId, BundleHash, Bundle, State);
|
||||
{error, Reason} ->
|
||||
audit_event(BuildSha, EventId, #{bundle_sha256 => BundleHash}, {error, Reason}),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec apply_decoded_event(binary(), term(), binary(), map(), state()) ->
|
||||
{ok, state()} | {error, term()}.
|
||||
apply_decoded_event(BuildSha, EventId, BundleHash, Bundle, State) ->
|
||||
Summary0 = #{bundle_sha256 => BundleHash},
|
||||
case gateway_hotpatch_loader:apply_bundle(Bundle) of
|
||||
{ok, Summary} ->
|
||||
handle_applied_event(BuildSha, EventId, Summary0, Summary, State);
|
||||
{error, Reason} ->
|
||||
audit_event(BuildSha, EventId, Summary0, {error, Reason}),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec handle_applied_event(binary(), term(), map(), map(), state()) -> {ok, state()}.
|
||||
handle_applied_event(BuildSha, EventId, Summary0, Summary, State) ->
|
||||
AuditSummary = maps:merge(Summary0, Summary),
|
||||
audit_event(BuildSha, EventId, AuditSummary, ok),
|
||||
logger:notice("Applied gateway hotpatch event ~0tp summary=~0tp", [EventId, AuditSummary]),
|
||||
{ok, State#state{
|
||||
applied_event_ids = [EventId | State#state.applied_event_ids],
|
||||
applied_count = State#state.applied_count + 1,
|
||||
last_error = undefined
|
||||
}}.
|
||||
|
||||
-spec audit_event(binary(), term(), map(), ok | {error, term()}) -> ok.
|
||||
audit_event(BuildSha, EventId, Summary, Result) when is_binary(EventId) ->
|
||||
case
|
||||
gateway_hotpatch_store:audit_applied(
|
||||
BuildSha, gateway_hotpatch_runtime:node_name(), EventId, Summary, Result
|
||||
)
|
||||
of
|
||||
ok ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
logger:warning("Gateway hotpatch audit write failed: ~0tp", [Reason]),
|
||||
ok
|
||||
end;
|
||||
audit_event(_BuildSha, _EventId, _Summary, _Result) ->
|
||||
ok.
|
||||
|
||||
-spec publish_status(state(), boolean()) -> ok.
|
||||
publish_status(State, Ready) ->
|
||||
gateway_hotpatch_runtime:put_status(state_status(State, Ready)).
|
||||
|
||||
-spec state_status(state(), boolean()) -> map().
|
||||
state_status(State, Ready) ->
|
||||
#{
|
||||
enabled => State#state.enabled,
|
||||
ready => Ready,
|
||||
build_sha => State#state.build_sha,
|
||||
applied_count => State#state.applied_count,
|
||||
applied_event_count => length(State#state.applied_event_ids),
|
||||
last_error => State#state.last_error
|
||||
}.
|
||||
|
||||
-spec schedule_poll(state()) -> ok.
|
||||
schedule_poll(#state{poll_interval_ms = PollIntervalMs}) ->
|
||||
erlang:send_after(PollIntervalMs, self(), ?POLL),
|
||||
ok.
|
||||
|
||||
-spec runtime_pos_integer(atom(), pos_integer()) -> pos_integer().
|
||||
runtime_pos_integer(Key, Default) ->
|
||||
normalize_pos_integer(gateway_hotpatch_runtime:get(Key, Default), Default).
|
||||
|
||||
-spec normalize_pos_integer(term(), pos_integer()) -> pos_integer().
|
||||
normalize_pos_integer(Value, _Default) when is_integer(Value), Value > 0 ->
|
||||
Value;
|
||||
normalize_pos_integer(_Value, Default) ->
|
||||
Default.
|
||||
@@ -1,78 +0,0 @@
|
||||
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
-module(gateway_hotpatch_runtime).
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([
|
||||
build_sha/0,
|
||||
node_name/0,
|
||||
is_enabled/0,
|
||||
get/2,
|
||||
put_ready/1,
|
||||
is_ready/0,
|
||||
put_status/1,
|
||||
status/0
|
||||
]).
|
||||
|
||||
-define(READY_KEY, {gateway_hotpatch, ready}).
|
||||
-define(STATUS_KEY, {gateway_hotpatch, status}).
|
||||
|
||||
-spec build_sha() -> binary().
|
||||
build_sha() ->
|
||||
case os:getenv("BUILD_SHA") of
|
||||
false -> build_version();
|
||||
"" -> build_version();
|
||||
Value -> type_conv:ensure_binary(Value, <<"dev">>)
|
||||
end.
|
||||
|
||||
-spec build_version() -> binary().
|
||||
build_version() ->
|
||||
case os:getenv("BUILD_VERSION") of
|
||||
false -> <<"dev">>;
|
||||
"" -> <<"dev">>;
|
||||
Value -> type_conv:ensure_binary(Value, <<"dev">>)
|
||||
end.
|
||||
|
||||
-spec node_name() -> binary().
|
||||
node_name() ->
|
||||
atom_to_binary(node(), utf8).
|
||||
|
||||
-spec is_enabled() -> boolean().
|
||||
is_enabled() ->
|
||||
case fluxer_gateway_env:get(hotpatch_enabled) of
|
||||
true -> true;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
-spec get(atom(), term()) -> term().
|
||||
get(Key, Default) ->
|
||||
case fluxer_gateway_env:get(Key) of
|
||||
undefined -> Default;
|
||||
Value -> Value
|
||||
end.
|
||||
|
||||
-spec put_ready(boolean()) -> ok.
|
||||
put_ready(Ready) when is_boolean(Ready) ->
|
||||
persistent_term:put(?READY_KEY, Ready),
|
||||
ok.
|
||||
|
||||
-spec is_ready() -> boolean().
|
||||
is_ready() ->
|
||||
case persistent_term:get(?READY_KEY, true) of
|
||||
false -> false;
|
||||
_ -> true
|
||||
end.
|
||||
|
||||
-spec put_status(map()) -> ok.
|
||||
put_status(Status) ->
|
||||
persistent_term:put(?STATUS_KEY, Status),
|
||||
ok.
|
||||
|
||||
-spec status() -> map().
|
||||
status() ->
|
||||
persistent_term:get(?STATUS_KEY, #{
|
||||
enabled => false,
|
||||
ready => true,
|
||||
build_sha => build_sha(),
|
||||
applied_count => 0
|
||||
}).
|
||||
@@ -1,277 +0,0 @@
|
||||
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
-module(gateway_hotpatch_store).
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([
|
||||
connect/0,
|
||||
fetch_events/1,
|
||||
append_event/5,
|
||||
append_event/6,
|
||||
audit_applied/5,
|
||||
normalize_event_rows/1
|
||||
]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-endif.
|
||||
|
||||
-define(STMT_FETCH_EVENTS, gateway_hotpatch_fetch_events_by_build).
|
||||
-define(STMT_APPEND_EVENT, gateway_hotpatch_append_event).
|
||||
-define(STMT_AUDIT_APPLIED, gateway_hotpatch_audit_applied).
|
||||
|
||||
-spec connect() -> ok | {error, term()}.
|
||||
connect() ->
|
||||
case gateway_hotpatch_runtime:get(hotpatch_cassandra_hosts, undefined) of
|
||||
Hosts when is_list(Hosts), Hosts =/= [] -> connect_hosts(Hosts);
|
||||
Hosts when is_binary(Hosts), byte_size(Hosts) > 0 ->
|
||||
start_erlcass(binary_to_list(Hosts));
|
||||
_ ->
|
||||
{error, missing_hotpatch_cassandra_hosts}
|
||||
end.
|
||||
|
||||
-spec connect_hosts(term()) -> ok | {error, term()}.
|
||||
connect_hosts(Hosts) ->
|
||||
case type_conv:to_list(Hosts) of
|
||||
String when is_list(String), String =/= [] -> start_erlcass(String);
|
||||
_ -> {error, missing_hotpatch_cassandra_hosts}
|
||||
end.
|
||||
|
||||
-spec fetch_events(binary()) -> {ok, [map()]} | {error, term()}.
|
||||
fetch_events(BuildSha) when is_binary(BuildSha) ->
|
||||
case erlcass:execute(?STMT_FETCH_EVENTS, [BuildSha]) of
|
||||
{ok, _Columns, Rows} -> {ok, normalize_event_rows(Rows)};
|
||||
ok -> {ok, []};
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec append_event(binary(), binary(), binary(), binary(), binary()) ->
|
||||
{ok, binary()} | {error, term()}.
|
||||
append_event(CreatedBy, SignerKeyId, Signature, BundleSha256, Bundle) when
|
||||
is_binary(CreatedBy),
|
||||
is_binary(SignerKeyId),
|
||||
is_binary(Signature),
|
||||
is_binary(BundleSha256),
|
||||
is_binary(Bundle)
|
||||
->
|
||||
BuildSha = gateway_hotpatch_runtime:build_sha(),
|
||||
append_event(BuildSha, CreatedBy, SignerKeyId, Signature, BundleSha256, Bundle).
|
||||
|
||||
-spec append_event(binary(), binary(), binary(), binary(), binary(), binary()) ->
|
||||
{ok, binary()} | {error, term()}.
|
||||
append_event(BuildSha, CreatedBy, SignerKeyId, Signature, BundleSha256, Bundle) when
|
||||
is_binary(BuildSha),
|
||||
is_binary(CreatedBy),
|
||||
is_binary(SignerKeyId),
|
||||
is_binary(Signature),
|
||||
is_binary(BundleSha256),
|
||||
is_binary(Bundle)
|
||||
->
|
||||
case erlcass_uuid:gen_time() of
|
||||
{ok, EventId} ->
|
||||
Params = append_event_params(
|
||||
BuildSha, EventId, CreatedBy, SignerKeyId, BundleSha256, Signature, Bundle
|
||||
),
|
||||
execute_append_event(EventId, Params);
|
||||
{error, Reason} ->
|
||||
{error, {event_id_failed, Reason}}
|
||||
end.
|
||||
|
||||
-spec append_event_params(
|
||||
binary(), binary(), binary(), binary(), binary(), binary(), binary()
|
||||
) -> list().
|
||||
append_event_params(BuildSha, EventId, CreatedBy, SignerKeyId, BundleSha256, Signature, Bundle) ->
|
||||
[
|
||||
BuildSha,
|
||||
EventId,
|
||||
1,
|
||||
<<"beam_bundle">>,
|
||||
CreatedBy,
|
||||
SignerKeyId,
|
||||
BundleSha256,
|
||||
Signature,
|
||||
Bundle
|
||||
].
|
||||
|
||||
-spec execute_append_event(binary(), list()) -> {ok, binary()} | {error, term()}.
|
||||
execute_append_event(EventId, Params) ->
|
||||
case erlcass:execute(?STMT_APPEND_EVENT, Params) of
|
||||
ok -> {ok, EventId};
|
||||
{ok, _Columns, _Rows} -> {ok, EventId};
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec audit_applied(binary(), binary(), binary(), map(), ok | {error, term()}) ->
|
||||
ok | {error, term()}.
|
||||
audit_applied(BuildSha, NodeName, EventId, Summary, Result) when
|
||||
is_binary(BuildSha), is_binary(NodeName), is_binary(EventId), is_map(Summary)
|
||||
->
|
||||
ModuleCount = maps:get(module_count, Summary, 0),
|
||||
BundleSha256 = maps:get(bundle_sha256, Summary, <<>>),
|
||||
Status = audit_status(Result),
|
||||
Error = audit_error(Result),
|
||||
Params = [BuildSha, NodeName, EventId, ModuleCount, BundleSha256, Status, Error],
|
||||
case erlcass:execute(?STMT_AUDIT_APPLIED, Params) of
|
||||
ok -> ok;
|
||||
{ok, _Columns, _Rows} -> ok;
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec normalize_event_rows([list()]) -> [map()].
|
||||
normalize_event_rows(Rows) when is_list(Rows) ->
|
||||
lists:filtermap(fun normalize_event_row/1, Rows).
|
||||
|
||||
-spec start_erlcass(string()) -> ok | {error, term()}.
|
||||
start_erlcass(Hosts) ->
|
||||
Keyspace = gateway_hotpatch_runtime:get(hotpatch_cassandra_keyspace, <<"fluxer">>),
|
||||
Port = gateway_hotpatch_runtime:get(hotpatch_cassandra_port, 9042),
|
||||
Options0 = [
|
||||
{contact_points, type_conv:ensure_binary(Hosts)},
|
||||
{port, Port},
|
||||
{latency_aware_routing, true},
|
||||
{token_aware_routing, true},
|
||||
{tcp_nodelay, true},
|
||||
{tcp_keepalive, {true, 60}},
|
||||
{connect_timeout, 5000},
|
||||
{request_timeout, 5000},
|
||||
{retry_policy, {default, true}},
|
||||
{default_consistency_level, 6}
|
||||
],
|
||||
case maybe_credentials(Options0) of
|
||||
{ok, Options} -> start_erlcass_with_options(Keyspace, Options);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec start_erlcass_with_options(binary(), list()) -> ok | {error, term()}.
|
||||
start_erlcass_with_options(Keyspace, Options) ->
|
||||
application:set_env(erlcass, keyspace, Keyspace),
|
||||
application:set_env(erlcass, cluster_options, Options),
|
||||
application:set_env(erlcass, log_level, 2),
|
||||
case application:ensure_all_started(erlcass) of
|
||||
{ok, _Apps} -> prepare_statements();
|
||||
{error, {erlcass, {already_started, erlcass}}} -> prepare_statements();
|
||||
{error, Reason} -> {error, {erlcass_start_failed, Reason}}
|
||||
end.
|
||||
|
||||
-spec prepare_statements() -> ok | {error, term()}.
|
||||
prepare_statements() ->
|
||||
Statements = [
|
||||
{?STMT_FETCH_EVENTS, <<
|
||||
"SELECT event_id, schema_version, kind, created_by, signer_key_id, "
|
||||
"bundle_sha256, signature, bundle "
|
||||
"FROM gateway_hotpatch_events_by_build WHERE build_sha = ?"
|
||||
>>},
|
||||
{?STMT_APPEND_EVENT, <<
|
||||
"INSERT INTO gateway_hotpatch_events_by_build "
|
||||
"(build_sha, event_id, schema_version, kind, created_at, created_by, "
|
||||
"signer_key_id, bundle_sha256, signature, bundle) "
|
||||
"VALUES (?, ?, ?, ?, toTimestamp(now()), ?, ?, ?, ?, ?)"
|
||||
>>},
|
||||
{?STMT_AUDIT_APPLIED, <<
|
||||
"INSERT INTO gateway_hotpatch_applied_by_node "
|
||||
"(build_sha, node_name, event_id, applied_at, module_count, bundle_sha256, "
|
||||
"status, error) "
|
||||
"VALUES (?, ?, ?, toTimestamp(now()), ?, ?, ?, ?)"
|
||||
>>}
|
||||
],
|
||||
prepare_statements(Statements).
|
||||
|
||||
-spec prepare_statements([{atom(), binary()}]) -> ok | {error, term()}.
|
||||
prepare_statements([]) ->
|
||||
ok;
|
||||
prepare_statements([{Id, Query} | Rest]) ->
|
||||
case prepare_statement(Id, Query) of
|
||||
ok -> prepare_statements(Rest);
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec prepare_statement(atom(), binary()) -> ok | {error, term()}.
|
||||
prepare_statement(Id, Query) ->
|
||||
case erlcass:add_prepare_statement(Id, Query) of
|
||||
ok -> ok;
|
||||
{error, already_exist} -> ok;
|
||||
{error, Reason} -> {error, {prepare_failed, Id, Reason}}
|
||||
end.
|
||||
|
||||
-spec maybe_credentials(list()) -> {ok, list()} | {error, term()}.
|
||||
maybe_credentials(Options) ->
|
||||
Username = gateway_hotpatch_runtime:get(hotpatch_cassandra_username, undefined),
|
||||
Password = gateway_hotpatch_runtime:get(hotpatch_cassandra_password, undefined),
|
||||
case {Username, Password} of
|
||||
{U, P} when is_binary(U), byte_size(U) > 0, is_binary(P), byte_size(P) > 0 ->
|
||||
{ok, [{credentials, {U, P}} | Options]};
|
||||
_ ->
|
||||
{error, missing_hotpatch_cassandra_credentials}
|
||||
end.
|
||||
|
||||
-spec normalize_event_row(list()) -> {true, map()} | false.
|
||||
normalize_event_row([
|
||||
EventId,
|
||||
SchemaVersion,
|
||||
Kind,
|
||||
CreatedBy,
|
||||
SignerKeyId,
|
||||
BundleSha256,
|
||||
Signature,
|
||||
Bundle
|
||||
]) when
|
||||
is_binary(EventId),
|
||||
is_binary(SignerKeyId),
|
||||
is_binary(BundleSha256),
|
||||
is_binary(Signature),
|
||||
is_binary(Bundle)
|
||||
->
|
||||
{true, #{
|
||||
event_id => EventId,
|
||||
schema_version => SchemaVersion,
|
||||
kind => Kind,
|
||||
created_by => CreatedBy,
|
||||
signer_key_id => SignerKeyId,
|
||||
bundle_sha256 => BundleSha256,
|
||||
signature => Signature,
|
||||
bundle => Bundle
|
||||
}};
|
||||
normalize_event_row(_Row) ->
|
||||
false.
|
||||
|
||||
-spec audit_status(ok | {error, term()}) -> binary().
|
||||
audit_status(ok) -> <<"ok">>;
|
||||
audit_status({error, _Reason}) -> <<"error">>.
|
||||
|
||||
-spec audit_error(ok | {error, term()}) -> binary().
|
||||
audit_error(ok) ->
|
||||
<<>>;
|
||||
audit_error({error, Reason}) ->
|
||||
iolist_to_binary(io_lib:format("~0tp", [Reason])).
|
||||
|
||||
-ifdef(TEST).
|
||||
|
||||
normalize_event_rows_test() ->
|
||||
EventId = <<1:128>>,
|
||||
Row = [
|
||||
EventId,
|
||||
1,
|
||||
<<"beam_bundle">>,
|
||||
<<"hampus">>,
|
||||
<<"ops">>,
|
||||
<<2:256>>,
|
||||
<<3:512>>,
|
||||
<<"bundle">>
|
||||
],
|
||||
?assertEqual(
|
||||
[
|
||||
#{
|
||||
event_id => EventId,
|
||||
schema_version => 1,
|
||||
kind => <<"beam_bundle">>,
|
||||
created_by => <<"hampus">>,
|
||||
signer_key_id => <<"ops">>,
|
||||
bundle_sha256 => <<2:256>>,
|
||||
signature => <<3:512>>,
|
||||
bundle => <<"bundle">>
|
||||
}
|
||||
],
|
||||
normalize_event_rows([Row, [invalid]])
|
||||
).
|
||||
|
||||
-endif.
|
||||
@@ -98,7 +98,7 @@ fallback_owner_for_role(Role) ->
|
||||
|
||||
-spec is_ready() -> boolean().
|
||||
is_ready() ->
|
||||
not is_draining() andalso gateway_hotpatch_reconciler:is_ready().
|
||||
not is_draining().
|
||||
|
||||
-spec is_draining() -> boolean().
|
||||
is_draining() ->
|
||||
|
||||
@@ -23,7 +23,6 @@ render_metrics() ->
|
||||
render_cluster_counters(),
|
||||
render_process_counts(),
|
||||
render_push_dispatcher_stats(),
|
||||
render_hotpatch_metrics(),
|
||||
render_vm_metrics()
|
||||
].
|
||||
|
||||
@@ -256,33 +255,6 @@ render_push_dispatcher_stats() ->
|
||||
]
|
||||
end.
|
||||
|
||||
-spec render_hotpatch_metrics() -> iolist().
|
||||
render_hotpatch_metrics() ->
|
||||
Status = safe_apply_map(fun gateway_hotpatch_reconciler:status/0),
|
||||
Ready = maps:get(ready, Status, true),
|
||||
Enabled = maps:get(enabled, Status, false),
|
||||
AppliedCount = maps:get(applied_count, Status, 0),
|
||||
[
|
||||
format_metric(
|
||||
<<"fluxer_gateway_hotpatch_enabled">>,
|
||||
<<"gauge">>,
|
||||
<<"Gateway hotpatch reconciler enabled">>,
|
||||
bool_metric(Enabled)
|
||||
),
|
||||
format_metric(
|
||||
<<"fluxer_gateway_hotpatch_ready">>,
|
||||
<<"gauge">>,
|
||||
<<"Gateway hotpatch reconciliation readiness">>,
|
||||
bool_metric(Ready)
|
||||
),
|
||||
format_metric(
|
||||
<<"fluxer_gateway_hotpatch_applied_events_total">>,
|
||||
<<"counter">>,
|
||||
<<"Gateway hotpatch events applied on this node">>,
|
||||
integer_to_binary(AppliedCount)
|
||||
)
|
||||
].
|
||||
|
||||
-spec render_vm_metrics() -> iolist().
|
||||
render_vm_metrics() ->
|
||||
Memory = safe_apply_list(fun erlang:memory/0),
|
||||
@@ -376,7 +348,3 @@ format_labeled_series(Name, Type, Help, LabelValues) ->
|
||||
|| {Label, Value} <- LabelValues
|
||||
]
|
||||
].
|
||||
|
||||
-spec bool_metric(term()) -> binary().
|
||||
bool_metric(true) -> <<"1">>;
|
||||
bool_metric(_) -> <<"0">>.
|
||||
@@ -159,7 +159,7 @@ flush_pending_syncs_clears_empty_batch_test() ->
|
||||
State = #{?SYNC_BATCH_STATE_KEY => #{pending_list_ids => #{}}},
|
||||
?assertEqual(#{}, flush_pending_syncs(State)).
|
||||
|
||||
queue_list_sync_drains_pre_hotpatch_pending_batch_test() ->
|
||||
queue_list_sync_drains_preexisting_pending_batch_test() ->
|
||||
Ref = erlang:send_after(60000, self(), ?FLUSH_SYNC_BATCH_MSG),
|
||||
State = #{
|
||||
?SYNC_BATCH_STATE_KEY => #{
|
||||
|
||||
Reference in new issue
Block a user