1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com
.google
.appengine
.tools
.admin
;
5 import com
.google
.appengine
.tools
.admin
.AppAdminFactory
.ApplicationProcessingOptions
;
6 import com
.google
.appengine
.tools
.admin
.AppAdminFactory
.ConnectOptions
;
7 import com
.google
.apphosting
.utils
.config
.BackendsXml
;
8 import com
.google
.apphosting
.utils
.config
.BackendsYamlReader
;
9 import com
.google
.apphosting
.utils
.config
.CronXml
;
11 import java
.io
.BufferedReader
;
13 import java
.io
.FileOutputStream
;
14 import java
.io
.FileReader
;
15 import java
.io
.IOException
;
16 import java
.io
.PrintWriter
;
17 import java
.io
.Reader
;
18 import java
.io
.StringWriter
;
19 import java
.lang
.reflect
.Constructor
;
20 import java
.nio
.file
.DirectoryStream
;
21 import java
.nio
.file
.Path
;
22 import java
.util
.ArrayList
;
23 import java
.util
.Collections
;
24 import java
.util
.List
;
27 * Our implementation of the AppAdmin interface.
30 public class AppAdminImpl
implements AppAdmin
{
32 private final ConnectOptions options
;
33 private final GenericApplication app
;
34 private final PrintWriter errorWriter
;
35 private final ApplicationProcessingOptions appOptions
;
36 private final Class
<?
extends AppVersionUpload
> appVersionUploadClass
;
37 private final UpdateOptions updateOptions
;
39 protected AppAdminImpl(ConnectOptions options
, GenericApplication app
, PrintWriter errorWriter
,
40 ApplicationProcessingOptions appOptions
,
41 Class
<?
extends AppVersionUpload
> appVersionUploadClass
) {
42 this.options
= options
;
44 this.errorWriter
= errorWriter
;
45 this.appOptions
= appOptions
;
46 this.appVersionUploadClass
= appVersionUploadClass
;
47 this.updateOptions
= new UpdateOptions();
50 protected ServerConnection
getServerConnection(ConnectOptions options
) {
51 return ServerConnectionFactory
.getServerConnection(options
);
55 public void update(UpdateListener listener
) {
56 ServerConnection connection
= getServerConnection(options
);
57 doUpdate(connection
, listener
, null);
58 listener
.onSuccess(new UpdateSuccessEvent(""));
62 public void updateBackend(String backendName
, UpdateListener listener
) {
63 ServerConnection connection
= getServerConnection(options
);
64 doUpdate(connection
, listener
, backendName
);
65 listener
.onSuccess(new UpdateSuccessEvent(""));
69 public void updateBackends(List
<String
> backendNames
, UpdateListener listener
) {
70 ServerConnection connection
= getServerConnection(options
);
71 for (String backendName
: backendNames
) {
72 doUpdate(connection
, listener
, backendName
);
74 listener
.onSuccess(new UpdateSuccessEvent(""));
78 public void updateAllBackends(UpdateListener listener
) {
79 ServerConnection connection
= getServerConnection(options
);
80 if (app
.getBackendsXml() != null) {
81 for (BackendsXml
.Entry backend
: app
.getBackendsXml().getBackends()) {
82 doUpdate(connection
, listener
, backend
.getName());
85 listener
.onSuccess(new UpdateSuccessEvent(""));
89 public void rollback() {
90 rollbackBackend(null);
94 public void rollbackBackend(String backend
) {
95 ServerConnection connection
= getServerConnection(options
);
97 AppVersionUpload uploader
= createAppVersionUpload(connection
, app
, backend
);
98 uploader
.forceRollback();
99 } catch (Throwable t
) {
100 errorWriter
.println("Unable to rollback:");
101 t
.printStackTrace(errorWriter
);
102 throw new AdminException("Unable to rollback app: " + t
.getMessage(), t
);
107 public void rollbackAllBackends() {
108 ServerConnection connection
= getServerConnection(options
);
109 if (app
.getBackendsXml() != null) {
111 for (BackendsXml
.Entry backend
: app
.getBackendsXml().getBackends()) {
112 AppVersionUpload uploader
= createAppVersionUpload(connection
, app
, backend
.getName());
113 uploader
.forceRollback();
115 } catch (Throwable t
) {
116 errorWriter
.println("Unable to rollback:");
117 t
.printStackTrace(errorWriter
);
118 throw new AdminException("Unable to rollback app: " + t
.getMessage(), t
);
124 public void setBackendState(String backendName
, BackendsXml
.State newState
) {
128 url
= "/api/backends/start";
131 url
= "/api/backends/stop";
134 throw new IllegalArgumentException("Cannot change to state: " + newState
);
137 ServerConnection connection
= getServerConnection(options
);
139 connection
.post(url
, "", "app_id", app
.getAppId(), "backend", backendName
);
140 } catch (Throwable t
) {
141 errorWriter
.println("Unable to change backend state:");
142 t
.printStackTrace(errorWriter
);
143 throw new AdminException("Unable to change backend state: " + t
.getMessage(), t
);
148 public List
<BackendsXml
.Entry
> listBackends() {
149 ServerConnection connection
= getServerConnection(options
);
151 String yaml
= connection
.post("/api/backends/list", "", "app_id", app
.getAppId());
152 if (yaml
.contains("No backends configured")) {
153 return Collections
.<BackendsXml
.Entry
>emptyList();
155 BackendsXml xml
= BackendsYamlReader
.parse(yaml
);
156 return xml
.getBackends();
158 } catch (Throwable t
) {
159 errorWriter
.println("Unable to list backends:");
160 t
.printStackTrace(errorWriter
);
161 throw new AdminException("Unable to list backends: " + t
.getMessage(), t
);
166 public void deleteBackend(String backendName
) {
167 ServerConnection connection
= getServerConnection(options
);
169 connection
.post("/api/backends/delete", "", "app_id", app
.getAppId(), "backend", backendName
);
170 } catch (Throwable t
) {
171 errorWriter
.println("Unable to delete backend:");
172 t
.printStackTrace(errorWriter
);
173 throw new AdminException("Unable to delete backend: " + t
.getMessage(), t
);
178 public void configureBackend(String backendName
) {
179 ServerConnection connection
= getServerConnection(options
);
181 connection
.post("/api/backends/configure", app
.getBackendsXml().toYaml(),
182 "app_id", app
.getAppId(), "backend", backendName
);
183 } catch (Throwable t
) {
184 errorWriter
.println("Unable to configure backend:");
185 t
.printStackTrace(errorWriter
);
186 throw new AdminException("Unable to configure backend: " + t
.getMessage(), t
);
190 private void changeModuleState(String url
) {
191 ServerConnection connection
= getServerConnection(options
);
195 "app_id", app
.getAppId(),
196 "module", app
.getModule() != null ? app
.getModule() : "",
197 "version", app
.getVersion());
198 } catch (Throwable t
) {
199 errorWriter
.println("Unable to change module state:");
200 t
.printStackTrace(errorWriter
);
201 throw new AdminException("Unable to change module state: " + t
.getMessage(), t
);
206 public void startModuleVersion() {
207 changeModuleState("/api/modules/start");
211 public void stopModuleVersion() {
212 changeModuleState("/api/modules/stop");
216 public void updateIndexes() {
217 ServerConnection connection
= getServerConnection(options
);
219 AppVersionUpload uploader
= createAppVersionUpload(connection
, app
, null);
220 uploader
.updateIndexes();
221 } catch (Throwable t
) {
222 errorWriter
.println("Unable to update indexes:");
223 t
.printStackTrace(errorWriter
);
224 throw new AdminException("Unable to update indexes for app: " + t
.getMessage(), t
);
229 public void updateCron() {
230 ServerConnection connection
= getServerConnection(options
);
232 AppVersionUpload uploader
= createAppVersionUpload(connection
, app
, null);
233 uploader
.updateCron();
234 } catch (Throwable t
) {
235 errorWriter
.println("Unable to update cron entries:");
236 t
.printStackTrace(errorWriter
);
237 throw new AdminException("Unable to update cron entries for app: " + t
.getMessage(), t
);
242 public void updateQueues() {
243 ServerConnection connection
= getServerConnection(options
);
245 AppVersionUpload uploader
= createAppVersionUpload(connection
, app
, null);
246 uploader
.updateQueue();
247 } catch (Throwable t
) {
248 errorWriter
.println("Unable to upload:");
249 t
.printStackTrace(errorWriter
);
250 throw new AdminException("Unable to update task queues for app: " + t
.getMessage(), t
);
255 public void updateDispatch() {
256 ServerConnection connection
= getServerConnection(options
);
258 AppVersionUpload uploader
= createAppVersionUpload(connection
, app
, null);
259 uploader
.updateDispatch();
260 } catch (Throwable t
) {
261 errorWriter
.println("Unable to update dispatch entries:");
262 t
.printStackTrace(errorWriter
);
263 throw new AdminException("Unable to update dispatch entries for app: " + t
.getMessage(), t
);
268 public void updateDos() {
269 ServerConnection connection
= getServerConnection(options
);
271 AppVersionUpload uploader
= createAppVersionUpload(connection
, app
, null);
272 uploader
.updateDos();
273 } catch (Throwable t
) {
274 errorWriter
.println("Unable to update DoS entries:");
275 t
.printStackTrace(errorWriter
);
276 throw new AdminException("Unable to update DoS entries for app: " + t
.getMessage(), t
);
281 public void setDefaultVersion() {
282 ServerConnection connection
= getServerConnection(options
);
284 AppVersionUpload uploader
= createAppVersionUpload(connection
, app
, null);
285 uploader
.setDefaultVersion();
286 } catch (Throwable t
) {
287 errorWriter
.println("Unable to set default version:");
288 t
.printStackTrace(errorWriter
);
289 throw new AdminException("Unable to set default version for app: " + t
.getMessage(), t
);
294 public List
<CronEntry
> cronInfo() {
296 List
<CronEntry
> result
= new ArrayList
<CronEntry
>();
298 CronXml cron
= app
.getCronXml();
302 for (CronXml
.Entry entry
: cron
.getEntries()) {
303 result
.add(new CronEntryImpl(entry
.getUrl(), entry
.getDescription(), entry
.getSchedule(),
304 entry
.getTimezone()));
307 } catch (Throwable t
) {
308 errorWriter
.println("Unable to display run times for cron entries:");
309 t
.printStackTrace(errorWriter
);
310 throw new AdminException("Unable to display run times for cron entries for app: "
311 + t
.getMessage(), t
);
316 public ResourceLimits
getResourceLimits() {
317 ServerConnection connection
= getServerConnection(options
);
319 ClientDeploySender clientDeploySender
= new NoLoggingClientDeploySender(connection
);
320 return ResourceLimits
.request(clientDeploySender
, app
);
321 } catch (Throwable t
) {
322 errorWriter
.println("Unable to get resource limits:");
323 t
.printStackTrace(errorWriter
);
324 throw new AdminException("Unable to get resource limits: "
325 + t
.getMessage(), t
);
330 public void vacuumIndexes(ConfirmationCallback
<IndexDeleter
.DeleteIndexAction
> callback
,
331 UpdateListener listener
) {
332 String appID
= app
.getAppId();
333 if (null == appID
|| appID
.isEmpty()) {
334 String message
= "This application does not have an ID.";
335 String detailMessage
=
336 "The vacuum_indexes operation may not be performed for"
337 + " an application that does not have an ID.";
338 AdminException e
= new AdminException(message
);
339 listener
.onFailure(new UpdateFailureEvent(e
, message
, detailMessage
));
342 ServerConnection connection
= getServerConnection(options
);
343 IndexDeleter deleter
= new IndexDeleter(connection
, app
, callback
, errorWriter
, listener
);
345 deleter
.deleteUnusedIndexes();
346 } catch (Exception e
) {
347 String message
= "Unable to perform vacuum_indexes";
348 listener
.onFailure(new UpdateFailureEvent(e
, message
, e
.getMessage()));
349 throw new AdminException(message
, e
);
354 public Reader
requestLogs(int numDays
, LogSeverity severity
,
355 boolean includeAll
) {
356 ServerConnection connection
= getServerConnection(options
);
358 File logFile
= File
.createTempFile(app
.getAppId() + "-" + app
.getVersion(), ".log");
359 logFile
.deleteOnExit();
360 LogFetcher logFetcher
= new LogFetcher(app
, connection
);
361 logFetcher
.fetch(numDays
, severity
, includeAll
, new FileOutputStream(logFile
));
362 return new BufferedReader(new FileReader(logFile
));
363 } catch (Exception ex
) {
364 throw new AdminException("Unable to retrieve the remote application logs:", ex
);
369 * Retrieve the list of versions of this application.
372 public String
listVersions() {
373 ServerConnection connection
= getServerConnection(options
);
375 return connection
.post("/api/versions/list", "", "app_id", app
.getAppId());
376 } catch (Throwable t
) {
377 errorWriter
.println("Unable to retrieve versions:");
378 t
.printStackTrace(errorWriter
);
379 throw new AdminException("Unable to retrieve versions: " + t
.getMessage(), t
);
384 * Delete the specified version of this application.
387 public String
deleteVersion(String appId
, String moduleId
, String versionId
) {
388 ServerConnection connection
= getServerConnection(options
);
390 return connection
.post("/api/versions/delete", "",
392 "module", moduleId
!= null ? moduleId
: "",
393 "version_match", versionId
);
394 } catch (Throwable t
) {
395 errorWriter
.println("Unable to delete version:");
396 t
.printStackTrace(errorWriter
);
397 throw new AdminException("Unable to delete version: " + t
.getMessage(), t
);
402 public String
debugVersion() {
403 if (null == app
.getAppId() || app
.getAppId().isEmpty()) {
404 throw new AdminException("This application does not have an id");
406 if (null == app
.getVersion() || app
.getVersion().isEmpty()) {
407 throw new AdminException("This application does not have a version");
410 ServerConnection connection
= getServerConnection(options
);
412 return connection
.post("/api/vms/debug", "",
413 "app_id", app
.getAppId(),
414 "module", app
.getModule() != null ? app
.getModule() : "",
415 "version_match", app
.getVersion());
416 } catch (Throwable t
) {
417 errorWriter
.println("Unable to debug version:");
418 t
.printStackTrace(errorWriter
);
419 throw new AdminException("Unable to debug version: " + t
.getMessage(), t
);
424 public String
debugVersionState() {
425 if (null == app
.getAppId() || app
.getAppId().isEmpty()) {
426 throw new AdminException("This application does not have an id");
428 if (null == app
.getVersion() || app
.getVersion().isEmpty()) {
429 throw new AdminException("This application does not have a version");
432 ServerConnection connection
= getServerConnection(options
);
434 return connection
.post("/api/vms/debugstate", "",
435 "app_id", app
.getAppId(),
436 "module", app
.getModule() != null ? app
.getModule() : "",
437 "version_match", app
.getVersion());
438 } catch (Throwable t
) {
439 errorWriter
.println("Unable to get state for debug version call:");
440 t
.printStackTrace(errorWriter
);
441 throw new AdminException("Unable to get state for debug version: " + t
.getMessage(), t
);
446 public void migrateTraffic() {
448 if (app
.getModule() != null && !app
.getModule().equals("")
449 && !app
.getModule().equals("default")) {
450 throw new AdminException("Traffic migration does not support non-default modules yet");
453 ServerConnection connection
= getServerConnection(options
);
455 connection
.post("/api/appversion/migratetraffic", "",
456 "app_id", app
.getAppId(),
457 "version", app
.getVersion());
458 } catch (Throwable t
) {
459 errorWriter
.println("Unable to migrate traffic:");
460 t
.printStackTrace(errorWriter
);
461 throw new AdminException("Unable to migrate version: " + t
.getMessage(), t
);
465 private void requireAppId() {
466 if (null == app
.getAppId() || app
.getAppId().isEmpty()) {
467 throw new AdminException("This application does not have an id");
471 private void requireVersion() {
474 if (null == app
.getVersion() || app
.getVersion().isEmpty()) {
475 throw new AdminException("This application does not have a version");
480 public void stageApplicationWithDefaultResourceLimits(File stagingDir
) {
481 stageApplication(stagingDir
, false);
485 public void stageApplicationWithRemoteResourceLimits(File stagingDir
) {
486 stageApplication(stagingDir
, true);
490 * Convert a java appengine war directory into a format that can be deployed to
493 void stageApplication(File stagingDir
, boolean useRemoteResourceLimits
) {
494 if (stagingDir
== null) {
495 throw new AdminException("Staging dir is not a valid directory (it is null)");
497 if (stagingDir
.exists()) {
498 if (!stagingDir
.isDirectory()) {
499 throw new AdminException(stagingDir
.getPath()
500 + " is not a valid staging directory (it is a regular file)");
502 Path path
= stagingDir
.toPath();
503 try (DirectoryStream
<Path
> dirStream
= java
.nio
.file
.Files
.newDirectoryStream(path
)) {
504 if (dirStream
.iterator().hasNext()) {
505 throw new AdminException(
506 stagingDir
.getPath() + " is not a valid staging directory (it is not empty)");
508 } catch (IOException e
) {
509 throw new AdminException("Unable to stage application.", e
);
512 StringWriter detailsWriter
= new StringWriter();
514 ResourceLimits resourceLimits
= ResourceLimits
.newDefaultResourceLimits();
515 if (useRemoteResourceLimits
) {
516 ServerConnection connection
= getServerConnection(options
);
517 ClientDeploySender clientDeploySender
;
518 if (getUpdateOptions().getUpdateUsageReporting()) {
519 String sdkVersion
= getUpdateOptions().getSdkVersion();
520 clientDeploySender
= new LoggingClientDeploySender(connection
, sdkVersion
);
522 clientDeploySender
= new NoLoggingClientDeploySender(connection
);
524 resourceLimits
= ResourceLimits
.request(clientDeploySender
, app
);
527 app
.setDetailsWriter(new PrintWriter(detailsWriter
, true));
528 app
.createStagingDirectory(appOptions
, resourceLimits
, stagingDir
);
529 } catch (Throwable t
) {
530 errorWriter
.println("Unable to stage:");
531 t
.printStackTrace(errorWriter
);
532 throw new AdminException("Unable to stage app: " + t
.getMessage(), t
);
537 * Deploy a new version of this application. If successful, this method will
538 * return without throwing an exception but will not call
539 * {@link UpdateListener#onSuccess(UpdateSuccessEvent)}. The caller is responsible for
540 * calling that method.
542 private void doUpdate(ServerConnection connection
, UpdateListener listener
, String backend
) {
543 StringWriter detailsWriter
= new StringWriter();
545 AppVersionUpload uploader
= createAppVersionUpload(connection
, app
, backend
);
546 boolean updateGlobalConfigurations
= getUpdateOptions().getUpdateGlobalConfigurations();
547 boolean failOnPrecompilationError
= appOptions
.isFailOnPrecompilationError();
548 boolean ignoreEndpointsFailures
= appOptions
.isIgnoreEndpointsFailures();
549 ClientDeploySender clientDeploySender
;
550 if (getUpdateOptions().getUpdateUsageReporting()) {
551 String sdkVersion
= getUpdateOptions().getSdkVersion();
552 clientDeploySender
= new LoggingClientDeploySender(connection
, sdkVersion
);
554 clientDeploySender
= new NoLoggingClientDeploySender(connection
);
556 ResourceLimits resourceLimits
= ResourceLimits
.request(clientDeploySender
, app
);
558 app
.setListener(listener
);
559 app
.setDetailsWriter(new PrintWriter(detailsWriter
, true));
560 app
.createStagingDirectory(appOptions
, resourceLimits
);
561 app
.exportRepoInfoFile();
562 clientDeploySender
.setRuntime(AppVersionUpload
.getRuntime(app
.getAppYaml()));
563 uploader
.doUpload(resourceLimits
, updateGlobalConfigurations
, failOnPrecompilationError
,
564 ignoreEndpointsFailures
, clientDeploySender
);
565 } catch (Throwable t
) {
566 errorWriter
.println("Unable to update:");
567 t
.printStackTrace(errorWriter
);
568 listener
.onFailure(new UpdateFailureEvent(t
, t
.toString(), detailsWriter
.toString()));
569 throw new AdminException("Unable to update app: " + t
.getMessage(), t
);
573 private AppVersionUpload
createAppVersionUpload(ServerConnection connection
,
574 GenericApplication app
, String backend
) throws Exception
{
575 Constructor
<?
extends AppVersionUpload
> constructor
=
576 appVersionUploadClass
.getConstructor(ServerConnection
.class, GenericApplication
.class,
577 String
.class, Boolean
.TYPE
);
578 return constructor
.newInstance(connection
, app
, backend
, appOptions
.isBatchModeSet());
582 public UpdateOptions
getUpdateOptions() {
583 return updateOptions
;