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

Add spotVM and maxRunDuration feature to VM provisioning #492

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
67 changes: 67 additions & 0 deletions docs/integration-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Integration Tests

* GCP Project
* Create a service account with relevant access - See [Refer to IAM Credentials](Home.md#iam-credentials)
* Most of the tests require a VM with Java pre-installed at `/usr/bin/java`.
There are one or two which don't require java pre-installed, as they supply `startup-script` to gcloud apis, and install `java` on a plain linux Debian image.
* You can create an image with `java` preinstalled as,
```bash
project=<your-project>
zone=<your-zone>

# Create a debian based VM
gcloud compute instances create java-install-instance \
--project=$project \
--zone=$zone \
--machine-type=e2-medium \
--image-project=debian-cloud \
--image-family=debian-12

# Wait for the machine to start and access ssh connections. Install java via ssh
gcloud compute ssh java-install-instance \
--project=$project \
--zone=$zone \
--command="sudo apt-get update && sudo apt-get install -y openjdk-17-jdk"

# Ensure java is installed and print the java path
gcloud compute ssh java-install-instance \
--project=$project \
--zone=$zone \
--command="java -version"

gcloud compute ssh java-install-instance \
--project=$project \
--zone=$zone \
--command="which java"

# For creating image, you need to first stop the VM
gcloud compute instances stop java-install-instance \
--project=$project \
--zone=$zone

# Create an image from the VM
gcloud compute images create java-debian-12-image \
--source-disk=java-install-instance \
--source-disk-zone=$zone \
--project=$project \
--family=custom-java-debian-family

# Delete the VM
gcloud compute instances delete java-install-instance \
--project=$project \
--zone=$zone
```
* Export these environment variables
```bash
export GOOGLE_PROJECT_ID=<your-project>
export GOOGLE_SA_NAME=<name of the SA created in first step>
export GOOGLE_CREDENTIALS_FILE=<full path to the SA JSON file>
export GOOGLE_ZONE=<your-compute-zone>
export GOOGLE_REGION=<your-compute-region>
export GOOGLE_BOOT_DISK_PROJECT_ID=<your-project>
export GOOGLE_BOOT_DISK_IMAGE_NAME=java-debian-12-image # this is created in previous step
```
* Execute an integration test (example)
```bash
mvn clean test -Dtest=ComputeEngineCloudRestartPreemptedIT#testIfNodeWasPreempted
```
16 changes: 15 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,21 @@
<version>${powershell.version}</version>
<scope>test</scope>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-durable-task-step</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-job</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@

import static com.google.cloud.graphite.platforms.plugin.client.util.ClientUtil.nameFromSelfLink;
import static com.google.jenkins.plugins.computeengine.ComputeEngineCloud.checkPermissions;
import static com.google.jenkins.plugins.computeengine.ui.helpers.ProvisioningTypeValue.PREEMPTIBLE;
import static com.google.jenkins.plugins.computeengine.ui.helpers.ProvisioningTypeValue.SPOT;

import com.google.api.client.json.GenericJson;
import com.google.api.services.compute.model.AcceleratorConfig;
import com.google.api.services.compute.model.AttachedDisk;
import com.google.api.services.compute.model.AttachedDiskInitializeParams;
Expand All @@ -37,11 +40,17 @@
import com.google.api.services.compute.model.Zone;
import com.google.cloud.graphite.platforms.plugin.client.ClientFactory;
import com.google.cloud.graphite.platforms.plugin.client.ComputeClient;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.jenkins.plugins.computeengine.client.ClientUtil;
import com.google.jenkins.plugins.computeengine.ssh.GoogleKeyCredential;
import com.google.jenkins.plugins.computeengine.ssh.GoogleKeyPair;
import com.google.jenkins.plugins.computeengine.ssh.GooglePrivateKey;
import com.google.jenkins.plugins.computeengine.ui.helpers.PreemptibleVm;
import com.google.jenkins.plugins.computeengine.ui.helpers.ProvisioningType;
import com.google.jenkins.plugins.computeengine.ui.helpers.ProvisioningTypeValue;
import com.google.jenkins.plugins.computeengine.ui.helpers.SpotVm;
import com.google.jenkins.plugins.computeengine.ui.helpers.Standard;
import edu.umd.cs.findbugs.annotations.Nullable;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Extension;
Expand Down Expand Up @@ -120,7 +129,15 @@
private String machineType;
private String numExecutorsStr;
private String startupScript;
/**
* `preemptible` is no more exposed in the UI, however we are still keeping it in here, just to provide
* compatibility with old configurations. Use the {@link #provisioningType} field instead where
* {@link com.google.jenkins.plugins.computeengine.ui.helpers.PreemptibleVm} is used for preemptible instances.
* We are not marking it as {@code @Deprecated} because then CasC will result in ConfiguratorException.
*/
private boolean preemptible;

private ProvisioningType provisioningType;

Check warning on line 140 in src/main/java/com/google/jenkins/plugins/computeengine/InstanceConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 140 is not covered by tests
gbhat618 marked this conversation as resolved.
Show resolved Hide resolved
private String minCpuPlatform;
private String labels;
private String runAsUser;
Expand Down Expand Up @@ -492,9 +509,36 @@
return null;
}

