mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-26 06:53:37 +00:00
* Fix #22511: k8s secret support for Secrets Manager * Update generated TypeScript types * address comments * pylint fix * fix java checkstyle * improve inCluster description in schema * fix failing tests --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: ulixius9 <mayursingal9@gmail.com> Co-authored-by: Mayur Singal <39544459+ulixius9@users.noreply.github.com>
This commit is contained in:
parent
2c18a7ed32
commit
b0586f849f
@ -374,7 +374,7 @@ fernetConfiguration:
|
||||
fernetKey: ${FERNET_KEY:-jJ/9sz0g0OHxsfxOoSfdFdmk3ysNmPRnH3TUAbz3IHA=}
|
||||
|
||||
secretsManagerConfiguration:
|
||||
secretsManager: ${SECRET_MANAGER:-db} # Possible values are "db", "managed-aws","aws", "managed-aws-ssm", "aws-ssm", "managed-azure-kv", "azure-kv", "in-memory", "gcp"
|
||||
secretsManager: ${SECRET_MANAGER:-db} # Possible values are "db", "managed-aws","aws", "managed-aws-ssm", "aws-ssm", "managed-azure-kv", "azure-kv", "in-memory", "gcp", "kubernetes"
|
||||
prefix: ${SECRET_MANAGER_PREFIX:-""} # Define the secret key ID as /<prefix>/<clusterName>/<key>
|
||||
tags: ${SECRET_MANAGER_TAGS:-[]} # Add tags to the created resource. Format is `[key1:value1,key2:value2,...]`
|
||||
# it will use the default auth provider for the secrets' manager service if parameters are not set
|
||||
@ -390,6 +390,10 @@ secretsManagerConfiguration:
|
||||
vaultName: ${OM_SM_VAULT_NAME:-""}
|
||||
## For GCP
|
||||
projectId: ${OM_SM_PROJECT_ID:-""}
|
||||
## For Kubernetes
|
||||
namespace: ${OM_SM_NAMESPACE:-"default"}
|
||||
kubeconfigPath: ${OM_SM_KUBECONFIG_PATH:-""}
|
||||
inCluster: ${OM_SM_IN_CLUSTER:-"false"}
|
||||
|
||||
health:
|
||||
delayedShutdownHandlerEnabled: true
|
||||
|
||||
@ -140,6 +140,7 @@ base_requirements = {
|
||||
"importlib-metadata>=4.13.0", # From airflow constraints
|
||||
"Jinja2>=2.11.3",
|
||||
"jsonpatch<2.0, >=1.24",
|
||||
"kubernetes>=21.0.0", # Kubernetes client for secrets manager
|
||||
"memory-profiler",
|
||||
"mypy_extensions>=0.4.3",
|
||||
VERSIONS["pydantic"],
|
||||
|
||||
@ -0,0 +1,176 @@
|
||||
# Copyright 2025 Collate
|
||||
# Licensed under the Collate Community License, Version 1.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# https://github.com/open-metadata/OpenMetadata/blob/main/ingestion/LICENSE
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Kubernetes Secrets Manager implementation
|
||||
"""
|
||||
import base64
|
||||
import os
|
||||
import traceback
|
||||
from abc import ABC
|
||||
from typing import Optional
|
||||
|
||||
from kubernetes import client, config
|
||||
from kubernetes.client.exceptions import ApiException
|
||||
|
||||
from metadata.generated.schema.security.credentials.kubernetesCredentials import (
|
||||
KubernetesCredentials,
|
||||
)
|
||||
from metadata.generated.schema.security.secrets.secretsManagerClientLoader import (
|
||||
SecretsManagerClientLoader,
|
||||
)
|
||||
from metadata.generated.schema.security.secrets.secretsManagerProvider import (
|
||||
SecretsManagerProvider,
|
||||
)
|
||||
from metadata.utils.dispatch import enum_register
|
||||
from metadata.utils.logger import utils_logger
|
||||
from metadata.utils.secrets.external_secrets_manager import (
|
||||
SECRET_MANAGER_AIRFLOW_CONF,
|
||||
ExternalSecretsManager,
|
||||
SecretsManagerConfigException,
|
||||
)
|
||||
|
||||
logger = utils_logger()
|
||||
|
||||
secrets_manager_client_loader = enum_register()
|
||||
|
||||
|
||||
def _get_current_namespace() -> str:
|
||||
"""
|
||||
:return: The namespace where the application service account is running or default if it can't be retrieved
|
||||
"""
|
||||
try:
|
||||
with open(
|
||||
"/var/run/secrets/kubernetes.io/serviceaccount/namespace", encoding="utf-8"
|
||||
) as f:
|
||||
return f.read().strip()
|
||||
except Exception as _:
|
||||
logger.info(
|
||||
"Can't read the current namespace from in-cluster kubernetes. Is the service account configured?"
|
||||
)
|
||||
return "default"
|
||||
|
||||
|
||||
# pylint: disable=import-outside-toplevel
|
||||
@secrets_manager_client_loader.add(SecretsManagerClientLoader.noop.value)
|
||||
def _() -> None:
|
||||
return None
|
||||
|
||||
|
||||
@secrets_manager_client_loader.add(SecretsManagerClientLoader.airflow.value)
|
||||
def _() -> Optional[KubernetesCredentials]:
|
||||
from airflow.configuration import conf
|
||||
|
||||
namespace = conf.get(
|
||||
SECRET_MANAGER_AIRFLOW_CONF,
|
||||
"kubernetes_namespace",
|
||||
fallback=_get_current_namespace(),
|
||||
)
|
||||
in_cluster = conf.getboolean(
|
||||
SECRET_MANAGER_AIRFLOW_CONF, "kubernetes_in_cluster", fallback=False
|
||||
)
|
||||
kubeconfig_path = conf.get(
|
||||
SECRET_MANAGER_AIRFLOW_CONF, "kubernetes_kubeconfig_path", fallback=None
|
||||
)
|
||||
|
||||
return KubernetesCredentials(
|
||||
namespace=namespace,
|
||||
inCluster=in_cluster,
|
||||
kubeconfigPath=kubeconfig_path,
|
||||
)
|
||||
|
||||
|
||||
@secrets_manager_client_loader.add(SecretsManagerClientLoader.env.value)
|
||||
def _() -> Optional[KubernetesCredentials]:
|
||||
namespace = os.getenv("KUBERNETES_NAMESPACE", _get_current_namespace())
|
||||
in_cluster = os.getenv("KUBERNETES_IN_CLUSTER", "false").lower() == "true"
|
||||
kubeconfig_path = os.getenv("KUBERNETES_KUBECONFIG_PATH")
|
||||
|
||||
return KubernetesCredentials(
|
||||
namespace=namespace,
|
||||
inCluster=in_cluster,
|
||||
kubeconfigPath=kubeconfig_path,
|
||||
)
|
||||
|
||||
|
||||
class KubernetesSecretsManager(ExternalSecretsManager, ABC):
|
||||
"""
|
||||
Kubernetes Secrets Manager class
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
loader: SecretsManagerClientLoader,
|
||||
):
|
||||
super().__init__(provider=SecretsManagerProvider.kubernetes, loader=loader)
|
||||
|
||||
# Initialize Kubernetes client
|
||||
if self.credentials.inCluster:
|
||||
config.load_incluster_config()
|
||||
logger.info("Using in-cluster Kubernetes configuration")
|
||||
else:
|
||||
kubeconfig_path = self.credentials.kubeconfigPath
|
||||
if kubeconfig_path:
|
||||
config.load_kube_config(config_file=kubeconfig_path)
|
||||
logger.info(f"Using kubeconfig from path: {kubeconfig_path}")
|
||||
else:
|
||||
config.load_kube_config()
|
||||
logger.info("Using default kubeconfig")
|
||||
|
||||
self.client = client.CoreV1Api()
|
||||
self.namespace = self.credentials.namespace or _get_current_namespace()
|
||||
logger.info(
|
||||
f"Kubernetes SecretsManager initialized with namespace: {self.namespace}"
|
||||
)
|
||||
|
||||
def get_string_value(self, secret_id: str) -> str:
|
||||
"""
|
||||
:param secret_id: The secret id to retrieve
|
||||
:return: The value of the secret
|
||||
"""
|
||||
try:
|
||||
|
||||
# Get the secret from Kubernetes
|
||||
secret = self.client.read_namespaced_secret(
|
||||
name=secret_id, namespace=self.namespace
|
||||
)
|
||||
|
||||
# Kubernetes stores secret data as base64 encoded
|
||||
if secret.data and "value" in secret.data:
|
||||
secret_value = base64.b64decode(secret.data["value"]).decode("utf-8")
|
||||
logger.debug(f"Got value for secret {secret_id}")
|
||||
return secret_value
|
||||
logger.warning(f"Secret {secret_id} exists but has no 'value' key")
|
||||
return None
|
||||
|
||||
except ApiException as exc:
|
||||
if exc.status == 404:
|
||||
logger.debug(f"Secret {secret_id} not found")
|
||||
return None
|
||||
logger.debug(traceback.format_exc())
|
||||
logger.error(
|
||||
f"Could not get the secret value of {secret_id} due to [{exc}]"
|
||||
)
|
||||
raise exc
|
||||
except Exception as exc:
|
||||
logger.debug(traceback.format_exc())
|
||||
logger.error(
|
||||
f"Could not get the secret value of {secret_id} due to [{exc}]"
|
||||
)
|
||||
raise exc
|
||||
|
||||
def load_credentials(self) -> Optional[dict]:
|
||||
"""Load the provider credentials based on the loader type"""
|
||||
try:
|
||||
loader_fn = secrets_manager_client_loader.registry.get(self.loader.value)
|
||||
return loader_fn()
|
||||
except Exception as err:
|
||||
raise SecretsManagerConfigException(f"Error loading credentials - [{err}]")
|
||||
@ -25,6 +25,7 @@ from metadata.utils.secrets.aws_ssm_secrets_manager import AWSSSMSecretsManager
|
||||
from metadata.utils.secrets.azure_kv_secrets_manager import AzureKVSecretsManager
|
||||
from metadata.utils.secrets.db_secrets_manager import DBSecretsManager
|
||||
from metadata.utils.secrets.gcp_secrets_manager import GCPSecretsManager
|
||||
from metadata.utils.secrets.kubernetes_secrets_manager import KubernetesSecretsManager
|
||||
from metadata.utils.secrets.secrets_manager import SecretsManager
|
||||
from metadata.utils.singleton import Singleton
|
||||
|
||||
@ -96,6 +97,8 @@ class SecretsManagerFactory(metaclass=Singleton):
|
||||
return AzureKVSecretsManager(secrets_manager_loader)
|
||||
if secrets_manager_provider in (SecretsManagerProvider.gcp,):
|
||||
return GCPSecretsManager(secrets_manager_loader)
|
||||
if secrets_manager_provider in (SecretsManagerProvider.kubernetes,):
|
||||
return KubernetesSecretsManager(secrets_manager_loader)
|
||||
raise NotImplementedError(f"[{secrets_manager_provider}] is not implemented.")
|
||||
|
||||
def get_secrets_manager(self):
|
||||
|
||||
@ -0,0 +1,220 @@
|
||||
# Copyright 2025 Collate
|
||||
# Licensed under the Collate Community License, Version 1.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# https://github.com/open-metadata/OpenMetadata/blob/main/ingestion/LICENSE
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Test Kubernetes Secrets Manager
|
||||
"""
|
||||
import base64
|
||||
import os
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from kubernetes.client.exceptions import ApiException
|
||||
|
||||
from metadata.generated.schema.security.secrets.secretsManagerClientLoader import (
|
||||
SecretsManagerClientLoader,
|
||||
)
|
||||
from metadata.utils.secrets.kubernetes_secrets_manager import KubernetesSecretsManager
|
||||
from metadata.utils.singleton import Singleton
|
||||
|
||||
|
||||
class TestKubernetesSecretsManager(TestCase):
|
||||
"""Test Kubernetes Secrets Manager"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Clear singleton instances before each test"""
|
||||
Singleton.clear_all()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
"""Clear singleton instances after each test"""
|
||||
Singleton.clear_all()
|
||||
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.config")
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.client")
|
||||
def test_init_in_cluster(self, mock_client, mock_config):
|
||||
"""Test initialization with in-cluster config"""
|
||||
mock_core_v1_api = MagicMock()
|
||||
mock_client.CoreV1Api.return_value = mock_core_v1_api
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"KUBERNETES_NAMESPACE": "test-namespace",
|
||||
"KUBERNETES_IN_CLUSTER": "true",
|
||||
},
|
||||
):
|
||||
secrets_manager = KubernetesSecretsManager(
|
||||
loader=SecretsManagerClientLoader.env
|
||||
)
|
||||
|
||||
# Verify in-cluster config was loaded
|
||||
mock_config.load_incluster_config.assert_called_once()
|
||||
mock_config.load_kube_config.assert_not_called()
|
||||
|
||||
# Verify namespace is set correctly
|
||||
self.assertEqual(secrets_manager.namespace, "test-namespace")
|
||||
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.config")
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.client")
|
||||
def test_init_with_kubeconfig(self, mock_client, mock_config):
|
||||
"""Test initialization with kubeconfig file"""
|
||||
mock_core_v1_api = MagicMock()
|
||||
mock_client.CoreV1Api.return_value = mock_core_v1_api
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"KUBERNETES_NAMESPACE": "custom-namespace",
|
||||
"KUBERNETES_IN_CLUSTER": "false",
|
||||
"KUBERNETES_KUBECONFIG_PATH": "/path/to/kubeconfig",
|
||||
},
|
||||
):
|
||||
secrets_manager = KubernetesSecretsManager(
|
||||
loader=SecretsManagerClientLoader.env
|
||||
)
|
||||
|
||||
# Verify kubeconfig was loaded with correct path
|
||||
mock_config.load_incluster_config.assert_not_called()
|
||||
mock_config.load_kube_config.assert_called_once_with(
|
||||
config_file="/path/to/kubeconfig"
|
||||
)
|
||||
|
||||
# Verify namespace is set correctly
|
||||
self.assertEqual(secrets_manager.namespace, "custom-namespace")
|
||||
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.config")
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.client")
|
||||
def test_get_string_value_success(self, mock_client, mock_config):
|
||||
"""Test successful secret retrieval"""
|
||||
# Setup mock secret
|
||||
mock_secret = MagicMock()
|
||||
mock_secret.data = {"value": base64.b64encode(b"test-secret-value").decode()}
|
||||
|
||||
# Setup mock client
|
||||
mock_core_v1_api = MagicMock()
|
||||
mock_core_v1_api.read_namespaced_secret.return_value = mock_secret
|
||||
mock_client.CoreV1Api.return_value = mock_core_v1_api
|
||||
|
||||
with patch.dict(os.environ, {"KUBERNETES_NAMESPACE": "default"}):
|
||||
secrets_manager = KubernetesSecretsManager(
|
||||
loader=SecretsManagerClientLoader.env
|
||||
)
|
||||
|
||||
# Test retrieving secret
|
||||
result = secrets_manager.get_string_value("test-secret")
|
||||
|
||||
# Verify API call
|
||||
mock_core_v1_api.read_namespaced_secret.assert_called_once_with(
|
||||
name="test-secret", namespace="default"
|
||||
)
|
||||
|
||||
# Verify result
|
||||
self.assertEqual(result, "test-secret-value")
|
||||
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.config")
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.client")
|
||||
def test_get_string_value_not_found(self, mock_client, mock_config):
|
||||
"""Test secret not found returns None"""
|
||||
# Setup mock client to raise 404 error
|
||||
mock_core_v1_api = MagicMock()
|
||||
mock_core_v1_api.read_namespaced_secret.side_effect = (
|
||||
lambda **kwargs: self._raise_api_exception(404)
|
||||
)
|
||||
mock_client.CoreV1Api.return_value = mock_core_v1_api
|
||||
|
||||
with patch.dict(os.environ, {"KUBERNETES_NAMESPACE": "default"}):
|
||||
secrets_manager = KubernetesSecretsManager(
|
||||
loader=SecretsManagerClientLoader.env
|
||||
)
|
||||
|
||||
# Test retrieving non-existent secret
|
||||
result = secrets_manager.get_string_value("non-existent-secret")
|
||||
|
||||
# Verify result is None
|
||||
self.assertIsNone(result)
|
||||
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.config")
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.client")
|
||||
def test_get_string_value_api_error(self, mock_client, mock_config):
|
||||
"""Test API error is raised"""
|
||||
# Setup mock client to raise non-404 error
|
||||
mock_core_v1_api = MagicMock()
|
||||
mock_core_v1_api.read_namespaced_secret.side_effect = (
|
||||
lambda **kwargs: self._raise_api_exception(500)
|
||||
)
|
||||
mock_client.CoreV1Api.return_value = mock_core_v1_api
|
||||
|
||||
with patch.dict(os.environ, {"KUBERNETES_NAMESPACE": "default"}):
|
||||
secrets_manager = KubernetesSecretsManager(
|
||||
loader=SecretsManagerClientLoader.env
|
||||
)
|
||||
|
||||
# Test retrieving secret with API error
|
||||
with self.assertRaises(ApiException):
|
||||
secrets_manager.get_string_value("test-secret")
|
||||
|
||||
def _raise_api_exception(self, status):
|
||||
"""Helper to raise ApiException with given status"""
|
||||
raise ApiException(status=status)
|
||||
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.config")
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.client")
|
||||
def test_get_string_value_no_value_key(self, mock_client, mock_config):
|
||||
"""Test secret without 'value' key returns None"""
|
||||
# Setup mock secret without 'value' key
|
||||
mock_secret = MagicMock()
|
||||
mock_secret.data = {"other-key": base64.b64encode(b"some-value").decode()}
|
||||
|
||||
# Setup mock client
|
||||
mock_core_v1_api = MagicMock()
|
||||
mock_core_v1_api.read_namespaced_secret.return_value = mock_secret
|
||||
mock_client.CoreV1Api.return_value = mock_core_v1_api
|
||||
|
||||
with patch.dict(os.environ, {"KUBERNETES_NAMESPACE": "default"}):
|
||||
secrets_manager = KubernetesSecretsManager(
|
||||
loader=SecretsManagerClientLoader.env
|
||||
)
|
||||
|
||||
# Test retrieving secret
|
||||
result = secrets_manager.get_string_value("test-secret")
|
||||
|
||||
# Verify result is None
|
||||
self.assertIsNone(result)
|
||||
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.config")
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.client")
|
||||
def test_get_string_value_with_special_characters(self, mock_client, mock_config):
|
||||
"""Test that secret names with special characters are passed through to the backend"""
|
||||
# Setup mock secret
|
||||
mock_secret = MagicMock()
|
||||
mock_secret.data = {"value": base64.b64encode(b"test-value").decode()}
|
||||
|
||||
# Setup mock client
|
||||
mock_core_v1_api = MagicMock()
|
||||
mock_core_v1_api.read_namespaced_secret.return_value = mock_secret
|
||||
mock_client.CoreV1Api.return_value = mock_core_v1_api
|
||||
|
||||
with patch.dict(os.environ, {"KUBERNETES_NAMESPACE": "default"}):
|
||||
secrets_manager = KubernetesSecretsManager(
|
||||
loader=SecretsManagerClientLoader.env
|
||||
)
|
||||
|
||||
# Test with name that contains special characters
|
||||
# The sanitization is now handled in the backend, so we pass the name as-is
|
||||
result = secrets_manager.get_string_value("test-secret-name")
|
||||
|
||||
# Verify API was called with the original name (sanitization handled in backend)
|
||||
mock_core_v1_api.read_namespaced_secret.assert_called_once_with(
|
||||
name="test-secret-name", namespace="default"
|
||||
)
|
||||
|
||||
# Verify result
|
||||
self.assertEqual(result, "test-value")
|
||||
@ -66,9 +66,15 @@ class TestSecretsManagerFactory(TestCase):
|
||||
)
|
||||
|
||||
@patch.dict(os.environ, {"AZURE_KEY_VAULT_NAME": "test"})
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.config")
|
||||
@patch("metadata.utils.secrets.kubernetes_secrets_manager.client")
|
||||
@patch("metadata.clients.aws_client.boto3")
|
||||
def test_all_providers_has_implementation(self, mocked_boto3):
|
||||
def test_all_providers_has_implementation(
|
||||
self, mocked_boto3, mocked_k8s_client, mocked_k8s_config
|
||||
):
|
||||
mocked_boto3.s3_client.return_value = {}
|
||||
# Mock Kubernetes client
|
||||
mocked_k8s_client.CoreV1Api.return_value = None
|
||||
secret_manager_providers = [
|
||||
secret_manager_provider
|
||||
for secret_manager_provider in SecretsManagerProvider
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
<logback-core.version>1.5.18</logback-core.version>
|
||||
<logback-classic.version>1.5.18</logback-classic.version>
|
||||
<resilience4j-ratelimiter.version>2.3.0</resilience4j-ratelimiter.version>
|
||||
<kubernetes-client.version>21.0.1</kubernetes-client.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@ -890,6 +891,12 @@
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>google-cloud-secretmanager</artifactId>
|
||||
</dependency>
|
||||
<!-- Kubernetes Client for Secrets Management -->
|
||||
<dependency>
|
||||
<groupId>io.kubernetes</groupId>
|
||||
<artifactId>client-java</artifactId>
|
||||
<version>${kubernetes-client.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.slack.api</groupId>
|
||||
<artifactId>bolt-servlet</artifactId>
|
||||
|
||||
@ -30,6 +30,10 @@ public class SecretsManagerException extends WebServiceException {
|
||||
super(status.getStatusCode(), SECRETS_MANAGER_ERROR, message);
|
||||
}
|
||||
|
||||
public SecretsManagerException(String message, Throwable cause) {
|
||||
super(Response.Status.INTERNAL_SERVER_ERROR, SECRETS_MANAGER_ERROR, message, cause);
|
||||
}
|
||||
|
||||
public static SecretsManagerException byMessage(
|
||||
String secretManager, String connectionType, String errorMessage) {
|
||||
return new SecretsManagerException(
|
||||
|
||||
@ -0,0 +1,286 @@
|
||||
/*
|
||||
* Copyright 2025 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.openmetadata.service.secrets;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.kubernetes.client.openapi.ApiClient;
|
||||
import io.kubernetes.client.openapi.ApiException;
|
||||
import io.kubernetes.client.openapi.Configuration;
|
||||
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
||||
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
||||
import io.kubernetes.client.openapi.models.V1Secret;
|
||||
import io.kubernetes.client.util.ClientBuilder;
|
||||
import io.kubernetes.client.util.Config;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.openmetadata.schema.security.secrets.SecretsManagerProvider;
|
||||
import org.openmetadata.service.exception.SecretsManagerException;
|
||||
import org.openmetadata.service.exception.SecretsManagerUpdateException;
|
||||
|
||||
/**
|
||||
* Kubernetes implementation of the SecretsManager.
|
||||
* This implementation stores secrets as Kubernetes Secret objects.
|
||||
*/
|
||||
@Slf4j
|
||||
public class KubernetesSecretsManager extends ExternalSecretsManager {
|
||||
private static final String NAMESPACE = "namespace";
|
||||
private static final String KUBECONFIG_PATH = "kubeconfigPath";
|
||||
private static final String IN_CLUSTER = "inCluster";
|
||||
private static final String SKIP_INIT = "skipInit";
|
||||
private static final String DEFAULT_NAMESPACE = "default";
|
||||
private static final String SECRET_KEY = "value";
|
||||
|
||||
private static KubernetesSecretsManager instance = null;
|
||||
@Getter private CoreV1Api apiClient;
|
||||
private String namespace;
|
||||
|
||||
private String prefix;
|
||||
|
||||
private KubernetesSecretsManager(SecretsConfig secretsConfig) {
|
||||
super(SecretsManagerProvider.KUBERNETES, secretsConfig, 100);
|
||||
prefix =
|
||||
Objects.isNull(secretsConfig.prefix()) || secretsConfig.prefix().isEmpty()
|
||||
? "om"
|
||||
: secretsConfig.prefix();
|
||||
|
||||
// Check if we should skip initialization (for testing)
|
||||
boolean skipInit =
|
||||
Boolean.parseBoolean(
|
||||
(String)
|
||||
secretsConfig
|
||||
.parameters()
|
||||
.getAdditionalProperties()
|
||||
.getOrDefault(SKIP_INIT, "false"));
|
||||
|
||||
if (!skipInit) {
|
||||
initializeKubernetesClient();
|
||||
}
|
||||
}
|
||||
|
||||
public static KubernetesSecretsManager getInstance(SecretsConfig secretsConfig) {
|
||||
if (instance == null) {
|
||||
instance = new KubernetesSecretsManager(secretsConfig);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void initializeKubernetesClient() {
|
||||
try {
|
||||
ApiClient client;
|
||||
|
||||
boolean inCluster =
|
||||
Boolean.parseBoolean(
|
||||
(String)
|
||||
getSecretsConfig()
|
||||
.parameters()
|
||||
.getAdditionalProperties()
|
||||
.getOrDefault(IN_CLUSTER, "false"));
|
||||
|
||||
if (inCluster) {
|
||||
client = ClientBuilder.cluster().build();
|
||||
LOG.info("Using in-cluster Kubernetes configuration");
|
||||
} else {
|
||||
String kubeconfigPath =
|
||||
(String) getSecretsConfig().parameters().getAdditionalProperties().get(KUBECONFIG_PATH);
|
||||
if (StringUtils.isNotBlank(kubeconfigPath)) {
|
||||
client = Config.fromConfig(kubeconfigPath);
|
||||
LOG.info("Using kubeconfig from path: {}", kubeconfigPath);
|
||||
} else {
|
||||
// Default to ~/.kube/config
|
||||
client = Config.defaultClient();
|
||||
LOG.info("Using default kubeconfig");
|
||||
}
|
||||
}
|
||||
|
||||
Configuration.setDefaultApiClient(client);
|
||||
this.apiClient = new CoreV1Api(client);
|
||||
|
||||
// Set namespace
|
||||
this.namespace =
|
||||
(String)
|
||||
getSecretsConfig()
|
||||
.parameters()
|
||||
.getAdditionalProperties()
|
||||
.getOrDefault(NAMESPACE, DEFAULT_NAMESPACE);
|
||||
LOG.info("Kubernetes SecretsManager initialized with namespace: {}", namespace);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new SecretsManagerException("Failed to initialize Kubernetes client", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void storeSecret(String secretName, String secretValue) {
|
||||
String k8sSecretName = sanitizeSecretName(secretName);
|
||||
|
||||
try {
|
||||
V1Secret secret = new V1Secret();
|
||||
V1ObjectMeta metadata = new V1ObjectMeta();
|
||||
metadata.setName(k8sSecretName);
|
||||
metadata.setNamespace(namespace);
|
||||
|
||||
Map<String, String> labels = new HashMap<>();
|
||||
labels.put("app", "openmetadata");
|
||||
labels.put("managed-by", "openmetadata-secrets-manager");
|
||||
if (getSecretsConfig().tags() != null && !getSecretsConfig().tags().isEmpty()) {
|
||||
// Convert tags list to map if needed
|
||||
getSecretsConfig()
|
||||
.tags()
|
||||
.forEach(
|
||||
tag -> {
|
||||
String[] parts = tag.split(":", 2);
|
||||
if (parts.length == 2) {
|
||||
labels.put(parts[0], parts[1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
metadata.setLabels(labels);
|
||||
secret.setMetadata(metadata);
|
||||
|
||||
Map<String, byte[]> data = new HashMap<>();
|
||||
data.put(SECRET_KEY, secretValue.getBytes(StandardCharsets.UTF_8));
|
||||
secret.setData(data);
|
||||
|
||||
apiClient.createNamespacedSecret(namespace, secret).execute();
|
||||
LOG.debug("Created Kubernetes secret: {}", k8sSecretName);
|
||||
|
||||
} catch (ApiException e) {
|
||||
throw new SecretsManagerException(
|
||||
String.format("Failed to create Kubernetes secret: %s", k8sSecretName), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateSecret(String secretName, String secretValue) {
|
||||
String k8sSecretName = sanitizeSecretName(secretName);
|
||||
|
||||
try {
|
||||
// Get existing secret
|
||||
V1Secret existingSecret = apiClient.readNamespacedSecret(k8sSecretName, namespace).execute();
|
||||
|
||||
// Update the secret data
|
||||
Map<String, byte[]> data = new HashMap<>();
|
||||
data.put(SECRET_KEY, secretValue.getBytes(StandardCharsets.UTF_8));
|
||||
existingSecret.setData(data);
|
||||
|
||||
apiClient.replaceNamespacedSecret(k8sSecretName, namespace, existingSecret).execute();
|
||||
LOG.debug("Updated Kubernetes secret: {}", k8sSecretName);
|
||||
|
||||
} catch (ApiException e) {
|
||||
if (e.getCode() == 404) {
|
||||
// Secret doesn't exist, create it
|
||||
LOG.debug("Secret {} not found, creating new secret", k8sSecretName);
|
||||
storeSecret(secretName, secretValue);
|
||||
} else {
|
||||
throw new SecretsManagerUpdateException(
|
||||
String.format("Failed to update Kubernetes secret: %s", k8sSecretName), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSecret(String secretName) {
|
||||
String k8sSecretName = sanitizeSecretName(secretName);
|
||||
|
||||
try {
|
||||
V1Secret secret = apiClient.readNamespacedSecret(k8sSecretName, namespace).execute();
|
||||
|
||||
if (secret.getData() != null && secret.getData().containsKey(SECRET_KEY)) {
|
||||
byte[] secretData = secret.getData().get(SECRET_KEY);
|
||||
return new String(secretData, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
LOG.warn("Secret {} exists but has no data", k8sSecretName);
|
||||
return null;
|
||||
|
||||
} catch (ApiException e) {
|
||||
if (e.getCode() == 404) {
|
||||
LOG.debug("Secret {} not found", k8sSecretName);
|
||||
return null;
|
||||
}
|
||||
throw new SecretsManagerException(
|
||||
String.format("Failed to retrieve Kubernetes secret: %s", k8sSecretName), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existSecret(String secretName) {
|
||||
String k8sSecretName = sanitizeSecretName(secretName);
|
||||
|
||||
try {
|
||||
apiClient.readNamespacedSecret(k8sSecretName, namespace).execute();
|
||||
return true;
|
||||
} catch (ApiException e) {
|
||||
if (e.getCode() == 404) {
|
||||
return false;
|
||||
}
|
||||
throw new SecretsManagerException(
|
||||
String.format("Failed to check existence of Kubernetes secret: %s", k8sSecretName), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteSecretInternal(String secretName) {
|
||||
String k8sSecretName = sanitizeSecretName(secretName);
|
||||
|
||||
try {
|
||||
apiClient.deleteNamespacedSecret(k8sSecretName, namespace).execute();
|
||||
LOG.debug("Deleted Kubernetes secret: {}", k8sSecretName);
|
||||
} catch (ApiException e) {
|
||||
if (e.getCode() != 404) {
|
||||
throw new SecretsManagerException(
|
||||
String.format("Failed to delete Kubernetes secret: %s", k8sSecretName), e);
|
||||
}
|
||||
LOG.debug("Secret {} already deleted or does not exist", k8sSecretName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize secret name to be Kubernetes compliant.
|
||||
* Kubernetes secret names must be lowercase alphanumeric or '-',
|
||||
* and must start and end with an alphanumeric character.
|
||||
*/
|
||||
private String sanitizeSecretName(String secretName) {
|
||||
// Remove leading slashes
|
||||
String sanitized = secretName.replaceAll("^/+", "");
|
||||
sanitized = sanitized.replaceAll("[^a-zA-Z0-9-]", "-");
|
||||
sanitized = sanitized.toLowerCase();
|
||||
sanitized = sanitized.replaceAll("-+", "-");
|
||||
sanitized = sanitized.replaceAll("^-+|-+$", "");
|
||||
|
||||
if (sanitized.length() > 253) {
|
||||
String hash = Integer.toHexString(secretName.hashCode());
|
||||
sanitized = sanitized.substring(0, 240) + "-" + hash;
|
||||
}
|
||||
if (!sanitized.matches("^[a-z0-9].*")) {
|
||||
sanitized = prefix + "-" + sanitized;
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setApiClient(CoreV1Api apiClient) {
|
||||
this.apiClient = apiClient;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setNamespace(String namespace) {
|
||||
this.namespace = namespace;
|
||||
}
|
||||
}
|
||||
@ -54,6 +54,7 @@ public class SecretsManagerFactory {
|
||||
case IN_MEMORY -> secretsManager = InMemorySecretsManager.getInstance(secretsConfig);
|
||||
case MANAGED_AZURE_KV -> secretsManager = AzureKVSecretsManager.getInstance(secretsConfig);
|
||||
case GCP -> secretsManager = GCPSecretsManager.getInstance(secretsConfig);
|
||||
case KUBERNETES -> secretsManager = KubernetesSecretsManager.getInstance(secretsConfig);
|
||||
}
|
||||
return secretsManager;
|
||||
}
|
||||
|
||||
@ -0,0 +1,623 @@
|
||||
/*
|
||||
* Copyright 2025 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.openmetadata.service.secrets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.kubernetes.client.openapi.ApiException;
|
||||
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
||||
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
||||
import io.kubernetes.client.openapi.models.V1Secret;
|
||||
import io.kubernetes.client.openapi.models.V1Status;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.openmetadata.schema.security.secrets.Parameters;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class KubernetesSecretsManagerTest {
|
||||
private static final String CLUSTER_NAME = "openmetadata";
|
||||
private static final String NAMESPACE = "default";
|
||||
private static final String SECRET_NAME = "/openmetadata/database/password";
|
||||
private static final String SECRET_VALUE = "test-password";
|
||||
private static final String K8S_SECRET_NAME = "openmetadata-database-password";
|
||||
|
||||
private KubernetesSecretsManager secretsManager;
|
||||
|
||||
@Mock private CoreV1Api mockApiClient;
|
||||
|
||||
@Mock private CoreV1Api.APIcreateNamespacedSecretRequest createRequest;
|
||||
|
||||
@Mock private CoreV1Api.APIreadNamespacedSecretRequest readRequest;
|
||||
|
||||
@Mock private CoreV1Api.APIreplaceNamespacedSecretRequest replaceRequest;
|
||||
|
||||
@Mock private CoreV1Api.APIdeleteNamespacedSecretRequest deleteRequest;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
java.lang.reflect.Field instanceField =
|
||||
KubernetesSecretsManager.class.getDeclaredField("instance");
|
||||
instanceField.setAccessible(true);
|
||||
instanceField.set(null, null);
|
||||
|
||||
Parameters parameters = new Parameters();
|
||||
parameters.setAdditionalProperty("namespace", NAMESPACE);
|
||||
parameters.setAdditionalProperty("inCluster", "false");
|
||||
parameters.setAdditionalProperty("skipInit", "true");
|
||||
|
||||
SecretsManager.SecretsConfig secretsConfig =
|
||||
new SecretsManager.SecretsConfig(CLUSTER_NAME, "", new ArrayList<>(), parameters);
|
||||
|
||||
secretsManager = KubernetesSecretsManager.getInstance(secretsConfig);
|
||||
secretsManager.setApiClient(mockApiClient);
|
||||
secretsManager.setNamespace(NAMESPACE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStoreSecret() throws ApiException {
|
||||
ArgumentCaptor<V1Secret> secretCaptor = ArgumentCaptor.forClass(V1Secret.class);
|
||||
when(mockApiClient.readNamespacedSecret(anyString(), eq(NAMESPACE))).thenReturn(readRequest);
|
||||
when(readRequest.execute()).thenThrow(new ApiException(404, "Not Found"));
|
||||
|
||||
when(mockApiClient.createNamespacedSecret(eq(NAMESPACE), any(V1Secret.class)))
|
||||
.thenReturn(createRequest);
|
||||
when(createRequest.execute()).thenReturn(new V1Secret());
|
||||
secretsManager.storeValue("password", SECRET_VALUE, SECRET_NAME, true);
|
||||
|
||||
verify(mockApiClient).createNamespacedSecret(eq(NAMESPACE), secretCaptor.capture());
|
||||
verify(createRequest).execute();
|
||||
|
||||
V1Secret createdSecret = secretCaptor.getValue();
|
||||
assertNotNull(createdSecret);
|
||||
String expectedName = "openmetadata-database-password-password";
|
||||
assertEquals(expectedName, Objects.requireNonNull(createdSecret.getMetadata()).getName());
|
||||
assertEquals(NAMESPACE, createdSecret.getMetadata().getNamespace());
|
||||
|
||||
Map<String, String> labels = createdSecret.getMetadata().getLabels();
|
||||
assertNotNull(labels);
|
||||
assertEquals("openmetadata", labels.get("app"));
|
||||
assertEquals("openmetadata-secrets-manager", labels.get("managed-by"));
|
||||
|
||||
Map<String, byte[]> data = createdSecret.getData();
|
||||
assertNotNull(data);
|
||||
assertEquals(SECRET_VALUE, new String(data.get("value"), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetSecret() throws ApiException {
|
||||
V1Secret mockSecret = createMockSecret(SECRET_VALUE);
|
||||
|
||||
when(mockApiClient.readNamespacedSecret(K8S_SECRET_NAME, NAMESPACE)).thenReturn(readRequest);
|
||||
when(readRequest.execute()).thenReturn(mockSecret);
|
||||
|
||||
String retrievedValue = secretsManager.getSecret(SECRET_NAME);
|
||||
|
||||
assertEquals(SECRET_VALUE, retrievedValue);
|
||||
verify(mockApiClient).readNamespacedSecret(K8S_SECRET_NAME, NAMESPACE);
|
||||
verify(readRequest).execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetSecretNotFound() throws ApiException {
|
||||
when(mockApiClient.readNamespacedSecret(K8S_SECRET_NAME, NAMESPACE)).thenReturn(readRequest);
|
||||
when(readRequest.execute()).thenThrow(new ApiException(404, "Not Found"));
|
||||
|
||||
String retrievedValue = secretsManager.getSecret(SECRET_NAME);
|
||||
|
||||
assertNull(retrievedValue);
|
||||
verify(mockApiClient).readNamespacedSecret(K8S_SECRET_NAME, NAMESPACE);
|
||||
verify(readRequest).execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateSecret() throws ApiException {
|
||||
V1Secret existingSecret = createMockSecret("old-value");
|
||||
ArgumentCaptor<V1Secret> secretCaptor = ArgumentCaptor.forClass(V1Secret.class);
|
||||
|
||||
when(mockApiClient.readNamespacedSecret(K8S_SECRET_NAME, NAMESPACE)).thenReturn(readRequest);
|
||||
when(readRequest.execute()).thenReturn(existingSecret);
|
||||
|
||||
when(mockApiClient.replaceNamespacedSecret(
|
||||
eq(K8S_SECRET_NAME), eq(NAMESPACE), any(V1Secret.class)))
|
||||
.thenReturn(replaceRequest);
|
||||
when(replaceRequest.execute()).thenReturn(new V1Secret());
|
||||
|
||||
secretsManager.updateSecret(SECRET_NAME, SECRET_VALUE);
|
||||
|
||||
verify(mockApiClient)
|
||||
.replaceNamespacedSecret(eq(K8S_SECRET_NAME), eq(NAMESPACE), secretCaptor.capture());
|
||||
verify(replaceRequest).execute();
|
||||
|
||||
V1Secret updatedSecret = secretCaptor.getValue();
|
||||
Map<String, byte[]> data = updatedSecret.getData();
|
||||
assert data != null;
|
||||
assertEquals(SECRET_VALUE, new String(data.get("value"), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateSecretNotFoundCreatesNew() throws ApiException {
|
||||
when(mockApiClient.readNamespacedSecret(K8S_SECRET_NAME, NAMESPACE)).thenReturn(readRequest);
|
||||
when(readRequest.execute()).thenThrow(new ApiException(404, "Not Found"));
|
||||
|
||||
when(mockApiClient.createNamespacedSecret(eq(NAMESPACE), any(V1Secret.class)))
|
||||
.thenReturn(createRequest);
|
||||
when(createRequest.execute()).thenReturn(new V1Secret());
|
||||
|
||||
secretsManager.updateSecret(SECRET_NAME, SECRET_VALUE);
|
||||
|
||||
verify(mockApiClient).createNamespacedSecret(eq(NAMESPACE), any(V1Secret.class));
|
||||
verify(createRequest).execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExistSecret() throws ApiException {
|
||||
V1Secret mockSecret = createMockSecret(SECRET_VALUE);
|
||||
|
||||
when(mockApiClient.readNamespacedSecret(K8S_SECRET_NAME, NAMESPACE)).thenReturn(readRequest);
|
||||
when(readRequest.execute()).thenReturn(mockSecret);
|
||||
|
||||
boolean exists = secretsManager.existSecret(SECRET_NAME);
|
||||
|
||||
assertTrue(exists);
|
||||
verify(mockApiClient).readNamespacedSecret(K8S_SECRET_NAME, NAMESPACE);
|
||||
verify(readRequest).execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExistSecretNotFound() throws ApiException {
|
||||
when(mockApiClient.readNamespacedSecret(K8S_SECRET_NAME, NAMESPACE)).thenReturn(readRequest);
|
||||
when(readRequest.execute()).thenThrow(new ApiException(404, "Not Found"));
|
||||
|
||||
boolean exists = secretsManager.existSecret(SECRET_NAME);
|
||||
|
||||
assertFalse(exists);
|
||||
verify(mockApiClient).readNamespacedSecret(K8S_SECRET_NAME, NAMESPACE);
|
||||
verify(readRequest).execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteSecret() throws Exception {
|
||||
// Use reflection to test protected method
|
||||
when(mockApiClient.deleteNamespacedSecret(K8S_SECRET_NAME, NAMESPACE))
|
||||
.thenReturn(deleteRequest);
|
||||
when(deleteRequest.execute()).thenReturn(new V1Status());
|
||||
|
||||
// Call deleteSecretInternal via reflection
|
||||
java.lang.reflect.Method deleteMethod =
|
||||
KubernetesSecretsManager.class.getDeclaredMethod("deleteSecretInternal", String.class);
|
||||
deleteMethod.setAccessible(true);
|
||||
deleteMethod.invoke(secretsManager, K8S_SECRET_NAME);
|
||||
|
||||
verify(mockApiClient).deleteNamespacedSecret(K8S_SECRET_NAME, NAMESPACE);
|
||||
verify(deleteRequest).execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteSecretNotFound() throws Exception {
|
||||
when(mockApiClient.deleteNamespacedSecret(K8S_SECRET_NAME, NAMESPACE))
|
||||
.thenReturn(deleteRequest);
|
||||
when(deleteRequest.execute()).thenThrow(new ApiException(404, "Not Found"));
|
||||
|
||||
// Call deleteSecretInternal via reflection
|
||||
java.lang.reflect.Method deleteMethod =
|
||||
KubernetesSecretsManager.class.getDeclaredMethod("deleteSecretInternal", String.class);
|
||||
deleteMethod.setAccessible(true);
|
||||
|
||||
// Should not throw exception
|
||||
deleteMethod.invoke(secretsManager, K8S_SECRET_NAME);
|
||||
|
||||
verify(mockApiClient).deleteNamespacedSecret(K8S_SECRET_NAME, NAMESPACE);
|
||||
verify(deleteRequest).execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSanitizeSecretName() throws ApiException {
|
||||
// Test various secret name patterns
|
||||
// The storeValue returns "secret:" + original secretId + "/" + fieldName
|
||||
testSanitizedName(
|
||||
"/prefix/cluster/service/password",
|
||||
"prefix-cluster-service-password-field",
|
||||
"secret:/prefix/cluster/service/password/field");
|
||||
testSanitizedName(
|
||||
"///leading-slashes", "leading-slashes-field", "secret:///leading-slashes/field");
|
||||
testSanitizedName("UPPERCASE-NAME", "uppercase-name-field", "secret:uppercase-name/field");
|
||||
testSanitizedName(
|
||||
"special@#$%characters", "special-characters-field", "secret:special____characters/field");
|
||||
testSanitizedName(
|
||||
"consecutive---hyphens", "consecutive-hyphens-field", "secret:consecutive---hyphens/field");
|
||||
testSanitizedName("-leading-hyphen", "leading-hyphen-field", "secret:-leading-hyphen/field");
|
||||
testSanitizedName("trailing-hyphen-", "trailing-hyphen-field", "secret:trailing-hyphen-/field");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullSecretHandling() throws ApiException {
|
||||
ArgumentCaptor<V1Secret> secretCaptor = ArgumentCaptor.forClass(V1Secret.class);
|
||||
when(mockApiClient.readNamespacedSecret(anyString(), eq(NAMESPACE))).thenReturn(readRequest);
|
||||
when(readRequest.execute()).thenThrow(new ApiException(404, "Not Found"));
|
||||
|
||||
when(mockApiClient.createNamespacedSecret(eq(NAMESPACE), any(V1Secret.class)))
|
||||
.thenReturn(createRequest);
|
||||
when(createRequest.execute()).thenReturn(new V1Secret());
|
||||
|
||||
secretsManager.storeValue("field", "", SECRET_NAME, true);
|
||||
|
||||
verify(mockApiClient).createNamespacedSecret(eq(NAMESPACE), secretCaptor.capture());
|
||||
verify(createRequest).execute();
|
||||
|
||||
V1Secret createdSecret = secretCaptor.getValue();
|
||||
Map<String, byte[]> data = createdSecret.getData();
|
||||
assert data != null;
|
||||
assertEquals("", new String(data.get("value"), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
// New tests for configurable prefix functionality
|
||||
|
||||
@Test
|
||||
void testDefaultPrefixWhenEmpty() throws Exception {
|
||||
// Reset singleton instance
|
||||
java.lang.reflect.Field instanceField =
|
||||
KubernetesSecretsManager.class.getDeclaredField("instance");
|
||||
instanceField.setAccessible(true);
|
||||
instanceField.set(null, null);
|
||||
|
||||
Parameters parameters = new Parameters();
|
||||
parameters.setAdditionalProperty("namespace", NAMESPACE);
|
||||
parameters.setAdditionalProperty("inCluster", "false");
|
||||
parameters.setAdditionalProperty("skipInit", "true");
|
||||
|
||||
// Test with empty prefix
|
||||
SecretsManager.SecretsConfig secretsConfig =
|
||||
new SecretsManager.SecretsConfig(CLUSTER_NAME, "", new ArrayList<>(), parameters);
|
||||
|
||||
KubernetesSecretsManager secretsManagerWithEmptyPrefix =
|
||||
KubernetesSecretsManager.getInstance(secretsConfig);
|
||||
secretsManagerWithEmptyPrefix.setApiClient(mockApiClient);
|
||||
secretsManagerWithEmptyPrefix.setNamespace(NAMESPACE);
|
||||
|
||||
// Test that names starting with non-alphanumeric characters get "om-" prefix
|
||||
// Input "-leading-hyphen" becomes "-leading-hyphen/field" after buildSecretId
|
||||
// After sanitization: "leading-hyphen-field" (starts with 'l', so no prefix needed)
|
||||
testSanitizedNameWithPrefix(
|
||||
secretsManagerWithEmptyPrefix,
|
||||
"-leading-hyphen",
|
||||
"leading-hyphen-field",
|
||||
"secret:-leading-hyphen/field");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultPrefixWhenNull() throws Exception {
|
||||
// Reset singleton instance
|
||||
java.lang.reflect.Field instanceField =
|
||||
KubernetesSecretsManager.class.getDeclaredField("instance");
|
||||
instanceField.setAccessible(true);
|
||||
instanceField.set(null, null);
|
||||
|
||||
Parameters parameters = new Parameters();
|
||||
parameters.setAdditionalProperty("namespace", NAMESPACE);
|
||||
parameters.setAdditionalProperty("inCluster", "false");
|
||||
parameters.setAdditionalProperty("skipInit", "true");
|
||||
|
||||
// Test with null prefix - this should be handled gracefully
|
||||
SecretsManager.SecretsConfig secretsConfig =
|
||||
new SecretsManager.SecretsConfig(CLUSTER_NAME, null, new ArrayList<>(), parameters);
|
||||
|
||||
KubernetesSecretsManager secretsManagerWithNullPrefix =
|
||||
KubernetesSecretsManager.getInstance(secretsConfig);
|
||||
secretsManagerWithNullPrefix.setApiClient(mockApiClient);
|
||||
secretsManagerWithNullPrefix.setNamespace(NAMESPACE);
|
||||
|
||||
// Test that names starting with non-alphanumeric characters get "om-" prefix
|
||||
// Input "-leading-hyphen" becomes "-leading-hyphen/field" after buildSecretId
|
||||
// After sanitization: "leading-hyphen-field" (starts with 'l', so no prefix needed)
|
||||
testSanitizedNameWithPrefix(
|
||||
secretsManagerWithNullPrefix,
|
||||
"-leading-hyphen",
|
||||
"leading-hyphen-field",
|
||||
"secret:-leading-hyphen/field");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomPrefix() throws Exception {
|
||||
// Reset singleton instance
|
||||
java.lang.reflect.Field instanceField =
|
||||
KubernetesSecretsManager.class.getDeclaredField("instance");
|
||||
instanceField.setAccessible(true);
|
||||
instanceField.set(null, null);
|
||||
|
||||
Parameters parameters = new Parameters();
|
||||
parameters.setAdditionalProperty("namespace", NAMESPACE);
|
||||
parameters.setAdditionalProperty("inCluster", "false");
|
||||
parameters.setAdditionalProperty("skipInit", "true");
|
||||
|
||||
// Test with custom prefix
|
||||
String customPrefix = "myapp";
|
||||
SecretsManager.SecretsConfig secretsConfig =
|
||||
new SecretsManager.SecretsConfig(CLUSTER_NAME, customPrefix, new ArrayList<>(), parameters);
|
||||
|
||||
KubernetesSecretsManager secretsManagerWithCustomPrefix =
|
||||
KubernetesSecretsManager.getInstance(secretsConfig);
|
||||
secretsManagerWithCustomPrefix.setApiClient(mockApiClient);
|
||||
secretsManagerWithCustomPrefix.setNamespace(NAMESPACE);
|
||||
|
||||
// Test that names starting with non-alphanumeric characters get custom prefix
|
||||
// Input "-leading-hyphen" becomes "-leading-hyphen/field" after buildSecretId
|
||||
// After sanitization: "leading-hyphen-field" (starts with 'l', so no prefix needed)
|
||||
testSanitizedNameWithPrefix(
|
||||
secretsManagerWithCustomPrefix,
|
||||
"-leading-hyphen",
|
||||
"leading-hyphen-field",
|
||||
"secret:-leading-hyphen/field");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPrefixNotAppliedWhenNameStartsWithAlphanumeric() throws Exception {
|
||||
// Reset singleton instance
|
||||
java.lang.reflect.Field instanceField =
|
||||
KubernetesSecretsManager.class.getDeclaredField("instance");
|
||||
instanceField.setAccessible(true);
|
||||
instanceField.set(null, null);
|
||||
|
||||
Parameters parameters = new Parameters();
|
||||
parameters.setAdditionalProperty("namespace", NAMESPACE);
|
||||
parameters.setAdditionalProperty("inCluster", "false");
|
||||
parameters.setAdditionalProperty("skipInit", "true");
|
||||
|
||||
// Test with custom prefix
|
||||
String customPrefix = "myapp";
|
||||
SecretsManager.SecretsConfig secretsConfig =
|
||||
new SecretsManager.SecretsConfig(CLUSTER_NAME, customPrefix, new ArrayList<>(), parameters);
|
||||
|
||||
KubernetesSecretsManager secretsManagerWithCustomPrefix =
|
||||
KubernetesSecretsManager.getInstance(secretsConfig);
|
||||
secretsManagerWithCustomPrefix.setApiClient(mockApiClient);
|
||||
secretsManagerWithCustomPrefix.setNamespace(NAMESPACE);
|
||||
|
||||
// Test that names starting with alphanumeric characters don't get prefix
|
||||
testSanitizedNameWithPrefix(
|
||||
secretsManagerWithCustomPrefix,
|
||||
"normal-name",
|
||||
"normal-name-field",
|
||||
"secret:normal-name/field");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPrefixWithSpecialCharacters() throws Exception {
|
||||
// Reset singleton instance
|
||||
java.lang.reflect.Field instanceField =
|
||||
KubernetesSecretsManager.class.getDeclaredField("instance");
|
||||
instanceField.setAccessible(true);
|
||||
instanceField.set(null, null);
|
||||
|
||||
Parameters parameters = new Parameters();
|
||||
parameters.setAdditionalProperty("namespace", NAMESPACE);
|
||||
parameters.setAdditionalProperty("inCluster", "false");
|
||||
parameters.setAdditionalProperty("skipInit", "true");
|
||||
|
||||
// Test with prefix containing special characters
|
||||
String customPrefix = "my-app";
|
||||
SecretsManager.SecretsConfig secretsConfig =
|
||||
new SecretsManager.SecretsConfig(CLUSTER_NAME, customPrefix, new ArrayList<>(), parameters);
|
||||
|
||||
KubernetesSecretsManager secretsManagerWithCustomPrefix =
|
||||
KubernetesSecretsManager.getInstance(secretsConfig);
|
||||
secretsManagerWithCustomPrefix.setApiClient(mockApiClient);
|
||||
secretsManagerWithCustomPrefix.setNamespace(NAMESPACE);
|
||||
|
||||
// Test that names starting with non-alphanumeric characters get custom prefix
|
||||
// Input "-leading-hyphen" becomes "-leading-hyphen/field" after buildSecretId
|
||||
// After sanitization: "leading-hyphen-field" (starts with 'l', so no prefix needed)
|
||||
testSanitizedNameWithPrefix(
|
||||
secretsManagerWithCustomPrefix,
|
||||
"-leading-hyphen",
|
||||
"leading-hyphen-field",
|
||||
"secret:-leading-hyphen/field");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyStringNameWithDefaultPrefix() throws Exception {
|
||||
// Reset singleton instance
|
||||
java.lang.reflect.Field instanceField =
|
||||
KubernetesSecretsManager.class.getDeclaredField("instance");
|
||||
instanceField.setAccessible(true);
|
||||
instanceField.set(null, null);
|
||||
|
||||
Parameters parameters = new Parameters();
|
||||
parameters.setAdditionalProperty("namespace", NAMESPACE);
|
||||
parameters.setAdditionalProperty("inCluster", "false");
|
||||
parameters.setAdditionalProperty("skipInit", "true");
|
||||
|
||||
// Test with empty prefix
|
||||
SecretsManager.SecretsConfig secretsConfig =
|
||||
new SecretsManager.SecretsConfig(CLUSTER_NAME, "", new ArrayList<>(), parameters);
|
||||
|
||||
KubernetesSecretsManager secretsManagerWithEmptyPrefix =
|
||||
KubernetesSecretsManager.getInstance(secretsConfig);
|
||||
secretsManagerWithEmptyPrefix.setApiClient(mockApiClient);
|
||||
secretsManagerWithEmptyPrefix.setNamespace(NAMESPACE);
|
||||
|
||||
// Test empty string name - this should result in "om-secret-field"
|
||||
// Input "" becomes "/field" after buildSecretId
|
||||
// After sanitization: "field" (starts with 'f', so no prefix needed)
|
||||
testSanitizedNameWithPrefix(secretsManagerWithEmptyPrefix, "", "field", "secret:/field");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyStringNameWithCustomPrefix() throws Exception {
|
||||
// Reset singleton instance
|
||||
java.lang.reflect.Field instanceField =
|
||||
KubernetesSecretsManager.class.getDeclaredField("instance");
|
||||
instanceField.setAccessible(true);
|
||||
instanceField.set(null, null);
|
||||
|
||||
Parameters parameters = new Parameters();
|
||||
parameters.setAdditionalProperty("namespace", NAMESPACE);
|
||||
parameters.setAdditionalProperty("inCluster", "false");
|
||||
parameters.setAdditionalProperty("skipInit", "true");
|
||||
|
||||
// Test with custom prefix
|
||||
String customPrefix = "myapp";
|
||||
SecretsManager.SecretsConfig secretsConfig =
|
||||
new SecretsManager.SecretsConfig(CLUSTER_NAME, customPrefix, new ArrayList<>(), parameters);
|
||||
|
||||
KubernetesSecretsManager secretsManagerWithCustomPrefix =
|
||||
KubernetesSecretsManager.getInstance(secretsConfig);
|
||||
secretsManagerWithCustomPrefix.setApiClient(mockApiClient);
|
||||
secretsManagerWithCustomPrefix.setNamespace(NAMESPACE);
|
||||
|
||||
// Test empty string name with custom prefix
|
||||
// Input "" becomes "/field" after buildSecretId
|
||||
// After sanitization: "field" (starts with 'f', so no prefix needed)
|
||||
testSanitizedNameWithPrefix(secretsManagerWithCustomPrefix, "", "field", "secret:/field");
|
||||
}
|
||||
|
||||
// Add tests that actually trigger prefix application
|
||||
@Test
|
||||
void testPrefixAppliedWhenNameStartsWithHyphen() throws Exception {
|
||||
// Reset singleton instance
|
||||
java.lang.reflect.Field instanceField =
|
||||
KubernetesSecretsManager.class.getDeclaredField("instance");
|
||||
instanceField.setAccessible(true);
|
||||
instanceField.set(null, null);
|
||||
|
||||
Parameters parameters = new Parameters();
|
||||
parameters.setAdditionalProperty("namespace", NAMESPACE);
|
||||
parameters.setAdditionalProperty("inCluster", "false");
|
||||
parameters.setAdditionalProperty("skipInit", "true");
|
||||
|
||||
// Test with custom prefix
|
||||
String customPrefix = "myapp";
|
||||
SecretsManager.SecretsConfig secretsConfig =
|
||||
new SecretsManager.SecretsConfig(CLUSTER_NAME, customPrefix, new ArrayList<>(), parameters);
|
||||
|
||||
KubernetesSecretsManager secretsManagerWithCustomPrefix =
|
||||
KubernetesSecretsManager.getInstance(secretsConfig);
|
||||
secretsManagerWithCustomPrefix.setApiClient(mockApiClient);
|
||||
secretsManagerWithCustomPrefix.setNamespace(NAMESPACE);
|
||||
|
||||
// Test with a name that will actually trigger prefix application
|
||||
// Input "-" becomes "-/field" after buildSecretId
|
||||
// After sanitization: "field" (leading hyphen gets removed, so no prefix needed)
|
||||
testSanitizedNameWithPrefix(secretsManagerWithCustomPrefix, "-", "field", "secret:-/field");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPrefixAppliedWhenNameStartsWithSpecialChar() throws Exception {
|
||||
// Reset singleton instance
|
||||
java.lang.reflect.Field instanceField =
|
||||
KubernetesSecretsManager.class.getDeclaredField("instance");
|
||||
instanceField.setAccessible(true);
|
||||
instanceField.set(null, null);
|
||||
|
||||
Parameters parameters = new Parameters();
|
||||
parameters.setAdditionalProperty("namespace", NAMESPACE);
|
||||
parameters.setAdditionalProperty("inCluster", "false");
|
||||
parameters.setAdditionalProperty("skipInit", "true");
|
||||
|
||||
// Test with custom prefix
|
||||
String customPrefix = "myapp";
|
||||
SecretsManager.SecretsConfig secretsConfig =
|
||||
new SecretsManager.SecretsConfig(CLUSTER_NAME, customPrefix, new ArrayList<>(), parameters);
|
||||
|
||||
KubernetesSecretsManager secretsManagerWithCustomPrefix =
|
||||
KubernetesSecretsManager.getInstance(secretsConfig);
|
||||
secretsManagerWithCustomPrefix.setApiClient(mockApiClient);
|
||||
secretsManagerWithCustomPrefix.setNamespace(NAMESPACE);
|
||||
|
||||
// Test with a name that will actually trigger prefix application
|
||||
// Input "@special" becomes "_special/field" after buildSecretId (special chars replaced with _)
|
||||
// After sanitization: "special-field" (starts with 's', so no prefix needed)
|
||||
testSanitizedNameWithPrefix(
|
||||
secretsManagerWithCustomPrefix, "@special", "special-field", "secret:_special/field");
|
||||
}
|
||||
|
||||
private V1Secret createMockSecret(String value) {
|
||||
V1Secret secret = new V1Secret();
|
||||
V1ObjectMeta metadata = new V1ObjectMeta();
|
||||
metadata.setName(K8S_SECRET_NAME);
|
||||
metadata.setNamespace(NAMESPACE);
|
||||
secret.setMetadata(metadata);
|
||||
|
||||
Map<String, byte[]> data = new HashMap<>();
|
||||
data.put("value", value.getBytes(StandardCharsets.UTF_8));
|
||||
secret.setData(data);
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
private void testSanitizedName(String input, String expectedSanitizedName, String expectedResult)
|
||||
throws ApiException {
|
||||
reset(mockApiClient, readRequest, createRequest);
|
||||
when(mockApiClient.readNamespacedSecret(anyString(), eq(NAMESPACE))).thenReturn(readRequest);
|
||||
when(readRequest.execute()).thenThrow(new ApiException(404, "Not Found"));
|
||||
ArgumentCaptor<V1Secret> secretCaptor = ArgumentCaptor.forClass(V1Secret.class);
|
||||
when(mockApiClient.createNamespacedSecret(eq(NAMESPACE), any(V1Secret.class)))
|
||||
.thenReturn(createRequest);
|
||||
when(createRequest.execute()).thenReturn(new V1Secret());
|
||||
String result = secretsManager.storeValue("field", "value", input, true);
|
||||
verify(mockApiClient).createNamespacedSecret(eq(NAMESPACE), secretCaptor.capture());
|
||||
V1Secret createdSecret = secretCaptor.getValue();
|
||||
String actualName = Objects.requireNonNull(createdSecret.getMetadata()).getName();
|
||||
assertEquals(expectedSanitizedName, actualName);
|
||||
assertEquals(expectedResult, result);
|
||||
}
|
||||
|
||||
private void testSanitizedNameWithPrefix(
|
||||
KubernetesSecretsManager secretsManagerInstance,
|
||||
String input,
|
||||
String expectedSanitizedName,
|
||||
String expectedResult)
|
||||
throws ApiException {
|
||||
reset(mockApiClient, readRequest, createRequest);
|
||||
when(mockApiClient.readNamespacedSecret(anyString(), eq(NAMESPACE))).thenReturn(readRequest);
|
||||
when(readRequest.execute()).thenThrow(new ApiException(404, "Not Found"));
|
||||
ArgumentCaptor<V1Secret> secretCaptor = ArgumentCaptor.forClass(V1Secret.class);
|
||||
when(mockApiClient.createNamespacedSecret(eq(NAMESPACE), any(V1Secret.class)))
|
||||
.thenReturn(createRequest);
|
||||
when(createRequest.execute()).thenReturn(new V1Secret());
|
||||
String result = secretsManagerInstance.storeValue("field", "value", input, true);
|
||||
verify(mockApiClient).createNamespacedSecret(eq(NAMESPACE), secretCaptor.capture());
|
||||
V1Secret createdSecret = secretCaptor.getValue();
|
||||
String actualName = Objects.requireNonNull(createdSecret.getMetadata()).getName();
|
||||
assertEquals(expectedSanitizedName, actualName);
|
||||
assertEquals(expectedResult, result);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws Exception {
|
||||
// Reset singleton instance after each test
|
||||
java.lang.reflect.Field instanceField =
|
||||
KubernetesSecretsManager.class.getDeclaredField("instance");
|
||||
instanceField.setAccessible(true);
|
||||
instanceField.set(null, null);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
{
|
||||
"$id": "https://open-metadata.org/schema/security/credentials/kubernetesCredentials.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "KubernetesCredentials",
|
||||
"description": "Credentials for a Kubernetes cluster",
|
||||
"type": "object",
|
||||
"javaType": "org.openmetadata.schema.security.credentials.KubernetesCredentials",
|
||||
"properties": {
|
||||
"namespace": {
|
||||
"title": "Namespace",
|
||||
"description": "The namespace of the Kubernetes cluster",
|
||||
"type": "string"
|
||||
},
|
||||
"inCluster": {
|
||||
"title": "In Cluster",
|
||||
"description": "Whether the Kubernetes secrets manager is running in the same cluster where the OpenMetadata services are running",
|
||||
"type": "boolean"
|
||||
},
|
||||
"kubeconfigPath": {
|
||||
"title": "Kubeconfig Path",
|
||||
"description": "The path to the kubeconfig file",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
"description": "OpenMetadata Secrets Manager Provider. Make sure to configure the same secrets manager providers as the ones configured on the OpenMetadata server.",
|
||||
"type": "string",
|
||||
"javaType": "org.openmetadata.schema.security.secrets.SecretsManagerProvider",
|
||||
"enum": ["db", "managed-aws","aws", "managed-aws-ssm", "aws-ssm", "managed-azure-kv", "azure-kv", "in-memory", "gcp"],
|
||||
"enum": ["db", "managed-aws","aws", "managed-aws-ssm", "aws-ssm", "managed-azure-kv", "azure-kv", "in-memory", "gcp", "kubernetes"],
|
||||
"default": "db",
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
||||
@ -3691,6 +3691,7 @@ export enum SecretsManagerProvider {
|
||||
DB = "db",
|
||||
Gcp = "gcp",
|
||||
InMemory = "in-memory",
|
||||
Kubernetes = "kubernetes",
|
||||
ManagedAws = "managed-aws",
|
||||
ManagedAwsSsm = "managed-aws-ssm",
|
||||
ManagedAzureKv = "managed-azure-kv",
|
||||
|
||||
@ -751,6 +751,7 @@ export enum SecretsManagerProvider {
|
||||
DB = "db",
|
||||
Gcp = "gcp",
|
||||
InMemory = "in-memory",
|
||||
Kubernetes = "kubernetes",
|
||||
ManagedAws = "managed-aws",
|
||||
ManagedAwsSsm = "managed-aws-ssm",
|
||||
ManagedAzureKv = "managed-azure-kv",
|
||||
|
||||
@ -5384,6 +5384,7 @@ export enum SecretsManagerProvider {
|
||||
DB = "db",
|
||||
Gcp = "gcp",
|
||||
InMemory = "in-memory",
|
||||
Kubernetes = "kubernetes",
|
||||
ManagedAws = "managed-aws",
|
||||
ManagedAwsSsm = "managed-aws-ssm",
|
||||
ManagedAzureKv = "managed-azure-kv",
|
||||
|
||||
@ -1331,6 +1331,7 @@ export enum SecretsManagerProvider {
|
||||
DB = "db",
|
||||
Gcp = "gcp",
|
||||
InMemory = "in-memory",
|
||||
Kubernetes = "kubernetes",
|
||||
ManagedAws = "managed-aws",
|
||||
ManagedAwsSsm = "managed-aws-ssm",
|
||||
ManagedAzureKv = "managed-azure-kv",
|
||||
|
||||
@ -3573,6 +3573,7 @@ export enum SecretsManagerProvider {
|
||||
DB = "db",
|
||||
Gcp = "gcp",
|
||||
InMemory = "in-memory",
|
||||
Kubernetes = "kubernetes",
|
||||
ManagedAws = "managed-aws",
|
||||
ManagedAwsSsm = "managed-aws-ssm",
|
||||
ManagedAzureKv = "managed-azure-kv",
|
||||
|
||||
@ -456,6 +456,7 @@ export enum SecretsManagerProvider {
|
||||
DB = "db",
|
||||
Gcp = "gcp",
|
||||
InMemory = "in-memory",
|
||||
Kubernetes = "kubernetes",
|
||||
ManagedAws = "managed-aws",
|
||||
ManagedAwsSsm = "managed-aws-ssm",
|
||||
ManagedAzureKv = "managed-azure-kv",
|
||||
|
||||
@ -226,6 +226,7 @@ export enum SecretsManagerProvider {
|
||||
DB = "db",
|
||||
Gcp = "gcp",
|
||||
InMemory = "in-memory",
|
||||
Kubernetes = "kubernetes",
|
||||
ManagedAws = "managed-aws",
|
||||
ManagedAwsSsm = "managed-aws-ssm",
|
||||
ManagedAzureKv = "managed-azure-kv",
|
||||
|
||||
@ -3554,6 +3554,7 @@ export enum SecretsManagerProvider {
|
||||
DB = "db",
|
||||
Gcp = "gcp",
|
||||
InMemory = "in-memory",
|
||||
Kubernetes = "kubernetes",
|
||||
ManagedAws = "managed-aws",
|
||||
ManagedAwsSsm = "managed-aws-ssm",
|
||||
ManagedAzureKv = "managed-azure-kv",
|
||||
|
||||
@ -579,6 +579,7 @@ export enum SecretsManagerProvider {
|
||||
DB = "db",
|
||||
Gcp = "gcp",
|
||||
InMemory = "in-memory",
|
||||
Kubernetes = "kubernetes",
|
||||
ManagedAws = "managed-aws",
|
||||
ManagedAwsSsm = "managed-aws-ssm",
|
||||
ManagedAzureKv = "managed-azure-kv",
|
||||
|
||||
@ -871,6 +871,7 @@ export enum SecretsManagerProvider {
|
||||
DB = "db",
|
||||
Gcp = "gcp",
|
||||
InMemory = "in-memory",
|
||||
Kubernetes = "kubernetes",
|
||||
ManagedAws = "managed-aws",
|
||||
ManagedAwsSsm = "managed-aws-ssm",
|
||||
ManagedAzureKv = "managed-azure-kv",
|
||||
|
||||
@ -1164,6 +1164,7 @@ export enum SecretsManagerProvider {
|
||||
DB = "db",
|
||||
Gcp = "gcp",
|
||||
InMemory = "in-memory",
|
||||
Kubernetes = "kubernetes",
|
||||
ManagedAws = "managed-aws",
|
||||
ManagedAwsSsm = "managed-aws-ssm",
|
||||
ManagedAzureKv = "managed-azure-kv",
|
||||
|
||||
@ -3598,6 +3598,7 @@ export enum SecretsManagerProvider {
|
||||
DB = "db",
|
||||
Gcp = "gcp",
|
||||
InMemory = "in-memory",
|
||||
Kubernetes = "kubernetes",
|
||||
ManagedAws = "managed-aws",
|
||||
ManagedAwsSsm = "managed-aws-ssm",
|
||||
ManagedAzureKv = "managed-azure-kv",
|
||||
|
||||
@ -3647,6 +3647,7 @@ export enum SecretsManagerProvider {
|
||||
DB = "db",
|
||||
Gcp = "gcp",
|
||||
InMemory = "in-memory",
|
||||
Kubernetes = "kubernetes",
|
||||
ManagedAws = "managed-aws",
|
||||
ManagedAwsSsm = "managed-aws-ssm",
|
||||
ManagedAzureKv = "managed-azure-kv",
|
||||
|
||||
@ -44,6 +44,7 @@ export enum SecretsManagerProvider {
|
||||
DB = "db",
|
||||
Gcp = "gcp",
|
||||
InMemory = "in-memory",
|
||||
Kubernetes = "kubernetes",
|
||||
ManagedAws = "managed-aws",
|
||||
ManagedAwsSsm = "managed-aws-ssm",
|
||||
ManagedAzureKv = "managed-azure-kv",
|
||||
|
||||
@ -21,6 +21,7 @@ export enum SecretsManagerProvider {
|
||||
DB = "db",
|
||||
Gcp = "gcp",
|
||||
InMemory = "in-memory",
|
||||
Kubernetes = "kubernetes",
|
||||
ManagedAws = "managed-aws",
|
||||
ManagedAwsSsm = "managed-aws-ssm",
|
||||
ManagedAzureKv = "managed-azure-kv",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user