YARN-9281. Add express upgrade button to Appcatalog UI. Contributed by Eric Yang

This commit is contained in:
Billie Rinaldi 2019-04-13 18:55:11 +03:00
parent ebbda181e4
commit b2cdf809bc
13 changed files with 336 additions and 24 deletions

View File

@ -487,5 +487,4 @@
</build> </build>
</profile> </profile>
</profiles> </profiles>
</project> </project>

View File

@ -291,13 +291,12 @@ public class AppCatalogSolrClient {
docs.add(request); docs.add(request);
} }
// Commit Solr changes. try {
UpdateResponse detailsResponse = solr.add(docs); commitSolrChanges(solr, docs);
if (detailsResponse.getStatus() != 0) { } catch (IOException e) {
throw new IOException("Unable to register docker instance " throw new IOException("Unable to register docker instance "
+ "with application entry."); + "with application entry.", e);
} }
solr.commit();
} }
private SolrInputDocument incrementDownload(SolrDocument doc, private SolrInputDocument incrementDownload(SolrDocument doc,
@ -350,16 +349,10 @@ public class AppCatalogSolrClient {
buffer.setField("yarnfile_s", mapper.writeValueAsString(yarnApp)); buffer.setField("yarnfile_s", mapper.writeValueAsString(yarnApp));
docs.add(buffer); docs.add(buffer);
// Commit Solr changes. commitSolrChanges(solr, docs);
UpdateResponse detailsResponse = solr.add(docs);
if (detailsResponse.getStatus() != 0) {
throw new IOException("Unable to register application " +
"in Application Store.");
}
solr.commit();
} catch (SolrServerException | IOException e) { } catch (SolrServerException | IOException e) {
throw new IOException("Unable to register application " + throw new IOException("Unable to register application " +
"in Application Store. "+ e.getMessage()); "in Application Store. ", e);
} }
} }
@ -389,16 +382,64 @@ public class AppCatalogSolrClient {
buffer.setField("yarnfile_s", mapper.writeValueAsString(yarnApp)); buffer.setField("yarnfile_s", mapper.writeValueAsString(yarnApp));
docs.add(buffer); docs.add(buffer);
// Commit Solr changes. commitSolrChanges(solr, docs);
UpdateResponse detailsResponse = solr.add(docs);
if (detailsResponse.getStatus() != 0) {
throw new IOException("Unable to register application " +
"in Application Store.");
}
solr.commit();
} catch (SolrServerException | IOException e) { } catch (SolrServerException | IOException e) {
throw new IOException("Unable to register application " + throw new IOException("Unable to register application " +
"in Application Store. "+ e.getMessage()); "in Application Store. ", e);
} }
} }
public void upgradeApp(Service service) throws IOException,
SolrServerException {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Collection<SolrInputDocument> docs = new HashSet<SolrInputDocument>();
SolrClient solr = getSolrClient();
if (service!=null) {
String name = service.getName();
String app = "";
SolrQuery query = new SolrQuery();
query.setQuery("id:" + name);
query.setFilterQueries("type_s:AppEntry");
query.setRows(1);
QueryResponse response;
try {
response = solr.query(query);
Iterator<SolrDocument> appList = response.getResults().listIterator();
while (appList.hasNext()) {
SolrDocument d = appList.next();
app = d.get("app_s").toString();
}
} catch (SolrServerException | IOException e) {
LOG.error("Error in finding deployed application: " + name, e);
}
// Register deployed application instance with AppList
SolrInputDocument request = new SolrInputDocument();
request.addField("type_s", "AppEntry");
request.addField("id", name);
request.addField("name_s", name);
request.addField("app_s", app);
request.addField("yarnfile_s", mapper.writeValueAsString(service));
docs.add(request);
}
try {
commitSolrChanges(solr, docs);
} catch (IOException e) {
throw new IOException("Unable to register docker instance "
+ "with application entry.", e);
}
}
private void commitSolrChanges(SolrClient solr,
Collection<SolrInputDocument> docs)
throws IOException, SolrServerException {
// Commit Solr changes.
UpdateResponse detailsResponse = solr.add(docs);
if (detailsResponse.getStatus() != 0) {
throw new IOException("Failed to commit document in solr, status code: "
+ detailsResponse.getStatus());
}
solr.commit();
}
} }

