outposts: support json patch for Kubernetes (#6319)
This commit is contained in:
		| @ -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()) | ||||||
|  | |||||||
							
								
								
									
										27
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										27
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @ -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
	
	Block a user
	 ChandonPierre
					ChandonPierre