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 all 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,6 +18,7 @@

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 com.google.api.services.compute.model.AcceleratorConfig;
import com.google.api.services.compute.model.AttachedDisk;
Expand All @@ -37,11 +38,15 @@
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.Standard;
import edu.umd.cs.findbugs.annotations.Nullable;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Extension;
Expand Down Expand Up @@ -120,7 +125,7 @@
private String machineType;
private String numExecutorsStr;
private String startupScript;
private boolean preemptible;
private ProvisioningType provisioningType;
gbhat618 marked this conversation as resolved.
Show resolved Hide resolved
private String minCpuPlatform;
private String labels;
private String runAsUser;
Expand Down Expand Up @@ -167,6 +172,10 @@
@Setter(AccessLevel.PROTECTED)
protected transient ComputeEngineCloud cloud;

/** @deprecated Use {@link #provisioningType} instead. */
@SuppressWarnings("DeprecatedIsStillUsed")
private transient boolean preemptible;

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

View check run for this annotation

ci.jenkins.io / Java Compiler

compiler:compile

NORMAL: deprecated item is not annotated with @deprecated

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

View check run for this annotation

ci.jenkins.io / JavaDoc

JavaDoc @Deprecated

NORMAL: [dep-ann] deprecated item is not annotated with @deprecated

private static List<Metadata.Items> mergeMetadataItems(List<Metadata.Items> winner, List<Metadata.Items> loser) {
if (loser == null) {
loser = new ArrayList<Metadata.Items>();
Expand Down Expand Up @@ -236,6 +245,19 @@
this.createSnapshot = createSnapshot && this.oneShot;
}

/**
* Required for JCasC Compatibility,
Copy link
Member

Choose a reason for hiding this comment

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

JCasC never guarantees backwards compatibility, and users should be aware of that. IMO fix the test instead.

* Without this setter explicitly defined, the {@code ConfigAsCodeTest} test fails as `preemptible` is deprecated.
* Also see,
* <a href="https://github.com/jenkinsci/configuration-as-code-plugin/blob/master/docs/REQUIREMENTS.md#rule-1-dont-write-code-for-data-binding"></a>
*/
@DataBoundSetter
public void setPreemptible(boolean preemptible) {
if (preemptible) {
this.provisioningType = new PreemptibleVm();
}
}

public static Integer intOrDefault(String toParse, Integer defaultTo) {
Integer toReturn;
try {
Expand Down Expand Up @@ -288,6 +310,13 @@
return launchTimeoutSeconds * 1000;
}

/**
* This getter is only for backward compatibility for `preemptible` field.
*/
public boolean getPreemptible() {
return provisioningType != null && provisioningType.getValue() == PREEMPTIBLE;

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

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 317 is only partially covered, 2 branches are missing
}

public void appendLabels(Map<String, String> labels) {
if (googleLabels == null) {
googleLabels = new HashMap<>();
Expand Down Expand Up @@ -357,6 +386,10 @@
this.networkInterfaceIpStackMode = new NetworkInterfaceSingleStack(externalAddress);
this.externalAddress = null;
}
/* deprecating `preemptible` in favor of extensible `provisioningType` */
if (preemptible && provisioningType == null) {
provisioningType = new PreemptibleVm();
}
return this;
}

Expand Down Expand Up @@ -492,9 +525,13 @@
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);
if (provisioningType == null) {
return scheduling;
}
provisioningType.configure(scheduling);
return scheduling;
}

Expand Down Expand Up @@ -590,359 +627,369 @@
return SshConfiguration.builder().customPrivateKeyCredentialsId("").build();
}

@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 ProvisioningType defaultProvisioningType() {
return new Standard();
}

public static NetworkConfiguration defaultNetworkConfiguration() {
return new AutofilledNetworkConfiguration();
}

private static ComputeClient computeClient(Jenkins context, String credentialsId) throws IOException {
if (computeClient != null) {
return computeClient;
}
ClientFactory clientFactory = ClientUtil.getClientFactory(context, credentialsId);
return clientFactory.computeClient();
}

@Override
public String getHelpFile(String fieldName) {
String p = super.getHelpFile(fieldName);
if (p == null) {
Descriptor d = Jenkins.get().getDescriptor(ComputeEngineInstance.class);
if (d != null) p = d.getHelpFile(fieldName);
}
return p;
}

