Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ dependencies {
compileOnly 'io.nextflow:nf-lang:25.04.4'
implementation 'com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey3:3.80.3'
implementation 'com.oracle.oci.sdk:oci-java-sdk-objectstorage:3.80.3'
// Enables OKE Workload Identity authentication for pods running in
// Oracle Kubernetes Engine (Instance/Resource principals ship in the common SDK).
implementation 'com.oracle.oci.sdk:oci-java-sdk-addons-oke-workload-identity:3.80.3'
}

asciidoctor{
Expand Down
64 changes: 64 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,70 @@ tenancy=ocid1.tenancy.oc1..aaaaaaaayyyyyyyyyyyyyyyyyyyyyyyyy
region=us-ashburn-1
----

== Authentication

By default (`authType = 'auto'`) the plugin uses inline API key credentials when
they are supplied in the config, otherwise it falls back to the local
`~/.oci/config` file — the original behaviour. For Kubernetes pods running in OKE
you can instead use Workload Identity, a credential-less method that avoids
shipping API keys into your containers.

Select a method with the `oci.authType` config option (or the `OCI_AUTH_TYPE`
environment variable):

[cols="1,3", options="header"]
|===
| `authType` | Description

| `auto` (default)
| Inline API key credentials when supplied, otherwise the local `~/.oci/config` file.

| `workload_identity`
| *OKE Workload Identity* — the recommended method for pods running in an Oracle
Kubernetes Engine (enhanced) cluster. No API keys are needed; OKE mounts a
service account token into the pod and the plugin federates it to an OCI
principal.

| `simple`
| Inline API key credentials supplied directly in the Nextflow config.

| `config_file`
| The standard `~/.oci/config` file.
|===

=== Kubernetes / OKE (recommended)

For workloads running in OKE, use Workload Identity so no long-lived credentials
are stored in the pod. First grant access by creating the IAM policy and an
enhanced-cluster service account (see the
https://docs.oracle.com/en-us/iaas/Content/ContEng/Tasks/contenggrantingworkloadaccesstoresources.htm[OCI docs]),
then configure the plugin:

.nextflow.config
[source]
----
oci {
authType = 'workload_identity'
region = 'us-ashburn-1'
}
----

The service account token is read from the standard OKE pod mount path by default.
Override it with `oci.tokenPath` if your cluster mounts it elsewhere.

=== Inline credentials

[source]
----
oci {
region = 'us-ashburn-1'
tenantId = 'ocid1.tenancy.oc1..aaaa...'
userId = 'ocid1.user.oc1..aaaa...'
fingerprint = '12:34:56:78:90:ab:cd:ef:12:34:56:78:90:ab:cd:ef'
privateKey = '-----BEGIN PRIVATE KEY-----\n...'
}
----

== ObjectStorage FileSystem

The nf-oci plugin brings native Oracle Cloud Infrastructure support to the Nextflow ecosystem. It implements a dedicated file system provider that allows users to interact with OCI Object Storage using a standardized protocol.
Expand Down
22 changes: 20 additions & 2 deletions src/main/groovy/incsteps/plugin/oci/client/OciClient.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package incsteps.plugin.oci.client


import com.oracle.bmc.Region
import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider
import com.oracle.bmc.auth.RegionProvider
import com.oracle.bmc.http.client.jersey3.Jersey3HttpProvider
import com.oracle.bmc.objectstorage.ObjectStorage
import com.oracle.bmc.objectstorage.ObjectStorageClient
Expand Down Expand Up @@ -38,14 +40,30 @@ class OciClient {
}

private ObjectStorageClient getObjectStorageClient(){
final useRegion = ociConfig.region
final provider = ociConfig.authentificationProvider?.provider
return ObjectStorageClient.builder()
.httpProvider(Jersey3HttpProvider.instance)
.region(Region.fromRegionCode(useRegion))
.region(resolveRegion(provider))
.build(provider)
}

/**
* Determines the region for the client. An explicitly configured region always
* wins; otherwise, when the provider advertises one (e.g. OKE Workload Identity)
* that region is used, falling back to the default region only as a last resort.
*/
private Region resolveRegion(AbstractAuthenticationDetailsProvider provider){
final configured = ociConfig.configuredRegion
if( configured )
return Region.fromRegionCode(configured)
if( provider instanceof RegionProvider ) {
final region = ((RegionProvider) provider).region
if( region )
return region
}
return Region.US_PHOENIX_1
}

private <T> T runWithPermit(Supplier<T> action) {
try {
if (semaphore != null) semaphore.acquire();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,56 +5,124 @@ import com.oracle.bmc.Region
import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider
import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider
import com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider
import com.oracle.bmc.auth.okeworkloadidentity.OkeWorkloadIdentityAuthenticationDetailsProvider
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

import java.nio.charset.StandardCharsets

/**
* Resolves the OCI {@link AbstractAuthenticationDetailsProvider} used by the plugin.
*
* The method is selected via the {@code oci.authType} config option (or the
* {@code OCI_AUTH_TYPE} environment variable):
*
* <ul>
* <li>{@code auto} (default) - inline API key credentials when supplied, otherwise
* the local {@code ~/.oci/config} file.</li>
* <li>{@code workload_identity} - OKE Workload Identity, for pods running in an
* Oracle Kubernetes Engine (enhanced) cluster. The credential-less method for
* Kubernetes workloads.</li>
* <li>{@code simple} - explicit API key credentials supplied inline.</li>
* <li>{@code config_file} - the standard {@code ~/.oci/config} file.</li>
* </ul>
*/
@Slf4j
@CompileStatic
class AuthentificationDetailProvider {

static final String AUTH_AUTO = 'auto'
static final String AUTH_SIMPLE = 'simple'
static final String AUTH_CONFIG_FILE = 'config_file'
static final String AUTH_WORKLOAD_IDENTITY = 'workload_identity'

final AbstractAuthenticationDetailsProvider provider

AuthentificationDetailProvider(Map otps, String region){
provider = build(otps, region)
AuthentificationDetailProvider(Map opts, String region) {
provider = build(opts, region)
}

AbstractAuthenticationDetailsProvider getProvider() {
this.provider
}

private AbstractAuthenticationDetailsProvider buildEnvProvider(Map otps, String region){
if( !otps.containsKey("tenantId") )
return null
if( !otps.containsKey("userId") )
return null
if( !otps.containsKey("fingerprint") )
return null
if( !otps.containsKey("privateKey") )
private AbstractAuthenticationDetailsProvider build(Map opts, String region) {
final authType = normalizeAuthType(opts.get('authType'))
switch (authType) {
case AUTH_SIMPLE:
return requireProvider(buildSimpleProvider(opts, region), AUTH_SIMPLE)
case AUTH_CONFIG_FILE:
return buildConfigFileProvider(opts)
case AUTH_WORKLOAD_IDENTITY:
return buildWorkloadIdentityProvider(opts, region)
default:
return autoDetect(opts, region)
}
}

/** Inline API key credentials when supplied, otherwise the local config file. */
private AbstractAuthenticationDetailsProvider autoDetect(Map opts, String region) {
return buildSimpleProvider(opts, region) ?: buildConfigFileProvider(opts)
}

protected boolean hasSimpleCredentials(Map opts) {
opts.get('tenantId') && opts.get('userId') && opts.get('fingerprint') && opts.get('privateKey')
}

private AbstractAuthenticationDetailsProvider buildSimpleProvider(Map opts, String region) {
if (!hasSimpleCredentials(opts))
return null
final String privKey = otps.get("privateKey").toString()
return SimpleAuthenticationDetailsProvider.builder()
.tenantId(otps.get("tenantId").toString())
.userId(otps.get("userId").toString())
.fingerprint(otps.get("fingerprint").toString())
.region(Region.fromRegionCode(region))
.privateKeySupplier(() -> {
return new ByteArrayInputStream(privKey.getBytes(StandardCharsets.UTF_8))
})
.build();
}

private AbstractAuthenticationDetailsProvider buildDefaultProvider(Map opts){
final ConfigFileReader.ConfigFile configFile = ConfigFileReader.parseDefault(opts.get("profile")?.toString())
final ConfigFileAuthenticationDetailsProvider provider = new ConfigFileAuthenticationDetailsProvider(configFile)
final String privKey = opts.get('privateKey').toString()
final builder = SimpleAuthenticationDetailsProvider.builder()
.tenantId(opts.get('tenantId').toString())
.userId(opts.get('userId').toString())
.fingerprint(opts.get('fingerprint').toString())
.privateKeySupplier(() -> new ByteArrayInputStream(privKey.getBytes(StandardCharsets.UTF_8)))
if (region)
builder.region(Region.fromRegionCode(region))
return builder.build()
}

private AbstractAuthenticationDetailsProvider buildConfigFileProvider(Map opts) {
final ConfigFileReader.ConfigFile configFile = ConfigFileReader.parseDefault(opts.get('profile')?.toString())
return new ConfigFileAuthenticationDetailsProvider(configFile)
}

private AbstractAuthenticationDetailsProvider buildWorkloadIdentityProvider(Map opts, String region) {
final builder = OkeWorkloadIdentityAuthenticationDetailsProvider.builder()
// The OKE SDK reads the service account token from the standard pod mount path
// by default; only override it when the user points us elsewhere.
final tokenPath = opts.get('tokenPath')?.toString()
if (tokenPath)
builder.tokenPath(tokenPath)
if (region)
builder.region(Region.fromRegionCode(region))
return builder.build()
}

private static AbstractAuthenticationDetailsProvider requireProvider(AbstractAuthenticationDetailsProvider provider, String authType) {
if (!provider)
throw new IllegalStateException("OCI authType '${authType}' was requested but required credentials are missing")
return provider
}

private AbstractAuthenticationDetailsProvider build(Map opts, String region){
def ret = buildEnvProvider(opts, region)
if( !ret ){
ret = buildDefaultProvider(opts)
/** Resolves the configured auth type to one of the supported canonical values. */
protected static String normalizeAuthType(Object raw) {
if (!raw)
return AUTH_AUTO
final value = raw.toString().trim().toLowerCase()
switch (value) {
case AUTH_AUTO:
return AUTH_AUTO
case AUTH_SIMPLE:
return AUTH_SIMPLE
case AUTH_CONFIG_FILE:
return AUTH_CONFIG_FILE
case AUTH_WORKLOAD_IDENTITY:
return AUTH_WORKLOAD_IDENTITY
default:
throw new IllegalArgumentException("Unknown OCI authType '${raw}'. Valid values: " +
"auto, simple, config_file, workload_identity")
}
return ret
}

}
41 changes: 40 additions & 1 deletion src/main/groovy/incsteps/plugin/oci/config/OciConfig.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,36 @@ class OciConfig implements ConfigScope{
""")
final String profile

@ConfigOption
@Description("""
Authentication method to use. One of `auto` (default, uses inline API key
credentials when supplied otherwise `~/.oci/config`), `workload_identity`
(OKE Workload Identity, recommended for Kubernetes pods), `simple`
(inline API key) or `config_file` (`~/.oci/config`).
""")
final String authType

@ConfigOption
@Description("""
Path to the Kubernetes service account token used by `workload_identity`
authentication. Defaults to the standard OKE pod mount path.
""")
final String tokenPath

OciConfig(){
this([:])
}

OciConfig(Map opts){
this.profile = getOciProfile0(SysEnv.get(), opts)
this.region = getOciRegion(SysEnv.get(), opts)
this.authType = getOciAuthType(SysEnv.get(), opts)
this.tokenPath = opts.tokenPath as String
this.objectStorageConfig = new OciObjectStorageConfig( (Map)opts.storage ?: Collections.emptyMap())
this.authentificationProvider = new AuthentificationDetailProvider(opts, region)
// make the resolved auth settings visible to the provider regardless of their source
final Map authOpts = new LinkedHashMap(opts)
authOpts.authType = authType
this.authentificationProvider = new AuthentificationDetailProvider(authOpts, region)
}

AuthentificationDetailProvider getAuthentificationProvider(){
Expand All @@ -52,6 +73,11 @@ class OciConfig implements ConfigScope{
return region ?: Region.US_PHOENIX_1.regionCode
}

/** The region explicitly set via config/env, or {@code null} if unset. */
String getConfiguredRegion(){
return region
}

static protected String getOciProfile0(Map env, Map<String,Object> config) {

final profile = config?.profile as String
Expand All @@ -68,6 +94,19 @@ class OciConfig implements ConfigScope{
}


static protected String getOciAuthType(Map env, Map<String,Object> config) {

final authType = config?.authType as String
if( authType )
return authType

if( env?.containsKey('OCI_AUTH_TYPE'))
return env.get('OCI_AUTH_TYPE')

return null
}


static protected String getOciRegion(Map env, Map config) {

def home = Paths.get(System.properties.get('user.home') as String)
Expand Down
Loading