Clean up resource handler. 87/11187/1
authorTommy Carpenter <tommy@research.att.com>
Thu, 7 Sep 2017 02:08:30 +0000 (22:08 -0400)
committerTommy Carpenter <tommy@research.att.com>
Fri, 8 Sep 2017 18:28:28 +0000 (14:28 -0400)
Refactors some resource_handler functions to split between pure and impure.
Adds more unit tests and removes integration_tests

IssueID: DCAEGEN2-99
Change-Id: I667e3cdeb7346b87b5e8a54dcfbf5058858f10a6
Signed-off-by: Tommy Carpenter <tommy@research.att.com>
bin/build_and_unit_test.sh
get_version.sh
rebar.config
src/cdapbroker.app.src
src/resource_handler.erl
src/resource_handler_tests.erl [new file with mode: 0644]
swagger/swagger.html
swagger/swagger.json
swagger/swagger.yaml
test/apitest/apitest_SUITE.erl

index 7a2c757..9c3e3a4 100755 (executable)
@@ -2,4 +2,5 @@
 rm -rf _build/;
 rebar3 upgrade;
 rebar3 release;
-rebar3 eunit
+rebar3 eunit --cover;
+rebar3 cover
index 3eba5a3..2624244 100755 (executable)
@@ -1,3 +1,3 @@
 #!/bin/sh
 #todo... build this so that it parses automatically
-echo "4.0.4
+echo "4.0.5
index e65730a..ae03f88 100644 (file)
@@ -1,6 +1,6 @@
 {relx, [
         {release,
-            {cdapbroker,"4.0.4"},
+            {cdapbroker,"4.0.5"},
             [cdapbroker]
         },
         %{extend_start_script,true},
index 157b141..b95d534 100644 (file)
@@ -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,
index a9703fc..8c1b343 100644 (file)
@@ -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 (file)
index 0000000..cab9558
--- /dev/null
@@ -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}).
+
index 0cce633..80b2176 100644 (file)
@@ -12,7 +12,7 @@
 <body>
 <div class="container">
     <h1>CDAP Broker API</h1>
-    <p class="sw-info">Version: <span class="sw-info-version">4.0.4</span></p>
+    <p class="sw-info">Version: <span class="sw-info-version">4.0.5</span></p>
     <p></p>
     
     
