├── Jenkinsfile ├── images └── plugin-configuration.png ├── src └── main │ ├── resources │ ├── index.jelly │ └── io │ │ └── alauda │ │ └── jenkins │ │ └── plugins │ │ └── credentials │ │ └── KubernetesCredentialsProviderConfiguration │ │ └── config.jelly │ ├── webapp │ └── images │ │ ├── 16x16 │ │ └── alauda.png │ │ ├── 24x24 │ │ └── alauda.png │ │ ├── 32x32 │ │ └── alauda.png │ │ └── 48x48 │ │ └── alauda.png │ └── java │ └── io │ └── alauda │ └── jenkins │ └── plugins │ └── credentials │ ├── metadata │ ├── NamespaceProvider.java │ ├── MetadataProvider.java │ └── CredentialsWithMetadata.java │ ├── CredentialsUtils.java │ ├── rule │ └── KubernetesSecretRule.java │ ├── convertor │ ├── ServiceAccountTokenCredentialsConverter.java │ ├── OpaqueCredentialsConverter.java │ ├── UsernamePasswordCredentialsConvertor.java │ ├── CredentialsConversionException.java │ └── SecretToCredentialConverter.java │ ├── scope │ ├── JenkinsRootScope.java │ └── KubernetesSecretScope.java │ ├── KubernetesCredentialsProviderConfiguration.java │ ├── AlaudaKubernetesCredentialsStore.java │ ├── SecretUtils.java │ └── KubernetesCredentialsProvider.java ├── scripts ├── run.sh ├── restart.sh └── upload.sh ├── sonar-project.properties ├── README.md ├── LICENSE ├── .gitignore ├── pom.xml └── Alauda.Jenkinsfile /Jenkinsfile: -------------------------------------------------------------------------------- 1 | buildPlugin() -------------------------------------------------------------------------------- /images/plugin-configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/alauda-devops-credentials-provider-plugin/master/images/plugin-configuration.png -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | This plugin will provide a credentials store backed by Kubernetes 4 |
5 | -------------------------------------------------------------------------------- /src/main/webapp/images/16x16/alauda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/alauda-devops-credentials-provider-plugin/master/src/main/webapp/images/16x16/alauda.png -------------------------------------------------------------------------------- /src/main/webapp/images/24x24/alauda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/alauda-devops-credentials-provider-plugin/master/src/main/webapp/images/24x24/alauda.png -------------------------------------------------------------------------------- /src/main/webapp/images/32x32/alauda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/alauda-devops-credentials-provider-plugin/master/src/main/webapp/images/32x32/alauda.png -------------------------------------------------------------------------------- /src/main/webapp/images/48x48/alauda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/alauda-devops-credentials-provider-plugin/master/src/main/webapp/images/48x48/alauda.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/plugins/credentials/metadata/NamespaceProvider.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.plugins.credentials.metadata; 2 | 3 | import hudson.Extension; 4 | import io.kubernetes.client.models.V1Secret; 5 | 6 | @Extension 7 | public class NamespaceProvider implements MetadataProvider { 8 | 9 | public static final String NAMESPACE_METADATA = "namespace"; 10 | 11 | @Override 12 | public void attach(V1Secret secret, CredentialsWithMetadata credentialsWithMetadata) { 13 | credentialsWithMetadata.addMetadata(NAMESPACE_METADATA, secret.getMetadata().getNamespace()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | # must be unique in a given SonarQube instance 2 | sonar.projectKey=alauda-devops-credentials-provider 3 | # this is the name and version displayed in the SonarQube UI. Was mandatory prior to SonarQube 6.1. 4 | sonar.projectName=alauda-devops-credentials-provider-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/resources/io/alauda/jenkins/plugins/credentials/KubernetesCredentialsProviderConfiguration/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/plugins/credentials/CredentialsUtils.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.plugins.credentials; 2 | 3 | import com.cloudbees.plugins.credentials.CredentialsScope; 4 | import hudson.util.Secret; 5 | import io.kubernetes.client.models.V1ObjectMeta; 6 | import org.jenkinsci.plugins.plaincredentials.StringCredentials; 7 | import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; 8 | 9 | public final class CredentialsUtils { 10 | 11 | private CredentialsUtils() {} 12 | 13 | public static StringCredentials convertToStringCredentials(V1ObjectMeta meta, String token) { 14 | return new StringCredentialsImpl(CredentialsScope.GLOBAL, SecretUtils.getCredentialId(meta), 15 | SecretUtils.getCredentialDescription(meta), Secret.fromString(token)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alauda DevOps Credentials Provider plugin 2 | 3 | 4 | This plugin will sync secrets from Kubernetes to Jenkins and convert them to Credentials stored in memory. 5 | 6 | It provides four extension points for other plugins to implement to have more powerful functionality. 7 | 8 | 1. SecretToCredentialConverter defines which type of secret should be convert and how to convert it. 9 | 2. MetadataProvider allows adding metadata to Credentials 10 | 3. KubernetesSecretRule defines which secret should 11 | 4. KubernetesSecretScope defines where those secrets can be used 12 | 13 | ### Configuration 14 | ![Plugin Configuration](./images/plugin-configuration.png "Plugin Configuration") 15 | 16 | - Global Namespaces - secrets in these namespaces can be used globally 17 | - Label Selector - label selector to watch a group of secrets -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/plugins/credentials/metadata/MetadataProvider.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.plugins.credentials.metadata; 2 | 3 | import hudson.ExtensionList; 4 | import hudson.ExtensionPoint; 5 | import io.kubernetes.client.models.V1Secret; 6 | 7 | /** 8 | * Provide metadata sources that implementations can get metadata from sources and add them to credentials. 9 | */ 10 | public interface MetadataProvider extends ExtensionPoint { 11 | 12 | /** 13 | * Get metadata from K8s secret and add them to its corresponding credentials 14 | * @param secret Secret in K8s 15 | * @param credentialsWithMetadata credentials that converted from Secret 16 | */ 17 | void attach(V1Secret secret, CredentialsWithMetadata credentialsWithMetadata); 18 | 19 | static ExtensionList all() { 20 | return ExtensionList.lookup(MetadataProvider.class); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/plugins/credentials/metadata/CredentialsWithMetadata.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.plugins.credentials.metadata; 2 | 3 | import com.cloudbees.plugins.credentials.common.IdCredentials; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | public class CredentialsWithMetadata { 9 | 10 | private C credentials; 11 | private Map metadata = new HashMap<>(); 12 | 13 | public CredentialsWithMetadata(C credentials) { 14 | this.credentials = credentials; 15 | } 16 | 17 | public C getCredentials() { 18 | return credentials; 19 | } 20 | 21 | public void setCredentials(C credentials) { 22 | this.credentials = credentials; 23 | } 24 | 25 | public void addMetadata(String key, String value) { 26 | metadata.put(key, value); 27 | } 28 | 29 | public String getMetadata(String key) { 30 | return metadata.get(key); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/plugins/credentials/rule/KubernetesSecretRule.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.plugins.credentials.rule; 2 | 3 | import hudson.ExtensionList; 4 | import hudson.ExtensionPoint; 5 | import io.kubernetes.client.models.V1Secret; 6 | 7 | /** 8 | * Defines a series of rules that apply to secret 9 | */ 10 | public interface KubernetesSecretRule extends ExtensionPoint { 11 | 12 | /** 13 | * Check if the secret should be excluded. 14 | * @param secret secret will be checked. 15 | * @return true, if the secret should be excluded. 16 | */ 17 | boolean exclude(V1Secret secret); 18 | 19 | static ExtensionList all() { 20 | return ExtensionList.lookup(KubernetesSecretRule.class); 21 | } 22 | 23 | static boolean shouldExclude(V1Secret secret) { 24 | ExtensionList filters = all(); 25 | 26 | return filters.stream().anyMatch(e -> e.exclude(secret)); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/plugins/credentials/convertor/ServiceAccountTokenCredentialsConverter.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.plugins.credentials.convertor; 2 | 3 | import com.cloudbees.plugins.credentials.common.IdCredentials; 4 | import hudson.Extension; 5 | import io.alauda.jenkins.plugins.credentials.CredentialsUtils; 6 | import io.alauda.jenkins.plugins.credentials.SecretUtils; 7 | import io.kubernetes.client.models.V1Secret; 8 | 9 | @Extension 10 | public class ServiceAccountTokenCredentialsConverter extends SecretToCredentialConverter { 11 | private static final String SERVICE_ACCOUNT_TOKEN_TYPE = "kubernetes.io/service-account-token"; 12 | 13 | 14 | @Override 15 | public boolean canConvert(String type) { 16 | return SERVICE_ACCOUNT_TOKEN_TYPE.equals(type); 17 | } 18 | 19 | @Override 20 | public IdCredentials convert(V1Secret secret) throws CredentialsConversionException { 21 | SecretUtils.requireNonNull(secret.getData(), "kubernetes.io/service-account-token definition contains no data"); 22 | String token = SecretUtils.getNonNullSecretData(secret, "token", "kubernetes.io/service-account-token credential is missing the token"); 23 | 24 | return CredentialsUtils.convertToStringCredentials(secret.getMetadata(), token); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/plugins/credentials/convertor/OpaqueCredentialsConverter.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.plugins.credentials.convertor; 2 | 3 | import com.cloudbees.plugins.credentials.common.IdCredentials; 4 | import hudson.Extension; 5 | import io.alauda.jenkins.plugins.credentials.CredentialsUtils; 6 | import io.alauda.jenkins.plugins.credentials.SecretUtils; 7 | import io.kubernetes.client.models.V1Secret; 8 | 9 | import java.util.Map; 10 | import java.util.Optional; 11 | 12 | @Extension 13 | public class OpaqueCredentialsConverter extends SecretToCredentialConverter { 14 | private static final String OPAQUE_TYPE = "Opaque"; 15 | 16 | @Override 17 | public boolean canConvert(String type) { 18 | return OPAQUE_TYPE.equals(type); 19 | } 20 | 21 | @Override 22 | public IdCredentials convert(V1Secret secret) throws CredentialsConversionException { 23 | Map data = secret.getData(); 24 | if(data == null) { 25 | return null; 26 | } 27 | 28 | Optional tokenOpt = SecretUtils.getOptionalSecretData(secret, "token", ""); 29 | if(tokenOpt.isPresent()) { 30 | String token = tokenOpt.get(); 31 | return CredentialsUtils.convertToStringCredentials(secret.getMetadata(), token); 32 | } 33 | 34 | return null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/plugins/credentials/scope/JenkinsRootScope.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.plugins.credentials.scope; 2 | 3 | import hudson.Extension; 4 | import hudson.model.ItemGroup; 5 | import io.alauda.jenkins.plugins.credentials.KubernetesCredentialsProviderConfiguration; 6 | import io.alauda.jenkins.plugins.credentials.metadata.CredentialsWithMetadata; 7 | import io.alauda.jenkins.plugins.credentials.metadata.NamespaceProvider; 8 | import jenkins.model.Jenkins; 9 | import org.apache.commons.lang.StringUtils; 10 | 11 | import java.util.Arrays; 12 | 13 | @Extension 14 | public class JenkinsRootScope implements KubernetesSecretScope { 15 | @Override 16 | public boolean isInScope(ItemGroup owner) { 17 | return owner == Jenkins.getInstance(); 18 | } 19 | 20 | @Override 21 | public boolean shouldShowInScope(ItemGroup owner, CredentialsWithMetadata credentialsWithMetadata) { 22 | if (!isInScope(owner)) { 23 | return false; 24 | } 25 | 26 | String namespace = credentialsWithMetadata.getMetadata(NamespaceProvider.NAMESPACE_METADATA); 27 | if (StringUtils.isEmpty(namespace)) { 28 | return false; 29 | } 30 | 31 | String globalNamespaces = KubernetesCredentialsProviderConfiguration.get().getGlobalNamespaces(); 32 | if (globalNamespaces == null) { 33 | return false; 34 | } 35 | 36 | return Arrays.asList(globalNamespaces.split(",")).contains(namespace); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/plugins/credentials/scope/KubernetesSecretScope.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.plugins.credentials.scope; 2 | 3 | import hudson.ExtensionList; 4 | import hudson.ExtensionPoint; 5 | import hudson.model.ItemGroup; 6 | import io.alauda.jenkins.plugins.credentials.metadata.CredentialsWithMetadata; 7 | 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | 11 | /** 12 | * Scope defines where we can see credentials and what credentials we should display in these places. 13 | */ 14 | public interface KubernetesSecretScope extends ExtensionPoint { 15 | 16 | /** 17 | * Check if this scope should include ItemGroup. 18 | * @param owner ItemGroup will be checked. 19 | * @return true if the scope includes this ItemGroup. 20 | */ 21 | boolean isInScope(ItemGroup owner); 22 | 23 | /** 24 | * Check if the credentials should show under the ItemGroup 25 | * @return true if the credentials should show in the ItemGroup 26 | */ 27 | boolean shouldShowInScope(ItemGroup owner, CredentialsWithMetadata credentialsWithMetadata); 28 | 29 | static ExtensionList all() { 30 | return ExtensionList.lookup(KubernetesSecretScope.class); 31 | } 32 | 33 | static boolean hasMatchedScope(ItemGroup owner) { 34 | ExtensionList scopes = all(); 35 | 36 | return scopes.stream().anyMatch(s -> s.isInScope(owner)); 37 | } 38 | 39 | static List matchedScopes(ItemGroup owner) { 40 | ExtensionList scopes = all(); 41 | 42 | return scopes.stream().filter(s -> s.isInScope(owner)).collect(Collectors.toList()); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/plugins/credentials/KubernetesCredentialsProviderConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.plugins.credentials; 2 | 3 | import hudson.Extension; 4 | import jenkins.model.GlobalConfiguration; 5 | import net.sf.json.JSONObject; 6 | import org.kohsuke.stapler.DataBoundSetter; 7 | import org.kohsuke.stapler.StaplerRequest; 8 | 9 | import javax.annotation.Nonnull; 10 | 11 | @Extension(ordinal = 202) 12 | public class KubernetesCredentialsProviderConfiguration extends GlobalConfiguration { 13 | 14 | private String globalNamespaces = "global-credentials"; 15 | private String labelSelector; 16 | 17 | public static KubernetesCredentialsProviderConfiguration get() { 18 | return GlobalConfiguration.all().get(KubernetesCredentialsProviderConfiguration.class); 19 | 20 | } 21 | 22 | public KubernetesCredentialsProviderConfiguration() { 23 | load(); 24 | } 25 | 26 | @Override 27 | public boolean configure(StaplerRequest req, JSONObject json) throws FormException { 28 | req.bindJSON(this, json); 29 | save(); 30 | return true; 31 | } 32 | 33 | public String getGlobalNamespaces() { 34 | return globalNamespaces; 35 | } 36 | 37 | @DataBoundSetter 38 | public void setGlobalNamespaces(String globalNamespaces) { 39 | this.globalNamespaces = globalNamespaces; 40 | } 41 | 42 | public String getLabelSelector() { 43 | return labelSelector; 44 | } 45 | 46 | @DataBoundSetter 47 | public void setLabelSelector(String labelSelector) { 48 | this.labelSelector = labelSelector; 49 | } 50 | 51 | @Nonnull 52 | @Override 53 | public String getDisplayName() { 54 | return "Alauda Credential Provider"; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /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-devops-credentials-provider.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/main/java/io/alauda/jenkins/plugins/credentials/convertor/UsernamePasswordCredentialsConvertor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2018 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package io.alauda.jenkins.plugins.credentials.convertor; 25 | 26 | import com.cloudbees.plugins.credentials.CredentialsScope; 27 | import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; 28 | import hudson.Extension; 29 | import io.alauda.jenkins.plugins.credentials.SecretUtils; 30 | import io.kubernetes.client.models.V1Secret; 31 | 32 | /** 33 | * SecretToCredentialConvertor that converts {@link UsernamePasswordCredentialsImpl}. 34 | */ 35 | @Extension 36 | public class UsernamePasswordCredentialsConvertor extends SecretToCredentialConverter { 37 | 38 | @Override 39 | public boolean canConvert(String type) { 40 | return "kubernetes.io/basic-auth".equals(type); 41 | } 42 | 43 | @Override 44 | public UsernamePasswordCredentialsImpl convert(V1Secret secret) throws CredentialsConversionException { 45 | SecretUtils.requireNonNull(secret.getData(), "kubernetes.io/basic-auth definition contains no data"); 46 | 47 | String username = SecretUtils.getNonNullSecretData(secret, "username", "kubernetes.io/basic-auth credential is missing the username"); 48 | String password = SecretUtils.getNonNullSecretData(secret, "password", "kubernetes.io/basic-auth credential is missing the password"); 49 | 50 | return new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, SecretUtils.getCredentialId(secret), SecretUtils.getCredentialDescription(secret), username, password); 51 | 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/plugins/credentials/convertor/CredentialsConversionException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2018 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package io.alauda.jenkins.plugins.credentials.convertor; 25 | 26 | import javax.annotation.Nullable; 27 | 28 | /** 29 | * Exception thrown when a credential could not be converted. 30 | */ 31 | public class CredentialsConversionException extends Exception { 32 | 33 | private static final long serialVersionUID = 1L; 34 | 35 | /** 36 | * Constructs a new CredentialsConversionException with the specified detail message. 37 | * 38 | * The cause is not initialized, and may subsequently be initialized by a call to {@link #initCause}. 39 | * 40 | * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} 41 | * method. 42 | */ 43 | public CredentialsConversionException(String message) { 44 | super(message); 45 | } 46 | 47 | /** 48 | * Constructs a new CredentialsConversionException with the specified detail message and cause. 49 | * 50 | * Note that the detail message associated with {@code cause} is not automatically incorporated in this 51 | * exception's detail message. 52 | * 53 | * @param message the detail message (which is saved for later retrieval by the {@link #getMessage()} method). 54 | * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). (A {@code null} 55 | * value is permitted, and indicates that the cause is nonexistent or unknown.) 56 | */ 57 | public CredentialsConversionException(String message, @Nullable Throwable cause) { 58 | super(message, cause); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/**/dictionaries 10 | .idea/**/shelf 11 | 12 | # Sensitive or high-churn files 13 | .idea/**/dataSources/ 14 | .idea/**/dataSources.ids 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | .idea/**/dbnavigator.xml 20 | 21 | # Gradle 22 | .idea/**/gradle.xml 23 | .idea/**/libraries 24 | 25 | # CMake 26 | cmake-build-debug/ 27 | cmake-build-release/ 28 | 29 | # Mongo Explorer plugin 30 | .idea/**/mongoSettings.xml 31 | 32 | # File-based project format 33 | *.iws 34 | 35 | # IntelliJ 36 | out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Cursive Clojure plugin 45 | .idea/replstate.xml 46 | 47 | # Crashlytics plugin (for Android Studio and IntelliJ) 48 | com_crashlytics_export_strings.xml 49 | crashlytics.properties 50 | crashlytics-build.properties 51 | fabric.properties 52 | 53 | # Editor-based Rest Client 54 | .idea/httpRequests 55 | ### Java template 56 | # Compiled class file 57 | *.class 58 | 59 | # Log file 60 | *.log 61 | 62 | # BlueJ files 63 | *.ctxt 64 | 65 | # Mobile Tools for Java (J2ME) 66 | .mtj.tmp/ 67 | 68 | # Package Files # 69 | *.jar 70 | *.war 71 | *.nar 72 | *.ear 73 | *.zip 74 | *.tar.gz 75 | *.rar 76 | 77 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 78 | hs_err_pid* 79 | ### Maven template 80 | target/ 81 | pom.xml.tag 82 | pom.xml.releaseBackup 83 | pom.xml.versionsBackup 84 | pom.xml.next 85 | release.properties 86 | dependency-reduced-pom.xml 87 | buildNumber.properties 88 | .mvn/timing.properties 89 | 90 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 91 | !/.mvn/wrapper/maven-wrapper.jar 92 | ### macOS template 93 | # General 94 | .DS_Store 95 | .AppleDouble 96 | .LSOverride 97 | 98 | # Icon must end with two \r 99 | Icon 100 | 101 | # Thumbnails 102 | ._* 103 | 104 | # Files that might appear in the root of a volume 105 | .DocumentRevisions-V100 106 | .fseventsd 107 | .Spotlight-V100 108 | .TemporaryItems 109 | .Trashes 110 | .VolumeIcon.icns 111 | .com.apple.timemachine.donotpresent 112 | 113 | # Directories potentially created on remote AFP share 114 | .AppleDB 115 | .AppleDesktop 116 | Network Trash Folder 117 | Temporary Items 118 | .apdisk 119 | ### Example user template template 120 | ### Example user template 121 | 122 | # IntelliJ project files 123 | .idea 124 | *.iml 125 | out 126 | gen 127 | work 128 | 129 | scripts/env.sh -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.jenkins-ci.plugins 6 | plugin 7 | 3.4 8 | 9 | 10 | io.alauda.jenkins.plugins 11 | alauda-devops-credentials-provider 12 | 2.3.1-SNAPSHOT 13 | hpi 14 | 15 | 2.83 16 | 8 17 | 18 | Alauda DevOps Credentials Provider Plugin 19 | This plugin will provide a credentials store backed by Kubernetes 20 | 21 | 22 | 23 | MIT License 24 | https://opensource.org/licenses/MIT 25 | 26 | 27 | 28 | 29 | 30 | alaudaDevelopers 31 | Alauda Developers 32 | alauda 33 | http://alauda.io/ 34 | devs@alauda.io 35 | 36 | 37 | 38 | https://wiki.jenkins.io/display/JENKINS/Alauda+DevOps+Credentials+Provider+Plugin 39 | 40 | scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git 41 | scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git 42 | https://github.com/jenkinsci/${project.artifactId}-plugin 43 | HEAD 44 | 45 | 46 | 47 | 48 | repo.jenkins-ci.org 49 | https://repo.jenkins-ci.org/public/ 50 | 51 | 52 | 53 | 54 | repo.jenkins-ci.org 55 | https://repo.jenkins-ci.org/public/ 56 | 57 | 58 | 59 | 60 | 61 | org.jenkins-ci.plugins 62 | credentials 63 | 2.1.16 64 | 65 | 66 | org.jenkins-ci.plugins 67 | cloudbees-folder 68 | 6.1.2 69 | 70 | 71 | io.alauda.jenkins.plugins 72 | alauda-kubernetes-support 73 | 2.3.0 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/plugins/credentials/convertor/SecretToCredentialConverter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2018 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package io.alauda.jenkins.plugins.credentials.convertor; 25 | 26 | import com.cloudbees.plugins.credentials.common.IdCredentials; 27 | import hudson.ExtensionList; 28 | import hudson.ExtensionPoint; 29 | import io.kubernetes.client.models.V1Secret; 30 | 31 | /** 32 | * Class that converts a secret of a given type to an {@link IdCredentials}. 33 | */ 34 | public abstract class SecretToCredentialConverter implements ExtensionPoint { 35 | 36 | /** 37 | * Check if this converter can transform secrets of a given type. 38 | * @param type the type of secret. This is normally the symbol attached to the credential type by the credential binding plugin. 39 | * @return {@code true} iff this {@code SecretToCredentialConvertor} can convert secrets of the specified type. 40 | */ 41 | public abstract boolean canConvert(String type); 42 | 43 | /** 44 | * Convert the given {@code Secret} to an {@code IdCredential}. 45 | * This will only be called for a secret of a type that the class has previously returned {@code true} from {@link #canConvert(String)}. 46 | * @param secret the Secret to convert. 47 | * @throws CredentialsConversionException if the Secret could not be converted. 48 | * @return the IdCredentials created from the secret. 49 | */ 50 | public abstract IdCredentials convert(V1Secret secret) throws CredentialsConversionException; 51 | 52 | /** 53 | * Helper to obtain all the implementations of this {@code ExtensionPoint} 54 | * @return the ExtensionList containing all of the implementations. 55 | */ 56 | public static ExtensionList all() { 57 | return ExtensionList.lookup(SecretToCredentialConverter.class); 58 | } 59 | 60 | /** 61 | * Helper to obtain the SecretToCredentialConvertor that can convert this type of secret. 62 | * @param type the type of the secret to convert. 63 | * @return the SecretToCredentialConvertor to use for converting the secret. 64 | */ 65 | public static SecretToCredentialConverter lookup(String type) { 66 | ExtensionList all = all(); 67 | for (SecretToCredentialConverter stcc : all) { 68 | if (stcc.canConvert(type)) { 69 | return stcc; 70 | } 71 | } 72 | return null; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /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-devops-credentials-provider-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-devops-credentials-provider-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 | // sonar scan 93 | stage('Sonar') { 94 | steps { 95 | script { 96 | container('tools') { 97 | deploy.scan( 98 | REPOSITORY, 99 | GIT_BRANCH, 100 | SONARQUBE_SCM_CREDENTIALS, 101 | FOLDER, 102 | DEBUG, 103 | OWNER, 104 | SCM_FEEDBACK_ACCOUNT).start() 105 | } 106 | } 107 | } 108 | } 109 | } 110 | 111 | // (optional) 112 | // happens at the end of the pipeline 113 | post { 114 | // 成功 115 | success { 116 | echo "Horay!" 117 | script { 118 | deploy.notificationSuccess(DEPLOYMENT, DINGDING_BOT, "流水线完成了", RELEASE_BUILD) 119 | } 120 | } 121 | // 失败 122 | failure { 123 | // check the npm log 124 | // fails lets check if it 125 | script { 126 | echo "damn!" 127 | deploy.notificationFailed(DEPLOYMENT, DINGDING_BOT, "流水线失败了", RELEASE_BUILD) 128 | } 129 | } 130 | always { junit allowEmptyResults: true, testResults: '**/target/surefire-reports/**/*.xml' } 131 | } 132 | } 133 | 134 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/plugins/credentials/AlaudaKubernetesCredentialsStore.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.plugins.credentials; 2 | 3 | import com.cloudbees.plugins.credentials.Credentials; 4 | import com.cloudbees.plugins.credentials.CredentialsProvider; 5 | import com.cloudbees.plugins.credentials.CredentialsStore; 6 | import com.cloudbees.plugins.credentials.CredentialsStoreAction; 7 | import com.cloudbees.plugins.credentials.domains.Domain; 8 | import hudson.model.ItemGroup; 9 | import hudson.model.ModelObject; 10 | import hudson.security.ACL; 11 | import hudson.security.Permission; 12 | import jenkins.model.Jenkins; 13 | import org.acegisecurity.Authentication; 14 | import org.jenkins.ui.icon.Icon; 15 | import org.jenkins.ui.icon.IconSet; 16 | import org.jenkins.ui.icon.IconType; 17 | import org.kohsuke.stapler.export.ExportedBean; 18 | 19 | import javax.annotation.Nonnull; 20 | import java.util.Collections; 21 | import java.util.List; 22 | 23 | public class AlaudaKubernetesCredentialsStore extends CredentialsStore { 24 | private final KubernetesCredentialsProvider provider; 25 | private final AlaudaKubernetesCredentialsStoreAction action = new AlaudaKubernetesCredentialsStoreAction(this); 26 | 27 | private ItemGroup owner; 28 | 29 | public AlaudaKubernetesCredentialsStore(KubernetesCredentialsProvider provider, ItemGroup owner) { 30 | super(KubernetesCredentialsProvider.class); 31 | this.provider = provider; 32 | this.owner = owner; 33 | } 34 | 35 | 36 | @Nonnull 37 | @Override 38 | public ModelObject getContext() { 39 | return owner; 40 | } 41 | 42 | @Override 43 | public boolean hasPermission(@Nonnull Authentication a, @Nonnull Permission permission) { 44 | return CredentialsProvider.VIEW.equals(permission) && 45 | Jenkins.getInstance().getACL().hasPermission(a, permission); 46 | } 47 | 48 | @Nonnull 49 | @Override 50 | public List getCredentials(@Nonnull Domain domain) { 51 | if (Domain.global().equals(domain) && Jenkins.getInstance().hasPermission(CredentialsProvider.VIEW)) 52 | return provider.getCredentialsWithinScope(Credentials.class, owner, ACL.SYSTEM); 53 | return Collections.emptyList(); 54 | } 55 | 56 | @Override 57 | public boolean addCredentials(@Nonnull Domain domain, @Nonnull Credentials credentials) { 58 | return false; 59 | } 60 | 61 | @Override 62 | public boolean removeCredentials(@Nonnull Domain domain, @Nonnull Credentials credentials) { 63 | return false; 64 | } 65 | 66 | @Override 67 | public boolean updateCredentials(@Nonnull Domain domain, @Nonnull Credentials current, @Nonnull Credentials replacement) { 68 | return false; 69 | } 70 | 71 | @Nonnull 72 | @Override 73 | public CredentialsStoreAction getStoreAction() { 74 | return action; 75 | } 76 | 77 | /** 78 | * Expose the store. 79 | */ 80 | @ExportedBean 81 | public static class AlaudaKubernetesCredentialsStoreAction extends CredentialsStoreAction { 82 | 83 | private final AlaudaKubernetesCredentialsStore store; 84 | 85 | private AlaudaKubernetesCredentialsStoreAction(AlaudaKubernetesCredentialsStore store) { 86 | this.store = store; 87 | addIcons(); 88 | } 89 | 90 | @Override 91 | @Nonnull 92 | public CredentialsStore getStore() { 93 | return store; 94 | } 95 | 96 | @Override 97 | public String getDisplayName() { 98 | return "Alauda DevOps"; 99 | } 100 | 101 | private void addIcons() { 102 | IconSet.icons.addIcon(new Icon("icon-credentials-alauda-store icon-sm", 103 | "alauda-devops-credentials-provider/images/16x16/alauda.png", 104 | Icon.ICON_SMALL_STYLE, IconType.PLUGIN)); 105 | IconSet.icons.addIcon(new Icon("icon-credentials-alauda-store icon-md", 106 | "alauda-devops-credentials-provider/images/24x24/alauda.png", 107 | Icon.ICON_MEDIUM_STYLE, IconType.PLUGIN)); 108 | IconSet.icons.addIcon(new Icon("icon-credentials-alauda-store icon-lg", 109 | "alauda-devops-credentials-provider/images/32x32/alauda.png", 110 | Icon.ICON_LARGE_STYLE, IconType.PLUGIN)); 111 | IconSet.icons.addIcon(new Icon("icon-credentials-alauda-store icon-xlg", 112 | "alauda-devops-credentials-provider/images/48x48/alauda.png", 113 | Icon.ICON_XLARGE_STYLE, IconType.PLUGIN)); 114 | } 115 | 116 | @Override 117 | public String getIconFileName() { 118 | return isVisible() 119 | ? "/plugin/alauda-devops-credentials-provider/images/32x32/alauda.png" 120 | : null; 121 | } 122 | 123 | @Override 124 | public String getIconClassName() { 125 | return isVisible() 126 | ? "icon-credentials-alauda-store" 127 | : null; 128 | } 129 | 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/plugins/credentials/SecretUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2018 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package io.alauda.jenkins.plugins.credentials; 25 | 26 | import edu.umd.cs.findbugs.annotations.CheckForNull; 27 | import edu.umd.cs.findbugs.annotations.Nullable; 28 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 29 | import io.alauda.jenkins.plugins.credentials.convertor.CredentialsConversionException; 30 | import io.kubernetes.client.models.V1ObjectMeta; 31 | import io.kubernetes.client.models.V1Secret; 32 | import org.kohsuke.accmod.Restricted; 33 | import org.kohsuke.accmod.restrictions.NoExternalUse; 34 | 35 | import java.nio.ByteBuffer; 36 | import java.nio.CharBuffer; 37 | import java.nio.charset.CharacterCodingException; 38 | import java.nio.charset.CharsetDecoder; 39 | import java.nio.charset.CodingErrorAction; 40 | import java.nio.charset.StandardCharsets; 41 | import java.util.Base64; 42 | import java.util.Map; 43 | import java.util.Optional; 44 | import java.util.logging.Level; 45 | import java.util.logging.Logger; 46 | 47 | /** 48 | * Collection of utilities for working with {@link V1Secret}s. 49 | * Note: API may be subject to change. 50 | */ 51 | public final class SecretUtils { 52 | 53 | private static final Logger LOG = Logger.getLogger(SecretUtils.class.getName()); 54 | 55 | /** Optional Kubernetes annotation for the credential description */ 56 | private static final String JENKINS_IO_CREDENTIALS_DESCRIPTION_ANNOTATION = "jenkins.io/credentials-description"; 57 | 58 | /** Annotation prefix for the optional custom mapping of data */ 59 | private static final String JENKINS_IO_CREDENTIALS_KEYBINDING_ANNOTATION_PREFIX = "jenkins.io/credentials-keybinding-"; 60 | 61 | public static final String JENKINS_IO_CREDENTIALS_TYPE_LABEL = "jenkins.io/credentials-type"; 62 | 63 | private SecretUtils() {} 64 | 65 | /** 66 | * Convert a String representation of the base64 encoded bytes of a UTF-8 String back to a String. 67 | * @param s the base64 encoded String representation of the bytes. 68 | * @return the String or {@code null} if the string could not be converted. 69 | */ 70 | @CheckForNull 71 | @Restricted(NoExternalUse.class) // API is not yet concrete 72 | public static String base64DecodeToString(String s) { 73 | byte[] bytes = base64Decode(s); 74 | if (bytes != null) { 75 | try { 76 | CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); 77 | decoder.onMalformedInput(CodingErrorAction.REPORT); 78 | decoder.onUnmappableCharacter(CodingErrorAction.REPORT); 79 | CharBuffer decode = decoder.decode(ByteBuffer.wrap(bytes)); 80 | return decode.toString(); 81 | } catch (CharacterCodingException ex) { 82 | LOG.log(Level.WARNING, "failed to covert Secret, is this a valid UTF-8 string? {0}", ex.getMessage()); 83 | } 84 | } 85 | return null; 86 | } 87 | 88 | /** 89 | * Convert a String representation of the base64 encoded bytes back to a byte[]. 90 | * @param s the base64 encoded representation of the bytes. 91 | * @return the byte[] or {@code null} if the string could not be converted. 92 | */ 93 | @CheckForNull 94 | @Restricted(NoExternalUse.class) // API is not yet concrete 95 | public static byte[] base64Decode(String s) { 96 | try { 97 | return Base64.getDecoder().decode(s); 98 | } catch (IllegalArgumentException ex) { 99 | LOG.log(Level.WARNING, "failed to base64decode Secret, is the format valid? {0}", ex.getMessage()); 100 | } 101 | return null; 102 | } 103 | 104 | /** 105 | * Obtain the credential ID from a given {@code Secret}. 106 | * @param s the secret whose id we want to obtain. 107 | * @return the credential ID for a given secret. 108 | */ 109 | public static String getCredentialId(V1Secret s) { 110 | // we must have a metadata as the label that identifies this as a Jenkins credential needs to be present 111 | return getCredentialId(s.getMetadata()); 112 | } 113 | 114 | public static String getCredentialId(V1ObjectMeta meta) { 115 | // we must have a metadata as the label that identifies this as a Jenkins credential needs to be present 116 | return meta.getNamespace() + "-" + meta.getName(); 117 | } 118 | 119 | /** 120 | * Obtain the credential description from a given {@code Secret}. 121 | * @param s the secret whose description we want to obtain. 122 | * @return the credential description for a given secret. 123 | */ 124 | @CheckForNull 125 | public static String getCredentialDescription(V1Secret s) { 126 | return getCredentialDescription(s.getMetadata()); 127 | } 128 | 129 | public static String getCredentialDescription(V1ObjectMeta meta) { 130 | // we must have a metadata as the label that identifies this as a Jenkins credential needs to be present 131 | Map annotations = meta.getAnnotations(); 132 | if (annotations != null) { 133 | return annotations.get(JENKINS_IO_CREDENTIALS_DESCRIPTION_ANNOTATION); 134 | } 135 | return null; 136 | } 137 | 138 | /** 139 | * Checks that {@code obj} is not {@code null}. 140 | * @param obj the Object to check for {@code null}. 141 | * @param exceptionMessage detail message to be used in the event that a CredentialsConversionException is thrown. 142 | * @param the type of the obj. 143 | * @return {@code obj} if not {@code null}. 144 | * @throws CredentialsConversionException iff {@code obj} is {@code null}. 145 | */ 146 | public static T requireNonNull(@Nullable T obj, String exceptionMessage) throws CredentialsConversionException { 147 | if (obj == null) { 148 | throw new CredentialsConversionException(exceptionMessage); 149 | } 150 | return obj; 151 | } 152 | 153 | /** 154 | * Checks that {@code obj} is not {@code null}. 155 | * @param obj the Object to check for {@code null}. 156 | * @param exceptionMessage detail message to be used in the event that a CredentialsConversionException is thrown. 157 | * @param mapped an optional mapping (adds a {@code "mapped to " + mapped} to the exception message if this is non null. 158 | * @param the type of the obj. 159 | * @return {@code obj} if not {@code null}. 160 | * @throws CredentialsConversionException iff {@code obj} is {@code null}. 161 | */ 162 | public static T requireNonNull(@Nullable T obj, String exceptionMessage, @Nullable String mapped) throws CredentialsConversionException { 163 | if (obj == null) { 164 | if (mapped != null) { 165 | throw new CredentialsConversionException(exceptionMessage.concat(" (mapped to " + mapped + ")")); 166 | } 167 | throw new CredentialsConversionException(exceptionMessage); 168 | } 169 | return obj; 170 | } 171 | 172 | 173 | /** 174 | * Get the data for the specified key (or the mapped key if key is mapped), or throw a 175 | * CredentialsConversionException if the data for the given key was not present.. 176 | * 177 | * @param s the Secret 178 | * @param key the key to get the data for (which may be mapped to another key). 179 | * @param exceptionMessage the detailMessage of the exception if the data for the key (or mapped key) was not 180 | * present. 181 | * @return The data for the given key. 182 | * @throws CredentialsConversionException if the data was not present. 183 | */ 184 | @SuppressFBWarnings(value= {"ES_COMPARING_PARAMETER_STRING_WITH_EQ"}, justification="the string will be the same string if not mapped") 185 | public static String getNonNullSecretData(V1Secret s, String key, String exceptionMessage) throws CredentialsConversionException { 186 | String mappedKey = getKeyName(s, key); 187 | if (mappedKey.equals(key)) { // use String == as getKeyName(key) will return key if no custom mapping is defined) 188 | String data = s.getData().get(key) == null ? null : new String(s.getData().get(key), StandardCharsets.UTF_8); 189 | return requireNonNull(data, exceptionMessage, null); 190 | } 191 | String data = s.getData().get(mappedKey) == null ? null : new String(s.getData().get(mappedKey), StandardCharsets.UTF_8); 192 | return requireNonNull(data, exceptionMessage, mappedKey); 193 | } 194 | 195 | /** 196 | * Get optional data for the specified key (or the mapped key if key is mapped) 197 | * 198 | * @param s the Secret 199 | * @param key the key to get the data for (which may be mapped to another key). 200 | * @param exceptionMessage the detailMessage of the exception if the data for the key (or mapped key) was not 201 | * present. 202 | * @return Optional data for specified key 203 | * @throws CredentialsConversionException if the data was not present. 204 | */ 205 | public static Optional getOptionalSecretData(V1Secret s, String key, String exceptionMessage) throws CredentialsConversionException { 206 | String mappedKey = getKeyName(s, key); 207 | if (s.getData().containsKey(key) || s.getData().containsKey(mappedKey)) { 208 | return Optional.of(getNonNullSecretData(s, key, exceptionMessage)); 209 | } 210 | return Optional.empty(); 211 | } 212 | 213 | /** 214 | * Get the mapping for the specified key name. Secrets can override the defaults used by the plugin by specifying an 215 | * attribute of the type {@code jenkins.io/credentials-keybinding-name} containing the custom name - for example 216 | * {@code jenkins.io/credentials-keybinding-foo=wibble}. 217 | * 218 | * @param s the secret to inspect for a custom name. 219 | * @param key the name of the key we are looking for. 220 | * @return the custom mapping for the key or {@code key} (identical object) if there is no custom mapping. 221 | */ 222 | public static String getKeyName(V1Secret s, String key) { 223 | Map annotations = s.getMetadata().getAnnotations(); 224 | if (annotations != null){ 225 | final String annotationName = JENKINS_IO_CREDENTIALS_KEYBINDING_ANNOTATION_PREFIX + key; 226 | String customMapping = annotations.get(annotationName); 227 | if (customMapping == null) { 228 | // no entry 229 | return key; 230 | } 231 | if (customMapping.isEmpty()){ 232 | LOG.log(Level.WARNING, "Secret {0} contains a mapping annotation {1} but has no entry - mapping will " 233 | + "not be performed", 234 | new Object[] {s.getMetadata().getName(), annotationName}); 235 | return key; 236 | } 237 | return customMapping; 238 | } 239 | return key; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/main/java/io/alauda/jenkins/plugins/credentials/KubernetesCredentialsProvider.java: -------------------------------------------------------------------------------- 1 | package io.alauda.jenkins.plugins.credentials; 2 | 3 | import com.cloudbees.plugins.credentials.Credentials; 4 | import com.cloudbees.plugins.credentials.CredentialsProvider; 5 | import com.cloudbees.plugins.credentials.CredentialsStore; 6 | import com.cloudbees.plugins.credentials.common.IdCredentials; 7 | import hudson.Extension; 8 | import hudson.ExtensionList; 9 | import hudson.model.ItemGroup; 10 | import hudson.model.ModelObject; 11 | import hudson.security.ACL; 12 | import io.alauda.jenkins.devops.support.KubernetesCluster; 13 | import io.alauda.jenkins.devops.support.KubernetesClusterConfigurationListener; 14 | import io.alauda.jenkins.plugins.credentials.convertor.CredentialsConversionException; 15 | import io.alauda.jenkins.plugins.credentials.convertor.SecretToCredentialConverter; 16 | import io.alauda.jenkins.plugins.credentials.metadata.CredentialsWithMetadata; 17 | import io.alauda.jenkins.plugins.credentials.metadata.MetadataProvider; 18 | import io.alauda.jenkins.plugins.credentials.rule.KubernetesSecretRule; 19 | import io.alauda.jenkins.plugins.credentials.scope.JenkinsRootScope; 20 | import io.alauda.jenkins.plugins.credentials.scope.KubernetesSecretScope; 21 | import io.kubernetes.client.ApiClient; 22 | import io.kubernetes.client.apis.CoreV1Api; 23 | import io.kubernetes.client.extended.controller.Controller; 24 | import io.kubernetes.client.extended.controller.ControllerManager; 25 | import io.kubernetes.client.extended.controller.builder.ControllerBuilder; 26 | import io.kubernetes.client.extended.controller.builder.ControllerManagerBuilder; 27 | import io.kubernetes.client.extended.controller.reconciler.Reconciler; 28 | import io.kubernetes.client.extended.controller.reconciler.Request; 29 | import io.kubernetes.client.extended.controller.reconciler.Result; 30 | import io.kubernetes.client.informer.SharedIndexInformer; 31 | import io.kubernetes.client.informer.SharedInformerFactory; 32 | import io.kubernetes.client.informer.cache.Lister; 33 | import io.kubernetes.client.models.V1ObjectMeta; 34 | import io.kubernetes.client.models.V1Secret; 35 | import io.kubernetes.client.models.V1SecretList; 36 | import jenkins.model.Jenkins; 37 | import org.acegisecurity.Authentication; 38 | import org.slf4j.Logger; 39 | import org.slf4j.LoggerFactory; 40 | 41 | import javax.annotation.Nonnull; 42 | import java.util.*; 43 | import java.util.concurrent.ConcurrentHashMap; 44 | import java.util.concurrent.ExecutorService; 45 | import java.util.concurrent.Executors; 46 | 47 | @Extension 48 | public class KubernetesCredentialsProvider extends CredentialsProvider implements KubernetesClusterConfigurationListener { 49 | 50 | private static final Logger logger = LoggerFactory.getLogger(KubernetesCredentialsProvider.class); 51 | private static final String CONTROLLER_NAME = "SecretController"; 52 | 53 | // Maps of credentials keyed by credentials ID 54 | private ConcurrentHashMap credentials = new ConcurrentHashMap<>(); 55 | private ControllerManager controllerManager; 56 | private ExecutorService controllerManagerThread; 57 | 58 | 59 | @Override 60 | public void onConfigChange(KubernetesCluster cluster, ApiClient client) { 61 | shutDown(null); 62 | 63 | SharedInformerFactory factory = new SharedInformerFactory(); 64 | ControllerManagerBuilder manangerBuilder = ControllerBuilder 65 | .controllerManagerBuilder(factory); 66 | 67 | String labelSelector = KubernetesCredentialsProviderConfiguration.get().getLabelSelector(); 68 | 69 | CoreV1Api coreV1Api = new CoreV1Api(); 70 | 71 | SharedIndexInformer secretInformer = factory.sharedIndexInformerFor( 72 | callGeneratorParams -> coreV1Api.listSecretForAllNamespacesCall( 73 | null, 74 | null, 75 | labelSelector, 76 | null, 77 | null, 78 | callGeneratorParams.resourceVersion, 79 | callGeneratorParams.timeoutSeconds, 80 | callGeneratorParams.watch, 81 | null, 82 | null), V1Secret.class, V1SecretList.class); 83 | 84 | 85 | Controller controller = ControllerBuilder.defaultBuilder(factory).watch( 86 | (workQueue) -> 87 | ControllerBuilder.controllerWatchBuilder(V1Secret.class, workQueue) 88 | .withWorkQueueKeyFunc(secret -> 89 | new Request(secret.getMetadata().getNamespace(), secret.getMetadata().getName())) 90 | .withOnAddFilter(secret -> { 91 | logger.debug("[{}] receives event: Add; Secret '{}/{}'", 92 | CONTROLLER_NAME, 93 | secret.getMetadata().getNamespace(), secret.getMetadata().getName()); 94 | return true; 95 | }) 96 | .withOnUpdateFilter((oldSecret, newSecret) -> { 97 | String namespace = oldSecret.getMetadata().getNamespace(); 98 | String name = oldSecret.getMetadata().getName(); 99 | 100 | logger.debug("[{}] receives event: Update; Secret '{}/{}'", 101 | CONTROLLER_NAME, 102 | namespace, name); 103 | 104 | return true; 105 | }) 106 | .withOnDeleteFilter((secret, aBoolean) -> { 107 | logger.debug("[{}] receives event: Add; Secret '{}/{}'", 108 | CONTROLLER_NAME, 109 | secret.getMetadata().getNamespace(), secret.getMetadata().getName()); 110 | return true; 111 | }).build()) 112 | .withReconciler(new SecretReconciler(new Lister<>(secretInformer.getIndexer()))) 113 | .withName(CONTROLLER_NAME) 114 | .withWorkerCount(4) 115 | .build(); 116 | 117 | controllerManager = manangerBuilder.addController(controller).build(); 118 | 119 | controllerManagerThread = Executors.newSingleThreadExecutor(); 120 | controllerManagerThread.submit(() -> controllerManager.run()); 121 | } 122 | 123 | @Override 124 | public void onConfigError(KubernetesCluster cluster, Throwable reason) { 125 | shutDown(reason); 126 | } 127 | 128 | private void shutDown(Throwable reason) { 129 | if (controllerManager != null) { 130 | controllerManager.shutdown(); 131 | controllerManager = null; 132 | } 133 | 134 | if (controllerManagerThread != null && !controllerManagerThread.isShutdown()) { 135 | controllerManagerThread.shutdown(); 136 | } 137 | 138 | if (reason != null) { 139 | logger.error("Alauda DevOps Credentials Provider is stopped, reason {}", reason); 140 | } else { 141 | logger.error("Alauda DevOps Credentials Provider is stopped, reason is null, might be stopped by user"); 142 | } 143 | } 144 | 145 | 146 | class SecretReconciler implements Reconciler { 147 | 148 | private Lister secretLister; 149 | 150 | public SecretReconciler(Lister secretLister) { 151 | this.secretLister = secretLister; 152 | } 153 | 154 | @Override 155 | public Result reconcile(Request request) { 156 | String namespace = request.getNamespace(); 157 | String name = request.getName(); 158 | 159 | V1Secret secret = secretLister.namespace(namespace).get(name); 160 | if (secret == null) { 161 | logger.debug("[{}] Unable to get Secret '{}/{}' from local list, will remove it", getControllerName(), namespace, name); 162 | String credId = SecretUtils.getCredentialId(new V1ObjectMeta().namespace(namespace).name(name)); 163 | if (credentials.containsKey(credId)) { 164 | logger.debug("Secret Deleted - {}", credId); 165 | credentials.remove(credId); 166 | } 167 | return new Result(false); 168 | } 169 | 170 | IdCredentials cred = convertSecret(secret); 171 | if (cred != null) { 172 | logger.debug("Secret Added - {}", cred.getId()); 173 | CredentialsWithMetadata credWithMetadata = addMetadataToCredentials(secret, cred); 174 | credentials.put(cred.getId(), credWithMetadata); 175 | return new Result(false); 176 | } 177 | 178 | return new Result(false); 179 | } 180 | 181 | public String getControllerName() { 182 | return CONTROLLER_NAME; 183 | } 184 | } 185 | 186 | 187 | @Nonnull 188 | @Override 189 | public List getCredentials(@Nonnull Class type, final ItemGroup itemGroup, Authentication authentication) { 190 | logger.debug("getCredentials called with type {} and authentication {}", type.getName(), authentication); 191 | if (ACL.SYSTEM.equals(authentication)) { 192 | List credentialsWithinScopes = getCredentialsWithinScope(type, itemGroup, authentication); 193 | 194 | List scopes = KubernetesSecretScope.matchedScopes(itemGroup); 195 | if (scopes.stream().anyMatch(s -> s.getClass().equals(JenkinsRootScope.class))) { 196 | return credentialsWithinScopes; 197 | } 198 | 199 | 200 | JenkinsRootScope rootScope = ExtensionList.lookup(JenkinsRootScope.class).get(0); 201 | credentials.forEach((s, credentialsWithMetadata) -> { 202 | if (rootScope.shouldShowInScope(Jenkins.getInstance(), credentialsWithMetadata) 203 | && type.isAssignableFrom(credentialsWithMetadata.getCredentials().getClass())) { 204 | C c = type.cast(credentialsWithMetadata.getCredentials()); 205 | if (!credentialsWithinScopes.contains(c)) { 206 | credentialsWithinScopes.add(c); 207 | } 208 | } 209 | 210 | }); 211 | return credentialsWithinScopes; 212 | } 213 | return Collections.emptyList(); 214 | } 215 | 216 | public List getCredentialsWithinScope(@Nonnull Class type, final ItemGroup itemGroup, Authentication authentication) { 217 | logger.debug("getCredentials called with type {} and authentication {}", type.getName(), authentication); 218 | if (ACL.SYSTEM.equals(authentication)) { 219 | List list = new ArrayList<>(); 220 | Set ids = new HashSet<>(); 221 | 222 | List scopes = KubernetesSecretScope.matchedScopes(itemGroup); 223 | 224 | credentials.forEach((id, credentialsWithMetadata) -> { 225 | if (scopes.stream().anyMatch(scope -> scope.shouldShowInScope(itemGroup, credentialsWithMetadata)) 226 | && type.isAssignableFrom(credentialsWithMetadata.getCredentials().getClass()) && ids.add(id)) { 227 | list.add(type.cast(credentialsWithMetadata.getCredentials())); 228 | } 229 | }); 230 | 231 | return list; 232 | } 233 | return Collections.emptyList(); 234 | } 235 | 236 | 237 | private CredentialsWithMetadata addMetadataToCredentials(V1Secret s, IdCredentials cred) { 238 | CredentialsWithMetadata credWithMetadata = new CredentialsWithMetadata<>(cred); 239 | 240 | MetadataProvider.all().forEach(metadataProvider -> metadataProvider.attach(s, credWithMetadata)); 241 | 242 | return credWithMetadata; 243 | } 244 | 245 | 246 | private IdCredentials convertSecret(V1Secret s) { 247 | if (KubernetesSecretRule.shouldExclude(s)) { 248 | return null; 249 | } 250 | 251 | String type = getSecretType(s); 252 | SecretToCredentialConverter lookup = SecretToCredentialConverter.lookup(type); 253 | if (lookup != null) { 254 | try { 255 | return lookup.convert(s); 256 | } catch (CredentialsConversionException ex) { 257 | // do not spam the logs with the stacktrace... 258 | logger.debug("Failed to convert Secret '" + SecretUtils.getCredentialId(s) + "' of type " + type, ex); 259 | return null; 260 | } 261 | } 262 | return null; 263 | } 264 | 265 | private String getSecretType(V1Secret s) { 266 | return s.getType(); 267 | } 268 | 269 | @Override 270 | public CredentialsStore getStore(ModelObject object) { 271 | if (!(object instanceof ItemGroup)) { 272 | return null; 273 | } 274 | 275 | ItemGroup owner = (ItemGroup) object; 276 | if (!KubernetesSecretScope.hasMatchedScope(owner)) { 277 | return null; 278 | } 279 | 280 | return new AlaudaKubernetesCredentialsStore(this, owner); 281 | } 282 | 283 | @Override 284 | @Nonnull 285 | public String getDisplayName() { 286 | return "Alauda DevOps Credentials Provider"; 287 | } 288 | 289 | @Override 290 | public String getIconClassName() { 291 | return "icon-credentials-alauda-store"; 292 | } 293 | } 294 | --------------------------------------------------------------------------------