Skip to content

Commit

Permalink
Merge branch 'main' into KU-326/bootstrap-with-taints
Browse files Browse the repository at this point in the history
  • Loading branch information
addyess authored Apr 20, 2024
2 parents 486ecbf + efff0cb commit 0f1d309
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 66 deletions.
179 changes: 121 additions & 58 deletions charms/worker/k8s/lib/charms/k8s/v0/k8sd_api_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ class utilises different connection factories (UnixSocketConnectionFactory
import socket
from contextlib import contextmanager
from http.client import HTTPConnection, HTTPException
from typing import Generator, List, Optional, Type, TypeVar
from typing import Any, Dict, Generator, List, Optional, Type, TypeVar

import yaml
from pydantic import AnyHttpUrl, BaseModel, Field, SecretStr, validator

# The unique Charmhub library identifier, never change it
Expand Down Expand Up @@ -162,8 +163,8 @@ class ClusterMember(BaseModel):

name: str
address: str
cluster_role: str = Field(None, alias="cluster-role")
datastore_role: str = Field(None, alias="datastore-role")
cluster_role: Optional[str] = Field(None, alias="cluster-role")
datastore_role: Optional[str] = Field(None, alias="datastore-role")


class DNSConfig(BaseModel):
Expand All @@ -176,10 +177,10 @@ class DNSConfig(BaseModel):
upstream_nameservers: List of upstream nameservers for DNS resolution.
"""

enabled: bool = Field(None)
cluster_domain: str = Field(None, alias="cluster-domain")
service_ip: str = Field(None, alias="service-ip")
upstream_nameservers: List[str] = Field(None, alias="upstream-nameservers")
enabled: Optional[bool] = Field(None)
cluster_domain: Optional[str] = Field(None, alias="cluster-domain")
service_ip: Optional[str] = Field(None, alias="service-ip")
upstream_nameservers: Optional[List[str]] = Field(None, alias="upstream-nameservers")


class IngressConfig(BaseModel):
Expand All @@ -191,9 +192,9 @@ class IngressConfig(BaseModel):
enable_proxy_protocol: Optional flag to enable or disable proxy protocol.
"""

enabled: bool = Field(None)
default_tls_secret: str = Field(None, alias="default-tls-secret")
enable_proxy_protocol: bool = Field(None, alias="enable-proxy-protocol")
enabled: Optional[bool] = Field(None)
default_tls_secret: Optional[str] = Field(None, alias="default-tls-secret")
enable_proxy_protocol: Optional[bool] = Field(None, alias="enable-proxy-protocol")


class LoadBalancerConfig(BaseModel):
Expand All @@ -211,15 +212,15 @@ class LoadBalancerConfig(BaseModel):
bgp_peer_port: The port for BGP peering.
"""

enabled: bool = Field(None)
cidrs: List[str] = Field(None)
l2_enabled: bool = Field(None, alias="l2-enabled")
l2_interfaces: List[str] = Field(None, alias="l2-interfaces")
bgp_enabled: bool = Field(None, alias="bgp-enabled")
bgp_local_asn: int = Field(None, alias="bgp-local-asn")
bgp_peer_address: str = Field(None, alias="bgp-peer-address")
bgp_peer_asn: int = Field(None, alias="bgp-peer-asn")
bgp_peer_port: int = Field(None, alias="bgp-peer-port")
enabled: Optional[bool] = Field(None)
cidrs: Optional[List[str]] = Field(None)
l2_enabled: Optional[bool] = Field(None, alias="l2-enabled")
l2_interfaces: Optional[List[str]] = Field(None, alias="l2-interfaces")
bgp_enabled: Optional[bool] = Field(None, alias="bgp-enabled")
bgp_local_asn: Optional[int] = Field(None, alias="bgp-local-asn")
bgp_peer_address: Optional[str] = Field(None, alias="bgp-peer-address")
bgp_peer_asn: Optional[int] = Field(None, alias="bgp-peer-asn")
bgp_peer_port: Optional[int] = Field(None, alias="bgp-peer-port")


class LocalStorageConfig(BaseModel):
Expand All @@ -232,10 +233,10 @@ class LocalStorageConfig(BaseModel):
set_default: Optional flag to set this as the default storage option.
"""

enabled: bool = Field(None)
local_path: str = Field(None, alias="local-path")
reclaim_policy: str = Field(None, alias="reclaim-policy")
set_default: bool = Field(None, alias="set-default")
enabled: Optional[bool] = Field(None)
local_path: Optional[str] = Field(None, alias="local-path")
reclaim_policy: Optional[str] = Field(None, alias="reclaim-policy")
set_default: Optional[bool] = Field(None, alias="set-default")


class NetworkConfig(BaseModel):
Expand All @@ -245,7 +246,7 @@ class NetworkConfig(BaseModel):
enabled: Optional flag which represents the status of Network.
"""

enabled: bool = Field(None)
enabled: Optional[bool] = Field(None)


class GatewayConfig(BaseModel):
Expand All @@ -255,7 +256,7 @@ class GatewayConfig(BaseModel):
enabled: Optional flag which represents the status of Gateway.
"""

enabled: bool = Field(None)
enabled: Optional[bool] = Field(None)


class MetricsServerConfig(BaseModel):
Expand All @@ -265,7 +266,7 @@ class MetricsServerConfig(BaseModel):
enabled: Optional flag which represents the status of MetricsServer.
"""

enabled: bool = Field(None)
enabled: Optional[bool] = Field(None)


class UserFacingClusterConfig(BaseModel):
Expand All @@ -282,14 +283,14 @@ class UserFacingClusterConfig(BaseModel):
cloud_provider: The cloud provider for the cluster.
"""

network: NetworkConfig = Field(None)
dns: DNSConfig = Field(None)
ingress: IngressConfig = Field(None)
load_balancer: LoadBalancerConfig = Field(None, alias="load-balancer")
local_storage: LocalStorageConfig = Field(None, alias="local-storage")
gateway: GatewayConfig = Field(None)
metrics_server: MetricsServerConfig = Field(None, alias="metrics-server")
cloud_provider: str = Field(None, alias="cloud-provider")
network: Optional[NetworkConfig] = Field(None)
dns: Optional[DNSConfig] = Field(None)
ingress: Optional[IngressConfig] = Field(None)
load_balancer: Optional[LoadBalancerConfig] = Field(None, alias="load-balancer")
local_storage: Optional[LocalStorageConfig] = Field(None, alias="local-storage")
gateway: Optional[GatewayConfig] = Field(None)
metrics_server: Optional[MetricsServerConfig] = Field(None, alias="metrics-server")
cloud_provider: Optional[str] = Field(None, alias="cloud-provider")


class BootstrapConfig(BaseModel):
Expand All @@ -308,20 +309,22 @@ class BootstrapConfig(BaseModel):
datastore_ca_cert (str): The CA certificate for the datastore.
datastore_client_cert (str): The client certificate for accessing the datastore.
datastore_client_key (str): The client key for accessing the datastore.
extra_sans (List[str]): List of extra sans for the self-signed certificates
"""

cluster_config: UserFacingClusterConfig = Field(None, alias="cluster-config")
control_plane_taints: List[str] = Field(None, alias="control-plane-taints")
pod_cidr: str = Field(None, alias="pod-cidr")
service_cidr: str = Field(None, alias="service-cidr")
disable_rbac: bool = Field(None, alias="disable-rbac")
secure_port: int = Field(None, alias="secure-port")
k8s_dqlite_port: int = Field(None, alias="k8s-dqlite-port")
datastore_type: str = Field(None, alias="datastore-type")
datastore_servers: List[AnyHttpUrl] = Field(None, alias="datastore-servers")
datastore_ca_cert: str = Field(None, alias="datastore-ca-crt")
datastore_client_cert: str = Field(None, alias="datastore-client-crt")
datastore_client_key: str = Field(None, alias="datastore-client-key")
cluster_config: Optional[UserFacingClusterConfig] = Field(None, alias="cluster-config")
control_plane_taints: Optional[List[str]] = Field(None, alias="control-plane-taints")
pod_cidr: Optional[str] = Field(None, alias="pod-cidr")
service_cidr: Optional[str] = Field(None, alias="service-cidr")
disable_rbac: Optional[bool] = Field(None, alias="disable-rbac")
secure_port: Optional[int] = Field(None, alias="secure-port")
k8s_dqlite_port: Optional[int] = Field(None, alias="k8s-dqlite-port")
datastore_type: Optional[str] = Field(None, alias="datastore-type")
datastore_servers: Optional[List[AnyHttpUrl]] = Field(None, alias="datastore-servers")
datastore_ca_cert: Optional[str] = Field(None, alias="datastore-ca-crt")
datastore_client_cert: Optional[str] = Field(None, alias="datastore-client-crt")
datastore_client_key: Optional[str] = Field(None, alias="datastore-client-key")
extra_sans: Optional[List[str]] = Field(None, alias="extra-sans")


class CreateClusterRequest(BaseModel):
Expand All @@ -348,6 +351,68 @@ class UpdateClusterConfigRequest(BaseModel):
config: UserFacingClusterConfig


class NodeJoinConfig(BaseModel, allow_population_by_field_name=True):
"""Request model for the config on a node joining the cluster.
Attributes:
kubelet_crt (str): node's certificate
kubelet_key (str): node's certificate key
"""

kubelet_crt: Optional[str] = Field(None, alias="kubelet-crt")
kubelet_key: Optional[str] = Field(None, alias="kubelet-key")


class ControlPlaneNodeJoinConfig(NodeJoinConfig, allow_population_by_field_name=True):
"""Request model for the config on a control-plane node joining the cluster.
Attributes:
extra_sans (List[str]): List of extra sans for the self-signed certificates
apiserver_crt (str): apiserver certificate
apiserver_client_key (str): apiserver certificate key
front_proxy_client_crt (str): front-proxy certificate
front_proxy_client_key (str): front-proxy certificate key
"""

extra_sans: Optional[List[str]] = Field(None, alias="extra-sans")

apiserver_crt: Optional[str] = Field(None, alias="apiserver-crt")
apiserver_client_key: Optional[str] = Field(None, alias="apiserver-key")
front_proxy_client_crt: Optional[str] = Field(None, alias="front-proxy-client-crt")
front_proxy_client_key: Optional[str] = Field(None, alias="front-proxy-client-key")


class JoinClusterRequest(BaseModel, allow_population_by_field_name=True):
"""Request model for a node joining the cluster.
Attributes:
name (str): node's certificate
address (str): node's certificate key
token (str): token
config (NodeJoinConfig): Node Config
"""

name: str
address: str
token: SecretStr
config: Optional[NodeJoinConfig] = Field(None)

def dict(self, **kwds) -> Dict[Any, Any]:
"""Render object into a dict.
Arguments:
kwds: keyword arguments
Returns:
dict mapping of the object
"""
rendered = super().dict(**kwds)
rendered["token"] = self.token.get_secret_value()
if self.config:
rendered["config"] = yaml.safe_dump(self.config.dict(**kwds))
return rendered


class DatastoreStatus(BaseModel):
"""information regarding the active datastore.
Expand All @@ -356,8 +421,8 @@ class DatastoreStatus(BaseModel):
external_url: (str): list of external_urls
"""

datastore_type: str = Field(None, alias="type")
external_url: str = Field(None, alias="external-url")
datastore_type: Optional[str] = Field(None, alias="type")
external_url: Optional[str] = Field(None, alias="external-url")


class ClusterStatus(BaseModel):
Expand All @@ -371,9 +436,9 @@ class ClusterStatus(BaseModel):
"""

ready: bool = Field(False)
members: List[ClusterMember] = Field(None)
config: UserFacingClusterConfig = Field(None)
datastore: DatastoreStatus = Field(None)
members: Optional[List[ClusterMember]] = Field(None)
config: Optional[UserFacingClusterConfig] = Field(None)
datastore: Optional[DatastoreStatus] = Field(None)


class ClusterMetadata(BaseModel):
Expand All @@ -394,7 +459,7 @@ class GetClusterStatusResponse(BaseRequestModel):
Can be None if the status is not available.
"""

metadata: ClusterMetadata = Field(None)
metadata: Optional[ClusterMetadata] = Field(None)


class KubeConfigMetadata(BaseModel):
Expand Down Expand Up @@ -599,17 +664,15 @@ def create_join_token(self, name: str, worker: bool = False) -> SecretStr:
join_response = self._send_request(endpoint, "POST", CreateJoinTokenResponse, body)
return join_response.metadata.token

def join_cluster(self, name: str, address: str, token: str):
def join_cluster(self, config: JoinClusterRequest):
"""Join a node to the k8s cluster.
Args:
name (str): Name of the node.
address (str): address to which k8sd should be bound
token (str): The join token for this node.
config: JoinClusterRequest: config to join the cluster
"""
endpoint = "/1.0/k8sd/cluster/join"
body = {"name": name, "address": address, "token": token}
self._send_request(endpoint, "POST", EmptyResponse, body)
request = config.dict(exclude_none=True, by_alias=True)
self._send_request(endpoint, "POST", EmptyResponse, request)

def remove_node(self, name: str, force: bool = True):
"""Remove a node from the cluster.
Expand Down
9 changes: 8 additions & 1 deletion charms/worker/k8s/lib/charms/operator_libs_linux/v2/snap.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 4
LIBPATCH = 5


# Regex to locate 7-bit C1 ANSI sequences
Expand Down Expand Up @@ -580,10 +580,17 @@ def ensure(
# We are installing or refreshing a snap.
if self._state not in (SnapState.Present, SnapState.Latest):
# The snap is not installed, so we install it.
logger.info(
"Installing snap %s, revision %s, tracking %s", self._name, revision, channel
)
self._install(channel, cohort, revision)
else:
# The snap is installed, but we are changing it (e.g., switching channels).
logger.info(
"Refreshing snap %s, revision %s, tracking %s", self._name, revision, channel
)
self._refresh(channel=channel, cohort=cohort, revision=revision, devmode=devmode)
logger.info("The snap installation completed successfully")

self._update_snap_apps()
self._state = state
Expand Down
24 changes: 20 additions & 4 deletions charms/worker/k8s/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@
from charms.grafana_agent.v0.cos_agent import COSAgentProvider
from charms.k8s.v0.k8sd_api_manager import (
BootstrapConfig,
ControlPlaneNodeJoinConfig,
CreateClusterRequest,
DNSConfig,
InvalidResponseError,
JoinClusterRequest,
K8sdAPIManager,
K8sdConnectionError,
NetworkConfig,
Expand Down Expand Up @@ -64,6 +66,16 @@
SUPPORTED_DATASTORES = ["dqlite", "etcd"]


def _get_public_address() -> str:
"""Get public address from juju.
Returns:
(str) public ip address of the unit
"""
cmd = ["unit-get", "public-address"]
return subprocess.check_output(cmd).decode("UTF-8").strip()


class K8sCharm(ops.CharmBase):
"""A charm for managing a K8s cluster via the k8s snap.
Expand Down Expand Up @@ -223,6 +235,7 @@ def _bootstrap_k8s_snap(self):
self._configure_datastore(bootstrap_config)
bootstrap_config.service_cidr = self.config["service-cidr"]
bootstrap_config.control_plane_taints = self.config["register-with-taints"].split()
bootstrap_config.extra_sans = [_get_public_address()]

status.add(ops.MaintenanceStatus("Bootstrapping Cluster"))

Expand Down Expand Up @@ -448,7 +461,12 @@ def _join_cluster(self):
node_name = self.get_node_name()
cluster_addr = f"{address}:{K8SD_PORT}"
log.info("Joining %s to %s...", node_name, cluster_addr)
self.api_manager.join_cluster(node_name, cluster_addr, token)
request = JoinClusterRequest(name=node_name, address=cluster_addr, token=token)
if self.is_control_plane:
request.config = ControlPlaneNodeJoinConfig()
request.config.extra_sans = [_get_public_address()]

self.api_manager.join_cluster(request)
log.info("Success")

def _reconcile(self, event):
Expand Down Expand Up @@ -584,9 +602,7 @@ def _get_external_kubeconfig(self, event: ops.ActionEvent):
server = event.params.get("server")
if not server:
log.info("No server requested, use public-address")
cmd = ["unit-get", "public-address"]
addr = subprocess.check_output(cmd).decode("UTF-8").strip()
server = f"{addr}:6443"
server = f"{_get_public_address()}:6443"
log.info("Requesting kubeconfig for server=%s", server)
resp = self.api_manager.get_kubeconfig(server)
event.set_results({"kubeconfig": resp})
Expand Down
Loading

0 comments on commit 0f1d309

Please sign in to comment.