Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

notice when app engine java components aren't installed #207

Closed
wants to merge 16 commits into from
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ sudo: false
language: java
jdk:
- openjdk7
install:
# download Cloud SDK
- wget https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-124.0.0-linux-x86_64.tar.gz
- tar -xzvf google-cloud-sdk-124.0.0-linux-x86_64.tar.gz
# update all Cloud SDK components
- gcloud components update --quiet
# add App Engine component to Cloud SDK
- gcloud components install app-engine-java --quiet
cache:
directories:
- $HOME/.m2
Expand All @@ -16,5 +24,7 @@ script:

env:
global:
- PATH=$PWD/google-cloud-sdk/bin:$PATH
- CLOUDSDK_CORE_DISABLE_USAGE_REPORTING=true
- secure: "da45ryIB/m37Gkpon/Uy6PPl1AhsUMdxgG7aeQIM1alh3Np4JwqKHobdP8tC0Qc/vb5fqfisEZEGPukh9Kxw95pRATO/QrQrsxYISMXUUc+Dlvo8WLeDq5F2LdUSn3933H9p5Mk8bKrZql+Jb+Il5S+B3Ib8uO+e3L4itrGKu5dw6i8TAxMggUjK8L6RuRYZeXMNiw3iaLjlHhNVEZ7F7RQ/gsHT2LhzybY6gfVJU+8AHhwEv+Tuapz5QCYTah6pwKqP+EQPhFlfrow9zdBfQm7m4h+uU+TB67VzZi46pAx+drC1quW+HZllutMHKM+cR13HsTET9qbFmV00cr2gZ3UXMVPX+PG1johZj0gs8s/S0+MVh6IQ0QLMWSqgJH/ULlyoU9PLE+PMs8A2qprO8NALuS3GXgvMQ8tHGuAzUVht/CxH5WTxW8nGFv8lwCIrT+m0hWi26gXWpNItC4N5GawoJQp+eWW0dO2Uko34kC3CIkLppRGxgzQWQQHkR+Hh+Yi3AzZsUzz30YRezF+G1RuQ68Va9yZ9qgei3h0Ppx2OIZ6fyiGg6Z8MHzOA8AULoe/Sz7CsfCiOyjTWmccRSl79wc0kpZwrF2qLO46LD3DgpGcfcKEB/cqcdwj/SA4QJoRcwUPSqy2eXb6ILhHA9UGlSBwluPZ2tSXDh2zxCL0="
- secure: "Z/Haq0MHdfBpJKntdYZFdK3uxUUk+jjiNzuuOt2hLa7P5XxpoHsboRykWSY41tWD5NnXfXoJvcHPASwhz2OesQcyNCCEHxsikkNRk+XUgFjyu8I3Ohm8Fya1k9gn+Xlz9uDBtoGN1e2SHhfEY+a9a5nGvv4aLVblBewt9J14su8fc4WSyDYggoZnUjB69DzIJrBL/ol0VOqlo8/vrcbWRbB/hlJ3QwKsFkehIhx5//GVdEJGqM2CZNJqG/bVLaSIoqQPqd6v3eyemnS2O5bXNIoNEq9yUWGSm1PfnagBYMzkHcqNrg5D1aYI+vFFeZe4PAsBaE6Xw/trCMtdRr5uDKXdNW8s8zwMKfOYru3zY9KTq2N5Fl/HgazGfimpmAjXqpH74+WaITmQmCZ6LuLb7hgltF+f2OY71bHJI7lGeuCraiwb6ECFIZiF9+FA6m0DXU6fCsajoThUtkRSpTMxDTAMzlrH/Kw2SzwEfuGk3evsb937pLUkL1kNxSc6GXhdyRrLTSrYgoqavjtNUN5S7v7MqrmO4R6UF1ZtJOAxWJ1a4W/kQP6S/36u8dZhCWbds78EocOH/+fsJ8vlCmTAnFEbtZkSpmFHjGrpPu0gVYdG+u96eAtKv90p2IF8bwCzFQRh1U25OlcxHKZfDgGzplR1Cy8DdJoNcH3KJKQd1Cw="
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@
<artifactId>jsr305</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20160810</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.google.cloud.tools.appengine.cloudsdk;

import com.google.cloud.tools.appengine.api.AppEngineException;