View File

@ -27,6 +27,7 @@ import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.yarn.appcatalog.model.AppEntry; import org.apache.hadoop.yarn.appcatalog.model.AppEntry;
import org.apache.hadoop.yarn.service.api.records.Service; import org.apache.hadoop.yarn.service.api.records.Service;
import org.apache.hadoop.yarn.service.api.records.ServiceState;
import org.apache.hadoop.yarn.service.api.records.KerberosPrincipal; import org.apache.hadoop.yarn.service.api.records.KerberosPrincipal;
import org.apache.hadoop.yarn.service.client.ApiServiceClient; import org.apache.hadoop.yarn.service.client.ApiServiceClient;
@ -171,4 +172,25 @@ public class YarnServiceClient {
LOG.error("Error in fetching application status: ", e); LOG.error("Error in fetching application status: ", e);
} }
} }
public void upgradeApp(Service app) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
String appInstanceId = app.getName();
app.setState(ServiceState.EXPRESS_UPGRADING);
String yarnFile = mapper.writeValueAsString(app);
ClientResponse response;
try {
response = asc.getApiClient(asc.getServicePath(appInstanceId))
.put(ClientResponse.class, yarnFile);
if (response.getStatus() >= 299) {
String message = response.getEntity(String.class);
throw new RuntimeException("Failed : HTTP error code : "
+ response.getStatus() + " error: " + message);
}
} catch (UniformInterfaceException | ClientHandlerException
| IOException e) {
LOG.error("Error in stopping application: ", e);
}
}
} }

View File

@ -18,8 +18,12 @@
package org.apache.hadoop.yarn.appcatalog.controller; package org.apache.hadoop.yarn.appcatalog.controller;
import java.io.IOException;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
@ -32,6 +36,7 @@ import org.apache.hadoop.yarn.appcatalog.application.YarnServiceClient;
import org.apache.hadoop.yarn.appcatalog.model.AppEntry; import org.apache.hadoop.yarn.appcatalog.model.AppEntry;
import org.apache.hadoop.yarn.service.api.records.Service; import org.apache.hadoop.yarn.service.api.records.Service;
import org.apache.hadoop.yarn.service.api.records.ServiceState; import org.apache.hadoop.yarn.service.api.records.ServiceState;
import org.apache.solr.client.solrj.SolrServerException;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
@ -262,4 +267,33 @@ public class AppDetailsController {
} }
return Response.ok().build(); return Response.ok().build();
} }
/**
* Upgrade an application.
*
* @apiGroup AppDetailController
* @apiName upgradeApp
* @api {put} /app_details/upgrade/{id} Upgrade one instance of application.
* @apiParam {String} id Application Name to upgrade.
* @apiSuccess {String} text
* @apiError BadRequest Requested application does not upgrade.
* @return Web response code
*/
@Path("upgrade/{id}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response upgradeApp(@PathParam("id") String id, Service app) {
try {
AppCatalogSolrClient sc = new AppCatalogSolrClient();
sc.upgradeApp(app);
YarnServiceClient yc = new YarnServiceClient();
yc.upgradeApp(app);
} catch (IOException | SolrServerException e) {
return Response.status(Status.BAD_REQUEST).entity(e.toString()).build();
}
String output = "{\"status\":\"Application upgrade requested.\",\"id\":\"" +
app.getName() + "\"}";
return Response.status(Status.ACCEPTED).entity(output).build();
}
} }

View File