private Scheduling scheduling() {
@VisibleForTesting
Scheduling scheduling() {
gbhat618 marked this conversation as resolved.
Show resolved Hide resolved
Scheduling scheduling = new Scheduling();
scheduling.setPreemptible(preemptible);
long maxRunDurationSeconds = 0;
if (provisioningType != null) { // check `null` for backward compatibility
ProvisioningTypeValue ptValue = provisioningType.getValue();
if (ptValue == PREEMPTIBLE) {
gbhat618 marked this conversation as resolved.
Show resolved Hide resolved
scheduling.setPreemptible(true);
} else if (provisioningType.getValue() == SPOT) {
maxRunDurationSeconds = ((SpotVm) provisioningType).getMaxRunDurationSeconds();
scheduling.setProvisioningModel("SPOT");
} else {
maxRunDurationSeconds = ((Standard) provisioningType).getMaxRunDurationSeconds();
}
if (maxRunDurationSeconds > 0) {
GenericJson j = new GenericJson();
j.set("seconds", maxRunDurationSeconds);
scheduling.set("maxRunDuration", j);
/* Note: Only the instance is set to delete here, not the disk. Disk deletion is based on the
`bootDiskAutoDelete` config value. For instance termination at `maxRunDuration`, GCP supports two
termination actions: DELETE and STOP.
For Jenkins agents, DELETE is more appropriate. If the agent instance is needed again, it can be
recreated using the disk, which should have been anticipated and disk should be set to not delete in
`bootDiskAutoDelete`.
*/
scheduling.setInstanceTerminationAction("DELETE");
}
} else if (preemptible) { // keeping the check for `preemptible` for backward compatibility
scheduling.setPreemptible(true);
} // else: standard provisioning
return scheduling;
}

Expand Down Expand Up @@ -551,6 +595,11 @@
}
}

@SuppressWarnings("unused")
public ProvisioningType defaultProvisioningType() {
return this.preemptible ? new PreemptibleVm() : new Standard();
gbhat618 marked this conversation as resolved.
Show resolved Hide resolved
}

@Extension
public static final class DescriptorImpl extends Descriptor<InstanceConfiguration> {
private static ComputeClient computeClient;
Expand Down Expand Up @@ -943,6 +992,11 @@
return FormValidation.ok();
}

@SuppressWarnings("unused")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
@SuppressWarnings("unused")
@SuppressWarnings("unused") // jelly

public List<ProvisioningType.ProvisioningTypeDescriptor> getProvisioningTypes() {
return ExtensionList.lookup(ProvisioningType.ProvisioningTypeDescriptor.class);

Check warning on line 997 in src/main/java/com/google/jenkins/plugins/computeengine/InstanceConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 997 is not covered by tests
}