public List<NetworkConfiguration.NetworkConfigurationDescriptor> getNetworkConfigurationDescriptors() {
List<NetworkConfiguration.NetworkConfigurationDescriptor> d =
Jenkins.get().getDescriptorList(NetworkConfiguration.class);
// No deprecated regions
Iterator it = d.iterator();
while (it.hasNext()) {
NetworkConfiguration.NetworkConfigurationDescriptor o =
(NetworkConfiguration.NetworkConfigurationDescriptor) it.next();
if (o.clazz.getName().equals("NetworkConfiguration")) {
it.remove();
}
}
return d;
}

public FormValidation doCheckNetworkTags(@QueryParameter String value) {
if (value == null || value.isEmpty()) {
return FormValidation.ok();
}

String re = "[a-z]([-a-z0-9]*[a-z0-9])?";
for (String tag : value.split(" ")) {
if (!tag.matches(re)) {
return FormValidation.error("Tags must be space-delimited and each tag must match regex" + re);
}
}

return FormValidation.ok();
}

public FormValidation doCheckNamePrefix(@QueryParameter String value) {
if (value == null || value.isEmpty()) {
return FormValidation.error("A prefix is required");
}

String re = "[a-z]([-a-z0-9]*[a-z0-9])?";
if (!value.matches(re)) {
return FormValidation.error("Prefix must match regex " + re);
}

Integer maxLen = 50;
if (value.length() > maxLen) {
return FormValidation.error("Maximum length is " + maxLen);
}
return FormValidation.ok();
}

public FormValidation doCheckDescription(@QueryParameter String value) {
if (value == null || value.isEmpty()) {
return FormValidation.error("A description is required");
}
return FormValidation.ok();
}

public ListBoxModel doFillRegionItems(
@AncestorInPath Jenkins context,
@QueryParameter("projectId") @RelativePath("..") final String projectId,
@QueryParameter("credentialsId") @RelativePath("..") final String credentialsId) {
checkPermissions(Jenkins.get(), Jenkins.ADMINISTER);
ListBoxModel items = new ListBoxModel();
items.add("");
try {
ComputeClient compute = computeClient(context, credentialsId);
List<Region> regions = compute.listRegions(projectId);

for (Region r : regions) {
items.add(r.getName(), r.getSelfLink());
}
return items;
} catch (IOException ioe) {
items.clear();
items.add("Error retrieving regions");
return items;
}
}

public ListBoxModel doFillTemplateItems(
@AncestorInPath Jenkins context,
@QueryParameter("projectId") @RelativePath("..") final String projectId,
@QueryParameter("credentialsId") @RelativePath("..") final String credentialsId) {
checkPermissions(Jenkins.get(), Jenkins.ADMINISTER);
ListBoxModel items = new ListBoxModel();
items.add("");
try {
ComputeClient compute = computeClient(context, credentialsId);
List<InstanceTemplate> instanceTemplates = compute.listTemplates(projectId);

for (InstanceTemplate instanceTemplate : instanceTemplates) {
items.add(instanceTemplate.getName(), instanceTemplate.getSelfLink());
}
return items;
} catch (IOException ioe) {
items.clear();
items.add("Error retrieving instanceTemplates");
return items;
}
}

public FormValidation doCheckRegion(@QueryParameter String value) {
if (StringUtils.isEmpty(value)) {
return FormValidation.error("Please select a region...");
}
return FormValidation.ok();
}

public ListBoxModel doFillZoneItems(
@AncestorInPath Jenkins context,
@QueryParameter("projectId") @RelativePath("..") final String projectId,
@QueryParameter("region") final String region,
@QueryParameter("credentialsId") @RelativePath("..") final String credentialsId) {
checkPermissions(Jenkins.get(), Jenkins.ADMINISTER);
ListBoxModel items = new ListBoxModel();
items.add("");
try {
ComputeClient compute = computeClient(context, credentialsId);
List<Zone> zones = compute.listZones(projectId, region);

for (Zone z : zones) {
items.add(z.getName(), z.getSelfLink());
}
return items;
} catch (IOException ioe) {
items.clear();
items.add("Error retrieving zones");
return items;
} catch (IllegalArgumentException iae) {
// TODO log
return null;
}
}