@ -53,6 +53,9 @@ app.config(['$routeProvider',
}).when('/deploy/:id', { }).when('/deploy/:id', {
templateUrl: 'partials/deploy.html', templateUrl: 'partials/deploy.html',
controller: 'DeployAppController' controller: 'DeployAppController'
}).when('/upgrade/:id', {
templateUrl: 'partials/upgrade.html',
controller: 'UpgradeAppController'
}).otherwise({ }).otherwise({
redirectTo: '/' redirectTo: '/'
}); });

View File

@ -127,6 +127,10 @@ controllers.controller("AppDetailsController", [ '$scope', '$interval', '$rootSc
}, errorCallback); }, errorCallback);
} }
$scope.upgradeApp = function(id) {
window.location = '/#!/upgrade/' + id;
}
$scope.canDeployApp = function() { $scope.canDeployApp = function() {
return true; return true;
}; };
@ -306,6 +310,56 @@ controllers.controller("DeployAppController", [ '$scope', '$rootScope', '$http',
}]); }]);
controllers.controller("UpgradeAppController", [ '$scope', '$rootScope', '$http',
'$routeParams', function($scope, $rootScope, $http, $routeParams) {
$scope.message = null;
$scope.error = null;
$scope.appName = $routeParams.id;
$scope.refreshAppDetails = function() {
$http({
method : 'GET',
url : '/v1/app_details/status/' + $scope.appName
}).then(successCallback, errorCallback);
}
$scope.upgradeApp = function(app) {
$rootScope.$emit("showLoadScreen", {});
$http({
method : 'PUT',
url : '/v1/app_details/upgrade/' + $scope.appName,
data : JSON.stringify($scope.details)
}).then(function(data, status, headers, config) {
$rootScope.$emit("RefreshAppList", {});
window.location = '/#!/app/' + data.data.id;
}, function(data, status, headers, config) {
$rootScope.$emit("hideLoadScreen", {});
$scope.error = data.data;
$('#error-message').html(data.data);
$('#myModal').modal('show');
console.log('error', data, status);
});
}
function successCallback(response) {
if (response.data.yarnfile.components.length!=0) {
$scope.details = response.data.yarnfile;
} else {
// When application is in accepted or failed state, it does not
// have components detail, hence we update states only.
$scope.details.state = response.data.yarnfile.state;
}
}
function errorCallback(response) {
$rootScope.$emit("hideLoadScreen", {});
$scope.error = "Error in getting application detail.";
$('#error-message').html($scope.error);
$('#myModal').modal('show');
}
$scope.refreshAppDetails();
}]);
controllers.controller("LoadScreenController", [ '$scope', '$rootScope', '$http', function($scope, $rootScope, $http) { controllers.controller("LoadScreenController", [ '$scope', '$rootScope', '$http', function($scope, $rootScope, $http) {
$scope.loadScreen = "hide"; $scope.loadScreen = "hide";

View File

@ -184,6 +184,11 @@
background-color: #FFF; background-color: #FFF;
box-shadow: 0 0 2px 0 #1391c1; box-shadow: 0 0 2px 0 #1391c1;
} }
.btn-secondary:visited {
color: #429929;
background-color: #FFF;
box-shadow: 0 0 2px 0 #1391c1;
}
.btn-secondary[disabled], .btn-secondary[disabled],
.btn-secondary:focus[disabled], .btn-secondary:focus[disabled],
.btn-secondary.disabled, .btn-secondary.disabled,

View File

@ -14,8 +14,9 @@
<div class="container content"> <div class="container content">
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-12 col-lg-12"> <div class="col-xs-12 col-md-12 col-lg-12">
<a ng-click="restartApp(appName)" class="btn btn-secondary"><span class="glyphicon glyphicon-play"></span> Start</a>
<a ng-click="stopApp(appName)" class="btn btn-secondary"><span class="glyphicon glyphicon-stop"></span> Stop</a> <a ng-click="stopApp(appName)" class="btn btn-secondary"><span class="glyphicon glyphicon-stop"></span> Stop</a>
<a ng-click="restartApp(appName)" class="btn btn-secondary"><span class="glyphicon glyphicon-refresh"></span> Start</a> <a ng-click="upgradeApp(appName)" class="btn btn-secondary"><span class="glyphicon glyphicon-circle-arrow-up"></span> Upgrade</a>
<div style="display:inline-block;" ng-repeat="(key, value) in details.yarnfile.quicklinks"> <div style="display:inline-block;" ng-repeat="(key, value) in details.yarnfile.quicklinks">
<a href="{{value}}" class="btn btn-secondary" ng-hide="checkServiceLink()"><span class="glyphicon glyphicon-new-window"></span> {{key}}</a> <a href="{{value}}" class="btn btn-secondary" ng-hide="checkServiceLink()"><span class="glyphicon glyphicon-new-window"></span> {{key}}</a>
</div> </div>

View File

@ -0,0 +1,114 @@
<!---
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. See accompanying LICENSE file.
-->
<div class="container content">
<div class="row">
<div class="col-xs-12 col-md-12 col-lg-12">
<div class="form-group">
<h1>Upgrade Application</h1>
</div>
<div class="form-group">
<label>Application Name</label>
<input type=text name="name" class="form-control" ng-model="details.name" readonly />
</div>
<div class="form-group">
<label>Version</label>
<input type=text name="version" class="form-control" ng-model="details.version" />
</div>
<div class="form-group">
<label>Quick Link</label>
<textarea json-text name="quicklink" class="form-control" ng-model="details.quicklinks"/></textarea>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-md-12 col-lg-12" ng-repeat="docker in details.components track by $index">
<div class="panel">
<div class="form-group">
<label>Component Name</label>
<input type=text name="name" class="form-control" ng-model="docker.name" readonly />
</div>
<div class="form-group">
<label>Artifact</label>
<input type=text name="artifact_id" class="form-control" ng-model="docker.artifact.id" />
</div>
<div class="form-group">
<label>Number of containers</label>
<input type=text name="artifact_id" class="form-control" ng-model="docker.number_of_containers" readonly />
</div>
<div class="form-group">
<label>Launch Command</label>
<input type=text name="launch_command" class="form-control" ng-model="docker.launch_command" />
</div>
<div class="form-group">
<label>CPU</label>
<input type=text name="cpus" class="form-control" ng-model="docker.resource.cpus" readonly />
</div>
<div class="form-group">
<label>Memory</label>
<input type=text name="memory" class="form-control" ng-model="docker.resource.memory" readonly />
</div>
<div class="form-group">
<input type="checkbox" ng-attr-id="{{'checkbox-priv-' + $index}}" ng-model="docker.run_privileged_container">
<label for="checkbox-priv-{{$index}}"> Privileged Container</label>
</div>
<div class="form-group">
<label>Dependencies</label>
<input json-text type=text name="dependencies" class="form-control" ng-model="docker.dependencies" />
</div>
<div class="form-group">
<label>Placement Policy</label>
<input type=text name="placement" class="form-control" ng-model="docker.placement_policy.constraints" readonly />
</div>
<div class="form-group">
<label>Environments</label>
<textarea json-text name="env" class="form-control" ng-model="docker.configuration.env"/></textarea>
</div>
<div class="form-group">
<label>Properties</label>
<textarea json-text name="properties" class="form-control" ng-model="docker.configuration.properties"/></textarea>
</div>
</div>
</div>
</div>
<br>
<div class="row">
<div class="col-xs-12 col-md-12 col-lg-12">
<p>
<a class="btn btn-secondary" ng-click="upgradeApp(details)">Upgrade</a>
<a class="btn btn-secondary" href="/#!/app/{{details.name}}">CANCEL</a>
</div>
</div>
<div class="modal fade" id="myModal" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title" ng-if="message">Info</h4>
<h4 class="modal-title" ng-if="error">Error</h4>
</div>
<div class="modal-body">
<p class="infobox bg-info" ng-if="message">{{message}}</p>
<div id="error-message"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -29,7 +29,7 @@
<!-- Custom styles for this template --> <!-- Custom styles for this template -->
<link href="css/theme.css" rel="stylesheet"> <link href="css/theme.css" rel="stylesheet">
<link href="css/bootstrap-hadoop.min.css" rel="stylesheet"> <link href="css/bootstrap-hadoop.css" rel="stylesheet">
</head> </head>

View File

@ -18,6 +18,7 @@
package org.apache.hadoop.yarn.appcatalog.application; package org.apache.hadoop.yarn.appcatalog.application;
import org.apache.hadoop.yarn.appcatalog.model.AppEntry;
import org.apache.hadoop.yarn.appcatalog.model.AppStoreEntry; import org.apache.hadoop.yarn.appcatalog.model.AppStoreEntry;
import org.apache.hadoop.yarn.appcatalog.model.Application; import org.apache.hadoop.yarn.appcatalog.model.Application;
import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrClient;
@ -127,4 +128,22 @@ public class TestAppCatalogSolrClient {
} }
} }
@Test
public void testUpgradeApp() throws Exception {
Application example = new Application();
String expected = "2.0";
String actual = "";
example.setOrganization("jenkins-ci.org");
example.setVersion("1.0");
example.setName("jenkins");
example.setDescription("World leading open source automation system.");
example.setIcon("/css/img/feather.png");
spy.register(example);
spy.deployApp("test", example);
example.setVersion("2.0");
spy.upgradeApp(example);
List<AppEntry> appEntries = spy.listAppEntries();
actual = appEntries.get(appEntries.size() -1).getYarnfile().getVersion();
assertEquals(expected, actual);
}
} }

