-module(grisp_tools_deploy). % API -export([run/1]). -import(grisp_tools_util, [event/2]). -import(grisp_tools_util, [shell/2]). -import(grisp_tools_util, [exec/3]). -import(grisp_tools_util, [mv/2]). -import(grisp_tools_util, [rm/1]). -import(grisp_tools_util, [with_file/3]). %--- API ----------------------------------------------------------------------- run(State) -> grisp_tools_util:weave(State, [ fun grisp_tools_step:config/1, {deploy, [ {validate, [ fun grisp_tools_step:apps/1, fun grisp_tools_step:version/1 ]}, fun grisp_tools_step:collect/1, fun package/1, fun release/1, fun copy/1 ]} ]). %--- Tasks --------------------------------------------------------------------- package(#{custom_build := true, build := #{hash := #{value := Hash}}} = State0) -> event(State0, [{type, {custom_build, Hash}}]); package(#{build := #{hash := #{value := Hash}}} = State0) -> State1 = event(State0, [{type, {package, Hash}}]), grisp_tools_util:weave(State1, [ fun meta/1, fun init/1, fun download/1, fun extract/1 ]). release(#{paths := #{install := InstallPath}} = State0) -> Release = maps:get(release, State0, #{}), release(State0, maps:merge(Release, #{erts => InstallPath})). copy(State0) -> grisp_tools_util:pipe(State0, [ fun(S) -> run_script(pre_script, S) end, fun copy_release/1, fun copy_files/1, fun(S) -> run_script(post_script, S) end ]). %--- Internal ------------------------------------------------------------------ % Package downloads meta(State0) -> Meta = grisp_tools_util:package_cache_meta(State0), State1 = event(State0, [package, {meta, Meta}]), mapz:deep_put([package, meta], Meta, State1). init(State0) -> File = grisp_tools_util:package_cache_file(State0), Tmp = grisp_tools_util:package_cache_temp(State0), grisp_tools_util:ensure_dir(Tmp), State1 = event(State0, [{deleting_tmp_file, Tmp}]), rm(Tmp), mapz:deep_merge([State1, #{package => #{file => File, tmp => Tmp}}]). download(#{package := #{meta := Meta}} = State0) -> Client = http_init(), URI = grisp_tools_util:cdn_path(otp, State0), Headers = [{"If-None-Match", ETag} || #{etag := ETag} <- [Meta]], Options = [{stream, {self, once}}, {sync, false}], State1 = event(State0, [{uri, URI}]), ReqID = http_get(URI, Headers, Options, Client), State2 = download_loop(ReqID, State1), case State2 of #{package := #{state := downloaded, tmp := Tmp, file := File, meta := NewMeta}} -> mv(Tmp, File), grisp_tools_util:package_cache_meta(State0, NewMeta); #{package := #{tmp := Tmp}} -> rm(Tmp) end, State2. extract(#{package := #{state := downloaded, file := File}} = State0) -> #{paths := #{install := InstallPath}} = State0, State1 = event(State0, [{start, File}]), case erl_tar:extract(File, [compressed, {cwd, InstallPath}]) of ok -> event(State1, [done]); {error, Reason} -> event(State1, [{error, Reason}]) end; extract(State0) -> event(State0, ['_skip']). copy_files(#{copy := #{destination := Dest, force := Force}, release := Release} = State0) -> #{paths := #{install := InstallPath}} = State0, State1 = event(State0, [files, {init, Dest}]), ERTSPath = filelib:wildcard(binary_to_list(filename:join(InstallPath, "erts-*"))), "erts-" ++ ERTSVsn = filename:basename(ERTSPath), #{name := RelName, version := RelVsn} = Release, Context = #{ release_name => RelName, release_version => RelVsn, erts_vsn => ERTSVsn }, maps:fold( fun(_Name, File, S) -> write_file(Dest, File, Force, Context, S) end, State1, mapz:deep_get([deploy, overlay, files], State0) ). write_file(Dest, #{target := Target} = File, Force, Context, State0) -> Path = filename:join(Dest, Target), State1 = event(State0, [files, {copy, File}]), force_execute(Path, Force, fun(F) -> grisp_tools_util:ensure_dir(F), grisp_tools_util:write_file(Dest, File, Context) end, State1). force_execute(File, Force, Fun, State0) -> State1 = case {filelib:is_file(File), Force} of {true, false} -> event(State0, [files, {error, {exists, File}}]); _ -> State0 end, Fun(File), State1. copy_release(#{release := Release, copy := Copy} = State0) -> #{name := RelName, dir := Source} = Release, #{destination := Dest, force := Force} = Copy, Target = filename:join(Dest, RelName), State1 = event(State0, [release, {copy, Source, Target}]), CopyExe = case Force of true -> "cp -Rf"; false -> "cp -R" end, State2 = case filelib:is_dir(Dest) of false -> event(State1, [release, {error, target_dir_missing, Dest}]); true -> State1 end, Command = string:join([CopyExe, qoute(Source ++ "/"), qoute(Target)], " "), {Output, State3} = shell(State2, Command), event(State3, [release, {copy, {result, Output}}]). qoute(String) -> "\"" ++ String ++ "\"". % Helpers download_loop(ReqID, #{package := #{tmp := Tmp}} = State0) -> with_file(Tmp, [raw, append, binary], fun(Handle) -> download_loop({ReqID, undefined}, Handle, State0, 0) end). download_loop({ReqID, RequestPid}, Handle, State0, Bytes) -> receive % Progress: {http, {ReqID, stream_start, Headers, Pid}} -> ContentLength = proplists:get_value("content-length", Headers), Size = case ContentLength of undefined -> undefined; Length -> list_to_integer(Length) end, State1 = event(State0, [{start, Size}]), ok = httpc:stream_next(Pid), download_loop({ReqID, Pid}, Handle, State1, Bytes); {http, {ReqID, stream, BinBodyPart}} -> NewBytes = Bytes + byte_size(BinBodyPart), State1 = event(State0, [{progress, NewBytes}]), ok = httpc:stream_next(RequestPid), ok = file:write(Handle, BinBodyPart), download_loop({ReqID, RequestPid}, Handle, State1, NewBytes); % Result: {http, {ReqID, stream_end, Headers}} -> NewETag = case lists:keyfind("etag", 1, Headers) of {"etag", ServerETag} -> ServerETag; false -> undefined end, State1 = event(State0, [{complete, NewETag}]), mapz:deep_merge([State1, #{package => #{ state => downloaded, meta => #{etag => NewETag}} }]); {http, {ReqID, {{_HTTPVer, 304, "Not Modified"}, _Headers, _Body}}} -> State1 = event(State0, ['_skip']), mapz:deep_merge([State1, #{package => #{state => not_modified}}]); {http, {ReqID, {{_HTTPVer, 404, "Not Found"}, _Headers, _Body}}} -> #{ otp_version := {_, _, _, OTPVersion}, build := #{hash := #{value := Hash}} } = State0, error({package, {not_found, OTPVersion, Hash}}); {http, Other} -> State1 = event(State0, [{error, Other}]), mapz:deep_merge([State1, #{package => #{state => error}}]) after 120000 -> State1 = event(State0, [{error, timeout}]), mapz:deep_merge([State1, #{package => #{state => error}}]) end. http_init() -> {ok, InetsPid} = inets:start(httpc, [{profile, grisp_tools}], stand_alone), InetsPid. http_get(URI, Headers, Options, InetsPid) -> Request = {URI, Headers}, HTTPOptions = [{connect_timeout, 5000}], {ok, ID} = httpc:request(get, Request, HTTPOptions, Options, InetsPid), ID. run_script(Name, State0) -> case mapz:deep_get([scripts, Name], State0, undefined) of undefined -> State0; Script -> State1 = event(State0, [Name, {run, Script}]), {{ok, Output}, State2} = shell(State1, Script), event(State2, [Name, {result, Output}]) end. release(State0, Release) -> State1 = event(State0, [{start, Release}]), {Spec, State2} = exec(release, State1, [Release]), Merged = maps:merge(Release, Spec), State3 = event(State2, [{done, Merged}]), mapz:deep_merge(State3, #{release => Merged}).