diff options
author | Jan Lehnardt <jan@apache.org> | 2013-07-31 15:12:30 +0200 |
---|---|---|
committer | Jan Lehnardt <jan@apache.org> | 2013-08-02 21:17:03 +0200 |
commit | 9a7ff964c61ac51913d5e7701d15bf2736ae66cf (patch) | |
tree | 2ec0ee2ce91176ae0be7593fc95501ac3756a054 | |
parent | fb78b466b5a79bd6384915ba3b306ec0153c6579 (diff) | |
download | couchdb-9a7ff964c61ac51913d5e7701d15bf2736ae66cf.tar.gz |
add couch_plugins
-rw-r--r-- | src/couch_plugins/ebin/couch_plugins.app | 9 | ||||
-rw-r--r-- | src/couch_plugins/src/couch_plugins.app.src | 12 | ||||
-rw-r--r-- | src/couch_plugins/src/couch_plugins.erl | 248 | ||||
-rw-r--r-- | src/couch_plugins/src/couch_plugins_httpd.erl | 10 |
4 files changed, 279 insertions, 0 deletions
diff --git a/src/couch_plugins/ebin/couch_plugins.app b/src/couch_plugins/ebin/couch_plugins.app new file mode 100644 index 000000000..b67645e4a --- /dev/null +++ b/src/couch_plugins/ebin/couch_plugins.app @@ -0,0 +1,9 @@ +{application,couch_plugins, + [{description,"A CouchDB Plugin Installer"}, + {vsn,"1"}, + {registered,[]}, + {applications,[kernel,stdlib]}, + {mod,{couch_plugins_app,[]}}, + {env,[]}, + {modules,[couch_plugins,couch_plugins_app,couch_plugins_httpd, + couch_plugins_sup]}]}. diff --git a/src/couch_plugins/src/couch_plugins.app.src b/src/couch_plugins/src/couch_plugins.app.src new file mode 100644 index 000000000..059663c50 --- /dev/null +++ b/src/couch_plugins/src/couch_plugins.app.src @@ -0,0 +1,12 @@ +{application, couch_plugins, + [ + {description, "A CouchDB Plugin Installer"}, + {vsn, "1"}, + {registered, []}, + {applications, [ + kernel, + stdlib + ]}, + {mod, { couch_plugins_app, []}}, + {env, []} + ]}. diff --git a/src/couch_plugins/src/couch_plugins.erl b/src/couch_plugins/src/couch_plugins.erl new file mode 100644 index 000000000..e406b2a34 --- /dev/null +++ b/src/couch_plugins/src/couch_plugins.erl @@ -0,0 +1,248 @@ +-module(couch_plugins). +-include("couch_db.hrl"). +%% Application callbacks +-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", "Z9xK+OKLRvqKx3uoQHsiTuv6mrY="}]}). + + +-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. + +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) -> + 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. + + + + +-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 + diff --git a/src/couch_plugins/src/couch_plugins_httpd.erl b/src/couch_plugins/src/couch_plugins_httpd.erl new file mode 100644 index 000000000..1e61aa251 --- /dev/null +++ b/src/couch_plugins/src/couch_plugins_httpd.erl @@ -0,0 +1,10 @@ +-module(couch_plugins_httpd). + +-export([handle_req/1]). + +-include_lib("couch_db.hrl"). + +handle_req(#httpd{method='PUT'}=Req) -> + couch_httpd:send_json(Req, 202, {[{ok, true}]}); +handle_req(Req) -> + couch_httpd:send_method_not_allowed(Req, "PUT"). |