This commit is contained in:
anlicheng 2026-05-09 12:48:17 +08:00
parent 87e14773ba
commit 5a1ce77dde
2 changed files with 21 additions and 88 deletions

View File

@ -33,7 +33,6 @@
`command``command_response``Domain` 表示业务域,目前支持: `command``command_response``Domain` 表示业务域,目前支持:
- `auth`
- `container` - `container`
## 鉴权请求 ## 鉴权请求
@ -52,37 +51,19 @@
```erlang ```erlang
{response, Ref, {auth_response, ok}} {response, Ref, {auth_response, ok}}
{response, Ref, {auth_response, {error, {denied, Reason}}}}
{response, Ref, {auth_response, {error, {failed, Reason}}}} {response, Ref, {auth_response, {error, {failed, Reason}}}}
``` ```
处理语义: 处理语义:
- `ok``efka` 进入 `activated` 状态。 - `ok``efka` 进入 `activated` 状态。
- `{error, {denied, Reason}}``efka` 进入 `restricted` 状态,不能正常上报数据,但仍可接收部分命令。
- `{error, {failed, Reason}}`:鉴权失败,连接关闭后重连。 - `{error, {failed, Reason}}`:鉴权失败,连接关闭后重连。
## 授权控制命令 ## 授权控制
`iot` 对 `efka` 的授权控制使用 command 语义: `/host/activate` 只修改 `iot` 本地和持久化的 host 授权状态,不再向 `efka` 下发 auth command。`efka` 可以继续保持连接并发送数据,是否处理这些数据由 `iot_host` 当前状态决定。
```erlang 因此当前协议没有 `{command, Ref, {auth, ...}}``{command_response, Ref, {auth, ...}}`。授权关闭时,`iot_host` 保持 channel 在线,但不处理上报数据;授权重新打开后,已在线的 channel 可以继续使用。
{command, Ref, {auth, activate}}
{command, Ref, {auth, deactivate}}
```
`efka` 回复:
```erlang
{command_response, Ref, {auth, ok}}
{command_response, Ref, {auth, {error, Reason}}}
```
处理语义:
- `activate`:如果 `efka` 已经是 `activated`,直接回复 `ok`;否则重新发送 `auth_request`,等待鉴权结果后再回复该 command。
- `deactivate``efka` 进入 `restricted` 状态,并回复 `ok`
- `iot` 侧会异步提交 auth command 并等待对应 `Ref``command_response`,再向 `/host/activate` HTTP 调用方返回结果;等待超时为 10 秒,超时返回 `timeout`,无效响应返回 `invalid response`
## 容器管理命令 ## 容器管理命令
@ -252,5 +233,6 @@ ok
- 旧容器管理:`{request, Ref, {container_request, ...}}` - 旧容器管理:`{request, Ref, {container_request, ...}}`
- 旧容器回复:`{response, Ref, {container_response, ...}}` - 旧容器回复:`{response, Ref, {container_response, ...}}`
- 旧授权控制:`{message, {auth_control, Command}}` - 旧授权控制:`{message, {auth_control, Command}}`
- 已移除的 auth command`{command, Ref, {auth, activate | deactivate}}`
如果需要滚动升级,应先增加临时兼容分支或引入协议版本协商。 如果需要滚动升级,应先增加临时兼容分支或引入协议版本协商。

View File

