diff options
Diffstat (limited to 'src/resource_handler.erl')
-rw-r--r-- | src/resource_handler.erl | 465 |
1 files changed, 465 insertions, 0 deletions
diff --git a/src/resource_handler.erl b/src/resource_handler.erl new file mode 100644 index 0000000..a9703fc --- /dev/null +++ b/src/resource_handler.erl @@ -0,0 +1,465 @@ +-module(resource_handler). +-compile({parse_transform, leptus_pt}). + +%% leptus callbacks +-export([init/3]). +-export([terminate/4]). +-export([get/3, put/3, delete/3, post/3]). +-export([cross_domains/3]). + +%%for keeping state +%%The application record is defined in application.hrl +%%In Mnesia, the first element is the type of record and the second element is the key +%%http://erlang.org/doc/apps/mnesia/Mnesia_chap2.html +-include("application.hrl"). + +-define(CONSURL, dict:fetch("consulurl", State)). +-define(CDAPURL, dict:fetch("cdapurl", State)). +%below come from config map +-define(HCInterval, maps:get(<<"hcinterval">>, dict:fetch("configmap", State))). +-define(AutoDeregisterAfter, maps:get(<<"autoderegisterafter">>, dict:fetch("configmap", State))). +-define(PipelineHealthLimit, maps:get(<<"pipelinehealthlimit">>, dict:fetch("configmap", State))). +-define(PUBLICFIELDS, [<<"appname">>, <<"apptype">>, <<"namespace">>, <<"healthcheckurl">>, <<"metricsurl">>, <<"url">>, <<"connectionurl">>, <<"serviceendpoints">>]). + +%super lazy macros/imports... +-import(logging, [audit/3, metrics/3, err/2]). +-import(util, [iso/0, to_str/1]). +%lazy concat +-define(SC(L), util:concat(L)). +%lazy shorthand to write info audit records. man I miss defines in python. c ftw. +-define(AUDI(Req, Bts, XER, Rcode), audit(info, Req, [{bts, Bts}, {xer,XER}, {rcode, RCode}, {mod, mod()}])). + + +%%% +%%Helper functions +%%% +mod() -> to_str(?MODULE). + +get_request_id(Req) -> + %ECOMP request tracing + %see if we got a X-ECOMP-REQUESTID, or generate a new one if not + HXER = leptus_req:header(Req, <<"x-ecomp-requestid">>), + case HXER of + undefined -> + XER = util:gen_uuid(), + %LOL, use the client ip here to shame them into their request id + audit(warning, Req, [{bts, iso()}, {xer, XER}, {mod, mod()}, {msg, "Request is missing requestID. Assigned this one."}]), %eelf documentation says to log this message if requestid was missing + XER; + _ -> + binary_to_list(HXER) %httpc expects strings as headers, so this needs to be str for subsequent passing + end. + +%shared-code function initlization that creates a begining timestamp and gets or generates a request ID +init_api_call(Req) -> + Bts = iso(), %record begining timestamp + XER = get_request_id(Req), %get or generate XER + {Bts, XER}. + +lookup_application(Appname) -> + %do a lookup in mnesia of an appname + Ret = mnesia:transaction(fun() -> mnesia:match_object(application, {application, Appname, '_', '_', '_', '_', '_', '_', '_', '_'}, read) end), + case Ret of + {atomic, []} -> none; %no matches + {atomic, [Rec]} -> Rec + %fail hard if there was more than one result + end. + +appname_to_application_map(Appname) -> + %return a Map of an Mnesia record + Rec = lookup_application(Appname), + case Rec of + none -> none; + {application, Appname, AppType, Namespace, Healthcheckurl, Metricsurl, Url, Connectionurl, ServiceEndpoints, CreationTime} -> + #{<<"appname">> => Appname, + <<"apptype">> => AppType, + <<"namespace">> => Namespace, + <<"healthcheckurl">> => Healthcheckurl, + <<"metricsurl">> => Metricsurl, + <<"url">> => Url, + <<"connectionurl">> => Connectionurl, + <<"serviceendpoints">> => ServiceEndpoints, + <<"creationtime">> => CreationTime + } + end. + +appname_to_field_vals(Appname, FieldList) -> + %Return just a list of values of an application with fields FieldList + M = appname_to_application_map(Appname), + case M of + none -> none; + _ -> [maps:get(F, M) || F <- FieldList] + end. + +appname_to_application_http(XER, Appname, State) -> + %Return an HTTP response of an application record. If this is a program flowlet style app, additionally return it's bound and unbound config + A = appname_to_application_map(Appname), + case A of + none -> {404, "", State}; + _ -> + Body = maps:with(?PUBLICFIELDS, A), + case maps:get(<<"apptype">>, Body) of + %if program-flowlet style app, append the bound and unbound config into the return JSON + <<"program-flowlet">> -> + UB = case consul_interface:consul_get_configuration(XER, Appname, ?CONSURL) of + {200, Unbound} -> Unbound; + {_, _} -> <<"WARNING: COULD NOT FETCH CONFIG FROM CONSUL">> + end, + B = case cdap_interface:get_app_config(XER, Appname, maps:get(<<"namespace">>, Body), ?CDAPURL) of + {200, Bound} -> Bound; + {_, _} -> <<"WARNING: COULD NOT FETCH CONFIG FROM CDAP">> + end, + CM = #{<<"unbound_config">> => UB, + <<"bound_config">> => B}, + {200, {json, maps:merge(Body, CM)}, State}; + %TODO! can we do something for hydrator apps? + <<"hydrator-pipeline">> -> + {200, {json, Body}, State} + end + end. + +-spec parse_progflow_put_body_map(map()) -> + {binary(), binary(), string(), binary(), binary(), map(), map(), any(), lprogram(), any()}. %TODO! Spec parsedservices and parsedprogrampreferences so we don't have any() here... +parse_progflow_put_body_map(Body) -> + Namespace = maps:get(<<"namespace">>, Body), + Streamname = maps:get(<<"streamname">>, Body), + JarURL = maps:get(<<"jar_url">>, Body), + ArtifactName = maps:get(<<"artifact_name">>, Body), + ArtifactVersion = maps:get(<<"artifact_version">>, Body), + AppConfig = maps:get(<<"app_config">>, Body), + AppPreferences = maps:get(<<"app_preferences">>, Body), + ParsedServices = lists:map(fun(S) -> {maps:get(<<"service_name">>, S), + maps:get(<<"service_endpoint">>, S), + maps:get(<<"endpoint_method">>, S)} + end, maps:get(<<"services">>, Body)), + Programs = lists:map(fun(P) -> #program{type=maps:get(<<"program_type">>, P), + id= maps:get(<<"program_id">>, P)} + end, maps:get(<<"programs">>, Body)), + ParsedProgramPreferences = lists:map(fun(P) -> {maps:get(<<"program_type">>, P), + maps:get(<<"program_id">>, P), + maps:get(<<"program_pref">>, P)} + end, maps:get(<<"program_preferences">>, Body)), + {Namespace, Streamname, JarURL, ArtifactName, ArtifactVersion, AppConfig, AppPreferences, ParsedServices, Programs, ParsedProgramPreferences}. + +parse_hydrator_pipeline_put_body_map(Body) -> + Namespace = maps:get(<<"namespace">>, Body), + Streamname = maps:get(<<"streamname">>, Body), + PipelineConfigJsonURL = maps:get(<<"pipeline_config_json_url">>, Body), + + %Dependencies is optional. This function will normalize it's return with [] if the dependencies key was not passed in. + ParsedDependencies = case maps:is_key(<<"dependencies">>, Body) of + true -> + D = maps:get(<<"dependencies">>, Body), + %crash and let caller deal with it if not a list or if required keys are missing. Else parse it into + % {artifact-extends-header, artifact_name, artifact-version-header, artifact_url} + %tuples + % + %regarding the binart_to_lists: these all come in as binaries but they need to be "strings" (which are just lists of integers in erlang) + %for headers requiring strings, see http://stackoverflow.com/questions/28292576/setting-headers-in-a-httpc-post-request-in-erlang + % + lists:map(fun(X) -> {binary_to_list(maps:get(<<"artifact_extends_header">>, X)), + maps:get(<<"artifact_name">>, X), + binary_to_list(maps:get(<<"artifact_version_header">>, X)), + maps:get(<<"artifact_url">>, X), + %even if dependencies is specified, ui_properties is optional. This will normalize it's return with 'none' if not passed in + case maps:is_key(<<"ui_properties_url">>, X) of true -> maps:get(<<"ui_properties_url">>, X); false -> none end + } end, D); + false -> [] %normalize optional user input into []; just prevents user from having to explicitly pass in [] + end, + + {Namespace, Streamname, PipelineConfigJsonURL, ParsedDependencies}. + +parse_put_body(B) -> + Body = jiffy:decode(B, [return_maps]), + Type = maps:get(<<"cdap_application_type">>, Body), + case Type of + <<"program-flowlet">> -> + {pf, <<"program-flowlet">>, parse_progflow_put_body_map(Body)}; + <<"hydrator-pipeline">> -> + {hp, <<"hydrator-pipeline">>, parse_hydrator_pipeline_put_body_map(Body)}; + _ -> + unsupported + end. + +delete_app_helper(Appname, State, XER, Req) -> + %Helper because it is used by both delete and rollback on failed deploy + % + %%Internal Crisis Alert: + % + %I pondered this for some time. There are three points of state for this: the cdap cluster, consul, and the broker's internal database + %The question is, if something in the delete fails, do we: + %1) Tell the user to try again later + %2) Clean up as much as we can, log the error, and keep going + % + %I have decided for now on taking number 2). This is the "Cloudify" way of doing things where you don't raise a NonRecoerable in a Delete operation. + %This has the benefit that this delete operation can be used as the *rollback*, so if anything fails in the deploy, this delete function is called to clean up any dirty state. + % + %Number 1 is not so straitforward, because "putting back things the way they were" is difficult. For example, the deletion from CDAP succeeds, but Consul can't be reached. + %What happens? Do I *redeploy* the CDAP app to try to make their state as it was before the botched delete was called? + % + %My conclusion is that transactions across distributed systems is hard. It's much easier if it is all local (e.g., Transactions in a single Postgres DB) + % + %SO, as a result of this decision, the broker does *NOT* assert the status code of any delete operations to be 200. + %The only way this function does not return a 200 is if I can't even delete from my own database. + % + metrics(info, Req, [{bts, iso()}, {xer, XER}, {mod, mod()}, {msg, io_lib:format("Delete recieved for ~s", [Appname])}]), + case appname_to_field_vals(Appname, [<<"apptype">>, <<"namespace">>]) of + none -> {404, "Tried to delete an application that was not registered", State}; + [AppType, Namespace] -> + try + case AppType of + <<"program-flowlet">> -> + ok = workflows:undeploy_cdap_app(Req, XER, Appname, ?CDAPURL, ?CONSURL, Namespace), + %delete from the program-flowlet supplementary table + {atomic, ok} = mnesia:transaction(fun() -> mnesia:delete(prog_flow_supp, Appname, write) end); + <<"hydrator-pipeline">> -> ok = workflows:undeploy_hydrator_pipeline(Req, XER, Appname, Namespace, ?CDAPURL, ?CONSURL) + end, + %delete from application table (shared between both types of apps) + {atomic, ok} = mnesia:transaction(fun() -> mnesia:delete(application, Appname, write) end), + {200, "", State} %Return + catch + %this is really bad, means I can't even delete from my own database. For now, log and pray. + %generic failure catch-all, catastrophic + Class:Reason -> + err(emergency, [{xer, XER}, {msg, io_lib:format("Catastrophic failure, can't delete ~s from my database. ~s:~s", [Appname, Class, Reason])}]), + err(error, [{xer, XER}, {msg, io_lib:format("~nError Stacktrace:~s", [lager:pr_stacktrace(erlang:get_stacktrace(), {Class, Reason})])}]), + {500, "Please report this error", State} + end + end. + +%%%CALLBACKS %%% +init(_Route, _Req, State) -> + {ok, State}. +terminate(_Reason, _Route, _Req, _State) -> + ok. +%%%FOR Cors support +%%%Note! only matches on host. Does not handle ports. See: https://github.com/s1n4/leptus/issues/55 +cross_domains(_Route, _Req, State) -> + {['_'], State}. + +%%%GET Methods +get("/", Req, State) -> + %The broker's "info" endpoint; returns some possibly useful information + {Bts, XER} = init_api_call(Req), + Apps = util:get_all_appnames_from_db(), + {UT, _} = statistics(wall_clock), + CDAPVer = cdap_interface:get_cdap_cluster_version(XER, ?CDAPURL), + RB = {[ + {<<"cdap cluster version">>, CDAPVer}, + {<<"managed cdap url">>, ?CDAPURL}, + {<<"cdap GUI port">>, cdap_interface:get_cdap_gui_port_from_version(CDAPVer)}, + {<<"number of applications registered">>, length(Apps)}, + {<<"uptime (s)">>, UT/1000}, + {<<"broker API version">>, util:get_my_version()} + ]}, + {RCode, RBody, RState} = {200, {json, RB}, State}, + ?AUDI(Req, Bts, XER, Rcode), + {RCode, RBody, RState}; +get("/application", Req, State) -> + %get a list of all registered apps + {Bts, XER} = init_api_call(Req), + {RCode, RBody, RState} = {200, {json, util:get_all_appnames_from_db()}, State}, + ?AUDI(Req, Bts, XER, Rcode), + {RCode, RBody, RState}; +get("/application/:appname", Req, State) -> + %get information about a registered application + {Bts, XER} = init_api_call(Req), + Appname = leptus_req:param(Req, appname), + {RCode, RBody, RState} = appname_to_application_http(XER, Appname, State), + ?AUDI(Req, Bts, XER, Rcode), + {RCode, RBody, RState}; +get("/application/:appname/metrics", Req, State) -> + %get metrics for a registered application + {Bts, XER} = init_api_call(Req), + Appname = leptus_req:param(Req, appname), + {RCode, RBody, RState} = case appname_to_field_vals(Appname, [<<"apptype">>, <<"namespace">>]) of + none -> {404, "", State}; + [<<"program-flowlet">>, Namespace] -> + {ReturnCode, ReturnBody} = cdap_interface:get_app_metrics(XER, Appname, Namespace, ?CDAPURL), %warning, see note in README, this always reutrns 200 + {ReturnCode, {json, ReturnBody}, State}; + [<<"hydrator-pipeline">>, Namespace] -> + lager:warning("WARNING, metrics not actually implemented yet for pipelines!!"), + {ReturnCode, ReturnBody} = cdap_interface:get_pipeline_metrics(Appname, Namespace, ?CDAPURL), + {ReturnCode, {json, ReturnBody}, State} + end, + ?AUDI(Req, Bts, XER, Rcode), + {RCode, RBody, RState}; +get("/application/:appname/healthcheck", Req, State) -> + %get healthcheck of an application + {Bts, XER} = init_api_call(Req), + Appname = leptus_req:param(Req, appname), + lager:info(io_lib:format("Get Healthcheck recieved for ~s", [Appname])), + {RCode, RBody, RState} = case appname_to_field_vals(Appname, [<<"apptype">>, <<"namespace">>]) of + none -> {404, "", State}; + [<<"program-flowlet">>, Namespace] -> + {cdap_interface:get_app_healthcheck(XER, Appname, Namespace, ?CDAPURL), "", State}; + [<<"hydrator-pipeline">>, Namespace] -> + {cdap_interface:get_pipeline_healthcheck(XER, Appname, Namespace, ?CDAPURL, ?PipelineHealthLimit), "", State} + end, + ?AUDI(Req, Bts, XER, Rcode), + {RCode, RBody, RState}. + +%%%DELETE Methods +delete("/application/:appname", Req, State) -> + %Uninstall and delete a CDAP app + {Bts, XER} = init_api_call(Req), + Appname = leptus_req:param(Req, appname), + {RCode, RBody, RState} = delete_app_helper(Appname, State, XER, Req), + ?AUDI(Req, Bts, XER, Rcode), + {RCode, RBody, RState}. + +%%%PUT Methods +put("/application/:appname", Req, State) -> + %create a new registration; deploys and starts a cdap application + {Bts, XER} = init_api_call(Req), + Appname = leptus_req:param(Req, appname), + {RCode, RBody, RState} = case appname_to_field_vals(Appname, [<<"appname">>]) of + [Appname] -> + {400, "Put recieved on /application/:appname but appname is already registered. Call /application/:appname/reconfigure if trying to reconfigure or delete first", State}; + none -> %no matches, create the resource, return the application record + %Initial put requires the put body parameters + case try parse_put_body(leptus_req:body_raw(Req)) catch _:_ -> invalid end of + %could not parse the body + invalid -> {400, "Invalid PUT Body or unparseable URL", State}; + + %unsupported cdap application type + unsupported -> {404, "Unsupported CDAP Application Type", State}; + + {Type, AppType, Params} -> + %form shared info + %hateaos cuz they aintaos + {RequestUrl,_} = cowboy_req:url((leptus_req:get_req(Req))), + Metricsurl = <<RequestUrl/binary, <<"/metrics">>/binary>>, + Healthcheckurl = <<RequestUrl/binary, <<"/healthcheck">>/binary>>, + + try + case Type of + hp -> + {Namespace, Streamname, PipelineConfigJsonURL, ParsedDependencies} = Params, + ConnectionURL = cdap_interface:form_stream_url_from_streamname(?CDAPURL, Namespace, Streamname), + + %TODO: This! + ServiceEndpoints = [], %unclear if this is possible with pipelines + + %write into mnesia, deploy + A = #application{appname = Appname, apptype = AppType, namespace = Namespace, healthcheckurl = Healthcheckurl, metricsurl = Metricsurl, url = RequestUrl, connectionurl = ConnectionURL, serviceendpoints = ServiceEndpoints, creationtime=erlang:system_time()}, + {atomic,ok} = mnesia:transaction(fun() -> mnesia:write(A) end), + ok = workflows:deploy_hydrator_pipeline(Req, XER, Appname, Namespace, ?CDAPURL, PipelineConfigJsonURL, ParsedDependencies, ?CONSURL, RequestUrl, Healthcheckurl, ?HCInterval, ?AutoDeregisterAfter), + metrics(info, Req, [{bts, iso()}, {xer, XER}, {mod, mod()}, {msg, io_lib:format("New Hydrator Application Created: ~p", [lager:pr(A, ?MODULE)])}]), %see Record Pretty Printing: https://github.com/basho/lager + ok; + pf -> + {Namespace, Streamname, JarURL, ArtifactName, ArtifactVersion, AppConfig, AppPreferences, ParsedServices, Programs, ParsedProgramPreferences} = Params, + %Form URLs that are part of the record + %NOTE: These are both String concatenation functions and neither make an HTTP call so not catching normal {Code, Status} return here + ConnectionURL = cdap_interface:form_stream_url_from_streamname(?CDAPURL, Namespace, Streamname), + ServiceEndpoints = lists:map(fun(X) -> cdap_interface:form_service_json_from_service_tuple(Appname, Namespace, ?CDAPURL, X) end, ParsedServices), + + %write into mnesia. deploy + A = #application{appname = Appname, apptype = AppType, namespace = Namespace, healthcheckurl = Healthcheckurl, metricsurl = Metricsurl, url = RequestUrl, connectionurl = ConnectionURL, serviceendpoints = ServiceEndpoints, creationtime=erlang:system_time()}, + ASupplemental = #prog_flow_supp{appname = Appname, programs = Programs}, + {atomic,ok} = mnesia:transaction(fun() -> mnesia:write(A) end), %warning, here be mnesia magic that knows what table you want to write to based on the record type + {atomic,ok} = mnesia:transaction(fun() -> mnesia:write(ASupplemental) end), %warning: "" + ok = workflows:deploy_cdap_app(Req, XER, Appname, ?CONSURL, ?CDAPURL, ?HCInterval, ?AutoDeregisterAfter, AppConfig, JarURL, ArtifactName, ArtifactVersion, Namespace, AppPreferences, ParsedProgramPreferences, Programs, RequestUrl, Healthcheckurl), + metrics(info, Req, [{bts, iso()}, {xer, XER}, {mod, mod()}, {msg, io_lib:format("New Program-Flowlet Application Created: ~p with supplemental data: ~p", [lager:pr(A, ?MODULE), lager:pr(ASupplemental, ?MODULE)])}]), + ok + end, + appname_to_application_http(XER, Appname, State) + + catch + %catch a bad HTTP error code + error:{badmatch, {BadErrorCode, BadStatusMsg}} -> + err(error, [{xer, XER}, {msg, io_lib:format("Badmatch caught in Deploy. Rolling Back. ~p ~s", [BadErrorCode, BadStatusMsg])}]), + {_,_,_} = delete_app_helper(Appname, State, XER, Req), + {BadErrorCode, BadStatusMsg, State}; %pass the bad error/status back to user + Class:Reason -> + %generic failure catch-all, catastrophic + err(error, [{xer, XER}, {msg, io_lib:format("~nUnexpected Exception caught in Deploy. Error Stacktrace:~s", [lager:pr_stacktrace(erlang:get_stacktrace(), {Class, Reason})])}]), + {_,_,_} = delete_app_helper(Appname, State, XER, Req), + {500, "Please report this error", State} + end + end + end, + ?AUDI(Req, Bts, XER, Rcode), + {RCode, RBody, RState}; +put("/application/:appname/reconfigure", Req, State) -> + %if appname already is registerd, trigger a consul pull and reconfigure + {Bts, XER} = init_api_call(Req), + Appname = leptus_req:param(Req, appname), + {RCode, RBody, RState} = case appname_to_field_vals(Appname, [<<"namespace">>]) of + none -> {404, "Reconfigure recieved but the app is not registered", State}; + [Namespace] -> + D = jiffy:decode(leptus_req:body_raw(Req), [return_maps]), + case try maps:get(<<"config">>, D) catch _:_ -> invalid end of + invalid -> {400, "Invalid PUT Reconfigure Body: key 'config' is missing", State}; + Config -> + case try maps:get(<<"reconfiguration_type">>, D) catch _:_ -> invalid end of + invalid -> {400, "Invalid PUT Reconfigure Body: key 'reconfiguration_type' is missing", State}; + <<"program-flowlet-app-config">> -> + %reconfigure a program-flowlet style app's app config + try + ok = workflows:app_config_reconfigure(Req, XER, Appname, Namespace, ?CONSURL, ?CDAPURL, Config), + {200, "", State} + catch Class:Reason -> + err(error, [{xer,XER}, {msg, io_lib:format("~nError Stacktrace:~s", [lager:pr_stacktrace(erlang:get_stacktrace(), {Class, Reason})])}]), + {500, "", State} + end; + <<"program-flowlet-app-preferences">> -> + %reconfigure a program-flowlet style app's app config + try + ok = workflows:app_preferences_reconfigure(Req, XER, Appname, Namespace, ?CONSURL, ?CDAPURL, Config), + {200, "", State} + catch Class:Reason -> + err(error, [{xer,XER}, {msg, io_lib:format("~nError Stacktrace:~s", [lager:pr_stacktrace(erlang:get_stacktrace(), {Class, Reason})])}]), + {500, "", State} + end; + <<"program-flowlet-smart">> -> + %try to "figure out" whether the supplied JSON contains keys in appconfig, app preferences, or both + try + ok = workflows:smart_reconfigure(Req, XER, Appname, Namespace, ?CONSURL, ?CDAPURL, Config), + {200, "", State} + catch + %catch a bad HTTP error code; also catches the non-overlapping configuration case + error:{badmatch, {BadErrorCode, BadStatusMsg}} -> + err(error, [{xer, XER}, {msg, io_lib:format("~p ~s", [BadErrorCode, BadStatusMsg])}]), + {BadErrorCode, BadStatusMsg, State}; + Class:Reason -> + err(error, [{xer,XER}, {msg, io_lib:format("~nError Stacktrace:~s", [lager:pr_stacktrace(erlang:get_stacktrace(), {Class, Reason})])}]), + {500, "", State} + end; + NI -> + %TODO! Implement other types of reconfig once CDAP APIs exis + {501, io_lib:format("This type (~s) of reconfiguration is not implemented", [NI]), State} + end + end + end, + ?AUDI(Req, Bts, XER, Rcode), + {RCode, RBody, RState}. + +%%%POST methods +post("/application/delete", Req, State) -> + %This follows the AWS S3 Multi Key Delete: http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html + %Except I added an additional special value called "*" + {Bts, XER} = init_api_call(Req), + {RCode, RBody, RState} = case try + B = maps:get(<<"appnames">>, jiffy:decode(leptus_req:body_raw(Req), [return_maps])), + true = erlang:is_list(B), + B + catch _:_ -> + invalid + end + of + invalid -> {400, "Invalid PUT Body", State}; + IDs -> + case IDs of + [] -> {200, "EMPTY PUT BODY", State}; + _ -> + %<<"*">> -> + %this block deleted all apps, but decided this backdoor wasn't very RESTy + %% {atomic, Apps} = mnesia:transaction(fun() -> mnesia:match_object(application, {application, '_', '_', '_', '_', '_', '_', '_', '_', '_'}, read) end), + % AppsToDelete = lists:map(fun(X) -> {application, Appname, _,_,_,_,_,_,_,_} = X, Appname end, Apps), + Returns = lists:map(fun(X) -> delete_app_helper(X, State, XER, Req) end, IDs), + RL = lists:map(fun({RC, _, _}) -> RC end, Returns), + {200, jiffy:encode(RL), State} + end + end, + ?AUDI(Req, Bts, XER, Rcode), + {RCode, RBody, RState}. |