From 55ae962012999113fa3b17f6d8fc892535a6d3a1 Mon Sep 17 00:00:00 2001 From: Tuan Pham Date: Sat, 2 May 2026 10:00:53 +1000 Subject: [PATCH 1/2] Add S3-backed config storage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- conf/zeppelin-env.cmd.template | 3 + conf/zeppelin-env.sh.template | 4 + conf/zeppelin-site.xml.template | 25 ++ docs/setup/operation/configuration.md | 24 ++ docs/setup/storage/configuration_storage.md | 41 ++- zeppelin-plugins/configstorage/s3/pom.xml | 115 ++++++++ .../zeppelin/storage/S3ConfigStorage.java | 274 ++++++++++++++++++ .../zeppelin/storage/S3ConfigStorageTest.java | 175 +++++++++++ zeppelin-plugins/pom.xml | 2 + .../zeppelin/conf/ZeppelinConfiguration.java | 10 + .../zeppelin/storage/ConfigStorage.java | 70 ++++- .../zeppelin/storage/ConfigStorageTest.java | 82 ++++++ 12 files changed, 816 insertions(+), 9 deletions(-) create mode 100644 zeppelin-plugins/configstorage/s3/pom.xml create mode 100644 zeppelin-plugins/configstorage/s3/src/main/java/org/apache/zeppelin/storage/S3ConfigStorage.java create mode 100644 zeppelin-plugins/configstorage/s3/src/test/java/org/apache/zeppelin/storage/S3ConfigStorageTest.java create mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/storage/ConfigStorageTest.java diff --git a/conf/zeppelin-env.cmd.template b/conf/zeppelin-env.cmd.template index 15c88fd4ca8..a300fcfd0d0 100644 --- a/conf/zeppelin-env.cmd.template +++ b/conf/zeppelin-env.cmd.template @@ -38,6 +38,9 @@ REM set ZEPPELIN_NOTEBOOK_S3_KMS_KEY_ID REM AWS KMS key ID REM set ZEPPELIN_NOTEBOOK_S3_KMS_KEY_REGION REM AWS KMS key region REM set ZEPPELIN_NOTEBOOK_S3_SSE REM Server-side encryption enabled for notebooks REM set ZEPPELIN_NOTEBOOK_S3_PATH_STYLE_ACCESS REM Path style access for S3 bucket +REM set ZEPPELIN_CONFIG_STORAGE_CLASS REM Configuration persistence layer implementation +REM set ZEPPELIN_CONFIG_S3_DIR REM S3 prefix or URI for interpreter.json, notebook-authorization.json, and credentials.json +REM set ZEPPELIN_CONFIG_S3_CANNED_ACL REM Optional canned ACL for S3-backed configuration files REM set ZEPPELIN_IDENT_STRING REM A string representing this instance of zeppelin. $USER by default. REM set ZEPPELIN_NICENESS REM The scheduling priority for daemons. Defaults to 0. REM set ZEPPELIN_INTERPRETER_LOCALREPO REM Local repository for interpreter's additional dependency loading diff --git a/conf/zeppelin-env.sh.template b/conf/zeppelin-env.sh.template index e8160b563c9..f784b0258d6 100644 --- a/conf/zeppelin-env.sh.template +++ b/conf/zeppelin-env.sh.template @@ -46,6 +46,10 @@ # export ZEPPELIN_NOTEBOOK_S3_SSE # Server-side encryption enabled for notebooks # export ZEPPELIN_NOTEBOOK_S3_PATH_STYLE_ACCESS # Path style access for S3 bucket +# export ZEPPELIN_CONFIG_STORAGE_CLASS # Configuration persistence layer implementation +# export ZEPPELIN_CONFIG_S3_DIR # S3 prefix or URI for interpreter.json, notebook-authorization.json, and credentials.json +# export ZEPPELIN_CONFIG_S3_CANNED_ACL # Optional canned ACL for S3-backed configuration files + # export ZEPPELIN_NOTEBOOK_GCS_STORAGE_DIR # GCS "directory" (prefix) under which notebooks are saved. E.g. gs://example-bucket/path/to/dir # export GOOGLE_APPLICATION_CREDENTIALS # Provide a service account key file for GCS and BigQuery API calls (overrides application default credentials) diff --git a/conf/zeppelin-site.xml.template b/conf/zeppelin-site.xml.template index d5e54b91f16..0d7409decc1 100755 --- a/conf/zeppelin-site.xml.template +++ b/conf/zeppelin-site.xml.template @@ -165,6 +165,31 @@ --> + + + + + + diff --git a/docs/setup/operation/configuration.md b/docs/setup/operation/configuration.md index 9588cd25a5b..3aff8539b77 100644 --- a/docs/setup/operation/configuration.md +++ b/docs/setup/operation/configuration.md @@ -316,6 +316,30 @@ Sources descending by priority: Optional override to control which signature algorithm should be used to sign AWS requests + +
ZEPPELIN_CONFIG_STORAGE_CLASS
+
zeppelin.config.storage.class
+ org.apache.zeppelin.storage.LocalConfigStorage + Configuration persistence layer implementation for interpreter.json, notebook-authorization.json, and credentials.json + + +
ZEPPELIN_CONFIG_FS_DIR
+
zeppelin.config.fs.dir
+ + Path for FileSystemConfigStorage + + +
ZEPPELIN_CONFIG_S3_DIR
+
zeppelin.config.s3.dir
+ + S3 prefix or s3://bucket/prefix URI used by S3ConfigStorage; defaults to {zeppelin.notebook.s3.user}/config + + +
ZEPPELIN_CONFIG_S3_CANNED_ACL
+
zeppelin.config.s3.cannedAcl
+ + Optional canned ACL for S3-backed configuration files; keep unset unless required because configuration files can contain secrets +
ZEPPELIN_NOTEBOOK_AZURE_CONNECTION_STRING
zeppelin.notebook.azure.connectionString
diff --git a/docs/setup/storage/configuration_storage.md b/docs/setup/storage/configuration_storage.md index 3a5bbff9dfb..e289df8f07b 100644 --- a/docs/setup/storage/configuration_storage.md +++ b/docs/setup/storage/configuration_storage.md @@ -27,7 +27,44 @@ limitations under the License. Zeppelin has lots of configuration which is stored in files: - `interpreter.json` (This file contains all the interpreter setting info) - `notebook-authorization.json` (This file contains all the note authorization info) -- `credential.json` (This file contains the credential info) +- `credentials.json` (This file contains the credential info) + +## Configuration Storage in S3 + +Set the following properties in `zeppelin-site.xml` to persist Zeppelin configuration +state in S3. This stores `interpreter.json`, `notebook-authorization.json`, and +`credentials.json` under a shared S3 prefix. + +```xml + + zeppelin.config.storage.class + org.apache.zeppelin.storage.S3ConfigStorage + configuration persistence layer implementation + + + zeppelin.config.s3.dir + s3://bucket_name/user/config + S3 prefix or URI for Zeppelin configuration files + +``` + +`zeppelin.config.s3.dir` can be either an S3 URI such as +`s3://bucket_name/user/config` or a key prefix such as `user/config`. When only a +key prefix is provided, `zeppelin.notebook.s3.bucket` is used as the bucket. When +`zeppelin.config.s3.dir` is omitted, Zeppelin stores configuration under +`{zeppelin.notebook.s3.user}/config`. + +`S3ConfigStorage` uses the same S3 client settings as `S3NotebookRepo`, including +`zeppelin.notebook.s3.endpoint`, `zeppelin.notebook.s3.pathStyleAccess`, +`zeppelin.notebook.s3.sse`, `zeppelin.notebook.s3.kmsKeyID`, +`zeppelin.notebook.s3.kmsKeyRegion`, +`zeppelin.notebook.s3.encryptionMaterialsProvider`, +and `zeppelin.notebook.s3.signerOverride`. + +`S3ConfigStorage` does not inherit `zeppelin.notebook.s3.cannedAcl`, because +configuration files can contain credentials and other sensitive values. If your +bucket ownership policy requires a canned ACL, set `zeppelin.config.s3.cannedAcl` +explicitly and avoid public or broadly readable ACLs. ## Configuration Storage in hadoop compatible file system @@ -62,4 +99,4 @@ By default, zeppelin store configuration on local file system. path on local file system -``` \ No newline at end of file +``` diff --git a/zeppelin-plugins/configstorage/s3/pom.xml b/zeppelin-plugins/configstorage/s3/pom.xml new file mode 100644 index 00000000000..1336d18329d --- /dev/null +++ b/zeppelin-plugins/configstorage/s3/pom.xml @@ -0,0 +1,115 @@ + + + + + + 4.0.0 + + + zengine-plugins-parent + org.apache.zeppelin + 0.13.0-SNAPSHOT + ../../../zeppelin-plugins + + + configstorage-s3 + jar + Zeppelin: Plugin S3ConfigStorage + ConfigStorage implementation based on S3 + + + 1.12.261 + ConfigStorage/S3ConfigStorage + + + + + com.amazonaws + aws-java-sdk-s3 + ${aws.sdk.version} + + + commons-logging + commons-logging + + + + + + com.amazonaws + aws-java-sdk-sts + ${aws.sdk.version} + + + commons-logging + commons-logging + + + + + + org.gaul + s3proxy + 2.0.0 + test + + + org.eclipse.jetty + jetty-http + + + org.eclipse.jetty + jetty-io + + + com.google.guava + guava + + + org.eclipse.jetty + jetty-util + + + com.google.errorprone + error_prone_annotations + + + com.google.code.findbugs + jsr305 + + + javax.annotation + javax.annotation-api + + + jakarta.xml.bind + jakarta.xml.bind-api + + + + + + + + + maven-dependency-plugin + + + + diff --git a/zeppelin-plugins/configstorage/s3/src/main/java/org/apache/zeppelin/storage/S3ConfigStorage.java b/zeppelin-plugins/configstorage/s3/src/main/java/org/apache/zeppelin/storage/S3ConfigStorage.java new file mode 100644 index 00000000000..aa5dfe9ae84 --- /dev/null +++ b/zeppelin-plugins/configstorage/s3/src/main/java/org/apache/zeppelin/storage/S3ConfigStorage.java @@ -0,0 +1,274 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.zeppelin.storage; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.ClientConfiguration; +import com.amazonaws.ClientConfigurationFactory; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.regions.Region; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3EncryptionClient; +import com.amazonaws.services.s3.S3ClientOptions; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.CryptoConfiguration; +import com.amazonaws.services.s3.model.EncryptionMaterialsProvider; +import com.amazonaws.services.s3.model.GetObjectRequest; +import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.model.S3Object; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; +import org.apache.zeppelin.interpreter.InterpreterInfoSaving; +import org.apache.zeppelin.notebook.NotebookAuthorizationInfoSaving; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; + +/** + * Stores Zeppelin mutable configuration JSON files in S3. + */ +public class S3ConfigStorage extends ConfigStorage { + + private static final Logger LOGGER = LoggerFactory.getLogger(S3ConfigStorage.class); + + private static final String S3 = "s3"; + private static final String S3A = "s3a"; + private static final String INTERPRETER_SETTING_FILE = "interpreter.json"; + private static final String NOTEBOOK_AUTHORIZATION_FILE = "notebook-authorization.json"; + private static final String CREDENTIALS_FILE = "credentials.json"; + + private final AmazonS3 s3client; + private final String bucketName; + private final String configPrefix; + private final boolean useServerSideEncryption; + private final CannedAccessControlList objectCannedAcl; + + public S3ConfigStorage(ZeppelinConfiguration zConf) throws IOException { + super(zConf); + S3Location configLocation = resolveConfigLocation(zConf); + this.bucketName = configLocation.bucketName; + this.configPrefix = configLocation.keyPrefix; + this.useServerSideEncryption = zConf.isS3ServerSideEncryption(); + if (StringUtils.isNotBlank(zConf.getConfigS3CannedAcl())) { + this.objectCannedAcl = CannedAccessControlList.valueOf(zConf.getConfigS3CannedAcl()); + } else { + this.objectCannedAcl = null; + } + this.s3client = createS3Client(zConf); + LOGGER.info("Using S3 prefix s3://{}/{} to store Zeppelin Config", bucketName, configPrefix); + } + + @Override + public void save(InterpreterInfoSaving settingInfos) throws IOException { + saveJson("Interpreter Settings", objectKey(INTERPRETER_SETTING_FILE), settingInfos.toJson()); + } + + @Override + public InterpreterInfoSaving loadInterpreterSettings() throws IOException { + String json = loadJson("Interpreter Settings", objectKey(INTERPRETER_SETTING_FILE)); + return json == null ? null : buildInterpreterInfoSaving(json); + } + + @Override + public void save(NotebookAuthorizationInfoSaving authorizationInfoSaving) throws IOException { + saveJson("notebook authorization", objectKey(NOTEBOOK_AUTHORIZATION_FILE), + authorizationInfoSaving.toJson()); + } + + @Override + public NotebookAuthorizationInfoSaving loadNotebookAuthorization() throws IOException { + String json = loadJson("notebook authorization", objectKey(NOTEBOOK_AUTHORIZATION_FILE)); + return json == null ? null : NotebookAuthorizationInfoSaving.fromJson(json); + } + + @Override + public String loadCredentials() throws IOException { + return loadJson("Credentials", objectKey(CREDENTIALS_FILE)); + } + + @Override + public void saveCredentials(String credentials) throws IOException { + saveJson("Credentials", objectKey(CREDENTIALS_FILE), credentials); + } + + public void close() { + if (s3client != null) { + s3client.shutdown(); + } + } + + private String loadJson(String storageName, String key) throws IOException { + try { + if (!s3client.doesObjectExist(bucketName, key)) { + LOGGER.warn("{} file s3://{}/{} is not existed", storageName, bucketName, key); + return null; + } + LOGGER.info("Load {} from S3: s3://{}/{}", storageName, bucketName, key); + S3Object s3Object = s3client.getObject(new GetObjectRequest(bucketName, key)); + try (InputStream input = s3Object.getObjectContent()) { + return IOUtils.toString(input, zConf.getString(ConfVars.ZEPPELIN_ENCODING)); + } + } catch (AmazonClientException e) { + throw new IOException("Fail to load " + storageName + " from S3", e); + } + } + + private void saveJson(String storageName, String key, String json) throws IOException { + try { + LOGGER.info("Save {} to S3: s3://{}/{}", storageName, bucketName, key); + byte[] bytes = json.getBytes(Charset.forName(zConf.getString(ConfVars.ZEPPELIN_ENCODING))); + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(bytes.length); + metadata.setContentType("application/json"); + if (useServerSideEncryption) { + metadata.setSSEAlgorithm(ObjectMetadata.AES_256_SERVER_SIDE_ENCRYPTION); + } + PutObjectRequest putRequest = new PutObjectRequest(bucketName, key, + new ByteArrayInputStream(bytes), metadata); + if (objectCannedAcl != null) { + putRequest.withCannedAcl(objectCannedAcl); + } + s3client.putObject(putRequest); + } catch (AmazonClientException e) { + throw new IOException("Fail to save " + storageName + " to S3", e); + } + } + + private String objectKey(String fileName) { + return StringUtils.isBlank(configPrefix) ? fileName : configPrefix + "/" + fileName; + } + + private static S3Location resolveConfigLocation(ZeppelinConfiguration zConf) throws IOException { + String configuredDir = firstNonBlank( + zConf.getConfigS3Dir(), + zConf.getS3User() + "/config"); + + try { + URI uri = new URI(configuredDir); + String scheme = uri.getScheme(); + if (StringUtils.isBlank(scheme)) { + return new S3Location(zConf.getS3BucketName(), normalizePrefix(configuredDir)); + } + if (!S3.equalsIgnoreCase(scheme) && !S3A.equalsIgnoreCase(scheme)) { + throw new IOException("Unsupported S3 config storage URI scheme: " + scheme); + } + if (StringUtils.isBlank(uri.getHost())) { + throw new IOException("S3 config storage URI must include a bucket: " + configuredDir); + } + return new S3Location(uri.getHost(), normalizePrefix(uri.getPath())); + } catch (URISyntaxException e) { + throw new IOException("Invalid S3 config storage location: " + configuredDir, e); + } + } + + private static String firstNonBlank(String first, String second) { + if (StringUtils.isNotBlank(first)) { + return first; + } + return second; + } + + private static String normalizePrefix(String keyPrefix) { + if (StringUtils.isBlank(keyPrefix)) { + return ""; + } + String normalized = keyPrefix.replace('\\', '/'); + while (normalized.startsWith("/")) { + normalized = normalized.substring(1); + } + while (normalized.endsWith("/")) { + normalized = normalized.substring(0, normalized.length() - 1); + } + return normalized; + } + + private static AmazonS3 createS3Client(ZeppelinConfiguration zConf) throws IOException { + AWSCredentialsProvider credentialsProvider = new DefaultAWSCredentialsProviderChain(); + CryptoConfiguration cryptoConf = new CryptoConfiguration(); + String keyRegion = zConf.getS3KMSKeyRegion(); + + if (StringUtils.isNotBlank(keyRegion)) { + cryptoConf.setAwsKmsRegion(Region.getRegion(Regions.fromName(keyRegion))); + } + + ClientConfiguration clientConf = createClientConfiguration(zConf); + AmazonS3 client; + String kmsKeyID = zConf.getS3KMSKeyID(); + if (StringUtils.isNotBlank(kmsKeyID)) { + KMSEncryptionMaterialsProvider emp = new KMSEncryptionMaterialsProvider(kmsKeyID); + client = new AmazonS3EncryptionClient(credentialsProvider, emp, clientConf, cryptoConf); + } else if (StringUtils.isNotBlank(zConf.getS3EncryptionMaterialsProviderClass())) { + EncryptionMaterialsProvider emp = createCustomProvider(zConf); + client = new AmazonS3EncryptionClient(credentialsProvider, emp, clientConf, cryptoConf); + } else { + client = new AmazonS3Client(credentialsProvider, clientConf); + } + client.setS3ClientOptions(S3ClientOptions.builder() + .setPathStyleAccess(zConf.isS3PathStyleAccess()).build()); + client.setEndpoint(zConf.getS3Endpoint()); + return client; + } + + private static ClientConfiguration createClientConfiguration(ZeppelinConfiguration zConf) { + ClientConfiguration config = new ClientConfigurationFactory().getConfig(); + String s3SignerOverride = zConf.getS3SignerOverride(); + if (StringUtils.isNotBlank(s3SignerOverride)) { + config.setSignerOverride(s3SignerOverride); + } + return config; + } + + private static EncryptionMaterialsProvider createCustomProvider(ZeppelinConfiguration zConf) + throws IOException { + String empClassname = zConf.getS3EncryptionMaterialsProviderClass(); + try { + Object empInstance = Class.forName(empClassname).getDeclaredConstructor().newInstance(); + if (empInstance instanceof EncryptionMaterialsProvider) { + return (EncryptionMaterialsProvider) empInstance; + } + throw new IOException("Class " + empClassname + " does not implement " + + EncryptionMaterialsProvider.class.getName()); + } catch (ReflectiveOperationException e) { + throw new IOException("Unable to instantiate encryption materials provider class " + + empClassname + ": " + e, e); + } + } + + private static class S3Location { + private final String bucketName; + private final String keyPrefix; + + private S3Location(String bucketName, String keyPrefix) { + this.bucketName = bucketName; + this.keyPrefix = keyPrefix; + } + } +} diff --git a/zeppelin-plugins/configstorage/s3/src/test/java/org/apache/zeppelin/storage/S3ConfigStorageTest.java b/zeppelin-plugins/configstorage/s3/src/test/java/org/apache/zeppelin/storage/S3ConfigStorageTest.java new file mode 100644 index 00000000000..d6b5bfd265e --- /dev/null +++ b/zeppelin-plugins/configstorage/s3/src/test/java/org/apache/zeppelin/storage/S3ConfigStorageTest.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.zeppelin.storage; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.interpreter.InterpreterInfoSaving; +import org.apache.zeppelin.notebook.NotebookAuthorizationInfoSaving; +import org.gaul.s3proxy.junit.S3ProxyExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class S3ConfigStorageTest { + + private static final String CONFIG_PREFIX = "config-root"; + + @RegisterExtension + static S3ProxyExtension s3Proxy = S3ProxyExtension.builder() + .withCredentials("access", "secret") + .build(); + + private AmazonS3 s3Client; + private S3ConfigStorage configStorage; + private String bucket; + private String previousAccessKeyId; + private String previousSecretKey; + + @BeforeEach + void setUp() throws IOException { + previousAccessKeyId = System.getProperty("aws.accessKeyId"); + previousSecretKey = System.getProperty("aws.secretKey"); + System.setProperty("aws.accessKeyId", s3Proxy.getAccessKey()); + System.setProperty("aws.secretKey", s3Proxy.getSecretKey()); + + s3Client = AmazonS3ClientBuilder + .standard() + .withCredentials(new AWSStaticCredentialsProvider( + new BasicAWSCredentials(s3Proxy.getAccessKey(), s3Proxy.getSecretKey()))) + .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration( + s3Proxy.getUri().toString(), Regions.US_EAST_1.getName())) + .build(); + bucket = "test-bucket-" + UUID.randomUUID(); + s3Client.createBucket(bucket); + + ZeppelinConfiguration zConf = ZeppelinConfiguration.load(); + zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_S3_ENDPOINT.getVarName(), + s3Proxy.getUri().toString()); + zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_S3_BUCKET.getVarName(), + bucket); + zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONFIG_S3_DIR.getVarName(), + CONFIG_PREFIX); + configStorage = new S3ConfigStorage(zConf); + } + + @AfterEach + void tearDown() { + if (configStorage != null) { + configStorage.close(); + } + if (s3Client != null) { + s3Client.shutdown(); + } + restoreSystemProperty("aws.accessKeyId", previousAccessKeyId); + restoreSystemProperty("aws.secretKey", previousSecretKey); + } + + @Test + void testSaveAndLoadConfigFiles() throws IOException { + assertNull(configStorage.loadInterpreterSettings()); + assertNull(configStorage.loadNotebookAuthorization()); + assertNull(configStorage.loadCredentials()); + + InterpreterInfoSaving interpreterInfoSaving = new InterpreterInfoSaving(); + configStorage.save(interpreterInfoSaving); + + NotebookAuthorizationInfoSaving authorizationInfoSaving = + new NotebookAuthorizationInfoSaving(new ConcurrentHashMap<>()); + configStorage.save(authorizationInfoSaving); + + String credentials = "{\n \"credentialsMap\": {}\n}"; + configStorage.saveCredentials(credentials); + + InterpreterInfoSaving loadedInterpreterInfo = configStorage.loadInterpreterSettings(); + assertNotNull(loadedInterpreterInfo); + assertTrue(loadedInterpreterInfo.interpreterSettings.isEmpty()); + + NotebookAuthorizationInfoSaving loadedAuthorization = configStorage.loadNotebookAuthorization(); + assertNotNull(loadedAuthorization); + assertTrue(loadedAuthorization.getAuthInfo().isEmpty()); + + assertEquals(credentials, configStorage.loadCredentials()); + assertTrue(s3Client.doesObjectExist(bucket, CONFIG_PREFIX + "/interpreter.json")); + assertTrue(s3Client.doesObjectExist(bucket, CONFIG_PREFIX + "/notebook-authorization.json")); + assertTrue(s3Client.doesObjectExist(bucket, CONFIG_PREFIX + "/credentials.json")); + } + + @Test + void testS3UriCanOverrideBucketAndPrefix() throws IOException { + String uriBucket = "uri-bucket-" + UUID.randomUUID(); + s3Client.createBucket(uriBucket); + + ZeppelinConfiguration zConf = ZeppelinConfiguration.load(); + zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_S3_ENDPOINT.getVarName(), + s3Proxy.getUri().toString()); + zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONFIG_S3_DIR.getVarName(), + "s3://" + uriBucket + "/custom/config/"); + + S3ConfigStorage uriStorage = new S3ConfigStorage(zConf); + try { + uriStorage.saveCredentials("{}"); + assertTrue(s3Client.doesObjectExist(uriBucket, "custom/config/credentials.json")); + } finally { + uriStorage.close(); + } + } + + @Test + void testDefaultPrefixUsesS3UserConfigDirectory() throws IOException { + ZeppelinConfiguration zConf = ZeppelinConfiguration.load(); + zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_S3_ENDPOINT.getVarName(), + s3Proxy.getUri().toString()); + zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_S3_BUCKET.getVarName(), + bucket); + zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_S3_USER.getVarName(), + "zeppelin-user"); + + S3ConfigStorage defaultPrefixStorage = new S3ConfigStorage(zConf); + try { + defaultPrefixStorage.saveCredentials("{}"); + assertTrue(s3Client.doesObjectExist(bucket, + "zeppelin-user/config/credentials.json")); + } finally { + defaultPrefixStorage.close(); + } + } + + private void restoreSystemProperty(String name, String value) { + if (value == null) { + System.clearProperty(name); + } else { + System.setProperty(name, value); + } + } +} diff --git a/zeppelin-plugins/pom.xml b/zeppelin-plugins/pom.xml index 19cd1bd1afa..a74389a8a42 100644 --- a/zeppelin-plugins/pom.xml +++ b/zeppelin-plugins/pom.xml @@ -37,6 +37,8 @@ + configstorage/s3 + notebookrepo/s3 notebookrepo/github notebookrepo/azure diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java index 3b9ebee0bad..94636b3322f 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java @@ -720,6 +720,14 @@ public String getConfigFSDir(boolean absolute) { } } + public String getConfigS3Dir() { + return getString(ConfVars.ZEPPELIN_CONFIG_S3_DIR); + } + + public String getConfigS3CannedAcl() { + return getString(ConfVars.ZEPPELIN_CONFIG_S3_CANNED_ACL); + } + public List getAllowedOrigins() { if (getString(ConfVars.ZEPPELIN_ALLOWED_ORIGINS).isEmpty()) { @@ -1033,6 +1041,8 @@ public enum ConfVars { ZEPPELIN_NOTEBOOK_AUTO_INTERPRETER_BINDING("zeppelin.notebook.autoInterpreterBinding", true), ZEPPELIN_CONF_DIR("zeppelin.conf.dir", "conf"), ZEPPELIN_CONFIG_FS_DIR("zeppelin.config.fs.dir", ""), + ZEPPELIN_CONFIG_S3_DIR("zeppelin.config.s3.dir", ""), + ZEPPELIN_CONFIG_S3_CANNED_ACL("zeppelin.config.s3.cannedAcl", null), ZEPPELIN_CONFIG_STORAGE_CLASS("zeppelin.config.storage.class", "org.apache.zeppelin.storage.LocalConfigStorage"), ZEPPELIN_DEP_LOCALREPO("zeppelin.dep.localrepo", "local-repo"), diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/ConfigStorage.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/ConfigStorage.java index 6c9692dd8ff..79faa1b02a9 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/ConfigStorage.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/ConfigStorage.java @@ -26,16 +26,20 @@ import org.apache.zeppelin.notebook.NotebookAuthorizationInfoSaving; import org.apache.zeppelin.util.ReflectionUtils; +import java.io.File; import java.io.IOException; - +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; import java.util.List; + /** * Interface for storing zeppelin configuration. * - * 1. interpreter-setting.json - * 2. helium.json - * 3. notebook-authorization.json - * 4. credentials.json + * 1. interpreter.json + * 2. notebook-authorization.json + * 3. credentials.json * */ public abstract class ConfigStorage { @@ -47,10 +51,62 @@ public abstract class ConfigStorage { public static ConfigStorage createConfigStorage(ZeppelinConfiguration zConf) throws IOException { String configStorageClass = zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONFIG_STORAGE_CLASS); - return ReflectionUtils.createClazzInstance(configStorageClass, - new Class[] {ZeppelinConfiguration.class}, new Object[] {zConf}); + try { + return ReflectionUtils.createClazzInstance(configStorageClass, + new Class[] {ZeppelinConfiguration.class}, new Object[] {zConf}); + } catch (IOException e) { + if (!(e.getCause() instanceof ClassNotFoundException)) { + throw e; + } + ConfigStorage configStorage = loadConfigStoragePlugin(zConf, configStorageClass); + if (configStorage != null) { + return configStorage; + } + throw e; + } } + private static ConfigStorage loadConfigStoragePlugin(ZeppelinConfiguration zConf, + String configStorageClass) + throws IOException { + String simpleClassName = configStorageClass.substring(configStorageClass.lastIndexOf(".") + 1); + File pluginFolder = new File(zConf.getPluginsDir() + File.separator + + "ConfigStorage" + File.separator + simpleClassName); + if (!pluginFolder.exists() || pluginFolder.isFile()) { + return null; + } + + File[] pluginFiles = pluginFolder.listFiles(); + if (pluginFiles == null || pluginFiles.length == 0) { + return null; + } + + List urls = new ArrayList<>(); + for (File pluginFile : pluginFiles) { + urls.add(pluginFile.toURI().toURL()); + } + + URLClassLoader pluginClassLoader = + new URLClassLoader(urls.toArray(new URL[0]), ConfigStorage.class.getClassLoader()); + try { + Class clazz = Class.forName(configStorageClass, true, pluginClassLoader); + if (!ConfigStorage.class.isAssignableFrom(clazz)) { + throw new IOException("Class " + configStorageClass + " does not extend " + + ConfigStorage.class.getName()); + } + return (ConfigStorage) clazz.getConstructor(ZeppelinConfiguration.class).newInstance(zConf); + } catch (ClassNotFoundException e) { + throw new IOException("Unable to load config storage plugin class: " + configStorageClass, e); + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException e) { + throw new IOException("Unable to instantiate config storage plugin: " + configStorageClass, e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } + throw new IOException("Unable to instantiate config storage plugin: " + configStorageClass, e); + } + } protected ConfigStorage(ZeppelinConfiguration zConf) { this.zConf = zConf; diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/storage/ConfigStorageTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/storage/ConfigStorageTest.java new file mode 100644 index 00000000000..2a549702525 --- /dev/null +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/storage/ConfigStorageTest.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.zeppelin.storage; + +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class ConfigStorageTest { + + @TempDir + Path tempDir; + + @Test + void createConfigStorageLoadsPluginWhenClassIsNotOnMainClasspath() throws Exception { + String className = "org.apache.zeppelin.storage.plugin.TestPluginConfigStorage"; + Path pluginsDir = tempDir.resolve("plugins"); + Path pluginDir = pluginsDir.resolve("ConfigStorage").resolve("TestPluginConfigStorage"); + Path pluginClassesDir = pluginDir.resolve("classes"); + Files.createDirectories(pluginClassesDir); + + Path sourceFile = tempDir.resolve("source") + .resolve("org").resolve("apache").resolve("zeppelin") + .resolve("storage").resolve("plugin").resolve("TestPluginConfigStorage.java"); + Files.createDirectories(sourceFile.getParent()); + Files.writeString(sourceFile, String.join(System.lineSeparator(), + "package org.apache.zeppelin.storage.plugin;", + "public class TestPluginConfigStorage extends org.apache.zeppelin.storage.ConfigStorage {", + " public TestPluginConfigStorage(org.apache.zeppelin.conf.ZeppelinConfiguration zConf) {", + " super(zConf);", + " }", + " public void save(org.apache.zeppelin.interpreter.InterpreterInfoSaving settingInfos) { }", + " public org.apache.zeppelin.interpreter.InterpreterInfoSaving loadInterpreterSettings() { return null; }", + " public void save(org.apache.zeppelin.notebook.NotebookAuthorizationInfoSaving authorizationInfoSaving) { }", + " public org.apache.zeppelin.notebook.NotebookAuthorizationInfoSaving loadNotebookAuthorization() {", + " return null;", + " }", + " public String loadCredentials() { return null; }", + " public void saveCredentials(String credentials) { }", + "}")); + + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull(compiler); + int compileResult = compiler.run(null, null, null, + "-classpath", System.getProperty("java.class.path"), + "-d", pluginClassesDir.toString(), + sourceFile.toString()); + assertEquals(0, compileResult); + + ZeppelinConfiguration zConf = ZeppelinConfiguration.load(); + zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_PLUGINS_DIR.getVarName(), + pluginsDir.toString()); + zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONFIG_STORAGE_CLASS.getVarName(), + className); + + ConfigStorage configStorage = ConfigStorage.createConfigStorage(zConf); + assertEquals(className, configStorage.getClass().getName()); + } +} From 340efcf24f6e541c78d5a9bc74b9a794669ef7e0 Mon Sep 17 00:00:00 2001 From: Tuan Pham Date: Sat, 2 May 2026 11:07:42 +1000 Subject: [PATCH 2/2] Reuse FileSystemConfigStorage for S3 config Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- conf/zeppelin-env.cmd.template | 3 +- conf/zeppelin-env.sh.template | 3 +- conf/zeppelin-site.xml.template | 23 +- docs/setup/operation/configuration.md | 14 +- docs/setup/storage/configuration_storage.md | 47 ++- zeppelin-plugins/configstorage/s3/pom.xml | 115 -------- .../zeppelin/storage/S3ConfigStorage.java | 274 ------------------ .../zeppelin/storage/S3ConfigStorageTest.java | 175 ----------- zeppelin-plugins/pom.xml | 2 - .../zeppelin/conf/ZeppelinConfiguration.java | 10 - .../zeppelin/storage/ConfigStorage.java | 62 +--- .../storage/FileSystemConfigStorage.java | 5 + .../zeppelin/storage/ConfigStorageTest.java | 82 ------ 13 files changed, 41 insertions(+), 774 deletions(-) delete mode 100644 zeppelin-plugins/configstorage/s3/pom.xml delete mode 100644 zeppelin-plugins/configstorage/s3/src/main/java/org/apache/zeppelin/storage/S3ConfigStorage.java delete mode 100644 zeppelin-plugins/configstorage/s3/src/test/java/org/apache/zeppelin/storage/S3ConfigStorageTest.java delete mode 100644 zeppelin-zengine/src/test/java/org/apache/zeppelin/storage/ConfigStorageTest.java diff --git a/conf/zeppelin-env.cmd.template b/conf/zeppelin-env.cmd.template index a300fcfd0d0..db972faaf71 100644 --- a/conf/zeppelin-env.cmd.template +++ b/conf/zeppelin-env.cmd.template @@ -39,8 +39,7 @@ REM set ZEPPELIN_NOTEBOOK_S3_KMS_KEY_REGION REM AWS KMS key region REM set ZEPPELIN_NOTEBOOK_S3_SSE REM Server-side encryption enabled for notebooks REM set ZEPPELIN_NOTEBOOK_S3_PATH_STYLE_ACCESS REM Path style access for S3 bucket REM set ZEPPELIN_CONFIG_STORAGE_CLASS REM Configuration persistence layer implementation -REM set ZEPPELIN_CONFIG_S3_DIR REM S3 prefix or URI for interpreter.json, notebook-authorization.json, and credentials.json -REM set ZEPPELIN_CONFIG_S3_CANNED_ACL REM Optional canned ACL for S3-backed configuration files +REM set ZEPPELIN_CONFIG_FS_DIR REM Path for interpreter.json, notebook-authorization.json, and credentials.json REM set ZEPPELIN_IDENT_STRING REM A string representing this instance of zeppelin. $USER by default. REM set ZEPPELIN_NICENESS REM The scheduling priority for daemons. Defaults to 0. REM set ZEPPELIN_INTERPRETER_LOCALREPO REM Local repository for interpreter's additional dependency loading diff --git a/conf/zeppelin-env.sh.template b/conf/zeppelin-env.sh.template index f784b0258d6..d3a0ec90d4d 100644 --- a/conf/zeppelin-env.sh.template +++ b/conf/zeppelin-env.sh.template @@ -47,8 +47,7 @@ # export ZEPPELIN_NOTEBOOK_S3_PATH_STYLE_ACCESS # Path style access for S3 bucket # export ZEPPELIN_CONFIG_STORAGE_CLASS # Configuration persistence layer implementation -# export ZEPPELIN_CONFIG_S3_DIR # S3 prefix or URI for interpreter.json, notebook-authorization.json, and credentials.json -# export ZEPPELIN_CONFIG_S3_CANNED_ACL # Optional canned ACL for S3-backed configuration files +# export ZEPPELIN_CONFIG_FS_DIR # Path for interpreter.json, notebook-authorization.json, and credentials.json # export ZEPPELIN_NOTEBOOK_GCS_STORAGE_DIR # GCS "directory" (prefix) under which notebooks are saved. E.g. gs://example-bucket/path/to/dir # export GOOGLE_APPLICATION_CREDENTIALS # Provide a service account key file for GCS and BigQuery API calls (overrides application default credentials) diff --git a/conf/zeppelin-site.xml.template b/conf/zeppelin-site.xml.template index 0d7409decc1..e410a1948e8 100755 --- a/conf/zeppelin-site.xml.template +++ b/conf/zeppelin-site.xml.template @@ -165,27 +165,22 @@ --> - - - - + + + + + diff --git a/docs/setup/operation/configuration.md b/docs/setup/operation/configuration.md index 3aff8539b77..6ef3850c1b0 100644 --- a/docs/setup/operation/configuration.md +++ b/docs/setup/operation/configuration.md @@ -326,19 +326,7 @@ Sources descending by priority:
ZEPPELIN_CONFIG_FS_DIR
zeppelin.config.fs.dir
- Path for FileSystemConfigStorage - - -
ZEPPELIN_CONFIG_S3_DIR
-
zeppelin.config.s3.dir
- - S3 prefix or s3://bucket/prefix URI used by S3ConfigStorage; defaults to {zeppelin.notebook.s3.user}/config - - -
ZEPPELIN_CONFIG_S3_CANNED_ACL
-
zeppelin.config.s3.cannedAcl
- - Optional canned ACL for S3-backed configuration files; keep unset unless required because configuration files can contain secrets + Path for FileSystemConfigStorage, for example hdfs://... or s3a://bucket/prefix. S3A requires hadoop-aws and compatible AWS SDK jars on the Zeppelin server classpath.
ZEPPELIN_NOTEBOOK_AZURE_CONNECTION_STRING
diff --git a/docs/setup/storage/configuration_storage.md b/docs/setup/storage/configuration_storage.md index e289df8f07b..fa62462dc88 100644 --- a/docs/setup/storage/configuration_storage.md +++ b/docs/setup/storage/configuration_storage.md @@ -29,42 +29,39 @@ Zeppelin has lots of configuration which is stored in files: - `notebook-authorization.json` (This file contains all the note authorization info) - `credentials.json` (This file contains the credential info) -## Configuration Storage in S3 +## Configuration Storage in S3-compatible object storage -Set the following properties in `zeppelin-site.xml` to persist Zeppelin configuration -state in S3. This stores `interpreter.json`, `notebook-authorization.json`, and -`credentials.json` under a shared S3 prefix. +Zeppelin can persist configuration state in S3-compatible object storage by reusing +`FileSystemConfigStorage` with Hadoop S3A. This keeps configuration storage on the +same Hadoop-compatible storage abstraction used for HDFS and other filesystems. + +Set the following properties in `zeppelin-site.xml`: ```xml zeppelin.config.storage.class - org.apache.zeppelin.storage.S3ConfigStorage + org.apache.zeppelin.storage.FileSystemConfigStorage configuration persistence layer implementation - zeppelin.config.s3.dir - s3://bucket_name/user/config - S3 prefix or URI for Zeppelin configuration files + zeppelin.config.fs.dir + s3a://bucket_name/user/config + S3A path for Zeppelin configuration files ``` -`zeppelin.config.s3.dir` can be either an S3 URI such as -`s3://bucket_name/user/config` or a key prefix such as `user/config`. When only a -key prefix is provided, `zeppelin.notebook.s3.bucket` is used as the bucket. When -`zeppelin.config.s3.dir` is omitted, Zeppelin stores configuration under -`{zeppelin.notebook.s3.user}/config`. - -`S3ConfigStorage` uses the same S3 client settings as `S3NotebookRepo`, including -`zeppelin.notebook.s3.endpoint`, `zeppelin.notebook.s3.pathStyleAccess`, -`zeppelin.notebook.s3.sse`, `zeppelin.notebook.s3.kmsKeyID`, -`zeppelin.notebook.s3.kmsKeyRegion`, -`zeppelin.notebook.s3.encryptionMaterialsProvider`, -and `zeppelin.notebook.s3.signerOverride`. - -`S3ConfigStorage` does not inherit `zeppelin.notebook.s3.cannedAcl`, because -configuration files can contain credentials and other sensitive values. If your -bucket ownership policy requires a canned ACL, set `zeppelin.config.s3.cannedAcl` -explicitly and avoid public or broadly readable ACLs. +Also ensure the Zeppelin server classpath contains the Hadoop S3A runtime: +`hadoop-aws` built for the same Hadoop version as Zeppelin, plus its compatible +AWS SDK dependencies. `HADOOP_CONF_DIR` is still required so Zeppelin can find +the Hadoop configuration files that define S3A credentials, endpoint, encryption, +and bucket policy settings. For example, configure properties such as +`fs.s3a.aws.credentials.provider`, `fs.s3a.endpoint`, +`fs.s3a.server-side-encryption-algorithm`, and +`fs.s3a.server-side-encryption.key` in your Hadoop configuration as needed. + +S3A does not enforce POSIX permissions on objects. When credentials persistence is +enabled, protect `credentials.json` with S3 bucket policy, object ownership, and +encryption settings instead of relying on owner-only file permissions. ## Configuration Storage in hadoop compatible file system diff --git a/zeppelin-plugins/configstorage/s3/pom.xml b/zeppelin-plugins/configstorage/s3/pom.xml deleted file mode 100644 index 1336d18329d..00000000000 --- a/zeppelin-plugins/configstorage/s3/pom.xml +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - 4.0.0 - - - zengine-plugins-parent - org.apache.zeppelin - 0.13.0-SNAPSHOT - ../../../zeppelin-plugins - - - configstorage-s3 - jar - Zeppelin: Plugin S3ConfigStorage - ConfigStorage implementation based on S3 - - - 1.12.261 - ConfigStorage/S3ConfigStorage - - - - - com.amazonaws - aws-java-sdk-s3 - ${aws.sdk.version} - - - commons-logging - commons-logging - - - - - - com.amazonaws - aws-java-sdk-sts - ${aws.sdk.version} - - - commons-logging - commons-logging - - - - - - org.gaul - s3proxy - 2.0.0 - test - - - org.eclipse.jetty - jetty-http - - - org.eclipse.jetty - jetty-io - - - com.google.guava - guava - - - org.eclipse.jetty - jetty-util - - - com.google.errorprone - error_prone_annotations - - - com.google.code.findbugs - jsr305 - - - javax.annotation - javax.annotation-api - - - jakarta.xml.bind - jakarta.xml.bind-api - - - - - - - - - maven-dependency-plugin - - - - diff --git a/zeppelin-plugins/configstorage/s3/src/main/java/org/apache/zeppelin/storage/S3ConfigStorage.java b/zeppelin-plugins/configstorage/s3/src/main/java/org/apache/zeppelin/storage/S3ConfigStorage.java deleted file mode 100644 index aa5dfe9ae84..00000000000 --- a/zeppelin-plugins/configstorage/s3/src/main/java/org/apache/zeppelin/storage/S3ConfigStorage.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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 org.apache.zeppelin.storage; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.ClientConfiguration; -import com.amazonaws.ClientConfigurationFactory; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.regions.Region; -import com.amazonaws.regions.Regions; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.AmazonS3EncryptionClient; -import com.amazonaws.services.s3.S3ClientOptions; -import com.amazonaws.services.s3.model.CannedAccessControlList; -import com.amazonaws.services.s3.model.CryptoConfiguration; -import com.amazonaws.services.s3.model.EncryptionMaterialsProvider; -import com.amazonaws.services.s3.model.GetObjectRequest; -import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.amazonaws.services.s3.model.S3Object; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; -import org.apache.zeppelin.interpreter.InterpreterInfoSaving; -import org.apache.zeppelin.notebook.NotebookAuthorizationInfoSaving; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.Charset; - -/** - * Stores Zeppelin mutable configuration JSON files in S3. - */ -public class S3ConfigStorage extends ConfigStorage { - - private static final Logger LOGGER = LoggerFactory.getLogger(S3ConfigStorage.class); - - private static final String S3 = "s3"; - private static final String S3A = "s3a"; - private static final String INTERPRETER_SETTING_FILE = "interpreter.json"; - private static final String NOTEBOOK_AUTHORIZATION_FILE = "notebook-authorization.json"; - private static final String CREDENTIALS_FILE = "credentials.json"; - - private final AmazonS3 s3client; - private final String bucketName; - private final String configPrefix; - private final boolean useServerSideEncryption; - private final CannedAccessControlList objectCannedAcl; - - public S3ConfigStorage(ZeppelinConfiguration zConf) throws IOException { - super(zConf); - S3Location configLocation = resolveConfigLocation(zConf); - this.bucketName = configLocation.bucketName; - this.configPrefix = configLocation.keyPrefix; - this.useServerSideEncryption = zConf.isS3ServerSideEncryption(); - if (StringUtils.isNotBlank(zConf.getConfigS3CannedAcl())) { - this.objectCannedAcl = CannedAccessControlList.valueOf(zConf.getConfigS3CannedAcl()); - } else { - this.objectCannedAcl = null; - } - this.s3client = createS3Client(zConf); - LOGGER.info("Using S3 prefix s3://{}/{} to store Zeppelin Config", bucketName, configPrefix); - } - - @Override - public void save(InterpreterInfoSaving settingInfos) throws IOException { - saveJson("Interpreter Settings", objectKey(INTERPRETER_SETTING_FILE), settingInfos.toJson()); - } - - @Override - public InterpreterInfoSaving loadInterpreterSettings() throws IOException { - String json = loadJson("Interpreter Settings", objectKey(INTERPRETER_SETTING_FILE)); - return json == null ? null : buildInterpreterInfoSaving(json); - } - - @Override - public void save(NotebookAuthorizationInfoSaving authorizationInfoSaving) throws IOException { - saveJson("notebook authorization", objectKey(NOTEBOOK_AUTHORIZATION_FILE), - authorizationInfoSaving.toJson()); - } - - @Override - public NotebookAuthorizationInfoSaving loadNotebookAuthorization() throws IOException { - String json = loadJson("notebook authorization", objectKey(NOTEBOOK_AUTHORIZATION_FILE)); - return json == null ? null : NotebookAuthorizationInfoSaving.fromJson(json); - } - - @Override - public String loadCredentials() throws IOException { - return loadJson("Credentials", objectKey(CREDENTIALS_FILE)); - } - - @Override - public void saveCredentials(String credentials) throws IOException { - saveJson("Credentials", objectKey(CREDENTIALS_FILE), credentials); - } - - public void close() { - if (s3client != null) { - s3client.shutdown(); - } - } - - private String loadJson(String storageName, String key) throws IOException { - try { - if (!s3client.doesObjectExist(bucketName, key)) { - LOGGER.warn("{} file s3://{}/{} is not existed", storageName, bucketName, key); - return null; - } - LOGGER.info("Load {} from S3: s3://{}/{}", storageName, bucketName, key); - S3Object s3Object = s3client.getObject(new GetObjectRequest(bucketName, key)); - try (InputStream input = s3Object.getObjectContent()) { - return IOUtils.toString(input, zConf.getString(ConfVars.ZEPPELIN_ENCODING)); - } - } catch (AmazonClientException e) { - throw new IOException("Fail to load " + storageName + " from S3", e); - } - } - - private void saveJson(String storageName, String key, String json) throws IOException { - try { - LOGGER.info("Save {} to S3: s3://{}/{}", storageName, bucketName, key); - byte[] bytes = json.getBytes(Charset.forName(zConf.getString(ConfVars.ZEPPELIN_ENCODING))); - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(bytes.length); - metadata.setContentType("application/json"); - if (useServerSideEncryption) { - metadata.setSSEAlgorithm(ObjectMetadata.AES_256_SERVER_SIDE_ENCRYPTION); - } - PutObjectRequest putRequest = new PutObjectRequest(bucketName, key, - new ByteArrayInputStream(bytes), metadata); - if (objectCannedAcl != null) { - putRequest.withCannedAcl(objectCannedAcl); - } - s3client.putObject(putRequest); - } catch (AmazonClientException e) { - throw new IOException("Fail to save " + storageName + " to S3", e); - } - } - - private String objectKey(String fileName) { - return StringUtils.isBlank(configPrefix) ? fileName : configPrefix + "/" + fileName; - } - - private static S3Location resolveConfigLocation(ZeppelinConfiguration zConf) throws IOException { - String configuredDir = firstNonBlank( - zConf.getConfigS3Dir(), - zConf.getS3User() + "/config"); - - try { - URI uri = new URI(configuredDir); - String scheme = uri.getScheme(); - if (StringUtils.isBlank(scheme)) { - return new S3Location(zConf.getS3BucketName(), normalizePrefix(configuredDir)); - } - if (!S3.equalsIgnoreCase(scheme) && !S3A.equalsIgnoreCase(scheme)) { - throw new IOException("Unsupported S3 config storage URI scheme: " + scheme); - } - if (StringUtils.isBlank(uri.getHost())) { - throw new IOException("S3 config storage URI must include a bucket: " + configuredDir); - } - return new S3Location(uri.getHost(), normalizePrefix(uri.getPath())); - } catch (URISyntaxException e) { - throw new IOException("Invalid S3 config storage location: " + configuredDir, e); - } - } - - private static String firstNonBlank(String first, String second) { - if (StringUtils.isNotBlank(first)) { - return first; - } - return second; - } - - private static String normalizePrefix(String keyPrefix) { - if (StringUtils.isBlank(keyPrefix)) { - return ""; - } - String normalized = keyPrefix.replace('\\', '/'); - while (normalized.startsWith("/")) { - normalized = normalized.substring(1); - } - while (normalized.endsWith("/")) { - normalized = normalized.substring(0, normalized.length() - 1); - } - return normalized; - } - - private static AmazonS3 createS3Client(ZeppelinConfiguration zConf) throws IOException { - AWSCredentialsProvider credentialsProvider = new DefaultAWSCredentialsProviderChain(); - CryptoConfiguration cryptoConf = new CryptoConfiguration(); - String keyRegion = zConf.getS3KMSKeyRegion(); - - if (StringUtils.isNotBlank(keyRegion)) { - cryptoConf.setAwsKmsRegion(Region.getRegion(Regions.fromName(keyRegion))); - } - - ClientConfiguration clientConf = createClientConfiguration(zConf); - AmazonS3 client; - String kmsKeyID = zConf.getS3KMSKeyID(); - if (StringUtils.isNotBlank(kmsKeyID)) { - KMSEncryptionMaterialsProvider emp = new KMSEncryptionMaterialsProvider(kmsKeyID); - client = new AmazonS3EncryptionClient(credentialsProvider, emp, clientConf, cryptoConf); - } else if (StringUtils.isNotBlank(zConf.getS3EncryptionMaterialsProviderClass())) { - EncryptionMaterialsProvider emp = createCustomProvider(zConf); - client = new AmazonS3EncryptionClient(credentialsProvider, emp, clientConf, cryptoConf); - } else { - client = new AmazonS3Client(credentialsProvider, clientConf); - } - client.setS3ClientOptions(S3ClientOptions.builder() - .setPathStyleAccess(zConf.isS3PathStyleAccess()).build()); - client.setEndpoint(zConf.getS3Endpoint()); - return client; - } - - private static ClientConfiguration createClientConfiguration(ZeppelinConfiguration zConf) { - ClientConfiguration config = new ClientConfigurationFactory().getConfig(); - String s3SignerOverride = zConf.getS3SignerOverride(); - if (StringUtils.isNotBlank(s3SignerOverride)) { - config.setSignerOverride(s3SignerOverride); - } - return config; - } - - private static EncryptionMaterialsProvider createCustomProvider(ZeppelinConfiguration zConf) - throws IOException { - String empClassname = zConf.getS3EncryptionMaterialsProviderClass(); - try { - Object empInstance = Class.forName(empClassname).getDeclaredConstructor().newInstance(); - if (empInstance instanceof EncryptionMaterialsProvider) { - return (EncryptionMaterialsProvider) empInstance; - } - throw new IOException("Class " + empClassname + " does not implement " - + EncryptionMaterialsProvider.class.getName()); - } catch (ReflectiveOperationException e) { - throw new IOException("Unable to instantiate encryption materials provider class " - + empClassname + ": " + e, e); - } - } - - private static class S3Location { - private final String bucketName; - private final String keyPrefix; - - private S3Location(String bucketName, String keyPrefix) { - this.bucketName = bucketName; - this.keyPrefix = keyPrefix; - } - } -} diff --git a/zeppelin-plugins/configstorage/s3/src/test/java/org/apache/zeppelin/storage/S3ConfigStorageTest.java b/zeppelin-plugins/configstorage/s3/src/test/java/org/apache/zeppelin/storage/S3ConfigStorageTest.java deleted file mode 100644 index d6b5bfd265e..00000000000 --- a/zeppelin-plugins/configstorage/s3/src/test/java/org/apache/zeppelin/storage/S3ConfigStorageTest.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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 org.apache.zeppelin.storage; - -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.client.builder.AwsClientBuilder; -import com.amazonaws.regions.Regions; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.interpreter.InterpreterInfoSaving; -import org.apache.zeppelin.notebook.NotebookAuthorizationInfoSaving; -import org.gaul.s3proxy.junit.S3ProxyExtension; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.io.IOException; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class S3ConfigStorageTest { - - private static final String CONFIG_PREFIX = "config-root"; - - @RegisterExtension - static S3ProxyExtension s3Proxy = S3ProxyExtension.builder() - .withCredentials("access", "secret") - .build(); - - private AmazonS3 s3Client; - private S3ConfigStorage configStorage; - private String bucket; - private String previousAccessKeyId; - private String previousSecretKey; - - @BeforeEach - void setUp() throws IOException { - previousAccessKeyId = System.getProperty("aws.accessKeyId"); - previousSecretKey = System.getProperty("aws.secretKey"); - System.setProperty("aws.accessKeyId", s3Proxy.getAccessKey()); - System.setProperty("aws.secretKey", s3Proxy.getSecretKey()); - - s3Client = AmazonS3ClientBuilder - .standard() - .withCredentials(new AWSStaticCredentialsProvider( - new BasicAWSCredentials(s3Proxy.getAccessKey(), s3Proxy.getSecretKey()))) - .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration( - s3Proxy.getUri().toString(), Regions.US_EAST_1.getName())) - .build(); - bucket = "test-bucket-" + UUID.randomUUID(); - s3Client.createBucket(bucket); - - ZeppelinConfiguration zConf = ZeppelinConfiguration.load(); - zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_S3_ENDPOINT.getVarName(), - s3Proxy.getUri().toString()); - zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_S3_BUCKET.getVarName(), - bucket); - zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONFIG_S3_DIR.getVarName(), - CONFIG_PREFIX); - configStorage = new S3ConfigStorage(zConf); - } - - @AfterEach - void tearDown() { - if (configStorage != null) { - configStorage.close(); - } - if (s3Client != null) { - s3Client.shutdown(); - } - restoreSystemProperty("aws.accessKeyId", previousAccessKeyId); - restoreSystemProperty("aws.secretKey", previousSecretKey); - } - - @Test - void testSaveAndLoadConfigFiles() throws IOException { - assertNull(configStorage.loadInterpreterSettings()); - assertNull(configStorage.loadNotebookAuthorization()); - assertNull(configStorage.loadCredentials()); - - InterpreterInfoSaving interpreterInfoSaving = new InterpreterInfoSaving(); - configStorage.save(interpreterInfoSaving); - - NotebookAuthorizationInfoSaving authorizationInfoSaving = - new NotebookAuthorizationInfoSaving(new ConcurrentHashMap<>()); - configStorage.save(authorizationInfoSaving); - - String credentials = "{\n \"credentialsMap\": {}\n}"; - configStorage.saveCredentials(credentials); - - InterpreterInfoSaving loadedInterpreterInfo = configStorage.loadInterpreterSettings(); - assertNotNull(loadedInterpreterInfo); - assertTrue(loadedInterpreterInfo.interpreterSettings.isEmpty()); - - NotebookAuthorizationInfoSaving loadedAuthorization = configStorage.loadNotebookAuthorization(); - assertNotNull(loadedAuthorization); - assertTrue(loadedAuthorization.getAuthInfo().isEmpty()); - - assertEquals(credentials, configStorage.loadCredentials()); - assertTrue(s3Client.doesObjectExist(bucket, CONFIG_PREFIX + "/interpreter.json")); - assertTrue(s3Client.doesObjectExist(bucket, CONFIG_PREFIX + "/notebook-authorization.json")); - assertTrue(s3Client.doesObjectExist(bucket, CONFIG_PREFIX + "/credentials.json")); - } - - @Test - void testS3UriCanOverrideBucketAndPrefix() throws IOException { - String uriBucket = "uri-bucket-" + UUID.randomUUID(); - s3Client.createBucket(uriBucket); - - ZeppelinConfiguration zConf = ZeppelinConfiguration.load(); - zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_S3_ENDPOINT.getVarName(), - s3Proxy.getUri().toString()); - zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONFIG_S3_DIR.getVarName(), - "s3://" + uriBucket + "/custom/config/"); - - S3ConfigStorage uriStorage = new S3ConfigStorage(zConf); - try { - uriStorage.saveCredentials("{}"); - assertTrue(s3Client.doesObjectExist(uriBucket, "custom/config/credentials.json")); - } finally { - uriStorage.close(); - } - } - - @Test - void testDefaultPrefixUsesS3UserConfigDirectory() throws IOException { - ZeppelinConfiguration zConf = ZeppelinConfiguration.load(); - zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_S3_ENDPOINT.getVarName(), - s3Proxy.getUri().toString()); - zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_S3_BUCKET.getVarName(), - bucket); - zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_S3_USER.getVarName(), - "zeppelin-user"); - - S3ConfigStorage defaultPrefixStorage = new S3ConfigStorage(zConf); - try { - defaultPrefixStorage.saveCredentials("{}"); - assertTrue(s3Client.doesObjectExist(bucket, - "zeppelin-user/config/credentials.json")); - } finally { - defaultPrefixStorage.close(); - } - } - - private void restoreSystemProperty(String name, String value) { - if (value == null) { - System.clearProperty(name); - } else { - System.setProperty(name, value); - } - } -} diff --git a/zeppelin-plugins/pom.xml b/zeppelin-plugins/pom.xml index a74389a8a42..19cd1bd1afa 100644 --- a/zeppelin-plugins/pom.xml +++ b/zeppelin-plugins/pom.xml @@ -37,8 +37,6 @@ - configstorage/s3 - notebookrepo/s3 notebookrepo/github notebookrepo/azure diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java index 94636b3322f..3b9ebee0bad 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java @@ -720,14 +720,6 @@ public String getConfigFSDir(boolean absolute) { } } - public String getConfigS3Dir() { - return getString(ConfVars.ZEPPELIN_CONFIG_S3_DIR); - } - - public String getConfigS3CannedAcl() { - return getString(ConfVars.ZEPPELIN_CONFIG_S3_CANNED_ACL); - } - public List getAllowedOrigins() { if (getString(ConfVars.ZEPPELIN_ALLOWED_ORIGINS).isEmpty()) { @@ -1041,8 +1033,6 @@ public enum ConfVars { ZEPPELIN_NOTEBOOK_AUTO_INTERPRETER_BINDING("zeppelin.notebook.autoInterpreterBinding", true), ZEPPELIN_CONF_DIR("zeppelin.conf.dir", "conf"), ZEPPELIN_CONFIG_FS_DIR("zeppelin.config.fs.dir", ""), - ZEPPELIN_CONFIG_S3_DIR("zeppelin.config.s3.dir", ""), - ZEPPELIN_CONFIG_S3_CANNED_ACL("zeppelin.config.s3.cannedAcl", null), ZEPPELIN_CONFIG_STORAGE_CLASS("zeppelin.config.storage.class", "org.apache.zeppelin.storage.LocalConfigStorage"), ZEPPELIN_DEP_LOCALREPO("zeppelin.dep.localrepo", "local-repo"), diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/ConfigStorage.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/ConfigStorage.java index 79faa1b02a9..421b6f211f4 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/ConfigStorage.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/ConfigStorage.java @@ -26,12 +26,7 @@ import org.apache.zeppelin.notebook.NotebookAuthorizationInfoSaving; import org.apache.zeppelin.util.ReflectionUtils; -import java.io.File; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.ArrayList; import java.util.List; /** @@ -51,61 +46,8 @@ public abstract class ConfigStorage { public static ConfigStorage createConfigStorage(ZeppelinConfiguration zConf) throws IOException { String configStorageClass = zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONFIG_STORAGE_CLASS); - try { - return ReflectionUtils.createClazzInstance(configStorageClass, - new Class[] {ZeppelinConfiguration.class}, new Object[] {zConf}); - } catch (IOException e) { - if (!(e.getCause() instanceof ClassNotFoundException)) { - throw e; - } - ConfigStorage configStorage = loadConfigStoragePlugin(zConf, configStorageClass); - if (configStorage != null) { - return configStorage; - } - throw e; - } - } - - private static ConfigStorage loadConfigStoragePlugin(ZeppelinConfiguration zConf, - String configStorageClass) - throws IOException { - String simpleClassName = configStorageClass.substring(configStorageClass.lastIndexOf(".") + 1); - File pluginFolder = new File(zConf.getPluginsDir() + File.separator - + "ConfigStorage" + File.separator + simpleClassName); - if (!pluginFolder.exists() || pluginFolder.isFile()) { - return null; - } - - File[] pluginFiles = pluginFolder.listFiles(); - if (pluginFiles == null || pluginFiles.length == 0) { - return null; - } - - List urls = new ArrayList<>(); - for (File pluginFile : pluginFiles) { - urls.add(pluginFile.toURI().toURL()); - } - - URLClassLoader pluginClassLoader = - new URLClassLoader(urls.toArray(new URL[0]), ConfigStorage.class.getClassLoader()); - try { - Class clazz = Class.forName(configStorageClass, true, pluginClassLoader); - if (!ConfigStorage.class.isAssignableFrom(clazz)) { - throw new IOException("Class " + configStorageClass + " does not extend " - + ConfigStorage.class.getName()); - } - return (ConfigStorage) clazz.getConstructor(ZeppelinConfiguration.class).newInstance(zConf); - } catch (ClassNotFoundException e) { - throw new IOException("Unable to load config storage plugin class: " + configStorageClass, e); - } catch (NoSuchMethodException | InstantiationException | IllegalAccessException e) { - throw new IOException("Unable to instantiate config storage plugin: " + configStorageClass, e); - } catch (InvocationTargetException e) { - Throwable cause = e.getCause(); - if (cause instanceof IOException) { - throw (IOException) cause; - } - throw new IOException("Unable to instantiate config storage plugin: " + configStorageClass, e); - } + return ReflectionUtils.createClazzInstance(configStorageClass, + new Class[] {ZeppelinConfiguration.class}, new Object[] {zConf}); } protected ConfigStorage(ZeppelinConfiguration zConf) { diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/FileSystemConfigStorage.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/FileSystemConfigStorage.java index ac7d108f27b..f4937961c7e 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/FileSystemConfigStorage.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/FileSystemConfigStorage.java @@ -41,6 +41,7 @@ public class FileSystemConfigStorage extends ConfigStorage { private static final Logger LOGGER = LoggerFactory.getLogger(FileSystemConfigStorage.class); + private static final String S3A = "s3a"; private FileSystemStorage fs; private Path interpreterSettingPath; @@ -55,6 +56,10 @@ public FileSystemConfigStorage(ZeppelinConfiguration zConf) throws IOException { Path configPath = this.fs.makeQualified(new Path(configDir)); this.fs.tryMkDir(configPath); LOGGER.info("Using folder {} to store Zeppelin Config", configPath); + if (zConf.credentialsPersist() && S3A.equalsIgnoreCase(configPath.toUri().getScheme())) { + LOGGER.warn("S3A does not enforce POSIX file permissions. Protect {} with S3 bucket policy, " + + "object ownership, and encryption settings.", zConf.getCredentialsPath(false)); + } this.interpreterSettingPath = fs.makeQualified(new Path(zConf.getInterpreterSettingPath(false))); this.authorizationPath = fs.makeQualified(new Path(zConf.getNotebookAuthorizationPath(false))); this.credentialPath = fs.makeQualified(new Path(zConf.getCredentialsPath(false))); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/storage/ConfigStorageTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/storage/ConfigStorageTest.java deleted file mode 100644 index 2a549702525..00000000000 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/storage/ConfigStorageTest.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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 org.apache.zeppelin.storage; - -import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import javax.tools.JavaCompiler; -import javax.tools.ToolProvider; -import java.nio.file.Files; -import java.nio.file.Path; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -class ConfigStorageTest { - - @TempDir - Path tempDir; - - @Test - void createConfigStorageLoadsPluginWhenClassIsNotOnMainClasspath() throws Exception { - String className = "org.apache.zeppelin.storage.plugin.TestPluginConfigStorage"; - Path pluginsDir = tempDir.resolve("plugins"); - Path pluginDir = pluginsDir.resolve("ConfigStorage").resolve("TestPluginConfigStorage"); - Path pluginClassesDir = pluginDir.resolve("classes"); - Files.createDirectories(pluginClassesDir); - - Path sourceFile = tempDir.resolve("source") - .resolve("org").resolve("apache").resolve("zeppelin") - .resolve("storage").resolve("plugin").resolve("TestPluginConfigStorage.java"); - Files.createDirectories(sourceFile.getParent()); - Files.writeString(sourceFile, String.join(System.lineSeparator(), - "package org.apache.zeppelin.storage.plugin;", - "public class TestPluginConfigStorage extends org.apache.zeppelin.storage.ConfigStorage {", - " public TestPluginConfigStorage(org.apache.zeppelin.conf.ZeppelinConfiguration zConf) {", - " super(zConf);", - " }", - " public void save(org.apache.zeppelin.interpreter.InterpreterInfoSaving settingInfos) { }", - " public org.apache.zeppelin.interpreter.InterpreterInfoSaving loadInterpreterSettings() { return null; }", - " public void save(org.apache.zeppelin.notebook.NotebookAuthorizationInfoSaving authorizationInfoSaving) { }", - " public org.apache.zeppelin.notebook.NotebookAuthorizationInfoSaving loadNotebookAuthorization() {", - " return null;", - " }", - " public String loadCredentials() { return null; }", - " public void saveCredentials(String credentials) { }", - "}")); - - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - assertNotNull(compiler); - int compileResult = compiler.run(null, null, null, - "-classpath", System.getProperty("java.class.path"), - "-d", pluginClassesDir.toString(), - sourceFile.toString()); - assertEquals(0, compileResult); - - ZeppelinConfiguration zConf = ZeppelinConfiguration.load(); - zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_PLUGINS_DIR.getVarName(), - pluginsDir.toString()); - zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_CONFIG_STORAGE_CLASS.getVarName(), - className); - - ConfigStorage configStorage = ConfigStorage.createConfigStorage(zConf); - assertEquals(className, configStorage.getClass().getName()); - } -}