From 20786a92c6b59c7f7a2d022b78effe039d8e4dd0 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Thu, 14 Dec 2023 11:28:55 -0800 Subject: [PATCH] Use Jenkins project-wide formatter configuration (#366) --- pom.xml | 295 ++--- .../plugins/k8sengine/ClusterUtil.java | 100 +- .../plugins/k8sengine/CredentialsUtil.java | 216 ++- .../jenkins/plugins/k8sengine/KubeConfig.java | 362 ++--- .../plugins/k8sengine/KubectlWrapper.java | 418 +++--- .../k8sengine/KubernetesEngineBuilder.java | 1159 ++++++++--------- .../k8sengine/KubernetesVerifiers.java | 351 +++-- .../jenkins/plugins/k8sengine/Manifests.java | 349 ++--- .../plugins/k8sengine/VerificationTask.java | 202 ++- .../plugins/k8sengine/client/ClientUtil.java | 89 +- .../client/ContainerScopeRequirement.java | 16 +- .../client/RetryHttpInitializerWrapper.java | 104 +- .../plugins/k8sengine/ClusterUtilTest.java | 81 +- .../k8sengine/CredentialsUtilTest.java | 107 +- .../jenkins/plugins/k8sengine/ITUtil.java | 181 ++- .../plugins/k8sengine/KubeConfigTest.java | 226 ++-- .../KubernetesEngineBuilderClusterTest.java | 392 +++--- .../k8sengine/KubernetesEngineBuilderIT.java | 412 +++--- ...bernetesEngineBuilderMetricsLabelTest.java | 407 +++--- .../KubernetesEngineBuilderNamespaceTest.java | 70 +- .../KubernetesEngineBuilderPipelineIT.java | 358 +++-- .../KubernetesEngineBuilderTest.java | 532 ++++---- .../k8sengine/KubernetesVerifiersTest.java | 92 +- .../k8sengine/client/ClientUtilIT.java | 57 +- .../k8sengine/client/ClientUtilTest.java | 98 +- 25 files changed, 3206 insertions(+), 3468 deletions(-) diff --git a/pom.xml b/pom.xml index 238a2481..2cfdc1bf 100644 --- a/pom.xml +++ b/pom.xml @@ -14,26 +14,26 @@ --> 4.0.0 - org.jenkins-ci.plugins plugin - 4.72 - + 4.76 + google-kubernetes-engine ${revision}.${changelist} hpi - Google Kubernetes Engine Plugin This plugin allows Jenkins to publish build artifacts to Kubernetes clusters running on Google Kubernetes Engine. https://github.com/jenkinsci/google-kubernetes-engine-plugin + Google https://www.google.com + Apache License, Version 2.0 @@ -58,8 +58,8 @@ scm:git:https://github.com/${gitHubRepo}.git scm:git:git@github.com:${gitHubRepo}.git - https://github.com/${gitHubRepo} ${scmTag} + https://github.com/${gitHubRepo} @@ -72,15 +72,11 @@ 572 93 0.3.0 + false - - com.google.code.gson - gson - 2.10.1 - io.jenkins.tools.bom bom-2.387.x @@ -88,74 +84,58 @@ pom import + + com.google.code.gson + gson + 2.10.1 + + - org.jenkins-ci.plugins - credentials - - - org.jenkins-ci.plugins - structs - - - org.jenkins-ci.plugins - display-url-api - - - org.jenkins-ci.plugins - apache-httpcomponents-client-4-api - - - org.jenkins-ci.plugins - jackson2-api - - - org.jenkins-ci.plugins.workflow - workflow-step-api - - - org.jenkinsci.plugins - pipeline-model-definition - test - - - org.jenkins-ci.plugins - git - - - org.jenkins-ci.plugins - google-oauth-plugin - - - org.mockito - mockito-core - test - - - junit - junit - test + com.google.apis + google-api-services-cloudresourcemanager + v1-rev${cloudresourcemanager.revision}-${google.api.version} + + + com.google.api-client + google-api-client + + - org.jenkins-ci.plugins - junit + com.google.apis + google-api-services-container + v1-rev${container.revision}-${google.api.version} + + + com.google.api-client + google-api-client + + com.google.cloud.graphite gcp-client ${gcp-plugin-core.version} + + + com.google.api-client + google-api-client + + + + com.google.guava + guava + + com.google.http-client google-http-client ${google.http.version} - - org.apache.httpcomponents - httpclient - com.google.code.findbugs jsr305 @@ -165,6 +145,15 @@ com.google.guava guava + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpcore + @@ -172,6 +161,11 @@ google-http-client-jackson2 1.35.0 + + + com.fasterxml.jackson.core + jackson-core + com.google.http-client google-http-client @@ -183,31 +177,17 @@ google-oauth-client 1.34.1 - - com.google.http-client - google-http-client - com.google.guava guava + + com.google.http-client + google-http-client + - - com.google.apis - google-api-services-cloudresourcemanager - v1-rev${cloudresourcemanager.revision}-${google.api.version} - - - com.google.apis - google-api-services-container - v1-rev${container.revision}-${google.api.version} - - - io.jenkins.plugins - snakeyaml-api - com.jayway.jsonpath json-path @@ -220,6 +200,10 @@ + + io.jenkins.plugins + snakeyaml-api + io.projectreactor reactor-core @@ -230,12 +214,69 @@ reactor-extra 3.4.2 + + org.jenkins-ci.plugins + apache-httpcomponents-client-4-api + + + org.jenkins-ci.plugins + credentials + + + org.jenkins-ci.plugins + display-url-api + + + org.jenkins-ci.plugins + git + + + org.jenkins-ci.plugins + google-oauth-plugin + + + com.google.guava + guava + + + + + org.jenkins-ci.plugins + jackson2-api + + + org.jenkins-ci.plugins + junit + + + org.jenkins-ci.plugins + structs + + + org.jenkins-ci.plugins.workflow + workflow-step-api + + + junit + junit + test + org.jenkins-ci.plugins matrix-project test + + org.jenkinsci.plugins + pipeline-model-definition + test + + + org.mockito + mockito-core + test + @@ -244,6 +285,7 @@ https://repo.jenkins-ci.org/public/ + repo.jenkins-ci.org @@ -255,92 +297,12 @@ org.apache.maven.plugins - maven-javadoc-plugin - 3.2.0 - - public - 8 - - - - validate - validate - - javadoc - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - -Xlint:all - - - - au.com.acegi - xml-format-maven-plugin - 3.1.2 - - - validate - validate - - xml-format - - - - - pom.xml - - - - org.codehaus.mojo - tidy-maven-plugin - 1.1.0 - - - validate - validate - - pom - - - - - - org.apache.maven.plugins - maven-release-plugin - - xml-format:xml-format tidy:pom - - - - com.coveo - fmt-maven-plugin - 2.9 + maven-failsafe-plugin - src/main/java - src/test/java - true - .*\.java - false - false - + true + false + ${skipITs} - - - - format - - - - - - org.apache.maven.plugins - maven-failsafe-plugin @@ -349,11 +311,6 @@ - - true - false - ${skipITs} - maven-surefire-plugin @@ -367,7 +324,7 @@ 2.7 - + html diff --git a/src/main/java/com/google/jenkins/plugins/k8sengine/ClusterUtil.java b/src/main/java/com/google/jenkins/plugins/k8sengine/ClusterUtil.java index 42bbbede..8fc1575d 100644 --- a/src/main/java/com/google/jenkins/plugins/k8sengine/ClusterUtil.java +++ b/src/main/java/com/google/jenkins/plugins/k8sengine/ClusterUtil.java @@ -23,58 +23,58 @@ /** Utility functions for converting between {@link Cluster}s and their String representations. */ class ClusterUtil { - /** - * Given a GKE {@link Cluster} return a String representation containing the name and location. - * - * @param cluster The {@link Cluster} object to extract values from. - * @return A String of the form "name (location)" based on the given cluster's properties. - */ - static String toNameAndLocation(Cluster cluster) { - Preconditions.checkNotNull(cluster); - return toNameAndLocation(cluster.getName(), cluster.getLocation()); - } + /** + * Given a GKE {@link Cluster} return a String representation containing the name and location. + * + * @param cluster The {@link Cluster} object to extract values from. + * @return A String of the form "name (location)" based on the given cluster's properties. + */ + static String toNameAndLocation(Cluster cluster) { + Preconditions.checkNotNull(cluster); + return toNameAndLocation(cluster.getName(), cluster.getLocation()); + } - /** - * Given a name and location for a cluster, return the combined String representation. - * - * @param name A non-empty cluster name - * @param location A non-empty GCP resource location. - * @return A String of the form "name (location)". - */ - static String toNameAndLocation(String name, String location) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(name)); - Preconditions.checkArgument(!Strings.isNullOrEmpty(location)); - return String.format("%s (%s)", name, location); - } + /** + * Given a name and location for a cluster, return the combined String representation. + * + * @param name A non-empty cluster name + * @param location A non-empty GCP resource location. + * @return A String of the form "name (location)". + */ + static String toNameAndLocation(String name, String location) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(name)); + Preconditions.checkArgument(!Strings.isNullOrEmpty(location)); + return String.format("%s (%s)", name, location); + } - /** - * Only used for mocking the {@link - * com.google.cloud.graphite.platforms.plugin.client.ContainerClient}. Constructs a {@link - * Cluster} from the given nameAndLocation value. - * - * @param nameAndLocation A non-empty String of the form "name (location)" - * @return A cluster with the name and location properties from the provided nameAndLocation. - */ - @VisibleForTesting - static Cluster fromNameAndLocation(String nameAndLocation) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(nameAndLocation)); - String[] values = valuesFromNameAndLocation(nameAndLocation); - return new Cluster().setName(values[0]).setLocation(values[1]); - } + /** + * Only used for mocking the {@link + * com.google.cloud.graphite.platforms.plugin.client.ContainerClient}. Constructs a {@link + * Cluster} from the given nameAndLocation value. + * + * @param nameAndLocation A non-empty String of the form "name (location)" + * @return A cluster with the name and location properties from the provided nameAndLocation. + */ + @VisibleForTesting + static Cluster fromNameAndLocation(String nameAndLocation) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(nameAndLocation)); + String[] values = valuesFromNameAndLocation(nameAndLocation); + return new Cluster().setName(values[0]).setLocation(values[1]); + } - /** - * Extracts the individual values from a combined nameAndLocation String. - * - * @param nameAndLocation A non-empty String of the form "name (location)" - * @return The String array {name, location} from the provided value. - */ - static String[] valuesFromNameAndLocation(String nameAndLocation) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(nameAndLocation)); - String[] clusters = nameAndLocation.split(" [(]"); - if (clusters.length != 2) { - throw new IllegalArgumentException("nameAndLocation should be of the form 'name (location)'"); + /** + * Extracts the individual values from a combined nameAndLocation String. + * + * @param nameAndLocation A non-empty String of the form "name (location)" + * @return The String array {name, location} from the provided value. + */ + static String[] valuesFromNameAndLocation(String nameAndLocation) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(nameAndLocation)); + String[] clusters = nameAndLocation.split(" [(]"); + if (clusters.length != 2) { + throw new IllegalArgumentException("nameAndLocation should be of the form 'name (location)'"); + } + clusters[1] = clusters[1].substring(0, clusters[1].length() - 1); + return clusters; } - clusters[1] = clusters[1].substring(0, clusters[1].length() - 1); - return clusters; - } } diff --git a/src/main/java/com/google/jenkins/plugins/k8sengine/CredentialsUtil.java b/src/main/java/com/google/jenkins/plugins/k8sengine/CredentialsUtil.java index 96b46ed6..b51c710b 100644 --- a/src/main/java/com/google/jenkins/plugins/k8sengine/CredentialsUtil.java +++ b/src/main/java/com/google/jenkins/plugins/k8sengine/CredentialsUtil.java @@ -35,128 +35,120 @@ /** Provides a library of utility functions for credentials-related work. */ public class CredentialsUtil { - /** - * Get the Google Robot Credentials for the given credentialsId. - * - * @param itemGroup A handle to the Jenkins instance. Must be non-null. - * @param domainRequirements A list of domain requirements. Must be non-null. - * @param credentialsId The ID of the GoogleRobotCredentials to be retrieved from Jenkins and - * utilized for authorization. Must be non-empty or non-null and exist in credentials store. - * @return Google Robot Credential for the given credentialsId. - * @throws AbortException If there was an issue retrieving the Google Robot Credentials. - */ - public static GoogleRobotCredentials getRobotCredentials( - ItemGroup itemGroup, - ImmutableList domainRequirements, - String credentialsId) - throws AbortException { - Preconditions.checkNotNull(itemGroup); - Preconditions.checkNotNull(domainRequirements); - Preconditions.checkArgument(!Strings.isNullOrEmpty(credentialsId)); + /** + * Get the Google Robot Credentials for the given credentialsId. + * + * @param itemGroup A handle to the Jenkins instance. Must be non-null. + * @param domainRequirements A list of domain requirements. Must be non-null. + * @param credentialsId The ID of the GoogleRobotCredentials to be retrieved from Jenkins and + * utilized for authorization. Must be non-empty or non-null and exist in credentials store. + * @return Google Robot Credential for the given credentialsId. + * @throws AbortException If there was an issue retrieving the Google Robot Credentials. + */ + public static GoogleRobotCredentials getRobotCredentials( + ItemGroup itemGroup, ImmutableList domainRequirements, String credentialsId) + throws AbortException { + Preconditions.checkNotNull(itemGroup); + Preconditions.checkNotNull(domainRequirements); + Preconditions.checkArgument(!Strings.isNullOrEmpty(credentialsId)); - GoogleRobotCredentials robotCreds = - CredentialsMatchers.firstOrNull( - CredentialsProvider.lookupCredentials( - GoogleRobotCredentials.class, itemGroup, ACL.SYSTEM, domainRequirements), - CredentialsMatchers.withId(credentialsId)); + GoogleRobotCredentials robotCreds = CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentials( + GoogleRobotCredentials.class, itemGroup, ACL.SYSTEM, domainRequirements), + CredentialsMatchers.withId(credentialsId)); - if (robotCreds == null) { - throw new AbortException(Messages.ClientFactory_FailedToRetrieveCredentials(credentialsId)); + if (robotCreds == null) { + throw new AbortException(Messages.ClientFactory_FailedToRetrieveCredentials(credentialsId)); + } + + return robotCreds; } - return robotCreds; - } + /** + * Get the Credential from the Google robot credentials for GKE access. + * + * @param robotCreds Google Robot Credential for desired service account. + * @return Google Credential for the service account. + * @throws AbortException if there was an error initializing HTTP transport. + */ + public static Credential getGoogleCredential(GoogleRobotCredentials robotCreds) throws AbortException { + Credential credential; + try { + credential = robotCreds.getGoogleCredential(new ContainerScopeRequirement()); + } catch (GeneralSecurityException gse) { + throw new AbortException(Messages.ClientFactory_FailedToInitializeHTTPTransport(gse.getMessage())); + } - /** - * Get the Credential from the Google robot credentials for GKE access. - * - * @param robotCreds Google Robot Credential for desired service account. - * @return Google Credential for the service account. - * @throws AbortException if there was an error initializing HTTP transport. - */ - public static Credential getGoogleCredential(GoogleRobotCredentials robotCreds) - throws AbortException { - Credential credential; - try { - credential = robotCreds.getGoogleCredential(new ContainerScopeRequirement()); - } catch (GeneralSecurityException gse) { - throw new AbortException( - Messages.ClientFactory_FailedToInitializeHTTPTransport(gse.getMessage())); + return credential; } - return credential; - } - - /** - * Given a credentialsId and Jenkins context, returns the access token. - * - * @param itemGroup A handle to the Jenkins instance. Must be non-null. - * @param credentialsId The service account credential's id. Must be non-null. - * @return Access token from OAuth to allow kubectl to interact with the cluster. - * @throws IOException If an error occurred fetching the access token. - */ - static String getAccessToken(ItemGroup itemGroup, String credentialsId) throws IOException { - Preconditions.checkArgument(!Strings.isNullOrEmpty(credentialsId)); - Preconditions.checkNotNull(itemGroup); - GoogleRobotCredentials robotCreds = - getRobotCredentials(itemGroup, ImmutableList.of(), credentialsId); + /** + * Given a credentialsId and Jenkins context, returns the access token. + * + * @param itemGroup A handle to the Jenkins instance. Must be non-null. + * @param credentialsId The service account credential's id. Must be non-null. + * @return Access token from OAuth to allow kubectl to interact with the cluster. + * @throws IOException If an error occurred fetching the access token. + */ + static String getAccessToken(ItemGroup itemGroup, String credentialsId) throws IOException { + Preconditions.checkArgument(!Strings.isNullOrEmpty(credentialsId)); + Preconditions.checkNotNull(itemGroup); + GoogleRobotCredentials robotCreds = getRobotCredentials(itemGroup, ImmutableList.of(), credentialsId); - Credential googleCredential = getGoogleCredential(robotCreds); - return getAccessToken(googleCredential); - } - /** - * Wrapper to get access token for service account with this credentialsId. Uses Jenkins.get() as - * context. - * - * @param credentialsId The service account credential's id. Must be non-null. - * @return Access token from OAuth to allow kubectl to interact with the cluster. - * @throws IOException If an error occurred fetching the access token. - */ - static String getAccessToken(String credentialsId) throws IOException { - return getAccessToken(Jenkins.get(), credentialsId); - } + Credential googleCredential = getGoogleCredential(robotCreds); + return getAccessToken(googleCredential); + } + /** + * Wrapper to get access token for service account with this credentialsId. Uses Jenkins.get() as + * context. + * + * @param credentialsId The service account credential's id. Must be non-null. + * @return Access token from OAuth to allow kubectl to interact with the cluster. + * @throws IOException If an error occurred fetching the access token. + */ + static String getAccessToken(String credentialsId) throws IOException { + return getAccessToken(Jenkins.get(), credentialsId); + } - /** - * Given the Google Credential, retrieve the access token. - * - * @param googleCredential Google Credential to get an access token. Must be non-null. - * @return Access token from OAuth to allow kubectl to interact with the cluster. - * @throws IOException If an error occured fetching the access token. - */ - static String getAccessToken(Credential googleCredential) throws IOException { - Preconditions.checkNotNull(googleCredential); + /** + * Given the Google Credential, retrieve the access token. + * + * @param googleCredential Google Credential to get an access token. Must be non-null. + * @return Access token from OAuth to allow kubectl to interact with the cluster. + * @throws IOException If an error occured fetching the access token. + */ + static String getAccessToken(Credential googleCredential) throws IOException { + Preconditions.checkNotNull(googleCredential); - googleCredential.refreshToken(); - return googleCredential.getAccessToken(); - } + googleCredential.refreshToken(); + return googleCredential.getAccessToken(); + } - /** - * Given a credentialsId and Jenkins context, return the default project ID for the service - * account credentials specified. - * - * @param itemGroup A handle to the Jenkins instance. Must be non-null. - * @param credentialsId The service account credential's ID. Must be non-null. - * @return The project ID specified when creating the credential in the Jenkins credential store. - * @throws AbortException If an error occurred fetching the credential. - */ - static String getDefaultProjectId(ItemGroup itemGroup, String credentialsId) - throws AbortException { - Preconditions.checkArgument(!Strings.isNullOrEmpty(credentialsId)); - Preconditions.checkNotNull(itemGroup); - GoogleRobotCredentials robotCreds = - getRobotCredentials(itemGroup, ImmutableList.of(), credentialsId); - return Strings.isNullOrEmpty(robotCreds.getProjectId()) ? "" : robotCreds.getProjectId(); - } + /** + * Given a credentialsId and Jenkins context, return the default project ID for the service + * account credentials specified. + * + * @param itemGroup A handle to the Jenkins instance. Must be non-null. + * @param credentialsId The service account credential's ID. Must be non-null. + * @return The project ID specified when creating the credential in the Jenkins credential store. + * @throws AbortException If an error occurred fetching the credential. + */ + static String getDefaultProjectId(ItemGroup itemGroup, String credentialsId) throws AbortException { + Preconditions.checkArgument(!Strings.isNullOrEmpty(credentialsId)); + Preconditions.checkNotNull(itemGroup); + GoogleRobotCredentials robotCreds = getRobotCredentials(itemGroup, ImmutableList.of(), credentialsId); + return Strings.isNullOrEmpty(robotCreds.getProjectId()) ? "" : robotCreds.getProjectId(); + } - /** - * Wrapper to get the default project ID for a credential using Jenkins.get() as the context. - * - * @param credentialsId The service account credential's ID. Must be non-null. - * @return The project ID specified when creating the credential in the Jenkins credential store. - * @throws AbortException If an error occurred fetching the credential. - */ - static String getDefaultProjectId(String credentialsId) throws AbortException { - Preconditions.checkArgument(!Strings.isNullOrEmpty(credentialsId)); - return getDefaultProjectId(Jenkins.get(), credentialsId); - } + /** + * Wrapper to get the default project ID for a credential using Jenkins.get() as the context. + * + * @param credentialsId The service account credential's ID. Must be non-null. + * @return The project ID specified when creating the credential in the Jenkins credential store. + * @throws AbortException If an error occurred fetching the credential. + */ + static String getDefaultProjectId(String credentialsId) throws AbortException { + Preconditions.checkArgument(!Strings.isNullOrEmpty(credentialsId)); + return getDefaultProjectId(Jenkins.get(), credentialsId); + } } diff --git a/src/main/java/com/google/jenkins/plugins/k8sengine/KubeConfig.java b/src/main/java/com/google/jenkins/plugins/k8sengine/KubeConfig.java index f201718a..f9ab94b8 100644 --- a/src/main/java/com/google/jenkins/plugins/k8sengine/KubeConfig.java +++ b/src/main/java/com/google/jenkins/plugins/k8sengine/KubeConfig.java @@ -30,185 +30,187 @@ * supports a server-side method for this functionality: b/120097899. */ public class KubeConfig { - private static final String KUBECONTEXT_FORMAT = "gke_%s_%s_%s"; - private static final String KUBESERVER_FORMAT = "https://%s"; - private static final String API_VERSION = "v1"; - private static final String CONFIG_KIND = "Config"; - - private ImmutableList contexts; - private ImmutableList clusters; - private ImmutableList users; - private String currentContext; - - private KubeConfig() {} - - /** @return This config's contexts. */ - public ImmutableList getContexts() { - return contexts; - } - - private void setContexts(ImmutableList contexts) { - this.contexts = Preconditions.checkNotNull(contexts); - } - - /** @return This config's clusters. */ - public ImmutableList getClusters() { - return clusters; - } - - private void setClusters(ImmutableList clusters) { - this.clusters = Preconditions.checkNotNull(clusters); - } - - /** @return This config's users. */ - public ImmutableList getUsers() { - return users; - } - - private void setUsers(ImmutableList users) { - this.users = Preconditions.checkNotNull(users); - } - - /** @return This config's current context. */ - public String getCurrentContext() { - return currentContext; - } - - private void setCurrentContext(String currentContext) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(currentContext)); - this.currentContext = currentContext; - } - - /** - * Write a Yaml dump of this {@link KubeConfig}'s data to the specified {@link Writer}. - * NOTE(craigatgoogle): The logic here is taken directly from the `gcloud containers clutsers - * get-credentials` command's implementation: link. - * - * @return A string containing a Yaml dump of this {@link KubeConfig}. - * @throws IOException If an error was encountered while exporting to Yaml. - */ - public String toYaml() throws IOException { - return new Yaml() - .dumpAsMap( - new ImmutableMap.Builder() - .put("apiVersion", API_VERSION) - .put("kind", CONFIG_KIND) - .put("current-context", getCurrentContext()) - .put("clusters", getClusters()) - .put("contexts", getContexts()) - .put("users", getUsers()) - .build()); - } - - /** Builder for {@link KubeConfig}. */ - public static class Builder { - private KubeConfig config; - - public Builder() { - config = new KubeConfig(); - } - - public Builder currentContext(String currentContext) { - config.setCurrentContext(currentContext); - return this; - } - - public Builder users(ImmutableList users) { - config.setUsers(users); - return this; - } - - public Builder contexts(ImmutableList contexts) { - config.setContexts(contexts); - return this; - } - - public Builder clusters(ImmutableList clusters) { - config.setClusters(clusters); - return this; - } - - public KubeConfig build() { - Preconditions.checkArgument(!Strings.isNullOrEmpty(config.getCurrentContext())); - return config; - } - } - - /** - * Creates a {@link KubeConfig} from the specified {@link Cluster}. - * - * @param projectId The ID of the project the cluster resides in. - * @param cluster The cluster data will be drawn from. - * @param accessToken Access token for GKE API access. - * @return A {@link KubeConfig} from the specified {@link Cluster}. - */ - public static KubeConfig fromCluster(String projectId, Cluster cluster, String accessToken) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(projectId)); - Preconditions.checkNotNull(cluster); - - final String currentContext = - contextString(projectId, cluster.getLocation(), cluster.getName()); - return new KubeConfig.Builder() - .currentContext(currentContext) - .contexts(ImmutableList.of(context(currentContext))) - .users(ImmutableList.of(user(currentContext, cluster, accessToken))) - .clusters(ImmutableList.of(cluster(currentContext, cluster))) - .build(); - } - - @VisibleForTesting - static String contextString(String project, String location, String cluster) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(project)); - Preconditions.checkArgument(!Strings.isNullOrEmpty(location)); - Preconditions.checkArgument(!Strings.isNullOrEmpty(cluster)); - return String.format(KUBECONTEXT_FORMAT, project, location, cluster); - } - - @VisibleForTesting - static String clusterServer(Cluster cluster) { - Preconditions.checkNotNull(cluster); - return String.format(KUBESERVER_FORMAT, cluster.getEndpoint()); - } - - private static ImmutableMap context(String currentContext) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(currentContext)); - return new ImmutableMap.Builder() - .put("name", currentContext) - .put( - "context", - new ImmutableMap.Builder() - .put("cluster", currentContext) - .put("user", currentContext) - .put("namespace", "default") - .build()) - .build(); - } - - private static ImmutableMap user( - String currentContext, Cluster cluster, String accessToken) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(currentContext)); - Preconditions.checkNotNull(cluster); - Preconditions.checkArgument(!Strings.isNullOrEmpty(accessToken)); - - return new ImmutableMap.Builder() - .put("name", currentContext) - .put("user", new ImmutableMap.Builder().put("token", accessToken).build()) - .build(); - } - - private static ImmutableMap cluster(String currentContext, Cluster cluster) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(currentContext)); - Preconditions.checkNotNull(cluster); - return new ImmutableMap.Builder() - .put("name", currentContext) - .put( - "cluster", - new ImmutableMap.Builder() - .put("server", clusterServer(cluster)) + private static final String KUBECONTEXT_FORMAT = "gke_%s_%s_%s"; + private static final String KUBESERVER_FORMAT = "https://%s"; + private static final String API_VERSION = "v1"; + private static final String CONFIG_KIND = "Config"; + + private ImmutableList contexts; + private ImmutableList clusters; + private ImmutableList users; + private String currentContext; + + private KubeConfig() {} + + /** @return This config's contexts. */ + public ImmutableList getContexts() { + return contexts; + } + + private void setContexts(ImmutableList contexts) { + this.contexts = Preconditions.checkNotNull(contexts); + } + + /** @return This config's clusters. */ + public ImmutableList getClusters() { + return clusters; + } + + private void setClusters(ImmutableList clusters) { + this.clusters = Preconditions.checkNotNull(clusters); + } + + /** @return This config's users. */ + public ImmutableList getUsers() { + return users; + } + + private void setUsers(ImmutableList users) { + this.users = Preconditions.checkNotNull(users); + } + + /** @return This config's current context. */ + public String getCurrentContext() { + return currentContext; + } + + private void setCurrentContext(String currentContext) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(currentContext)); + this.currentContext = currentContext; + } + + /** + * Write a Yaml dump of this {@link KubeConfig}'s data to the specified {@link Writer}. + * NOTE(craigatgoogle): The logic here is taken directly from the `gcloud containers clutsers + * get-credentials` command's implementation: link. + * + * @return A string containing a Yaml dump of this {@link KubeConfig}. + * @throws IOException If an error was encountered while exporting to Yaml. + */ + public String toYaml() throws IOException { + return new Yaml() + .dumpAsMap(new ImmutableMap.Builder() + .put("apiVersion", API_VERSION) + .put("kind", CONFIG_KIND) + .put("current-context", getCurrentContext()) + .put("clusters", getClusters()) + .put("contexts", getContexts()) + .put("users", getUsers()) + .build()); + } + + /** Builder for {@link KubeConfig}. */ + public static class Builder { + private KubeConfig config; + + public Builder() { + config = new KubeConfig(); + } + + public Builder currentContext(String currentContext) { + config.setCurrentContext(currentContext); + return this; + } + + public Builder users(ImmutableList users) { + config.setUsers(users); + return this; + } + + public Builder contexts(ImmutableList contexts) { + config.setContexts(contexts); + return this; + } + + public Builder clusters(ImmutableList clusters) { + config.setClusters(clusters); + return this; + } + + public KubeConfig build() { + Preconditions.checkArgument(!Strings.isNullOrEmpty(config.getCurrentContext())); + return config; + } + } + + /** + * Creates a {@link KubeConfig} from the specified {@link Cluster}. + * + * @param projectId The ID of the project the cluster resides in. + * @param cluster The cluster data will be drawn from. + * @param accessToken Access token for GKE API access. + * @return A {@link KubeConfig} from the specified {@link Cluster}. + */ + public static KubeConfig fromCluster(String projectId, Cluster cluster, String accessToken) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(projectId)); + Preconditions.checkNotNull(cluster); + + final String currentContext = contextString(projectId, cluster.getLocation(), cluster.getName()); + return new KubeConfig.Builder() + .currentContext(currentContext) + .contexts(ImmutableList.of(context(currentContext))) + .users(ImmutableList.of(user(currentContext, cluster, accessToken))) + .clusters(ImmutableList.of(cluster(currentContext, cluster))) + .build(); + } + + @VisibleForTesting + static String contextString(String project, String location, String cluster) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(project)); + Preconditions.checkArgument(!Strings.isNullOrEmpty(location)); + Preconditions.checkArgument(!Strings.isNullOrEmpty(cluster)); + return String.format(KUBECONTEXT_FORMAT, project, location, cluster); + } + + @VisibleForTesting + static String clusterServer(Cluster cluster) { + Preconditions.checkNotNull(cluster); + return String.format(KUBESERVER_FORMAT, cluster.getEndpoint()); + } + + private static ImmutableMap context(String currentContext) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(currentContext)); + return new ImmutableMap.Builder() + .put("name", currentContext) + .put( + "context", + new ImmutableMap.Builder() + .put("cluster", currentContext) + .put("user", currentContext) + .put("namespace", "default") + .build()) + .build(); + } + + private static ImmutableMap user(String currentContext, Cluster cluster, String accessToken) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(currentContext)); + Preconditions.checkNotNull(cluster); + Preconditions.checkArgument(!Strings.isNullOrEmpty(accessToken)); + + return new ImmutableMap.Builder() + .put("name", currentContext) .put( - "certificate-authority-data", cluster.getMasterAuth().getClusterCaCertificate()) - .build()) - .build(); - } + "user", + new ImmutableMap.Builder() + .put("token", accessToken) + .build()) + .build(); + } + + private static ImmutableMap cluster(String currentContext, Cluster cluster) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(currentContext)); + Preconditions.checkNotNull(cluster); + return new ImmutableMap.Builder() + .put("name", currentContext) + .put( + "cluster", + new ImmutableMap.Builder() + .put("server", clusterServer(cluster)) + .put( + "certificate-authority-data", + cluster.getMasterAuth().getClusterCaCertificate()) + .build()) + .build(); + } } diff --git a/src/main/java/com/google/jenkins/plugins/k8sengine/KubectlWrapper.java b/src/main/java/com/google/jenkins/plugins/k8sengine/KubectlWrapper.java index a32c74d2..5c25adcd 100644 --- a/src/main/java/com/google/jenkins/plugins/k8sengine/KubectlWrapper.java +++ b/src/main/java/com/google/jenkins/plugins/k8sengine/KubectlWrapper.java @@ -36,252 +36,244 @@ * method is added: issue#555. */ public class KubectlWrapper { - private static final Logger LOGGER = Logger.getLogger(KubectlWrapper.class.getName()); - private static final String CHARSET = "UTF-8"; + private static final Logger LOGGER = Logger.getLogger(KubectlWrapper.class.getName()); + private static final String CHARSET = "UTF-8"; - private KubeConfig kubeConfig; - private Launcher launcher; - private FilePath workspace; - private String namespace; - private boolean verboseLogging; + private KubeConfig kubeConfig; + private Launcher launcher; + private FilePath workspace; + private String namespace; + private boolean verboseLogging; - private KubectlWrapper() {} + private KubectlWrapper() {} - private KubeConfig getKubeConfig() { - return kubeConfig; - } + private KubeConfig getKubeConfig() { + return kubeConfig; + } - private void setKubeConfig(KubeConfig kubeConfig) { - this.kubeConfig = kubeConfig; - } + private void setKubeConfig(KubeConfig kubeConfig) { + this.kubeConfig = kubeConfig; + } - private void setLauncher(Launcher launcher) { - this.launcher = launcher; - } + private void setLauncher(Launcher launcher) { + this.launcher = launcher; + } - private Launcher getLauncher() { - return launcher; - } + private Launcher getLauncher() { + return launcher; + } - private void setWorkspace(FilePath workspace) { - this.workspace = workspace; - } + private void setWorkspace(FilePath workspace) { + this.workspace = workspace; + } - private FilePath getWorkspace() { - return workspace; - } + private FilePath getWorkspace() { + return workspace; + } - private void setNamespace(String namespace) { - this.namespace = namespace == null ? "" : namespace; - } + private void setNamespace(String namespace) { + this.namespace = namespace == null ? "" : namespace; + } - private String getNamespace() { - return this.namespace; - } + private String getNamespace() { + return this.namespace; + } - private void setVerboseLogging(boolean verboseLogging) { - this.verboseLogging = verboseLogging; - } + private void setVerboseLogging(boolean verboseLogging) { + this.verboseLogging = verboseLogging; + } - private boolean isVerboseLogging() { - return verboseLogging; - } + private boolean isVerboseLogging() { + return verboseLogging; + } - /** - * Runs the specified kubectl command. - * - * @param command The kubectl command to be run. - * @param args Arguments for the command. - * @throws IOException If an error occurred while executing the command. - * @throws InterruptedException If an error occured while executing the command. - * @return result From kubectl command. - */ - public String runKubectlCommand(String command, ImmutableList args) - throws IOException, InterruptedException { - String output = ""; - FilePath tempDir = null; - try { - // Set up the kubeconfig file for authentication - tempDir = WorkspaceList.tempDir(workspace); - if (tempDir == null) { - throw new IOException("tempDir is null"); - } - tempDir.mkdirs(); - FilePath kubeConfigFile = tempDir.createTempFile(".kube", "config"); - String config = getKubeConfig().toYaml(); + /** + * Runs the specified kubectl command. + * + * @param command The kubectl command to be run. + * @param args Arguments for the command. + * @throws IOException If an error occurred while executing the command. + * @throws InterruptedException If an error occured while executing the command. + * @return result From kubectl command. + */ + public String runKubectlCommand(String command, ImmutableList args) + throws IOException, InterruptedException { + String output = ""; + FilePath tempDir = null; + try { + // Set up the kubeconfig file for authentication + tempDir = WorkspaceList.tempDir(workspace); + if (tempDir == null) { + throw new IOException("tempDir is null"); + } + tempDir.mkdirs(); + FilePath kubeConfigFile = tempDir.createTempFile(".kube", "config"); + String config = getKubeConfig().toYaml(); - // Setup the kubeconfig - kubeConfigFile.write(config, /* encoding */ null); - launchAndJoinCommand( - getLauncher(), - new ArgumentListBuilder() - .add("kubectl") - .add("--kubeconfig") - .add(kubeConfigFile.getRemote()) - .add("config") - .add("use-context") - .add(kubeConfig.getCurrentContext()) - .toList(), - verboseLogging); + // Setup the kubeconfig + kubeConfigFile.write(config, /* encoding */ null); + launchAndJoinCommand( + getLauncher(), + new ArgumentListBuilder() + .add("kubectl") + .add("--kubeconfig") + .add(kubeConfigFile.getRemote()) + .add("config") + .add("use-context") + .add(kubeConfig.getCurrentContext()) + .toList(), + verboseLogging); - // Run the kubectl command - ArgumentListBuilder kubectlCmdBuilder = - new ArgumentListBuilder() - .add("kubectl") - .add("--kubeconfig") - .add(kubeConfigFile.getRemote()) - .add(command); - if (!namespace.isEmpty()) { - kubectlCmdBuilder.add("--namespace").add(namespace); - } - args.forEach(kubectlCmdBuilder::add); - output = launchAndJoinCommand(getLauncher(), kubectlCmdBuilder.toList(), verboseLogging); - } catch (IOException | InterruptedException e) { - LOGGER.log( - Level.SEVERE, - String.format("Failed to execute kubectl command: %s, args: %s", command, args), - e); - throw e; - } finally { - try { - if (tempDir != null) { - tempDir.deleteRecursive(); + // Run the kubectl command + ArgumentListBuilder kubectlCmdBuilder = new ArgumentListBuilder() + .add("kubectl") + .add("--kubeconfig") + .add(kubeConfigFile.getRemote()) + .add(command); + if (!namespace.isEmpty()) { + kubectlCmdBuilder.add("--namespace").add(namespace); + } + args.forEach(kubectlCmdBuilder::add); + output = launchAndJoinCommand(getLauncher(), kubectlCmdBuilder.toList(), verboseLogging); + } catch (IOException | InterruptedException e) { + LOGGER.log( + Level.SEVERE, String.format("Failed to execute kubectl command: %s, args: %s", command, args), e); + throw e; + } finally { + try { + if (tempDir != null) { + tempDir.deleteRecursive(); + } + } catch (Exception ee) { + LOGGER.log(Level.WARNING, String.format("Failed to delete dir: %s", tempDir), ee); + } } - } catch (Exception ee) { - LOGGER.log(Level.WARNING, String.format("Failed to delete dir: %s", tempDir), ee); - } - } - - return output; - } - private static String launchAndJoinCommand( - Launcher launcher, List args, boolean verboseLogging) - throws IOException, InterruptedException { - ByteArrayOutputStream cmdLogStream = new ByteArrayOutputStream(); - int status = - launcher - .launch() - .cmds(args) - .stderr(cmdLogStream) - .stdout(cmdLogStream) - .quiet(!verboseLogging) - .join(); - if (status != 0) { - String logs = cmdLogStream.toString(CHARSET); - LOGGER.log(Level.SEVERE, String.format("kubectl command log: %s", logs)); - throw new IOException( - String.format( - "Failed to launch command args: %s, status: %s. Logs: %s", args, status, logs)); + return output; } - return cmdLogStream.toString(CHARSET); - } - - /** - * Using the kubectl CLI tool as the API client for the caller, this method unmarshalls the JSON - * output of the CLI to a JSON Object. - * - * @param kind The kind of Kubernetes Object. - * @param name The name of the Kubernetes Object. - * @return The JSON object unmarshalled from the kubectl get command's output. - * @throws IOException If an error occurred while executing the command. - * @throws InterruptedException If an error occurred while executing the command. - */ - public Object getObject(String kind, String name) throws IOException, InterruptedException { - String json = runKubectlCommand("get", ImmutableList.of(kind, name, "-o", "json")); - return Configuration.defaultConfiguration().jsonProvider().parse(json); - } - - /** - * Using the kubectl CLI tool as the API client for the caller, this method unmarshalls the JSON - * output of objects matching the supplied labels. - * - * @param kind The kind of Kubernetes Object. - * @param labels The key-value labels set represented as a map. - * @return A list of JSON Objects unmarshalled from the kubectl get command's output. - * @throws IOException If an error occurred while executing the command. - * @throws InterruptedException If an error occurred while executing the command. - * @throws InvalidJsonException If an error occurred parsing the JSON return value. - */ - @SuppressWarnings("unchecked") - public ImmutableList getObjectsThatMatchLabels(String kind, Map labels) - throws IOException, InterruptedException, InvalidJsonException { - String labelsArg = - labels.keySet().stream() - .map((k) -> String.format("%s=%s", k, labels.get(k))) - .collect(Collectors.joining(",")); - String json = runKubectlCommand("get", ImmutableList.of(kind + "s", labelsArg)); - Map result = - (Map) Configuration.defaultConfiguration().jsonProvider().parse(json); - List items = (List) result.get("items"); - return ImmutableList.copyOf(items); - } - - /** Builder for {@link KubectlWrapper}. */ - public static class Builder { - private KubectlWrapper wrapper = new KubectlWrapper(); + private static String launchAndJoinCommand(Launcher launcher, List args, boolean verboseLogging) + throws IOException, InterruptedException { + ByteArrayOutputStream cmdLogStream = new ByteArrayOutputStream(); + int status = launcher.launch() + .cmds(args) + .stderr(cmdLogStream) + .stdout(cmdLogStream) + .quiet(!verboseLogging) + .join(); + if (status != 0) { + String logs = cmdLogStream.toString(CHARSET); + LOGGER.log(Level.SEVERE, String.format("kubectl command log: %s", logs)); + throw new IOException( + String.format("Failed to launch command args: %s, status: %s. Logs: %s", args, status, logs)); + } - /** - * Sets the {@link Launcher} to be used by the wrapper. - * - * @param launcher The {@link Launcher} to be set. - * @return A reference to the {@link Builder}. - */ - public Builder launcher(Launcher launcher) { - wrapper.setLauncher(launcher); - return this; + return cmdLogStream.toString(CHARSET); } /** - * Sets the {@link KubeConfig} to be used by the wrapper. + * Using the kubectl CLI tool as the API client for the caller, this method unmarshalls the JSON + * output of the CLI to a JSON Object. * - * @param kubeConfig The {@link KubeConfig} to be set. - * @return A reference to the {@link Builder}. + * @param kind The kind of Kubernetes Object. + * @param name The name of the Kubernetes Object. + * @return The JSON object unmarshalled from the kubectl get command's output. + * @throws IOException If an error occurred while executing the command. + * @throws InterruptedException If an error occurred while executing the command. */ - public Builder kubeConfig(KubeConfig kubeConfig) { - wrapper.setKubeConfig(kubeConfig); - return this; + public Object getObject(String kind, String name) throws IOException, InterruptedException { + String json = runKubectlCommand("get", ImmutableList.of(kind, name, "-o", "json")); + return Configuration.defaultConfiguration().jsonProvider().parse(json); } /** - * Sets the workspace to be used by the wrapper. + * Using the kubectl CLI tool as the API client for the caller, this method unmarshalls the JSON + * output of objects matching the supplied labels. * - * @param workspace The workspace to be set. - * @return A reference to the {@link Builder}. + * @param kind The kind of Kubernetes Object. + * @param labels The key-value labels set represented as a map. + * @return A list of JSON Objects unmarshalled from the kubectl get command's output. + * @throws IOException If an error occurred while executing the command. + * @throws InterruptedException If an error occurred while executing the command. + * @throws InvalidJsonException If an error occurred parsing the JSON return value. */ - public Builder workspace(FilePath workspace) { - wrapper.setWorkspace(workspace); - return this; + @SuppressWarnings("unchecked") + public ImmutableList getObjectsThatMatchLabels(String kind, Map labels) + throws IOException, InterruptedException, InvalidJsonException { + String labelsArg = labels.keySet().stream() + .map((k) -> String.format("%s=%s", k, labels.get(k))) + .collect(Collectors.joining(",")); + String json = runKubectlCommand("get", ImmutableList.of(kind + "s", labelsArg)); + Map result = (Map) + Configuration.defaultConfiguration().jsonProvider().parse(json); + List items = (List) result.get("items"); + return ImmutableList.copyOf(items); } - /** - * Sets the namespace to be used by the wrapper. - * - * @param namespace The namespace to be set. - * @return A reference to the {@link Builder}. - */ - public Builder namespace(String namespace) { - wrapper.setNamespace(namespace); - return this; - } + /** Builder for {@link KubectlWrapper}. */ + public static class Builder { + private KubectlWrapper wrapper = new KubectlWrapper(); - Builder verboseLogging(boolean verboseLogging) { - wrapper.setVerboseLogging(verboseLogging); - return this; - } + /** + * Sets the {@link Launcher} to be used by the wrapper. + * + * @param launcher The {@link Launcher} to be set. + * @return A reference to the {@link Builder}. + */ + public Builder launcher(Launcher launcher) { + wrapper.setLauncher(launcher); + return this; + } - /** - * Builds a new {@link KubectlWrapper}. - * - * @return A new {@link KubectlWrapper}. - */ - public KubectlWrapper build() { - Preconditions.checkNotNull(wrapper.getLauncher()); - Preconditions.checkNotNull(wrapper.getKubeConfig()); - Preconditions.checkNotNull(wrapper.getWorkspace()); - Preconditions.checkNotNull(wrapper.getNamespace()); - return wrapper; + /** + * Sets the {@link KubeConfig} to be used by the wrapper. + * + * @param kubeConfig The {@link KubeConfig} to be set. + * @return A reference to the {@link Builder}. + */ + public Builder kubeConfig(KubeConfig kubeConfig) { + wrapper.setKubeConfig(kubeConfig); + return this; + } + + /** + * Sets the workspace to be used by the wrapper. + * + * @param workspace The workspace to be set. + * @return A reference to the {@link Builder}. + */ + public Builder workspace(FilePath workspace) { + wrapper.setWorkspace(workspace); + return this; + } + + /** + * Sets the namespace to be used by the wrapper. + * + * @param namespace The namespace to be set. + * @return A reference to the {@link Builder}. + */ + public Builder namespace(String namespace) { + wrapper.setNamespace(namespace); + return this; + } + + Builder verboseLogging(boolean verboseLogging) { + wrapper.setVerboseLogging(verboseLogging); + return this; + } + + /** + * Builds a new {@link KubectlWrapper}. + * + * @return A new {@link KubectlWrapper}. + */ + public KubectlWrapper build() { + Preconditions.checkNotNull(wrapper.getLauncher()); + Preconditions.checkNotNull(wrapper.getKubeConfig()); + Preconditions.checkNotNull(wrapper.getWorkspace()); + Preconditions.checkNotNull(wrapper.getNamespace()); + return wrapper; + } } - } } diff --git a/src/main/java/com/google/jenkins/plugins/k8sengine/KubernetesEngineBuilder.java b/src/main/java/com/google/jenkins/plugins/k8sengine/KubernetesEngineBuilder.java index c109d0bb..55d9e1a7 100644 --- a/src/main/java/com/google/jenkins/plugins/k8sengine/KubernetesEngineBuilder.java +++ b/src/main/java/com/google/jenkins/plugins/k8sengine/KubernetesEngineBuilder.java @@ -68,614 +68,605 @@ /** Provides a build step for publishing build artifacts to a Kubernetes cluster running on GKE. */ public class KubernetesEngineBuilder extends Builder implements SimpleBuildStep, Serializable { - public static final long serialVersionUID = 333L; - private static final Logger LOGGER = Logger.getLogger(KubernetesEngineBuilder.class.getName()); - static final String EMPTY_NAME = "- none -"; - static final String EMPTY_VALUE = ""; - static final int DEFAULT_VERIFY_TIMEOUT_MINUTES = 5; - static final String METRICS_LABEL_KEY = "app.kubernetes.io/managed-by"; - static final String METRICS_LABEL_VALUE = "graphite-jenkins-gke"; - static final ImmutableSet METRICS_TARGET_TYPES = - ImmutableSet.of("Deployment", "Service", "ReplicaSet"); - - private String credentialsId; - private String projectId; - @Deprecated private String zone; - private String location; - private String clusterName; - private String namespace; - private String manifestPattern; - private boolean verifyDeployments; - private int verifyTimeoutInMinutes = DEFAULT_VERIFY_TIMEOUT_MINUTES; - private boolean verifyServices; - private boolean isTestCleanup; - private boolean verboseLogging = false; - private LinkedList afterBuildStepStack; - - /** Constructs a new {@link KubernetesEngineBuilder}. */ - @DataBoundConstructor - public KubernetesEngineBuilder() {} - - public String getCredentialsId() { - return this.credentialsId; - } - - @DataBoundSetter - public void setCredentialsId(String credentialsId) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(credentialsId)); - this.credentialsId = credentialsId; - } - - public String getProjectId() { - return this.projectId; - } - - @DataBoundSetter - public void setProjectId(String projectId) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(projectId)); - this.projectId = projectId; - } - - @Deprecated - public String getZone() { - return getLocation(); - } - - @Deprecated - @DataBoundSetter - public void setZone(String zone) { - setLocation(zone); - } - - public String getLocation() { - setupLocation(); - return this.location; - } - - @DataBoundSetter - public void setLocation(String location) { - setupLocation(); - Preconditions.checkArgument(!Strings.isNullOrEmpty(location)); - this.location = location; - } - - private void setupLocation() { - if (Strings.isNullOrEmpty(this.location)) { - this.location = this.zone; - this.zone = null; - } - } - - public String getClusterName() { - return this.clusterName; - } - - @DataBoundSetter - public void setClusterName(String clusterName) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(clusterName)); - this.clusterName = clusterName; - } - - public String getCluster() { - setupLocation(); - return ClusterUtil.toNameAndLocation(this.clusterName, this.location); - } - - @DataBoundSetter - public void setCluster(String cluster) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(cluster)); - String[] values = ClusterUtil.valuesFromNameAndLocation(cluster); - setClusterName(values[0]); - setLocation(values[1]); - } - - public String getNamespace() { - return this.namespace; - } - - @DataBoundSetter - public void setNamespace(String namespace) { - this.namespace = namespace == null ? "" : namespace; - } - - public String getManifestPattern() { - return this.manifestPattern; - } - - @DataBoundSetter - public void setManifestPattern(String manifestPattern) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(manifestPattern)); - this.manifestPattern = manifestPattern; - } - - @DataBoundSetter - public void setVerifyDeployments(boolean verifyDeployments) { - this.verifyDeployments = verifyDeployments; - } - - public boolean isVerifyDeployments() { - return this.verifyDeployments; - } - - @DataBoundSetter - public void setVerifyServices(boolean verifyServices) { - this.verifyServices = verifyServices; - } - - public boolean isVerifyServices() { - return this.verifyServices; - } - - public int getVerifyTimeoutInMinutes() { - return verifyTimeoutInMinutes; - } - - @DataBoundSetter - public void setVerifyTimeoutInMinutes(int verifyTimeoutInMinutes) { - this.verifyTimeoutInMinutes = verifyTimeoutInMinutes; - } - - public boolean isVerboseLogging() { - return this.verboseLogging; - } - - @DataBoundSetter - public void setVerboseLogging(boolean verboseLogging) { - this.verboseLogging = verboseLogging; - } - - @VisibleForTesting - void pushAfterBuildStep(KubeConfigAfterBuildStep afterBuildStep) { - if (afterBuildStepStack == null) { - afterBuildStepStack = new LinkedList<>(); - } - - afterBuildStepStack.push(afterBuildStep); - } - - /** {@inheritDoc} */ - @Override - public void perform( - @NonNull Run run, - @NonNull FilePath workspace, - @NonNull Launcher launcher, - @NonNull TaskListener listener) - throws InterruptedException, IOException { - LOGGER.log( - Level.INFO, - String.format( - "GKE Deploying, projectId: %s cluster: %s location: %s", - projectId, clusterName, getLocation())); - ContainerClient client = getContainerClient(credentialsId); - Cluster cluster = client.getCluster(projectId, getLocation(), clusterName); - - // generate a kubeconfig for the cluster - KubeConfig kubeConfig = - KubeConfig.fromCluster(projectId, cluster, CredentialsUtil.getAccessToken(credentialsId)); - - KubectlWrapper kubectl = - new KubectlWrapper.Builder() - .workspace(workspace) - .launcher(launcher) - .kubeConfig(kubeConfig) - .namespace(namespace) - .verboseLogging(verboseLogging) - .build(); - - FilePath manifestFile = workspace.child(manifestPattern); - addMetricsLabel(manifestFile); - kubectl.runKubectlCommand("apply", ImmutableList.of("-f", manifestFile.getRemote())); - try { - if (verifyDeployments && !verify(kubectl, manifestPattern, workspace, listener.getLogger())) { - throw new AbortException(Messages.KubernetesEngineBuilder_KubernetesObjectsNotVerified()); - } - } finally { - // run the after build step if it exists - // NOTE(craigatgoogle): Due to the reflective way this class is created, initializers aren't - // run, so we still have to check for null. - if (afterBuildStepStack != null) { - while (!afterBuildStepStack.isEmpty()) { - afterBuildStepStack.pop().perform(kubeConfig, run, workspace, launcher, listener); - } - } - } - } - - @Override - public BuildStepMonitor getRequiredMonitorService() { - return BuildStepMonitor.BUILD; - } - - /** - * Adds a Kubernetes user label unique to this Jenkins plugin to the specified manifest, - * (in-place) in order to enable Jenkins GKE/GCE non-identifying usage metrics. Behavior with - * malformed manifests is undefined. - * - * @param manifestFile The manifest file to be modified. - * @throws IOException If an error occurred while reading/writing the manifest file. - * @throws InterruptedException If an error occurred while parsing/dumping YAML. - */ - @VisibleForTesting - static void addMetricsLabel(FilePath manifestFile) throws InterruptedException, IOException { - Manifests manifests = Manifests.fromFile(manifestFile); - for (Manifests.ManifestObject manifest : - manifests.getObjectManifestsOfKinds(METRICS_TARGET_TYPES)) { - manifest.addLabel(METRICS_LABEL_KEY, METRICS_LABEL_VALUE); - } - - manifests.write(); - } - - /** - * Verify the application of the supplied {@link Manifests.ManifestObject}'s to the Kubernetes - * cluster. - * - * @param kubectl The {@link KubectlWrapper} for running the queries on the Kubernetes cluster. - * @param manifestPattern The manifest pattern for the list of manifest files to use. - * @param workspace The {@link FilePath} to the build workspace directory. - * @param consoleLogger The {@link PrintStream} for Jenkins console output. - * @return If the verification succeeded. - * @throws InterruptedException If an error occurred during verification. - * @throws IOException If an error occurred during verification. - */ - private boolean verify( - KubectlWrapper kubectl, String manifestPattern, FilePath workspace, PrintStream consoleLogger) - throws InterruptedException, IOException { - LOGGER.log( - Level.INFO, - String.format( - "GKE verifying deployment to, projectId: %s cluster: %s location: %s manifests: %s", - projectId, clusterName, getLocation(), workspace.child(manifestPattern))); - - consoleLogger.println( - String.format("Verifying manifests: %s", workspace.child(manifestPattern))); - - Manifests manifests = Manifests.fromFile(workspace.child(manifestPattern)); - - // Filter by the kinds of manifests being verified. - List manifestObjects = - manifests.getObjectManifestsOfKinds(ImmutableSet.of(KubernetesVerifiers.DEPLOYMENT_KIND)); - - consoleLogger.println( - Messages.KubernetesEngineBuilder_VerifyingNObjects(manifestObjects.size())); - - return VerificationTask.verifyObjects( - kubectl, manifestObjects, consoleLogger, verifyTimeoutInMinutes); - } - - /** - * Ensures the executing user has the permissions to be running this step. - * - * @throws AccessDeniedException If the user lacks the proper permissions. - */ - private static void checkPermissions() { - Jenkins jenkins = Jenkins.getInstanceOrNull(); - // This check ensures mocks don't break. - if (jenkins != null) { - jenkins.checkPermission(Job.CONFIGURE); - } - } - - @Symbol("kubernetesEngineDeploy") - @Extension - public static final class DescriptorImpl extends BuildStepDescriptor { - private ClientFactory clientFactory; - private String defaultProjectId; + public static final long serialVersionUID = 333L; + private static final Logger LOGGER = Logger.getLogger(KubernetesEngineBuilder.class.getName()); + static final String EMPTY_NAME = "- none -"; + static final String EMPTY_VALUE = ""; + static final int DEFAULT_VERIFY_TIMEOUT_MINUTES = 5; + static final String METRICS_LABEL_KEY = "app.kubernetes.io/managed-by"; + static final String METRICS_LABEL_VALUE = "graphite-jenkins-gke"; + static final ImmutableSet METRICS_TARGET_TYPES = ImmutableSet.of("Deployment", "Service", "ReplicaSet"); + private String credentialsId; + private String projectId; + + @Deprecated + private String zone; + + private String location; + private String clusterName; + private String namespace; + private String manifestPattern; + private boolean verifyDeployments; + private int verifyTimeoutInMinutes = DEFAULT_VERIFY_TIMEOUT_MINUTES; + private boolean verifyServices; + private boolean isTestCleanup; + private boolean verboseLogging = false; + private LinkedList afterBuildStepStack; + + /** Constructs a new {@link KubernetesEngineBuilder}. */ + @DataBoundConstructor + public KubernetesEngineBuilder() {} + + public String getCredentialsId() { + return this.credentialsId; + } + + @DataBoundSetter + public void setCredentialsId(String credentialsId) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(credentialsId)); + this.credentialsId = credentialsId; + } + + public String getProjectId() { + return this.projectId; + } + + @DataBoundSetter + public void setProjectId(String projectId) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(projectId)); + this.projectId = projectId; + } + + @Deprecated + public String getZone() { + return getLocation(); + } + + @Deprecated + @DataBoundSetter + public void setZone(String zone) { + setLocation(zone); + } + + public String getLocation() { + setupLocation(); + return this.location; + } + + @DataBoundSetter + public void setLocation(String location) { + setupLocation(); + Preconditions.checkArgument(!Strings.isNullOrEmpty(location)); + this.location = location; + } + + private void setupLocation() { + if (Strings.isNullOrEmpty(this.location)) { + this.location = this.zone; + this.zone = null; + } + } + + public String getClusterName() { + return this.clusterName; + } + + @DataBoundSetter + public void setClusterName(String clusterName) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(clusterName)); + this.clusterName = clusterName; + } + + public String getCluster() { + setupLocation(); + return ClusterUtil.toNameAndLocation(this.clusterName, this.location); + } + + @DataBoundSetter + public void setCluster(String cluster) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(cluster)); + String[] values = ClusterUtil.valuesFromNameAndLocation(cluster); + setClusterName(values[0]); + setLocation(values[1]); + } + + public String getNamespace() { + return this.namespace; + } + + @DataBoundSetter + public void setNamespace(String namespace) { + this.namespace = namespace == null ? "" : namespace; + } + + public String getManifestPattern() { + return this.manifestPattern; + } + + @DataBoundSetter + public void setManifestPattern(String manifestPattern) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(manifestPattern)); + this.manifestPattern = manifestPattern; + } + + @DataBoundSetter + public void setVerifyDeployments(boolean verifyDeployments) { + this.verifyDeployments = verifyDeployments; + } + + public boolean isVerifyDeployments() { + return this.verifyDeployments; + } + + @DataBoundSetter + public void setVerifyServices(boolean verifyServices) { + this.verifyServices = verifyServices; + } + + public boolean isVerifyServices() { + return this.verifyServices; + } + + public int getVerifyTimeoutInMinutes() { + return verifyTimeoutInMinutes; + } - @NonNull + @DataBoundSetter + public void setVerifyTimeoutInMinutes(int verifyTimeoutInMinutes) { + this.verifyTimeoutInMinutes = verifyTimeoutInMinutes; + } + + public boolean isVerboseLogging() { + return this.verboseLogging; + } + + @DataBoundSetter + public void setVerboseLogging(boolean verboseLogging) { + this.verboseLogging = verboseLogging; + } + + @VisibleForTesting + void pushAfterBuildStep(KubeConfigAfterBuildStep afterBuildStep) { + if (afterBuildStepStack == null) { + afterBuildStepStack = new LinkedList<>(); + } + + afterBuildStepStack.push(afterBuildStep); + } + + /** {@inheritDoc} */ @Override - public String getDisplayName() { - return Messages.KubernetesEngineBuilder_DisplayName(); + public void perform( + @NonNull Run run, + @NonNull FilePath workspace, + @NonNull Launcher launcher, + @NonNull TaskListener listener) + throws InterruptedException, IOException { + LOGGER.log( + Level.INFO, + String.format( + "GKE Deploying, projectId: %s cluster: %s location: %s", + projectId, clusterName, getLocation())); + ContainerClient client = getContainerClient(credentialsId); + Cluster cluster = client.getCluster(projectId, getLocation(), clusterName); + + // generate a kubeconfig for the cluster + KubeConfig kubeConfig = + KubeConfig.fromCluster(projectId, cluster, CredentialsUtil.getAccessToken(credentialsId)); + + KubectlWrapper kubectl = new KubectlWrapper.Builder() + .workspace(workspace) + .launcher(launcher) + .kubeConfig(kubeConfig) + .namespace(namespace) + .verboseLogging(verboseLogging) + .build(); + + FilePath manifestFile = workspace.child(manifestPattern); + addMetricsLabel(manifestFile); + kubectl.runKubectlCommand("apply", ImmutableList.of("-f", manifestFile.getRemote())); + try { + if (verifyDeployments && !verify(kubectl, manifestPattern, workspace, listener.getLogger())) { + throw new AbortException(Messages.KubernetesEngineBuilder_KubernetesObjectsNotVerified()); + } + } finally { + // run the after build step if it exists + // NOTE(craigatgoogle): Due to the reflective way this class is created, initializers aren't + // run, so we still have to check for null. + if (afterBuildStepStack != null) { + while (!afterBuildStepStack.isEmpty()) { + afterBuildStepStack.pop().perform(kubeConfig, run, workspace, launcher, listener); + } + } + } } @Override - public boolean isApplicable(Class jobType) { - return true; + public BuildStepMonitor getRequiredMonitorService() { + return BuildStepMonitor.BUILD; } + /** + * Adds a Kubernetes user label unique to this Jenkins plugin to the specified manifest, + * (in-place) in order to enable Jenkins GKE/GCE non-identifying usage metrics. Behavior with + * malformed manifests is undefined. + * + * @param manifestFile The manifest file to be modified. + * @throws IOException If an error occurred while reading/writing the manifest file. + * @throws InterruptedException If an error occurred while parsing/dumping YAML. + */ @VisibleForTesting - ClientFactory getClientFactory(Jenkins context, String credentialsId) throws AbortException { - if (this.clientFactory == null || updateCredentialsId(credentialsId)) { - this.clientFactory = ClientUtil.getClientFactory(context, credentialsId); - } - return this.clientFactory; + static void addMetricsLabel(FilePath manifestFile) throws InterruptedException, IOException { + Manifests manifests = Manifests.fromFile(manifestFile); + for (Manifests.ManifestObject manifest : manifests.getObjectManifestsOfKinds(METRICS_TARGET_TYPES)) { + manifest.addLabel(METRICS_LABEL_KEY, METRICS_LABEL_VALUE); + } + + manifests.write(); } - @VisibleForTesting - String getDefaultProjectId(Jenkins context, String credentialsId) throws AbortException { - if (this.defaultProjectId == null || updateCredentialsId(credentialsId)) { - this.defaultProjectId = CredentialsUtil.getDefaultProjectId(context, credentialsId); - } - return this.defaultProjectId; + /** + * Verify the application of the supplied {@link Manifests.ManifestObject}'s to the Kubernetes + * cluster. + * + * @param kubectl The {@link KubectlWrapper} for running the queries on the Kubernetes cluster. + * @param manifestPattern The manifest pattern for the list of manifest files to use. + * @param workspace The {@link FilePath} to the build workspace directory. + * @param consoleLogger The {@link PrintStream} for Jenkins console output. + * @return If the verification succeeded. + * @throws InterruptedException If an error occurred during verification. + * @throws IOException If an error occurred during verification. + */ + private boolean verify( + KubectlWrapper kubectl, String manifestPattern, FilePath workspace, PrintStream consoleLogger) + throws InterruptedException, IOException { + LOGGER.log( + Level.INFO, + String.format( + "GKE verifying deployment to, projectId: %s cluster: %s location: %s manifests: %s", + projectId, clusterName, getLocation(), workspace.child(manifestPattern))); + + consoleLogger.println(String.format("Verifying manifests: %s", workspace.child(manifestPattern))); + + Manifests manifests = Manifests.fromFile(workspace.child(manifestPattern)); + + // Filter by the kinds of manifests being verified. + List manifestObjects = + manifests.getObjectManifestsOfKinds(ImmutableSet.of(KubernetesVerifiers.DEPLOYMENT_KIND)); + + consoleLogger.println(Messages.KubernetesEngineBuilder_VerifyingNObjects(manifestObjects.size())); + + return VerificationTask.verifyObjects(kubectl, manifestObjects, consoleLogger, verifyTimeoutInMinutes); } - private boolean updateCredentialsId(String credentialsId) { - if (this.credentialsId == null || !this.credentialsId.equals(credentialsId)) { - this.credentialsId = credentialsId; - return true; - } - return false; - } - - public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Jenkins context) { - if (context == null || !context.hasPermission(CredentialsProvider.VIEW)) { - return new StandardListBoxModel(); - } - - return new StandardListBoxModel() - .includeEmptyValue() - .includeMatchingAs( - ACL.SYSTEM, - context, - StandardCredentials.class, - Collections.emptyList(), - CredentialsMatchers.instanceOf(GoogleOAuth2Credentials.class)); - } - - public FormValidation doCheckCredentialsId( - @AncestorInPath Jenkins context, - @QueryParameter("credentialsId") final String credentialsId) { - checkPermissions(); - if (credentialsId.isEmpty()) { - return FormValidation.error(Messages.KubernetesEngineBuilder_NoCredential()); - } - - try { - CredentialsUtil.getAccessToken(context, credentialsId); - } catch (IOException ex) { - LOGGER.log(Level.SEVERE, Messages.KubernetesEngineBuilder_CredentialAuthFailed(), ex); - return FormValidation.error(Messages.KubernetesEngineBuilder_CredentialAuthFailed()); - } - - return FormValidation.ok(); - } - - public ListBoxModel doFillProjectIdItems( - @AncestorInPath Jenkins context, - @QueryParameter("projectId") final String projectId, - @QueryParameter("credentialsId") final String credentialsId) { - checkPermissions(); - ListBoxModel items = new ListBoxModel(); - items.add(EMPTY_NAME, EMPTY_VALUE); - if (Strings.isNullOrEmpty(credentialsId)) { - return items; - } - - ClientFactory clientFactory; - String defaultProjectId; - try { - clientFactory = this.getClientFactory(context, credentialsId); - defaultProjectId = getDefaultProjectId(context, credentialsId); - } catch (AbortException | RuntimeException ex) { - LOGGER.log(Level.SEVERE, Messages.KubernetesEngineBuilder_CredentialAuthFailed(), ex); - items.clear(); - items.add(Messages.KubernetesEngineBuilder_CredentialAuthFailed(), EMPTY_VALUE); - return items; - } - - try { - CloudResourceManagerClient client = clientFactory.cloudResourceManagerClient(); - List projects = client.listProjects(); - - if (projects.isEmpty()) { - return items; + /** + * Ensures the executing user has the permissions to be running this step. + * + * @throws AccessDeniedException If the user lacks the proper permissions. + */ + private static void checkPermissions() { + Jenkins jenkins = Jenkins.getInstanceOrNull(); + // This check ensures mocks don't break. + if (jenkins != null) { + jenkins.checkPermission(Job.CONFIGURE); } + } - projects.stream() - .filter(p -> !p.getProjectId().equals(defaultProjectId)) - .forEach(p -> items.add(p.getProjectId())); + @Symbol("kubernetesEngineDeploy") + @Extension + public static final class DescriptorImpl extends BuildStepDescriptor { + private ClientFactory clientFactory; + private String defaultProjectId; + private String credentialsId; + + @NonNull + @Override + public String getDisplayName() { + return Messages.KubernetesEngineBuilder_DisplayName(); + } - if (Strings.isNullOrEmpty(defaultProjectId)) { - selectOption(items, projectId); - return items; + @Override + public boolean isApplicable(Class jobType) { + return true; } - if (projects.size() == items.size() && Strings.isNullOrEmpty(projectId)) { - items.add(new Option(defaultProjectId, defaultProjectId, true)); - } else { - // Add defaultProjectId anyway, but select the appropriate projectID based on - // the previously entered projectID - items.add(defaultProjectId); - selectOption(items, projectId); + @VisibleForTesting + ClientFactory getClientFactory(Jenkins context, String credentialsId) throws AbortException { + if (this.clientFactory == null || updateCredentialsId(credentialsId)) { + this.clientFactory = ClientUtil.getClientFactory(context, credentialsId); + } + return this.clientFactory; } - return items; - } catch (IOException ioe) { - LOGGER.log(Level.SEVERE, Messages.KubernetesEngineBuilder_ProjectIDFillError(), ioe); - items.clear(); - items.add(Messages.KubernetesEngineBuilder_ProjectIDFillError(), EMPTY_VALUE); - return items; - } - } - - public FormValidation doCheckProjectId( - @AncestorInPath Jenkins context, - @QueryParameter("projectId") final String projectId, - @QueryParameter("credentialsId") final String credentialsId) { - checkPermissions(); - if (Strings.isNullOrEmpty(credentialsId) && Strings.isNullOrEmpty(projectId)) { - return FormValidation.error(Messages.KubernetesEngineBuilder_ProjectIDRequired()); - } else if (Strings.isNullOrEmpty(credentialsId)) { - return FormValidation.error(Messages.KubernetesEngineBuilder_ProjectCredentialIDRequired()); - } - - ClientFactory clientFactory; - try { - clientFactory = getClientFactory(context, credentialsId); - } catch (AbortException | RuntimeException e) { - return FormValidation.error(Messages.KubernetesEngineBuilder_CredentialAuthFailed()); - } - - try { - CloudResourceManagerClient client = clientFactory.cloudResourceManagerClient(); - List projects = client.listProjects(); - if (Strings.isNullOrEmpty(projectId)) { - return FormValidation.error(Messages.KubernetesEngineBuilder_ProjectIDRequired()); + + @VisibleForTesting + String getDefaultProjectId(Jenkins context, String credentialsId) throws AbortException { + if (this.defaultProjectId == null || updateCredentialsId(credentialsId)) { + this.defaultProjectId = CredentialsUtil.getDefaultProjectId(context, credentialsId); + } + return this.defaultProjectId; } - Optional matchingProject = - projects.stream().filter(p -> projectId.equals(p.getProjectId())).findFirst(); - if (!matchingProject.isPresent()) { - return FormValidation.error( - Messages.KubernetesEngineBuilder_ProjectIDNotUnderCredential()); + private boolean updateCredentialsId(String credentialsId) { + if (this.credentialsId == null || !this.credentialsId.equals(credentialsId)) { + this.credentialsId = credentialsId; + return true; + } + return false; } - } catch (IOException ioe) { - return FormValidation.error(Messages.KubernetesEngineBuilder_ProjectIDVerificationError()); - } - - return FormValidation.ok(); - } - - public ListBoxModel doFillClusterItems( - @AncestorInPath Jenkins context, - @QueryParameter("cluster") final String cluster, - @QueryParameter("credentialsId") final String credentialsId, - @QueryParameter("projectId") final String projectId) { - checkPermissions(); - ListBoxModel items = new ListBoxModel(); - items.add(EMPTY_NAME, EMPTY_VALUE); - if (Strings.isNullOrEmpty(credentialsId) || Strings.isNullOrEmpty(projectId)) { - return items; - } - - ClientFactory clientFactory; - try { - clientFactory = getClientFactory(context, credentialsId); - } catch (AbortException | RuntimeException ex) { - LOGGER.log(Level.SEVERE, Messages.KubernetesEngineBuilder_CredentialAuthFailed(), ex); - items.clear(); - items.add(Messages.KubernetesEngineBuilder_CredentialAuthFailed(), EMPTY_VALUE); - return items; - } - - try { - ContainerClient client = clientFactory.containerClient(); - List clusters = client.listAllClusters(projectId); - - if (clusters.isEmpty()) { - return items; + + public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Jenkins context) { + if (context == null || !context.hasPermission(CredentialsProvider.VIEW)) { + return new StandardListBoxModel(); + } + + return new StandardListBoxModel() + .includeEmptyValue() + .includeMatchingAs( + ACL.SYSTEM, + context, + StandardCredentials.class, + Collections.emptyList(), + CredentialsMatchers.instanceOf(GoogleOAuth2Credentials.class)); } - clusters.forEach(c -> items.add(ClusterUtil.toNameAndLocation(c))); - selectOption(items, cluster); - return items; - } catch (IOException ioe) { - LOGGER.log(Level.SEVERE, Messages.KubernetesEngineBuilder_ClusterFillError(), ioe); - items.clear(); - items.add(Messages.KubernetesEngineBuilder_ClusterFillError(), EMPTY_VALUE); - return items; - } - } - - public FormValidation doCheckCluster( - @AncestorInPath Jenkins context, - @QueryParameter("cluster") final String cluster, - @QueryParameter("credentialsId") final String credentialsId, - @QueryParameter("projectId") final String projectId) { - checkPermissions(); - if (Strings.isNullOrEmpty(credentialsId) && Strings.isNullOrEmpty(cluster)) { - return FormValidation.error(Messages.KubernetesEngineBuilder_ClusterRequired()); - } else if (Strings.isNullOrEmpty(credentialsId)) { - return FormValidation.error(Messages.KubernetesEngineBuilder_ClusterCredentialIDRequired()); - } - - ClientFactory clientFactory; - try { - clientFactory = getClientFactory(context, credentialsId); - } catch (AbortException | RuntimeException ex) { - return FormValidation.error(Messages.KubernetesEngineBuilder_CredentialAuthFailed()); - } - - if (Strings.isNullOrEmpty(projectId) && Strings.isNullOrEmpty(cluster)) { - return FormValidation.error(Messages.KubernetesEngineBuilder_ClusterRequired()); - } else if (Strings.isNullOrEmpty(projectId)) { - return FormValidation.error(Messages.KubernetesEngineBuilder_ClusterProjectIDRequired()); - } - - try { - ContainerClient client = clientFactory.containerClient(); - List clusters = client.listAllClusters(projectId); - if (Strings.isNullOrEmpty(cluster)) { - return FormValidation.error(Messages.KubernetesEngineBuilder_ClusterRequired()); - } else if (clusters.size() == 0) { - return FormValidation.error(Messages.KubernetesEngineBuilder_NoClusterInProject()); + public FormValidation doCheckCredentialsId( + @AncestorInPath Jenkins context, @QueryParameter("credentialsId") final String credentialsId) { + checkPermissions(); + if (credentialsId.isEmpty()) { + return FormValidation.error(Messages.KubernetesEngineBuilder_NoCredential()); + } + + try { + CredentialsUtil.getAccessToken(context, credentialsId); + } catch (IOException ex) { + LOGGER.log(Level.SEVERE, Messages.KubernetesEngineBuilder_CredentialAuthFailed(), ex); + return FormValidation.error(Messages.KubernetesEngineBuilder_CredentialAuthFailed()); + } + + return FormValidation.ok(); } - Optional clusterOption = - clusters.stream() - .filter(c -> cluster.equals(ClusterUtil.toNameAndLocation(c))) - .findFirst(); - if (!clusterOption.isPresent()) { - return FormValidation.error(Messages.KubernetesEngineBuilder_ClusterNotInProject()); + + public ListBoxModel doFillProjectIdItems( + @AncestorInPath Jenkins context, + @QueryParameter("projectId") final String projectId, + @QueryParameter("credentialsId") final String credentialsId) { + checkPermissions(); + ListBoxModel items = new ListBoxModel(); + items.add(EMPTY_NAME, EMPTY_VALUE); + if (Strings.isNullOrEmpty(credentialsId)) { + return items; + } + + ClientFactory clientFactory; + String defaultProjectId; + try { + clientFactory = this.getClientFactory(context, credentialsId); + defaultProjectId = getDefaultProjectId(context, credentialsId); + } catch (AbortException | RuntimeException ex) { + LOGGER.log(Level.SEVERE, Messages.KubernetesEngineBuilder_CredentialAuthFailed(), ex); + items.clear(); + items.add(Messages.KubernetesEngineBuilder_CredentialAuthFailed(), EMPTY_VALUE); + return items; + } + + try { + CloudResourceManagerClient client = clientFactory.cloudResourceManagerClient(); + List projects = client.listProjects(); + + if (projects.isEmpty()) { + return items; + } + + projects.stream() + .filter(p -> !p.getProjectId().equals(defaultProjectId)) + .forEach(p -> items.add(p.getProjectId())); + + if (Strings.isNullOrEmpty(defaultProjectId)) { + selectOption(items, projectId); + return items; + } + + if (projects.size() == items.size() && Strings.isNullOrEmpty(projectId)) { + items.add(new Option(defaultProjectId, defaultProjectId, true)); + } else { + // Add defaultProjectId anyway, but select the appropriate projectID based on + // the previously entered projectID + items.add(defaultProjectId); + selectOption(items, projectId); + } + return items; + } catch (IOException ioe) { + LOGGER.log(Level.SEVERE, Messages.KubernetesEngineBuilder_ProjectIDFillError(), ioe); + items.clear(); + items.add(Messages.KubernetesEngineBuilder_ProjectIDFillError(), EMPTY_VALUE); + return items; + } } - } catch (IOException ioe) { - return FormValidation.error(Messages.KubernetesEngineBuilder_ClusterVerificationError()); - } - return FormValidation.ok(); - } - - public FormValidation doCheckNamespace(@QueryParameter("namespace") final String namespace) { - checkPermissions(); - /* Regex from - * https://github.com/kubernetes/apimachinery/blob/7d08eb7a76fdbc79f7bc1b5fb061ae44f3324bfa/pkg/util/validation/validation.go#L110 - */ - if (!Strings.isNullOrEmpty(namespace) - && !namespace.matches("[a-z0-9]([-a-z0-9]*[a-z0-9])?")) { - return FormValidation.error(Messages.KubernetesEngineBuilder_NamespaceInvalid()); - } - return FormValidation.ok(); - } - - public FormValidation doCheckManifestPattern( - @QueryParameter("manifestPattern") final String manifestPattern) { - checkPermissions(); - if (Strings.isNullOrEmpty(manifestPattern)) { - return FormValidation.error(Messages.KubernetesEngineBuilder_ManifestRequired()); - } - return FormValidation.ok(); - } - - public FormValidation doCheckVerifyTimeoutInMinutes( - @QueryParameter("verifyTimeoutInMinutes") final String verifyTimeoutInMinutes) { - checkPermissions(); - if (Strings.isNullOrEmpty(verifyTimeoutInMinutes)) { - return FormValidation.error( - Messages.KubernetesEngineBuilder_VerifyTimeoutInMinutesRequired()); - } - - if (!verifyTimeoutInMinutes.matches("([1-9]\\d*)")) { - return FormValidation.error( - Messages.KubernetesEngineBuilder_VerifyTimeoutInMinutesFormatError()); - } - - return FormValidation.ok(); - } - } - - private static void selectOption(ListBoxModel listBoxModel, String optionValue) { - Optional