public List<NetworkInterfaceIpStackMode.Descriptor> getNetworkInterfaceIpStackModeDescriptors() {
return ExtensionList.lookup(NetworkInterfaceIpStackMode.Descriptor.class);
}
Expand All @@ -958,7 +1012,9 @@
instanceConfiguration.setMachineType(this.machineType);
instanceConfiguration.setNumExecutorsStr(this.numExecutorsStr);
instanceConfiguration.setStartupScript(this.startupScript);
// even though `preemptible` is deprecated, we still set it here for backward compatibility
instanceConfiguration.setPreemptible(this.preemptible);
instanceConfiguration.setProvisioningType(this.provisioningType);
instanceConfiguration.setMinCpuPlatform(this.minCpuPlatform);
instanceConfiguration.setLabelString(this.labels);
instanceConfiguration.setRunAsUser(this.runAsUser);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.google.jenkins.plugins.credentials.oauth.GoogleOAuth2Credentials;
import com.google.jenkins.plugins.credentials.oauth.GoogleRobotCredentials;
import hudson.AbortException;
import hudson.Main;
import hudson.model.ItemGroup;
import hudson.security.ACL;
import java.io.IOException;
Expand Down Expand Up @@ -73,6 +74,20 @@
ItemGroup itemGroup, List<DomainRequirement> domainRequirements, String credentialsId)
throws AbortException {

/* During the integration tests, the parameter `credentialId`=<Project-Id> that we have set during
integration test. But the actual credential created within Jenkins is having `id` as a random UUID.

So the `CredentialsMatchers.firstOrNull` was returning `null` due to `CredentialsMatchers.withId(credentialsId)`
*/
if (Main.isUnitTest) {

Check warning on line 82 in src/main/java/com/google/jenkins/plugins/computeengine/client/ClientUtil.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 82 is only partially covered, one branch is missing
var credentialList = CredentialsProvider.lookupCredentials(
GoogleOAuth2Credentials.class, itemGroup, ACL.SYSTEM, domainRequirements);
if (!credentialList.isEmpty()) {

Check warning on line 85 in src/main/java/com/google/jenkins/plugins/computeengine/client/ClientUtil.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 85 is only partially covered, one branch is missing
return (GoogleRobotCredentials) credentialList.get(0);
}
return null;

Check warning on line 88 in src/main/java/com/google/jenkins/plugins/computeengine/client/ClientUtil.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 88 is not covered by tests
}

Copy link
Author

Choose a reason for hiding this comment

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

The test credentials are created using this constructor, where it is passing an empty id.
https://github.com/jenkinsci/google-oauth-plugin/blob/5367a8a1b9c97e14a5188150f897fa91da1d9777/src/main/java/com/google/jenkins/plugins/credentials/oauth/GoogleRobotPrivateKeyCredentials.java#L65-L70

which ends up at https://github.com/jenkinsci/credentials-plugin/blob/1e306e8f6c54f8eeef973b3387b8c223bd60ae1c/src/main/java/com/cloudbees/plugins/credentials/common/IdCredentials.java#L87-L96
thus having a random uuid.

seems like at somepoint the google-credential-plugin might have got this behavior (or sending blank id, instead of projectid).

As the integration tests in this repository are not run often, this never got brought up..

GoogleOAuth2Credentials credentials = CredentialsMatchers.firstOrNull(
CredentialsProvider.lookupCredentials(
GoogleOAuth2Credentials.class, itemGroup, ACL.SYSTEM, domainRequirements),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2024 CloudBees, 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.jenkins.plugins.computeengine.ui.helpers;

import hudson.Extension;
import org.kohsuke.stapler.DataBoundConstructor;

@SuppressWarnings("unused")
public class PreemptibleVm extends ProvisioningType {

@DataBoundConstructor
public PreemptibleVm() {
super(ProvisioningTypeValue.PREEMPTIBLE);
}

@Extension
public static class DescriptorImpl extends ProvisioningTypeDescriptor {
@Override
public String getDisplayName() {
return "Preemptible VM";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2024 CloudBees, 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.jenkins.plugins.computeengine.ui.helpers;

import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import org.kohsuke.stapler.DataBoundSetter;

/**
* ProvisioningType represents the type of VM to be provisioned.
*/
public abstract class ProvisioningType extends AbstractDescribableImpl<ProvisioningType> {

private ProvisioningTypeValue value;

public ProvisioningType(ProvisioningTypeValue value) {
this.value = value;
}

public ProvisioningTypeValue getValue() {
return value;
}

@SuppressWarnings("unused")
@DataBoundSetter
public void setProvisioningTypeValue(ProvisioningTypeValue value) {
this.value = value;
}

Check warning on line 42 in src/main/java/com/google/jenkins/plugins/computeengine/ui/helpers/ProvisioningType.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 41-42 are not covered by tests

public abstract static class ProvisioningTypeDescriptor extends Descriptor<ProvisioningType> {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2024 CloudBees, 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.jenkins.plugins.computeengine.ui.helpers;

public enum ProvisioningTypeValue {
Copy link
Member

Choose a reason for hiding this comment

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

Using an enum here kind of defeats the purpose of having extensibility. I think it is not needed anyway as it is only used for casc compatibility which is not usually kept as per Bobby. But just in case you face this use case in the future, I would recommend either adding a specific check method returning boolean on the base implementation, or a marker interface that PreemptibleVm could implement

STANDARD,
SPOT,
PREEMPTIBLE;
}
Loading
Loading