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(
|
||||
string="Verify SSL Certificate",
|
||||
default=True,
|
||||
help="Disable for clusters with self-signed certificates (development only)"
|
||||
help="Disable for clusters with self-signed certificates (development only)",
|
||||
)
|
||||
|
||||
# Relations
|
||||
|
|
@ -132,15 +132,21 @@ class K8sCluster(models.Model):
|
|||
|
||||
# Get the configuration and ensure SSL verification is properly set
|
||||
configuration = client.Configuration.get_default_copy()
|
||||
|
||||
|
||||
# Apply SSL verification setting from cluster configuration
|
||||
configuration.verify_ssl = 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
|
||||
# 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"]:
|
||||
if "certificate-authority-data" in cluster_info.get(
|
||||
"cluster", {}
|
||||
|
|
@ -239,22 +245,47 @@ class K8sCluster(models.Model):
|
|||
for item in instances.get("items", []):
|
||||
metadata = item.get("metadata", {})
|
||||
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
|
||||
instance = self.env["k8s.odoo.instance"].search(
|
||||
[
|
||||
("cluster_id", "=", self.id),
|
||||
("name", "=", metadata.get("name")),
|
||||
("namespace", "=", metadata.get("namespace")),
|
||||
("name", "=", name),
|
||||
("namespace", "=", namespace),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
values = {
|
||||
# Prepare instance data
|
||||
instance_data = {
|
||||
"cluster_id": self.id,
|
||||
"name": metadata.get("name"),
|
||||
"namespace": metadata.get("namespace"),
|
||||
"name": name,
|
||||
"namespace": namespace,
|
||||
"spec": json.dumps(spec, indent=2),
|
||||
"status": json.dumps(status, indent=2),
|
||||
"phase": status.get("phase", "Unknown"),
|
||||
|
|
@ -263,9 +294,9 @@ class K8sCluster(models.Model):
|
|||
}
|
||||
|
||||
if instance:
|
||||
instance.write(values)
|
||||
instance.write(instance_data)
|
||||
else:
|
||||
self.env["k8s.odoo.instance"].create(values)
|
||||
self.env["k8s.odoo.instance"].create(instance_data)
|
||||
|
||||
synced_count += 1
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import json
|
|||
import logging
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from kubernetes import client
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -67,45 +68,218 @@ class K8sOdooInstance(models.Model):
|
|||
help="Last time this record was synchronized from Kubernetes",
|
||||
)
|
||||
|
||||
# Computed fields from spec
|
||||
image = fields.Char(
|
||||
string="Docker Image", compute="_compute_spec_fields", store=True
|
||||
# Real-time computed fields from cluster (always fresh)
|
||||
current_image = fields.Char(
|
||||
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(
|
||||
string="Replicas", compute="_compute_spec_fields", store=True
|
||||
# Editable spec fields (will sync back to cluster)
|
||||
image_editable = fields.Char(
|
||||
string="Docker Image", help="Docker image for the Odoo instance"
|
||||
)
|
||||
|
||||
ingress_hosts = fields.Text(
|
||||
string="Ingress Hosts", compute="_compute_spec_fields", store=True
|
||||
replicas_editable = fields.Integer(
|
||||
string="Replicas", default=1, help="Number of replicas to run"
|
||||
)
|
||||
|
||||
@api.depends("spec")
|
||||
def _compute_spec_fields(self):
|
||||
"""Extract commonly used fields from spec JSON"""
|
||||
# Resource fields
|
||||
cpu_request = fields.Char(
|
||||
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:
|
||||
if instance.spec:
|
||||
try:
|
||||
spec_data = json.loads(instance.spec)
|
||||
instance.image = spec_data.get("image", "")
|
||||
instance.replicas = spec_data.get("replicas", 0)
|
||||
# Initialize with empty values
|
||||
instance.current_image = ""
|
||||
instance.current_replicas = 0
|
||||
instance.current_ingress_hosts = ""
|
||||
instance.current_cpu_request = ""
|
||||
instance.current_cpu_limit = ""
|
||||
instance.current_memory_request = ""
|
||||
instance.current_memory_limit = ""
|
||||
instance.current_ingress_enabled = False
|
||||
|
||||
# Extract ingress hosts
|
||||
ingress = spec_data.get("ingress", {})
|
||||
hosts = ingress.get("hosts", [])
|
||||
instance.ingress_hosts = ", ".join(hosts) if hosts else ""
|
||||
if not instance.cluster_id or not instance.cluster_id.active:
|
||||
continue
|
||||
|
||||
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:
|
||||
_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.replicas = 0
|
||||
instance.ingress_hosts = ""
|
||||
instance.ready_replicas = 0
|
||||
instance.available_replicas = 0
|
||||
instance.ingress_url = ""
|
||||
instance.conditions = ""
|
||||
else:
|
||||
instance.image = ""
|
||||
instance.replicas = 0
|
||||
instance.ingress_hosts = ""
|
||||
instance.ready_replicas = 0
|
||||
instance.available_replicas = 0
|
||||
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):
|
||||
"""Custom name display"""
|
||||
|
|
@ -179,17 +353,168 @@ class K8sOdooInstance(models.Model):
|
|||
},
|
||||
}
|
||||
|
||||
def action_refresh(self):
|
||||
"""Refresh this instance from Kubernetes"""
|
||||
def action_reset_to_current(self):
|
||||
"""Reset editable fields to current cluster values"""
|
||||
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):
|
||||
"""Transient model for displaying JSON content in a dialog"""
|
||||
|
||||
_name = "k8s.spec.viewer"
|
||||
_description = "Kubernetes Spec/Status Viewer"
|
||||
_description = "Kubernetes Spec Viewer"
|
||||
|
||||
title = fields.Char(string="Title", readonly=True)
|
||||
content = fields.Text(string="Content", readonly=True)
|
||||
|
|
|
|||
|
|
@ -5,28 +5,28 @@
|
|||
<field name="name">k8s.odoo.instance.list</field>
|
||||
<field name="model">k8s.odoo.instance</field>
|
||||
<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="namespace"/>
|
||||
<field name="name"/>
|
||||
<field name="image"/>
|
||||
<field name="replicas"/>
|
||||
<field name="current_image"/>
|
||||
<field name="current_replicas"/>
|
||||
<field name="ready_replicas"/>
|
||||
<field name="phase" widget="badge"/>
|
||||
<field name="url" widget="url"/>
|
||||
<field name="ingress_url" widget="url"/>
|
||||
<field name="last_updated"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Instance Form View -->
|
||||
<record id="view_k8s_odoo_instance_form" model="ir.ui.view">
|
||||
<field name="name">k8s.odoo.instance.form</field>
|
||||
<field name="model">k8s.odoo.instance</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Odoo Instance" create="false" edit="false">
|
||||
<form string="Odoo Instance" create="false">
|
||||
<header>
|
||||
<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"/>
|
||||
</header>
|
||||
<sheet>
|
||||
|
|
@ -40,33 +40,123 @@
|
|||
</div>
|
||||
|
||||
<group>
|
||||
<group string="Basic Information">
|
||||
<field name="image"/>
|
||||
<field name="replicas"/>
|
||||
<field name="url" widget="url"/>
|
||||
<field name="last_updated"/>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="cluster_id"/>
|
||||
<field name="namespace"/>
|
||||
<field name="phase" widget="badge"/>
|
||||
</group>
|
||||
<group string="Network">
|
||||
<field name="ingress_hosts"/>
|
||||
<group>
|
||||
<field name="ingress_url" widget="url"/>
|
||||
<field name="last_updated"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Specification">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<button name="action_view_spec" string="View Full Spec" type="object" class="btn btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
<field name="spec" widget="text" readonly="1"/>
|
||||
<page string="Configuration">
|
||||
<group>
|
||||
<group string="Application">
|
||||
<label for="image_editable" string="Docker Image"/>
|
||||
<div class="o_row">
|
||||
<field name="image_editable" placeholder="e.g., odoo:17.0" class="oe_inline" style="width: 60%;"/>
|
||||
<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 string="Status">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
|
||||
<page string="Status Details">
|
||||
<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"/>
|
||||
</div>
|
||||
</div>
|
||||
<field name="status" widget="text" readonly="1"/>
|
||||
<field name="status" widget="text" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
|
|
@ -83,7 +173,7 @@
|
|||
<field name="name"/>
|
||||
<field name="cluster_id"/>
|
||||
<field name="namespace"/>
|
||||
<field name="image"/>
|
||||
<field name="current_image"/>
|
||||
<field name="url"/>
|
||||
<separator/>
|
||||
<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="Namespace" name="group_namespace" domain="[]" context="{'group_by': 'namespace'}"/>
|
||||
<filter string="Phase" name="group_phase" domain="[]" context="{'group_by': 'phase'}"/>
|
||||
<filter string="Image" name="group_image" domain="[]" context="{'group_by': 'image'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
|
|
|
|||
Loading…
Reference in a new issue