You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ja...@apache.org on 2013/08/02 22:08:38 UTC

[24/49] git commit: updated refs/heads/1867-feature-plugins to d269b53

add couch_plugins


Project: http://git-wip-us.apache.org/repos/asf/couchdb/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb/commit/9a7ff964
Tree: http://git-wip-us.apache.org/repos/asf/couchdb/tree/9a7ff964
Diff: http://git-wip-us.apache.org/repos/asf/couchdb/diff/9a7ff964

Branch: refs/heads/1867-feature-plugins
Commit: 9a7ff964c61ac51913d5e7701d15bf2736ae66cf
Parents: fb78b46
Author: Jan Lehnardt <ja...@apache.org>
Authored: Wed Jul 31 15:12:30 2013 +0200
Committer: Jan Lehnardt <ja...@apache.org>
Committed: Fri Aug 2 21:17:03 2013 +0200

----------------------------------------------------------------------
 src/couch_plugins/ebin/couch_plugins.app      |   9 +
 src/couch_plugins/src/couch_plugins.app.src   |  12 +
 src/couch_plugins/src/couch_plugins.erl       | 248 +++++++++++++++++++++
 src/couch_plugins/src/couch_plugins_httpd.erl |  10 +
 4 files changed, 279 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb/blob/9a7ff964/src/couch_plugins/ebin/couch_plugins.app
----------------------------------------------------------------------
diff --git a/src/couch_plugins/ebin/couch_plugins.app b/src/couch_plugins/ebin/couch_plugins.app
new file mode 100644
index 0000000..b67645e
--- /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]}]}.

http://git-wip-us.apache.org/repos/asf/couchdb/blob/9a7ff964/src/couch_plugins/src/couch_plugins.app.src
----------------------------------------------------------------------
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 0000000..059663c
--- /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, []}
+ ]}.

http://git-wip-us.apache.org/repos/asf/couchdb/blob/9a7ff964/src/couch_plugins/src/couch_plugins.erl
----------------------------------------------------------------------
diff --git a/src/couch_plugins/src/couch_plugins.erl b/src/couch_plugins/src/couch_plugins.erl
new file mode 100644
index 0000000..e406b2a
--- /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
+

http://git-wip-us.apache.org/repos/asf/couchdb/blob/9a7ff964/src/couch_plugins/src/couch_plugins_httpd.erl
----------------------------------------------------------------------
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 0000000..1e61aa2
--- /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").