move to standalone plugin
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package recreationaltech.plugin.client
|
||||
|
||||
import groovy.util.logging.Slf4j
|
||||
import nextflow.util.Duration
|
||||
|
||||
import javax.net.ssl.KeyManager
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.EqualsAndHashCode
|
||||
/**
|
||||
* Models the kubernetes cluster client configuration settings
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@CompileStatic
|
||||
@Slf4j
|
||||
class ClientConfig {
|
||||
|
||||
boolean verifySsl
|
||||
|
||||
String server
|
||||
|
||||
String namespace
|
||||
|
||||
/**
|
||||
* k8s service account name
|
||||
* https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/
|
||||
*/
|
||||
String serviceAccount
|
||||
|
||||
String token
|
||||
|
||||
byte[] sslCert
|
||||
|
||||
byte[] clientCert
|
||||
|
||||
byte[] clientKey
|
||||
|
||||
KeyManager[] keyManagers
|
||||
|
||||
K8sRetryConfig retryConfig
|
||||
|
||||
/**
|
||||
* Timeout when reading from Input stream when a connection is established to a resource.
|
||||
* If the timeout expires before there is data available for read, a {@link java.net.SocketTimeoutException} is raised
|
||||
*/
|
||||
Duration httpReadTimeout
|
||||
|
||||
/**
|
||||
* Timeout when opening a communications link to the resource referenced by K8sClient request connection
|
||||
* If the timeout expires before there is data available for read, a {@link java.net.SocketTimeoutException} is raised
|
||||
*/
|
||||
Duration httpConnectTimeout
|
||||
|
||||
/**
|
||||
* When true signal that the configuration was retrieved from within a K8s cluster
|
||||
*/
|
||||
boolean isFromCluster
|
||||
|
||||
String getNamespace() { namespace ?: 'default' }
|
||||
|
||||
ClientConfig() {
|
||||
retryConfig = new K8sRetryConfig()
|
||||
}
|
||||
|
||||
String toString() {
|
||||
"${this.class.getSimpleName()}[ server=$server, namespace=$namespace, serviceAccount=$serviceAccount, token=${cut(token)}, sslCert=${cut(sslCert)}, clientCert=${cut(clientCert)}, clientKey=${cut(clientKey)}, verifySsl=$verifySsl, fromFile=$isFromCluster, httpReadTimeout=$httpReadTimeout, httpConnectTimeout=$httpConnectTimeout, retryConfig=$retryConfig ]"
|
||||
}
|
||||
|
||||
private String cut(String str) {
|
||||
if( !str ) return '-'
|
||||
return str.size()<10 ? str : str[0..10].toString() + '..'
|
||||
}
|
||||
|
||||
private String cut(byte[] bytes) {
|
||||
if( !bytes ) return '-'
|
||||
cut(bytes.encodeBase64().toString())
|
||||
}
|
||||
|
||||
static ClientConfig discover(String context, String namespace, String serviceAccount) {
|
||||
new ConfigDiscovery().discover(context, namespace, serviceAccount)
|
||||
}
|
||||
|
||||
static ClientConfig fromNextflowConfig(Map opts, String namespace, String serviceAccount) {
|
||||
final result = new ClientConfig()
|
||||
|
||||
if( opts.server )
|
||||
result.server = opts.server
|
||||
|
||||
if( opts.token )
|
||||
result.token = opts.token
|
||||
else if( opts.tokenFile )
|
||||
result.token = Paths.get(opts.tokenFile.toString()).getText('UTF-8')
|
||||
|
||||
result.namespace = namespace ?: opts.namespace ?: 'default'
|
||||
|
||||
result.serviceAccount = serviceAccount ?: 'default'
|
||||
|
||||
if( opts.verifySsl )
|
||||
result.verifySsl = opts.verifySsl as boolean
|
||||
|
||||
if( opts.sslCert )
|
||||
result.sslCert = opts.sslCert.toString().decodeBase64()
|
||||
else if( opts.sslCertFile )
|
||||
result.sslCert = Paths.get(opts.sslCertFile.toString()).bytes
|
||||
|
||||
if( opts.clientCert )
|
||||
result.clientCert = opts.clientCert.toString().decodeBase64()
|
||||
else if( opts.clientCertFile )
|
||||
result.clientCert = Paths.get(opts.clientCertFile.toString()).bytes
|
||||
|
||||
if( opts.clientKey )
|
||||
result.clientKey = opts.clientKey.toString().decodeBase64()
|
||||
else if( opts.clientKeyFile )
|
||||
result.clientKey = Paths.get(opts.clientKeyFile.toString()).bytes
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static ClientConfig fromUserAndCluster(Map user, Map cluster, Path location) {
|
||||
final base = location.isDirectory() ? location : location.parent
|
||||
final result = new ClientConfig()
|
||||
if( user.token )
|
||||
result.token = user.token
|
||||
|
||||
else if( user.tokenFile ) {
|
||||
result.token = Paths.get(user.tokenFile.toString()).getText('UTF-8')
|
||||
}
|
||||
|
||||
if( user."client-certificate" )
|
||||
result.clientCert = base.resolve(user."client-certificate".toString()).bytes
|
||||
|
||||
else if( user."client-certificate-data" )
|
||||
result.clientCert = user."client-certificate-data".toString().decodeBase64()
|
||||
|
||||
if( user."client-key" )
|
||||
result.clientKey = base.resolve(user."client-key".toString()).bytes
|
||||
|
||||
else if( user."client-key-data" )
|
||||
result.clientKey = user."client-key-data".toString().decodeBase64()
|
||||
|
||||
// -- cluster settings
|
||||
|
||||
if( cluster.server )
|
||||
result.server = cluster.server
|
||||
|
||||
if( cluster."certificate-authority-data" )
|
||||
result.sslCert = cluster."certificate-authority-data".toString().decodeBase64()
|
||||
|
||||
else if( cluster."certificate-authority" )
|
||||
result.sslCert = base.resolve(cluster."certificate-authority".toString()).bytes
|
||||
|
||||
result.verifySsl = cluster."insecure-skip-tls-verify" != true
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package recreationaltech.plugin.client
|
||||
|
||||
import javax.net.ssl.KeyManager
|
||||
import javax.net.ssl.KeyManagerFactory
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.security.KeyStore
|
||||
import static nextflow.util.StringUtils.formatHostName
|
||||
|
||||
import groovy.util.logging.Slf4j
|
||||
import org.yaml.snakeyaml.Yaml
|
||||
/**
|
||||
* Discover Kubernetes configuration from system environment
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
@Slf4j
|
||||
class ConfigDiscovery {
|
||||
|
||||
private Map<String,String> env = System.getenv()
|
||||
|
||||
ConfigDiscovery() { }
|
||||
|
||||
/**
|
||||
* Discover Kubernetes client configuration from current environment using
|
||||
* either the .kube/config file or the pod service service account virtual
|
||||
* file system when running in a pod.
|
||||
*
|
||||
* @param contextName The K8s config context name.
|
||||
* @param namespace The K8s cluster namespace
|
||||
* @param serviceAccount The K8s cluster service account
|
||||
* @return The e
|
||||
*/
|
||||
ClientConfig discover(String contextName, String namespace, String serviceAccount) {
|
||||
|
||||
// Note: System.getProperty('user.home') may not report the correct home path when
|
||||
// running in a container. Use env HOME instead.
|
||||
def home = System.getenv('HOME')
|
||||
def kubeConfig = env.get('KUBECONFIG') ? env.get('KUBECONFIG') : "$home/.kube/config"
|
||||
def configFile = Paths.get(kubeConfig)
|
||||
|
||||
// determine the Kubernetes client configuration via the `.kube/config` file
|
||||
if( configFile.exists() ) {
|
||||
return fromKubeConfig(configFile, contextName, namespace, serviceAccount)
|
||||
}
|
||||
else {
|
||||
log.debug "K8s config file does not exist: $configFile"
|
||||
}
|
||||
|
||||
// determine the Kubernetes client configuration via the pod environment
|
||||
if( env.get('KUBERNETES_SERVICE_HOST') ) {
|
||||
return fromCluster(env, namespace, serviceAccount)
|
||||
}
|
||||
else {
|
||||
log.debug "K8s env variable KUBERNETES_SERVICE_HOST is not defined"
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Unable to lookup Kubernetes cluster configuration")
|
||||
}
|
||||
|
||||
protected ClientConfig fromCluster(Map<String,String> env, String cfgNamespace, String serviceAccount) {
|
||||
|
||||
// See https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/#accessing-the-api-from-a-pod
|
||||
|
||||
final host = env.get('KUBERNETES_SERVICE_HOST')
|
||||
final port = env.get('KUBERNETES_SERVICE_PORT')
|
||||
final server = formatHostName(host, port)
|
||||
|
||||
final cert = path('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt').bytes
|
||||
final token = path('/var/run/secrets/kubernetes.io/serviceaccount/token').text
|
||||
final namespace = path('/var/run/secrets/kubernetes.io/serviceaccount/namespace').text
|
||||
|
||||
return new ClientConfig(
|
||||
server: server,
|
||||
token: token,
|
||||
namespace: cfgNamespace ?: namespace,
|
||||
serviceAccount: serviceAccount,
|
||||
sslCert: cert,
|
||||
isFromCluster: true )
|
||||
}
|
||||
|
||||
protected Path path(String path) {
|
||||
Paths.get(path)
|
||||
}
|
||||
|
||||
protected ClientConfig fromKubeConfig(Path path, String contextName, String namespace, String serviceAccount) {
|
||||
def yaml = (Map)new Yaml().load(Files.newInputStream(path))
|
||||
|
||||
contextName ?= yaml."current-context" as String
|
||||
|
||||
final allContext = yaml.contexts as List
|
||||
final allClusters = yaml.clusters as List
|
||||
final allUsers = yaml.users as List
|
||||
final context = allContext.find { Map it -> it.name == contextName } ?.context
|
||||
if( !context )
|
||||
throw new IllegalArgumentException("Unknown Kubernetes context: $contextName -- check config file: $path")
|
||||
final userName = context?.user
|
||||
final clusterName = context?.cluster
|
||||
final user = allUsers.find{ Map it -> it.name == userName } ?.user ?: [:]
|
||||
final cluster = allClusters.find{ Map it -> it.name == clusterName } ?.cluster ?: [:]
|
||||
|
||||
final config = ClientConfig.fromUserAndCluster(user, cluster, path)
|
||||
|
||||
// the namespace provided should have priority over the context current namespace
|
||||
config.namespace = namespace ?: context?.namespace ?: 'default'
|
||||
|
||||
config.serviceAccount = serviceAccount ?: 'default'
|
||||
|
||||
if( config.clientCert && config.clientKey ) {
|
||||
config.keyManagers = createKeyManagers(config.clientCert, config.clientKey)
|
||||
}
|
||||
else if( !config.token ) {
|
||||
config.token = discoverAuthToken(contextName, config.namespace, config.serviceAccount)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
protected KeyStore createKeyStore0(byte[] clientCert, byte[] clientKey, char[] passphrase, String alg) {
|
||||
def cert = new ByteArrayInputStream(clientCert)
|
||||
def key = new ByteArrayInputStream(clientKey)
|
||||
return SSLUtils.createKeyStore(cert, key, alg, passphrase, null, null)
|
||||
}
|
||||
|
||||
protected KeyStore createKeyStore(byte[] clientCert, byte[] clientKey, char[] passphrase) {
|
||||
try {
|
||||
// try first RSA algorithm
|
||||
return createKeyStore0(clientCert, clientKey, passphrase, "RSA")
|
||||
}
|
||||
catch (Exception e1) {
|
||||
// fallback to EC algorithm
|
||||
try {
|
||||
return createKeyStore0(clientCert, clientKey, passphrase, "EC")
|
||||
}
|
||||
catch (Exception e2) {
|
||||
// if still fails, throws the first exception
|
||||
throw e1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected KeyManager[] createKeyManagers(byte[] clientCert, byte[] clientKey) {
|
||||
final passphrase = "".toCharArray()
|
||||
final keyStore = createKeyStore(clientCert, clientKey, passphrase)
|
||||
final kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
kmf.init(keyStore, passphrase);
|
||||
return kmf.getKeyManagers();
|
||||
}
|
||||
|
||||
String discoverAuthToken(String context, String namespace, String serviceAccount) {
|
||||
context ?= 'default'
|
||||
namespace ?= 'default'
|
||||
serviceAccount ?= 'default'
|
||||
|
||||
final cmd = "kubectl --context $context -n ${namespace} get secret -o=jsonpath='{.items[?(@.metadata.annotations.kubernetes\\.io/service-account\\.name==\"$serviceAccount\")].data.token}'"
|
||||
final proc = new ProcessBuilder('bash','-o','pipefail','-c', cmd).start()
|
||||
final status = proc.waitFor()
|
||||
final text = proc.inputStream?.text
|
||||
if( status==0 && text ) {
|
||||
try {
|
||||
return new String(text.trim().decodeBase64())
|
||||
}
|
||||
catch( Exception e ) {
|
||||
log.warn "Unable to decode K8s cluster auth token '$text' -- cause: ${e.message}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
final cause = proc.errorStream?.text ?: text
|
||||
final msg = cause ? "\n- cmd : $cmd\n- exit : $status\n- cause:\n${cause.indent(' ')}" : ''
|
||||
log.warn "[K8s] unable to fetch auth token ${msg}"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package recreationaltech.plugin.client
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
|
||||
/**
|
||||
* Model a Kubernetes API response
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
@CompileStatic
|
||||
class K8sResponseApi {
|
||||
|
||||
private int code
|
||||
|
||||
private InputStream stream
|
||||
|
||||
private String text
|
||||
|
||||
K8sResponseApi(int code, InputStream stream) {
|
||||
this.code = code
|
||||
this.stream = stream
|
||||
}
|
||||
|
||||
String toString() {
|
||||
"code=$code; stream=$stream"
|
||||
}
|
||||
|
||||
int getCode() { code }
|
||||
|
||||
InputStream getStream() { stream }
|
||||
|
||||
String getText() {
|
||||
if( text == null ) {
|
||||
text = stream?.text
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package recreationaltech.plugin.client
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.util.logging.Slf4j
|
||||
/**
|
||||
* Model a kubernetes invalid response
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
|
||||
@Slf4j
|
||||
@CompileStatic
|
||||
class K8sResponseException extends Exception {
|
||||
|
||||
K8sResponseJson response
|
||||
|
||||
K8sResponseException(K8sResponseJson response) {
|
||||
super(msg0(response))
|
||||
this.response = response
|
||||
}
|
||||
|
||||
K8sResponseException(String message, K8sResponseJson response) {
|
||||
super(msg1(message,response))
|
||||
this.response = response
|
||||
}
|
||||
|
||||
K8sResponseException(String message, InputStream response) {
|
||||
this(message, new K8sResponseJson(fetch(response)))
|
||||
}
|
||||
|
||||
static private String msg1( String msg, K8sResponseJson resp ) {
|
||||
if( !msg && resp==null )
|
||||
return null
|
||||
|
||||
if( msg && resp != null ) {
|
||||
def sep = resp.isRawText() ? ' -- ' : '\n'
|
||||
return "${msg}${sep}${msg0(resp)}"
|
||||
}
|
||||
else if( msg ) {
|
||||
return msg
|
||||
}
|
||||
else {
|
||||
return msg0(resp)
|
||||
}
|
||||
}
|
||||
|
||||
static private String msg0( K8sResponseJson response ) {
|
||||
if( response == null )
|
||||
return null
|
||||
|
||||
if( response.isRawText() )
|
||||
response.getRawText()
|
||||
else
|
||||
"\n${response.toString().indent(' ')}"
|
||||
}
|
||||
|
||||
static private String fetch(InputStream stream) {
|
||||
try {
|
||||
return stream?.text
|
||||
}
|
||||
catch( Exception e ) {
|
||||
log.debug "Unable to fetch response text -- Cause: ${e.message ?: e}"
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package recreationaltech.plugin.client
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.util.logging.Slf4j
|
||||
|
||||
/**
|
||||
* Model the response of a kubernetes api request
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
@Slf4j
|
||||
@CompileStatic
|
||||
class K8sResponseJson implements Map {
|
||||
|
||||
@Delegate
|
||||
private Map response
|
||||
|
||||
private String rawText
|
||||
|
||||
K8sResponseJson(Map response) {
|
||||
this.response = response
|
||||
}
|
||||
|
||||
K8sResponseJson(String response) {
|
||||
this.response = toJson(response)
|
||||
this.rawText = response
|
||||
}
|
||||
|
||||
boolean isRawText() { !response && rawText }
|
||||
|
||||
String getRawText() { rawText }
|
||||
|
||||
static private Map toJson(String raw) {
|
||||
try {
|
||||
return (Map)new JsonSlurper().parseText(raw)
|
||||
}
|
||||
catch( Exception e ) {
|
||||
log.trace "[K8s] cannot parse response to json -- raw: ${raw? '\n'+raw.indent(' ') :'null'}"
|
||||
return Collections.emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
static private String prettyPrint(String json) {
|
||||
try {
|
||||
JsonOutput.prettyPrint(json)
|
||||
}
|
||||
catch( Exception e ) {
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
String toString() {
|
||||
response ? prettyPrint(JsonOutput.toJson(response)) : rawText
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package recreationaltech.plugin.client
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.EqualsAndHashCode
|
||||
import groovy.transform.ToString
|
||||
import nextflow.config.spec.ConfigOption
|
||||
import nextflow.config.spec.ConfigScope
|
||||
import nextflow.script.dsl.Description
|
||||
import nextflow.util.Duration
|
||||
|
||||
/**
|
||||
* Model retry policy configuration
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
@ToString(includePackage = false, includeNames = true)
|
||||
@EqualsAndHashCode
|
||||
@CompileStatic
|
||||
class K8sRetryConfig implements ConfigScope {
|
||||
|
||||
@ConfigOption
|
||||
@Description("""
|
||||
Delay when retrying failed API requests (default: `250ms`).
|
||||
""")
|
||||
Duration delay = Duration.of('250ms')
|
||||
|
||||
@ConfigOption
|
||||
@Description("""
|
||||
Max delay when retrying failed API requests (default: `90s`).
|
||||
""")
|
||||
Duration maxDelay = Duration.of('90s')
|
||||
|
||||
@ConfigOption
|
||||
@Description("""
|
||||
Max attempts when retrying failed API requests (default: `4`).
|
||||
""")
|
||||
int maxAttempts = 4
|
||||
|
||||
@ConfigOption
|
||||
@Description("""
|
||||
Jitter value when retrying failed API requests (default: `0.25`).
|
||||
""")
|
||||
double jitter = 0.25
|
||||
|
||||
K8sRetryConfig() {
|
||||
this(Collections.emptyMap())
|
||||
}
|
||||
|
||||
K8sRetryConfig(Map config) {
|
||||
if( config.delay )
|
||||
delay = config.delay as Duration
|
||||
if( config.maxDelay )
|
||||
maxDelay = config.maxDelay as Duration
|
||||
if( config.maxAttempts )
|
||||
maxAttempts = config.maxAttempts as int
|
||||
if( config.jitter )
|
||||
jitter = config.jitter as double
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package recreationaltech.plugin.client
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import nextflow.exception.ProcessException
|
||||
import nextflow.exception.ShowOnlyExceptionMessage
|
||||
|
||||
/**
|
||||
* Exception raised when a pod cannot be scheduled because
|
||||
* e.g. the container image cannot be pulled, required resources
|
||||
* cannot be fulfilled, etc.
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
@CompileStatic
|
||||
class PodUnschedulableException extends ProcessException implements ShowOnlyExceptionMessage {
|
||||
|
||||
PodUnschedulableException(String message, Throwable cause) {
|
||||
super(message,cause)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package recreationaltech.plugin.client;
|
||||
|
||||
/**
|
||||
* This file is derived from
|
||||
* https://github.com/kubernetes-client/java/blob/master/util/src/main/java/io/kubernetes/client/util/SSLUtils.java
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Security;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.RSAPrivateCrtKeySpec;
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
|
||||
|
||||
public class SSLUtils {
|
||||
|
||||
public static boolean isNotNullOrEmpty(String val) {
|
||||
return val != null && val.length() > 0;
|
||||
}
|
||||
|
||||
public static KeyManager[] keyManagers(String certData, String certFile, String keyData, String keyFile,
|
||||
String algo, String passphrase, String keyStoreFile, String keyStorePassphrase)
|
||||
throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, CertificateException,
|
||||
InvalidKeySpecException, IOException {
|
||||
KeyManager[] keyManagers = null;
|
||||
if ((isNotNullOrEmpty(certData) || isNotNullOrEmpty(certFile))
|
||||
&& (isNotNullOrEmpty(keyData) || isNotNullOrEmpty(keyFile))) {
|
||||
KeyStore keyStore = createKeyStore(certData, certFile, keyData, keyFile, algo, passphrase, keyStoreFile,
|
||||
keyStorePassphrase);
|
||||
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
kmf.init(keyStore, passphrase.toCharArray());
|
||||
keyManagers = kmf.getKeyManagers();
|
||||
}
|
||||
return keyManagers;
|
||||
}
|
||||
|
||||
|
||||
public static KeyStore createKeyStore(String clientCertData, String clientCertFile, String clientKeyData,
|
||||
String clientKeyFile, String clientKeyAlgo, String clientKeyPassphrase, String keyStoreFile,
|
||||
String keyStorePassphrase) throws IOException, CertificateException, NoSuchAlgorithmException,
|
||||
InvalidKeySpecException, KeyStoreException {
|
||||
try (InputStream certInputStream = getInputStreamFromDataOrFile(clientCertData, clientCertFile);
|
||||
InputStream keyInputStream = getInputStreamFromDataOrFile(clientKeyData, clientKeyFile)) {
|
||||
return createKeyStore(certInputStream, keyInputStream, clientKeyAlgo,
|
||||
clientKeyPassphrase != null ? clientKeyPassphrase.toCharArray() : null,
|
||||
keyStoreFile, getKeyStorePassphrase(keyStorePassphrase));
|
||||
}
|
||||
}
|
||||
|
||||
static private PrivateKey generateEcKey(InputStream keyInputStream) throws IOException {
|
||||
PrivateKey privateKey=null;
|
||||
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
|
||||
Object object = new PEMParser(new InputStreamReader(keyInputStream)).readObject();
|
||||
if (object instanceof PEMKeyPair) {
|
||||
PEMKeyPair keys = (PEMKeyPair) object;
|
||||
privateKey = new JcaPEMKeyConverter().getKeyPair(keys).getPrivate();
|
||||
}
|
||||
if( object instanceof PrivateKeyInfo) {
|
||||
PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo)object;
|
||||
privateKey = new JcaPEMKeyConverter().getPrivateKey(privateKeyInfo);
|
||||
}
|
||||
if( privateKey == null) {
|
||||
throw new IOException("Unsupported EC algorithm");
|
||||
}
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
static private PrivateKey generateStdKey(InputStream keyInputStream, String clientKeyAlgo) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
byte[] keyBytes = decodePem(keyInputStream);
|
||||
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(clientKeyAlgo);
|
||||
try {
|
||||
// First let's try PKCS8
|
||||
return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||
}
|
||||
catch (InvalidKeySpecException e) {
|
||||
// Otherwise try PKCS1
|
||||
RSAPrivateCrtKeySpec keySpec = decodePKCS1(keyBytes);
|
||||
return keyFactory.generatePrivate(keySpec);
|
||||
}
|
||||
}
|
||||
|
||||
public static KeyStore createKeyStore(InputStream certInputStream, InputStream keyInputStream, String clientKeyAlgo,
|
||||
char[] clientKeyPassphrase, String keyStoreFile, char[] keyStorePassphrase) throws IOException,
|
||||
CertificateException, NoSuchAlgorithmException, InvalidKeySpecException, KeyStoreException {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X509");
|
||||
X509Certificate cert = (X509Certificate) certFactory.generateCertificate(certInputStream);
|
||||
|
||||
PrivateKey privateKey = clientKeyAlgo.equals("EC")
|
||||
? generateEcKey(keyInputStream)
|
||||
: generateStdKey(keyInputStream, clientKeyAlgo);
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance("JKS");
|
||||
if (keyStoreFile != null && keyStoreFile.length() > 0) {
|
||||
keyStore.load(new FileInputStream(keyStoreFile), keyStorePassphrase);
|
||||
} else {
|
||||
loadDefaultKeyStoreFile(keyStore, keyStorePassphrase);
|
||||
}
|
||||
|
||||
String alias = cert.getSubjectX500Principal().getName();
|
||||
keyStore.setKeyEntry(alias, privateKey, clientKeyPassphrase, new Certificate[] { cert });
|
||||
|
||||
return keyStore;
|
||||
}
|
||||
|
||||
// This method is inspired and partly taken over from
|
||||
// http://oauth.googlecode.com/svn/code/java/
|
||||
// All credits to belong to them.
|
||||
private static byte[] decodePem(InputStream keyInputStream) throws IOException {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(keyInputStream));
|
||||
try {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.contains("-----BEGIN ")) {
|
||||
return readBytes(reader, line.trim().replace("BEGIN", "END"));
|
||||
}
|
||||
}
|
||||
throw new IOException("PEM is invalid: no begin marker");
|
||||
} finally {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readBytes(BufferedReader reader, String endMarker) throws IOException {
|
||||
String line;
|
||||
StringBuffer buf = new StringBuffer();
|
||||
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.indexOf(endMarker) != -1) {
|
||||
return Base64.decodeBase64(buf.toString());
|
||||
}
|
||||
buf.append(line.trim());
|
||||
}
|
||||
throw new IOException("PEM is invalid : No end marker");
|
||||
}
|
||||
|
||||
public static RSAPrivateCrtKeySpec decodePKCS1(byte[] keyBytes) throws IOException {
|
||||
DerParser parser = new DerParser(keyBytes);
|
||||
Asn1Object sequence = parser.read();
|
||||
sequence.validateSequence();
|
||||
parser = new DerParser(sequence.getValue());
|
||||
parser.read();
|
||||
|
||||
return new RSAPrivateCrtKeySpec(next(parser), next(parser), next(parser), next(parser), next(parser),
|
||||
next(parser), next(parser), next(parser));
|
||||
}
|
||||
|
||||
private static BigInteger next(DerParser parser) throws IOException {
|
||||
return parser.read().getInteger();
|
||||
}
|
||||
|
||||
static class DerParser {
|
||||
|
||||
private InputStream in;
|
||||
|
||||
DerParser(byte[] bytes) throws IOException {
|
||||
this.in = new ByteArrayInputStream(bytes);
|
||||
}
|
||||
|
||||
Asn1Object read() throws IOException {
|
||||
int tag = in.read();
|
||||
|
||||
if (tag == -1) {
|
||||
throw new IOException("Invalid DER: stream too short, missing tag");
|
||||
}
|
||||
|
||||
int length = getLength();
|
||||
byte[] value = new byte[length];
|
||||
if (in.read(value) < length) {
|
||||
throw new IOException("Invalid DER: stream too short, missing value");
|
||||
}
|
||||
|
||||
return new Asn1Object(tag, value);
|
||||
}
|
||||
|
||||
private int getLength() throws IOException {
|
||||
int i = in.read();
|
||||
if (i == -1) {
|
||||
throw new IOException("Invalid DER: length missing");
|
||||
}
|
||||
|
||||
if ((i & ~0x7F) == 0) {
|
||||
return i;
|
||||
}
|
||||
|
||||
int num = i & 0x7F;
|
||||
if (i >= 0xFF || num > 4) {
|
||||
throw new IOException("Invalid DER: length field too big (" + i + ")");
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[num];
|
||||
if (in.read(bytes) < num) {
|
||||
throw new IOException("Invalid DER: length too short");
|
||||
}
|
||||
|
||||
return new BigInteger(1, bytes).intValue();
|
||||
}
|
||||
}
|
||||
|
||||
static class Asn1Object {
|
||||
|
||||
private final int type;
|
||||
private final byte[] value;
|
||||
private final int tag;
|
||||
|
||||
public Asn1Object(int tag, byte[] value) {
|
||||
this.tag = tag;
|
||||
this.type = tag & 0x1F;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public byte[] getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
BigInteger getInteger() throws IOException {
|
||||
if (type != 0x02) {
|
||||
throw new IOException("Invalid DER: object is not integer"); //$NON-NLS-1$
|
||||
}
|
||||
return new BigInteger(value);
|
||||
}
|
||||
|
||||
void validateSequence() throws IOException {
|
||||
if (type != 0x10) {
|
||||
throw new IOException("Invalid DER: not a sequence");
|
||||
}
|
||||
if ((tag & 0x20) != 0x20) {
|
||||
throw new IOException("Invalid DER: can't parse primitive entity");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadDefaultKeyStoreFile(KeyStore keyStore, char[] keyStorePassphrase)
|
||||
throws CertificateException, NoSuchAlgorithmException, IOException {
|
||||
|
||||
String keyStorePath = System.getProperty("javax.net.ssl.keyStore");
|
||||
if (keyStorePath != null && keyStorePath.length() > 0) {
|
||||
File keyStoreFile = new File(keyStorePath);
|
||||
if (loadDefaultStoreFile(keyStore, keyStoreFile, keyStorePassphrase)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
keyStore.load(null);
|
||||
}
|
||||
|
||||
private static boolean loadDefaultStoreFile(KeyStore keyStore, File fileToLoad, char[] passphrase)
|
||||
throws CertificateException, NoSuchAlgorithmException, IOException {
|
||||
if (fileToLoad.exists() && fileToLoad.isFile() && fileToLoad.length() > 0) {
|
||||
keyStore.load(new FileInputStream(fileToLoad), passphrase);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static InputStream getInputStreamFromDataOrFile(String data, String file) throws FileNotFoundException {
|
||||
if (data != null) {
|
||||
byte[] bytes = Base64.decodeBase64(data);
|
||||
// TODO handle non-base64 here?
|
||||
return new ByteArrayInputStream(bytes);
|
||||
}
|
||||
if (file != null) {
|
||||
return new FileInputStream(file);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static char[] getKeyStorePassphrase(String keyStorePassphrase) {
|
||||
if (keyStorePassphrase == null || keyStorePassphrase.length() == 0) {
|
||||
return System.getProperty("javax.net.ssl.keyStorePassword", "changeit").toCharArray();
|
||||
}
|
||||
return keyStorePassphrase.toCharArray();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user