public FormValidation doCheckZone(@QueryParameter String value) {
if (StringUtils.isEmpty(value)) {
return FormValidation.error("Please select a zone...");
}
return FormValidation.ok();
}

public ListBoxModel doFillMachineTypeItems(
@AncestorInPath Jenkins context,
@QueryParameter("projectId") @RelativePath("..") final String projectId,
@QueryParameter("zone") final String zone,
@QueryParameter("credentialsId") @RelativePath("..") final String credentialsId) {
checkPermissions(Jenkins.get(), Jenkins.ADMINISTER);
ListBoxModel items = new ListBoxModel();
items.add("");
try {
ComputeClient compute = computeClient(context, credentialsId);
List<MachineType> machineTypes = compute.listMachineTypes(projectId, zone);

for (MachineType m : machineTypes) {
items.add(m.getName(), m.getSelfLink());
}
return items;
} catch (IOException ioe) {
items.clear();
items.add("Error retrieving machine types");
return items;
} catch (IllegalArgumentException iae) {
// TODO log
return null;
}
}

public FormValidation doCheckMachineType(@QueryParameter String value) {
if (StringUtils.isEmpty(value)) {
return FormValidation.error("Please select a machine type...");
}
return FormValidation.ok();
}

public ListBoxModel doFillMinCpuPlatformItems(
@AncestorInPath Jenkins context,
@QueryParameter("projectId") @RelativePath("..") final String projectId,
@QueryParameter("zone") final String zone,
@QueryParameter("credentialsId") @RelativePath("..") final String credentialsId) {
checkPermissions(Jenkins.get(), Jenkins.ADMINISTER);
ListBoxModel items = new ListBoxModel();
items.add("");
try {
ComputeClient compute = computeClient(context, credentialsId);
List<String> cpuPlatforms = compute.listCpuPlatforms(projectId, zone);

for (String cpuPlatform : cpuPlatforms) {
items.add(cpuPlatform);
}
return items;
} catch (IOException ioe) {
items.clear();
items.add("Error retrieving cpu Platforms");
return items;
} catch (IllegalArgumentException iae) {
// TODO log
return null;
}
}

public ListBoxModel doFillBootDiskTypeItems(
@AncestorInPath Jenkins context,
@QueryParameter("projectId") @RelativePath("..") final String projectId,
@QueryParameter("zone") String zone,
@QueryParameter("credentialsId") @RelativePath("..") final String credentialsId) {
checkPermissions(Jenkins.get(), Jenkins.ADMINISTER);
ListBoxModel items = new ListBoxModel();
try {
ComputeClient compute = computeClient(context, credentialsId);
List<DiskType> diskTypes = compute.listBootDiskTypes(projectId, zone);

for (DiskType dt : diskTypes) {
items.add(dt.getName(), dt.getSelfLink());
}
return items;
} catch (IOException ioe) {
items.clear();
items.add("Error retrieving disk types");
return items;
} catch (IllegalArgumentException iae) {
// TODO: log
return null;
}
}

public ListBoxModel doFillBootDiskSourceImageProjectItems(
@AncestorInPath Jenkins context,
@QueryParameter("projectId") @RelativePath("..") final String projectId) {
checkPermissions(Jenkins.get(), Jenkins.ADMINISTER);
ListBoxModel items = new ListBoxModel();
items.add("");
items.add(projectId);
for (String v : KNOWN_IMAGE_PROJECTS) {
items.add(v);
}
return items;
}

public FormValidation doCheckBootDiskSourceImageProject(@QueryParameter String value) {
if (StringUtils.isEmpty(value)) {
return FormValidation.warning("Please select source image project...");
}
return FormValidation.ok();
}

public ListBoxModel doFillBootDiskSourceImageNameItems(
@AncestorInPath Jenkins context,
@QueryParameter("bootDiskSourceImageProject") final String projectId,
@QueryParameter("credentialsId") @RelativePath("..") final String credentialsId) {
checkPermissions(Jenkins.get(), Jenkins.ADMINISTER);
ListBoxModel items = new ListBoxModel();
items.add("");
try {
ComputeClient compute = computeClient(context, credentialsId);
List<Image> images = compute.listImages(projectId);

for (Image i : images) {
items.add(i.getName(), i.getSelfLink());
}
} catch (IOException ioe) {
items.clear();
items.add("Error retrieving images for project");
} catch (IllegalArgumentException iae) {
// TODO: log
return null;
}
return items;
}

