YARN-9281. Add express upgrade button to Appcatalog UI. Contributed by Eric Yang
This commit is contained in:
parent
ebbda181e4
commit
b2cdf809bc
|
@ -487,5 +487,4 @@
|
|||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -291,13 +291,12 @@ public class AppCatalogSolrClient {
|
|||
docs.add(request);
|
||||
}
|
||||
|
||||
// Commit Solr changes.
|
||||
UpdateResponse detailsResponse = solr.add(docs);
|
||||
if (detailsResponse.getStatus() != 0) {
|
||||
try {
|
||||
commitSolrChanges(solr, docs);
|
||||
} catch (IOException e) {
|
||||
throw new IOException("Unable to register docker instance "
|
||||
+ "with application entry.");
|
||||
+ "with application entry.", e);
|
||||
}
|
||||
solr.commit();
|
||||
}
|
||||
|
||||
private SolrInputDocument incrementDownload(SolrDocument doc,
|
||||
|
@ -350,16 +349,10 @@ public class AppCatalogSolrClient {
|
|||
buffer.setField("yarnfile_s", mapper.writeValueAsString(yarnApp));
|
||||
|
||||
docs.add(buffer);
|
||||
// Commit Solr changes.
|
||||
UpdateResponse detailsResponse = solr.add(docs);
|
||||
if (detailsResponse.getStatus() != 0) {
|
||||
throw new IOException("Unable to register application " +
|
||||
"in Application Store.");
|
||||
}
|
||||
solr.commit();
|
||||
commitSolrChanges(solr, docs);
|
||||
} catch (SolrServerException | IOException e) {
|
||||
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));
|
||||
|
||||
docs.add(buffer);
|
||||
commitSolrChanges(solr, docs);
|
||||
} catch (SolrServerException | IOException e) {
|
||||
throw new IOException("Unable to register application " +
|
||||
"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("Unable to register application " +
|
||||
"in Application Store.");
|
||||
throw new IOException("Failed to commit document in solr, status code: "
|
||||
+ detailsResponse.getStatus());
|
||||
}
|
||||
solr.commit();
|
||||
} catch (SolrServerException | IOException e) {
|
||||
throw new IOException("Unable to register application " +
|
||||
"in Application Store. "+ e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.apache.hadoop.conf.Configuration;
|
|||
import org.apache.hadoop.security.UserGroupInformation;
|
||||
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.ServiceState;
|
||||
import org.apache.hadoop.yarn.service.api.records.KerberosPrincipal;
|
||||
import org.apache.hadoop.yarn.service.client.ApiServiceClient;
|
||||
|
||||
|
@ -171,4 +172,25 @@ public class YarnServiceClient {
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,12 @@
|
|||
|
||||
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.POST;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
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.service.api.records.Service;
|
||||
import org.apache.hadoop.yarn.service.api.records.ServiceState;
|
||||
import org.apache.solr.client.solrj.SolrServerException;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
|
||||
|
@ -262,4 +267,33 @@ public class AppDetailsController {
|
|||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,9 @@ app.config(['$routeProvider',
|
|||
}).when('/deploy/:id', {
|
||||
templateUrl: 'partials/deploy.html',
|
||||
controller: 'DeployAppController'
|
||||
}).when('/upgrade/:id', {
|
||||
templateUrl: 'partials/upgrade.html',
|
||||
controller: 'UpgradeAppController'
|
||||
}).otherwise({
|
||||
redirectTo: '/'
|
||||
});
|
||||
|
|
|
@ -127,6 +127,10 @@ controllers.controller("AppDetailsController", [ '$scope', '$interval', '$rootSc
|
|||
}, errorCallback);
|
||||
}
|
||||
|
||||
$scope.upgradeApp = function(id) {
|
||||
window.location = '/#!/upgrade/' + id;
|
||||
}
|
||||
|
||||
$scope.canDeployApp = function() {
|
||||
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) {
|
||||
$scope.loadScreen = "hide";
|
||||
|
||||
|
|
|
@ -184,6 +184,11 @@
|
|||
background-color: #FFF;
|
||||
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:focus[disabled],
|
||||
.btn-secondary.disabled,
|
||||
|
|
|
@ -14,8 +14,9 @@
|
|||
<div class="container content">
|
||||
<div class="row">
|
||||
<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="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">
|
||||
<a href="{{value}}" class="btn btn-secondary" ng-hide="checkServiceLink()"><span class="glyphicon glyphicon-new-window"></span> {{key}}</a>
|
||||
</div>
|
||||
|
|
|
@ -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">×</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>
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<!-- Custom styles for this template -->
|
||||
<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>
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
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.Application;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,4 +135,23 @@ public class AppDetailsControllerTest {
|
|||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
<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="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="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" />
|
||||
|
|
Loading…
Reference in New Issue