index fbfd932..f1b1480 100644 (file)
@@ -1,7 +1,7 @@
 {
     "swagger": "2.0",
     "info": {
-        "version": "4.0.4",
+        "version": "4.0.5",
         "title": "CDAP Broker API"
     },
     "paths": {
index ed8fd68..a7d14f8 100644 (file)
@@ -5,7 +5,7 @@ swagger: '2.0'
 
 # This is your document metadata
 info:
-  version: "4.0.4"
+  version: "4.0.5"
   title: CDAP Broker API
 
 paths:
index 971204c..1f7a3b2 100644 (file)
@@ -6,9 +6,9 @@
 % 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.
@@ -21,7 +21,7 @@
 -include_lib("common_test/include/ct.hrl").
 -include("../../src/application.hrl").
 -export([all/0, groups/0, init_per_suite/1, end_per_suite/1]).
--export([server_health_test/1, app_deploy/1, hydrator_deploy/1, app_teardown/1, app_test/1, app_reconfigure/1, test_failures/1, app_botch_flows/1, app_botch_delete/1, app_botch_consul_delete/1, invalid_reconfigure/1, delete_all/1,
+-export([server_health_test/1, app_deploy/1, hydrator_deploy/1, app_teardown/1, app_test/1, app_reconfigure/1, test_failures/1, app_botch_flows/1, app_botch_delete/1, app_botch_consul_delete/1, delete_all/1,
         hydrator_app_teardown/1, hydrator_test/1,
         hydrator_wdeps_deploy/1,
         hydrator_wdeps_test/1,
@@ -30,7 +30,7 @@
 
 %lazy shorthands (yay C style macros! miss these in python)
 -define(SC(L), util:concat(L)).
--define(PLG(K, PL), proplists:get_value(K, PL)). 
+-define(PLG(K, PL), proplists:get_value(K, PL)).
 -define(XER, "testing-XER").
 -define(D(X), erlang:display(X)).
 
@@ -40,7 +40,6 @@ all() -> [
           {group, apibotchedflows},
           {group, apibotcheddeleted},
           {group, apibotchedconsuldeleted},
-          {group, invalidreconfig},
           {group, apideleteall}
          ].
 groups() ->  [
@@ -91,15 +90,7 @@ groups() ->  [
                 app_botch_consul_delete,
                 app_teardown
               ]},
-              {invalidreconfig, %call reconfigure on an app that DNE
-              [],
-              [
-                server_health_test,
-                app_deploy,
-                invalid_reconfigure,
-                app_teardown
-              ]},
-              {apideleteall, 
+              {apideleteall,
               [],
               [
                 server_health_test,
@@ -119,9 +110,9 @@ setup_rels(Config, D) ->
     %  4 Broker binds config
     %  5 Broker pushes bound config to CDAP
     %  Between state 1 and 3 consul is in an inconsistent state where it has only the rels key but not the config key. Not so sure about this. They seem to be a pair. Maybe the rels key should be pushed to the source node to be dealt with.
-    % #Here, we are mocking step 1 
+    % #Here, we are mocking step 1
     URL = ?SC([?PLG(consul_url, Config), "/v1/kv/", ?PLG(appname, Config), ":rel"]),
-    case D of 
+    case D of
         setup -> {200,"true"} = httpabs:put(?XER, URL, "application/json", jiffy:encode([<<"666_fake_testing_service">>]));
         teardown -> {200,"true"} = httpabs:delete(?XER, URL)
     end,
@@ -131,7 +122,7 @@ setup_fake_testing_service(Config, D) ->
     %register a fake testing service to test that the CDAP app recieved it's bound configuration properly
     Name = <<"666_fake_testing_service">>,
     SrvURL = ?SC([?PLG(consul_url, Config), "/v1/catalog/service/", Name]),
-    case D of 
+    case D of
         setup ->
             URL = ?SC([?PLG(consul_url, Config), "/v1/agent/service/register"]),
             Body = {[{<<"name">>, Name},
@@ -147,10 +138,10 @@ setup_fake_testing_service(Config, D) ->
             httpabs:get(?XER, SrvURL)
     end.
 
-get_config_consul(C) -> 
+get_config_consul(C) ->
     %get config from consul. returns the code too for tests testing for a 404
-    {RC, RB} = consul_interface:consul_get_configuration(?XER, ?PLG(appname, C), ?PLG(consul_url, C)), 
-    case RC of 
+    {RC, RB} = consul_interface:consul_get_configuration(?XER, ?PLG(appname, C), ?PLG(consul_url, C)),
+    case RC of
         200 ->  {RC,  util:ejson_to_map(RB)};
         _   ->  {RC, RB}
     end.
@@ -158,7 +149,7 @@ get_config_consul(C) ->
 get_config_cdap(C) ->
     {RC, RB} = cdap_interface:get_app_config(?XER, ?PLG(appname, C), ?PLG(namespace, C),?PLG(cdap_url, C)),
     case RC of
-        200 ->  
+        200 ->
                 %I think CDAP is DOUBLY encoding JSON!!
                 {RC, jiffy:decode(jiffy:decode(jiffy:encode(RB)), [return_maps])};
         _   ->  {RC, RB}
@@ -171,10 +162,10 @@ get_preferences_cdap(C) ->
         _   ->  {RC, RB}
     end.
 
-get_preferences_consul(C) -> 
+get_preferences_consul(C) ->
     %get preferences from consul. returns the code too for tests testing for a 404
     {RC, RB} = consul_interface:consul_get_preferences(?XER, ?PLG(appname, C), ?PLG(consul_url, C)),
-    case RC of 
+    case RC of
         200 ->  {RC,  util:ejson_to_map(RB)};
         _   ->  {RC, RB}
     end.
@@ -189,7 +180,7 @@ valid_deploy_body(C) ->
          {<<"artifact_version">>, ?PLG(art_ver, C)},
          {<<"app_config">>,  ?PLG(init_config, C)},
          {<<"app_preferences">>, ?PLG(init_preferences, C)},
-         {<<"services">>, [{[{<<"service_name">>, <<"Greeting">>},  
+         {<<"services">>, [{[{<<"service_name">>, <<"Greeting">>},
                              {<<"service_endpoint">>, <<"greet">>},
                              {<<"endpoint_method">>, <<"GET">>}]}]},
          {<<"programs">>,  [
@@ -198,8 +189,8 @@ valid_deploy_body(C) ->
                             {[{<<"program_type">>, <<"services">>},
                               {<<"program_id">>, <<"Greeting">>}]}]},
          {<<"program_preferences">>, [
-                                      {[{<<"program_type">>,<<"flows">>}, 
-                                        {<<"program_id">>, <<"WhoFlow">>}, 
+                                      {[{<<"program_type">>,<<"flows">>},
+                                        {<<"program_id">>, <<"WhoFlow">>},
                                         {<<"program_pref">>, ?PLG(whoflowpref, C)}]}
                                       ]}
          ]}.
@@ -209,7 +200,7 @@ valid_deploy_body(C) ->
 init_per_suite(_C) ->
     %get platform ENVs
     [MyName, ConsulURL, _, _] = util:get_platform_envs_and_config(),
-    
+
     BrokerUrl = case os:getenv("BROKER_TEST_TYPE") of
         false -> %no env variable means start the broker on localhost
             %start a local broker
@@ -229,7 +220,7 @@ init_per_suite(_C) ->
 
     {200, RB} = httpabs:get(?XER, BrokerUrl),
     CDAPUrl = maps:get(<<"managed cdap url">>, jiffy:decode(RB, [return_maps])),
-    
+
     %set properties that are shared between program-flowlet and hydrator
     Namespace = <<"testns">>,
     CDAPUrlNS = ?SC([CDAPUrl, "/v3/namespaces/", Namespace]),
@@ -242,14 +233,14 @@ init_per_suite(_C) ->
     HydratorAppname = <<"hydratortest">>,
     HydratorAppURL = ?SC([CDAPUrlNS, "/apps/", HydratorAppname]),
     HydratorStreamname = <<"s1">>, %horrible name but not made by me
-    
+
     HydratorWDepsAppname = <<"hydratorwdepstest">>,
     HydratorWDepsAppURL = ?SC([CDAPUrlNS, "/apps/", HydratorWDepsAppname]),
     HydratorWDepsStreamname = <<"t1">>, %horrible name but not made by me
 
-    %Set up this test suites configuration 
-    [{broker_url, BrokerUrl}, 
-     {cdap_url,   CDAPUrl}, 
+    %Set up this test suites configuration
+    [{broker_url, BrokerUrl},
+     {cdap_url,   CDAPUrl},
      {cdap_ns_url, CDAPUrlNS},
      {jar_url, ?SC([Nexus, "/jar_files/HelloWorld-3.4.3.jar"])},
      {consul_url, ConsulURL},
@@ -314,7 +305,7 @@ server_health_test(C) ->
 
 app_deploy(C) -> %C == Config
     %Deploy the test application
-    
+
     %Deploy the rel key
     {200, _} = setup_rels(C, setup),
 
@@ -324,7 +315,7 @@ app_deploy(C) -> %C == Config
 
     ExpectedBoundConfg = maps:from_list([
                                {<<"services_calls">> ,  [<<"666.666.666.666:13">>]},
-                               {<<"streams_produces">>, [<<"666.666.666.666:13">>]}, 
+                               {<<"streams_produces">>, [<<"666.666.666.666:13">>]},
                                {<<"donotresolveme">> ,   <<"donotabsolveme">>}
                                ]),
 
@@ -346,13 +337,13 @@ app_deploy(C) -> %C == Config
     %assert the current appliccation list does not contain our test app
     {200,RB0} = httpabs:get(?XER, ?SC([?PLG(broker_url, C), "/application"])),
     true = lists:all(fun(X) -> X /=  ?PLG(appname, C) end, jiffy:decode(RB0)),
-    
+
     %deploy the app
     Body = valid_deploy_body(C),
     {200, RB} = httpabs:put(?XER, ?PLG(broker_app_url, C), "application/json", jiffy:encode(Body)),
-    
+
     %The CDAP APIs return the config as a JSON dumped to a string, so we need to get that back into a real JSON to have key-order-independent equality testing
-    Fix = fun(X) -> 
+    Fix = fun(X) ->
             RBMap = jiffy:decode(X, [return_maps]),
             maps:update(<<"bound_config">>, jiffy:decode(maps:get(<<"bound_config">>, RBMap), [return_maps]), RBMap)
         end,
@@ -370,10 +361,10 @@ app_deploy(C) -> %C == Config
 
     %make sure it is in CDAP
     {200, _} = httpabs:get(?XER, ?PLG(app_url, C)),
-    
+
     %check metrics
     {200, _} = httpabs:get(?XER, ?SC([?PLG(broker_app_url, C), "/metrics"])),
-    
+
     %check healthcheck
     {200, _} = httpabs:get(?XER,?SC([?PLG(broker_app_url, C), "/healthcheck"])),
 
@@ -383,7 +374,7 @@ app_deploy(C) -> %C == Config
 
     %check that the UNbound config is correct
     true = {200, util:ejson_to_map(?PLG(init_config, C))} == get_config_consul(C),
-    
+
     %check that the preferences in Consul is correct
     InitPrefMap = util:ejson_to_map(?PLG(init_preferences, C)),
     true = {200, InitPrefMap} == get_preferences_consul(C),
@@ -402,7 +393,7 @@ app_deploy(C) -> %C == Config
     true = ExpectedBoundConfg == CDAPConfig,
 
     %try to put the same app again and assert you get a 400
-    {400,"State: Bad Request. Return Body: Put recieved on /application/:appname but appname is already registered. Call /application/:appname/reconfigure if trying to reconfigure or delete first"} = 
+    {400,"State: Bad Request. Return Body: Put recieved on /application/:appname but appname is already registered. Call /application/:appname/reconfigure if trying to reconfigure or delete first"} =
         httpabs:put(?XER, ?PLG(broker_app_url, C), "application/json", jiffy:encode(Body)).
 
 hydrator_deploy(C) ->
@@ -426,7 +417,7 @@ hydrator_deploy(C) ->
     %assert the current appliccation list does not contain our test app
     {200,RB0} = httpabs:get(?XER, ?SC([?PLG(broker_url, C), "/application"])),
     true = lists:all(fun(X) -> X /=  ?PLG(hydrator_appname, C) end, jiffy:decode(RB0)),
-    
+
     %try the deploy
     {200, RB1} = httpabs:put(?XER, ?PLG(broker_hydrator_app_url, C), "application/json", jiffy:encode(Body)),
     true = jiffy:decode(RB1, [return_maps]) == Expected,
@@ -437,17 +428,17 @@ hydrator_deploy(C) ->
 
     %make sure it is in CDAP
     {200, _} = httpabs:get(?XER, ?PLG(hydrator_app_url, C)),
-     
+
     %assert the current application list now includes our new app
     {200, RB3} = httpabs:get(?XER, ?SC([?PLG(broker_url, C), "/application"])),
     true = lists:any(fun(X) -> X ==  ?PLG(hydrator_appname, C) end, jiffy:decode(RB3)),
-    
+
     %check healthcheck
     {200, _} = httpabs:get(?XER,?SC([?PLG(broker_hydrator_app_url, C), "/healthcheck"])),
-    
+
     %check metrics
     {200, _} = httpabs:get(?XER,?SC([?PLG(broker_hydrator_app_url, C), "/metrics"])),
-    
+
     %make sure that the service is registered. TODO! Could get more fancy by manually checking a healthcheck
     {200, RBHC} = httpabs:get(?XER,?PLG(consul_hydrator_app_url, C)),
     true = jiffy:decode(RBHC) /= []
@@ -486,7 +477,7 @@ hydrator_wdeps_deploy(C) ->
     %try the deploy
     {200, RB1} = httpabs:put(?XER, ?PLG(broker_hydrator_wdeps_app_url, C), "application/json", jiffy:encode(Body)),
     true = jiffy:decode(RB1, [return_maps]) == Expected,
-    
+
     %make sure properties are loaded, test artifact
     {200, _} = httpabs:get(?XER,?SC([?PLG(cdap_ns_url, C), "/artifacts/", ?PLG(hydrator_wdeps_artname, C), "/versions/", ?PLG(hydrator_wdeps_artver, C), "/properties"])),
 
@@ -496,22 +487,22 @@ hydrator_wdeps_deploy(C) ->
 
     %make sure it is in CDAP
     {200, _} = httpabs:get(?XER,?PLG(hydrator_wdeps_app_url, C)),
-     
+
     %assert the current application list now includes our new app
     {200, RB3} = httpabs:get(?XER,?SC([?PLG(broker_url, C), "/application"])),
     true = lists:any(fun(X) -> X ==  ?PLG(hydrator_wdeps_appname, C) end, jiffy:decode(RB3)),
-    
+
     %check healthcheck
     {200, _} = httpabs:get(?XER,?SC([?PLG(broker_hydrator_wdeps_app_url, C), "/healthcheck"])),
-    
+
     %check metrics
     {200, _} = httpabs:get(?XER,?SC([?PLG(broker_hydrator_wdeps_app_url, C), "/metrics"])),
-    
+
     %make sure that the service is registered. TODO! Could get more fancy by manually checking a healthcheck
     {200, RBHC} = httpabs:get(?XER,?PLG(consul_hydrator_wdeps_app_url, C)),
     true = jiffy:decode(RBHC) /= []
     .
-    
+
 hydrator_test(C) ->
     %test te app by injecting some data into the stream and getting it out
         %Sleeping since HTTP services may still be booting up: see https://issues.cask.co/browse/CDAP-812
@@ -554,7 +545,7 @@ app_reconfigure(C) ->
     true = {200, ReconfigMap} == get_config_consul(C),
     %test new config right in cdap
     true = {200, ReconfigMap} == get_config_cdap(C),
+
     %Test preferences reconfiguration
     %check that the preferences in Consul is correct
     InitMap = util:ejson_to_map(?PLG(init_preferences, C)),
@@ -594,8 +585,8 @@ app_reconfigure(C) ->
     true = {200, ExpectedNewPreferencesBoth} == get_preferences_cdap(C),
     true = {200, ExpectedNewConfigBoth} == get_config_consul(C),
     true = {200, ExpectedNewConfigBoth} == get_config_cdap(C),
-    
-    %try to give it a smart where there are no overlaps 
+
+    %try to give it a smart where there are no overlaps
     SmartReconfigNone = {[{<<"EMPTY">>, <<"LIKE YOUR SOUL">>}]},
     {400, _} = httpabs:put(?XER, ?SC([?PLG(broker_app_url, C), "/reconfigure"]), "application/json", jiffy:encode({[{<<"reconfiguration_type">>, <<"program-flowlet-smart">>},{<<"config">>, SmartReconfigNone}]})),
     true = {200, ExpectedNewPreferencesBoth} == get_preferences_consul(C),
@@ -609,18 +600,18 @@ app_botch_flows(C) ->
     {200, _} = httpabs:get(?XER,?SC([?PLG(broker_app_url, C), "/healthcheck"])),
 
     %purposely shut down a flow "manually" to test that undeploy works with a "partially deployed" app
-    {200, []} = cdap_interface:exec_programs(?XER, ?PLG(appname, C), ?PLG(namespace, C), ?PLG(cdap_url, C), 
+    {200, []} = cdap_interface:exec_programs(?XER, ?PLG(appname, C), ?PLG(namespace, C), ?PLG(cdap_url, C),
                                                 [#program{type = <<"flows">>, id = <<"WhoFlow">>}, #program{type = <<"services">>, id = <<"Greeting">>}], "stop"),
     %make sure healthcheck now fails
     {400, _} = httpabs:get(?XER,?SC([?PLG(broker_app_url, C), "/healthcheck"]))
     .
 
 app_botch_delete(C) ->
-    %purposely shut down flows and then delete the app from the CDAP api to test undeploy works with a [gone] app 
-    {200, []} = cdap_interface:exec_programs(?XER, ?PLG(appname, C), ?PLG(namespace, C), ?PLG(cdap_url, C), 
+    %purposely shut down flows and then delete the app from the CDAP api to test undeploy works with a [gone] app
+    {200, []} = cdap_interface:exec_programs(?XER, ?PLG(appname, C), ?PLG(namespace, C), ?PLG(cdap_url, C),
                                                 [#program{type = <<"flows">>, id = <<"WhoFlow">>}, #program{type = <<"services">>, id = <<"Greeting">>}], "stop"),
     {200, []} = cdap_interface:delete_app(?XER, ?PLG(appname, C), ?PLG(namespace, C), ?PLG(cdap_url, C)),
-    
+
     %make sure healthcheck now fails
     {400, _} = httpabs:get(?XER,?SC([?PLG(broker_app_url, C), "/healthcheck"]))
     .
@@ -636,7 +627,7 @@ app_teardown(C) ->
 
     %teardown the test application
     {200, []} = httpabs:delete(?XER, ?PLG(broker_app_url, C)),
-    
+
     %make sure the broker deleted the config from Consul
     {404, _} = get_config_consul(C),
 
@@ -645,17 +636,17 @@ app_teardown(C) ->
 
     %make sure the broker app url no longer exists
     {404, _ } = httpabs:get(?XER,?PLG(broker_app_url, C)),
-    
+
     %teardown the testing rels
     {404, _} = setup_rels(C, teardown),
-    
+
     %teardown the fake service and make sure it is gone
     {200, Srv} = setup_fake_testing_service(C, teardown),
     true = Srv == "[]",
-    
+
     %cdap app gone
     {404,"State: Not Found. Return Body: 'application:testns.hwtest.-SNAPSHOT' was not found."} = httpabs:get(?XER,?PLG(app_url, C)),
-    
+
     %make sure that the service is not registered. TODO! Could get more fancy by manually checking a healthcheck
     {200, RBHC} = httpabs:get(?XER,?PLG(consul_app_url, C)),
     true = jiffy:decode(RBHC) == [].
@@ -727,9 +718,9 @@ hydrator_wdeps_teardown(C) ->
 test_failures(C) ->
     %test things that should fail
     %delete a non-existent app
-    {404, "State: Not Found. Return Body: Tried to delete an application that was not registered"} = 
+    {404, "State: Not Found. Return Body: Tried to delete an application that was not registered"} =
         httpabs:delete(?XER, ?SC([?PLG(broker_app_url, C), "MYFRIENDOFMISERY"])),
-    
+
     %malformed Broker put
     URL = ?SC([?PLG(broker_app_url, C), "FAILURETEST"]),
     Body = {[
@@ -747,7 +738,7 @@ test_failures(C) ->
       {<<"artifact_version">>, ?PLG(art_ver, C)},
       {<<"app_config">>,  ?PLG(init_config, C)},
       {<<"app_preferences">>, ?PLG(init_preferences, C)},
-      {<<"services">>, [{[{<<"service_name">>, <<"Greeting">>},  
+      {<<"services">>, [{[{<<"service_name">>, <<"Greeting">>},
                           {<<"service_endpoint">>, <<"greet">>},
                           {<<"endpoint_method">>, <<"GET">>}]}]},
       {<<"programs">>,  [
@@ -762,7 +753,7 @@ test_failures(C) ->
     {404,_} = httpabs:put(?XER, URL, "application/json", jiffy:encode(Body2)),
     %make sure the rollback happened
     {200, "[]"} = httpabs:get(?XER,?SC([?PLG(broker_url, C), "/application"])),
-    
+
     %try to deploy with a bad URL where bad means nonexistent (504)
     Body3 = {[
          {<<"cdap_application_type">>, <<"program-flowlet">>},
@@ -796,20 +787,6 @@ test_failures(C) ->
          {400,"State: Bad Request. Return Body: ERROR: The following URL is malformed: THIS IS NOT EVEN A URL WHAT ARE YOU DOING TO ME"} = httpabs:put(?XER, URL, "application/json", jiffy:encode(Body4))
     .
 
-invalid_reconfigure(C) ->
-    %test reconfiguring an app that does not exist despite put body being correct
-    {404,"State: Not Found. Return Body: Reconfigure recieved but the app is not registered"} = httpabs:put(?XER, ?SC([?PLG(broker_app_url, C), "THE_VOID", "/reconfigure"]), "application/json", jiffy:encode({[{<<"reconfiguration_type">>, <<"program-flowlet-app-config>">>}, {<<"config">>, {[{<<"foo">>, <<"bar">>}]}}]})),
-
-     %test reconfiguring with an invalid PUT body (missing "reconfiguration_type")
-    {400,"State: Bad Request. Return Body: Invalid PUT Reconfigure Body: key 'reconfiguration_type' is missing"} = httpabs:put(?XER, ?SC([?PLG(broker_app_url,C),  "/reconfigure"]), "application/json", jiffy:encode({[{<<"config">>, <<"bar">>}]})),
-
-     %test reconfiguring with an invalid PUT body (missing app_config)
-    {400,"State: Bad Request. Return Body: Invalid PUT Reconfigure Body: key 'config' is missing"} = httpabs:put(?XER, ?SC([?PLG(broker_app_url,C),  "/reconfigure"]), "application/json", jiffy:encode({[{<<"reconfiguration_type">>, <<"program-flowlet-app-config">>}, {<<"foo">>, <<"bar">>}]})),
-
-    %test reconfiguring an invalid (unimplemented) type
-    {501, "State: Not Implemented. Return Body: This type (EMPTINESS) of reconfiguration is not implemented"} = httpabs:put(?XER, ?SC([?PLG(broker_app_url,C),  "/reconfigure"]), "application/json", jiffy:encode({[{<<"config">>, <<"bar">>}, {<<"reconfiguration_type">>, <<"EMPTINESS">>}]}))
-    .
-
 delete_all(C) ->
     %test invalid key
     Body1 = jiffy:encode({[{<<"ids">>, [<<"hwtest">>]}]}),
@@ -823,4 +800,4 @@ delete_all(C) ->
     %teardown the fake service and make sure it is gone
     {200, Srv} = setup_fake_testing_service(C, teardown),
     true = Srv == "[]".
-    
+