summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cdapbroker.app.src2
-rw-r--r--src/resource_handler.erl313
-rw-r--r--src/resource_handler_tests.erl136
3 files changed, 296 insertions, 155 deletions
diff --git a/src/cdapbroker.app.src b/src/cdapbroker.app.src
index 157b141..b95d534 100644
--- a/src/cdapbroker.app.src
+++ b/src/cdapbroker.app.src
@@ -1,6 +1,6 @@
{application, cdapbroker,
[{description, "Interface between Consul and CDAP in DCAE"},
- {vsn, "4.0.4"},
+ {vsn, "4.0.5"},
{registered, []},
{mod, { cdapbroker_app, []}},
{applications,
diff --git a/src/resource_handler.erl b/src/resource_handler.erl
index a9703fc..8c1b343 100644
--- a/src/resource_handler.erl
+++ b/src/resource_handler.erl
@@ -39,13 +39,13 @@ 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 ->
+ 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.
@@ -58,24 +58,24 @@ init_api_call(Req) ->
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
+ case Ret of
{atomic, []} -> none; %no matches
- {atomic, [Rec]} -> Rec
+ {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;
+ case Rec of
+ none -> none;
{application, Appname, AppType, Namespace, Healthcheckurl, Metricsurl, Url, Connectionurl, ServiceEndpoints, CreationTime} ->
- #{<<"appname">> => Appname,
+ #{<<"appname">> => Appname,
<<"apptype">> => AppType,
<<"namespace">> => Namespace,
- <<"healthcheckurl">> => Healthcheckurl,
- <<"metricsurl">> => Metricsurl,
- <<"url">> => Url,
+ <<"healthcheckurl">> => Healthcheckurl,
+ <<"metricsurl">> => Metricsurl,
+ <<"url">> => Url,
<<"connectionurl">> => Connectionurl,
<<"serviceendpoints">> => ServiceEndpoints,
<<"creationtime">> => CreationTime
@@ -85,7 +85,7 @@ appname_to_application_map(Appname) ->
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
+ case M of
none -> none;
_ -> [maps:get(F, M) || F <- FieldList]
end.
@@ -93,14 +93,14 @@ appname_to_field_vals(Appname, FieldList) ->
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};
- _ ->
+ 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
+ UB = case consul_interface:consul_get_configuration(XER, Appname, ?CONSURL) of
{200, Unbound} -> Unbound;
{_, _} -> <<"WARNING: COULD NOT FETCH CONFIG FROM CONSUL">>
end,
@@ -112,74 +112,11 @@ appname_to_application_http(XER, Appname, State) ->
<<"bound_config">> => B},
{200, {json, maps:merge(Body, CM)}, State};
%TODO! can we do something for hydrator apps?
- <<"hydrator-pipeline">> ->
+ <<"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
%
@@ -190,7 +127,7 @@ delete_app_helper(Appname, State, XER, Req) ->
%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.
+ %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.
@@ -198,16 +135,16 @@ delete_app_helper(Appname, State, XER, Req) ->
%
%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.
+ %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])}]),
+ 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">> ->
+ 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);
@@ -216,8 +153,8 @@ delete_app_helper(Appname, State, XER, Req) ->
%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.
+ 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])}]),
@@ -288,7 +225,7 @@ get("/application/:appname/healthcheck", Req, State) ->
{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
+ {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};
@@ -305,22 +242,128 @@ delete("/application/:appname", Req, State) ->
Appname = leptus_req:param(Req, appname),
{RCode, RBody, RState} = delete_app_helper(Appname, State, XER, Req),
?AUDI(Req, Bts, XER, Rcode),
- {RCode, RBody, RState}.
+ {RCode, RBody, RState}.
%%%PUT Methods
+parse_put_body(B) ->
+ %parse the PUT body to application
+ try
+ Body = jiffy:decode(B, [return_maps]),
+ Type = maps:get(<<"cdap_application_type">>, Body),
+ case Type == <<"hydrator-pipeline">> orelse Type == <<"program-flowlet">> of
+ false -> unsupported;
+ true ->
+ %common to both
+ Namespace = maps:get(<<"namespace">>, Body),
+ Streamname = maps:get(<<"streamname">>, Body),
+ case Type of
+ <<"program-flowlet">> ->
+ 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)),
+ {pf, <<"program-flowlet">>, {Namespace, Streamname, JarURL, ArtifactName, ArtifactVersion, AppConfig, AppPreferences, ParsedServices, Programs, ParsedProgramPreferences}};
+ <<"hydrator-pipeline">> ->
+ 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}
+ %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,
+ {hp, <<"hydrator-pipeline">>, {Namespace, Streamname, PipelineConfigJsonURL, ParsedDependencies}}
+ end
+ end
+ catch _:_ -> invalid
+ end.
+
+parse_reconfiguration_put_body(Body) ->
+ try
+ D = jiffy:decode(Body, [return_maps]),
+ ReconfigurationType = maps:get(<<"reconfiguration_type">>, D),
+ Config = maps:get(<<"config">>, D),
+ case ReconfigurationType == <<"program-flowlet-app-config">> orelse
+ ReconfigurationType == <<"program-flowlet-app-preferences">> orelse
+ ReconfigurationType == <<"program-flowlet-smart">> of
+ false -> notimplemented;
+ true -> {ReconfigurationType, Config}
+ end
+ catch _:_ -> invalid
+ end.
+
+handle_reconfigure_put(Req, State, XER, Appname, ReqBody, AppnameToNS) ->
+ %handle the reconfiguration put. broker out from the http call, and takes the lookup func as an arg, to allow for better unit testing.
+ %this is still not a pure function due to the workflows call, still needs enhancement
+ case AppnameToNS(Appname) of
+ none -> {404, "Reconfigure recieved but the app is not registered", State};
+ Namespace ->
+ ParsedBody = parse_reconfiguration_put_body(ReqBody),
+ case ParsedBody of
+ invalid -> {400, "Invalid PUT Reconfigure Body", State};
+ notimplemented -> {501, "This type of reconfiguration is not implemented", State};
+ {ReconfigurationType, Config} ->
+ try
+ ok = case ReconfigurationType of
+ <<"program-flowlet-app-config">> ->
+ %reconfigure a program-flowlet style app's app config
+ workflows:app_config_reconfigure(Req, XER, Appname, Namespace, ?CONSURL, ?CDAPURL, Config);
+ <<"program-flowlet-app-preferences">> ->
+ %reconfigure a program-flowlet style app's app config
+ workflows:app_preferences_reconfigure(Req, XER, Appname, Namespace, ?CONSURL, ?CDAPURL, Config);
+ <<"program-flowlet-smart">> ->
+ %try to "figure out" whether the supplied JSON contains keys in appconfig, app preferences, or both
+ workflows:smart_reconfigure(Req, XER, Appname, Namespace, ?CONSURL, ?CDAPURL, Config)
+ end,
+ {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
+ end
+ end.
+
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] ->
+ [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
+ case parse_put_body(leptus_req:body_raw(Req)) 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};
@@ -330,13 +373,13 @@ put("/application/:appname", Req, State) ->
{RequestUrl,_} = cowboy_req:url((leptus_req:get_req(Req))),
Metricsurl = <<RequestUrl/binary, <<"/metrics">>/binary>>,
Healthcheckurl = <<RequestUrl/binary, <<"/healthcheck">>/binary>>,
-
+
try
- case Type of
- hp ->
+ 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
@@ -350,21 +393,21 @@ put("/application/:appname", Req, State) ->
{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),
+ 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)])}]),
+ 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
%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])}]),
@@ -384,53 +427,15 @@ 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,
+ ReqBody = leptus_req:body_raw(Req),
+ AppnameToNS = fun(App) ->
+ X = appname_to_field_vals(App, [<<"namespace">>]),
+ case X of
+ none -> X;
+ [Namespace] -> Namespace
+ end
+ end,
+ {RCode, RBody, RState} = handle_reconfigure_put(Req, State, XER, Appname, ReqBody, AppnameToNS),
?AUDI(Req, Bts, XER, Rcode),
{RCode, RBody, RState}.
@@ -439,20 +444,20 @@ 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
+ {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
+ 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),
diff --git a/src/resource_handler_tests.erl b/src/resource_handler_tests.erl
new file mode 100644
index 0000000..cab9558
--- /dev/null
+++ b/src/resource_handler_tests.erl
@@ -0,0 +1,136 @@
+% ============LICENSE_START=======================================================
+% org.onap.dcae
+% ================================================================================
+% Copyright (c) 2017 AT&T Intellectual Property. All rights reserved.
+% ================================================================================
+% 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.
+% ============LICENSE_END=========================================================
+%
+% ECOMP is a trademark and service mark of AT&T Intellectual Property.
+
+-module(resource_handler_tests).
+-include_lib("eunit/include/eunit.hrl").
+-include("application.hrl").
+-import(resource_handler, [
+ parse_put_body/1,
+ parse_reconfiguration_put_body/1,
+ handle_reconfigure_put/6
+ ]).
+
+parse_put_body_test() ->
+ Valid = {[
+ {<<"cdap_application_type">>, <<"program-flowlet">>},
+ {<<"namespace">>, <<"ns">>},
+ {<<"streamname">>, <<"sn">>},
+ {<<"jar_url">>, <<"www.foo.com">>},
+ {<<"artifact_name">>, <<"art_name">>},
+ {<<"artifact_version">>, <<"art_ver">>},
+ {<<"app_config">>, {[{<<"foo">>,<<"bar">>}]}},
+ {<<"app_preferences">>, {[{<<"foop">>,<<"barp">>}]}},
+ {<<"services">>, [{[{<<"service_name">>, <<"Greeting">>},
+ {<<"service_endpoint">>, <<"greet">>},
+ {<<"endpoint_method">>, <<"GET">>}]}]},
+ {<<"programs">>, [
+ {[{<<"program_type">>, <<"flows">>},
+ {<<"program_id">>, <<"WhoFlow">>}]},
+ {[{<<"program_type">>, <<"services">>},
+ {<<"program_id">>, <<"Greeting">>}]}]},
+ {<<"program_preferences">>, [
+ {[{<<"program_type">>,<<"flows">>},
+ {<<"program_id">>, <<"WhoFlow">>},
+ {<<"program_pref">>, {[{<<"foopprog">>,<<"barpprog">>}]}}]}
+ ]}
+ ]},
+ %{Namespace, Streamname, JarURL, ArtifactName, ArtifactVersion, AppConfig, AppPreferences, ParsedServices, Programs, ParsedProgramPreferences}
+ ExpectedL = {<<"ns">>, <<"sn">>, <<"www.foo.com">>, <<"art_name">>, <<"art_ver">>,
+ #{<<"foo">>=><<"bar">>},
+ #{<<"foop">>=><<"barp">>},
+ [{<<"Greeting">>,<<"greet">>,<<"GET">>}],
+ [#program{type = <<"flows">>, id = <<"WhoFlow">>}, #program{type = <<"services">>, id = <<"Greeting">>}],
+ [{<<"flows">>,<<"WhoFlow">>,#{<<"foopprog">>=><<"barpprog">>}}]},
+ Expected = {pf, <<"program-flowlet">>, ExpectedL},
+ ?assert(parse_put_body(jiffy:encode(Valid)) == Expected),
+
+ ValidHydrator1 =
+ {[
+ {<<"cdap_application_type">>, <<"hydrator-pipeline">>},
+ {<<"namespace">>, <<"ns">>},
+ {<<"streamname">>, <<"sn">>},
+ {<<"pipeline_config_json_url">>, "www.foo.com"}
+ ]},
+ ExpectedHy1 = {hp,<<"hydrator-pipeline">>,{<<"ns">>,<<"sn">>,"www.foo.com",[]}},
+ ?assert(parse_put_body(jiffy:encode(ValidHydrator1)) == ExpectedHy1),
+
+ ValidHydrator2 =
+ {[
+ {<<"cdap_application_type">>, <<"hydrator-pipeline">>},
+ {<<"namespace">>, <<"ns">>},
+ {<<"streamname">>, <<"sn">>},
+ {<<"pipeline_config_json_url">>, "www.foo.com"},
+ {<<"dependencies">>, [
+ {[
+ {<<"artifact_extends_header">>, <<"system:cdap-data-pipeline[4.1.0,5.0.0)">>},
+ {<<"artifact_name">>, <<"art carney">>},
+ {<<"artifact_version_header">>, <<"1.0.0-SNAPSHOT">>},
+ {<<"artifact_url">>, <<"www.foo.com/sup/baphomet.jar">>},
+ {<<"ui_properties_url">>, <<"www.foo2.com/sup/baphomet.jar">>}
+ ]}
+ ]}
+ ]},
+ %{hp, <<"hydrator-pipeline">>, {Namespace, Streamname, PipelineConfigJsonURL, ParsedDependencies}}
+ ExpectedHy2 = {hp,<<"hydrator-pipeline">>,{<<"ns">>,<<"sn">>,"www.foo.com",[{"system:cdap-data-pipeline[4.1.0,5.0.0)",<<"art carney">>,"1.0.0-SNAPSHOT",<<"www.foo.com/sup/baphomet.jar">>,<<"www.foo2.com/sup/baphomet.jar">>}]}},
+ ?assert(parse_put_body(jiffy:encode(ValidHydrator2)) == ExpectedHy2),
+
+ InvalidType = {[{<<"cdap_application_type">>, <<"NOT TODAY">>}]},
+ erlang:display(parse_put_body(jiffy:encode(InvalidType))),
+ ?assert(parse_put_body(jiffy:encode(InvalidType)) == unsupported),
+
+ InvalidMissing = {[
+ {<<"cdap_application_type">>, <<"program-flowlet">>},
+ {<<"namespace">>, <<"ns">>}
+ ]},
+ ?assert(parse_put_body(jiffy:encode(InvalidMissing)) == invalid).
+
+reconfiguration_put_test() ->
+ %test reconfiguring with an invalid PUT body (missing "reconfiguration_type")
+ AppnameToNS = fun(X) ->
+ case X of
+ <<"notexist">> -> none;
+ <<"exist">> -> <<"ns">>
+ end
+ end,
+ EmptyD = dict:new(),
+
+ I1 = jiffy:encode({[{<<"config">>, <<"bar">>}]}),
+ ?assert(parse_reconfiguration_put_body(I1) == invalid),
+ ?assert(handle_reconfigure_put("", EmptyD, "testXER", <<"exist">>, I1, AppnameToNS) == {400,"Invalid PUT Reconfigure Body",EmptyD}),
+
+ %test reconfiguring with an invalid PUT body (missing app_config)
+ I2 = jiffy:encode({[{<<"reconfiguration_type">>, <<"program-flowlet-app-config">>}, {<<"foo">>, <<"bar">>}]}),
+ ?assert(parse_reconfiguration_put_body(I2) == invalid),
+ ?assert(handle_reconfigure_put("", EmptyD, "testXER", <<"exist">>, I2, AppnameToNS) == {400,"Invalid PUT Reconfigure Body",EmptyD}),
+
+ %test reconfiguring an invalid (unimplemented) type
+ I3 = jiffy:encode({[{<<"config">>, <<"bar">>}, {<<"reconfiguration_type">>, <<"EMPTINESS">>}]}),
+ ?assert(parse_reconfiguration_put_body(I3) == notimplemented),
+ ?assert(handle_reconfigure_put("", EmptyD, "testXER", <<"exist">>, I3, AppnameToNS) == {501,"This type of reconfiguration is not implemented",EmptyD}),
+
+ Valid = jiffy:encode({[{<<"config">>, {[{<<"foo">>, <<"bar">>}]}}, {<<"reconfiguration_type">>,<<"program-flowlet-app-config">>}]}),
+ ?assert(parse_reconfiguration_put_body(Valid) == {<<"program-flowlet-app-config">>,#{<<"foo">>=><<"bar">>}}),
+
+ %no unit test for handle_reconfigure_put yet
+
+ %test for valid but missing
+ %?assert(handle_reconfigure_put("", EmptyD, "testXER", <<"exist">>, I3, AppnameToNS) == {501,"This type of reconfiguration is not implemented",EmptyD}),
+ ?assert(handle_reconfigure_put("", EmptyD, "testXER", <<"notexist">>, Valid, AppnameToNS) == {404,"Reconfigure recieved but the app is not registered", EmptyD}).
+