public FormValidation doCheckBootDiskSourceImageName(@QueryParameter String value) {
if (StringUtils.isEmpty(value)) {
return FormValidation.warning("Please select source image...");
}
return FormValidation.ok();
}

public FormValidation doCheckBootDiskSizeGbStr(
@AncestorInPath Jenkins context,
@QueryParameter String value,
@QueryParameter("bootDiskSourceImageProject") final String projectId,
@QueryParameter("bootDiskSourceImageName") final String imageName,
@QueryParameter("credentialsId") @RelativePath("..") final String credentialsId) {
checkPermissions(Jenkins.get(), Jenkins.ADMINISTER);
if (Strings.isNullOrEmpty(credentialsId)
|| Strings.isNullOrEmpty(projectId)
|| Strings.isNullOrEmpty(imageName)) return FormValidation.ok();

try {
ComputeClient compute = computeClient(context, credentialsId);
Image i = compute.getImage(nameFromSelfLink(projectId), nameFromSelfLink(imageName));
if (i == null) return FormValidation.error("Could not find image " + imageName);
Long bootDiskSizeGb = Long.parseLong(value);
if (bootDiskSizeGb < i.getDiskSizeGb()) {
return FormValidation.error(String.format(
"The disk image you have chosen requires a minimum of %dGB. Please increase boot disk size to accommodate.",
i.getDiskSizeGb()));
}
} catch (IOException ioe) {
return FormValidation.error(ioe, "Error validating boot disk size");
}
return FormValidation.ok();
}

public FormValidation doCheckLabelString(@QueryParameter String value, @QueryParameter Node.Mode mode) {
if (mode == Node.Mode.EXCLUSIVE && (value == null || value.trim().isEmpty())) {
return FormValidation.warning("You may want to assign labels to this node;"
+ " it's marked to only run jobs that are exclusively tied to itself or a label.");
}
return FormValidation.ok();
}

public FormValidation doCheckCreateSnapshot(
@AncestorInPath Jenkins context,
@QueryParameter boolean value,
@QueryParameter("oneShot") boolean oneShot) {
if (!oneShot && value) {
return FormValidation.error(Messages.InstanceConfiguration_SnapshotConfigError());
}
return FormValidation.ok();
}

public FormValidation doCheckNumExecutorsStr(
@AncestorInPath Jenkins context,
@QueryParameter String value,
@QueryParameter("oneShot") boolean oneShot) {
int numExecutors = intOrDefault(value, DEFAULT_NUM_EXECUTORS);
if (numExecutors < 1) {
return FormValidation.error(Messages.InstanceConfiguration_NumExecutorsLessThanOneConfigError());
} else if (numExecutors > 1 && oneShot) {
return FormValidation.error(Messages.InstanceConfiguration_NumExecutorsOneShotError());
}
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 990 in src/main/java/com/google/jenkins/plugins/computeengine/InstanceConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 632-990 are not covered by tests
}

public List<NetworkInterfaceIpStackMode.Descriptor> getNetworkInterfaceIpStackModeDescriptors() {
return ExtensionList.lookup(NetworkInterfaceIpStackMode.Descriptor.class);
}
Expand All @@ -958,7 +1005,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,48 @@
/*
* 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 com.google.api.services.compute.model.Scheduling;
import hudson.Extension;
import org.kohsuke.stapler.DataBoundConstructor;

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

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

@Override
public void configure(Scheduling scheduling) {
scheduling.setPreemptible(true);
}

@Extension
public static class DescriptorImpl extends ProvisioningTypeDescriptor {
@Override
public String getDisplayName() {
return "Preemptible VM";
}

@Override
public boolean isMaxRunDurationSupported() {
return false;

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

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 45 is not covered by tests
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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 com.google.api.client.json.GenericJson;
import com.google.api.services.compute.model.Scheduling;
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 44 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 43-44 are not covered by tests

protected void configureMaxRunDuration(Scheduling scheduling, long maxRunDurationSeconds) {
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");
}
}

public abstract void configure(Scheduling scheduling);

public abstract static class ProvisioningTypeDescriptor extends Descriptor<ProvisioningType> {

@SuppressWarnings("unused")
public abstract boolean isMaxRunDurationSupported();
}
}
Loading
Loading