outposts: support json patch for Kubernetes (#6319)
This commit is contained in:
parent
a728dad166
commit
d435a65cfd
|
@ -1,16 +1,20 @@
|
||||||
"""Base Kubernetes Reconciler"""
|
"""Base Kubernetes Reconciler"""
|
||||||
|
from json import dumps
|
||||||
from typing import TYPE_CHECKING, Generic, Optional, TypeVar
|
from typing import TYPE_CHECKING, Generic, Optional, TypeVar
|
||||||
|
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from kubernetes.client import V1ObjectMeta
|
from jsonpatch import JsonPatchConflict, JsonPatchException, JsonPatchTestFailed, apply_patch
|
||||||
|
from kubernetes.client import ApiClient, V1ObjectMeta
|
||||||
from kubernetes.client.exceptions import ApiException, OpenApiException
|
from kubernetes.client.exceptions import ApiException, OpenApiException
|
||||||
from kubernetes.client.models.v1_deployment import V1Deployment
|
from kubernetes.client.models.v1_deployment import V1Deployment
|
||||||
from kubernetes.client.models.v1_pod import V1Pod
|
from kubernetes.client.models.v1_pod import V1Pod
|
||||||
|
from requests import Response
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
from urllib3.exceptions import HTTPError
|
from urllib3.exceptions import HTTPError
|
||||||
|
|
||||||
from authentik import __version__
|
from authentik import __version__
|
||||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||||
|
from authentik.outposts.controllers.base import ControllerException
|
||||||
from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate
|
from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -34,11 +38,23 @@ class KubernetesObjectReconciler(Generic[T]):
|
||||||
self.namespace = controller.outpost.config.kubernetes_namespace
|
self.namespace = controller.outpost.config.kubernetes_namespace
|
||||||
self.logger = get_logger().bind(type=self.__class__.__name__)
|
self.logger = get_logger().bind(type=self.__class__.__name__)
|
||||||
|
|
||||||
|
def get_patch(self):
|
||||||
|
"""Get any patches that apply to this CRD"""
|
||||||
|
patches = self.controller.outpost.config.kubernetes_json_patches
|
||||||
|
if not patches:
|
||||||
|
return None
|
||||||
|
return patches.get(self.name, None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_embedded(self) -> bool:
|
def is_embedded(self) -> bool:
|
||||||
"""Return true if the current outpost is embedded"""
|
"""Return true if the current outpost is embedded"""
|
||||||
return self.controller.outpost.managed == MANAGED_OUTPOST
|
return self.controller.outpost.managed == MANAGED_OUTPOST
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reconciler_name() -> str:
|
||||||
|
"""A name this reconciler is identified by in the configuration"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def noop(self) -> bool:
|
def noop(self) -> bool:
|
||||||
"""Return true if this object should not be created/updated/deleted in this cluster"""
|
"""Return true if this object should not be created/updated/deleted in this cluster"""
|
||||||
|
@ -55,6 +71,23 @@ class KubernetesObjectReconciler(Generic[T]):
|
||||||
}
|
}
|
||||||
).lower()
|
).lower()
|
||||||
|
|
||||||
|
def get_patched_reference_object(self) -> T:
|
||||||
|
"""Get patched reference object"""
|
||||||
|
reference = self.get_reference_object()
|
||||||
|
patch = self.get_patch()
|
||||||
|
v1deploy_json = ApiClient().sanitize_for_serialization(reference)
|
||||||
|
try:
|
||||||
|
if patch is not None:
|
||||||
|
ref_v1deploy = apply_patch(v1deploy_json, patch)
|
||||||
|
else:
|
||||||
|
ref_v1deploy = v1deploy_json
|
||||||
|
except (JsonPatchException, JsonPatchConflict, JsonPatchTestFailed) as exc:
|
||||||
|
raise ControllerException(f"JSON Patch failed: {exc}") from exc
|
||||||
|
mock_response = Response()
|
||||||
|
mock_response.data = dumps(ref_v1deploy)
|
||||||
|
|
||||||
|
return ApiClient().deserialize(mock_response, reference.__class__.__name__)
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
def up(self):
|
def up(self):
|
||||||
"""Create object if it doesn't exist, update if needed or recreate if needed."""
|
"""Create object if it doesn't exist, update if needed or recreate if needed."""
|
||||||
|
@ -62,7 +95,7 @@ class KubernetesObjectReconciler(Generic[T]):
|
||||||
if self.noop:
|
if self.noop:
|
||||||
self.logger.debug("Object is noop")
|
self.logger.debug("Object is noop")
|
||||||
return
|
return
|
||||||
reference = self.get_reference_object()
|
reference = self.get_patched_reference_object()
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
current = self.retrieve()
|
current = self.retrieve()
|
||||||
|
@ -129,6 +162,16 @@ class KubernetesObjectReconciler(Generic[T]):
|
||||||
if current.metadata.labels != reference.metadata.labels:
|
if current.metadata.labels != reference.metadata.labels:
|
||||||
raise NeedsUpdate()
|
raise NeedsUpdate()
|
||||||
|
|
||||||
|
patch = self.get_patch()
|
||||||
|
if patch is not None:
|
||||||
|
current_json = ApiClient().sanitize_for_serialization(current)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if apply_patch(current_json, patch) != current_json:
|
||||||
|
raise NeedsUpdate()
|
||||||
|
except (JsonPatchException, JsonPatchConflict, JsonPatchTestFailed) as exc:
|
||||||
|
raise ControllerException(f"JSON Patch failed: {exc}") from exc
|
||||||
|
|
||||||
def create(self, reference: T):
|
def create(self, reference: T):
|
||||||
"""API Wrapper to create object"""
|
"""API Wrapper to create object"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
|
@ -43,6 +43,10 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
||||||
self.api = AppsV1Api(controller.client)
|
self.api = AppsV1Api(controller.client)
|
||||||
self.outpost = self.controller.outpost
|
self.outpost = self.controller.outpost
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reconciler_name() -> str:
|
||||||
|
return "deployment"
|
||||||
|
|
||||||
def reconcile(self, current: V1Deployment, reference: V1Deployment):
|
def reconcile(self, current: V1Deployment, reference: V1Deployment):
|
||||||
compare_ports(
|
compare_ports(
|
||||||
current.spec.template.spec.containers[0].ports,
|
current.spec.template.spec.containers[0].ports,
|
||||||
|
|
|
@ -24,6 +24,10 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
|
||||||
super().__init__(controller)
|
super().__init__(controller)
|
||||||
self.api = CoreV1Api(controller.client)
|
self.api = CoreV1Api(controller.client)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reconciler_name() -> str:
|
||||||
|
return "secret"
|
||||||
|
|
||||||
def reconcile(self, current: V1Secret, reference: V1Secret):
|
def reconcile(self, current: V1Secret, reference: V1Secret):
|
||||||
super().reconcile(current, reference)
|
super().reconcile(current, reference)
|
||||||
for key in reference.data.keys():
|
for key in reference.data.keys():
|
||||||
|
|
|
@ -20,6 +20,10 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
|
||||||
super().__init__(controller)
|
super().__init__(controller)
|
||||||
self.api = CoreV1Api(controller.client)
|
self.api = CoreV1Api(controller.client)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reconciler_name() -> str:
|
||||||
|
return "service"
|
||||||
|
|
||||||
def reconcile(self, current: V1Service, reference: V1Service):
|
def reconcile(self, current: V1Service, reference: V1Service):
|
||||||
compare_ports(current.spec.ports, reference.spec.ports)
|
compare_ports(current.spec.ports, reference.spec.ports)
|
||||||
# run the base reconcile last, as that will probably raise NeedsUpdate
|
# run the base reconcile last, as that will probably raise NeedsUpdate
|
||||||
|
|
|
@ -71,6 +71,10 @@ class PrometheusServiceMonitorReconciler(KubernetesObjectReconciler[PrometheusSe
|
||||||
self.api_ex = ApiextensionsV1Api(controller.client)
|
self.api_ex = ApiextensionsV1Api(controller.client)
|
||||||
self.api = CustomObjectsApi(controller.client)
|
self.api = CustomObjectsApi(controller.client)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reconciler_name() -> str:
|
||||||
|
return "prometheus servicemonitor"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def noop(self) -> bool:
|
def noop(self) -> bool:
|
||||||
return (not self._crd_exists()) or (self.is_embedded)
|
return (not self._crd_exists()) or (self.is_embedded)
|
||||||
|
|
|
@ -64,12 +64,19 @@ class KubernetesController(BaseController):
|
||||||
super().__init__(outpost, connection)
|
super().__init__(outpost, connection)
|
||||||
self.client = KubernetesClient(connection)
|
self.client = KubernetesClient(connection)
|
||||||
self.reconcilers = {
|
self.reconcilers = {
|
||||||
"secret": SecretReconciler,
|
SecretReconciler.reconciler_name(): SecretReconciler,
|
||||||
"deployment": DeploymentReconciler,
|
DeploymentReconciler.reconciler_name(): DeploymentReconciler,
|
||||||
"service": ServiceReconciler,
|
ServiceReconciler.reconciler_name(): ServiceReconciler,
|
||||||
"prometheus servicemonitor": PrometheusServiceMonitorReconciler,
|
PrometheusServiceMonitorReconciler.reconciler_name(): (
|
||||||
|
PrometheusServiceMonitorReconciler
|
||||||
|
),
|
||||||
}
|
}
|
||||||
self.reconcile_order = ["secret", "deployment", "service", "prometheus servicemonitor"]
|
self.reconcile_order = [
|
||||||
|
SecretReconciler.reconciler_name(),
|
||||||
|
DeploymentReconciler.reconciler_name(),
|
||||||
|
ServiceReconciler.reconciler_name(),
|
||||||
|
PrometheusServiceMonitorReconciler.reconciler_name(),
|
||||||
|
]
|
||||||
|
|
||||||
def up(self):
|
def up(self):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Outpost models"""
|
"""Outpost models"""
|
||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Iterable, Optional
|
from typing import Any, Iterable, Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from dacite.core import from_dict
|
from dacite.core import from_dict
|
||||||
|
@ -75,6 +75,7 @@ class OutpostConfig:
|
||||||
kubernetes_service_type: str = field(default="ClusterIP")
|
kubernetes_service_type: str = field(default="ClusterIP")
|
||||||
kubernetes_disabled_components: list[str] = field(default_factory=list)
|
kubernetes_disabled_components: list[str] = field(default_factory=list)
|
||||||
kubernetes_image_pull_secrets: list[str] = field(default_factory=list)
|
kubernetes_image_pull_secrets: list[str] = field(default_factory=list)
|
||||||
|
kubernetes_json_patches: Optional[dict[str, list[dict[str, Any]]]] = field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class OutpostModel(Model):
|
class OutpostModel(Model):
|
||||||
|
|
|
@ -31,6 +31,10 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||||
super().__init__(controller)
|
super().__init__(controller)
|
||||||
self.api = NetworkingV1Api(controller.client)
|
self.api = NetworkingV1Api(controller.client)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reconciler_name() -> str:
|
||||||
|
return "ingress"
|
||||||
|
|
||||||
def _check_annotations(self, reference: V1Ingress):
|
def _check_annotations(self, reference: V1Ingress):
|
||||||
"""Check that all annotations *we* set are correct"""
|
"""Check that all annotations *we* set are correct"""
|
||||||
for key, value in self.get_ingress_annotations().items():
|
for key, value in self.get_ingress_annotations().items():
|
||||||
|
|
|
@ -17,6 +17,10 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler):
|
||||||
if not self.reconciler.crd_exists():
|
if not self.reconciler.crd_exists():
|
||||||
self.reconciler = Traefik2MiddlewareReconciler(controller)
|
self.reconciler = Traefik2MiddlewareReconciler(controller)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reconciler_name() -> str:
|
||||||
|
return "traefik middleware"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def noop(self) -> bool:
|
def noop(self) -> bool:
|
||||||
return self.reconciler.noop
|
return self.reconciler.noop
|
||||||
|
|
|
@ -67,6 +67,10 @@ class Traefik3MiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]
|
||||||
self.crd_version = "v1alpha1"
|
self.crd_version = "v1alpha1"
|
||||||
self.crd_plural = "middlewares"
|
self.crd_plural = "middlewares"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reconciler_name() -> str:
|
||||||
|
return "traefik middleware"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def noop(self) -> bool:
|
def noop(self) -> bool:
|
||||||
if not ProxyProvider.objects.filter(
|
if not ProxyProvider.objects.filter(
|
||||||
|
|
|
@ -16,7 +16,9 @@ class ProxyKubernetesController(KubernetesController):
|
||||||
DeploymentPort(9300, "http-metrics", "tcp"),
|
DeploymentPort(9300, "http-metrics", "tcp"),
|
||||||
DeploymentPort(9443, "https", "tcp"),
|
DeploymentPort(9443, "https", "tcp"),
|
||||||
]
|
]
|
||||||
self.reconcilers["ingress"] = IngressReconciler
|
self.reconcilers[IngressReconciler.reconciler_name()] = IngressReconciler
|
||||||
self.reconcilers["traefik middleware"] = TraefikMiddlewareReconciler
|
self.reconcilers[
|
||||||
self.reconcile_order.append("ingress")
|
TraefikMiddlewareReconciler.reconciler_name()
|
||||||
self.reconcile_order.append("traefik middleware")
|
] = TraefikMiddlewareReconciler
|
||||||
|
self.reconcile_order.append(IngressReconciler.reconciler_name())
|
||||||
|
self.reconcile_order.append(TraefikMiddlewareReconciler.reconciler_name())
|
||||||
|
|
|
@ -1809,6 +1809,31 @@ pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"
|
||||||
plugins = ["setuptools"]
|
plugins = ["setuptools"]
|
||||||
requirements-deprecated-finder = ["pip-api", "pipreqs"]
|
requirements-deprecated-finder = ["pip-api", "pipreqs"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonpatch"
|
||||||
|
version = "1.33"
|
||||||
|
description = "Apply JSON-Patches (RFC 6902)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
|
||||||
|
files = [
|
||||||
|
{file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"},
|
||||||
|
{file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
jsonpointer = ">=1.9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonpointer"
|
||||||
|
version = "2.4"
|
||||||
|
description = "Identify specific nodes in a JSON document (RFC 6901)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
|
||||||
|
files = [
|
||||||
|
{file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"},
|
||||||
|
{file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jsonschema"
|
name = "jsonschema"
|
||||||
version = "4.17.3"
|
version = "4.17.3"
|
||||||
|
@ -4186,4 +4211,4 @@ files = [
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "06466753c4ce0063905809123b1e2bb444034d84acdd108dcb20a9f92ce12fa6"
|
content-hash = "ab00edcd235c1c92dad9a91ace11d50df4564297193683cca7aa2b207ca27be6"
|
||||||
|
|
|
@ -172,6 +172,7 @@ webauthn = "*"
|
||||||
wsproto = "*"
|
wsproto = "*"
|
||||||
xmlsec = "*"
|
xmlsec = "*"
|
||||||
zxcvbn = "*"
|
zxcvbn = "*"
|
||||||
|
jsonpatch = "*"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
bandit = "*"
|
bandit = "*"
|
||||||
|
|
|
@ -35,6 +35,19 @@ class OutpostKubernetesTests(TestCase):
|
||||||
service_connection=self.service_connection,
|
service_connection=self.service_connection,
|
||||||
)
|
)
|
||||||
self.outpost.providers.add(self.provider)
|
self.outpost.providers.add(self.provider)
|
||||||
|
self.outpost.config.kubernetes_json_patches = {
|
||||||
|
"deployment": [
|
||||||
|
{
|
||||||
|
"op": "add",
|
||||||
|
"path": "/spec/template/spec/containers/0/resources",
|
||||||
|
"value": {
|
||||||
|
"requests": {"cpu": "2000m", "memory": "2000Mi"},
|
||||||
|
"limits": {"cpu": "4000m", "memory": "8000Mi"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
self.outpost.providers.add(self.provider)
|
||||||
self.outpost.save()
|
self.outpost.save()
|
||||||
|
|
||||||
def test_deployment_reconciler(self):
|
def test_deployment_reconciler(self):
|
||||||
|
@ -46,6 +59,18 @@ class OutpostKubernetesTests(TestCase):
|
||||||
|
|
||||||
config = self.outpost.config
|
config = self.outpost.config
|
||||||
config.kubernetes_replicas = 3
|
config.kubernetes_replicas = 3
|
||||||
|
config.kubernetes_json_patches = {
|
||||||
|
"deployment": [
|
||||||
|
{
|
||||||
|
"op": "add",
|
||||||
|
"path": "/spec/template/spec/containers/0/resources",
|
||||||
|
"value": {
|
||||||
|
"requests": {"cpu": "1000m", "memory": "2000Mi"},
|
||||||
|
"limits": {"cpu": "2000m", "memory": "4000Mi"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
self.outpost.config = config
|
self.outpost.config = config
|
||||||
|
|
||||||
with self.assertRaises(NeedsUpdate):
|
with self.assertRaises(NeedsUpdate):
|
||||||
|
|
|
@ -27,7 +27,7 @@ Depending on your platform, some native dependencies might be required. On macOS
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
As long as [this issue](https://github.com/xmlsec/python-xmlsec/issues/252) about `libxmlsec-1.3.0` is open, a workaround is required to install a compatible version of `libxmlsec1` with brew, see [this comment](https://github.com/xmlsec/python-xmlsec/issues/254#issuecomment-1511135314).
|
As long as [this issue](https://github.com/xmlsec/python-xmlsec/issues/252) about `libxmlsec-1.3.0` is open, a workaround is required to install a compatible version of `libxmlsec1` with brew, see [this comment](https://github.com/xmlsec/python-xmlsec/issues/254#issuecomment-1612005910).
|
||||||
:::
|
:::
|
||||||
|
|
||||||
First, you need to create an isolated Python environment. To create the environment and install dependencies, run the following commands in the same directory as your authentik git repository:
|
First, you need to create an isolated Python environment. To create the environment and install dependencies, run the following commands in the same directory as your authentik git repository:
|
||||||
|
|
|
@ -64,4 +64,18 @@ kubernetes_image_pull_secrets: []
|
||||||
# (Available with 2022.11.0+)
|
# (Available with 2022.11.0+)
|
||||||
# Applies to: proxy outposts
|
# Applies to: proxy outposts
|
||||||
kubernetes_ingress_class_name: null
|
kubernetes_ingress_class_name: null
|
||||||
|
# Optionally apply an RFC 6902 compliant patch to the Kubernetes objects. This value expects
|
||||||
|
# a mapping of a key which can be any of the values from `kubernetes_disabled_components`,
|
||||||
|
# which configures which component the patches are applied to. For example:
|
||||||
|
# deployment:
|
||||||
|
# - op: add
|
||||||
|
# path: "/spec/template/spec/containers/0/resources"
|
||||||
|
# value:
|
||||||
|
# requests:
|
||||||
|
# cpu: 2000m
|
||||||
|
# memory: 2000Mi
|
||||||
|
# limits:
|
||||||
|
# cpu: 4000m
|
||||||
|
# memory: 8000Mi
|
||||||
|
kubernetes_json_patches: null
|
||||||
```
|
```
|
||||||
|
|
|
@ -32,9 +32,8 @@ The following outpost settings are used:
|
||||||
- 'prometheus servicemonitor'
|
- 'prometheus servicemonitor'
|
||||||
- 'ingress'
|
- 'ingress'
|
||||||
- 'traefik middleware'
|
- 'traefik middleware'
|
||||||
- `kubernetes_image_pull_secrets`: If the above docker image is in a private repository, use these secrets to pull.
|
- `kubernetes_image_pull_secrets`: If the above docker image is in a private repository, use these secrets to pull. (NOTE: The secret must be created manually in the namespace first.)
|
||||||
|
- `kubernetes_json_patches`: Applies an RFC 6902 compliant JSON patch to the Kubernetes objects.
|
||||||
NOTE: The secret must be created manually in the namespace first.
|
|
||||||
|
|
||||||
## Permissions
|
## Permissions
|
||||||
|
|
||||||
|
|
Reference in New Issue