@ -24,9 +24,8 @@
%% agent的状态 activated %% agent的状态 activated
-define(STATE_DISCONNECTED, disconnected). -define(STATE_DISCONNECTED, disconnected).
%%
-define(STATE_AUTH, auth). -define(STATE_AUTH, auth).
%%
-define(STATE_RESTRICTED, restricted).
%% %%
-define(STATE_ACTIVATED, activated). -define(STATE_ACTIVATED, activated).
@ -34,8 +33,6 @@
socket :: undefined | ssl:sslsocket(), socket :: undefined | ssl:sslsocket(),
%% auth请求的refauth请求和响应的对应关系 %% auth请求的refauth请求和响应的对应关系
auth_ref = undefined :: undefined | reference(), auth_ref = undefined :: undefined | reference(),
%% iot auth command ref
auth_control_ref = undefined :: undefined | reference(),
dropped_message_count = 0 :: non_neg_integer() dropped_message_count = 0 :: non_neg_integer()
}). }).
@ -127,7 +124,7 @@ handle_event({call, From}, dropped_message_count, _StateName, State = #state{dro
{keep_state, State, [{reply, From, DroppedCount}]}; {keep_state, State, [{reply, From, DroppedCount}]};
%% %%
handle_event(info, {timeout, _, create_transport}, ?STATE_DISCONNECTED, State = #state{}) -> handle_event(info, {timeout, _, create_transport}, ?STATE_DISCONNECTED, State) ->
case connect_socket() of case connect_socket() of
{ok, Socket} -> {ok, Socket} ->
Ref = make_ref(), Ref = make_ref(),
@ -139,12 +136,11 @@ handle_event(info, {timeout, _, create_transport}, ?STATE_DISCONNECTED, State =
{keep_state, State#state{socket = undefined}} {keep_state, State#state{socket = undefined}}
end; end;
handle_event(state_timeout, auth_timeout, ?STATE_AUTH, State = #state{socket = Socket, auth_control_ref = AuthControlRef}) -> handle_event(state_timeout, auth_timeout, ?STATE_AUTH, State = #state{socket = Socket}) ->
logger:debug("[efka_client] auth request timeout"), logger:debug("[efka_client] auth request timeout"),
maybe_send_auth_response(Socket, AuthControlRef, {error, timeout}),
disconnect(Socket), disconnect(Socket),
schedule_reconnect(), schedule_reconnect(),
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined, auth_control_ref = undefined}}; {next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined}};
%% %%
handle_event(info, flush_cache, ?STATE_ACTIVATED, State = #state{socket = Socket}) -> handle_event(info, flush_cache, ?STATE_ACTIVATED, State = #state{socket = Socket}) ->
@ -167,73 +163,56 @@ handle_event(info, {ssl_error, Socket, Reason}, _, State = #state{socket = Socke
logger:debug("[efka_client] ssl error: ~p", [Reason]), logger:debug("[efka_client] ssl error: ~p", [Reason]),
disconnect(Socket), disconnect(Socket),
schedule_reconnect(), schedule_reconnect(),
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined, auth_control_ref = undefined}}; {next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined}};
handle_event(info, {ssl_closed, Socket}, _, State = #state{socket = Socket}) -> handle_event(info, {ssl_closed, Socket}, _, State = #state{socket = Socket}) ->
schedule_reconnect(), schedule_reconnect(),
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined, auth_control_ref = undefined}}; {next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined}};
%%% ssl收到的消息会先 binary_to_term %%% ssl收到的消息会先 binary_to_term
%% iot 使 command/command_response %% iot 使 command/command_response
handle_event(internal, {command, Ref, {container, #{action := list}}}, handle_event(internal, {command, Ref, {container, #{action := list}}}, ?STATE_ACTIVATED, State = #state{socket = Socket}) ->
?STATE_ACTIVATED, State = #state{socket = Socket}) ->
Reply = docker_commands:get_containers(), Reply = docker_commands:get_containers(),
send_container_response(Socket, Ref, Reply), send_container_response(Socket, Ref, Reply),
{keep_state, State}; {keep_state, State};
handle_event(internal, {command, Ref, {container, #{action := deploy, task_id := TaskId, params := Params}}}, handle_event(internal, {command, Ref, {container, #{action := deploy, task_id := TaskId, params := Params}}}, ?STATE_ACTIVATED, State = #state{socket = Socket}) ->
?STATE_ACTIVATED, State = #state{socket = Socket}) ->
Reply = docker_deploy_manager:deploy(TaskId, Params), Reply = docker_deploy_manager:deploy(TaskId, Params),
send_container_response(Socket, Ref, Reply), send_container_response(Socket, Ref, Reply),
{keep_state, State}; {keep_state, State};
handle_event(internal, {command, Ref, {container, #{action := start, target := Target}}}, handle_event(internal, {command, Ref, {container, #{action := start, target := Target}}}, ?STATE_ACTIVATED, State = #state{socket = Socket}) ->
?STATE_ACTIVATED, State = #state{socket = Socket}) ->
Reply = docker_commands:start_container(container_target(Target)), Reply = docker_commands:start_container(container_target(Target)),
send_container_response(Socket, Ref, Reply), send_container_response(Socket, Ref, Reply),
{keep_state, State}; {keep_state, State};
handle_event(internal, {command, Ref, {container, #{action := stop, target := Target, timeout_seconds := TimeoutSeconds}}}, handle_event(internal, {command, Ref, {container, #{action := stop, target := Target, timeout_seconds := TimeoutSeconds}}}, ?STATE_ACTIVATED, State = #state{socket = Socket}) ->
?STATE_ACTIVATED, State = #state{socket = Socket}) ->
Reply = docker_commands:stop_container(container_target(Target), TimeoutSeconds), Reply = docker_commands:stop_container(container_target(Target), TimeoutSeconds),
send_container_response(Socket, Ref, Reply), send_container_response(Socket, Ref, Reply),
{keep_state, State}; {keep_state, State};
handle_event(internal, {command, Ref, {container, #{action := kill, target := Target, signal := Signal}}}, handle_event(internal, {command, Ref, {container, #{action := kill, target := Target, signal := Signal}}}, ?STATE_ACTIVATED, State = #state{socket = Socket}) ->
?STATE_ACTIVATED, State = #state{socket = Socket}) ->
Reply = docker_commands:kill_container(container_target(Target), to_binary(Signal)), Reply = docker_commands:kill_container(container_target(Target), to_binary(Signal)),
send_container_response(Socket, Ref, Reply), send_container_response(Socket, Ref, Reply),
{keep_state, State}; {keep_state, State};
handle_event(internal, {command, Ref, {container, #{action := remove, target := Target, force := Force, remove_volumes := RemoveVolumes}}}, handle_event(internal, {command, Ref, {container, #{action := remove, target := Target, force := Force, remove_volumes := RemoveVolumes}}}, ?STATE_ACTIVATED, State = #state{socket = Socket}) ->
?STATE_ACTIVATED, State = #state{socket = Socket}) ->
Reply = docker_commands:remove_container(container_target(Target), to_bool(Force), to_bool(RemoveVolumes)), Reply = docker_commands:remove_container(container_target(Target), to_bool(Force), to_bool(RemoveVolumes)),
send_container_response(Socket, Ref, Reply), send_container_response(Socket, Ref, Reply),
{keep_state, State}; {keep_state, State};
handle_event(internal, {command, Ref, {container, #{action := config, target := Target, config := Config}}}, handle_event(internal, {command, Ref, {container, #{action := config, target := Target, config := Config}}}, ?STATE_ACTIVATED, State = #state{socket = Socket}) ->
?STATE_ACTIVATED, State = #state{socket = Socket}) ->
Reply = docker_helper:update_container_config(container_target(Target), iolist_to_binary(Config)), Reply = docker_helper:update_container_config(container_target(Target), iolist_to_binary(Config)),
send_container_response(Socket, Ref, Reply), send_container_response(Socket, Ref, Reply),
{keep_state, State}; {keep_state, State};
handle_event(internal, {command, Ref, {container, Request}}, ?STATE_RESTRICTED, State = #state{socket = Socket}) ->
logger:notice("[efka_client] get an invalid command: ~p, agent restricted", [Request]),
send_container_response(Socket, Ref, {error, <<"agent restricted">>}),
{keep_state, State};
handle_event(internal, {command, Ref, {container, Request}}, _StateName, State = #state{socket = Socket}) -> handle_event(internal, {command, Ref, {container, Request}}, _StateName, State = #state{socket = Socket}) ->
logger:notice("[efka_client] get an invalid command: ~p, agent invalid", [Request]), logger:notice("[efka_client] get an invalid command: ~p, agent invalid", [Request]),
send_container_response(Socket, Ref, {error, <<"agent invalid">>}), send_container_response(Socket, Ref, {error, <<"agent invalid">>}),
{keep_state, State}; {keep_state, State};
%% response %% response
handle_event(internal, {response, AuthRef, {auth_response, ok}}, ?STATE_AUTH, State = #state{socket = Socket, auth_ref = AuthRef, auth_control_ref = AuthControlRef}) -> handle_event(internal, {response, AuthRef, {auth_response, ok}}, ?STATE_AUTH, State = #state{auth_ref = AuthRef}) ->
logger:debug("[efka_client] auth success"), logger:debug("[efka_client] auth success"),
maybe_send_auth_response(Socket, AuthControlRef, ok), {next_state, ?STATE_ACTIVATED, State#state{auth_ref = undefined}, [{next_event, info, flush_cache}]};
{next_state, ?STATE_ACTIVATED, State#state{auth_ref = undefined, auth_control_ref = undefined}, [{next_event, info, flush_cache}]}; handle_event(internal, {response, AuthRef, {auth_response, {error, Reason}}}, ?STATE_AUTH, State = #state{socket = Socket, auth_ref = AuthRef}) ->
handle_event(internal, {response, AuthRef, {auth_response, {error, {denied, Message}}}}, ?STATE_AUTH, State = #state{socket = Socket, auth_ref = AuthRef, auth_control_ref = AuthControlRef}) ->
logger:debug("[efka_client] auth denied, message: ~p", [Message]),
maybe_send_auth_response(Socket, AuthControlRef, {error, {denied, Message}}),
{next_state, ?STATE_RESTRICTED, State#state{auth_ref = undefined, auth_control_ref = undefined}};
handle_event(internal, {response, AuthRef, {auth_response, {error, Reason}}}, ?STATE_AUTH, State = #state{socket = Socket, auth_ref = AuthRef, auth_control_ref = AuthControlRef}) ->
logger:debug("[efka_client] auth failed, reason: ~p", [Reason]), logger:debug("[efka_client] auth failed, reason: ~p", [Reason]),
maybe_send_auth_response(Socket, AuthControlRef, {error, Reason}),
disconnect(Socket), disconnect(Socket),
schedule_reconnect(), schedule_reconnect(),
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined, auth_control_ref = undefined}}; {next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined}};
handle_event(internal, {response, _Ref, Reply}, StateName, State) -> handle_event(internal, {response, _Ref, Reply}, StateName, State) ->
logger:warning("[efka_client] ignore unexpected response in state ~p: ~p", [StateName, Reply]), logger:warning("[efka_client] ignore unexpected response in state ~p: ~p", [StateName, Reply]),
{keep_state, State}; {keep_state, State};
@ -241,23 +220,6 @@ handle_event(internal, {command_response, _Ref, Reply}, StateName, State) ->
logger:warning("[efka_client] ignore unexpected command_response in state ~p: ~p", [StateName, Reply]), logger:warning("[efka_client] ignore unexpected command_response in state ~p: ~p", [StateName, Reply]),
{keep_state, State}; {keep_state, State};
%%
handle_event(internal, {command, CommandRef, {auth, Cmd}}, StateName, State = #state{socket = Socket}) ->
logger:debug("[efka_client] auth cmd: ~p", [Cmd]),
case {Cmd, StateName} of
{activate, ?STATE_ACTIVATED} ->
send_auth_response(Socket, CommandRef, ok),
{keep_state, State};
{activate, _} ->
Ref = make_ref(),
AuthPacket = auth_packet(Ref),
ok = ssl:send(Socket, AuthPacket),
{next_state, ?STATE_AUTH, State#state{auth_ref = Ref, auth_control_ref = CommandRef}, [{state_timeout, 5000, auth_timeout}]};
{deactivate, _} ->
send_auth_response(Socket, CommandRef, ok),
{next_state, ?STATE_RESTRICTED, State#state{auth_ref = undefined, auth_control_ref = undefined}}
end;
%% Pub/Sub机制 %% Pub/Sub机制
handle_event(internal, {message, {pub, #{topic := Topic, qos := Qos, content := Content}}}, ?STATE_ACTIVATED, State) -> handle_event(internal, {message, {pub, #{topic := Topic, qos := Qos, content := Content}}}, ?STATE_ACTIVATED, State) ->
logger:debug("[efka_client] get pub topic: ~p, qos: ~p, content: ~p", [Topic, Qos, Content]), logger:debug("[efka_client] get pub topic: ~p, qos: ~p, content: ~p", [Topic, Qos, Content]),
@ -327,17 +289,6 @@ send_container_response(Socket, Ref, Reply) ->
Packet = term_to_binary({command_response, Ref, {container, Reply}}), Packet = term_to_binary({command_response, Ref, {container, Reply}}),
ok = ssl:send(Socket, Packet). ok = ssl:send(Socket, Packet).
-spec send_auth_response(ssl:sslsocket(), reference(), term()) -> ok.
send_auth_response(Socket, Ref, Reply) ->
Packet = term_to_binary({command_response, Ref, {auth, Reply}}),
ok = ssl:send(Socket, Packet).
-spec maybe_send_auth_response(ssl:sslsocket(), undefined | reference(), term()) -> ok.
maybe_send_auth_response(_Socket, undefined, _Reply) ->
ok;
maybe_send_auth_response(Socket, Ref, Reply) when is_reference(Ref) ->
send_auth_response(Socket, Ref, Reply).
-spec container_target(map()) -> binary(). -spec container_target(map()) -> binary().
container_target(Target) when is_map(Target) -> container_target(Target) when is_map(Target) ->
NameBin = to_binary(maps:get(name, Target, <<>>)), NameBin = to_binary(maps:get(name, Target, <<>>)),