├── Jenkinsfile ├── .mvn ├── maven.config └── extensions.xml ├── images └── configuration.png ├── src ├── main │ ├── resources │ │ ├── index.jelly │ │ └── io │ │ │ └── alauda │ │ │ └── jenkins │ │ │ └── devops │ │ │ └── support │ │ │ ├── KubernetesClusterConfiguration │ │ │ └── config.jelly │ │ │ └── KubernetesCluster │ │ │ └── config.jelly │ └── java │ │ └── io │ │ └── alauda │ │ └── jenkins │ │ └── devops │ │ └── support │ │ ├── KubernetesClusterConfigurationListener.java │ │ ├── exception │ │ └── KubernetesClientException.java │ │ ├── utils │ │ ├── CredentialsUtils.java │ │ └── SyncPluginConfigurationCompatiblilityMigrater.java │ │ ├── KubernetesClusterConfiguration.java │ │ ├── client │ │ └── Clients.java │ │ └── KubernetesCluster.java └── test │ └── java │ └── io │ └── alauda │ └── jenkins │ └── devops │ └── support │ └── KubernetesClusterConfigurationTest.java ├── scripts ├── run.sh ├── restart.sh └── upload.sh ├── README.md ├── sonar-project.properties ├── LICENSE ├── .github └── settings.yml ├── .gitignore ├── Alauda.Jenkinsfile └── pom.xml /Jenkinsfile: -------------------------------------------------------------------------------- 1 | buildPlugin() -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -------------------------------------------------------------------------------- /images/configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/alauda-kubernetes-support-plugin/master/images/configuration.png -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | This plugin allows users to config kubernetes and share configuration to other plugins. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/io/alauda/jenkins/devops/support/KubernetesClusterConfiguration/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.0-beta-7 6 | 7 | 8 | -------------------------------------------------------------------------------- /scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | TOKEN_ADMIN="c6fc6a59033d1144fec5a565cb2b6796" 6 | ADDRESS="http://localhost:30251" 7 | export JENKINS_CONTAINER=$(docker ps | grep jenkins | head -n1 | awk '{print $1;}') 8 | echo $JENKINS_CONTAINER 9 | if [ "$JENKINS_CONTAINER" == "" ]; then 10 | echo "Jenkins container not found" 11 | exit 1; 12 | fi 13 | mvn clean install -DskipTests 14 | docker cp target/alauda-devops-sync.hpi $JENKINS_CONTAINER:/var/jenkins_home/plugins 15 | ./restart.sh 16 | -------------------------------------------------------------------------------- /scripts/restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | TOKEN_ADMIN="c6fc6a59033d1144fec5a565cb2b6796" 6 | ADDRESS="http://localhost:30251" 7 | NAMESPACE=default-1 8 | DEPLOY=jenkins 9 | 10 | echo "Restarting jenkins..." 11 | curl -v -XPOST $ADDRESS/safeRestart -u 'admin:'$TOKEN_ADMIN 12 | kubectl logs -f --tail=50 -n $NAMESPACE deploy/$DEPLOY 13 | echo "====================================================" 14 | echo "Jenkins restarted... tailling logs again..." 15 | echo "====================================================" 16 | sleep 2 17 | kubectl logs -f --tail=100 -n $NAMESPACE deploy/$DEPLOY 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Alauda Kubernetes Support Plugin 2 | 3 | This plugin allows user to config kubernetes cluster and provide correspondent client for other plugins to use. 4 | 5 | ![plugin configuration](./images/configuration.png "Plugin Configuration") 6 | 7 | 8 | * Kubernetes URL - the address of Kubernetes Cluster, if left it to blank, it will use local cluster address. 9 | * Credentials - the token to connect to cluster, the Credentials type is Secret Text. 10 | * Disable https certificate check - whether to skip tls verify 11 | * Server Certificate Authority - certificate used to create secure connections to cluster 12 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | # must be unique in a given SonarQube instance 2 | sonar.projectKey=alauda-devops-support 3 | # this is the name and version displayed in the SonarQube UI. Was mandatory prior to SonarQube 6.1. 4 | sonar.projectName=alauda-devops-support-plugin 5 | 6 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 7 | # This property is optional if sonar.modules is set. 8 | sonar.sources=src/main/java 9 | 10 | # Encoding of the source code. Default is default system encoding 11 | sonar.sourceEncoding=UTF-8 12 | 13 | sonar.java.source=1.8 14 | 15 | sonar.java.binaries=./target/classes 16 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/devops/support/KubernetesClusterConfigurationListener.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.devops.support; 2 | 3 | import hudson.ExtensionList; 4 | import hudson.ExtensionPoint; 5 | import io.kubernetes.client.ApiClient; 6 | 7 | public interface KubernetesClusterConfigurationListener extends ExtensionPoint { 8 | void onConfigChange(KubernetesCluster cluster, ApiClient client); 9 | 10 | void onConfigError(KubernetesCluster cluster, Throwable reason); 11 | 12 | static ExtensionList all() { 13 | return ExtensionList.lookup(KubernetesClusterConfigurationListener.class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/devops/support/exception/KubernetesClientException.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.devops.support.exception; 2 | 3 | public class KubernetesClientException extends Exception { 4 | public KubernetesClientException() { 5 | } 6 | 7 | public KubernetesClientException(String message) { 8 | super(message); 9 | } 10 | 11 | public KubernetesClientException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public KubernetesClientException(Throwable cause) { 16 | super(cause); 17 | } 18 | 19 | public KubernetesClientException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 20 | super(message, cause, enableSuppression, writableStackTrace); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alauda.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | name: alauda-kubernetes-support-plugin 3 | description: Alauda Kubernetes support plugin 4 | homepage: http://www.alauda.cn/ 5 | private: false 6 | has_issues: true 7 | has_wiki: false 8 | has_downloads: false 9 | default_branch: master 10 | allow_squash_merge: true 11 | allow_merge_commit: true 12 | allow_rebase_merge: true 13 | labels: 14 | - name: newbie 15 | color: abe7f4 16 | description: 新手上路 17 | - name: bug 18 | color: d73a4a 19 | description: Something isn't working 20 | - name: enhancement 21 | color: a2eeef 22 | description: New feature or request 23 | - name: help wanted 24 | color: 008672 25 | description: Extra attention is needed 26 | branches: 27 | - name: master 28 | protection: 29 | required_pull_request_reviews: 30 | required_approving_review_count: 2 31 | dismiss_stale_reviews: true 32 | require_code_owner_reviews: false 33 | dismissal_restrictions: 34 | users: [] 35 | teams: [] 36 | required_status_checks: 37 | strict: true 38 | contexts: [] 39 | enforce_admins: false 40 | restrictions: 41 | users: [] 42 | teams: [] 43 | -------------------------------------------------------------------------------- /src/main/resources/io/alauda/jenkins/devops/support/KubernetesCluster/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/devops/support/utils/CredentialsUtils.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.devops.support.utils; 2 | 3 | import com.cloudbees.plugins.credentials.CredentialsMatchers; 4 | import com.cloudbees.plugins.credentials.CredentialsProvider; 5 | import hudson.security.ACL; 6 | import jenkins.model.Jenkins; 7 | import org.apache.commons.lang.StringUtils; 8 | import org.jenkinsci.plugins.plaincredentials.StringCredentials; 9 | 10 | import java.security.GeneralSecurityException; 11 | import java.util.Collections; 12 | 13 | public class CredentialsUtils { 14 | public static String getToken(String credentialId) throws GeneralSecurityException { 15 | if (StringUtils.isEmpty(credentialId)) { 16 | return ""; 17 | } 18 | 19 | StringCredentials secretCredentials = 20 | CredentialsMatchers.firstOrNull( 21 | CredentialsProvider.lookupCredentials(StringCredentials.class, Jenkins.getInstance(), ACL.SYSTEM, Collections.emptyList()), 22 | CredentialsMatchers.withId(credentialId)); 23 | 24 | if (secretCredentials == null) { 25 | throw new GeneralSecurityException(String.format("Credential with id %s not found", credentialId)); 26 | } 27 | 28 | return secretCredentials.getSecret().getPlainText(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /scripts/upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # current script needs env.sh to setup some environment variables, see also example file below: 3 | # export JENKINS_USER=admin 4 | # export JENKINS_TOKEN=11aa342e9a392f64cc7c56548e17fe6d09 5 | # export JENKINS_URL=http://jenkins.com 6 | # 7 | # export REMOTE_USER=193151e7-48e9-402c-a608-a10301a29a72 8 | # export REMOTE_TOKEN=274e8a79536e8a34f596a5a5d17d3e46 9 | # export REMOTE_URL=http://jenkins.cn 10 | 11 | if [ "$JENKINS_USER" == "" ]; then 12 | export JENKINS_USER=admin 13 | fi 14 | 15 | if [ ! -f "$(dirname "${BASH_SOURCE[0]}")/env.sh" ]; then 16 | echo 'we need the env.sh to setup vars' 17 | exit -1 18 | fi 19 | 20 | source $(dirname "${BASH_SOURCE[0]}")/env.sh 21 | 22 | issuer=$(curl -k -u $JENKINS_USER:$JENKINS_TOKEN $JENKINS_URL/crumbIssuer/api/json -s -o /dev/null -w %{http_code}) 23 | if [ "$issuer" == "200" ]; then 24 | export issuer=$(curl -k -u $JENKINS_USER:$JENKINS_TOKEN $JENKINS_URL/crumbIssuer/api/json -s) 25 | issuer=$(python -c "import json;import os;issuer=os.getenv('issuer');issuer=json.loads(issuer);print issuer['crumb']") 26 | else 27 | issuer="" 28 | fi 29 | 30 | export target_file=$(dirname "${BASH_SOURCE[0]}")"/../target/alauda-kubernetes-support.hpi" 31 | echo "target file is $target_file" 32 | 33 | # support fetch plugin from the remote server 34 | if [ "$#" == "1" ]; then 35 | if [[ "$1" =~ ^http.* ]]; then 36 | echo "going to download plugin from remote: $1" 37 | 38 | remote_issuer=$(curl -k -u $REMOTE_USER:$REMOTE_TOKEN $REMOTE_URL/crumbIssuer/api/json -s -o /dev/null -w %{http_code}) 39 | if [ "$remote_issuer" == "200" ]; then 40 | export remote_issuer=$(curl -k -u $REMOTE_USER:$REMOTE_TOKEN $REMOTE_URL/crumbIssuer/api/json -s) 41 | remote_issuer=$(python -c "import json;import os;issuer=os.getenv('remote_issuer');issuer=json.loads(issuer);print issuer['crumb']") 42 | else 43 | remote_issuer="" 44 | fi 45 | curl -k -u $REMOTE_USER:$REMOTE_TOKEN $1 --header "Jenkins-Crumb: $remote_issuer" -o $target_file 46 | else 47 | echo "not a correct url: $1" 48 | fi 49 | fi 50 | 51 | curl -k -u $JENKINS_USER:$JENKINS_TOKEN $JENKINS_URL/pluginManager/uploadPlugin -F "name=@$target_file" --header "Jenkins-Crumb: $issuer" 52 | curl -k -u $JENKINS_USER:$JENKINS_TOKEN $JENKINS_URL/restart -X POST --header "Jenkins-Crumb: $issuer" 53 | -------------------------------------------------------------------------------- /src/test/java/io/alauda/jenkins/devops/support/KubernetesClusterConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.devops.support; 2 | 3 | import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput; 4 | import com.gargoylesoftware.htmlunit.html.HtmlForm; 5 | import com.gargoylesoftware.htmlunit.html.HtmlTextInput; 6 | import org.junit.Test; 7 | import static org.junit.Assert.*; 8 | import org.junit.Rule; 9 | import org.jvnet.hudson.test.RestartableJenkinsRule; 10 | 11 | public class KubernetesClusterConfigurationTest { 12 | 13 | @Rule 14 | public RestartableJenkinsRule rr = new RestartableJenkinsRule(); 15 | 16 | @Test 17 | public void uiAndStorage() { 18 | final String masterUrl = "http://localhost/master"; 19 | final boolean defaultCluster = true; 20 | final boolean managerCluster = false; 21 | 22 | rr.then(r -> { 23 | assertNull("not set initially", KubernetesClusterConfiguration.get().getCluster()); 24 | HtmlForm config = r.createWebClient().goTo("configure").getFormByName("config"); 25 | HtmlTextInput textbox = config.getInputByName("_.masterUrl"); 26 | textbox.setText(masterUrl); 27 | HtmlCheckBoxInput checkBoxInput = config.getInputByName("_.skipTlsVerify"); 28 | checkBoxInput.setChecked(false); 29 | config.getInputByName("_.defaultCluster").setChecked(defaultCluster); 30 | config.getInputByName("_.managerCluster").setChecked(managerCluster); 31 | r.submit(config); 32 | 33 | // assert config result 34 | assertClusterConfig(masterUrl, defaultCluster, managerCluster); 35 | }); 36 | rr.then(r -> { 37 | assertClusterConfig(masterUrl, defaultCluster, managerCluster); 38 | }); 39 | } 40 | 41 | private void assertClusterConfig(final String masterUrl, final boolean defaultCluster, final boolean managerCluster) { 42 | KubernetesClusterConfiguration clusterConfig = KubernetesClusterConfiguration.get(); 43 | assertEquals("global config page let us edit it", masterUrl, 44 | clusterConfig.getCluster().getMasterUrl()); 45 | assertEquals("DefaultCluster setting is not working", defaultCluster, 46 | clusterConfig.getCluster().isDefaultCluster()); 47 | assertEquals("ManagerCluster setting is not working", managerCluster, 48 | clusterConfig.getCluster().isManagerCluster()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Example user template template 3 | ### Example user template 4 | 5 | # IntelliJ project files 6 | .idea 7 | *.iml 8 | out 9 | gen### JetBrains template 10 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 11 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 12 | 13 | # User-specific stuff 14 | .idea/**/tasks.xml 15 | .idea/**/dictionaries 16 | .idea/**/shelf 17 | 18 | # Sensitive or high-churn files 19 | .idea/**/dataSources/ 20 | .idea/**/dataSources.ids 21 | .idea/**/dataSources.local.xml 22 | .idea/**/sqlDataSources.xml 23 | .idea/**/dynamic.xml 24 | .idea/**/uiDesigner.xml 25 | .idea/**/dbnavigator.xml 26 | 27 | # Gradle 28 | .idea/**/gradle.xml 29 | # CMake 30 | cmake-build-debug/ 31 | cmake-build-release/ 32 | 33 | # Mongo Explorer plugin 34 | .idea/**/mongoSettings.xml 35 | 36 | # File-based project format 37 | *.iws 38 | 39 | # IntelliJ 40 | out/ 41 | 42 | # mpeltonen/sbt-idea plugin 43 | .idea_modules/ 44 | 45 | # JIRA plugin 46 | atlassian-ide-plugin.xml 47 | 48 | # Cursive Clojure plugin 49 | .idea/replstate.xml 50 | 51 | # Crashlytics plugin (for Android Studio and IntelliJ) 52 | com_crashlytics_export_strings.xml 53 | crashlytics.properties 54 | crashlytics-build.properties 55 | fabric.properties 56 | 57 | # Editor-based Rest Client 58 | .idea/httpRequests 59 | ### Java template 60 | # Compiled class file 61 | *.class 62 | 63 | # Log file 64 | *.log 65 | 66 | # BlueJ files 67 | *.ctxt 68 | 69 | # Mobile Tools for Java (J2ME) 70 | .mtj.tmp/ 71 | 72 | # Package Files # 73 | *.jar 74 | *.war 75 | *.nar 76 | *.ear 77 | *.zip 78 | *.tar.gz 79 | *.rar 80 | 81 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 82 | hs_err_pid* 83 | ### macOS template 84 | # General 85 | .DS_Store 86 | .AppleDouble 87 | .LSOverride 88 | 89 | # Icon must end with two \r 90 | Icon 91 | 92 | # Thumbnails 93 | ._* 94 | 95 | # Files that might appear in the root of a volume 96 | .DocumentRevisions-V100 97 | .fseventsd 98 | .Spotlight-V100 99 | .TemporaryItems 100 | .Trashes 101 | .VolumeIcon.icns 102 | .com.apple.timemachine.donotpresent 103 | 104 | # Directories potentially created on remote AFP share 105 | .AppleDB 106 | .AppleDesktop 107 | Network Trash Folder 108 | Temporary Items 109 | .apdisk 110 | 111 | work/ 112 | target/ 113 | scripts/env.sh 114 | 115 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/devops/support/KubernetesClusterConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.devops.support; 2 | 3 | import hudson.Extension; 4 | import io.alauda.jenkins.devops.support.client.Clients; 5 | import io.alauda.jenkins.devops.support.exception.KubernetesClientException; 6 | import io.alauda.jenkins.devops.support.utils.SyncPluginConfigurationCompatiblilityMigrater; 7 | import io.kubernetes.client.ApiClient; 8 | import io.kubernetes.client.Configuration; 9 | import jenkins.model.GlobalConfiguration; 10 | import org.kohsuke.stapler.DataBoundSetter; 11 | 12 | import java.util.LinkedList; 13 | import java.util.List; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.logging.Level; 16 | import java.util.logging.Logger; 17 | 18 | @Extension(ordinal = 203) 19 | public class KubernetesClusterConfiguration extends GlobalConfiguration { 20 | private static final Logger logger = Logger.getLogger(KubernetesClusterConfiguration.class.getName()); 21 | 22 | // We might config multiple servers in the future, so we use list to store them 23 | private List k8sClusters = new LinkedList<>(); 24 | 25 | public static KubernetesClusterConfiguration get() { 26 | return GlobalConfiguration.all().get(KubernetesClusterConfiguration.class); 27 | } 28 | 29 | 30 | public KubernetesClusterConfiguration() { 31 | // When Jenkins is restarted, load any saved configuration from disk. 32 | load(); 33 | KubernetesCluster clusterMigrateFromSync = SyncPluginConfigurationCompatiblilityMigrater.migrateConfigurationFromSyncPlugin(); 34 | if (clusterMigrateFromSync != null) { 35 | clusterMigrateFromSync.setDefaultCluster(true); 36 | clusterMigrateFromSync.setManagerCluster(true); 37 | setCluster(clusterMigrateFromSync); 38 | return; 39 | } 40 | 41 | 42 | if (k8sClusters.size() == 0) { 43 | KubernetesCluster cluster = new KubernetesCluster(); 44 | // WARN once we support multi-cluster configuration, you should change this behavior 45 | cluster.setDefaultCluster(true); 46 | cluster.setManagerCluster(true); 47 | setCluster(cluster); 48 | } else { 49 | triggerEvents(k8sClusters.get(0)); 50 | } 51 | 52 | } 53 | 54 | public KubernetesCluster getCluster() { 55 | if (k8sClusters == null || k8sClusters.size() == 0) { 56 | return null; 57 | } 58 | 59 | return k8sClusters.get(0); 60 | } 61 | 62 | 63 | /** 64 | * Together with {@link #getCluster()}, binds to entry in {@code config.jelly}. 65 | * 66 | * @param cluster the new value of this field 67 | */ 68 | @DataBoundSetter 69 | public void setCluster(KubernetesCluster cluster) { 70 | if (k8sClusters == null) { 71 | k8sClusters = new LinkedList<>(); 72 | } 73 | 74 | KubernetesCluster currentCluster = getCluster(); 75 | if (currentCluster != null && currentCluster.equals(cluster)) { 76 | return; 77 | } 78 | 79 | k8sClusters.clear(); 80 | k8sClusters.add(cluster); 81 | save(); 82 | 83 | triggerEvents(cluster); 84 | } 85 | 86 | 87 | private void triggerEvents(KubernetesCluster cluster) { 88 | try { 89 | ApiClient client = Clients.createClientFromCluster(cluster); 90 | client.getHttpClient().setReadTimeout(0, TimeUnit.SECONDS); 91 | // If we have more clusters to config in the future, we may need to remove this. 92 | Configuration.setDefaultApiClient(client); 93 | 94 | new Thread(() -> triggerConfigChangeEvent(cluster, client)).start(); 95 | } catch (KubernetesClientException e) { 96 | e.printStackTrace(); 97 | logger.log(Level.SEVERE, String.format("Unable to create client from cluster %s, reason %s", cluster.getMasterUrl(), e.getMessage())); 98 | new Thread(() -> triggerConfigErrorEvent(cluster, e)).start(); 99 | } 100 | } 101 | 102 | 103 | private void triggerConfigChangeEvent(KubernetesCluster cluster, ApiClient client) { 104 | KubernetesClusterConfigurationListener 105 | .all() 106 | .forEach(listener -> 107 | new Thread(() -> listener.onConfigChange(cluster, client)).start()); 108 | 109 | } 110 | 111 | private void triggerConfigErrorEvent(KubernetesCluster cluster, Throwable reason) { 112 | KubernetesClusterConfigurationListener.all() 113 | .forEach(listener -> 114 | listener.onConfigError(cluster, reason)); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Alauda.Jenkinsfile: -------------------------------------------------------------------------------- 1 | // https://jenkins.io/doc/book/pipeline/syntax/ 2 | // Multi-branch discovery pattern: PR-.* 3 | @Library('alauda-cicd') _ 4 | 5 | // global variables for pipeline 6 | def GIT_BRANCH 7 | def GIT_COMMIT 8 | def FOLDER = "." 9 | def DEBUG = false 10 | def deployment 11 | def RELEASE_VERSION 12 | def RELEASE_BUILD 13 | pipeline { 14 | // 运行node条件 15 | // 为了扩容jenkins的功能一般情况会分开一些功能到不同的node上面 16 | // 这样每个node作用比较清晰,并可以并行处理更多的任务量 17 | agent { label 'golang && java' } 18 | 19 | // (optional) 流水线全局设置 20 | options { 21 | // 保留多少流水线记录(建议不放在jenkinsfile里面) 22 | buildDiscarder(logRotator(numToKeepStr: '10')) 23 | 24 | // 不允许并行执行 25 | disableConcurrentBuilds() 26 | } 27 | 28 | //(optional) 环境变量 29 | environment { 30 | // for building an scanning 31 | REPOSITORY = "alauda-kubernetes-support-plugin" 32 | OWNER = "alauda" 33 | // sonar feedback user 34 | // needs to change together with the credentialsID 35 | SCM_FEEDBACK_ACCOUNT = "alaudabot" 36 | SONARQUBE_SCM_CREDENTIALS = "alaudabot" 37 | DEPLOYMENT = "alauda-kubernetes-support-plugin" 38 | DINGDING_BOT = "devops-chat-bot" 39 | TAG_CREDENTIALS = "alaudabot-github" 40 | } 41 | // stages 42 | stages { 43 | stage('Checkout') { 44 | steps { 45 | script { 46 | // checkout code 47 | def scmVars = checkout scm 48 | // extract git information 49 | env.GIT_COMMIT = scmVars.GIT_COMMIT 50 | env.GIT_BRANCH = scmVars.GIT_BRANCH 51 | GIT_COMMIT = "${scmVars.GIT_COMMIT}" 52 | GIT_BRANCH = "${scmVars.GIT_BRANCH}" 53 | pom = readMavenPom file: 'pom.xml' 54 | //RELEASE_VERSION = pom.properties['revision'] + pom.properties['sha1'] + pom.properties['changelist'] 55 | RELEASE_VERSION = pom.version 56 | } 57 | container('golang'){ 58 | // installing golang coverage and report tools 59 | sh "go get -u github.com/alauda/gitversion" 60 | script { 61 | if (GIT_BRANCH != "master") { 62 | def branch = GIT_BRANCH.replace("/","-").replace("_","-") 63 | RELEASE_BUILD = "${RELEASE_VERSION}.${branch}.${env.BUILD_NUMBER}" 64 | } else { 65 | sh "gitversion patch ${RELEASE_VERSION} > patch" 66 | RELEASE_BUILD = readFile("patch").trim() 67 | } 68 | 69 | sh ''' 70 | echo "commit=$GIT_COMMIT" > src/main/resources/debug.properties 71 | echo "build=$RELEASE_BUILD" >> src/main/resources/debug.properties 72 | echo "version=RELEASE_VERSION" >> src/main/resources/debug.properties 73 | cat src/main/resources/debug.properties 74 | ''' 75 | } 76 | } 77 | } 78 | } 79 | stage('Build') { 80 | steps { 81 | script { 82 | container('java'){ 83 | sh """ 84 | mvn clean install -U findbugs:findbugs -Dmaven.test.skip=true 85 | """ 86 | } 87 | 88 | archiveArtifacts 'target/*.hpi' 89 | } 90 | } 91 | } 92 | // stage('Release') { 93 | // when { 94 | // expression { 95 | // GIT_BRANCH == "master" 96 | // } 97 | // } 98 | // steps { 99 | // script { 100 | // container('java'){ 101 | // if(RELEASE_VERSION.endsWith('-SNAPSHOT')){ 102 | // sh 'mvn clean deploy -U -DskipTests' 103 | // } else { 104 | // } 105 | // } 106 | // } 107 | // } 108 | // } 109 | // sonar scan 110 | stage('Sonar') { 111 | steps { 112 | script { 113 | container('tools') { 114 | deploy.scan( 115 | REPOSITORY, 116 | GIT_BRANCH, 117 | SONARQUBE_SCM_CREDENTIALS, 118 | FOLDER, 119 | DEBUG, 120 | OWNER, 121 | SCM_FEEDBACK_ACCOUNT).start() 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | // (optional) 129 | // happens at the end of the pipeline 130 | post { 131 | // 成功 132 | success { 133 | echo "Horay!" 134 | script { 135 | deploy.notificationSuccess(DEPLOYMENT, DINGDING_BOT, "流水线完成了", RELEASE_BUILD) 136 | } 137 | } 138 | // 失败 139 | failure { 140 | // check the npm log 141 | // fails lets check if it 142 | script { 143 | echo "damn!" 144 | deploy.notificationFailed(DEPLOYMENT, DINGDING_BOT, "流水线失败了", RELEASE_BUILD) 145 | } 146 | } 147 | always { junit allowEmptyResults: true, testResults: '**/target/surefire-reports/**/*.xml' } 148 | } 149 | } 150 | 151 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/devops/support/client/Clients.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.devops.support.client; 2 | 3 | import io.alauda.jenkins.devops.support.KubernetesCluster; 4 | import io.alauda.jenkins.devops.support.exception.KubernetesClientException; 5 | import io.alauda.jenkins.devops.support.utils.CredentialsUtils; 6 | import io.kubernetes.client.ApiClient; 7 | import io.kubernetes.client.util.Config; 8 | import okio.Buffer; 9 | import org.apache.commons.lang.StringUtils; 10 | import org.jenkinsci.remoting.util.Charsets; 11 | 12 | import javax.annotation.Nonnull; 13 | import java.io.File; 14 | import java.io.FileNotFoundException; 15 | import java.io.IOException; 16 | import java.nio.file.Files; 17 | import java.nio.file.Paths; 18 | import java.security.GeneralSecurityException; 19 | import java.util.logging.Level; 20 | import java.util.logging.Logger; 21 | 22 | public final class Clients { 23 | 24 | private static final Logger logger = Logger.getLogger(Clients.class.getName()); 25 | 26 | private Clients() { 27 | } 28 | 29 | /** 30 | * Return a client. If client not exists, it will create a client from the cluster then return it. 31 | * 32 | * @param cluster Kubernetes cluster 33 | * @return Client that can connect to correspondent cluster, null if we cannot create client from cluster. 34 | */ 35 | @Nonnull 36 | public static ApiClient createClientFromCluster(@Nonnull KubernetesCluster cluster) throws KubernetesClientException { 37 | ApiClient client; 38 | // if master url is empty, we create client from local cluster 39 | if (StringUtils.isEmpty(cluster.getMasterUrl())) { 40 | try { 41 | client = Config.fromCluster(); 42 | return client; 43 | } catch (IOException e) { 44 | logger.log(Level.SEVERE, String.format("Unable to create a client from local cluster, reason %s", e.getMessage()), e); 45 | throw new KubernetesClientException(e); 46 | } 47 | } 48 | 49 | String token = ""; 50 | try { 51 | if (StringUtils.isEmpty(cluster.getCredentialsId())) { 52 | token = getTokenFromLocalCluster(); 53 | } else { 54 | token = CredentialsUtils.getToken(cluster.getCredentialsId()); 55 | } 56 | } catch (GeneralSecurityException | IOException e) { 57 | logger.log(Level.WARNING, String.format("Unable to get token for k8s client, reason %s", e.getMessage()), e); 58 | } 59 | client = Config.fromToken(cluster.getMasterUrl(), token, !cluster.isSkipTlsVerify()); 60 | 61 | if (!cluster.isSkipTlsVerify()) { 62 | try { 63 | Buffer buffer = new Buffer(); 64 | 65 | if (!StringUtils.isEmpty(cluster.getServerCertificateAuthority())) { 66 | if (new File(cluster.getServerCertificateAuthority()).isFile()) { 67 | buffer.write(Files.readAllBytes(Paths.get(cluster.getServerCertificateAuthority()))); 68 | } else { 69 | buffer.writeUtf8(cluster.getServerCertificateAuthority()); 70 | } 71 | } else { 72 | buffer.writeUtf8(getCAFromLocalCluster()); 73 | } 74 | if (buffer.size() != 0) { 75 | client.setSslCaCert(buffer.inputStream()); 76 | } 77 | } catch (IOException e) { 78 | logger.log(Level.WARNING, String.format("Unable to get ca for k8s client, reason %s", e.getMessage()), e); 79 | } 80 | } 81 | return client; 82 | } 83 | 84 | public static ApiClient createClientFromConfig(String masterUrl, String credentialsId, String serverCertificateAuthority, boolean skipTlsVerify) throws KubernetesClientException { 85 | KubernetesCluster cluster = new KubernetesCluster(); 86 | cluster.setMasterUrl(masterUrl); 87 | cluster.setCredentialsId(credentialsId); 88 | cluster.setServerCertificateAuthority(serverCertificateAuthority); 89 | cluster.setSkipTlsVerify(skipTlsVerify); 90 | 91 | return createClientFromCluster(cluster); 92 | } 93 | 94 | 95 | @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( 96 | value = "DMI_HARDCODED_ABSOLUTE_FILENAME", 97 | justification = "I know what I'm doing") 98 | @Nonnull 99 | private static String getTokenFromLocalCluster() throws IOException { 100 | if (Files.exists(Paths.get(Config.SERVICEACCOUNT_TOKEN_PATH))) { 101 | return new String(Files.readAllBytes(Paths.get(Config.SERVICEACCOUNT_TOKEN_PATH)), Charsets.UTF_8); 102 | } 103 | throw new FileNotFoundException(String.format("Unable to get token from %s", Config.SERVICEACCOUNT_TOKEN_PATH)); 104 | } 105 | 106 | @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( 107 | value = "DMI_HARDCODED_ABSOLUTE_FILENAME", 108 | justification = "I know what I'm doing") 109 | @Nonnull 110 | private static String getCAFromLocalCluster() throws IOException { 111 | if (Files.exists(Paths.get(Config.SERVICEACCOUNT_CA_PATH))) { 112 | return new String(Files.readAllBytes(Paths.get(Config.SERVICEACCOUNT_CA_PATH)), Charsets.UTF_8); 113 | } 114 | throw new FileNotFoundException(String.format("Unable to get CA from %s", Config.SERVICEACCOUNT_CA_PATH)); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/devops/support/utils/SyncPluginConfigurationCompatiblilityMigrater.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.devops.support.utils; 2 | 3 | import io.alauda.jenkins.devops.support.KubernetesCluster; 4 | import jenkins.model.Jenkins; 5 | import org.apache.commons.io.output.FileWriterWithEncoding; 6 | import org.w3c.dom.Document; 7 | import org.w3c.dom.Element; 8 | import org.w3c.dom.NodeList; 9 | import org.xml.sax.SAXException; 10 | 11 | import javax.xml.XMLConstants; 12 | import javax.xml.parsers.DocumentBuilder; 13 | import javax.xml.parsers.DocumentBuilderFactory; 14 | import javax.xml.parsers.ParserConfigurationException; 15 | import javax.xml.transform.*; 16 | import javax.xml.transform.dom.DOMSource; 17 | import javax.xml.transform.stream.StreamResult; 18 | import java.io.File; 19 | import java.io.IOException; 20 | import java.nio.charset.StandardCharsets; 21 | import java.util.logging.Level; 22 | import java.util.logging.Logger; 23 | 24 | public class SyncPluginConfigurationCompatiblilityMigrater { 25 | private static final Logger logger = Logger.getLogger(SyncPluginConfigurationCompatiblilityMigrater.class.getName()); 26 | private static final String SYNC_PLUGIN_CONFIGURATION_FILE_NAME = "io.alauda.jenkins.devops.sync.AlaudaSyncGlobalConfiguration.xml"; 27 | 28 | 29 | public static KubernetesCluster migrateConfigurationFromSyncPlugin() { 30 | File configFile = new File(Jenkins.getInstance().getRootDir(), SYNC_PLUGIN_CONFIGURATION_FILE_NAME); 31 | if (!configFile.exists() || !configFile.isFile()) { 32 | return null; 33 | } 34 | 35 | try { 36 | Document configDocument = readFileToXMLDocument(configFile); 37 | Element documentElement = configDocument.getDocumentElement(); 38 | if (documentElement == null) { 39 | return null; 40 | } 41 | 42 | KubernetesCluster cluster = new KubernetesCluster(); 43 | NodeList trustCertsNodeList = configDocument.getElementsByTagName("trustCerts"); 44 | if (trustCertsNodeList == null || trustCertsNodeList.getLength() == 0) { 45 | return null; 46 | } 47 | String trustCertsStr = trustCertsNodeList.item(0).getTextContent(); 48 | if ("true".equals(trustCertsStr) || "false".equals(trustCertsStr)) { 49 | cluster.setSkipTlsVerify(Boolean.parseBoolean(trustCertsStr)); 50 | } else { 51 | return null; 52 | } 53 | removeNodeList(trustCertsNodeList); 54 | 55 | NodeList serverNodeList = configDocument.getElementsByTagName("server"); 56 | if (serverNodeList == null || serverNodeList.getLength() == 0) { 57 | return null; 58 | } 59 | cluster.setMasterUrl(serverNodeList.item(0).getTextContent()); 60 | removeNodeList(serverNodeList); 61 | 62 | NodeList credentialsNodeList = configDocument.getElementsByTagName("credentialsId"); 63 | if (credentialsNodeList != null && credentialsNodeList.getLength() > 0) { 64 | cluster.setCredentialsId(credentialsNodeList.item(0).getTextContent()); 65 | removeNodeList(credentialsNodeList); 66 | } 67 | 68 | writeXMLDocumentToFile(configDocument, configFile); 69 | return cluster; 70 | } catch (ParserConfigurationException | IOException | TransformerException | SAXException e) { 71 | logger.log(Level.FINE, String.format("Unable to migrate configuration from old sync plugin, will skip it, reason: %s", e.getMessage()), e); 72 | } 73 | return null; 74 | } 75 | 76 | private static Document readFileToXMLDocument(File xml) throws ParserConfigurationException, IOException, SAXException { 77 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 78 | try { 79 | factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 80 | } catch (ParserConfigurationException e) { 81 | logger.log(Level.WARNING, "Failed to set FEATURE_SECURE_PROCESSING to true", e); 82 | } 83 | DocumentBuilder builder = factory.newDocumentBuilder(); 84 | 85 | return builder.parse(xml); 86 | } 87 | 88 | private static void writeXMLDocumentToFile(Document document, File xml) throws TransformerException, IOException { 89 | Transformer transformer = newSecureTransformerFactory().newTransformer(); 90 | transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 91 | 92 | StreamResult result = new StreamResult(new FileWriterWithEncoding(xml, StandardCharsets.UTF_8)); 93 | DOMSource source = new DOMSource(document); 94 | 95 | transformer.transform(source, result); 96 | } 97 | 98 | private static void removeNodeList(NodeList nodeList) { 99 | if (nodeList == null) { 100 | return; 101 | } 102 | for (int i = 0; i < nodeList.getLength(); i++) { 103 | nodeList.item(i).getParentNode().removeChild(nodeList.item(i)); 104 | } 105 | } 106 | 107 | private static TransformerFactory newSecureTransformerFactory() { 108 | TransformerFactory transformerFactory = TransformerFactory.newInstance(); 109 | try { 110 | transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 111 | } catch (TransformerConfigurationException e) { 112 | e.printStackTrace(); 113 | transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 114 | transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); 115 | } 116 | return transformerFactory; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.jenkins-ci.plugins 6 | plugin 7 | 2.36 8 | 9 | 10 | io.alauda.jenkins.plugins 11 | alauda-kubernetes-support 12 | 2.3.1-SNAPSHOT 13 | hpi 14 | 15 | 16 | 1.0 17 | -SNAPSHOT 18 | 2.83 19 | 8 20 | 21 | 22 | Alauda Kubernetes Suport Plugin 23 | This plugin allows users to config kubernetes and share configuration to other plugins. 24 | 25 | 26 | Alauda 27 | http://alauda.io 28 | 29 | 30 | 31 | 32 | MIT License 33 | https://opensource.org/licenses/MIT 34 | 35 | 36 | 37 | 38 | 39 | alaudaDevelopers 40 | Alauda Developers 41 | alauda 42 | http://alauda.io/ 43 | devs@alauda.io 44 | 45 | 46 | 47 | https://wiki.jenkins.io/display/JENKINS/Alauda+Kubernetes+Support+Plugin 48 | 49 | scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git 50 | scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git 51 | https://github.com/jenkinsci/${project.artifactId}-plugin 52 | HEAD 53 | 54 | 55 | 56 | 57 | repo.jenkins-ci.org 58 | https://repo.jenkins-ci.org/public/ 59 | 60 | 61 | 62 | 63 | repo.jenkins-ci.org 64 | https://repo.jenkins-ci.org/public/ 65 | 66 | 67 | 68 | 69 | 70 | io.kubernetes 71 | client-java 72 | 6.0.1 73 | 74 | 75 | org.bouncycastle 76 | bcprov-jdk15on 77 | 78 | 79 | org.bouncycastle 80 | bcpkix-jdk15on 81 | 82 | 83 | org.bouncycastle 84 | bcpg-jdk15on 85 | 86 | 87 | org.bouncycastle 88 | bcprov-ext-jdk15on 89 | 90 | 91 | 92 | 93 | io.kubernetes 94 | client-java-extended 95 | 6.0.1 96 | 97 | 98 | org.bouncycastle 99 | bcprov-jdk15on 100 | 101 | 102 | org.bouncycastle 103 | bcpkix-jdk15on 104 | 105 | 106 | org.bouncycastle 107 | bcpg-jdk15on 108 | 109 | 110 | org.bouncycastle 111 | bcprov-ext-jdk15on 112 | 113 | 114 | 115 | 116 | 117 | org.bouncycastle 118 | bcprov-jdk15on 119 | 1.59 120 | 121 | 122 | org.bouncycastle 123 | bcpkix-jdk15on 124 | 1.59 125 | 126 | 127 | org.bouncycastle 128 | bcpg-jdk15on 129 | 1.59 130 | 131 | 132 | org.bouncycastle 133 | bcprov-ext-jdk15on 134 | 1.59 135 | 136 | 137 | org.jenkins-ci.plugins 138 | credentials 139 | 2.1.16 140 | 141 | 142 | com.squareup.okhttp3 143 | okhttp 144 | 3.12.0 145 | 146 | 147 | org.jenkins-ci.plugins 148 | plain-credentials 149 | 1.4 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/devops/support/KubernetesCluster.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.devops.support; 2 | 3 | import com.cloudbees.plugins.credentials.common.StandardListBoxModel; 4 | import hudson.Extension; 5 | import hudson.model.AbstractDescribableImpl; 6 | import hudson.model.Descriptor; 7 | import hudson.security.ACL; 8 | import hudson.util.FormValidation; 9 | import hudson.util.ListBoxModel; 10 | import io.alauda.jenkins.devops.support.client.Clients; 11 | import io.alauda.jenkins.devops.support.exception.KubernetesClientException; 12 | import io.kubernetes.client.ApiClient; 13 | import io.kubernetes.client.ApiException; 14 | import io.kubernetes.client.apis.CoreV1Api; 15 | import io.kubernetes.client.models.V1NamespaceList; 16 | import jenkins.model.Jenkins; 17 | import org.jenkinsci.plugins.plaincredentials.StringCredentials; 18 | import org.kohsuke.stapler.DataBoundConstructor; 19 | import org.kohsuke.stapler.DataBoundSetter; 20 | import org.kohsuke.stapler.QueryParameter; 21 | 22 | import java.util.Objects; 23 | 24 | public class KubernetesCluster extends AbstractDescribableImpl { 25 | private String masterUrl; 26 | private String credentialsId; 27 | private boolean skipTlsVerify = false; 28 | private String serverCertificateAuthority; 29 | private boolean defaultCluster = false; 30 | private boolean managerCluster = false; 31 | 32 | @DataBoundConstructor 33 | public KubernetesCluster() { 34 | } 35 | 36 | public String getMasterUrl() { 37 | return masterUrl; 38 | } 39 | 40 | @DataBoundSetter 41 | public void setMasterUrl(String masterUrl) { 42 | this.masterUrl = masterUrl; 43 | } 44 | 45 | public String getCredentialsId() { 46 | return credentialsId; 47 | } 48 | 49 | @DataBoundSetter 50 | public void setCredentialsId(String credentialId) { 51 | this.credentialsId = credentialId; 52 | } 53 | 54 | public boolean isSkipTlsVerify() { 55 | return skipTlsVerify; 56 | } 57 | 58 | @DataBoundSetter 59 | public void setSkipTlsVerify(boolean skipTlsVerify) { 60 | this.skipTlsVerify = skipTlsVerify; 61 | } 62 | 63 | public String getServerCertificateAuthority() { 64 | return serverCertificateAuthority; 65 | } 66 | 67 | @DataBoundSetter 68 | public void setServerCertificateAuthority(String serverCertificateAuthority) { 69 | this.serverCertificateAuthority = serverCertificateAuthority; 70 | } 71 | 72 | public boolean isDefaultCluster() { 73 | return defaultCluster; 74 | } 75 | 76 | @DataBoundSetter 77 | public void setDefaultCluster(boolean defaultCluster) { 78 | this.defaultCluster = defaultCluster; 79 | } 80 | 81 | public boolean isManagerCluster() { 82 | return managerCluster; 83 | } 84 | 85 | @DataBoundSetter 86 | public void setManagerCluster(boolean managerCluster) { 87 | this.managerCluster = managerCluster; 88 | } 89 | 90 | @Override 91 | public boolean equals(Object o) { 92 | if (this == o) return true; 93 | if (o == null || getClass() != o.getClass()) return false; 94 | KubernetesCluster cluster = (KubernetesCluster) o; 95 | return skipTlsVerify == cluster.skipTlsVerify && 96 | defaultCluster == cluster.defaultCluster && 97 | managerCluster == cluster.managerCluster && 98 | Objects.equals(masterUrl, cluster.masterUrl) && 99 | Objects.equals(credentialsId, cluster.credentialsId) && 100 | Objects.equals(serverCertificateAuthority, cluster.serverCertificateAuthority); 101 | } 102 | 103 | @Override 104 | public int hashCode() { 105 | return Objects.hash(masterUrl, credentialsId, skipTlsVerify, serverCertificateAuthority, defaultCluster, managerCluster); 106 | } 107 | 108 | @Extension 109 | public static class AlaudaDevOpsK8sServerDescriptor extends Descriptor { 110 | 111 | public ListBoxModel doFillCredentialsIdItems(@QueryParameter String credentialsId) { 112 | if (credentialsId == null) { 113 | credentialsId = ""; 114 | } 115 | 116 | if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) { 117 | return new StandardListBoxModel() 118 | .includeCurrentValue(credentialsId); 119 | } 120 | 121 | return new StandardListBoxModel() 122 | .includeEmptyValue() 123 | .includeAs(ACL.SYSTEM, Jenkins.getInstance(), 124 | StringCredentials.class) 125 | .includeCurrentValue(credentialsId); 126 | } 127 | 128 | 129 | public FormValidation doVerifyConnect(@QueryParameter String masterUrl, 130 | @QueryParameter String credentialsId, 131 | @QueryParameter String serverCertificateAuthority, 132 | @QueryParameter boolean skipTlsVerify) { 133 | 134 | Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); 135 | 136 | ApiClient testClient; 137 | try { 138 | testClient = Clients.createClientFromConfig(masterUrl, credentialsId, serverCertificateAuthority, skipTlsVerify); 139 | } catch (KubernetesClientException e) { 140 | e.printStackTrace(); 141 | return FormValidation.error(e.getMessage()); 142 | } 143 | 144 | CoreV1Api api = new CoreV1Api(testClient); 145 | V1NamespaceList list; 146 | try { 147 | list = api.listNamespace(null, null, null, null, null, null, null, null); 148 | } catch (ApiException e) { 149 | return FormValidation.error(e.getMessage()); 150 | } 151 | 152 | if (list == null) { 153 | return FormValidation.error(String.format("Unable to connect to cluster %s", masterUrl)); 154 | } 155 | 156 | return FormValidation.ok(String.format("Connect to %s succeed", masterUrl)); 157 | } 158 | } 159 | } 160 | --------------------------------------------------------------------------------