k8s_odoo_manager: bidirectional sync seems to be working
This commit is contained in:
parent
9019d8f280
commit
8bbfeb49f5
3 changed files with 518 additions and 73 deletions
|
|
@ -84,7 +84,7 @@ class K8sCluster(models.Model):
|
||||||
verify_ssl = fields.Boolean(
|
verify_ssl = fields.Boolean(
|
||||||
string="Verify SSL Certificate",
|
string="Verify SSL Certificate",
|
||||||
default=True,
|
default=True,
|
||||||
help="Disable for clusters with self-signed certificates (development only)"
|
help="Disable for clusters with self-signed certificates (development only)",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Relations
|
# Relations
|
||||||
|
|
@ -132,15 +132,21 @@ class K8sCluster(models.Model):
|
||||||
|
|
||||||
# Get the configuration and ensure SSL verification is properly set
|
# Get the configuration and ensure SSL verification is properly set
|
||||||
configuration = client.Configuration.get_default_copy()
|
configuration = client.Configuration.get_default_copy()
|
||||||
|
|
||||||
# Apply SSL verification setting from cluster configuration
|
# Apply SSL verification setting from cluster configuration
|
||||||
configuration.verify_ssl = self.verify_ssl
|
configuration.verify_ssl = self.verify_ssl
|
||||||
if not self.verify_ssl:
|
if not self.verify_ssl:
|
||||||
_logger.warning(f"SSL verification disabled for cluster {self.name} - use only for development!")
|
_logger.warning(
|
||||||
|
f"SSL verification disabled for cluster {self.name} - use only for development!"
|
||||||
|
)
|
||||||
|
|
||||||
# If we have certificate-authority-data in kubeconfig, it should be used
|
# If we have certificate-authority-data in kubeconfig, it should be used
|
||||||
# The kubernetes client should handle this automatically, but let's ensure it's set
|
# The kubernetes client should handle this automatically, but let's ensure it's set
|
||||||
if self.verify_ssl and configuration.ssl_ca_cert is None and "clusters" in kubeconfig_dict:
|
if (
|
||||||
|
self.verify_ssl
|
||||||
|
and configuration.ssl_ca_cert is None
|
||||||
|
and "clusters" in kubeconfig_dict
|
||||||
|
):
|
||||||
for cluster_info in kubeconfig_dict["clusters"]:
|
for cluster_info in kubeconfig_dict["clusters"]:
|
||||||
if "certificate-authority-data" in cluster_info.get(
|
if "certificate-authority-data" in cluster_info.get(
|
||||||
"cluster", {}
|
"cluster", {}
|
||||||
|
|
@ -239,22 +245,47 @@ class K8sCluster(models.Model):
|
||||||
for item in instances.get("items", []):
|
for item in instances.get("items", []):
|
||||||
metadata = item.get("metadata", {})
|
metadata = item.get("metadata", {})
|
||||||
spec = item.get("spec", {})
|
spec = item.get("spec", {})
|
||||||
status = item.get("status", {})
|
name = metadata.get("name")
|
||||||
|
namespace = metadata.get("namespace")
|
||||||
|
|
||||||
|
# Fetch status separately using the status subresource
|
||||||
|
status = {}
|
||||||
|
try:
|
||||||
|
status_obj = custom_api.get_namespaced_custom_object_status(
|
||||||
|
group="bemade.org",
|
||||||
|
version="v1",
|
||||||
|
namespace=namespace,
|
||||||
|
plural="odooinstances",
|
||||||
|
name=name,
|
||||||
|
)
|
||||||
|
status = (
|
||||||
|
status_obj.get("status", {})
|
||||||
|
if isinstance(status_obj, dict)
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
_logger.info(
|
||||||
|
f"Fetched status for {name}: {list(status.keys()) if isinstance(status, dict) else 'not dict'}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning(f"Could not fetch status for {name}: {e}")
|
||||||
|
# Fall back to status from list (might be empty)
|
||||||
|
status = item.get("status", {})
|
||||||
|
|
||||||
# Find or create the instance record
|
# Find or create the instance record
|
||||||
instance = self.env["k8s.odoo.instance"].search(
|
instance = self.env["k8s.odoo.instance"].search(
|
||||||
[
|
[
|
||||||
("cluster_id", "=", self.id),
|
("cluster_id", "=", self.id),
|
||||||
("name", "=", metadata.get("name")),
|
("name", "=", name),
|
||||||
("namespace", "=", metadata.get("namespace")),
|
("namespace", "=", namespace),
|
||||||
],
|
],
|
||||||
limit=1,
|
limit=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
values = {
|
# Prepare instance data
|
||||||
|
instance_data = {
|
||||||
"cluster_id": self.id,
|
"cluster_id": self.id,
|
||||||
"name": metadata.get("name"),
|
"name": name,
|
||||||
"namespace": metadata.get("namespace"),
|
"namespace": namespace,
|
||||||
"spec": json.dumps(spec, indent=2),
|
"spec": json.dumps(spec, indent=2),
|
||||||
"status": json.dumps(status, indent=2),
|
"status": json.dumps(status, indent=2),
|
||||||
"phase": status.get("phase", "Unknown"),
|
"phase": status.get("phase", "Unknown"),
|
||||||
|
|
@ -263,9 +294,9 @@ class K8sCluster(models.Model):
|
||||||
}
|
}
|
||||||
|
|
||||||
if instance:
|
if instance:
|
||||||
instance.write(values)
|
instance.write(instance_data)
|
||||||
else:
|
else:
|
||||||
self.env["k8s.odoo.instance"].create(values)
|
self.env["k8s.odoo.instance"].create(instance_data)
|
||||||
|
|
||||||
synced_count += 1
|
synced_count += 1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
|
from kubernetes import client
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -67,45 +68,218 @@ class K8sOdooInstance(models.Model):
|
||||||
help="Last time this record was synchronized from Kubernetes",
|
help="Last time this record was synchronized from Kubernetes",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Computed fields from spec
|
# Real-time computed fields from cluster (always fresh)
|
||||||
image = fields.Char(
|
current_image = fields.Char(
|
||||||
string="Docker Image", compute="_compute_spec_fields", store=True
|
string="Current Image", compute="_compute_current_values"
|
||||||
|
)
|
||||||
|
current_replicas = fields.Integer(
|
||||||
|
string="Current Replicas", compute="_compute_current_values"
|
||||||
|
)
|
||||||
|
current_ingress_hosts = fields.Text(
|
||||||
|
string="Current Ingress Hosts", compute="_compute_current_values"
|
||||||
|
)
|
||||||
|
current_cpu_request = fields.Char(
|
||||||
|
string="Current CPU Request", compute="_compute_current_values"
|
||||||
|
)
|
||||||
|
current_cpu_limit = fields.Char(
|
||||||
|
string="Current CPU Limit", compute="_compute_current_values"
|
||||||
|
)
|
||||||
|
current_memory_request = fields.Char(
|
||||||
|
string="Current Memory Request", compute="_compute_current_values"
|
||||||
|
)
|
||||||
|
current_memory_limit = fields.Char(
|
||||||
|
string="Current Memory Limit", compute="_compute_current_values"
|
||||||
|
)
|
||||||
|
current_ingress_enabled = fields.Boolean(
|
||||||
|
string="Current Ingress Enabled", compute="_compute_current_values"
|
||||||
)
|
)
|
||||||
|
|
||||||
replicas = fields.Integer(
|
# Editable spec fields (will sync back to cluster)
|
||||||
string="Replicas", compute="_compute_spec_fields", store=True
|
image_editable = fields.Char(
|
||||||
|
string="Docker Image", help="Docker image for the Odoo instance"
|
||||||
)
|
)
|
||||||
|
|
||||||
ingress_hosts = fields.Text(
|
replicas_editable = fields.Integer(
|
||||||
string="Ingress Hosts", compute="_compute_spec_fields", store=True
|
string="Replicas", default=1, help="Number of replicas to run"
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.depends("spec")
|
# Resource fields
|
||||||
def _compute_spec_fields(self):
|
cpu_request = fields.Char(
|
||||||
"""Extract commonly used fields from spec JSON"""
|
string="CPU Request", help="CPU request (e.g., '100m', '0.5')"
|
||||||
|
)
|
||||||
|
|
||||||
|
cpu_limit = fields.Char(string="CPU Limit", help="CPU limit (e.g., '1000m', '2')")
|
||||||
|
|
||||||
|
memory_request = fields.Char(
|
||||||
|
string="Memory Request", help="Memory request (e.g., '512Mi', '1Gi')"
|
||||||
|
)
|
||||||
|
|
||||||
|
memory_limit = fields.Char(
|
||||||
|
string="Memory Limit", help="Memory limit (e.g., '1Gi', '2Gi')"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ingress fields
|
||||||
|
ingress_enabled = fields.Boolean(string="Enable Ingress", default=True)
|
||||||
|
|
||||||
|
ingress_hosts_editable = fields.Text(
|
||||||
|
string="Ingress Hosts", help="One host per line"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filestore fields
|
||||||
|
filestore_enabled = fields.Boolean(string="Enable Filestore", default=True)
|
||||||
|
|
||||||
|
filestore_size = fields.Char(
|
||||||
|
string="Filestore Size",
|
||||||
|
default="10Gi",
|
||||||
|
help="Size of the filestore PVC (e.g., '10Gi', '50Gi')",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Status fields
|
||||||
|
ready_replicas = fields.Integer(
|
||||||
|
string="Ready Replicas", compute="_compute_status_fields", store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Computed fields to detect changes
|
||||||
|
has_pending_changes = fields.Boolean(
|
||||||
|
string="Has Pending Changes", compute="_compute_pending_changes"
|
||||||
|
)
|
||||||
|
|
||||||
|
image_changed = fields.Boolean(
|
||||||
|
string="Image Changed", compute="_compute_pending_changes"
|
||||||
|
)
|
||||||
|
|
||||||
|
replicas_changed = fields.Boolean(
|
||||||
|
string="Replicas Changed", compute="_compute_pending_changes"
|
||||||
|
)
|
||||||
|
|
||||||
|
available_replicas = fields.Integer(
|
||||||
|
string="Available Replicas", compute="_compute_status_fields", store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
ingress_url = fields.Char(
|
||||||
|
string="Ingress URL", compute="_compute_status_fields", store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
conditions = fields.Text(
|
||||||
|
string="Conditions", compute="_compute_status_fields", store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compute_current_values(self):
|
||||||
|
"""Fetch current values from cluster in real-time"""
|
||||||
for instance in self:
|
for instance in self:
|
||||||
if instance.spec:
|
# Initialize with empty values
|
||||||
try:
|
instance.current_image = ""
|
||||||
spec_data = json.loads(instance.spec)
|
instance.current_replicas = 0
|
||||||
instance.image = spec_data.get("image", "")
|
instance.current_ingress_hosts = ""
|
||||||
instance.replicas = spec_data.get("replicas", 0)
|
instance.current_cpu_request = ""
|
||||||
|
instance.current_cpu_limit = ""
|
||||||
|
instance.current_memory_request = ""
|
||||||
|
instance.current_memory_limit = ""
|
||||||
|
instance.current_ingress_enabled = False
|
||||||
|
|
||||||
# Extract ingress hosts
|
if not instance.cluster_id or not instance.cluster_id.active:
|
||||||
ingress = spec_data.get("ingress", {})
|
continue
|
||||||
hosts = ingress.get("hosts", [])
|
|
||||||
instance.ingress_hosts = ", ".join(hosts) if hosts else ""
|
try:
|
||||||
|
k8s_client = instance.cluster_id._get_k8s_client()
|
||||||
|
custom_api = client.CustomObjectsApi(k8s_client)
|
||||||
|
|
||||||
|
# Fetch the current object from cluster
|
||||||
|
obj = custom_api.get_namespaced_custom_object(
|
||||||
|
group="bemade.org",
|
||||||
|
version="v1",
|
||||||
|
namespace=instance.namespace,
|
||||||
|
plural="odooinstances",
|
||||||
|
name=instance.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract spec values
|
||||||
|
spec = obj.get("spec", {})
|
||||||
|
instance.current_image = spec.get("image", "")
|
||||||
|
instance.current_replicas = spec.get("replicas", 0)
|
||||||
|
|
||||||
|
# Extract ingress
|
||||||
|
ingress = spec.get("ingress", {})
|
||||||
|
instance.current_ingress_enabled = ingress.get("enabled", False)
|
||||||
|
hosts = ingress.get("hosts", [])
|
||||||
|
instance.current_ingress_hosts = ", ".join(hosts) if hosts else ""
|
||||||
|
|
||||||
|
# Extract resources
|
||||||
|
resources = spec.get("resources", {})
|
||||||
|
requests = resources.get("requests", {})
|
||||||
|
limits = resources.get("limits", {})
|
||||||
|
instance.current_cpu_request = requests.get("cpu", "")
|
||||||
|
instance.current_memory_request = requests.get("memory", "")
|
||||||
|
instance.current_cpu_limit = limits.get("cpu", "")
|
||||||
|
instance.current_memory_limit = limits.get("memory", "")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning(
|
||||||
|
f"Could not fetch current values for {instance.name}: {e}"
|
||||||
|
)
|
||||||
|
# Values remain empty as initialized above
|
||||||
|
|
||||||
|
@api.depends("status")
|
||||||
|
def _compute_status_fields(self):
|
||||||
|
"""Extract status information from status JSON"""
|
||||||
|
for instance in self:
|
||||||
|
if instance.status:
|
||||||
|
try:
|
||||||
|
status_data = json.loads(instance.status)
|
||||||
|
|
||||||
|
# Extract replica counts
|
||||||
|
instance.ready_replicas = status_data.get("readyReplicas", 0)
|
||||||
|
instance.available_replicas = status_data.get(
|
||||||
|
"availableReplicas", 0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract ingress URL
|
||||||
|
ingress_status = status_data.get("ingress", {})
|
||||||
|
urls = ingress_status.get("urls", [])
|
||||||
|
instance.ingress_url = urls[0] if urls else ""
|
||||||
|
|
||||||
|
# Extract conditions
|
||||||
|
conditions = status_data.get("conditions", [])
|
||||||
|
condition_strings = []
|
||||||
|
for condition in conditions:
|
||||||
|
ctype = condition.get("type", "")
|
||||||
|
status = condition.get("status", "")
|
||||||
|
reason = condition.get("reason", "")
|
||||||
|
condition_strings.append(f"{ctype}: {status} ({reason})")
|
||||||
|
instance.conditions = "\n".join(condition_strings)
|
||||||
|
|
||||||
except (json.JSONDecodeError, KeyError) as e:
|
except (json.JSONDecodeError, KeyError) as e:
|
||||||
_logger.warning(
|
_logger.warning(
|
||||||
f"Failed to parse spec for instance {instance.name}: {e}"
|
f"Failed to parse status for instance {instance.name}: {e}"
|
||||||
)
|
)
|
||||||
instance.image = ""
|
instance.ready_replicas = 0
|
||||||
instance.replicas = 0
|
instance.available_replicas = 0
|
||||||
instance.ingress_hosts = ""
|
instance.ingress_url = ""
|
||||||
|
instance.conditions = ""
|
||||||
else:
|
else:
|
||||||
instance.image = ""
|
instance.ready_replicas = 0
|
||||||
instance.replicas = 0
|
instance.available_replicas = 0
|
||||||
instance.ingress_hosts = ""
|
instance.ingress_url = ""
|
||||||
|
instance.conditions = ""
|
||||||
|
|
||||||
|
@api.depends(
|
||||||
|
"image_editable", "current_image", "replicas_editable", "current_replicas"
|
||||||
|
)
|
||||||
|
def _compute_pending_changes(self):
|
||||||
|
"""Compute if there are pending changes to sync"""
|
||||||
|
for instance in self:
|
||||||
|
image_changed = bool(
|
||||||
|
instance.image_editable
|
||||||
|
and instance.image_editable != instance.current_image
|
||||||
|
)
|
||||||
|
replicas_changed = bool(
|
||||||
|
instance.replicas_editable
|
||||||
|
and instance.replicas_editable != instance.current_replicas
|
||||||
|
)
|
||||||
|
|
||||||
|
instance.image_changed = image_changed
|
||||||
|
instance.replicas_changed = replicas_changed
|
||||||
|
instance.has_pending_changes = image_changed or replicas_changed
|
||||||
|
|
||||||
def name_get(self):
|
def name_get(self):
|
||||||
"""Custom name display"""
|
"""Custom name display"""
|
||||||
|
|
@ -179,17 +353,168 @@ class K8sOdooInstance(models.Model):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def action_refresh(self):
|
def action_reset_to_current(self):
|
||||||
"""Refresh this instance from Kubernetes"""
|
"""Reset editable fields to current cluster values"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
return self.cluster_id.sync_odoo_instances()
|
|
||||||
|
# Copy current values to editable fields (current values are computed fresh)
|
||||||
|
self.write(
|
||||||
|
{
|
||||||
|
"image_editable": self.current_image,
|
||||||
|
"replicas_editable": self.current_replicas,
|
||||||
|
"cpu_request": self.current_cpu_request,
|
||||||
|
"cpu_limit": self.current_cpu_limit,
|
||||||
|
"memory_request": self.current_memory_request,
|
||||||
|
"memory_limit": self.current_memory_limit,
|
||||||
|
"ingress_enabled": self.current_ingress_enabled,
|
||||||
|
"ingress_hosts_editable": self.current_ingress_hosts,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.client",
|
||||||
|
"tag": "display_notification",
|
||||||
|
"params": {
|
||||||
|
"title": _("Reset Complete"),
|
||||||
|
"message": _("All fields reset to current cluster values"),
|
||||||
|
"type": "success",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_sync_to_cluster(self):
|
||||||
|
"""Public method to sync changes to cluster"""
|
||||||
|
self.ensure_one()
|
||||||
|
try:
|
||||||
|
self._patch_to_cluster()
|
||||||
|
|
||||||
|
# Show success notification and refresh form
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.client",
|
||||||
|
"tag": "display_notification",
|
||||||
|
"params": {
|
||||||
|
"title": _("Sync Successful"),
|
||||||
|
"message": _("Successfully synced %s to cluster") % self.name,
|
||||||
|
"type": "success",
|
||||||
|
"sticky": False,
|
||||||
|
"next": {
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": "k8s.odoo.instance",
|
||||||
|
"res_id": self.id,
|
||||||
|
"view_mode": "form",
|
||||||
|
"views": [(False, "form")],
|
||||||
|
"target": "current",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.client",
|
||||||
|
"tag": "display_notification",
|
||||||
|
"params": {
|
||||||
|
"title": _("Sync Failed"),
|
||||||
|
"message": _("Failed to sync %s: %s") % (self.name, str(e)),
|
||||||
|
"type": "danger",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _patch_to_cluster(self):
|
||||||
|
"""Patch this instance's changes back to the cluster"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
if not self.cluster_id.active:
|
||||||
|
_logger.warning(
|
||||||
|
f"Cluster {self.cluster_id.name} is not active, skipping sync"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
k8s_client = self.cluster_id._get_k8s_client()
|
||||||
|
custom_api = client.CustomObjectsApi(k8s_client)
|
||||||
|
|
||||||
|
# Build the patch data from editable fields
|
||||||
|
patch_data = self._build_patch_data()
|
||||||
|
|
||||||
|
if not patch_data:
|
||||||
|
_logger.info(f"No changes to sync for {self.name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
_logger.info(f"Patching {self.name} with: {patch_data}")
|
||||||
|
|
||||||
|
# Apply the patch
|
||||||
|
custom_api.patch_namespaced_custom_object(
|
||||||
|
group="bemade.org",
|
||||||
|
version="v1",
|
||||||
|
namespace=self.namespace,
|
||||||
|
plural="odooinstances",
|
||||||
|
name=self.name,
|
||||||
|
body=patch_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
_logger.info(f"Successfully patched {self.name} to cluster")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Failed to patch {self.name} to cluster: {e}"
|
||||||
|
_logger.error(error_msg)
|
||||||
|
raise UserError(_(error_msg))
|
||||||
|
|
||||||
|
def _build_patch_data(self):
|
||||||
|
"""Build patch data from editable fields"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
patch = {"spec": {}}
|
||||||
|
|
||||||
|
# Image
|
||||||
|
if self.image_editable:
|
||||||
|
patch["spec"]["image"] = self.image_editable
|
||||||
|
|
||||||
|
# Replicas
|
||||||
|
if self.replicas_editable:
|
||||||
|
patch["spec"]["replicas"] = self.replicas_editable
|
||||||
|
|
||||||
|
# Resources
|
||||||
|
resources = {}
|
||||||
|
if (
|
||||||
|
self.cpu_request
|
||||||
|
or self.cpu_limit
|
||||||
|
or self.memory_request
|
||||||
|
or self.memory_limit
|
||||||
|
):
|
||||||
|
if self.cpu_request or self.memory_request:
|
||||||
|
resources["requests"] = {}
|
||||||
|
if self.cpu_request:
|
||||||
|
resources["requests"]["cpu"] = self.cpu_request
|
||||||
|
if self.memory_request:
|
||||||
|
resources["requests"]["memory"] = self.memory_request
|
||||||
|
|
||||||
|
if self.cpu_limit or self.memory_limit:
|
||||||
|
resources["limits"] = {}
|
||||||
|
if self.cpu_limit:
|
||||||
|
resources["limits"]["cpu"] = self.cpu_limit
|
||||||
|
if self.memory_limit:
|
||||||
|
resources["limits"]["memory"] = self.memory_limit
|
||||||
|
|
||||||
|
if resources:
|
||||||
|
patch["spec"]["resources"] = resources
|
||||||
|
|
||||||
|
# Ingress
|
||||||
|
if hasattr(self, "ingress_enabled"): # Check if field exists
|
||||||
|
ingress = {"enabled": self.ingress_enabled}
|
||||||
|
if self.ingress_hosts_editable:
|
||||||
|
# Parse hosts (support both comma and newline separated)
|
||||||
|
hosts = self.ingress_hosts_editable.replace("\n", ",").split(",")
|
||||||
|
hosts = [h.strip() for h in hosts if h.strip()]
|
||||||
|
if hosts:
|
||||||
|
ingress["hosts"] = hosts
|
||||||
|
patch["spec"]["ingress"] = ingress
|
||||||
|
|
||||||
|
# Return None if no actual changes
|
||||||
|
return patch if patch["spec"] else None
|
||||||
|
|
||||||
|
|
||||||
class K8sSpecViewer(models.TransientModel):
|
class K8sSpecViewer(models.TransientModel):
|
||||||
"""Transient model for displaying JSON content in a dialog"""
|
"""Transient model for displaying JSON content in a dialog"""
|
||||||
|
|
||||||
_name = "k8s.spec.viewer"
|
_name = "k8s.spec.viewer"
|
||||||
_description = "Kubernetes Spec/Status Viewer"
|
_description = "Kubernetes Spec Viewer"
|
||||||
|
|
||||||
title = fields.Char(string="Title", readonly=True)
|
title = fields.Char(string="Title", readonly=True)
|
||||||
content = fields.Text(string="Content", readonly=True)
|
content = fields.Text(string="Content", readonly=True)
|
||||||
|
|
|
||||||
|
|
@ -5,28 +5,28 @@
|
||||||
<field name="name">k8s.odoo.instance.list</field>
|
<field name="name">k8s.odoo.instance.list</field>
|
||||||
<field name="model">k8s.odoo.instance</field>
|
<field name="model">k8s.odoo.instance</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<list string="Odoo Instances" decoration-success="phase == 'Running'" decoration-warning="phase == 'Upgrading'" decoration-danger="phase == 'Restoring'">
|
<list string="Odoo Instances" decoration-muted="phase == 'Pending'" decoration-success="phase == 'Running'" decoration-danger="phase == 'Failed'">
|
||||||
|
<field name="name"/>
|
||||||
<field name="cluster_id"/>
|
<field name="cluster_id"/>
|
||||||
<field name="namespace"/>
|
<field name="namespace"/>
|
||||||
<field name="name"/>
|
<field name="current_image"/>
|
||||||
<field name="image"/>
|
<field name="current_replicas"/>
|
||||||
<field name="replicas"/>
|
<field name="ready_replicas"/>
|
||||||
<field name="phase" widget="badge"/>
|
<field name="phase" widget="badge"/>
|
||||||
<field name="url" widget="url"/>
|
<field name="ingress_url" widget="url"/>
|
||||||
<field name="last_updated"/>
|
<field name="last_updated"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- Instance Form View -->
|
|
||||||
<record id="view_k8s_odoo_instance_form" model="ir.ui.view">
|
<record id="view_k8s_odoo_instance_form" model="ir.ui.view">
|
||||||
<field name="name">k8s.odoo.instance.form</field>
|
<field name="name">k8s.odoo.instance.form</field>
|
||||||
<field name="model">k8s.odoo.instance</field>
|
<field name="model">k8s.odoo.instance</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="Odoo Instance" create="false" edit="false">
|
<form string="Odoo Instance" create="false">
|
||||||
<header>
|
<header>
|
||||||
<button name="action_open_url" string="Open Instance" type="object" class="btn-primary" invisible="not url"/>
|
<button name="action_open_url" string="Open Instance" type="object" class="btn-primary" invisible="not url"/>
|
||||||
<button name="action_refresh" string="Refresh" type="object" class="btn-secondary"/>
|
<button name="action_reset_to_current" string="Reset to Current" type="object" class="btn-secondary" confirm="This will reset all your changes to current cluster values. Continue?"/>
|
||||||
|
<button name="action_sync_to_cluster" string="Sync to Cluster" type="object" class="btn-warning" confirm="This will apply your changes to the Kubernetes cluster. Continue?"/>
|
||||||
<field name="phase" widget="statusbar"/>
|
<field name="phase" widget="statusbar"/>
|
||||||
</header>
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
|
|
@ -40,33 +40,123 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<group>
|
<group>
|
||||||
<group string="Basic Information">
|
<group>
|
||||||
<field name="image"/>
|
<field name="name"/>
|
||||||
<field name="replicas"/>
|
<field name="cluster_id"/>
|
||||||
<field name="url" widget="url"/>
|
<field name="namespace"/>
|
||||||
<field name="last_updated"/>
|
<field name="phase" widget="badge"/>
|
||||||
</group>
|
</group>
|
||||||
<group string="Network">
|
<group>
|
||||||
<field name="ingress_hosts"/>
|
<field name="ingress_url" widget="url"/>
|
||||||
|
<field name="last_updated"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<notebook>
|
<notebook>
|
||||||
<page string="Specification">
|
<page string="Configuration">
|
||||||
<div class="row">
|
<group>
|
||||||
<div class="col-12">
|
<group string="Application">
|
||||||
<button name="action_view_spec" string="View Full Spec" type="object" class="btn btn-link"/>
|
<label for="image_editable" string="Docker Image"/>
|
||||||
</div>
|
<div class="o_row">
|
||||||
</div>
|
<field name="image_editable" placeholder="e.g., odoo:17.0" class="oe_inline" style="width: 60%;"/>
|
||||||
<field name="spec" widget="text" readonly="1"/>
|
<span class="text-muted" style="margin-left: 10px;">
|
||||||
|
Current: <field name="current_image" readonly="1" nolabel="1" class="oe_inline"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label for="replicas_editable" string="Replicas"/>
|
||||||
|
<div class="o_row">
|
||||||
|
<field name="replicas_editable" class="oe_inline" style="width: 60%;"/>
|
||||||
|
<span class="text-muted" style="margin-left: 10px;">
|
||||||
|
Current: <field name="current_replicas" readonly="1" nolabel="1" class="oe_inline"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group string="Status">
|
||||||
|
<field name="ready_replicas" readonly="1"/>
|
||||||
|
<field name="available_replicas" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<group string="Resource Requests">
|
||||||
|
<label for="cpu_request" string="CPU Request"/>
|
||||||
|
<div class="o_row">
|
||||||
|
<field name="cpu_request" placeholder="e.g., 100m" class="oe_inline" style="width: 60%;"/>
|
||||||
|
<span class="text-muted" style="margin-left: 10px;">
|
||||||
|
Current: <field name="current_cpu_request" readonly="1" nolabel="1" class="oe_inline"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label for="memory_request" string="Memory Request"/>
|
||||||
|
<div class="o_row">
|
||||||
|
<field name="memory_request" placeholder="e.g., 512Mi" class="oe_inline" style="width: 60%;"/>
|
||||||
|
<span class="text-muted" style="margin-left: 10px;">
|
||||||
|
Current: <field name="current_memory_request" readonly="1" nolabel="1" class="oe_inline"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group string="Resource Limits">
|
||||||
|
<label for="cpu_limit" string="CPU Limit"/>
|
||||||
|
<div class="o_row">
|
||||||
|
<field name="cpu_limit" placeholder="e.g., 1000m" class="oe_inline" style="width: 60%;"/>
|
||||||
|
<span class="text-muted" style="margin-left: 10px;">
|
||||||
|
Current: <field name="current_cpu_limit" readonly="1" nolabel="1" class="oe_inline"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label for="memory_limit" string="Memory Limit"/>
|
||||||
|
<div class="o_row">
|
||||||
|
<field name="memory_limit" placeholder="e.g., 1Gi" class="oe_inline" style="width: 60%;"/>
|
||||||
|
<span class="text-muted" style="margin-left: 10px;">
|
||||||
|
Current: <field name="current_memory_limit" readonly="1" nolabel="1" class="oe_inline"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<group string="Ingress Configuration">
|
||||||
|
<field name="ingress_enabled"/>
|
||||||
|
<span class="text-muted">
|
||||||
|
Current: <field name="current_ingress_enabled" readonly="1" nolabel="1" class="oe_inline"/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<label for="ingress_hosts_editable" string="Ingress Hosts"/>
|
||||||
|
<div class="o_row">
|
||||||
|
<field name="ingress_hosts_editable" placeholder="host1.example.com, host2.example.com" class="oe_inline" style="width: 60%;"/>
|
||||||
|
<span class="text-muted" style="margin-left: 10px;">
|
||||||
|
Current: <field name="current_ingress_hosts" readonly="1" nolabel="1" class="oe_inline"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group string="Storage (Read-Only)">
|
||||||
|
<field name="filestore_enabled" readonly="1"/>
|
||||||
|
<field name="filestore_size" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
</page>
|
</page>
|
||||||
<page string="Status">
|
|
||||||
<div class="row">
|
<page string="Status Details">
|
||||||
<div class="col-12">
|
<group>
|
||||||
|
<field name="conditions" widget="text" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
<page string="Raw Data">
|
||||||
|
<group>
|
||||||
|
<group string="Specification">
|
||||||
|
<button name="action_view_spec" string="View Full Spec" type="object" class="btn btn-link"/>
|
||||||
|
<field name="spec" widget="text" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<group string="Status">
|
||||||
<button name="action_view_status" string="View Full Status" type="object" class="btn btn-link"/>
|
<button name="action_view_status" string="View Full Status" type="object" class="btn btn-link"/>
|
||||||
</div>
|
<field name="status" widget="text" readonly="1"/>
|
||||||
</div>
|
</group>
|
||||||
<field name="status" widget="text" readonly="1"/>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
|
|
@ -83,7 +173,7 @@
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="cluster_id"/>
|
<field name="cluster_id"/>
|
||||||
<field name="namespace"/>
|
<field name="namespace"/>
|
||||||
<field name="image"/>
|
<field name="current_image"/>
|
||||||
<field name="url"/>
|
<field name="url"/>
|
||||||
<separator/>
|
<separator/>
|
||||||
<filter string="Running" name="running" domain="[('phase', '=', 'Running')]"/>
|
<filter string="Running" name="running" domain="[('phase', '=', 'Running')]"/>
|
||||||
|
|
@ -94,7 +184,6 @@
|
||||||
<filter string="Cluster" name="group_cluster" domain="[]" context="{'group_by': 'cluster_id'}"/>
|
<filter string="Cluster" name="group_cluster" domain="[]" context="{'group_by': 'cluster_id'}"/>
|
||||||
<filter string="Namespace" name="group_namespace" domain="[]" context="{'group_by': 'namespace'}"/>
|
<filter string="Namespace" name="group_namespace" domain="[]" context="{'group_by': 'namespace'}"/>
|
||||||
<filter string="Phase" name="group_phase" domain="[]" context="{'group_by': 'phase'}"/>
|
<filter string="Phase" name="group_phase" domain="[]" context="{'group_by': 'phase'}"/>
|
||||||
<filter string="Image" name="group_image" domain="[]" context="{'group_by': 'image'}"/>
|
|
||||||
</group>
|
</group>
|
||||||
</search>
|
</search>
|
||||||
</field>
|
</field>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue