Skip to content

Commit

Permalink
feat: enable Istio CNI plugin by default (#365)
Browse files Browse the repository at this point in the history
* feat: enable Istio CNI plugin by default

This commit enables the Istio CNI plugin by default in all new
deployments of istio-pilot provided the required charm configuration is
present; otherwise the control plane remains intact.
It is also ensured that the required configurations are present when
upgrading to future versions so the plugin is correctly installed on
existing control planes (versions <1.17).

Fixes #356
Part of #351
  • Loading branch information
DnPlas committed Apr 3, 2024
1 parent 5c86667 commit a460cfc
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 31 deletions.
14 changes: 14 additions & 0 deletions charms/istio-pilot/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ options:
type: string
description: |
The domain name to be used by the charm to send a Certificate Signing Request (CSR) to a TLS certificate provider. In the absence of this configuration option, the charm will try to use the ingress gateway service hostname (if configured by a LB) or its IP address.
cni-bin-dir:
type: string
default: ''
description: >
Path to CNI binaries, e.g. /opt/cni/bin. If not provided, the Istio control plane will be installed/upgraded with the Istio CNI plugin disabled.
This path depends on the Kubernetes installation, please refer to https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/
for information to find out the correct path.
cni-conf-dir:
type: string
default: ''
description: Path to conflist files describing the CNI configuration, e.g. /etc/cni/net.d. If not provided, the Istio control plane will be installed/upgraded
with the Istio CNI plugin disabled.
This path depends on the Kubernetes installation, please refer to https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/
for information to find out the correct path.
default-gateway:
type: string
default: istio-gateway
Expand Down
143 changes: 114 additions & 29 deletions charms/istio-pilot/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,11 @@ def __init__(self, *args):
self._field_manager = "lightkube"

# Instantiate a CertHandler
self.peer_relation_name = "peers"
self._cert_handler = CertHandler(
self,
key="istio-cert",
peer_relation_name="peers",
peer_relation_name=self.peer_relation_name,
cert_subject=self._cert_subject,
)

Expand Down Expand Up @@ -202,38 +203,66 @@ def _get_image_config(self):
image_config = yaml.safe_load(self.model.config[IMAGE_CONFIGURATION])
return image_config

def install(self, _):
"""Install charm."""
self._log_and_set_status(MaintenanceStatus("Deploying Istio control plane"))

@property
def _istioctl_extra_flags(self):
"""Return extra flags to pass to istioctl commands."""
image_config = self._get_image_config()
pilot_image = image_config["pilot-image"]
global_tag = image_config["global-tag"]
global_hub = image_config["global-hub"]
global_proxy_image = image_config["global-proxy-image"]
global_proxy_init_image = image_config["global-proxy-init-image"]

# Call istioctl install and set parameters based on image configuration
subprocess.check_call(
[
"./istioctl",
"install",
"-y",
"--set",
"profile=minimal",
"--set",
f"values.global.istioNamespace={self.model.name}",
"--set",
f"values.pilot.image={pilot_image}",
"--set",
f"values.global.tag={global_tag}",
"--set",
f"values.global.hub={global_hub}",
"--set",
f"values.global.proxy.image={global_proxy_image}",
"--set",
f"values.global.proxy_init.image={global_proxy_init_image}",
]
# Extra flags to pass to the istioctl install command
# These flags will configure the container images used by the control plane
extra_flags = [
"--set",
f"values.pilot.image={pilot_image}",
"--set",
f"values.global.tag={global_tag}",
"--set",
f"values.global.hub={global_hub}",
"--set",
f"values.global.proxy.image={global_proxy_image}",
"--set",
f"values.global.proxy_init.image={global_proxy_init_image}",
]

# The following are a set of flags that configure the CNI behaviour
# * components.cni.enabled enables the CNI plugin
# * values.cni.cniBinDir and values.cni.cniConfDir tell the plugin where to find
# the CNI binaries and config files
# * values.sidecarInjectorWebhook.injectedAnnotations allows users to inject any
# annotations to the sidecar injected Pods. This particular annotation helps
# provide a solution for canonical/istio-operators#356
if self._check_cni_configurations():
extra_flags.extend(
[
"--set",
"components.cni.enabled=true",
"--set",
f"values.cni.cniBinDir={self.model.config['cni-bin-dir']}",
"--set",
f"values.cni.cniConfDir={self.model.config['cni-conf-dir']}",
"--set",
"values.sidecarInjectorWebhook.injectedAnnotations.traffic\.sidecar\.istio\.io/excludeOutboundIPRanges=0.0.0.0/0", # noqa
]
)
return extra_flags

def install(self, _):
"""Install charm."""

self._log_and_set_status(
MaintenanceStatus("Deploying Istio control plane with Istio CNI plugin.")
)

# Call the istioctl wrapper to install the Istio Control Plane
istioctl = Istioctl(
ISTIOCTL_PATH,
self.model.name,
ISTIOCTL_DEPOYMENT_PROFILE,
istioctl_extra_flags=self._istioctl_extra_flags,
)

self.unit.status = ActiveStatus()
Expand Down Expand Up @@ -274,8 +303,9 @@ def set_tls(self, event) -> None:
def reconcile(self, event):
"""Reconcile the state of the charm.
This is the main entrypoint for the charm. It:
This is the main entrypoint for the method. It:
* Checks if we are the leader, exiting early with WaitingStatus if we are not
* Upgrades the Istio control plane if changes were made to the CNI plugin configurations
* Sends data to the istio-pilot relation
* Reconciles the ingress-auth relation, establishing whether we need authentication on our
ingress gateway
Expand All @@ -298,6 +328,15 @@ def reconcile(self, event):
# so that we can report them at the end.
handled_errors = []

# Call upgrade_charm in case there are new configurations that affect the control plane
# only if the CNI configurations have been provided and have changed from a previous state
# This is useful when there is a missing configuration during the install process
try:
if self._cni_config_changed:
self.upgrade_charm(event)
except GenericCharmRuntimeError as err:
handled_errors.append(err)

# Send istiod information to the istio-pilot relation
try:
self._handle_istio_pilot_relation()
Expand Down Expand Up @@ -366,8 +405,14 @@ def upgrade_charm(self, _):
Supports upgrade of exactly one minor version at a time.
"""
istioctl = Istioctl(ISTIOCTL_PATH, self.model.name, ISTIOCTL_DEPOYMENT_PROFILE)
self._log_and_set_status(MaintenanceStatus("Upgrading Istio"))
self._log_and_set_status(MaintenanceStatus("Upgrading Istio control plane."))

istioctl = Istioctl(
ISTIOCTL_PATH,
self.model.name,
ISTIOCTL_DEPOYMENT_PROFILE,
istioctl_extra_flags=self._istioctl_extra_flags,
)

# Check for version compatibility for the upgrade
try:
Expand Down Expand Up @@ -961,6 +1006,46 @@ def _log_and_set_status(self, status):

log_destination_map[type(status)](status.message)

def _check_cni_configurations(self) -> bool:
"""Return True if the necessary CNI configuration options are set, False otherwise."""
return self.model.config["cni-conf-dir"] and self.model.config["cni-bin-dir"]

def _cni_config_changed(self):
"""
Returns True if any of the CNI configuration options has changed from a previous state,
False otherwise.
"""
# The peer relation is required to store values, if it does not exist because it was
# removed by accident, the charm should fail
rel = self.model.get_relation(self.peer_relation_name, None)
if not rel:
raise GenericCharmRuntimeError(
"The istio-pilot charm requires a peer relation, make sure it exists."
)

# Get current values of the configuration options
current_cni_bin_dir = rel.data[self.unit].get("cni-bin-dir", None)
current_cni_conf_dir = rel.data[self.unit].get("cni-conf-dir", None)

# Update the values based on the configuration options
rel.data[self.unit].update({"cni-bin-dir": self.model.config["cni-bin-dir"]})
rel.data[self.unit].update({"cni-conf-dir": self.model.config["cni-conf-dir"]})

new_cni_bin_dir = rel.data[self.unit].get("cni-bin-dir", None)
new_cni_conf_dir = rel.data[self.unit].get("cni-conf-dir", None)

# Compare current vs new values and decide whether they have changed from a previous state
cni_bin_dir_changed = False
cni_conf_dir_changed = False
if current_cni_bin_dir != new_cni_bin_dir:
cni_bin_dir_changed = True

if current_cni_conf_dir != new_cni_conf_dir:
cni_conf_dir_changed = True

# If any of the configuration options changed, return True
return cni_bin_dir_changed or cni_conf_dir_changed


def _get_gateway_address_from_svc(svc):
"""Returns the gateway service address from a kubernetes Service.
Expand Down
Loading

0 comments on commit a460cfc

Please sign in to comment.