/**
* User needs to run <samp>gcloud components install app-engine-java</samp>.
*/
public class AppEngineComponentsNotInstalledException extends AppEngineException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't AppEngineException for errors caused by gcloud invocations?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also I haven't checked the styleguide recently, is the javadoc no longer required for all public classes?=

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added Javadoc. This seems close enough to AppEngineException to justify subclassing. I can change to a different exception if you like, but that would require clients to update.


AppEngineComponentsNotInstalledException(String message) {
super(message);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
Expand Down Expand Up @@ -70,22 +75,22 @@ public class CloudSdk {
@Nullable
private final File appCommandCredentialFile;
private final String appCommandOutputFormat;
private final WaitingProcessOutputLineListener runDevAppServerWaitListener;
private final WaitingProcessOutputLineListener outputLineListener;

private CloudSdk(Path sdkPath,
String appCommandMetricsEnvironment,
String appCommandMetricsEnvironmentVersion,
@Nullable File appCommandCredentialFile,
String appCommandOutputFormat,
ProcessRunner processRunner,
WaitingProcessOutputLineListener runDevAppServerWaitListener) {
WaitingProcessOutputLineListener outputLineListener) {
this.sdkPath = sdkPath;
this.appCommandMetricsEnvironment = appCommandMetricsEnvironment;
this.appCommandMetricsEnvironmentVersion = appCommandMetricsEnvironmentVersion;
this.appCommandCredentialFile = appCommandCredentialFile;
this.appCommandOutputFormat = appCommandOutputFormat;
this.processRunner = processRunner;
this.runDevAppServerWaitListener = runDevAppServerWaitListener;
this.outputLineListener = outputLineListener;

// Populate jar locations.
// TODO(joaomartins): Consider case where SDK doesn't contain these jars. Only App Engine
Expand All @@ -100,15 +105,14 @@ private CloudSdk(Path sdkPath,
/**
* Uses the process runner to execute the gcloud app command with the provided arguments.
*
* @param args The arguments to pass to "gcloud app" command.
* @param args the arguments to pass to "gcloud app" command
*/
public void runAppCommand(List<String> args) throws ProcessRunnerException {
List<String> command = new ArrayList<>();
command.add(getGCloudPath().toString());
command.add("app");
command.addAll(args);

command.add("--quiet");
command.addAll(GcloudArgs.get("format", appCommandOutputFormat));

Map<String, String> environment = Maps.newHashMap();
Expand All @@ -125,6 +129,38 @@ public void runAppCommand(List<String> args) throws ProcessRunnerException {
logCommand(command);
processRunner.setEnvironment(environment);
processRunner.run(command.toArray(new String[command.size()]));
processRunner.run(command.toArray(new String[command.size()]));
}

/**
* Checks whether the specified component is installed in the local environment.
*
* @return true iff the specified component is installed in the local environment
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{@code true}

*/
public boolean isComponentInstalled(String id) throws ProcessRunnerException {
List<String> command = new ArrayList<>();
command.add(getGCloudPath().toString());
command.add("components");
command.add("list");
command.add("--format=json");
command.add("--filter=id:" + id);

String json = processRunner.runSynchronously(command.toArray(new String[command.size()]));

try {
JSONTokener tokener = new JSONTokener(json);
JSONArray array = new JSONArray(tokener);
if (array.length() == 0) {
return false;
}
JSONObject object = array.getJSONObject(0);
JSONObject state = object.getJSONObject("state");
String name = state.getString("name");
return "Installed".equals(name);
} catch (JSONException | NullPointerException ex) {
throw new AppEngineException(
"Could not determine whether App Engine Java component is installed", ex);
}
}

/**
Expand All @@ -149,8 +185,8 @@ public void runDevAppServerCommand(List<String> args) throws ProcessRunnerExcept
processRunner.run(command.toArray(new String[command.size()]));

// wait for start if configured
if (runDevAppServerWaitListener != null) {
runDevAppServerWaitListener.await();
if (outputLineListener != null) {
outputLineListener.await();
}
}

Expand Down Expand Up @@ -233,7 +269,7 @@ public Path getJarPath(String jarName) {
/**
* Checks whether the configured Cloud SDK Path is valid.
*
* @throws AppEngineException when there is a validation error.
* @throws AppEngineException when there is a validation error
*/
public void validate() throws AppEngineException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if validate() expects errors why not just return the issues rather than throw them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do that. It is a breaking API change.

if (sdkPath == null) {
Expand Down Expand Up @@ -262,11 +298,20 @@ public void validate() throws AppEngineException {
"Validation Error: Java Tools jar location '"
+ JAR_LOCATIONS.get(JAVA_TOOLS_JAR) + "' is not a file.");
}
try {
if (!isComponentInstalled("app-engine-java")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the block above that checks the existence of JAVA_TOOLS_JAR sufficient to tell if the component is installed? I'm still not a fan of doing this expensive check using gcloud components list.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 to @meltsufin's comment: I tried running gcloud from a VM shared drive and it was glacially sloooow. If JAVA_TOOLS_JAR (or one of the jars) isn't available then we're either dealing with a missing app-engine-java component, or appengine-plugins-core is out of sync with the installed version and

Copy link
Member

@briandealwis briandealwis Sep 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use JAVA_TOOLS_JAR as a heuristic: if the path exists, then we seem to have app-engine-java installed, otherwise confirm with gcloud components list.

(or better yet, …/.install/app-engine-java.manifest, since that's most specific.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just tried running gcloud components remove app-engine-java on my Linux machine and it did delete ~/google-cloud-sdk/platform/google_appengine/google/appengine/tools/java/lib/appengine-tools-api.jar. When installed it back using gcloud components install app-engine-java, it reappeared. So, I think checking for existence of JAVA_TOOLS_JAR is sufficient.
@elharo, do you see a different result when you run these commands?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The practice of depending on the Cloud SDKs internal directory structure is not OK with the Cloud SDK team. They reserve the right to break this at their discretion. We need to move away from depending on incidental files distributed with the SDK and moving to using their api/cli for this check is part of that.

This is also why we're moving to using Maven central as the source for classpath deps.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's true that the Cloud SDK reserves the right to move the files, but until we drop the dependency on AppCfg in the app-engine-java component in the Cloud SDK, we can do the file-based check without making anything more fragile in the library.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Staging is in the process of being distributed to Maven Central. We should
be extracting ourselves out of this mess, rather than digging deeper into
it.

On Thu, Sep 8, 2016 at 12:46 PM, meltsufin [email protected] wrote:

In src/main/java/com/google/cloud/tools/appengine/cloudsdk/CloudSdk.java
#207 (comment)
:

@@ -262,11 +290,20 @@ public void validate() throws AppEngineException {
"Validation Error: Java Tools jar location '"
+ JAR_LOCATIONS.get(JAVA_TOOLS_JAR) + "' is not a file.");
}

  • try {
  •  if (!isComponentInstalled("app-engine-java")) {
    

It's true that the Cloud SDK reserves the right to move the files, but
until we drop the dependency on AppCfg in the app-engine-java component in
the Cloud SDK, we can do the file-based check without making anything more
fragile in the library.


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/GoogleCloudPlatform/appengine-plugins-core/pull/207/files/29147b0c66ac9c154d990808acfebec1ffc8f716#r78044155,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AHf5HRitzbZ-DsADGWSLzsSPa2aR3TLxks5qoDvvgaJpZM4J35Q-
.

throw new AppEngineComponentsNotInstalledException(
"Validation Error: App Engine Java component not installed");
}
} catch (ProcessRunnerException ex) {
throw new AppEngineException(
"Could not determine whether App Engine Java component is installed", ex);
}
}

@VisibleForTesting
WaitingProcessOutputLineListener getRunDevAppServerWaitListener() {
return runDevAppServerWaitListener;
return outputLineListener;
}

public static class Builder {
Expand Down Expand Up @@ -528,4 +573,5 @@ public int compare(CloudSdkResolver o1, CloudSdkResolver o2) {
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2016 Google Inc.
*
* 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.
*/

package com.google.cloud.tools.appengine.cloudsdk.internal.process;

import com.google.cloud.tools.appengine.cloudsdk.process.ProcessOutputLineListener;

class AccumulatingLineListener implements ProcessOutputLineListener {

private StringBuilder output = new StringBuilder();

@Override
public void onOutputLine(String line) {
output.append(line + "\n");
}

String getOutput() {
return output.toString();
}

public void clear() {
output = new StringBuilder();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ public DefaultProcessRunner(boolean async,
* <p>If any output listeners were configured, output will go to them only. Otherwise, process
* output will be redirected to the caller via inheritIO.
*
* @param command The shell command to execute
* @param command the shell command to execute
*/
@Override
public void run(String[] command) throws ProcessRunnerException {
try {
// Configure process builder.
Expand Down Expand Up @@ -131,17 +132,69 @@ public void run(String[] command) throws ProcessRunnerException {
throw new ProcessRunnerException(e);
}
}

/**
* Executes a not-too-long-lived shell command synchornously and returns stdout.
*
* <p>If any output listeners were configured, output will go to them only. Otherwise, process
* output will be redirected to the caller via inheritIO.
*
* @param command the shell command to execute
* @return everything printed on stdout
*/
@Override
public String runSynchronously(String[] command) throws ProcessRunnerException {
try {
// Configure process builder.
final ProcessBuilder processBuilder = new ProcessBuilder();

// If there are no listeners, we might still want to redirect stdout and stderr to the parent
// process, or not.
if (stdErrLineListeners.isEmpty() && inheritProcessOutput) {
processBuilder.redirectError(Redirect.INHERIT);
}
if (environment != null) {
processBuilder.environment().putAll(environment);
}

processBuilder.command(command);

Process process = processBuilder.start();
AccumulatingLineListener stdOut = new AccumulatingLineListener();
stdOutLineListeners.add(stdOut);
// Only handle stdout or stderr if there are listeners.
handleStdOut(process);
if (!stdErrLineListeners.isEmpty()) {
handleErrOut(process);
}

for (ProcessStartListener startListener : startListeners) {
startListener.onStart(process);
}

shutdownProcessHook(process);
syncRun(process);
stdOutLineListeners.remove(stdOut);

return stdOut.getOutput();

} catch (IOException | InterruptedException | IllegalThreadStateException e) {
throw new ProcessRunnerException(e);
}
}

/**
* Environment variables to append to the current system environment variables.
*/
@Override
public void setEnvironment(Map<String, String> environment) {
this.environment = environment;
}

private void handleStdOut(final Process process) {
final Scanner stdOut = new Scanner(process.getInputStream(), Charsets.UTF_8.name());
Thread stdOutThread = new Thread("standard-out") {
@Override
public void run() {
while (stdOut.hasNextLine() && !Thread.interrupted()) {
String line = stdOut.nextLine();
Expand All @@ -159,6 +212,7 @@ public void run() {
private void handleErrOut(final Process process) {
final Scanner stdErr = new Scanner(process.getErrorStream(), Charsets.UTF_8.name());
Thread stdErrThread = new Thread("standard-err") {
@Override
public void run() {
while (stdErr.hasNextLine() && !Thread.interrupted()) {
String line = stdErr.nextLine();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
public interface ProcessRunner {

void run(String[] command) throws ProcessRunnerException;

String runSynchronously(String[] command) throws ProcessRunnerException;

void setEnvironment(Map<String, String> environment);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.google.cloud.tools.appengine.cloudsdk;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.nio.file.Files;

import org.junit.Test;

import com.google.cloud.tools.appengine.cloudsdk.internal.process.ProcessRunnerException;

/**
* Integration tests for {@link CloudSdk} that require an installed CloudSdk instance.
*/
public class CloudSdkEnvironmentTest {

private CloudSdk sdk = new CloudSdk.Builder().build();

@Test
public void testGetSdkPath() {
assertTrue(Files.exists(sdk.getSdkPath()));
}

@Test
public void testIsComponentInstalled_true() throws ProcessRunnerException {
assertTrue(sdk.isComponentInstalled("app-engine-java"));
}

@Test
public void testIsComponentInstalled_False() throws ProcessRunnerException {
assertFalse(sdk.isComponentInstalled("no-such-component"));
}

@Test
public void testIsComponentInstalled_sequential() throws ProcessRunnerException {
assertTrue(sdk.isComponentInstalled("app-engine-java"));
assertFalse(sdk.isComponentInstalled("no-such-component"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;

import com.google.cloud.tools.appengine.cloudsdk.CloudSdk.Builder;

import com.google.cloud.tools.appengine.api.AppEngineException;
import com.google.cloud.tools.appengine.cloudsdk.CloudSdk.Builder;
import com.google.cloud.tools.appengine.cloudsdk.process.ProcessOutputLineListener;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand Down