This is an automated email from the ASF dual-hosted git repository.
roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new d4316e127c [#12297] feat(secret): Support
setSecretBinding/setSecretReference on catalog/schema alter (#12674)
d4316e127c is described below
commit d4316e127ceaf2e2bf82f504f8e06b22fa76d97f
Author: MaSai <[email protected]>
AuthorDate: Fri Aug 28 09:45:27 2026 +0800
[#12297] feat(secret): Support setSecretBinding/setSecretReference on
catalog/schema alter (#12674)
### What changes were proposed in this pull request?
Follow-up to #12646 (fileset alter). Adds catalog / schema alter support
for `setSecretBinding` / `setSecretReference`:
- API / DTO / OpenAPI / Java & Python clients: new `CatalogChange` and
`SchemaChange` types
- `CatalogManager` / `SchemaOperationDispatcher`: local
`prepare*SecretChanges` loops calling existing `SecretManager` alter
helpers, rewriting secret ops to `setProperty` / `removeProperty` (same
pattern as `FilesetOperationDispatcher`)
- `OperationDispatcher`: validate alter upserts for schema secret
changes
- Unit tests for alter removeProperty write-through cleanup and JSON
serde
### Why are the changes needed?
Create-time `secretBindings` / `secretReferences` for catalog and schema
are already on `main` (#12420). Catalog and schema alter still need the
typed secret update contract from the entity-secrets design (ยง5.9.4).
Fix: #12297
### Does this PR introduce _any_ user-facing change?
Yes. Catalog and schema alter APIs / REST update requests gain
`setSecretBinding` and `setSecretReference`.
### How was this patch tested?
```bash
./gradlew spotlessApply
./gradlew :core:test \
--tests 'org.apache.gravitino.secret.TestSecretManagerAlter' \
--tests
'org.apache.gravitino.catalog.TestCatalogManager.testAlterRemovePropertyDeletesWriteThroughSecret'
\
--tests
'org.apache.gravitino.catalog.TestCatalogManager.testSecretRollback' \
--tests
'org.apache.gravitino.catalog.TestSchemaOperationDispatcher.testAlterRemovePropertyDeletesWriteThroughSecret'
\
--tests
'org.apache.gravitino.catalog.TestSchemaOperationDispatcher.testCreateAndAlterSchemaRejectMaskedPlaceholder'
\
:common:test --tests 'org.apache.gravitino.json.TestRequestJsonSerDe' \
:docs:build \
-PskipITs
```
---------
Co-authored-by: Cursor <[email protected]>
---
.../java/org/apache/gravitino/CatalogChange.java | 118 ++++++++++++
.../java/org/apache/gravitino/SchemaChange.java | 118 ++++++++++++
.../catalog/fileset/FilesetCatalogOperations.java | 24 ++-
.../org/apache/gravitino/client/DTOConverters.java | 29 +++
.../client-python/gravitino/api/catalog_change.py | 86 +++++++++
.../client-python/gravitino/api/schema_change.py | 65 +++++++
.../gravitino/client/base_schema_catalog.py | 10 ++
.../gravitino/client/dto_converters.py | 10 ++
.../dto/requests/catalog_update_request.py | 64 ++++++-
.../dto/requests/schema_update_request.py | 63 +++++++
.../dto/requests/CatalogUpdateRequest.java | 125 ++++++++++++-
.../dto/requests/SchemaUpdateRequest.java | 125 ++++++++++++-
.../gravitino/json/TestRequestJsonSerDe.java | 41 +++--
.../apache/gravitino/catalog/CatalogManager.java | 80 ++++++---
.../catalog/FilesetOperationDispatcher.java | 83 +++------
.../gravitino/catalog/ManagedSchemaOperations.java | 34 +---
.../gravitino/catalog/OperationDispatcher.java | 6 +
.../gravitino/catalog/SchemaEntityChanges.java | 70 ++++++++
.../catalog/SchemaOperationDispatcher.java | 139 ++++++++++++++-
.../gravitino/secret/SecretAlterChanges.java | 198 +++++++++++++++++++++
.../gravitino/secret/SecretMaterialsHolder.java | 42 +++++
.../gravitino/catalog/TestCatalogManager.java | 10 ++
.../catalog/TestFilesetOperationDispatcher.java | 53 +-----
.../catalog/TestSchemaOperationDispatcher.java | 4 +
docs/open-api/catalogs.yaml | 65 +++++++
docs/open-api/schemas.yaml | 65 +++++++
26 files changed, 1545 insertions(+), 182 deletions(-)
diff --git a/api/src/main/java/org/apache/gravitino/CatalogChange.java
b/api/src/main/java/org/apache/gravitino/CatalogChange.java
index d1b82cf29d..d1af43eee1 100644
--- a/api/src/main/java/org/apache/gravitino/CatalogChange.java
+++ b/api/src/main/java/org/apache/gravitino/CatalogChange.java
@@ -20,6 +20,8 @@ package org.apache.gravitino;
import java.util.Objects;
import org.apache.gravitino.annotation.Evolving;
+import org.apache.gravitino.secret.SecretBinding;
+import org.apache.gravitino.secret.SecretReference;
/**
* A catalog change is a change to a catalog. It can be used to rename a
catalog, update the comment
@@ -69,6 +71,28 @@ public interface CatalogChange {
return new RemoveProperty(property);
}
+ /**
+ * Creates a new catalog change to bind a write-through secret for a
property.
+ *
+ * @param property The property name to bind.
+ * @param binding The write-through binding ({@code provider} + {@code
plaintext}).
+ * @return The catalog change.
+ */
+ static CatalogChange setSecretBinding(String property, SecretBinding
binding) {
+ return new SetSecretBinding(property, binding);
+ }
+
+ /**
+ * Creates a new catalog change to bind an external secret reference for a
property.
+ *
+ * @param property The property name to bind.
+ * @param reference The external secret locator ({@code provider} + {@code
attributes}).
+ * @return The catalog change.
+ */
+ static CatalogChange setSecretReference(String property, SecretReference
reference) {
+ return new SetSecretReference(property, reference);
+ }
+
/** A catalog change to rename the catalog. */
final class RenameCatalog implements CatalogChange {
private final String newName;
@@ -300,4 +324,98 @@ public interface CatalogChange {
return "REMOVEPROPERTY " + property;
}
}
+
+ /** A catalog change to bind a write-through secret for a property. */
+ final class SetSecretBinding implements CatalogChange {
+ private final String property;
+ private final SecretBinding binding;
+
+ private SetSecretBinding(String property, SecretBinding binding) {
+ this.property = property;
+ this.binding = binding;
+ }
+
+ /**
+ * Retrieves the property name being bound.
+ *
+ * @return The property name.
+ */
+ public String getProperty() {
+ return property;
+ }
+
+ /**
+ * Retrieves the write-through binding.
+ *
+ * @return The secret binding.
+ */
+ public SecretBinding getBinding() {
+ return binding;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SetSecretBinding that = (SetSecretBinding) o;
+ return Objects.equals(property, that.property) &&
Objects.equals(binding, that.binding);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(property, binding);
+ }
+
+ @Override
+ public String toString() {
+ return "SETSECRETBINDING " + property + " " + binding;
+ }
+ }
+
+ /** A catalog change to bind an external secret reference for a property. */
+ final class SetSecretReference implements CatalogChange {
+ private final String property;
+ private final SecretReference reference;
+
+ private SetSecretReference(String property, SecretReference reference) {
+ this.property = property;
+ this.reference = reference;
+ }
+
+ /**
+ * Retrieves the property name being bound.
+ *
+ * @return The property name.
+ */
+ public String getProperty() {
+ return property;
+ }
+
+ /**
+ * Retrieves the external secret reference.
+ *
+ * @return The secret reference.
+ */
+ public SecretReference getReference() {
+ return reference;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SetSecretReference that = (SetSecretReference) o;
+ return Objects.equals(property, that.property) &&
Objects.equals(reference, that.reference);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(property, reference);
+ }
+
+ @Override
+ public String toString() {
+ return "SETSECRETREFERENCE " + property + " " + reference;
+ }
+ }
}
diff --git a/api/src/main/java/org/apache/gravitino/SchemaChange.java
b/api/src/main/java/org/apache/gravitino/SchemaChange.java
index 45fc7c04ca..c8e2291160 100644
--- a/api/src/main/java/org/apache/gravitino/SchemaChange.java
+++ b/api/src/main/java/org/apache/gravitino/SchemaChange.java
@@ -22,6 +22,8 @@ package org.apache.gravitino;
import java.util.Objects;
import org.apache.gravitino.annotation.Evolving;
+import org.apache.gravitino.secret.SecretBinding;
+import org.apache.gravitino.secret.SecretReference;
/** NamespaceChange class to set the property and value pairs for the
namespace. */
@Evolving
@@ -48,6 +50,28 @@ public interface SchemaChange {
return new RemoveProperty(property);
}
+ /**
+ * Creates a schema change to bind a write-through secret for a property.
+ *
+ * @param property The property name to bind.
+ * @param binding The write-through binding ({@code provider} + {@code
plaintext}).
+ * @return The SchemaChange object.
+ */
+ static SchemaChange setSecretBinding(String property, SecretBinding binding)
{
+ return new SetSecretBinding(property, binding);
+ }
+
+ /**
+ * Creates a schema change to bind an external secret reference for a
property.
+ *
+ * @param property The property name to bind.
+ * @param reference The external secret locator ({@code provider} + {@code
attributes}).
+ * @return The SchemaChange object.
+ */
+ static SchemaChange setSecretReference(String property, SecretReference
reference) {
+ return new SetSecretReference(property, reference);
+ }
+
/** SchemaChange class to set the property and value pairs for the schema. */
final class SetProperty implements SchemaChange {
private final String property;
@@ -168,4 +192,98 @@ public interface SchemaChange {
return "REMOVEPROPERTY " + property;
}
}
+
+ /** SchemaChange to bind a write-through secret for a property. */
+ final class SetSecretBinding implements SchemaChange {
+ private final String property;
+ private final SecretBinding binding;
+
+ private SetSecretBinding(String property, SecretBinding binding) {
+ this.property = property;
+ this.binding = binding;
+ }
+
+ /**
+ * Retrieves the property name being bound.
+ *
+ * @return The property name.
+ */
+ public String getProperty() {
+ return property;
+ }
+
+ /**
+ * Retrieves the write-through binding.
+ *
+ * @return The secret binding.
+ */
+ public SecretBinding getBinding() {
+ return binding;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SetSecretBinding that = (SetSecretBinding) o;
+ return Objects.equals(property, that.property) &&
Objects.equals(binding, that.binding);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(property, binding);
+ }
+
+ @Override
+ public String toString() {
+ return "SETSECRETBINDING " + property + " " + binding;
+ }
+ }
+
+ /** SchemaChange to bind an external secret reference for a property. */
+ final class SetSecretReference implements SchemaChange {
+ private final String property;
+ private final SecretReference reference;
+
+ private SetSecretReference(String property, SecretReference reference) {
+ this.property = property;
+ this.reference = reference;
+ }
+
+ /**
+ * Retrieves the property name being bound.
+ *
+ * @return The property name.
+ */
+ public String getProperty() {
+ return property;
+ }
+
+ /**
+ * Retrieves the external secret reference.
+ *
+ * @return The secret reference.
+ */
+ public SecretReference getReference() {
+ return reference;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SetSecretReference that = (SetSecretReference) o;
+ return Objects.equals(property, that.property) &&
Objects.equals(reference, that.reference);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(property, reference);
+ }
+
+ @Override
+ public String toString() {
+ return "SETSECRETREFERENCE " + property + " " + reference;
+ }
+ }
}
diff --git
a/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java
b/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java
index f08c416d7d..24670074f6 100644
---
a/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java
+++
b/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java
@@ -66,6 +66,7 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.GravitinoEnv;
@@ -115,7 +116,10 @@ import org.apache.gravitino.meta.FilesetEntity;
import org.apache.gravitino.meta.SchemaEntity;
import org.apache.gravitino.metrics.MetricsSystem;
import org.apache.gravitino.metrics.source.FilesetCatalogMetricsSource;
+import org.apache.gravitino.secret.SecretAlterChanges;
import org.apache.gravitino.secret.SecretManager;
+import org.apache.gravitino.secret.SecretMaterial;
+import org.apache.gravitino.secret.SecretMaterialsHolder;
import org.apache.gravitino.utils.FilesetUtil;
import org.apache.gravitino.utils.NameIdentifierUtil;
import org.apache.gravitino.utils.NamespaceUtil;
@@ -679,14 +683,26 @@ public class FilesetCatalogOperations extends
ManagedSchemaOperations
throw new RuntimeException("Failed to load fileset " + ident, ioe);
}
+ SecretMaterialsHolder writtenSecretMaterials = new SecretMaterialsHolder();
+ boolean alterCommitted = false;
try {
FilesetEntity updatedFilesetEntity =
store.update(
ident,
FilesetEntity.class,
Entity.EntityType.FILESET,
- e -> updateFilesetEntity(ident, e, changes));
-
+ existing -> {
+ Map<String, String> currentProperties =
+ existing.properties() == null
+ ? new HashMap<>()
+ : new HashMap<>(existing.properties());
+ Pair<FilesetChange[], List<SecretMaterial>> secretResult =
+ SecretAlterChanges.prepareFilesetChanges(
+ secretManager, currentProperties, existing.id(),
changes);
+ writtenSecretMaterials.set(secretResult.getRight());
+ return updateFilesetEntity(ident, existing,
secretResult.getLeft());
+ });
+ alterCommitted = true;
return FilesetImpl.builder()
.withName(updatedFilesetEntity.name())
.withComment(updatedFilesetEntity.comment())
@@ -703,6 +719,10 @@ public class FilesetCatalogOperations extends
ManagedSchemaOperations
// This is happened when renaming a fileset to an existing fileset name.
throw new RuntimeException(
"Fileset with the same name " + ident.name() + " already exists",
aee);
+ } finally {
+ if (!alterCommitted) {
+ secretManager.rollbackSecrets(writtenSecretMaterials.get());
+ }
}
}
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
index 4c5408dfe7..84c6f2bbf2 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
@@ -187,6 +187,21 @@ class DTOConverters {
return new CatalogUpdateRequest.RemoveCatalogPropertyRequest(
((CatalogChange.RemoveProperty) change).getProperty());
+ } else if (change instanceof CatalogChange.SetSecretBinding) {
+ CatalogChange.SetSecretBinding setSecretBinding =
(CatalogChange.SetSecretBinding) change;
+ return new CatalogUpdateRequest.SetCatalogSecretBindingRequest(
+ setSecretBinding.getProperty(),
+ setSecretBinding.getBinding().provider(),
+ setSecretBinding.getBinding().plaintext());
+
+ } else if (change instanceof CatalogChange.SetSecretReference) {
+ CatalogChange.SetSecretReference setSecretReference =
+ (CatalogChange.SetSecretReference) change;
+ return new CatalogUpdateRequest.SetCatalogSecretReferenceRequest(
+ setSecretReference.getProperty(),
+ setSecretReference.getReference().provider(),
+ setSecretReference.getReference().attributes());
+
} else {
throw new IllegalArgumentException(
"Unknown change type: " + change.getClass().getSimpleName());
@@ -203,6 +218,20 @@ class DTOConverters {
return new SchemaUpdateRequest.RemoveSchemaPropertyRequest(
((SchemaChange.RemoveProperty) change).getProperty());
+ } else if (change instanceof SchemaChange.SetSecretBinding) {
+ SchemaChange.SetSecretBinding setSecretBinding =
(SchemaChange.SetSecretBinding) change;
+ return new SchemaUpdateRequest.SetSchemaSecretBindingRequest(
+ setSecretBinding.getProperty(),
+ setSecretBinding.getBinding().provider(),
+ setSecretBinding.getBinding().plaintext());
+
+ } else if (change instanceof SchemaChange.SetSecretReference) {
+ SchemaChange.SetSecretReference setSecretReference =
(SchemaChange.SetSecretReference) change;
+ return new SchemaUpdateRequest.SetSchemaSecretReferenceRequest(
+ setSecretReference.getProperty(),
+ setSecretReference.getReference().provider(),
+ setSecretReference.getReference().attributes());
+
} else {
throw new IllegalArgumentException(
"Unknown change type: " + change.getClass().getSimpleName());
diff --git a/clients/client-python/gravitino/api/catalog_change.py
b/clients/client-python/gravitino/api/catalog_change.py
index 0a66963dcc..c80cd9675c 100644
--- a/clients/client-python/gravitino/api/catalog_change.py
+++ b/clients/client-python/gravitino/api/catalog_change.py
@@ -17,6 +17,8 @@
from abc import ABC
+from gravitino.api.secret import SecretBinding, SecretReference
+
class CatalogChange(ABC):
"""
@@ -73,6 +75,32 @@ class CatalogChange(ABC):
"""
return CatalogChange.RemoveProperty(catalog_property)
+ @staticmethod
+ def set_secret_binding(catalog_property, binding: SecretBinding):
+ """Creates a catalog change to bind a write-through secret for a
property.
+
+ Args:
+ catalog_property: The property name to bind.
+ binding: The write-through secret binding.
+
+ Returns:
+ The catalog change.
+ """
+ return CatalogChange.SetSecretBinding(catalog_property, binding)
+
+ @staticmethod
+ def set_secret_reference(catalog_property, reference: SecretReference):
+ """Creates a catalog change to bind an external secret reference for a
property.
+
+ Args:
+ catalog_property: The property name to bind.
+ reference: The external secret reference.
+
+ Returns:
+ The catalog change.
+ """
+ return CatalogChange.SetSecretReference(catalog_property, reference)
+
class RenameCatalog:
"""A catalog change to rename the catalog."""
@@ -265,3 +293,61 @@ class CatalogChange(ABC):
A string summary of the property removal operation.
"""
return f"REMOVEPROPERTY {self._property}"
+
+ class SetSecretBinding:
+ """A catalog change to bind a write-through secret for a property."""
+
+ def __init__(self, catalog_property, binding: SecretBinding):
+ self._property = catalog_property
+ self._binding = binding
+
+ def property(self):
+ """Retrieves the property name being bound."""
+ return self._property
+
+ def binding(self):
+ """Retrieves the write-through secret binding."""
+ return self._binding
+
+ def __eq__(self, other) -> bool:
+ if not isinstance(other, CatalogChange.SetSecretBinding):
+ return False
+ return (
+ self._property == other.property()
+ and self._binding == other.binding()
+ )
+
+ def __hash__(self):
+ return hash((self._property, self._binding))
+
+ def __str__(self):
+ return f"SETSECRETBINDING {self._property} {self._binding}"
+
+ class SetSecretReference:
+ """A catalog change to bind an external secret reference for a
property."""
+
+ def __init__(self, catalog_property, reference: SecretReference):
+ self._property = catalog_property
+ self._reference = reference
+
+ def property(self):
+ """Retrieves the property name being bound."""
+ return self._property
+
+ def reference(self):
+ """Retrieves the external secret reference."""
+ return self._reference
+
+ def __eq__(self, other) -> bool:
+ if not isinstance(other, CatalogChange.SetSecretReference):
+ return False
+ return (
+ self._property == other.property()
+ and self._reference == other.reference()
+ )
+
+ def __hash__(self):
+ return hash((self._property, self._reference))
+
+ def __str__(self):
+ return f"SETSECRETREFERENCE {self._property} {self._reference}"
diff --git a/clients/client-python/gravitino/api/schema_change.py
b/clients/client-python/gravitino/api/schema_change.py
index c1cd70a9fc..2d6c8a3947 100644
--- a/clients/client-python/gravitino/api/schema_change.py
+++ b/clients/client-python/gravitino/api/schema_change.py
@@ -20,6 +20,8 @@ from dataclasses import dataclass, field
from dataclasses_json import config
+from gravitino.api.secret import SecretBinding, SecretReference
+
class SchemaChange(ABC):
"""NamespaceChange class to set the property and value pairs for the
namespace."""
@@ -49,6 +51,16 @@ class SchemaChange(ABC):
"""
return SchemaChange.RemoveProperty(schema_property)
+ @staticmethod
+ def set_secret_binding(schema_property: str, binding: SecretBinding):
+ """Creates a schema change to bind a write-through secret for a
property."""
+ return SchemaChange.SetSecretBinding(schema_property, binding)
+
+ @staticmethod
+ def set_secret_reference(schema_property: str, reference: SecretReference):
+ """Creates a schema change to bind an external secret reference for a
property."""
+ return SchemaChange.SetSecretReference(schema_property, reference)
+
@dataclass
class SetProperty:
"""SchemaChange class to set the property and value pairs for the
schema."""
@@ -149,3 +161,56 @@ class SchemaChange(ABC):
A string summary of the property removal operation.
"""
return f"REMOVEPROPERTY {self._property}"
+
+ @dataclass
+ class SetSecretBinding:
+ """SchemaChange class to bind a write-through secret for a property."""
+
+ _property: str = field(metadata=config(field_name="property"))
+ _binding: SecretBinding = field(metadata=config(field_name="binding"))
+
+ def property(self):
+ return self._property
+
+ def binding(self):
+ return self._binding
+
+ def __eq__(self, other):
+ if not isinstance(other, SchemaChange.SetSecretBinding):
+ return False
+ return (
+ self._property == other.property() and self._binding ==
other.binding()
+ )
+
+ def __hash__(self):
+ return hash((self._property, self._binding))
+
+ def __str__(self):
+ return f"SETSECRETBINDING {self._property} {self._binding}"
+
+ @dataclass
+ class SetSecretReference:
+ """SchemaChange class to bind an external secret reference for a
property."""
+
+ _property: str = field(metadata=config(field_name="property"))
+ _reference: SecretReference =
field(metadata=config(field_name="reference"))
+
+ def property(self):
+ return self._property
+
+ def reference(self):
+ return self._reference
+
+ def __eq__(self, other):
+ if not isinstance(other, SchemaChange.SetSecretReference):
+ return False
+ return (
+ self._property == other.property()
+ and self._reference == other.reference()
+ )
+
+ def __hash__(self):
+ return hash((self._property, self._reference))
+
+ def __str__(self):
+ return f"SETSECRETREFERENCE {self._property} {self._reference}"
diff --git a/clients/client-python/gravitino/client/base_schema_catalog.py
b/clients/client-python/gravitino/client/base_schema_catalog.py
index 16d9884a5a..66483a3dac 100644
--- a/clients/client-python/gravitino/client/base_schema_catalog.py
+++ b/clients/client-python/gravitino/client/base_schema_catalog.py
@@ -369,6 +369,16 @@ class BaseSchemaCatalog(
)
if isinstance(change, SchemaChange.RemoveProperty):
return
SchemaUpdateRequest.RemoveSchemaPropertyRequest(change.property())
+ if isinstance(change, SchemaChange.SetSecretBinding):
+ binding = change.binding()
+ return SchemaUpdateRequest.SetSchemaSecretBindingRequest(
+ change.property(), binding.provider, binding.plaintext
+ )
+ if isinstance(change, SchemaChange.SetSecretReference):
+ reference = change.reference()
+ return SchemaUpdateRequest.SetSchemaSecretReferenceRequest(
+ change.property(), reference.provider, reference.attributes
+ )
raise ValueError(f"Unknown change type: {type(change).__name__}")
def validate(self):
diff --git a/clients/client-python/gravitino/client/dto_converters.py
b/clients/client-python/gravitino/client/dto_converters.py
index c61ded134b..801e91b584 100644
--- a/clients/client-python/gravitino/client/dto_converters.py
+++ b/clients/client-python/gravitino/client/dto_converters.py
@@ -145,6 +145,16 @@ class DTOConverters:
return CatalogUpdateRequest.RemoveCatalogPropertyRequest(
change.get_property()
)
+ if isinstance(change, CatalogChange.SetSecretBinding):
+ binding = change.binding()
+ return CatalogUpdateRequest.SetCatalogSecretBindingRequest(
+ change.property(), binding.provider, binding.plaintext
+ )
+ if isinstance(change, CatalogChange.SetSecretReference):
+ reference = change.reference()
+ return CatalogUpdateRequest.SetCatalogSecretReferenceRequest(
+ change.property(), reference.provider, reference.attributes
+ )
raise ValueError(f"Unknown change type: {type(change).__name__}")
diff --git
a/clients/client-python/gravitino/dto/requests/catalog_update_request.py
b/clients/client-python/gravitino/dto/requests/catalog_update_request.py
index b4ba71c546..e39982f633 100644
--- a/clients/client-python/gravitino/dto/requests/catalog_update_request.py
+++ b/clients/client-python/gravitino/dto/requests/catalog_update_request.py
@@ -17,11 +17,12 @@
from abc import abstractmethod
from dataclasses import field, dataclass
-from typing import Optional
+from typing import Dict, Optional
from dataclasses_json import config
from gravitino.api.catalog_change import CatalogChange
+from gravitino.api.secret import SecretBinding, SecretReference
from gravitino.rest.rest_message import RESTRequest
@@ -124,3 +125,64 @@ class CatalogUpdateRequest:
def validate(self):
if not self._property:
raise ValueError('"property" field is required and cannot be
empty')
+
+ @dataclass
+ class SetCatalogSecretBindingRequest(CatalogUpdateRequestBase):
+ """Request to bind a write-through secret for a catalog property."""
+
+ _property: Optional[str] =
field(metadata=config(field_name="property"))
+ _provider: Optional[str] =
field(metadata=config(field_name="provider"))
+ _plaintext: Optional[str] =
field(metadata=config(field_name="plaintext"))
+
+ def __init__(self, catalog_property: str, provider: str, plaintext:
str):
+ super().__init__("setSecretBinding")
+ self._property = catalog_property
+ self._provider = provider
+ self._plaintext = plaintext
+
+ def catalog_change(self):
+ return CatalogChange.set_secret_binding(
+ self._property,
+ SecretBinding(self._provider, self._plaintext),
+ )
+
+ def validate(self):
+ if not self._property:
+ raise ValueError('"property" field is required and cannot be
empty')
+ if not self._provider:
+ raise ValueError('"provider" field is required and cannot be
empty')
+ if self._plaintext is None:
+ raise ValueError('"plaintext" field is required and cannot be
null')
+
+ @dataclass
+ class SetCatalogSecretReferenceRequest(CatalogUpdateRequestBase):
+ """Request to bind an external secret reference for a catalog
property."""
+
+ _property: Optional[str] =
field(metadata=config(field_name="property"))
+ _provider: Optional[str] =
field(metadata=config(field_name="provider"))
+ _attributes: Optional[Dict[str, str]] = field(
+ metadata=config(field_name="attributes")
+ )
+
+ def __init__(
+ self,
+ catalog_property: str,
+ provider: str,
+ attributes: Optional[Dict[str, str]] = None,
+ ):
+ super().__init__("setSecretReference")
+ self._property = catalog_property
+ self._provider = provider
+ self._attributes = attributes
+
+ def catalog_change(self):
+ return CatalogChange.set_secret_reference(
+ self._property,
+ SecretReference(self._provider, self._attributes or {}),
+ )
+
+ def validate(self):
+ if not self._property:
+ raise ValueError('"property" field is required and cannot be
empty')
+ if not self._provider:
+ raise ValueError('"provider" field is required and cannot be
empty')
diff --git
a/clients/client-python/gravitino/dto/requests/schema_update_request.py
b/clients/client-python/gravitino/dto/requests/schema_update_request.py
index 7475f5304e..7872d6e838 100644
--- a/clients/client-python/gravitino/dto/requests/schema_update_request.py
+++ b/clients/client-python/gravitino/dto/requests/schema_update_request.py
@@ -17,10 +17,12 @@
from abc import abstractmethod
from dataclasses import dataclass, field
+from typing import Dict, Optional
from dataclasses_json import config
from gravitino.api.schema_change import SchemaChange
+from gravitino.api.secret import SecretBinding, SecretReference
from gravitino.rest.rest_message import RESTRequest
@@ -95,3 +97,64 @@ class SchemaUpdateRequest:
def schema_change(self):
return SchemaChange.remove_property(self._property)
+
+ @dataclass
+ class SetSchemaSecretBindingRequest(SchemaUpdateRequestBase):
+ """Represents a request to bind a write-through secret for a schema
property."""
+
+ _property: Optional[str] =
field(metadata=config(field_name="property"))
+ _provider: Optional[str] =
field(metadata=config(field_name="provider"))
+ _plaintext: Optional[str] =
field(metadata=config(field_name="plaintext"))
+
+ def __init__(self, schema_property: str, provider: str, plaintext:
str):
+ super().__init__("setSecretBinding")
+ self._property = schema_property
+ self._provider = provider
+ self._plaintext = plaintext
+
+ def validate(self):
+ if not self._property:
+ raise ValueError('"property" field is required and cannot be
empty')
+ if not self._provider:
+ raise ValueError('"provider" field is required and cannot be
empty')
+ if self._plaintext is None:
+ raise ValueError('"plaintext" field is required and cannot be
null')
+
+ def schema_change(self):
+ return SchemaChange.set_secret_binding(
+ self._property,
+ SecretBinding(self._provider, self._plaintext),
+ )
+
+ @dataclass
+ class SetSchemaSecretReferenceRequest(SchemaUpdateRequestBase):
+ """Represents a request to bind an external secret reference for a
schema property."""
+
+ _property: Optional[str] =
field(metadata=config(field_name="property"))
+ _provider: Optional[str] =
field(metadata=config(field_name="provider"))
+ _attributes: Optional[Dict[str, str]] = field(
+ metadata=config(field_name="attributes")
+ )
+
+ def __init__(
+ self,
+ schema_property: str,
+ provider: str,
+ attributes: Optional[Dict[str, str]] = None,
+ ):
+ super().__init__("setSecretReference")
+ self._property = schema_property
+ self._provider = provider
+ self._attributes = attributes
+
+ def validate(self):
+ if not self._property:
+ raise ValueError('"property" field is required and cannot be
empty')
+ if not self._provider:
+ raise ValueError('"provider" field is required and cannot be
empty')
+
+ def schema_change(self):
+ return SchemaChange.set_secret_reference(
+ self._property,
+ SecretReference(self._provider, self._attributes or {}),
+ )
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/CatalogUpdateRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/CatalogUpdateRequest.java
index aabdf2901f..107f95c149 100644
---
a/common/src/main/java/org/apache/gravitino/dto/requests/CatalogUpdateRequest.java
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/CatalogUpdateRequest.java
@@ -23,12 +23,16 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.CatalogChange;
import org.apache.gravitino.rest.RESTRequest;
+import org.apache.gravitino.secret.SecretBinding;
+import org.apache.gravitino.secret.SecretReference;
/** Represents an interface for catalog update requests. */
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -43,7 +47,13 @@ import org.apache.gravitino.rest.RESTRequest;
name = "setProperty"),
@JsonSubTypes.Type(
value = CatalogUpdateRequest.RemoveCatalogPropertyRequest.class,
- name = "removeProperty")
+ name = "removeProperty"),
+ @JsonSubTypes.Type(
+ value = CatalogUpdateRequest.SetCatalogSecretBindingRequest.class,
+ name = "setSecretBinding"),
+ @JsonSubTypes.Type(
+ value = CatalogUpdateRequest.SetCatalogSecretReferenceRequest.class,
+ name = "setSecretReference")
})
public interface CatalogUpdateRequest extends RESTRequest {
@@ -214,4 +224,117 @@ public interface CatalogUpdateRequest extends RESTRequest
{
return CatalogChange.removeProperty(property);
}
}
+
+ /** Request to bind a write-through secret for a catalog property. */
+ @EqualsAndHashCode
+ @ToString(exclude = "plaintext")
+ class SetCatalogSecretBindingRequest implements CatalogUpdateRequest {
+
+ @Getter
+ @JsonProperty("property")
+ private final String property;
+
+ @Getter
+ @JsonProperty("provider")
+ private final String provider;
+
+ @Getter
+ @JsonProperty("plaintext")
+ private final String plaintext;
+
+ /**
+ * Constructor for SetCatalogSecretBindingRequest.
+ *
+ * @param property The property to bind.
+ * @param provider The registered secrets-provider instance name.
+ * @param plaintext The plaintext secret to write through.
+ */
+ public SetCatalogSecretBindingRequest(String property, String provider,
String plaintext) {
+ this.property = property;
+ this.provider = provider;
+ this.plaintext = plaintext;
+ }
+
+ /** Default constructor for SetCatalogSecretBindingRequest. */
+ public SetCatalogSecretBindingRequest() {
+ this(null, null, null);
+ }
+
+ /**
+ * Validates the fields of the request.
+ *
+ * @throws IllegalArgumentException if required fields are not set.
+ */
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(property), "\"property\" field is required
and cannot be empty");
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(provider), "\"provider\" field is required
and cannot be empty");
+ Preconditions.checkArgument(
+ plaintext != null, "\"plaintext\" field is required and cannot be
null");
+ }
+
+ @Override
+ public CatalogChange catalogChange() {
+ return CatalogChange.setSecretBinding(property, new
SecretBinding(provider, plaintext));
+ }
+ }
+
+ /** Request to bind an external secret reference for a catalog property. */
+ @EqualsAndHashCode
+ @ToString
+ class SetCatalogSecretReferenceRequest implements CatalogUpdateRequest {
+
+ @Getter
+ @JsonProperty("property")
+ private final String property;
+
+ @Getter
+ @JsonProperty("provider")
+ private final String provider;
+
+ @Getter
+ @JsonProperty("attributes")
+ private final Map<String, String> attributes;
+
+ /**
+ * Constructor for SetCatalogSecretReferenceRequest.
+ *
+ * @param property The property to bind.
+ * @param provider The registered secrets-provider instance name.
+ * @param attributes Provider-specific locator attributes.
+ */
+ public SetCatalogSecretReferenceRequest(
+ String property, String provider, Map<String, String> attributes) {
+ this.property = property;
+ this.provider = provider;
+ this.attributes = attributes;
+ }
+
+ /** Default constructor for SetCatalogSecretReferenceRequest. */
+ public SetCatalogSecretReferenceRequest() {
+ this(null, null, null);
+ }
+
+ /**
+ * Validates the fields of the request.
+ *
+ * @throws IllegalArgumentException if required fields are not set.
+ */
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(property), "\"property\" field is required
and cannot be empty");
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(provider), "\"provider\" field is required
and cannot be empty");
+ }
+
+ @Override
+ public CatalogChange catalogChange() {
+ return CatalogChange.setSecretReference(
+ property,
+ new SecretReference(provider, attributes == null ? ImmutableMap.of()
: attributes));
+ }
+ }
}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/SchemaUpdateRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/SchemaUpdateRequest.java
index 7a2e650309..99f57abfa6 100644
---
a/common/src/main/java/org/apache/gravitino/dto/requests/SchemaUpdateRequest.java
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/SchemaUpdateRequest.java
@@ -23,12 +23,16 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.SchemaChange;
import org.apache.gravitino.rest.RESTRequest;
+import org.apache.gravitino.secret.SecretBinding;
+import org.apache.gravitino.secret.SecretReference;
/** Represents a request to update a schema. */
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -39,7 +43,13 @@ import org.apache.gravitino.rest.RESTRequest;
name = "setProperty"),
@JsonSubTypes.Type(
value = SchemaUpdateRequest.RemoveSchemaPropertyRequest.class,
- name = "removeProperty")
+ name = "removeProperty"),
+ @JsonSubTypes.Type(
+ value = SchemaUpdateRequest.SetSchemaSecretBindingRequest.class,
+ name = "setSecretBinding"),
+ @JsonSubTypes.Type(
+ value = SchemaUpdateRequest.SetSchemaSecretReferenceRequest.class,
+ name = "setSecretReference")
})
public interface SchemaUpdateRequest extends RESTRequest {
@@ -147,4 +157,117 @@ public interface SchemaUpdateRequest extends RESTRequest {
return SchemaChange.removeProperty(property);
}
}
+
+ /** Represents a request to bind a write-through secret for a schema
property. */
+ @EqualsAndHashCode
+ @ToString(exclude = "plaintext")
+ class SetSchemaSecretBindingRequest implements SchemaUpdateRequest {
+
+ @Getter
+ @JsonProperty("property")
+ private final String property;
+
+ @Getter
+ @JsonProperty("provider")
+ private final String provider;
+
+ @Getter
+ @JsonProperty("plaintext")
+ private final String plaintext;
+
+ /**
+ * Creates a new SetSchemaSecretBindingRequest.
+ *
+ * @param property The property to bind.
+ * @param provider The registered secrets-provider instance name.
+ * @param plaintext The plaintext secret to write through.
+ */
+ public SetSchemaSecretBindingRequest(String property, String provider,
String plaintext) {
+ this.property = property;
+ this.provider = provider;
+ this.plaintext = plaintext;
+ }
+
+ /** Default constructor for Jackson deserialization. */
+ public SetSchemaSecretBindingRequest() {
+ this(null, null, null);
+ }
+
+ /**
+ * Validates the request.
+ *
+ * @throws IllegalArgumentException If the request is invalid, this
exception is thrown.
+ */
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(property), "\"property\" field is required
and cannot be empty");
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(provider), "\"provider\" field is required
and cannot be empty");
+ Preconditions.checkArgument(
+ plaintext != null, "\"plaintext\" field is required and cannot be
null");
+ }
+
+ @Override
+ public SchemaChange schemaChange() {
+ return SchemaChange.setSecretBinding(property, new
SecretBinding(provider, plaintext));
+ }
+ }
+
+ /** Represents a request to bind an external secret reference for a schema
property. */
+ @EqualsAndHashCode
+ @ToString
+ class SetSchemaSecretReferenceRequest implements SchemaUpdateRequest {
+
+ @Getter
+ @JsonProperty("property")
+ private final String property;
+
+ @Getter
+ @JsonProperty("provider")
+ private final String provider;
+
+ @Getter
+ @JsonProperty("attributes")
+ private final Map<String, String> attributes;
+
+ /**
+ * Creates a new SetSchemaSecretReferenceRequest.
+ *
+ * @param property The property to bind.
+ * @param provider The registered secrets-provider instance name.
+ * @param attributes Provider-specific locator attributes.
+ */
+ public SetSchemaSecretReferenceRequest(
+ String property, String provider, Map<String, String> attributes) {
+ this.property = property;
+ this.provider = provider;
+ this.attributes = attributes;
+ }
+
+ /** Default constructor for Jackson deserialization. */
+ public SetSchemaSecretReferenceRequest() {
+ this(null, null, null);
+ }
+
+ /**
+ * Validates the request.
+ *
+ * @throws IllegalArgumentException If the request is invalid, this
exception is thrown.
+ */
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(property), "\"property\" field is required
and cannot be empty");
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(provider), "\"provider\" field is required
and cannot be empty");
+ }
+
+ @Override
+ public SchemaChange schemaChange() {
+ return SchemaChange.setSecretReference(
+ property,
+ new SecretReference(provider, attributes == null ? ImmutableMap.of()
: attributes));
+ }
+ }
}
diff --git
a/common/src/test/java/org/apache/gravitino/json/TestRequestJsonSerDe.java
b/common/src/test/java/org/apache/gravitino/json/TestRequestJsonSerDe.java
index 5ccd0a1cf1..9887e1df86 100644
--- a/common/src/test/java/org/apache/gravitino/json/TestRequestJsonSerDe.java
+++ b/common/src/test/java/org/apache/gravitino/json/TestRequestJsonSerDe.java
@@ -28,6 +28,7 @@ import org.apache.gravitino.dto.requests.FilesetUpdateRequest;
import org.apache.gravitino.dto.requests.MetalakeCreateRequest;
import org.apache.gravitino.dto.requests.MetalakeUpdateRequest;
import org.apache.gravitino.dto.requests.MetalakeUpdatesRequest;
+import org.apache.gravitino.dto.requests.SchemaUpdateRequest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -159,20 +160,32 @@ public class TestRequestJsonSerDe {
}
@Test
- public void testFilesetUpdateRequestSerDe() throws JsonProcessingException {
- FilesetUpdateRequest req =
- new FilesetUpdateRequest.SetFilesetSecretBindingRequest("password",
"env", "secret");
- String serJson = JsonUtils.objectMapper().writeValueAsString(req);
- FilesetUpdateRequest deserReq =
- JsonUtils.objectMapper().readValue(serJson,
FilesetUpdateRequest.class);
- Assertions.assertEquals(req, deserReq);
-
- FilesetUpdateRequest req1 =
+ public void testSecretUpdateRequestSerDe() throws JsonProcessingException {
+ roundTrip(
+ new CatalogUpdateRequest.SetCatalogSecretBindingRequest("password",
"env", "secret"),
+ CatalogUpdateRequest.class);
+ roundTrip(
+ new CatalogUpdateRequest.SetCatalogSecretReferenceRequest(
+ "password", "vault", ImmutableMap.of("path",
"secret/data/my-password")),
+ CatalogUpdateRequest.class);
+ roundTrip(
+ new SchemaUpdateRequest.SetSchemaSecretBindingRequest("password",
"env", "secret"),
+ SchemaUpdateRequest.class);
+ roundTrip(
+ new SchemaUpdateRequest.SetSchemaSecretReferenceRequest(
+ "password", "vault", ImmutableMap.of("path",
"secret/data/my-password")),
+ SchemaUpdateRequest.class);
+ roundTrip(
+ new FilesetUpdateRequest.SetFilesetSecretBindingRequest("password",
"env", "secret"),
+ FilesetUpdateRequest.class);
+ roundTrip(
new FilesetUpdateRequest.SetFilesetSecretReferenceRequest(
- "password", "vault", ImmutableMap.of("path",
"secret/data/my-password"));
- String serJson1 = JsonUtils.objectMapper().writeValueAsString(req1);
- FilesetUpdateRequest deserReq1 =
- JsonUtils.objectMapper().readValue(serJson1,
FilesetUpdateRequest.class);
- Assertions.assertEquals(req1, deserReq1);
+ "password", "vault", ImmutableMap.of("path",
"secret/data/my-password")),
+ FilesetUpdateRequest.class);
+ }
+
+ private static <T> void roundTrip(T request, Class<T> type) throws
JsonProcessingException {
+ String json = JsonUtils.objectMapper().writeValueAsString(request);
+ Assertions.assertEquals(request, JsonUtils.objectMapper().readValue(json,
type));
}
}
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
index bd15b808bd..8cc0625f6a 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
@@ -108,9 +108,11 @@ import org.apache.gravitino.rel.SupportsPartitions;
import org.apache.gravitino.rel.Table;
import org.apache.gravitino.rel.TableCatalog;
import org.apache.gravitino.rel.ViewCatalog;
+import org.apache.gravitino.secret.SecretAlterChanges;
import org.apache.gravitino.secret.SecretBinding;
import org.apache.gravitino.secret.SecretManager;
import org.apache.gravitino.secret.SecretMaterial;
+import org.apache.gravitino.secret.SecretMaterialsHolder;
import org.apache.gravitino.secret.SecretPropertyUtils;
import org.apache.gravitino.secret.SecretReference;
import org.apache.gravitino.storage.IdGenerator;
@@ -926,23 +928,7 @@ public class CatalogManager implements CatalogDispatcher,
Closeable {
LockType.WRITE,
() -> {
try {
- CatalogEntity updatedCatalog =
- store.update(
- ident,
- CatalogEntity.class,
- EntityType.CATALOG,
- catalog -> {
- CatalogEntity.Builder newCatalogBuilder =
- newCatalogBuilder(ident.namespace(), catalog);
-
- Map<String, String> newProps =
- catalog.getProperties() == null
- ? new HashMap<>()
- : new HashMap<>(catalog.getProperties());
- newCatalogBuilder = updateEntity(newCatalogBuilder,
newProps, changes);
-
- return newCatalogBuilder.build();
- });
+ CatalogEntity updatedCatalog = alterCatalogUnderLock(ident,
changes);
// Invalidate after store.update() so that any background thread
that tries to reload
// the old catalog identifier from the store (after the
invalidate) will get
// NoSuchCatalogException instead of stale data. Invalidating
before the update creates
@@ -959,14 +945,17 @@ public class CatalogManager implements CatalogDispatcher,
Closeable {
catalogCache.put(convertedCatalog.nameIdentifier(), newWrapper);
return newWrapper.catalog();
- } catch (NoSuchEntityException ne) {
- LOG.warn("Catalog {} does not exist", ident, ne);
- throw new NoSuchCatalogException(CATALOG_DOES_NOT_EXIST_MSG,
ident);
+ } catch (NoSuchCatalogException e) {
+ throw e;
} catch (IllegalArgumentException iae) {
LOG.warn("Failed to alter catalog {} with unknown change", ident,
iae);
throw iae;
+ } catch (NoSuchEntityException ne) {
+ LOG.warn("Catalog {} does not exist", ident, ne);
+ throw new NoSuchCatalogException(CATALOG_DOES_NOT_EXIST_MSG,
ident);
+
} catch (IOException ioe) {
LOG.error("Failed to alter catalog {}", ident, ioe);
throw new RuntimeException(ioe);
@@ -974,6 +963,48 @@ public class CatalogManager implements CatalogDispatcher,
Closeable {
});
}
+ private CatalogEntity alterCatalogUnderLock(NameIdentifier ident,
CatalogChange... changes)
+ throws NoSuchCatalogException, IllegalArgumentException, IOException {
+ SecretMaterialsHolder writtenSecretMaterials = new SecretMaterialsHolder();
+ boolean alterCommitted = false;
+ try {
+ CatalogEntity updatedCatalog =
+ store.update(
+ ident,
+ CatalogEntity.class,
+ EntityType.CATALOG,
+ existing -> {
+ Map<String, String> currentProperties =
+ existing.getProperties() == null
+ ? new HashMap<>()
+ : new HashMap<>(existing.getProperties());
+
+ Pair<CatalogChange[], List<SecretMaterial>> secretResult =
+ SecretAlterChanges.prepareCatalogChanges(
+ secretManager, currentProperties, existing.id(),
changes);
+ writtenSecretMaterials.set(secretResult.getRight());
+ CatalogChange[] effectiveChanges = secretResult.getLeft();
+
+ CatalogEntity.Builder newCatalogBuilder =
+ newCatalogBuilder(ident.namespace(), existing);
+
+ Map<String, String> newProps =
+ existing.getProperties() == null
+ ? new HashMap<>()
+ : new HashMap<>(existing.getProperties());
+ return updateEntity(newCatalogBuilder, newProps,
effectiveChanges).build();
+ });
+ alterCommitted = true;
+ return updatedCatalog;
+ } catch (NoSuchEntityException e) {
+ throw new NoSuchCatalogException(CATALOG_DOES_NOT_EXIST_MSG, ident);
+ } finally {
+ if (!alterCommitted) {
+ secretManager.rollbackSecrets(writtenSecretMaterials.get());
+ }
+ }
+ }
+
@Override
public boolean dropCatalog(NameIdentifier ident, boolean force)
throws NonEmptyEntityException, CatalogInUseException {
@@ -1238,6 +1269,15 @@ public class CatalogManager implements
CatalogDispatcher, Closeable {
if (catalogChange instanceof SetProperty) {
SetProperty setProperty = (SetProperty) catalogChange;
upserts.put(setProperty.getProperty(), setProperty.getValue());
+ } else if (catalogChange instanceof
CatalogChange.SetSecretBinding) {
+ CatalogChange.SetSecretBinding setSecretBinding =
+ (CatalogChange.SetSecretBinding) catalogChange;
+ upserts.put(
+ setSecretBinding.getProperty(),
setSecretBinding.getBinding().plaintext());
+ } else if (catalogChange instanceof
CatalogChange.SetSecretReference) {
+ CatalogChange.SetSecretReference setSecretReference =
+ (CatalogChange.SetSecretReference) catalogChange;
+ upserts.put(setSecretReference.getProperty(),
setSecretReference.getProperty());
} else if (catalogChange instanceof RemoveProperty) {
RemoveProperty removeProperty = (RemoveProperty) catalogChange;
deletes.put(removeProperty.getProperty(),
removeProperty.getProperty());
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/FilesetOperationDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/FilesetOperationDispatcher.java
index 00f7bbe907..4dd30a42e9 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/FilesetOperationDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/FilesetOperationDispatcher.java
@@ -22,12 +22,10 @@ import static
org.apache.gravitino.Entity.EntityType.FILESET;
import static
org.apache.gravitino.catalog.PropertiesMetadataHelpers.validatePropertyForCreate;
import static
org.apache.gravitino.utils.NameIdentifierUtil.getCatalogIdentifier;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import javax.annotation.Nullable;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.NameIdentifier;
@@ -45,9 +43,11 @@ import org.apache.gravitino.file.FilesetChange;
import org.apache.gravitino.lock.LockType;
import org.apache.gravitino.lock.TreeLockUtils;
import org.apache.gravitino.meta.FilesetEntity;
+import org.apache.gravitino.secret.SecretAlterChanges;
import org.apache.gravitino.secret.SecretBinding;
import org.apache.gravitino.secret.SecretManager;
import org.apache.gravitino.secret.SecretMaterial;
+import org.apache.gravitino.secret.SecretMaterialsHolder;
import org.apache.gravitino.secret.SecretPropertyUtils;
import org.apache.gravitino.secret.SecretReference;
import org.apache.gravitino.storage.IdGenerator;
@@ -256,6 +256,24 @@ public class FilesetOperationDispatcher extends
OperationDispatcher implements F
private Fileset alterFilesetUnderLock(
NameIdentifier ident, NameIdentifier catalogIdent, FilesetChange...
changes) {
+ validateAlterProperties(ident,
HasPropertyMetadata::filesetPropertiesMetadata, changes);
+ if (usesFilesetCatalogEntityStore(catalogIdent)) {
+ return doWithCatalog(
+ catalogIdent,
+ c -> c.doWithFilesetOps(f -> f.alterFileset(ident, changes)),
+ NoSuchFilesetException.class,
+ IllegalArgumentException.class);
+ }
+ return alterFilesetWithPreparedSecrets(ident, catalogIdent, changes);
+ }
+
+ private boolean usesFilesetCatalogEntityStore(NameIdentifier catalogIdent) {
+ return doWithCatalog(
+ catalogIdent, c -> "fileset".equals(c.catalog().provider()),
NoSuchFilesetException.class);
+ }
+
+ private Fileset alterFilesetWithPreparedSecrets(
+ NameIdentifier ident, NameIdentifier catalogIdent, FilesetChange...
changes) {
Fileset currentFileset =
doWithCatalog(
catalogIdent,
@@ -274,8 +292,6 @@ public class FilesetOperationDispatcher extends
OperationDispatcher implements F
currentProperties = new HashMap<>();
}
- validateAlterProperties(ident,
HasPropertyMetadata::filesetPropertiesMetadata, changes);
-
StringIdentifier currentStringId =
getStringIdFromProperties(currentProperties);
long filesetId;
if (currentStringId != null) {
@@ -286,12 +302,13 @@ public class FilesetOperationDispatcher extends
OperationDispatcher implements F
filesetId = 0L;
}
- List<SecretMaterial> writtenSecretMaterials = List.of();
+ SecretMaterialsHolder writtenSecretMaterials = new SecretMaterialsHolder();
boolean alterCommitted = false;
try {
Pair<FilesetChange[], List<SecretMaterial>> secretResult =
- prepareFilesetSecretChanges(currentProperties, filesetId, changes);
- writtenSecretMaterials = secretResult.getRight();
+ SecretAlterChanges.prepareFilesetChanges(
+ secretManager, currentProperties, filesetId, changes);
+ writtenSecretMaterials.set(secretResult.getRight());
FilesetChange[] effectiveChanges = secretResult.getLeft();
Fileset altered =
@@ -304,7 +321,7 @@ public class FilesetOperationDispatcher extends
OperationDispatcher implements F
return altered;
} finally {
if (!alterCommitted) {
- secretManager.rollbackSecrets(writtenSecretMaterials);
+ secretManager.rollbackSecrets(writtenSecretMaterials.get());
}
}
}
@@ -373,54 +390,4 @@ public class FilesetOperationDispatcher extends
OperationDispatcher implements F
c -> c.doWithFilesetOps(f -> f.getFileLocation(ident, subPath,
locationName)),
NonEmptyEntityException.class));
}
-
- /**
- * Rewrites fileset changes that involve secrets into plain setProperty /
removeProperty, writing
- * secrets as needed. Rolls back any written materials if preparation fails.
- *
- * @param currentProperties current fileset properties (may be null)
- * @param entityId fileset entity id
- * @param changes fileset changes
- * @return effective changes and written write-through materials
- */
- private Pair<FilesetChange[], List<SecretMaterial>>
prepareFilesetSecretChanges(
- @Nullable Map<String, String> currentProperties, long entityId,
FilesetChange... changes) {
- Map<String, String> properties =
- currentProperties == null ? new HashMap<>() : new
HashMap<>(currentProperties);
- List<FilesetChange> out = new ArrayList<>(changes.length);
- List<SecretMaterial> written = new ArrayList<>();
- try {
- for (FilesetChange change : changes) {
- if (change instanceof FilesetChange.SetSecretBinding) {
- FilesetChange.SetSecretBinding c = (FilesetChange.SetSecretBinding)
change;
- String urn =
- secretManager.alterSetSecretBinding(
- properties, "fileset", entityId, c.getProperty(),
c.getBinding(), written);
- out.add(FilesetChange.setProperty(c.getProperty(), urn));
- } else if (change instanceof FilesetChange.SetSecretReference) {
- FilesetChange.SetSecretReference c =
(FilesetChange.SetSecretReference) change;
- String urn =
- secretManager.alterSetSecretReference(
- properties, "fileset", entityId, c.getProperty(),
c.getReference());
- out.add(FilesetChange.setProperty(c.getProperty(), urn));
- } else if (change instanceof FilesetChange.SetProperty) {
- FilesetChange.SetProperty c = (FilesetChange.SetProperty) change;
- String value =
- secretManager.alterSetProperty(
- properties, "fileset", entityId, c.getProperty(),
c.getValue());
- out.add(FilesetChange.setProperty(c.getProperty(), value));
- } else if (change instanceof FilesetChange.RemoveProperty) {
- FilesetChange.RemoveProperty c = (FilesetChange.RemoveProperty)
change;
- secretManager.alterRemoveProperty(properties, "fileset", entityId,
c.getProperty());
- out.add(change);
- } else {
- out.add(change);
- }
- }
- return Pair.of(out.toArray(new FilesetChange[0]), List.copyOf(written));
- } catch (RuntimeException e) {
- secretManager.rollbackSecrets(written);
- throw e;
- }
- }
}
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/ManagedSchemaOperations.java
b/core/src/main/java/org/apache/gravitino/catalog/ManagedSchemaOperations.java
index 1c2dbade53..e4a78a9db8 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/ManagedSchemaOperations.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/ManagedSchemaOperations.java
@@ -19,7 +19,6 @@
package org.apache.gravitino.catalog;
import com.google.common.base.Preconditions;
-import com.google.common.collect.Maps;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
@@ -203,37 +202,6 @@ public abstract class ManagedSchemaOperations implements
SupportsSchemas {
private SchemaEntity updateSchemaEntity(
NameIdentifier ident, SchemaEntity schemaEntity, SchemaChange...
changes) {
- Map<String, String> props =
- schemaEntity.properties() == null
- ? Maps.newHashMap()
- : Maps.newHashMap(schemaEntity.properties());
-
- for (SchemaChange change : changes) {
- if (change instanceof SchemaChange.SetProperty) {
- SchemaChange.SetProperty setProperty = (SchemaChange.SetProperty)
change;
- props.put(setProperty.getProperty(), setProperty.getValue());
- } else if (change instanceof SchemaChange.RemoveProperty) {
- SchemaChange.RemoveProperty removeProperty =
(SchemaChange.RemoveProperty) change;
- props.remove(removeProperty.getProperty());
- } else {
- throw new IllegalArgumentException(
- "Unsupported schema change: " + change.getClass().getSimpleName());
- }
- }
-
- return SchemaEntity.builder()
- .withName(schemaEntity.name())
- .withNamespace(ident.namespace())
- .withId(schemaEntity.id())
- .withComment(schemaEntity.comment())
- .withProperties(props)
- .withAuditInfo(
- AuditInfo.builder()
- .withCreator(schemaEntity.auditInfo().creator())
- .withCreateTime(schemaEntity.auditInfo().createTime())
-
.withLastModifier(PrincipalUtils.getCurrentPrincipal().getName())
- .withLastModifiedTime(Instant.now())
- .build())
- .build();
+ return SchemaEntityChanges.apply(ident, schemaEntity, changes);
}
}
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/OperationDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/OperationDispatcher.java
index 31fb86b76e..2b07cc8e99 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/OperationDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/OperationDispatcher.java
@@ -289,6 +289,12 @@ public abstract class OperationDispatcher {
} else if (item instanceof SchemaChange.SetProperty) {
SchemaChange.SetProperty setProperty = (SchemaChange.SetProperty) item;
properties.put(setProperty.getProperty(), setProperty.getValue());
+ } else if (item instanceof SchemaChange.SetSecretBinding) {
+ SchemaChange.SetSecretBinding setSecretBinding =
(SchemaChange.SetSecretBinding) item;
+ properties.put(setSecretBinding.getProperty(),
setSecretBinding.getBinding().plaintext());
+ } else if (item instanceof SchemaChange.SetSecretReference) {
+ SchemaChange.SetSecretReference setSecretReference =
(SchemaChange.SetSecretReference) item;
+ properties.put(setSecretReference.getProperty(),
setSecretReference.getProperty());
} else if (item instanceof FilesetChange.SetProperty) {
FilesetChange.SetProperty setProperty = (FilesetChange.SetProperty)
item;
properties.put(setProperty.getProperty(), setProperty.getValue());
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/SchemaEntityChanges.java
b/core/src/main/java/org/apache/gravitino/catalog/SchemaEntityChanges.java
new file mode 100644
index 0000000000..79f0dff91d
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/catalog/SchemaEntityChanges.java
@@ -0,0 +1,70 @@
+/*
+ * 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.gravitino.catalog;
+
+import com.google.common.collect.Maps;
+import java.time.Instant;
+import java.util.Map;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.SchemaChange;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.SchemaEntity;
+import org.apache.gravitino.utils.PrincipalUtils;
+
+/** Applies {@link SchemaChange} updates to {@link SchemaEntity}. */
+final class SchemaEntityChanges {
+
+ private SchemaEntityChanges() {}
+
+ static SchemaEntity apply(
+ NameIdentifier ident, SchemaEntity schemaEntity, SchemaChange...
changes) {
+ Map<String, String> props =
+ schemaEntity.properties() == null
+ ? Maps.newHashMap()
+ : Maps.newHashMap(schemaEntity.properties());
+
+ for (SchemaChange change : changes) {
+ if (change instanceof SchemaChange.SetProperty) {
+ SchemaChange.SetProperty setProperty = (SchemaChange.SetProperty)
change;
+ props.put(setProperty.getProperty(), setProperty.getValue());
+ } else if (change instanceof SchemaChange.RemoveProperty) {
+ SchemaChange.RemoveProperty removeProperty =
(SchemaChange.RemoveProperty) change;
+ props.remove(removeProperty.getProperty());
+ } else {
+ throw new IllegalArgumentException(
+ "Unsupported schema change: " + change.getClass().getSimpleName());
+ }
+ }
+
+ return SchemaEntity.builder()
+ .withName(schemaEntity.name())
+ .withNamespace(ident.namespace())
+ .withId(schemaEntity.id())
+ .withComment(schemaEntity.comment())
+ .withProperties(props)
+ .withAuditInfo(
+ AuditInfo.builder()
+ .withCreator(schemaEntity.auditInfo().creator())
+ .withCreateTime(schemaEntity.auditInfo().createTime())
+
.withLastModifier(PrincipalUtils.getCurrentPrincipal().getName())
+ .withLastModifiedTime(Instant.now())
+ .build())
+ .build();
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/SchemaOperationDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/SchemaOperationDispatcher.java
index c98dfe5add..46da516d60 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/SchemaOperationDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/SchemaOperationDispatcher.java
@@ -31,6 +31,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
+import org.apache.commons.lang3.tuple.Pair;
import org.apache.gravitino.EntityAlreadyExistsException;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.NameIdentifier;
@@ -50,9 +51,11 @@ import org.apache.gravitino.lock.TreeLockUtils;
import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.meta.FilesetEntity;
import org.apache.gravitino.meta.SchemaEntity;
+import org.apache.gravitino.secret.SecretAlterChanges;
import org.apache.gravitino.secret.SecretBinding;
import org.apache.gravitino.secret.SecretManager;
import org.apache.gravitino.secret.SecretMaterial;
+import org.apache.gravitino.secret.SecretMaterialsHolder;
import org.apache.gravitino.secret.SecretPropertyUtils;
import org.apache.gravitino.secret.SecretReference;
import org.apache.gravitino.storage.IdGenerator;
@@ -307,12 +310,10 @@ public class SchemaOperationDispatcher extends
OperationDispatcher implements Sc
ident,
LockType.WRITE,
() -> {
- validateAlterProperties(ident,
HasPropertyMetadata::schemaPropertiesMetadata, changes);
- Schema alteredSchema =
- doWithCatalog(
- catalogIdent,
- c -> c.doWithSchemaOps(s -> s.alterSchema(ident, changes)),
- NoSuchSchemaException.class);
+ Pair<Schema, SchemaChange[]> alterResult =
+ alterSchemaUnderLock(ident, catalogIdent, changes);
+ Schema alteredSchema = alterResult.getLeft();
+ SchemaChange[] effectiveChanges = alterResult.getRight();
// If the Schema is maintained by the Gravitino's store, we don't
have to alter again.
boolean isManagedSchema = isManagedEntity(catalogIdent,
Capability.Scope.SCHEMA);
@@ -361,7 +362,8 @@ public class SchemaOperationDispatcher extends
OperationDispatcher implements Sc
.withName(schemaEntity.name())
.withNamespace(ident.namespace())
.withProperties(
-
propertiesForSchemaEntityAlter(schemaEntity, changes))
+ propertiesForSchemaEntityAlter(
+ schemaEntity, effectiveChanges))
.withAuditInfo(
AuditInfo.builder()
.withCreator(schemaEntity.auditInfo().creator())
@@ -383,6 +385,129 @@ public class SchemaOperationDispatcher extends
OperationDispatcher implements Sc
});
}
+ private Pair<Schema, SchemaChange[]> alterSchemaUnderLock(
+ NameIdentifier ident, NameIdentifier catalogIdent, SchemaChange...
changes)
+ throws NoSuchSchemaException {
+ if (isManagedEntity(catalogIdent, Capability.Scope.SCHEMA)
+ && usesManagedSchemaOperations(catalogIdent)) {
+ return alterManagedSchemaUnderLock(ident, changes);
+ }
+ return alterExternalSchemaUnderLock(ident, catalogIdent, changes);
+ }
+
+ private boolean usesManagedSchemaOperations(NameIdentifier catalogIdent) {
+ return doWithCatalog(
+ catalogIdent,
+ c -> c.doWithSchemaOps(s -> s instanceof ManagedSchemaOperations),
+ NoSuchSchemaException.class);
+ }
+
+ private Pair<Schema, SchemaChange[]> alterManagedSchemaUnderLock(
+ NameIdentifier ident, SchemaChange... changes) throws
NoSuchSchemaException {
+ validateAlterProperties(ident,
HasPropertyMetadata::schemaPropertiesMetadata, changes);
+
+ SecretMaterialsHolder writtenSecretMaterials = new SecretMaterialsHolder();
+ SchemaChange[][] effectiveChangesHolder = new SchemaChange[1][];
+ boolean alterCommitted = false;
+ try {
+ SchemaEntity updatedEntity =
+ store.update(
+ ident,
+ SchemaEntity.class,
+ SCHEMA,
+ existing -> {
+ Map<String, String> currentProperties =
+ existing.properties() == null
+ ? new HashMap<>()
+ : new HashMap<>(existing.properties());
+ Pair<SchemaChange[], List<SecretMaterial>> secretResult =
+ SecretAlterChanges.prepareSchemaChanges(
+ secretManager, currentProperties, existing.id(),
changes);
+ writtenSecretMaterials.set(secretResult.getRight());
+ effectiveChangesHolder[0] = secretResult.getLeft();
+ return SchemaEntityChanges.apply(ident, existing,
secretResult.getLeft());
+ });
+ alterCommitted = true;
+ Schema alteredSchema =
+ ManagedSchemaOperations.ManagedSchema.builder()
+ .withName(ident.name())
+ .withComment(updatedEntity.comment())
+ .withProperties(updatedEntity.properties())
+ .withAuditInfo(updatedEntity.auditInfo())
+ .build();
+ return Pair.of(alteredSchema, effectiveChangesHolder[0]);
+ } catch (NoSuchEntityException e) {
+ throw new NoSuchSchemaException(e, "Schema %s does not exist", ident);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to alter schema " + ident, e);
+ } finally {
+ if (!alterCommitted) {
+ secretManager.rollbackSecrets(writtenSecretMaterials.get());
+ }
+ }
+ }
+
+ private Pair<Schema, SchemaChange[]> alterExternalSchemaUnderLock(
+ NameIdentifier ident, NameIdentifier catalogIdent, SchemaChange...
changes)
+ throws NoSuchSchemaException {
+ Schema currentSchema = null;
+ try {
+ currentSchema =
+ doWithCatalog(
+ catalogIdent,
+ c -> c.doWithSchemaOps(s -> s.loadSchema(ident)),
+ NoSuchSchemaException.class);
+ } catch (NoSuchSchemaException e) {
+ // Defer missing-schema handling to catalog alterSchema to preserve
catalog semantics.
+ }
+ // Prefer SchemaEntity properties for secret URNs (catalog loadSchema may
omit them).
+ SchemaEntity schemaEntityForSecrets = getEntity(ident, SCHEMA,
SchemaEntity.class);
+ Map<String, String> currentProperties;
+ if (schemaEntityForSecrets != null
+ && schemaEntityForSecrets.properties() != null
+ && !schemaEntityForSecrets.properties().isEmpty()) {
+ currentProperties = new HashMap<>(schemaEntityForSecrets.properties());
+ } else if (currentSchema != null && currentSchema.properties() != null) {
+ currentProperties = new HashMap<>(currentSchema.properties());
+ } else {
+ currentProperties = new HashMap<>();
+ }
+
+ validateAlterProperties(ident,
HasPropertyMetadata::schemaPropertiesMetadata, changes);
+
+ StringIdentifier currentStringId =
getStringIdFromProperties(currentProperties);
+ long entityIdForSecrets;
+ if (currentStringId != null) {
+ entityIdForSecrets = currentStringId.id();
+ } else if (schemaEntityForSecrets != null) {
+ entityIdForSecrets = schemaEntityForSecrets.id();
+ } else {
+ entityIdForSecrets = 0L;
+ }
+
+ SecretMaterialsHolder writtenSecretMaterials = new SecretMaterialsHolder();
+ boolean alterCommitted = false;
+ try {
+ Pair<SchemaChange[], List<SecretMaterial>> secretResult =
+ SecretAlterChanges.prepareSchemaChanges(
+ secretManager, currentProperties, entityIdForSecrets, changes);
+ writtenSecretMaterials.set(secretResult.getRight());
+ SchemaChange[] effectiveChanges = secretResult.getLeft();
+
+ Schema alteredSchema =
+ doWithCatalog(
+ catalogIdent,
+ c -> c.doWithSchemaOps(s -> s.alterSchema(ident,
effectiveChanges)),
+ NoSuchSchemaException.class);
+ alterCommitted = true;
+ return Pair.of(alteredSchema, effectiveChanges);
+ } finally {
+ if (!alterCommitted) {
+ secretManager.rollbackSecrets(writtenSecretMaterials.get());
+ }
+ }
+ }
+
/**
* Drops a schema.
*
diff --git
a/core/src/main/java/org/apache/gravitino/secret/SecretAlterChanges.java
b/core/src/main/java/org/apache/gravitino/secret/SecretAlterChanges.java
new file mode 100644
index 0000000000..1615847a0a
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/secret/SecretAlterChanges.java
@@ -0,0 +1,198 @@
+/*
+ * 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.gravitino.secret;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.gravitino.CatalogChange;
+import org.apache.gravitino.SchemaChange;
+import org.apache.gravitino.file.FilesetChange;
+
+/**
+ * Rewrites entity alter changes that involve secrets into plain setProperty /
removeProperty.
+ *
+ * <p>Rolls back any written materials if preparation fails.
+ */
+public final class SecretAlterChanges {
+
+ private SecretAlterChanges() {}
+
+ /**
+ * Prepares catalog alter changes that involve secrets.
+ *
+ * @param secretManager secret manager
+ * @param currentProperties current catalog properties (may be null)
+ * @param entityId catalog entity id
+ * @param changes catalog changes
+ * @return effective changes and written write-through materials
+ */
+ public static Pair<CatalogChange[], List<SecretMaterial>>
prepareCatalogChanges(
+ SecretManager secretManager,
+ @Nullable Map<String, String> currentProperties,
+ long entityId,
+ CatalogChange... changes) {
+ Map<String, String> properties =
+ currentProperties == null ? new HashMap<>() : new
HashMap<>(currentProperties);
+ List<CatalogChange> out = new ArrayList<>(changes.length);
+ List<SecretMaterial> written = new ArrayList<>();
+ try {
+ for (CatalogChange change : changes) {
+ if (change instanceof CatalogChange.SetSecretBinding) {
+ CatalogChange.SetSecretBinding c = (CatalogChange.SetSecretBinding)
change;
+ String urn =
+ secretManager.alterSetSecretBinding(
+ properties, "catalog", entityId, c.getProperty(),
c.getBinding(), written);
+ out.add(CatalogChange.setProperty(c.getProperty(), urn));
+ } else if (change instanceof CatalogChange.SetSecretReference) {
+ CatalogChange.SetSecretReference c =
(CatalogChange.SetSecretReference) change;
+ String urn =
+ secretManager.alterSetSecretReference(
+ properties, "catalog", entityId, c.getProperty(),
c.getReference());
+ out.add(CatalogChange.setProperty(c.getProperty(), urn));
+ } else if (change instanceof CatalogChange.SetProperty) {
+ CatalogChange.SetProperty c = (CatalogChange.SetProperty) change;
+ String value =
+ secretManager.alterSetProperty(
+ properties, "catalog", entityId, c.getProperty(),
c.getValue());
+ out.add(CatalogChange.setProperty(c.getProperty(), value));
+ } else if (change instanceof CatalogChange.RemoveProperty) {
+ CatalogChange.RemoveProperty c = (CatalogChange.RemoveProperty)
change;
+ secretManager.alterRemoveProperty(properties, "catalog", entityId,
c.getProperty());
+ out.add(change);
+ } else {
+ out.add(change);
+ }
+ }
+ return Pair.of(out.toArray(new CatalogChange[0]), List.copyOf(written));
+ } catch (RuntimeException e) {
+ secretManager.rollbackSecrets(written);
+ throw e;
+ }
+ }
+
+ /**
+ * Prepares schema alter changes that involve secrets.
+ *
+ * @param secretManager secret manager
+ * @param currentProperties current schema properties (may be null)
+ * @param entityId schema entity id
+ * @param changes schema changes
+ * @return effective changes and written write-through materials
+ */
+ public static Pair<SchemaChange[], List<SecretMaterial>>
prepareSchemaChanges(
+ SecretManager secretManager,
+ @Nullable Map<String, String> currentProperties,
+ long entityId,
+ SchemaChange... changes) {
+ Map<String, String> properties =
+ currentProperties == null ? new HashMap<>() : new
HashMap<>(currentProperties);
+ List<SchemaChange> out = new ArrayList<>(changes.length);
+ List<SecretMaterial> written = new ArrayList<>();
+ try {
+ for (SchemaChange change : changes) {
+ if (change instanceof SchemaChange.SetSecretBinding) {
+ SchemaChange.SetSecretBinding c = (SchemaChange.SetSecretBinding)
change;
+ String urn =
+ secretManager.alterSetSecretBinding(
+ properties, "schema", entityId, c.getProperty(),
c.getBinding(), written);
+ out.add(SchemaChange.setProperty(c.getProperty(), urn));
+ } else if (change instanceof SchemaChange.SetSecretReference) {
+ SchemaChange.SetSecretReference c =
(SchemaChange.SetSecretReference) change;
+ String urn =
+ secretManager.alterSetSecretReference(
+ properties, "schema", entityId, c.getProperty(),
c.getReference());
+ out.add(SchemaChange.setProperty(c.getProperty(), urn));
+ } else if (change instanceof SchemaChange.SetProperty) {
+ SchemaChange.SetProperty c = (SchemaChange.SetProperty) change;
+ String value =
+ secretManager.alterSetProperty(
+ properties, "schema", entityId, c.getProperty(),
c.getValue());
+ out.add(SchemaChange.setProperty(c.getProperty(), value));
+ } else if (change instanceof SchemaChange.RemoveProperty) {
+ SchemaChange.RemoveProperty c = (SchemaChange.RemoveProperty) change;
+ secretManager.alterRemoveProperty(properties, "schema", entityId,
c.getProperty());
+ out.add(change);
+ } else {
+ out.add(change);
+ }
+ }
+ return Pair.of(out.toArray(new SchemaChange[0]), List.copyOf(written));
+ } catch (RuntimeException e) {
+ secretManager.rollbackSecrets(written);
+ throw e;
+ }
+ }
+
+ /**
+ * Prepares fileset alter changes that involve secrets.
+ *
+ * @param secretManager secret manager
+ * @param currentProperties current fileset properties (may be null)
+ * @param entityId fileset entity id
+ * @param changes fileset changes
+ * @return effective changes and written write-through materials
+ */
+ public static Pair<FilesetChange[], List<SecretMaterial>>
prepareFilesetChanges(
+ SecretManager secretManager,
+ @Nullable Map<String, String> currentProperties,
+ long entityId,
+ FilesetChange... changes) {
+ Map<String, String> properties =
+ currentProperties == null ? new HashMap<>() : new
HashMap<>(currentProperties);
+ List<FilesetChange> out = new ArrayList<>(changes.length);
+ List<SecretMaterial> written = new ArrayList<>();
+ try {
+ for (FilesetChange change : changes) {
+ if (change instanceof FilesetChange.SetSecretBinding) {
+ FilesetChange.SetSecretBinding c = (FilesetChange.SetSecretBinding)
change;
+ String urn =
+ secretManager.alterSetSecretBinding(
+ properties, "fileset", entityId, c.getProperty(),
c.getBinding(), written);
+ out.add(FilesetChange.setProperty(c.getProperty(), urn));
+ } else if (change instanceof FilesetChange.SetSecretReference) {
+ FilesetChange.SetSecretReference c =
(FilesetChange.SetSecretReference) change;
+ String urn =
+ secretManager.alterSetSecretReference(
+ properties, "fileset", entityId, c.getProperty(),
c.getReference());
+ out.add(FilesetChange.setProperty(c.getProperty(), urn));
+ } else if (change instanceof FilesetChange.SetProperty) {
+ FilesetChange.SetProperty c = (FilesetChange.SetProperty) change;
+ String value =
+ secretManager.alterSetProperty(
+ properties, "fileset", entityId, c.getProperty(),
c.getValue());
+ out.add(FilesetChange.setProperty(c.getProperty(), value));
+ } else if (change instanceof FilesetChange.RemoveProperty) {
+ FilesetChange.RemoveProperty c = (FilesetChange.RemoveProperty)
change;
+ secretManager.alterRemoveProperty(properties, "fileset", entityId,
c.getProperty());
+ out.add(change);
+ } else {
+ out.add(change);
+ }
+ }
+ return Pair.of(out.toArray(new FilesetChange[0]), List.copyOf(written));
+ } catch (RuntimeException e) {
+ secretManager.rollbackSecrets(written);
+ throw e;
+ }
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/secret/SecretMaterialsHolder.java
b/core/src/main/java/org/apache/gravitino/secret/SecretMaterialsHolder.java
new file mode 100644
index 0000000000..b94ce938bc
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/secret/SecretMaterialsHolder.java
@@ -0,0 +1,42 @@
+/*
+ * 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.gravitino.secret;
+
+import java.util.List;
+
+/**
+ * Mutable holder so {@code store.update} lambdas can record written secrets
for rollback.
+ *
+ * <p>Used by catalog, schema, and fileset alter paths that prepare secrets
inside {@code
+ * store.update} and roll back on failure.
+ */
+public final class SecretMaterialsHolder {
+
+ private List<SecretMaterial> materials = List.of();
+
+ /** Returns written secret materials. */
+ public List<SecretMaterial> get() {
+ return materials;
+ }
+
+ /** Records written secret materials. */
+ public void set(List<SecretMaterial> materials) {
+ this.materials = materials;
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
index e140e9fc25..d890c95394 100644
--- a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
+++ b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
@@ -1585,6 +1585,16 @@ public class TestCatalogManager {
.get(PROPERTY_KEY4);
Assertions.assertTrue(SecretPropertyUtils.isSecretProperty(PROPERTY_KEY4, urn));
Assertions.assertEquals("s3cr3t",
secrets.readSecret(SecretUrn.parse(urn)));
+
+ manager.alterCatalog(ident, CatalogChange.removeProperty(PROPERTY_KEY4));
+ Assertions.assertFalse(
+ entityStore
+ .get(ident, EntityType.CATALOG, CatalogEntity.class)
+ .getProperties()
+ .containsKey(PROPERTY_KEY4));
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
secrets.readSecret(SecretUrn.parse(urn)));
+
Assertions.assertTrue(manager.dropCatalog(ident, true));
Assertions.assertThrows(
IllegalArgumentException.class, () ->
secrets.readSecret(SecretUrn.parse(urn)));
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestFilesetOperationDispatcher.java
b/core/src/test/java/org/apache/gravitino/catalog/TestFilesetOperationDispatcher.java
index bee22f6ecc..33cd97a175 100644
---
a/core/src/test/java/org/apache/gravitino/catalog/TestFilesetOperationDispatcher.java
+++
b/core/src/test/java/org/apache/gravitino/catalog/TestFilesetOperationDispatcher.java
@@ -365,6 +365,14 @@ public class TestFilesetOperationDispatcher extends
TestOperationDispatcher {
SecretConstants.ATTR_ENTITY_ID, String.valueOf(entityId),
SecretConstants.ATTR_PROPERTY_KEY, "k2"));
Assertions.assertEquals("s3cr3t", secrets.readSecret(urn));
+ filesets.alterFileset(ident, FilesetChange.removeProperty("k2"));
+ Assertions.assertFalse(
+ catalogManager
+ .loadCatalogAndWrap(NameIdentifier.of(metalake, catalog))
+ .doWithFilesetOps(ops -> ops.loadFileset(ident))
+ .properties()
+ .containsKey("k2"));
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
secrets.readSecret(urn));
Assertions.assertThrows(
FilesetAlreadyExistsException.class,
() ->
@@ -401,51 +409,6 @@ public class TestFilesetOperationDispatcher extends
TestOperationDispatcher {
"k3");
}
- @Test
- public void testAlterRemovePropertyDeletesWriteThroughSecret() throws
Exception {
- try (SecretManager secrets = memorySecretManager()) {
- AtomicLong nextId = new AtomicLong(9100L);
- IdGenerator ids = nextId::getAndIncrement;
- FilesetOperationDispatcher filesets =
- new FilesetOperationDispatcher(catalogManager, entityStore, ids,
secrets);
- new SchemaOperationDispatcher(catalogManager, entityStore, ids, secrets,
filesets)
- .createSchema(
- NameIdentifier.of(metalake, catalog,
"schema_secret_fileset_remove"),
- "comment",
- ImmutableMap.of("k1", "v1"));
-
- NameIdentifier ident =
- NameIdentifier.of(
- metalake, catalog, "schema_secret_fileset_remove",
"fileset_secret_remove");
- Map<String, SecretBinding> bindings = Map.of("k2", new
SecretBinding("memory", "s3cr3t"));
- Map<String, String> locations = Map.of(Fileset.LOCATION_NAME_UNKNOWN,
"loc");
- Map<String, String> props = ImmutableMap.of("k1", "v1");
- long entityId = nextId.get();
- filesets.createMultipleLocationFileset(
- ident, "comment", Fileset.Type.MANAGED, locations, props, bindings,
Map.of());
-
- SecretUrn urn =
- SecretUrn.buildWriteThrough(
- "memory",
- Map.of(
- SecretConstants.ATTR_ENTITY_TYPE, "fileset",
- SecretConstants.ATTR_ENTITY_ID, String.valueOf(entityId),
- SecretConstants.ATTR_PROPERTY_KEY, "k2"));
- Assertions.assertEquals("s3cr3t", secrets.readSecret(urn));
-
- filesets.alterFileset(ident, FilesetChange.removeProperty("k2"));
-
- Fileset stored =
- catalogManager
- .loadCatalogAndWrap(NameIdentifier.of(metalake, catalog))
- .doWithFilesetOps(ops -> ops.loadFileset(ident));
- Assertions.assertFalse(stored.properties().containsKey("k2"));
- Assertions.assertThrows(IllegalArgumentException.class, () ->
secrets.readSecret(urn));
-
- Assertions.assertTrue(filesets.dropFileset(ident));
- }
- }
-
private static SecretManager memorySecretManager() {
Config c = new Config(false) {};
Properties p = new Properties();
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestSchemaOperationDispatcher.java
b/core/src/test/java/org/apache/gravitino/catalog/TestSchemaOperationDispatcher.java
index e31802593d..05ca381d1c 100644
---
a/core/src/test/java/org/apache/gravitino/catalog/TestSchemaOperationDispatcher.java
+++
b/core/src/test/java/org/apache/gravitino/catalog/TestSchemaOperationDispatcher.java
@@ -467,6 +467,10 @@ public class TestSchemaOperationDispatcher extends
TestOperationDispatcher {
SecretConstants.ATTR_ENTITY_ID, String.valueOf(entity.id()),
SecretConstants.ATTR_PROPERTY_KEY, "k2"));
Assertions.assertEquals("s3cr3t", secrets.readSecret(urn));
+ d.alterSchema(ident, SchemaChange.removeProperty("k2"));
+ Assertions.assertFalse(
+ entityStore.get(ident, SCHEMA,
SchemaEntity.class).properties().containsKey("k2"));
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
secrets.readSecret(urn));
Assertions.assertThrows(
SchemaAlreadyExistsException.class,
() -> d.createSchema(ident, "comment", ImmutableMap.of("k1", "v1"),
bindings, Map.of()));
diff --git a/docs/open-api/catalogs.yaml b/docs/open-api/catalogs.yaml
index ae82be9ce8..1340ecea74 100644
--- a/docs/open-api/catalogs.yaml
+++ b/docs/open-api/catalogs.yaml
@@ -509,6 +509,8 @@ components:
- $ref: "#/components/schemas/UpdateCatalogCommentRequest"
- $ref: "#/components/schemas/SetCatalogPropertyRequest"
- $ref: "#/components/schemas/RemoveCatalogPropertyRequest"
+ - $ref: "#/components/schemas/SetCatalogSecretBindingRequest"
+ - $ref: "#/components/schemas/SetCatalogSecretReferenceRequest"
discriminator:
propertyName: "@type"
mapping:
@@ -516,6 +518,8 @@ components:
updateComment: "#/components/schemas/UpdateCatalogCommentRequest"
setProperty: "#/components/schemas/SetCatalogPropertyRequest"
removeProperty: "#/components/schemas/RemoveCatalogPropertyRequest"
+ setSecretBinding:
"#/components/schemas/SetCatalogSecretBindingRequest"
+ setSecretReference:
"#/components/schemas/SetCatalogSecretReferenceRequest"
RenameCatalogRequest:
type: object
@@ -594,6 +598,67 @@ components:
"property": "key2"
}
+ SetCatalogSecretBindingRequest:
+ type: object
+ required:
+ - "@type"
+ - property
+ - provider
+ - plaintext
+ properties:
+ "@type":
+ type: string
+ enum:
+ - setSecretBinding
+ property:
+ type: string
+ description: The property to bind
+ provider:
+ type: string
+ description: Registered secrets-provider instance name
+ plaintext:
+ type: string
+ description: Plaintext secret to write through
+ format: password
+ example: {
+ "@type": "setSecretBinding",
+ "property": "password",
+ "provider": "env",
+ "plaintext": "secret-value"
+ }
+
+ SetCatalogSecretReferenceRequest:
+ type: object
+ required:
+ - "@type"
+ - property
+ - provider
+ properties:
+ "@type":
+ type: string
+ enum:
+ - setSecretReference
+ property:
+ type: string
+ description: The property to bind
+ provider:
+ type: string
+ description: Registered secrets-provider instance name
+ attributes:
+ type: object
+ description: Provider-specific locator keys (empty object if none)
+ additionalProperties:
+ type: string
+ default: {}
+ example: {
+ "@type": "setSecretReference",
+ "property": "password",
+ "provider": "vault",
+ "attributes": {
+ "path": "secret/data/my-password"
+ }
+ }
+
responses:
diff --git a/docs/open-api/schemas.yaml b/docs/open-api/schemas.yaml
index df03ce4c52..3539e76da6 100644
--- a/docs/open-api/schemas.yaml
+++ b/docs/open-api/schemas.yaml
@@ -249,11 +249,15 @@ components:
oneOf:
- $ref: "#/components/schemas/SetSchemaPropertyRequest"
- $ref: "#/components/schemas/RemoveSchemaPropertyRequest"
+ - $ref: "#/components/schemas/SetSchemaSecretBindingRequest"
+ - $ref: "#/components/schemas/SetSchemaSecretReferenceRequest"
discriminator:
propertyName: "@type"
mapping:
setProperty: "#/components/schemas/SetSchemaPropertyRequest"
removeProperty: "#/components/schemas/RemoveSchemaPropertyRequest"
+ setSecretBinding:
"#/components/schemas/SetSchemaSecretBindingRequest"
+ setSecretReference:
"#/components/schemas/SetSchemaSecretReferenceRequest"
SetSchemaPropertyRequest:
type: object
@@ -295,6 +299,67 @@ components:
"property": "key2"
}
+ SetSchemaSecretBindingRequest:
+ type: object
+ required:
+ - "@type"
+ - property
+ - provider
+ - plaintext
+ properties:
+ "@type":
+ type: string
+ enum:
+ - setSecretBinding
+ property:
+ type: string
+ description: The property to bind
+ provider:
+ type: string
+ description: Registered secrets-provider instance name
+ plaintext:
+ type: string
+ description: Plaintext secret to write through
+ format: password
+ example: {
+ "@type": "setSecretBinding",
+ "property": "password",
+ "provider": "env",
+ "plaintext": "secret-value"
+ }
+
+ SetSchemaSecretReferenceRequest:
+ type: object
+ required:
+ - "@type"
+ - property
+ - provider
+ properties:
+ "@type":
+ type: string
+ enum:
+ - setSecretReference
+ property:
+ type: string
+ description: The property to bind
+ provider:
+ type: string
+ description: Registered secrets-provider instance name
+ attributes:
+ type: object
+ description: Provider-specific locator keys (empty object if none)
+ additionalProperties:
+ type: string
+ default: {}
+ example: {
+ "@type": "setSecretReference",
+ "property": "password",
+ "provider": "vault",
+ "attributes": {
+ "path": "secret/data/my-password"
+ }
+ }
+
responses:
SchemaResponse:
description: Returns include the schema object