blob: daac885185983b9247c8686246fd1897dc8d740a [file] [log] [blame]
% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.
-module(couch_plugins).
-include("couch_db.hrl").
-export([install/1]).
% couch_plugins:install({"geocouch", "http://127.0.0.1:8000", "1.0.0", [{"R15B03", "+XOJP6GSzmuO2qKdnjO+mWckXVs="}]}).
% couch_plugins:install({"geocouch", "http://people.apache.org/~jan/", "couchdb1.2.x_v0.3.0-11-gd83ba22", [{"R15B03", "ZetgdHj2bY2w37buulWVf3USOZs="}]}).
-define(PLUGIN_DIR, "/tmp/couchdb_plugins").
log(T) ->
?LOG_DEBUG("[couch_plugins] ~p ~n", [T]).
%% "geocouch", "http://localhost:8000/dist", "1.0.0"
-type plugin() :: {string(), string(), string(), list()}.
-spec install(plugin()) -> ok | {error, string()}.
install({Name, _BaseUrl, Version, Checksums}=Plugin) ->
log("Installing " ++ Name),
{ok, LocalFilename} = download(Plugin),
log("downloaded to " ++ LocalFilename),
ok = verify_checksum(LocalFilename, Checksums),
log("checksum verified"),
ok = untargz(LocalFilename),
log("extraction done"),
ok = add_code_path(Name, Version),
log("added code path"),
ok = load_config(Name, Version),
load_plugin(Name),
log("loaded plugin"),
ok.
-spec load_config(string(), string()) -> ok | {error, string()}.
load_config(Name, Version) ->
ConfigFile = ?PLUGIN_DIR ++ "/" ++ get_file_slug(Name, Version) ++ "/priv/config.erlt",
load_config_file(file_exists(ConfigFile), ConfigFile).
-spec load_config_file(boolean(), string()) -> ok | {error, string()}.
load_config_file(false, _) -> ok;
load_config_file(true, ConfigFile) ->
% read file
{ok, ConfigFileData} = file:read_file(ConfigFile),
% split by \n
Lines = binary:split(ConfigFileData, <<"\n">>, [global]),
% feed each line...
lists:foreach(
fun(<<>>) ->
ok; % skip empty lines
(<<";", _Rest/binary>>) ->
ok; % ignore comments
(Line) ->
% ...to couch_util:parse_term()...
case couch_util:parse_term(Line) of
{ok, {{Section, Key}, Value}} ->
% ...and set the configs
?LOG_DEBUG("parsed Line correctly: ~p", [Line]),
couch_config:set(Section, Key, Value);
Else ->
?LOG_ERROR("Error parsing plugin config from line ~s", [Line]),
Else
end
end, Lines),
ok.
-spec add_code_path(string(), string()) -> ok | {error, bad_directory}.
add_code_path(Name, Version) ->
PluginPath = ?PLUGIN_DIR ++ "/" ++ get_file_slug(Name, Version) ++ "/ebin",
case code:add_path(PluginPath) of
true -> ok;
Else ->
?LOG_ERROR("Failed to add PluginPath: '~s'", [PluginPath]),
Else
end.
-spec load_plugin(string()) -> ok | {error, atom()}.
load_plugin(NameList) ->
Name = list_to_atom(NameList),
application:load(Name).
-spec untargz(string()) -> {ok, string()} | {error, string()}.
untargz(Filename) ->
% read .gz file
{ok, GzData} = file:read_file(Filename),
% gunzip
log("unzipped"),
TarData = zlib:gunzip(GzData),
ok = filelib:ensure_dir(?PLUGIN_DIR),
% untar
erl_tar:extract({binary, TarData}, [{cwd, ?PLUGIN_DIR}, keep_old_files]).
% downloads a pluygin .tar.gz into a local plugins directory
-spec download(string()) -> ok | {error, string()}.
download({Name, _BaseUrl, Version, _Checksums}=Plugin) ->
TargetFile = "/tmp/" ++ get_filename(Name, Version),
case file_exists(TargetFile) of
%% wipe and redownload
true -> file:delete(TargetFile);
_Else -> ok
end,
Url = get_url(Plugin),
HTTPOptions = [
{connect_timeout, 30*1000}, % 30 seconds
{timeout, 30*1000} % 30 seconds
],
% todo: windows
Options = [
{stream, TargetFile}, % /tmp/something
{body_format, binary},
{full_result, false}
],
% todo: reduce to just httpc:request()
case httpc:request(get, {Url, []}, HTTPOptions, Options) of
{ok, _Result} ->
log("downloading " ++ Url),
{ok, TargetFile};
Error -> Error
end.
-spec verify_checksum(string(), list()) -> ok | {error, string()}.
verify_checksum(Filename, Checksums) ->
OTPRelease = erlang:system_info(otp_release),
case proplists:get_value(OTPRelease, Checksums) of
undefined ->
?LOG_ERROR("[couch_plugins] Can't find checksum for OTP Release '~s'", [OTPRelease]),
{error, no_checksum};
Checksum ->
do_verify_checksum(Filename, Checksum)
end.
-spec do_verify_checksum(string(), string()) -> ok | {error, string()}.
do_verify_checksum(Filename, Checksum) ->
?LOG_DEBUG("Filename: ~s", [Filename]),
case file:read_file(Filename) of
{ok, Data} ->
ComputedChecksum = binary_to_list(base64:encode(crypto:sha(Data))),
case ComputedChecksum of
Checksum -> ok;
_Else ->
?LOG_ERROR("Checksum mismatch. Wanted: '~p'. Got '~p'", [Checksum, ComputedChecksum]),
{error, checksum_mismatch}
end;
Error -> Error
end.
%% utils
-spec get_url(plugin()) -> string().
get_url({Name, BaseUrl, Version, _Checksums}) ->
BaseUrl ++ "/" ++ get_filename(Name, Version).
-spec get_filename(string(), string()) -> string().
get_filename(Name, Version) ->
get_file_slug(Name, Version) ++ ".tar.gz".
-spec get_file_slug(string(), string()) -> string().
get_file_slug(Name, Version) ->
% OtpRelease does not include patch levels like the -1 in R15B03-1
OTPRelease = erlang:system_info(otp_release),
Name ++ "-" ++ Version ++ "-" ++ OTPRelease.
-spec file_exists(string()) -> boolean().
file_exists(Filename) ->
does_file_exist(file:read_file_info(Filename)).
-spec does_file_exist(term()) -> boolean().
does_file_exist({error, enoent}) -> false;
does_file_exist(_Else) -> true.
% installing a plugin:
% - POST /_plugins -d {plugin-def}
% - get plugin definition
% - get download URL (matching erlang version)
% - download archive
% - match checksum
% - untar-gz archive into a plugins dir
% - code:add_path(“geocouch-{geocouch_version}-{erlang_version}/ebin”)
% - [cp geocouch-{geocouch_version}-{erlang_version}/etc/ ]
% - application:start(geocouch)
% - register plugin in plugin registry
% Plugin registry impl:
% - _plugins database
% - pro: known db ops
% - con: no need for replication, needs to be system db etc.
% - _config/plugins namespace in config
% - pro: lightweight, fits rarely-changing nature better
% - con: potentially not flexible enough
% /geocouch
% /geocouch/dist/
% /geocouch/dist/geocouch-{geocouch_version}-{erlang_version}.tar.gz
% tar.gz includes:
% geocouch-{geocouch_version}-{erlang_version}/
% geocouch-{geocouch_version}-{erlang_version}/ebin
% [geocouch-{geocouch_version}-{erlang_version}/config/config.erlt]
% [geocouch-{geocouch_version}-{erlang_version}/share/]
% config.erlt:
% // {{Section, Key}, Value}
% {{"httpd_db_handlers", "_spatial_cleanup"}, "{couch_spatial_http, handle_cleanup_req}"}
% {{"httpd_design_handlers", "_spatial"}, "{couch_spatial_http, handle_spatial_req}"}
% {{"httpd_design_handlers", "_list"}, "{couch_spatial_list, handle_view_list_req}"}
% {{"httpd_design_handlers", "_info"}, "{couch_spatial_http, handle_info_req}"}
% {{"httpd_design_handlers", "_compact"}, "{couch_spatial_http, handle_compact_req}"}
% milestones
% 1. MVP
% - erlang plugins only
% - no c deps
% - install via futon (admin only)
% - uninstall via futon (admin only)
% - load plugin.tgz from the web
% - no security checking
% - no identity checking
% - hardcoded list of plugins in futon
% - must publish on *.apache.org/*
% 2. Creator friendly
% - couchdb plugin template
% - easy to publish
% 3. Public registry
% - plugin authors can publish stuff independently, shows up in futon
%
% XXX Later
% - signing of plugin releases
% - signing verification of plugin releases
% Questions:
% - where should the downloaded .beam files put?
% - in couch 1.x.x context
% - in bigcouch context
% - what is a server-user owned data/ dir we can use for this, that isn’t db_dir or index_dir or log or var/run or /tmp