summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan Lehnardt <jan@apache.org>2013-07-31 15:12:30 +0200
committerJan Lehnardt <jan@apache.org>2013-08-02 21:17:03 +0200
commit9a7ff964c61ac51913d5e7701d15bf2736ae66cf (patch)
tree2ec0ee2ce91176ae0be7593fc95501ac3756a054
parentfb78b466b5a79bd6384915ba3b306ec0153c6579 (diff)
downloadcouchdb-9a7ff964c61ac51913d5e7701d15bf2736ae66cf.tar.gz
add couch_plugins
-rw-r--r--src/couch_plugins/ebin/couch_plugins.app9
-rw-r--r--src/couch_plugins/src/couch_plugins.app.src12
-rw-r--r--src/couch_plugins/src/couch_plugins.erl248
-rw-r--r--src/couch_plugins/src/couch_plugins_httpd.erl10
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").