View File

@ -135,4 +135,23 @@ public class AppDetailsControllerTest {
is("/app_details")); is("/app_details"));
} }
@Test
public void testUpgradeApp() throws Exception {
String id = "application1";
AppDetailsController ac = Mockito.mock(AppDetailsController.class);
Service yarnfile = new Service();
yarnfile.setVersion("1.0");
Component comp = new Component();
Container c = new Container();
c.setId("container-1");
List<Container> containers = new ArrayList<Container>();
containers.add(c);
comp.setContainers(containers);
yarnfile.addComponent(comp);
Response expected = Response.ok().build();
when(ac.upgradeApp(id, yarnfile)).thenReturn(Response.ok().build());
final Response actual = ac.upgradeApp(id, yarnfile);
assertEquals(expected.getStatus(), actual.getStatus());
}
} }

View File

@ -23,6 +23,7 @@
<field name="type_s" type="string" indexed="true" stored="true" required="false" multiValued="false" docValues="true" /> <field name="type_s" type="string" indexed="true" stored="true" required="false" multiValued="false" docValues="true" />
<field name="org_s" type="string" indexed="true" stored="true" required="false" multiValued="false" docValues="true" /> <field name="org_s" type="string" indexed="true" stored="true" required="false" multiValued="false" docValues="true" />
<field name="name_s" type="string" indexed="true" stored="true" required="false" multiValued="false" docValues="true" /> <field name="name_s" type="string" indexed="true" stored="true" required="false" multiValued="false" docValues="true" />
<field name="app_s" type="string" indexed="true" stored="true" required="false" multiValued="false" docValues="true" />
<field name="desc_s" type="string" indexed="true" stored="true" required="false" multiValued="false" docValues="true" /> <field name="desc_s" type="string" indexed="true" stored="true" required="false" multiValued="false" docValues="true" />
<field name="icon_s" type="string" indexed="true" stored="true" required="false" multiValued="false" docValues="true" /> <field name="icon_s" type="string" indexed="true" stored="true" required="false" multiValued="false" docValues="true" />
<field name="yarnfile_s" type="string" indexed="true" stored="true" required="false" multiValued="false" docValues="true" /> <field name="yarnfile_s" type="string" indexed="true" stored="true" required="false" multiValued="false" docValues="true" />