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

Make most ITs work with both linux and windows #143

Merged
merged 16 commits into from
Oct 17, 2019
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Jenkinsfile → Jenkinsfile.linux
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ pipeline {
GOOGLE_ZONE = "${GCE_IT_ZONE}"
GOOGLE_SA_NAME = "${GCE_IT_SA}"
BUILD_ARTIFACTS_BUCKET = "${GCE_IT_BUCKET}"
BUILD_ARTIFACTS = "build-${BRANCH_NAME}-${BUILD_ID}.tar.gz"
CLEAN_BRANCH_NAME = "${BRANCH_NAME}".replaceAll("[/&;<>|\\]]", "_")
BUILD_ARTIFACTS = "build-${CLEAN_BRANCH_NAME}-${BUILD_ID}.tar.gz"
}

stages {
Expand Down
60 changes: 60 additions & 0 deletions Jenkinsfile.windows
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2019 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.
*/

// Windows Agent IT pipeline, run manually.
pipeline {
agent {
kubernetes {
cloud 'kubernetes'
label 'maven-pod'
yamlFile 'jenkins/maven-pod.yaml'
}
}

environment {
GOOGLE_PROJECT_ID = "${GCE_IT_PROJECT_ID}"
GOOGLE_REGION = "${GCE_IT_REGION}"
GOOGLE_ZONE = "${GCE_IT_ZONE}"
GOOGLE_SA_NAME = "${GCE_IT_SA}"
GOOGLE_BOOT_DISK_PROJECT_ID = "${GCE_WINDOWS_IT_IMAGE_PROJECT}"
GOOGLE_BOOT_DISK_IMAGE_NAME = "${GCE_WINDOWS_IT_IMAGE_NAME}"
BUILD_ARTIFACTS_BUCKET = "${GCE_IT_BUCKET}"
CLEAN_BRANCH_NAME = "${BRANCH_NAME}".replaceAll("[/&;<>|\\]]", "_")
BUILD_ARTIFACTS = "windows-${CLEAN_BRANCH_NAME}-${BUILD_ID}.tar.gz"
}

stages {
stage("Build and test") {
steps {
container('maven') {
withCredentials([[$class: 'StringBinding', credentialsId: env.GCE_IT_CRED_ID, variable: 'GOOGLE_CREDENTIALS'],
[$class: 'StringBinding', credentialsId: env.GCE_WINDOWS_IT_JENKINS_PASSWORD, variable: 'GOOGLE_JENKINS_PASSWORD']]) {
catchError {
// build
sh "mvn clean package -ntp"

// run tests
sh "mvn verify -ntp -Dit.windows=true"
}

sh "jenkins/saveAndCompress.sh"
step([$class: 'ClassicUploadStep', credentialsId: env.GCE_BUCKET_CRED_ID, bucket: "gs://${BUILD_ARTIFACTS_BUCKET}", pattern: env.BUILD_ARTIFACTS])
}
}
}
}
}
}
18 changes: 17 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<skip.surefire.tests>${skipTests}</skip.surefire.tests>
<it.windows>false</it.windows>
<configuration-as-code.version>1.14</configuration-as-code.version>
<concurrency>10</concurrency>
<it.runOrder>balanced</it.runOrder>
</properties>

<dependencies>
Expand Down Expand Up @@ -262,14 +264,15 @@
<hudson.model.LoadStatistics.decay>0.5</hudson.model.LoadStatistics.decay>
<hudson.agents.NodeProvisioner.MARGIN>100</hudson.agents.NodeProvisioner.MARGIN>
<hudson.agents.NodeProvisioner.MARGIN0>1.0</hudson.agents.NodeProvisioner.MARGIN0>
<com.google.jenkins.plugins.computeengine.integration.ITUtil.windows>${it.windows}</com.google.jenkins.plugins.computeengine.integration.ITUtil.windows>
</systemPropertyVariables>
<excludes>
<exclude>**/ComputeEngineCloudWindowsIT.java</exclude>
<exclude>**/ConfigAsCodeWindowsIT.java</exclude>
<exclude>**/ITUtil.java</exclude>
</excludes>
<argLine>-Djenkins.test.timeout=${jenkinsRuleTimeout}</argLine>
<runOrder>balanced</runOrder>
<runOrder>${it.runOrder}</runOrder>
</configuration>
</plugin>
<plugin>
Expand Down Expand Up @@ -332,5 +335,18 @@
<concurrency>4</concurrency>
</properties>
</profile>
<profile>
<id>windows-agents</id>
<activation>
<property>
<name>it.windows</name>
<value>true</value>
</property>
</activation>
<properties>
<concurrency>10</concurrency>
<it.runOrder>reversealphabetical</it.runOrder>
</properties>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@
import com.google.api.services.compute.model.Instance;
import com.google.api.services.compute.model.Scheduling;
import hudson.model.Executor;
import hudson.model.Result;
import hudson.model.TaskListener;
import hudson.slaves.AbstractCloudComputer;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import jenkins.model.CauseOfInterruption;
import lombok.extern.java.Log;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.HttpRedirect;
Expand Down Expand Up @@ -76,14 +74,7 @@ private Boolean getPreemptedStatus(TaskListener listener, String nodeName) {

private void interruptExecutor(Executor executor, String nodeName) {
log.log(Level.INFO, "Terminating executor " + executor + " node " + nodeName);
executor.interrupt(
Result.FAILURE,
new CauseOfInterruption() {
@Override
public String getShortDescription() {
return "Instance " + nodeName + " was preempted";
}
});
executor.abortResult();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,27 +51,18 @@ protected Optional<Connection> setupConnection(
return Optional.empty();
}

Connection cleanupConn;
GoogleKeyPair kp = node.getSSHKeyPair().get();
boolean isBootstrapped = bootstrap(kp, computer, listener);
if (isBootstrapped) {
// connect fresh as ROOT
logInfo(computer, listener, "connect fresh as root");
cleanupConn = connectToSsh(computer, listener);
if (!cleanupConn.authenticateWithPublicKey(
node.getSshUser(), kp.getPrivateKey().toCharArray(), "")) {
logWarning(computer, listener, "Authentication failed");
return Optional.empty(); // failed to connect
}
} else {
Optional<Connection> bootstrapConn = bootstrap(kp, computer, listener);
if (!bootstrapConn.isPresent()) {
logWarning(computer, listener, "bootstrapresult failed");
return Optional.empty();
}

return Optional.of(cleanupConn);
return bootstrapConn;
}

private boolean bootstrap(GoogleKeyPair kp, ComputeEngineComputer computer, TaskListener listener)
private Optional<Connection> bootstrap(
GoogleKeyPair kp, ComputeEngineComputer computer, TaskListener listener)
throws IOException, Exception { // TODO: better exceptions
logInfo(computer, listener, "bootstrap");
ComputeEngineInstance node = computer.getNode();
Expand Down Expand Up @@ -105,14 +96,15 @@ private boolean bootstrap(GoogleKeyPair kp, ComputeEngineComputer computer, Task
}
if (!isAuthenticated) {
logWarning(computer, listener, "Authentication failed");
return false;
return Optional.empty();
}
} finally {
} catch (Exception e) {
Copy link
Contributor

Choose a reason for hiding this comment

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

In general I'm weary of swallowing exceptions like this. I think at the very least we should consider logging it as a warning.

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 logging

if (bootstrapConn != null) {
bootstrapConn.close();
}
return Optional.empty();
}
return true;
return Optional.of(bootstrapConn);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,12 @@ protected Optional<Connection> setupConnection(
logWarning(computer, listener, "Non-windows node provided");
return Optional.empty();
}
boolean isBootstrapped = bootstrap(computer, listener);
if (!isBootstrapped) {
Optional<Connection> bootstrapConn = bootstrap(computer, listener);
if (!bootstrapConn.isPresent()) {
logWarning(computer, listener, "bootstrapresult failed");
return Optional.empty();
}

// connect fresh as ROOT
logInfo(computer, listener, "connect fresh as root");
Connection cleanupConn = connectToSsh(computer, listener);
if (!authenticateSSH(node.getSshUser(), node.getWindowsConfig(), cleanupConn, listener)) {
logWarning(computer, listener, "Authentication failed");
return Optional.empty(); // failed to connect
}

return Optional.of(cleanupConn);
return bootstrapConn;
}

private boolean authenticateSSH(
Expand All @@ -87,8 +78,7 @@ private boolean authenticateSSH(
return isAuthenticated;
}

private boolean bootstrap(ComputeEngineComputer computer, TaskListener listener)
throws IOException, Exception { // TODO(evanbrown): better exceptions
private Optional<Connection> bootstrap(ComputeEngineComputer computer, TaskListener listener) {
if (computer == null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Preconditions.checkNotNull()

throw new IllegalArgumentException("A null ComputeEngineComputer was provided");
}
Expand Down Expand Up @@ -125,14 +115,15 @@ private boolean bootstrap(ComputeEngineComputer computer, TaskListener listener)
}
if (!isAuthenticated) {
logWarning(computer, listener, "Authentication failed");
return false;
return Optional.empty();
}
} finally {
} catch (Exception e) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as above re: swallowing exceptions.

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 logging

if (bootstrapConn != null) {
bootstrapConn.close();
}
return Optional.empty();
}
return true;
return Optional.ofNullable(bootstrapConn);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
*/
final class PreemptedCheckCallable extends MasterToSlaveCallable<Boolean, IOException> {
private static final String METADATA_SERVER_URL =
"http://metadata.google.internal/computeMetadata/v1/instance/preempted?wait_for_change=true";
"http://metadata.google.internal/computeMetadata/v1/instance/preempted?wait_for_change=%s";

private final TaskListener listener;

Expand All @@ -58,7 +58,18 @@ final class PreemptedCheckCallable extends MasterToSlaveCallable<Boolean, IOExce
*/
@Override
public Boolean call() throws IOException {
HttpRequest request = createMetadataRequest();
HttpRequest initialRequest = createMetadataRequest(false);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This came up in #148 as well. Having the preemption event occur before the agent is running and listening for preemption events is rare, but given that it can and does happen it makes sense to check that it's not already preempted before beginning the long poll to wait for a change to the metadata (which won't happen in that case).

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense.

HttpResponse initialResponse = initialRequest.execute();
final String initialResult = IOUtils.toString(initialResponse.getContent(), Charsets.UTF_8);
initialResponse.disconnect();
if ("TRUE".equals(initialResult)) {
listener
.getLogger()
.println("Instance was already preempted before monitoring metadata changes.");
return true;
}

HttpRequest request = createMetadataRequest(true);
listener.getLogger().println("Preemptive instance, listening to metadata for preemption event");
HttpResponse response = request.execute();
final String result = IOUtils.toString(response.getContent(), Charsets.UTF_8);
Expand All @@ -67,12 +78,16 @@ public Boolean call() throws IOException {
return "TRUE".equals(result);
}

private HttpRequest createMetadataRequest() throws IOException {
private HttpRequest createMetadataRequest(boolean waitForChange) throws IOException {
HttpTransport transport = new NetHttpTransport();
GenericUrl metadata = new GenericUrl(METADATA_SERVER_URL);
GenericUrl metadata = new GenericUrl(getMetadataServerUrl(waitForChange));
HttpRequest request = transport.createRequestFactory().buildGetRequest(metadata);
request.setHeaders(new HttpHeaders().set("Metadata-Flavor", "Google"));
request.setReadTimeout(Integer.MAX_VALUE);
return request;
}

private static String getMetadataServerUrl(boolean waitForChange) {
return String.format(METADATA_SERVER_URL, waitForChange);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.google.jenkins.plugins.computeengine.integration;

import lombok.Getter;

/**
* An enum defining commands that can be run on both linux and windows agents, with corresponding
* String format values for each platform.
*/
@Getter
public enum Commands {
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing javadoc header.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

ECHO("echo %s", "echo %s"),
EXIT("exit /b %s", "exit %s"),
SLEEP("ping /n %s /l 1024 google.com ", "sleep %s");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this is a little weird. I was not able to get the proper windows equivalent "timeout" to work. Looking around I found that there are some weird issues with running the command in non-interactive scripts, discussed in places such as this comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense. Is using Powershell a viable alternative to batch? If so, we could consider using -NonInteractive: https://docs.microsoft.com/en-us/powershell/module/Microsoft.PowerShell.Core/About/about_PowerShell_exe?view=powershell-5.1#-noninteractive

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the tip, while I had trouble using powershell within the existing batch scripts, this led me to finding the Powershell plugin which automatically applies the NonInteractive flag when running scripts, and I was able to remove this hack.


private String windows;
private String linux;

Commands(String windows, String linux) {
this.windows = windows;
this.linux = linux;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.google.jenkins.plugins.computeengine.integration;

import static com.google.jenkins.plugins.computeengine.integration.ITUtil.DEB_JAVA_STARTUP_SCRIPT;
import static com.google.jenkins.plugins.computeengine.integration.ITUtil.LABEL;
import static com.google.jenkins.plugins.computeengine.integration.ITUtil.NULL_TEMPLATE;
import static com.google.jenkins.plugins.computeengine.integration.ITUtil.PROJECT_ID;
Expand Down Expand Up @@ -79,7 +78,6 @@ public static void init() throws Exception {
cloud.setConfigurations(
ImmutableList.of(
instanceConfigurationBuilder()
.startupScript(DEB_JAVA_STARTUP_SCRIPT)
.numExecutorsStr(MULTIPLE_NUM_EXECUTORS)
.labels(LABEL)
.oneShot(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@

package com.google.jenkins.plugins.computeengine.integration;

import static com.google.jenkins.plugins.computeengine.integration.ITUtil.DEB_JAVA_STARTUP_SCRIPT;
import static com.google.jenkins.plugins.computeengine.integration.ITUtil.LABEL;
import static com.google.jenkins.plugins.computeengine.integration.ITUtil.NULL_TEMPLATE;
import static com.google.jenkins.plugins.computeengine.integration.ITUtil.NUM_EXECUTORS;
import static com.google.jenkins.plugins.computeengine.integration.ITUtil.TEST_TIMEOUT_MULTIPLIER;
import static com.google.jenkins.plugins.computeengine.integration.ITUtil.execute;
import static com.google.jenkins.plugins.computeengine.integration.ITUtil.getLabel;
import static com.google.jenkins.plugins.computeengine.integration.ITUtil.initClient;
import static com.google.jenkins.plugins.computeengine.integration.ITUtil.initCloud;
Expand All @@ -41,7 +41,6 @@
import hudson.model.Node;
import hudson.model.labels.LabelAtom;
import hudson.tasks.Builder;
import hudson.tasks.Shell;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.Future;
Expand Down Expand Up @@ -82,7 +81,6 @@ public static void init() throws Exception {

InstanceConfiguration instanceConfiguration =
instanceConfigurationBuilder()
.startupScript(DEB_JAVA_STARTUP_SCRIPT)
.numExecutorsStr(NUM_EXECUTORS)
.labels(LABEL)
.template(NULL_TEMPLATE)
Expand All @@ -102,7 +100,7 @@ public static void init() throws Exception {
.isIgnoreProxy());

project = jenkinsRule.createFreeStyleProject();
Builder step = new Shell("echo works");
Builder step = execute(Commands.ECHO, "works");
project.getBuildersList().add(step);
project.setAssignedLabel(new LabelAtom(LABEL));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.google.jenkins.plugins.computeengine.integration;

import static com.google.jenkins.plugins.computeengine.integration.ITUtil.DEB_JAVA_STARTUP_SCRIPT;
import static com.google.jenkins.plugins.computeengine.integration.ITUtil.LABEL;
import static com.google.jenkins.plugins.computeengine.integration.ITUtil.NULL_TEMPLATE;
import static com.google.jenkins.plugins.computeengine.integration.ITUtil.NUM_EXECUTORS;
Expand Down Expand Up @@ -78,7 +77,6 @@ public static void init() throws Exception {
cloud.setConfigurations(
ImmutableList.of(
instanceConfigurationBuilder()
.startupScript(DEB_JAVA_STARTUP_SCRIPT)
.numExecutorsStr(NUM_EXECUTORS)
.labels(MULTIPLE_LABEL)
.oneShot(false)
Expand Down
Loading