[ADD] base modules
6
kk_odoo_saas/__init__.py
Executable file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import utils
|
||||
from . import wizards
|
||||
50
kk_odoo_saas/__manifest__.py
Executable file
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': "CT Odoo SaaS",
|
||||
|
||||
'summary': """
|
||||
Out of the Box SaaS Module based on Kubernetes""",
|
||||
|
||||
'description': """
|
||||
Out of the Box SaaS Module based on Kubernetes
|
||||
""",
|
||||
|
||||
'author': "Muhammad Awais",
|
||||
'website': "https://codetuple.io",
|
||||
'category': 'Uncategorized',
|
||||
'version': '2.0.0',
|
||||
|
||||
# any module necessary for this one to work correctly
|
||||
'depends': ['base', 'smile_log', 'website_sale', 'product',
|
||||
'portal', 'auth_signup_verify_email', 'sale_subscription',
|
||||
'queue_job'],
|
||||
|
||||
'external_dependencies': {
|
||||
'python': [
|
||||
'kubernetes',
|
||||
],
|
||||
},
|
||||
# always loaded
|
||||
'data': [
|
||||
"security/security.xml",
|
||||
'security/ir.model.access.csv',
|
||||
'wizards/saas_app_delete.xml',
|
||||
'wizards/update_docker_image.xml',
|
||||
'views/app_views.xml',
|
||||
'views/config_views.xml',
|
||||
'views/assets.xml',
|
||||
'views/saas_app_website.xml',
|
||||
'views/templates.xml',
|
||||
'views/sale_subscription.xml',
|
||||
'views/logs_viewer.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/saas_package_views.xml',
|
||||
'data/data.xml',
|
||||
'data/email_templates.xml',
|
||||
],
|
||||
'qweb': [
|
||||
'static/src/xml/base.xml',
|
||||
],
|
||||
"application": True,
|
||||
|
||||
}
|
||||
3
kk_odoo_saas/controllers/__init__.py
Executable file
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import logs_viewer
|
||||
17
kk_odoo_saas/controllers/logs_viewer.py
Executable file
@@ -0,0 +1,17 @@
|
||||
from odoo.http import route, request, Controller
|
||||
import logging
|
||||
from odoo.addons.portal.controllers.portal import CustomerPortal
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SaaSAppsLogViewer(CustomerPortal):
|
||||
@route("/saas/instance/<int:app_id>", type="http", auth="user", methods=['GET'], website=True)
|
||||
def saas_app_log_viewer(self, app_id, **values):
|
||||
saas_app = request.env["kk_odoo_saas.app"].sudo().browse(app_id)
|
||||
if request.params.get('_'):
|
||||
logs = saas_app.get_timed_pod_logs(since_seconds=5)
|
||||
return logs
|
||||
return request.render(
|
||||
"kk_odoo_saas.saas_app_log_viewer", values
|
||||
)
|
||||
103
kk_odoo_saas/data/data.xml
Executable file
@@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="product_attribute_subscription" model="product.attribute">
|
||||
<field name="name">Subscription (SaaS)</field>
|
||||
<field name="sequence">30</field>
|
||||
<field name="display_type">select</field>
|
||||
</record>
|
||||
|
||||
<record id="product_attribute_value_subscription_annually" model="product.attribute.value">
|
||||
<field name="name">Annually</field>
|
||||
<field name="attribute_id" ref="product_attribute_subscription"/>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
|
||||
<record id="product_attribute_value_subscription_monthly" model="product.attribute.value">
|
||||
<field name="name">Monthly</field>
|
||||
<field name="attribute_id" ref="product_attribute_subscription"/>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
|
||||
<record id="product_attribute_value_subscription_trial" model="product.attribute.value">
|
||||
<field name="name">Trial</field>
|
||||
<field name="attribute_id" ref="product_attribute_subscription"/>
|
||||
<field name="sequence">30</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="product_users" model="product.template">
|
||||
<field name="name">Users</field>
|
||||
<field name="sale_ok" eval="True" />
|
||||
<field name="purchase_ok" eval="False" />
|
||||
<field name="is_saas_product" eval="True" />
|
||||
<field name="type">service</field>
|
||||
<field name="recurring_invoice" eval="True"/>
|
||||
<field name="list_price">0</field>
|
||||
</record>
|
||||
|
||||
<record id="product_users_attribute_subscription_line" model="product.template.attribute.line">
|
||||
<field name="product_tmpl_id" ref="product_users" />
|
||||
<field name="attribute_id" ref="product_attribute_subscription" />
|
||||
<field name="value_ids" eval="[(6, 0, [
|
||||
ref('kk_odoo_saas.product_attribute_value_subscription_annually'),
|
||||
ref('product_attribute_value_subscription_monthly'),
|
||||
ref('product_attribute_value_subscription_trial'),
|
||||
])]" />
|
||||
</record>
|
||||
|
||||
<function model="ir.model.data" name="_update_xmlids">
|
||||
<value model="base" eval="[{
|
||||
'xml_id': 'kk_odoo_saas.product_users_attribute_subscription_value_annually',
|
||||
'record': obj().env.ref('kk_odoo_saas.product_users_attribute_subscription_line').product_template_value_ids[0],
|
||||
'noupdate': True,
|
||||
}, {
|
||||
'xml_id': 'kk_odoo_saas.product_users_attribute_subscription_value_monthly',
|
||||
'record': obj().env.ref('kk_odoo_saas.product_users_attribute_subscription_line').product_template_value_ids[1],
|
||||
'noupdate': True,
|
||||
}, {
|
||||
'xml_id': 'kk_odoo_saas.product_users_attribute_subscription_value_trial',
|
||||
'record': obj().env.ref('kk_odoo_saas.product_users_attribute_subscription_line').product_template_value_ids[2],
|
||||
'noupdate': True,
|
||||
}]"/>
|
||||
</function>
|
||||
|
||||
<record id="product_users_attribute_subscription_value_annually" model="product.template.attribute.value">
|
||||
<field name="price_extra">120</field>
|
||||
</record>
|
||||
|
||||
<record id="product_users_attribute_subscription_value_monthly" model="product.template.attribute.value">
|
||||
<field name="price_extra">12.5</field>
|
||||
</record>
|
||||
|
||||
<record id="product_users_attribute_subscription_value_trial" model="product.template.attribute.value">
|
||||
<field name="price_extra">0</field>
|
||||
</record>
|
||||
|
||||
<function model="ir.model.data" name="_update_xmlids">
|
||||
<value model="base" eval="[{
|
||||
'xml_id': 'kk_odoo_saas.product_users_monthly',
|
||||
'record': obj().env.ref('kk_odoo_saas.product_users')._get_variant_for_combination(obj().env.ref('kk_odoo_saas.product_users_attribute_subscription_value_monthly')),
|
||||
'noupdate': True,
|
||||
}, {
|
||||
'xml_id': 'kk_odoo_saas.product_users_annually',
|
||||
'record': obj().env.ref('kk_odoo_saas.product_users')._get_variant_for_combination(obj().env.ref('kk_odoo_saas.product_users_attribute_subscription_value_annually')),
|
||||
'noupdate': True,
|
||||
}, {
|
||||
'xml_id': 'kk_odoo_saas.product_users_trial',
|
||||
'record': obj().env.ref('kk_odoo_saas.product_users')._get_variant_for_combination(obj().env.ref('kk_odoo_saas.product_users_attribute_subscription_value_trial')),
|
||||
'noupdate': True,
|
||||
},]"/>
|
||||
</function>
|
||||
|
||||
<data noupdate='1'>
|
||||
<record id="app_backup_sequence" model="ir.sequence">
|
||||
<field name="name">Backup Name</field>
|
||||
<field name="code">saas_app.backup</field>
|
||||
<field name="prefix">BACKUP</field>
|
||||
<field name="padding">6</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
</odoo>
|
||||
143
kk_odoo_saas/data/email_templates.xml
Executable file
@@ -0,0 +1,143 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- Email template for new users app -->
|
||||
<record id="app_invitation_email" model="mail.template">
|
||||
<field name="name">KK SaaS: App Invitation</field>
|
||||
<field name="model_id" ref="kk_odoo_saas.model_kk_odoo_saas_app"/>
|
||||
<field name="subject">Here are the Credentials of your Instance</field>
|
||||
<field name="email_from">"${object.admin_user.company_id.name | safe}" <${(object.admin_user.company_id.email ) | safe}></field>
|
||||
<field name="email_to">${object.admin_user.email_formatted | safe}</field>
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" cellspacing="0"
|
||||
style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590"
|
||||
style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
|
||||
<tbody>
|
||||
<!-- HEADER -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590"
|
||||
style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||
<tr>
|
||||
<td valign="middle">
|
||||
<span style="font-size: 10px;">Welcome to SaaS</span>
|
||||
<br/>
|
||||
<span style="font-size: 20px; font-weight: bold;">
|
||||
${object.admin_user.name}
|
||||
</span>
|
||||
</td>
|
||||
<td valign="middle" align="right">
|
||||
<img src="/logo.png?company=${object.admin_user.company_id.id}"
|
||||
style="padding: 0px; margin: 0px; height: auto; width: 80px;"
|
||||
alt="${object.admin_user.company_id.name}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" style="text-align:center;">
|
||||
<hr width="100%"
|
||||
style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- CONTENT -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590"
|
||||
style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||
<tr>
|
||||
<td valign="top" style="font-size: 13px;">
|
||||
<div>
|
||||
Dear ${object.admin_user.name},
|
||||
<br/>
|
||||
<br/>
|
||||
You have been invited by ${object.create_uid.name} of
|
||||
${object.admin_user.company_id.name} to connect on SaaS App.
|
||||
% set website_url = object.get_url()
|
||||
<br/>
|
||||
Your SaaS domain is:
|
||||
<b>
|
||||
<a href='${website_url}'>${website_url}</a>
|
||||
</b>
|
||||
<br/>
|
||||
Your sign in email is:
|
||||
<b>
|
||||
<a href="${website_url}/web/login?login=${object.admin_user.email}"
|
||||
target="_blank">${object.admin_user.email}
|
||||
</a>
|
||||
</b>
|
||||
<br/>
|
||||
Your Password is:
|
||||
<b>
|
||||
<p>${object.login_pwd}</p>
|
||||
</b>
|
||||
<br/>
|
||||
|
||||
<br/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
Enjoy SaaS!
|
||||
<br/>
|
||||
--<br/>The ${object.admin_user.company_id.name} Team
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align:center;">
|
||||
<hr width="100%"
|
||||
style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- FOOTER -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590"
|
||||
style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||
<tr>
|
||||
<td valign="middle" align="left">
|
||||
${object.admin_user.company_id.name}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="middle" align="left" style="opacity: 0.7;">
|
||||
${object.admin_user.company_id.phone}
|
||||
% if object.admin_user.company_id.email
|
||||
|
|
||||
<a href="'mailto:%s' % ${object.admin_user.company_id.email}"
|
||||
style="text-decoration:none; color: #454748;">
|
||||
${object.admin_user.company_id.email}
|
||||
</a>
|
||||
% endif
|
||||
% if object.admin_user.company_id.website
|
||||
|
|
||||
<a href="'%s' % ${object.admin_user.company_id.website}"
|
||||
style="text-decoration:none; color: #454748;">
|
||||
${object.admin_user.company_id.website}
|
||||
</a>
|
||||
% endif
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</field>
|
||||
<field name="lang">${object.admin_user.lang}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
13
kk_odoo_saas/models/__init__.py
Executable file
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import k8s_config
|
||||
from . import saas_period_product_mixin
|
||||
from . import product_template
|
||||
from . import saas_app_website
|
||||
from . import sale_subscription
|
||||
from . import sale_order
|
||||
from . import res_config_settings
|
||||
from . import saas_package
|
||||
|
||||
|
||||
80
kk_odoo_saas/models/cluster.py
Executable file
@@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class SaasK8sClusterNS(models.Model):
|
||||
_name = 'kk_odoo_saas.app.cluster.ns'
|
||||
_description = 'SaaS Cluster NameSpace'
|
||||
|
||||
name = fields.Char()
|
||||
status = fields.Char()
|
||||
age = fields.Char()
|
||||
all_json = fields.Text('Complete json')
|
||||
|
||||
|
||||
class SaasK8sClusterPod(models.Model):
|
||||
_name = 'kk_odoo_saas.app.cluster.pod'
|
||||
_description = 'SaaS Cluster Pod'
|
||||
|
||||
name = fields.Char()
|
||||
ns = fields.Char()
|
||||
ready = fields.Char()
|
||||
status = fields.Char()
|
||||
restarts = fields.Char()
|
||||
age = fields.Char()
|
||||
all_json = fields.Text('Complete json')
|
||||
|
||||
|
||||
class SaasK8sClusterDeployment(models.Model):
|
||||
_name = 'kk_odoo_saas.app.cluster.deployment'
|
||||
_description = 'SaaS Cluster Deployment'
|
||||
|
||||
name = fields.Char()
|
||||
ns = fields.Char()
|
||||
ready = fields.Char()
|
||||
age = fields.Char()
|
||||
all_json = fields.Text('Complete json')
|
||||
|
||||
|
||||
class SaasK8sClusterIngress(models.Model):
|
||||
_name = 'kk_odoo_saas.app.cluster.ingress'
|
||||
_description = 'SaaS Cluster Ingress'
|
||||
|
||||
name = fields.Char()
|
||||
ns = fields.Char()
|
||||
hosts = fields.Char()
|
||||
ing_class = fields.Char()
|
||||
addresses = fields.Char()
|
||||
ports = fields.Char()
|
||||
age = fields.Char()
|
||||
all_json = fields.Text('Complete json')
|
||||
|
||||
|
||||
class SaasK8sClusterService(models.Model):
|
||||
_name = 'kk_odoo_saas.app.cluster.service'
|
||||
_description = 'SaaS Cluster Service'
|
||||
|
||||
name = fields.Char()
|
||||
type_ = fields.Char()
|
||||
cluster_ip = fields.Char()
|
||||
external_ip = fields.Char()
|
||||
ports = fields.Char()
|
||||
age = fields.Char()
|
||||
all_json = fields.Text('Complete json')
|
||||
|
||||
|
||||
class SaasK8sClusterPV(models.Model):
|
||||
_name = 'kk_odoo_saas.app.cluster.pv'
|
||||
_description = 'SaaS Cluster PV'
|
||||
|
||||
name = fields.Char()
|
||||
capacity = fields.Char()
|
||||
access_modes = fields.Char()
|
||||
reclaim_policy = fields.Char()
|
||||
status = fields.Char()
|
||||
claim = fields.Char()
|
||||
storage_class = fields.Char()
|
||||
reason = fields.Char()
|
||||
age = fields.Char()
|
||||
all_json = fields.Text('Complete json')
|
||||
125
kk_odoo_saas/models/k8s_config.py
Executable file
@@ -0,0 +1,125 @@
|
||||
import psycopg2
|
||||
from odoo import models, fields, api
|
||||
from kubernetes import config, client
|
||||
from odoo.exceptions import UserError
|
||||
import yaml
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class SaaSAppK8sConfig(models.Model):
|
||||
_name = 'kk_odoo_saas.k8s.config'
|
||||
_description = 'Kubernetes Cluster Configuration'
|
||||
name = fields.Char()
|
||||
config_file = fields.Text('Yaml Configuration File')
|
||||
cluster_name = fields.Char('')
|
||||
namespaces = fields.Text('Namespaces in Cluster')
|
||||
domain_name = fields.Char('Domain Name')
|
||||
|
||||
def check_connectivity(self):
|
||||
if self.config_file:
|
||||
try:
|
||||
data2 = yaml.safe_load(self.config_file)
|
||||
config.load_kube_config_from_dict(data2)
|
||||
v1 = client.CoreV1Api()
|
||||
response = v1.list_namespace()
|
||||
nss = []
|
||||
for namespace in response.items:
|
||||
nss.append(namespace.metadata.name)
|
||||
nl = '\n'
|
||||
self.namespaces = nl.join(nss)
|
||||
except config.config_exception.ConfigException as e:
|
||||
raise UserError("Unable to Connect K8s Cluster")
|
||||
else:
|
||||
raise UserError("Please Add config file")
|
||||
|
||||
def validate_domain_name(self):
|
||||
if self.domain_name:
|
||||
pass
|
||||
|
||||
def get_default_config(self):
|
||||
for conf in self.search([], limit=1):
|
||||
return conf
|
||||
|
||||
def update_cluster_nodes(self):
|
||||
if self.config_file:
|
||||
try:
|
||||
data2 = yaml.safe_load(self.config_file)
|
||||
config.load_kube_config_from_dict(data2)
|
||||
v1 = client.CoreV1Api()
|
||||
response = v1.list_node()
|
||||
node_env = self.env['kk_odoo_saas.k8s.node']
|
||||
if response:
|
||||
node_env.search([]).unlink()
|
||||
for node in response.items:
|
||||
node_env.create({'name': node.metadata.name, 'labels': str(node.metadata.labels), 'annotations': str(node.metadata.annotations), 'taints': str(node.spec.taints), 'yaml_info': str(node)})
|
||||
except config.config_exception.ConfigException as e:
|
||||
_logger.error(e)
|
||||
raise UserError("Unable to Connect K8s Cluster")
|
||||
else:
|
||||
raise UserError("Please Add config file")
|
||||
|
||||
|
||||
class DockerImages(models.Model):
|
||||
_name = 'kk_odoo_saas.k8s.docker.images'
|
||||
|
||||
name = fields.Char('Image Name', required=True)
|
||||
tag = fields.Char('Tag Name', required=True, default='latest')
|
||||
description = fields.Char('Description')
|
||||
is_pvt_dkr_repo = fields.Boolean('Using Private Docker Repository')
|
||||
b64_dkr_config = fields.Text('base64 docker config json file')
|
||||
repo_link = fields.Char('Related Repository')
|
||||
base_version = fields.Selection([('14.0', '14.0'), ('15.0', '15.0'), ('16.0', '16.0')], required=True)
|
||||
# base_version is for pulling git code in folder e.g /var/lib/odoo/addons/14.0 etc.
|
||||
|
||||
@api.depends('name', 'tag')
|
||||
def name_get(self):
|
||||
res = []
|
||||
for record in self:
|
||||
name = record.name
|
||||
if record.tag:
|
||||
name = name + ':' + record.tag
|
||||
res.append((record.id, name))
|
||||
return res
|
||||
|
||||
|
||||
class Node(models.Model):
|
||||
_name = 'kk_odoo_saas.k8s.node'
|
||||
name = fields.Char('Node Name')
|
||||
labels = fields.Text('Labels')
|
||||
annotations = fields.Text()
|
||||
taints = fields.Text()
|
||||
|
||||
yaml_info = fields.Text("Yaml Description")
|
||||
|
||||
|
||||
class MasterDbCreds(models.Model):
|
||||
_name = 'kk_odoo_saas.k8s.master_db_creds'
|
||||
|
||||
name = fields.Char('DB Server Name')
|
||||
master_username = fields.Char('Master Username', default='postgres', required=True)
|
||||
master_pass = fields.Char('Master Password', required=True)
|
||||
server_url = fields.Char('Server URL', required=True)
|
||||
server_port = fields.Char('Server Port', default='5432', required=True)
|
||||
status = fields.Selection([('connected', 'Connected'), ('not_connected', 'Not Connected')], default='not_connected')
|
||||
|
||||
def check_connectivity(self):
|
||||
for rec in self:
|
||||
if rec.master_username and rec.master_pass and rec.server_url:
|
||||
try:
|
||||
conn = psycopg2.connect(database='postgres',
|
||||
user=rec.master_username,
|
||||
password=rec.master_pass,
|
||||
host=rec.server_url,
|
||||
port=rec.server_port or 5432)
|
||||
if conn:
|
||||
_logger.info("Connected to PG DB server")
|
||||
self.status = 'connected'
|
||||
return conn
|
||||
except Exception as e:
|
||||
_logger.exception(e)
|
||||
self.status = 'not_connected'
|
||||
raise UserError('Unable to Connect Postgres.\nPlease Check Postgres Credentials...!')
|
||||
else:
|
||||
self.status = 'not_connected'
|
||||
raise UserError('Please Enter Postgres Credentials...!')
|
||||
486
kk_odoo_saas/models/models.py
Executable file
@@ -0,0 +1,486 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import base64
|
||||
|
||||
import psycopg2
|
||||
import requests
|
||||
import os
|
||||
from odoo import models, fields, api, _
|
||||
from ..utils import k8s_deployment as k8s
|
||||
from ..utils import ingress, logs
|
||||
from ..utils import del_git_code as dc
|
||||
import re
|
||||
from odoo.exceptions import ValidationError, MissingError
|
||||
from odoo.addons.smile_log.tools import SmileDBLogger
|
||||
import logging
|
||||
import xmlrpc.client
|
||||
import random
|
||||
import string
|
||||
from odoo.addons.queue_job.exception import RetryableJobError
|
||||
from odoo.exceptions import AccessError
|
||||
from datetime import timedelta
|
||||
from ..utils import pg_server as pgx
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SaaSAppSslSecret(models.Model):
|
||||
_name = 'kk_odoo_saas.app.ssl_secret'
|
||||
name = fields.Char('Secret Name')
|
||||
|
||||
|
||||
class SaaSApp(models.Model):
|
||||
_name = 'kk_odoo_saas.app'
|
||||
_description = 'SaaS App'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
app_name = fields.Char('App Unique Id',
|
||||
default=lambda self: self.env['ir.sequence'].next_by_code('kk_odoo_saas.app'), tracking=True, copy=False)
|
||||
name = fields.Char(tracking=True)
|
||||
is_custom_image = fields.Boolean(default=True)
|
||||
docker_image = fields.Many2one('kk_odoo_saas.k8s.docker.images')
|
||||
is_pvt_dkr_repo = fields.Boolean('Using Private Docker Repository')
|
||||
|
||||
is_extra_addon = fields.Boolean('Use Extra Addons')
|
||||
extra_addons = fields.Char('Git Url', tracking=True)
|
||||
is_private_repo = fields.Boolean('Is Private Repository?')
|
||||
git_token = fields.Char('Auth Token')
|
||||
|
||||
# K8s values
|
||||
client = fields.Many2one('res.partner', related='admin_user.partner_id', tracking=True)
|
||||
country_id = fields.Many2one(string="Country", comodel_name='res.country',
|
||||
help="Country for which this instance is being deployed")
|
||||
|
||||
admin_user = fields.Many2one("res.users", "Client User", tracking=True)
|
||||
|
||||
def _get_default_cluster_config(self):
|
||||
cluster = self.env['kk_odoo_saas.k8s.config'].search([], limit=1)
|
||||
if cluster:
|
||||
return cluster.id
|
||||
return False
|
||||
|
||||
configuration = fields.Many2one('kk_odoo_saas.k8s.config', string='Configuration', default=_get_default_cluster_config)
|
||||
|
||||
domain_name = fields.Char(related='configuration.domain_name')
|
||||
sub_domain_name = fields.Char(required=True)
|
||||
|
||||
is_dedicated_node = fields.Boolean(string='Any Dedicated Node')
|
||||
node_id = fields.Many2one('kk_odoo_saas.k8s.node', string='Node')
|
||||
node_key = fields.Char()
|
||||
node_value = fields.Char()
|
||||
|
||||
demo_data = fields.Boolean('Install Demo Data')
|
||||
status = fields.Selection([('d', 'Draft'), ('l', 'Launched'), ('m', 'Modified'), ('del', 'Deleted')],
|
||||
string='Status', default='d', tracking=True)
|
||||
expiry_date = fields.Date(tracking=True)
|
||||
subscription_id = fields.Many2one('sale.subscription', string='Related Subscription', tracking=True)
|
||||
notes = fields.Text()
|
||||
module_ids = fields.Many2many(comodel_name='saas.app', string='Modules to install')
|
||||
|
||||
login_email = fields.Char('Login Email')
|
||||
login_pwd = fields.Char('Login Pwd')
|
||||
|
||||
master_login_email = fields.Char('Master Login Email')
|
||||
master_login_pwd = fields.Char('Master Login Pwd')
|
||||
|
||||
custom_domain_ids = fields.One2many('saas.app.custom.domain', 'saas_app_id', string='Custom Domains')
|
||||
|
||||
def _default_db_name(self):
|
||||
return self.sub_domain_name
|
||||
|
||||
k8s_logs = fields.Many2many('smile.log', string='K8s Logs', compute='_get_k8s_logs')
|
||||
|
||||
# db server relation
|
||||
def _get_default_db_server(self):
|
||||
db_server = self.env['kk_odoo_saas.k8s.master_db_creds'].search([], limit=1)
|
||||
if db_server:
|
||||
return db_server
|
||||
return False
|
||||
db_server_id = fields.Many2one('kk_odoo_saas.k8s.master_db_creds', string="DB Server", default=_get_default_db_server)
|
||||
client_db_name = fields.Char("Database Name", required=True)
|
||||
login_url = fields.Char('Login URL', compute='_get_instance_login_url')
|
||||
|
||||
# _sql_constraints = [
|
||||
# ('hostname_uniq', 'unique(hostname)', "A Domain already exists. Domain's name must be unique!"),
|
||||
# ]
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
_logger = logging.getLogger(__name__)
|
||||
if self:
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
res = super(SaaSApp, self).create(values)
|
||||
|
||||
if not res.validate_domain_name():
|
||||
_logger.error('Either Domain or Subdomain is not valid')
|
||||
raise ValidationError('Either Domain or Subdomain is not valid')
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
# if vals and 'status' not in vals and self.status not in ['d', 'del']:
|
||||
# vals.update({'status': 'm'})
|
||||
|
||||
res = super(SaaSApp, self).write(vals)
|
||||
|
||||
if 'custom_domain_ids' in vals:
|
||||
#todo: add validation, limit number of domains per instance
|
||||
self.update_app()
|
||||
|
||||
if 'sub_domain_name' or 'domain_name' in vals:
|
||||
if not self.validate_domain_name():
|
||||
_logger.error('Either Domain or Subdomain is not valid')
|
||||
raise ValidationError('Either Domain or Subdomain is not valid')
|
||||
return res
|
||||
|
||||
|
||||
@api.onchange('app_name')
|
||||
def set_sub_domain_name(self):
|
||||
self.sub_domain_name = self.app_name
|
||||
# also set the database name
|
||||
self.client_db_name = self.app_name
|
||||
|
||||
def validate_domain_name(self):
|
||||
if self.domain_name and self.sub_domain_name:
|
||||
full_name = self.sub_domain_name + self.domain_name
|
||||
domain_regex = r'(([\da-zA-Z])([_\w-]{,62})\.){,127}(([\da-zA-Z])[_\w-]{,61})?([\da-zA-Z]\.((xn\-\-[a-zA-Z\d]+)|([a-zA-Z\d]{2,})))'
|
||||
domain_regex = '{0}$'.format(domain_regex)
|
||||
valid_domain_name_regex = re.compile(domain_regex, re.IGNORECASE)
|
||||
full_name = full_name.lower().strip()
|
||||
if re.match(valid_domain_name_regex, full_name):
|
||||
return True
|
||||
return
|
||||
|
||||
def deploy_app(self):
|
||||
self.ensure_one()
|
||||
k8s.create_deployment(app_name=self.app_name, config_file=self.configuration.config_file, self=self)
|
||||
ingress.create_ingress(app_name=self.app_name, self=self)
|
||||
self.status = 'l'
|
||||
self.with_delay().post_init_tasks()
|
||||
|
||||
def delete_app_from_wizard(self, delete_db, delete_pv, delete_svc, delete_ing, delete_deployment):
|
||||
# if delete_db:
|
||||
# self.delete_database_remotely(db_master_pwd=db_master_pwd)
|
||||
k8s.delete_app_with_options(self, delete_db, delete_pv, delete_svc, delete_ing, delete_deployment)
|
||||
|
||||
self.status = 'del'
|
||||
|
||||
def update_app(self):
|
||||
k8s.update_app(self)
|
||||
self.status = 'l'
|
||||
|
||||
def get_url(self):
|
||||
return "http://{0}{1}".format(self.sub_domain_name, self.domain_name)
|
||||
|
||||
def deploy_apps_from_git(self):
|
||||
k8s.deploy_apps_from_git(self)
|
||||
|
||||
def restart_odoo_service(self):
|
||||
k8s.restart_odoo_service(self)
|
||||
|
||||
def action_show_subscription(self):
|
||||
self.ensure_one()
|
||||
assert self.subscription_id, "This app is not associated with any Subscription"
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Subscription",
|
||||
"res_model": "sale.subscription",
|
||||
"res_id": self.subscription_id.id,
|
||||
"view_mode": "form",
|
||||
}
|
||||
|
||||
def action_create_subscription(self):
|
||||
self.ensure_one()
|
||||
assert not self.subscription_id, "This app is already associated with Subscription"
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Subscription",
|
||||
"res_model": "sale.subscription",
|
||||
"view_mode": "form",
|
||||
"context": {
|
||||
"default_name": self.name + "'s SaaS Subscription",
|
||||
"default_build_id": self.id,
|
||||
"default_partner_id": self.client.id,
|
||||
}
|
||||
}
|
||||
|
||||
def _get_instance_login_url(self):
|
||||
for app in self:
|
||||
app.login_url = ''
|
||||
response, db = pgx.get_admin_credentials(app)
|
||||
if response and db:
|
||||
app.login_url = "https://{0}{1}/saas/login?db={2}&login={3}&passwd={4}".format(self.sub_domain_name,
|
||||
self.domain_name, db, response[0][0],
|
||||
response[0][1])
|
||||
else:
|
||||
_logger.info("Unknown Error!")
|
||||
|
||||
def action_connect_instance(self):
|
||||
self.ensure_one()
|
||||
response, db = pgx.get_admin_credentials(self)
|
||||
if response and db:
|
||||
login = response[0][0]
|
||||
password = response[0][1]
|
||||
login_url = "https://{0}{1}/saas/login?db={2}&login={3}&passwd={4}".format(self.sub_domain_name, self.domain_name, db, login, password)
|
||||
_logger.info("Login URL %r " % (login_url))
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': login_url,
|
||||
'target': 'new',
|
||||
}
|
||||
else:
|
||||
_logger.info("Unknown Error!")
|
||||
|
||||
|
||||
def create_instance_admin_user_for_client(self, models1, db, uid, password, client_pwd):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
try:
|
||||
adm_user_id = models1.execute_kw(db, uid, password,
|
||||
'res.users', 'search_read',
|
||||
[[['login', '=', 'admin']]], {'fields': ['groups_id'], 'limit': 1}
|
||||
)
|
||||
if adm_user_id:
|
||||
adm_user_id = adm_user_id[0]
|
||||
groups_ids = adm_user_id.get('groups_id', False)
|
||||
if groups_ids:
|
||||
new_user_id = models1.execute_kw(db, uid, password, 'res.users', 'create',
|
||||
[{'name': self.client.name,
|
||||
'login': self.client.email,
|
||||
'company_ids': [1], 'company_id': 1,
|
||||
'password': client_pwd}])
|
||||
if new_user_id:
|
||||
_logger.info('Created client Account on instance')
|
||||
return models1.execute_kw(db, uid, password, 'res.groups', 'write',
|
||||
[groups_ids, {'users': [(4, new_user_id)]}])
|
||||
except xmlrpc.client.Error as e:
|
||||
_logger.error(str(e))
|
||||
return
|
||||
|
||||
def reset_apps_admin_pwd(self):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
protocol = 'https'
|
||||
url = protocol + '://{0}{1}'.format(self.sub_domain_name, self.domain_name)
|
||||
db = self.sub_domain_name
|
||||
new_pwd_client = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10))
|
||||
new_pwd_master = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10))
|
||||
username = 'admin'
|
||||
password = 'admin'
|
||||
try:
|
||||
common = xmlrpc.client.ServerProxy('{}/xmlrpc/2/common'.format(url))
|
||||
uid = common.authenticate(db, username, password, {})
|
||||
_logger.info('Sending request to app with uid {}'.format(uid))
|
||||
models1 = xmlrpc.client.ServerProxy('{}/xmlrpc/2/object'.format(url))
|
||||
try:
|
||||
if self.create_instance_admin_user_for_client(models1, db, uid, password, new_pwd_client):
|
||||
_logger.info('Updated Client User\'s Access Rights on instance')
|
||||
self.update({'login_pwd': new_pwd_client, 'login_email': self.client.email})
|
||||
|
||||
adm_user_id = models1.execute_kw(db, uid, password,
|
||||
'res.users', 'search',
|
||||
[[['login', '=', 'admin']]], {'limit': 1}
|
||||
)[0]
|
||||
if models1.execute_kw(db, uid, password, 'res.users', 'write', [[adm_user_id], {
|
||||
'password': new_pwd_master,
|
||||
}]):
|
||||
self.update({'master_login_pwd': new_pwd_master, 'master_login_email': 'admin'})
|
||||
self.send_app_pwd_cred_email()
|
||||
_logger.info('Password and login changed Successfully')
|
||||
except xmlrpc.client.Error as e:
|
||||
_logger.error(str(e))
|
||||
except xmlrpc.client.Error as e:
|
||||
_logger.error(str(e))
|
||||
|
||||
def set_user_country(self):
|
||||
country_code = self.country_id.code
|
||||
if self.country_id and self.country_id.code:
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
protocol = 'https'
|
||||
url = protocol + '://{0}{1}'.format(self.sub_domain_name, self.domain_name)
|
||||
db = self.sub_domain_name
|
||||
username = 'admin'
|
||||
password = 'admin'
|
||||
try:
|
||||
common = xmlrpc.client.ServerProxy('{}/xmlrpc/2/common'.format(url))
|
||||
uid = common.authenticate(db, username, password, {})
|
||||
_logger.info('Sending request to app with uid {}'.format(uid))
|
||||
models1 = xmlrpc.client.ServerProxy('{}/xmlrpc/2/object'.format(url))
|
||||
try:
|
||||
country = models1.execute_kw(db, uid, password,
|
||||
'res.country', 'search_read',
|
||||
[[['code', 'ilike', country_code]]], {'fields': ['id'], 'limit': 1}
|
||||
)
|
||||
if country:
|
||||
country = country[0]
|
||||
if country:
|
||||
new_user_id = models1.execute_kw(db, uid, password, 'res.company', 'write',
|
||||
[[1], {'country_id': country_code and self.country_id.id,
|
||||
'currency_id': country_code and self.country_id.currency_id.id}])
|
||||
if new_user_id:
|
||||
_logger.info('Updated country of the user')
|
||||
except xmlrpc.client.Error as e:
|
||||
_logger.error(str(e))
|
||||
except xmlrpc.client.Error as e:
|
||||
_logger.error(str(e))
|
||||
|
||||
def post_init_tasks(self):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
if not self.check_site_assible():
|
||||
_logger.info('Waiting for the App to become live....')
|
||||
raise RetryableJobError('Unable to get the app live.')
|
||||
else:
|
||||
self.set_user_country()
|
||||
self.reset_apps_admin_pwd()
|
||||
|
||||
def check_site_assible(self):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
try:
|
||||
resp = requests.get('http://' + self.sub_domain_name + self.domain_name)
|
||||
_logger.info('App, sent this status code {}'.format(resp.status_code))
|
||||
if resp.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception as e:
|
||||
_logger.error(str(e))
|
||||
return False
|
||||
|
||||
def send_app_pwd_cred_email(self):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
template = False
|
||||
try:
|
||||
template = self.env.ref('kk_odoo_saas.app_invitation_email', raise_if_not_found=False)
|
||||
except ValueError:
|
||||
pass
|
||||
assert template._name == 'mail.template'
|
||||
|
||||
template_values = {
|
||||
'email_to': '${object.admin_user.email|safe}',
|
||||
'email_cc': False,
|
||||
'auto_delete': True,
|
||||
'partner_to': False,
|
||||
'scheduled_date': False,
|
||||
}
|
||||
template.write(template_values)
|
||||
|
||||
if not self.admin_user.email:
|
||||
_logger.error(_("Cannot send email: user %s has no email address.", self.admin_user.name))
|
||||
with self.env.cr.savepoint():
|
||||
try:
|
||||
template.send_mail(self.id, force_send=True, raise_exception=True)
|
||||
except Exception as e:
|
||||
_logger.error(str(e))
|
||||
_logger.info(_("App Details email sent for user <%s> to <%s>", self.admin_user.login, self.admin_user.email))
|
||||
|
||||
def _message_get_suggested_recipients(self):
|
||||
recipients = super(SaaSApp, self)._message_get_suggested_recipients()
|
||||
try:
|
||||
for saas_app in self:
|
||||
if saas_app.client:
|
||||
saas_app._message_add_suggested_recipient(recipients, partner=saas_app.client, reason=_('SaaS Client'))
|
||||
except AccessError: # no read access rights -> just ignore suggested recipients because this imply modifying followers
|
||||
pass
|
||||
return recipients
|
||||
|
||||
def _get_k8s_logs(self):
|
||||
for app in self:
|
||||
app.k8s_logs = False
|
||||
logs_ = self.env['smile.log'].search([('res_id', '=', app.id), ('model_name', '=', self._name)])
|
||||
for log in logs_:
|
||||
app.k8s_logs = [(4, log.id)]
|
||||
|
||||
def get_pod_logs(self):
|
||||
output = logs.read_logs(app_name=self.app_name, config_file=self.configuration.config_file, self=self, tail_lines=None)
|
||||
if output:
|
||||
result = base64.b64encode(output.encode())
|
||||
attachment_obj = self.env['ir.attachment']
|
||||
# create attachment
|
||||
attachment_id = attachment_obj.create(
|
||||
{'name': self.app_name+'-odoo-logs.log', 'datas': result, 'public': False})
|
||||
# prepare download url
|
||||
download_url = '/web/content/' + str(attachment_id.id) + '?download=true'
|
||||
# download
|
||||
return {
|
||||
"type": "ir.actions.act_url",
|
||||
"url": str(download_url),
|
||||
"target": "new",
|
||||
}
|
||||
else:
|
||||
raise MissingError('Unable to get logs \nReason: Running Pod / Container not found')
|
||||
|
||||
def action_log_viewer(self):
|
||||
return {
|
||||
"type": "ir.actions.act_url",
|
||||
"url": "/saas/instance/{app_id}".format(app_id=self.id),
|
||||
"target": "new",
|
||||
}
|
||||
|
||||
def get_timed_pod_logs(self, interval=None, since_seconds=None, previous=None, tail_lines=None):
|
||||
output = logs.read_logs(app_name=self.app_name, config_file=self.configuration.config_file, self=self, since_seconds=since_seconds)
|
||||
if output:
|
||||
return output
|
||||
|
||||
def update_docker_image(self, container_arguments, env_vars=False):
|
||||
patched_deployment = k8s.update_deployment(self=self, container_arguments=container_arguments, env_vars=env_vars)
|
||||
if patched_deployment:
|
||||
self.env['bus.bus'].sendone(
|
||||
(self._cr.dbname, 'res.partner', self.env.user.partner_id.id),
|
||||
{'type': 'simple_notification', 'title': 'Image Update in Progress',
|
||||
'message': 'Deployment in Progress with latest docker image'}
|
||||
)
|
||||
return True
|
||||
|
||||
def get_odoo_deployment(self):
|
||||
deployment = k8s.read_deployment(self=self)
|
||||
if deployment:
|
||||
return deployment
|
||||
return False
|
||||
|
||||
def refresh_node_list(self):
|
||||
if self.configuration:
|
||||
self.configuration.update_cluster_nodes()
|
||||
|
||||
def get_pg_db_connection(self, db='postgres'):
|
||||
for rec in self:
|
||||
if rec.db_server_id.master_pass and rec.db_server_id.master_username and rec.db_server_id.server_url:
|
||||
try:
|
||||
_logger.info("Going to connect to PG DB server")
|
||||
conn = psycopg2.connect(database=db,
|
||||
user=rec.db_server_id.master_username,
|
||||
password=rec.db_server_id.master_pass,
|
||||
host=rec.db_server_id.server_url,
|
||||
port=rec.db_server_id.server_port or 5432
|
||||
)
|
||||
if conn:
|
||||
_logger.info("Connected to PG DB server")
|
||||
return conn
|
||||
except Exception as e:
|
||||
_logger.exception(e)
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
def del_git_dir(self):
|
||||
base_version = self.docker_image.base_version
|
||||
delete_path = "/var/lib/odoo/addons/" + str(base_version)
|
||||
dc.del_git_dir(self, path=delete_path)
|
||||
|
||||
|
||||
class SaaSAppDomain(models.Model):
|
||||
_name = 'saas.app.custom.domain'
|
||||
_description = 'SaaS App Custom Domain'
|
||||
|
||||
name = fields.Char('Domain Name', required=True)
|
||||
saas_app_id = fields.Many2one('kk_odoo_saas.app')
|
||||
ssl = fields.Boolean('Enable SSL?', default=True)
|
||||
|
||||
|
||||
class DockerAccount(models.Model):
|
||||
_name = 'saas.docker.hub.account'
|
||||
|
||||
username = fields.Char('docker hub username')
|
||||
pwd = fields.Char('Password or Access Token')
|
||||
# for more info https://docs.docker.com/docker-hub/access-tokens/
|
||||
16
kk_odoo_saas/models/product_template.py
Executable file
@@ -0,0 +1,16 @@
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
saas_app_id = fields.Many2one("saas.app", ondelete="cascade", index=True)
|
||||
saas_package_id = fields.Many2one("saas.package", ondelete="cascade", index=True)
|
||||
is_saas_product = fields.Boolean("Is SaaS product?", default=False)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if vals.get("is_saas_product"):
|
||||
vals["taxes_id"] = [(5,)]
|
||||
vals["supplier_taxes_id"] = [(5,)]
|
||||
return super(ProductTemplate, self).create(vals)
|
||||
34
kk_odoo_saas/models/res_config_settings.py
Executable file
@@ -0,0 +1,34 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
show_packages = fields.Boolean(
|
||||
"Show packages", config_parameter="kk_odoo_saas.show_packages"
|
||||
)
|
||||
show_apps = fields.Boolean("Show apps", config_parameter="kk_odoo_saas.show_apps")
|
||||
show_buy_now_button = fields.Boolean(
|
||||
"Show 'Buy now' button", config_parameter="kk_odoo_saas.show_buy_now_button"
|
||||
)
|
||||
show_try_trial_button = fields.Boolean(
|
||||
"Show 'Try trial' button", config_parameter="kk_odoo_saas.show_try_trial_button"
|
||||
)
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
select_type = self.env["ir.config_parameter"].sudo()
|
||||
packages = select_type.get_param("kk_odoo_saas.show_packages")
|
||||
apps = select_type.get_param("kk_odoo_saas.show_apps")
|
||||
buy_now_button = select_type.get_param("kk_odoo_saas.show_buy_now_button")
|
||||
try_trial_button = select_type.get_param("kk_odoo_saas.show_try_trial_button")
|
||||
# fmt: off
|
||||
res.update({
|
||||
"show_packages": packages,
|
||||
"show_apps": apps,
|
||||
"show_buy_now_button": buy_now_button,
|
||||
"show_try_trial_button": try_trial_button,
|
||||
})
|
||||
# fmt: on
|
||||
return res
|
||||
0
kk_odoo_saas/models/res_users.py
Executable file
102
kk_odoo_saas/models/saas_app_website.py
Executable file
@@ -0,0 +1,102 @@
|
||||
import logging
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SaasApp(models.Model):
|
||||
_name = "saas.app"
|
||||
_description = "SaaS App"
|
||||
_order = "name"
|
||||
_inherit = "saas.period.product.mixin"
|
||||
|
||||
name = fields.Char("Technical Name", required=True, index=True)
|
||||
shortdesc = fields.Char("Module Name", required=True)
|
||||
dependency_ids = fields.Many2many("saas.app", "saas_apps_dependency_rel", "dep_id", "app_id", string="Dependencies")
|
||||
icon_image = fields.Binary("Icon")
|
||||
|
||||
allow_to_sell = fields.Boolean(default=True, string="Sellable")
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
res = super(SaasApp, self).create(vals)
|
||||
if not res.product_tmpl_id:
|
||||
res.product_tmpl_id = self.env["product.template"].create({
|
||||
"name": res.shortdesc,
|
||||
"image_1920": res.icon_image,
|
||||
"saas_app_id": res.id,
|
||||
"is_saas_product": True,
|
||||
"type": 'service',
|
||||
"purchase_ok": False,
|
||||
"subscription_template_id": self.env.ref("sale_subscription.monthly_subscription").id,
|
||||
"recurring_invoice": True,
|
||||
"website_published": True,
|
||||
"list_price": 0,
|
||||
})
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
res = super(SaasApp, self).write(vals)
|
||||
if vals.get('month_price', None) is not None or vals.get('year_price', None) is not None:
|
||||
self._update_variant_prices()
|
||||
return res
|
||||
|
||||
def _update_variant_prices(self):
|
||||
for app in self:
|
||||
for variant in app.product_tmpl_id.product_variant_ids:
|
||||
for attr in variant.product_template_attribute_value_ids:
|
||||
if attr.name == "Monthly":
|
||||
attr.update({'price_extra': app.month_price})
|
||||
if attr.name == "Annually":
|
||||
attr.update({'price_extra': app.year_price})
|
||||
|
||||
|
||||
def _search_or_create(self, ir_module):
|
||||
app = self.search([("name", "=", ir_module.name)])
|
||||
if not app:
|
||||
app = self.env["saas.app"].create({
|
||||
"name": ir_module.name,
|
||||
"shortdesc": ir_module.shortdesc,
|
||||
"icon_image": ir_module.icon_image
|
||||
})
|
||||
return app
|
||||
|
||||
def dependencies_str(self):
|
||||
self.ensure_one()
|
||||
visited_saas_module_ids = set()
|
||||
|
||||
def make_list(deps):
|
||||
result = []
|
||||
for dep in deps:
|
||||
if dep.id in visited_saas_module_ids:
|
||||
continue
|
||||
|
||||
visited_saas_module_ids.add(dep.id)
|
||||
result += [dep.name] + make_list(dep.dependency_ids)
|
||||
return result
|
||||
|
||||
return ",".join(make_list(self.dependency_ids))
|
||||
|
||||
@api.model
|
||||
def action_make_applist_from_local_instance(self):
|
||||
for x in map(self.browse, self._search([])):
|
||||
x.unlink()
|
||||
|
||||
def walk(parent_ir_module_name, parent_app_name=None):
|
||||
modules = self.env["ir.module.module.dependency"].sudo().search([("name", "=", parent_ir_module_name)]).mapped("module_id")
|
||||
for m in modules:
|
||||
app_name = None
|
||||
|
||||
if m.application:
|
||||
app = self.env["saas.app"]._search_or_create(m)
|
||||
|
||||
if parent_app_name:
|
||||
app.dependency_ids |= self.env["saas.app"].search([("name", "=", parent_app_name)])
|
||||
|
||||
app_name = app.name
|
||||
else:
|
||||
app_name = parent_app_name
|
||||
|
||||
walk(m.name, app_name)
|
||||
|
||||
walk("base")
|
||||
61
kk_odoo_saas/models/saas_package.py
Executable file
@@ -0,0 +1,61 @@
|
||||
import logging
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class SaasPackage(models.Model):
|
||||
_name = "saas.package"
|
||||
_inherit = ["saas.period.product.mixin"]
|
||||
|
||||
is_published = fields.Boolean("Publish It?", default=True)
|
||||
package_image = fields.Image(
|
||||
string='Package image'
|
||||
)
|
||||
name = fields.Char(copy=False)
|
||||
module_ids = fields.Many2many('saas.app', string="Modules to install")
|
||||
docker_image = fields.Many2one('kk_odoo_saas.k8s.docker.images', 'Related Docker Image')
|
||||
stripe_product_id = fields.Char('Stripe Id')
|
||||
subscription_template = fields.Many2one('sale.subscription.template')
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
res = super(SaasPackage, self).create(vals)
|
||||
if not res.product_tmpl_id:
|
||||
res.product_tmpl_id = self.env["product.template"].create({
|
||||
"name": res.name,
|
||||
"image_1920": res.package_image,
|
||||
"saas_package_id": res.id,
|
||||
"is_saas_product": True,
|
||||
"type": 'service',
|
||||
"purchase_ok": False,
|
||||
"subscription_template_id": self.env.ref("sale_subscription.monthly_subscription").id,
|
||||
"recurring_invoice": True,
|
||||
"website_published": True,
|
||||
"list_price": 0,
|
||||
})
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
res = super(SaasPackage, self).write(vals)
|
||||
if vals.get('month_price', None) is not None or vals.get('year_price', None) is not None:
|
||||
self._update_variant_prices()
|
||||
return res
|
||||
|
||||
def _update_variant_prices(self):
|
||||
for app in self:
|
||||
for variant in app.product_tmpl_id.product_variant_ids:
|
||||
for attr in variant.product_template_attribute_value_ids:
|
||||
if attr.name == "Monthly":
|
||||
attr.update({'price_extra': app.month_price})
|
||||
if attr.name == "Annually":
|
||||
attr.update({'price_extra': app.year_price})
|
||||
|
||||
|
||||
def refresh_page(self):
|
||||
# Empty-function for purpose of refreshing page
|
||||
pass
|
||||
|
||||
54
kk_odoo_saas/models/saas_period_product_mixin.py
Executable file
@@ -0,0 +1,54 @@
|
||||
from odoo import api, models, fields
|
||||
|
||||
|
||||
class SaasPeriodProductMixin(models.AbstractModel):
|
||||
_name = "saas.period.product.mixin"
|
||||
_description = "Period Product Mixin"
|
||||
|
||||
product_tmpl_id = fields.Many2one("product.template", ondelete="cascade", readonly=True)
|
||||
|
||||
month_product_id = fields.Many2one("product.product", string="Product for monthly subscription", compute="_compute_product_ids", store=True)
|
||||
year_product_id = fields.Many2one("product.product", string="Product for annually subscription", compute="_compute_product_ids", store=True)
|
||||
currency_id = fields.Many2one("res.currency", related="product_tmpl_id.currency_id")
|
||||
|
||||
# TODO: when following fields are written, you need to update prices on product.product
|
||||
month_price = fields.Float("Month price", default=0.0)
|
||||
year_price = fields.Float("Year price", default=0.0)
|
||||
|
||||
@api.depends("product_tmpl_id")
|
||||
def _compute_product_ids(self):
|
||||
patvs_month = self.env.ref("kk_odoo_saas.product_attribute_value_subscription_monthly")
|
||||
patvs_year = self.env.ref("kk_odoo_saas.product_attribute_value_subscription_annually")
|
||||
attr = self.env.ref("kk_odoo_saas.product_attribute_subscription")
|
||||
|
||||
for app in self:
|
||||
if not app.product_tmpl_id:
|
||||
app.month_product_id = app.year_product_id = self.env["product.product"]
|
||||
continue
|
||||
|
||||
line = self.env["product.template.attribute.line"].sudo().search([
|
||||
("product_tmpl_id", "=", app.product_tmpl_id.id),
|
||||
("attribute_id", "=", attr.id),
|
||||
])
|
||||
if not line:
|
||||
line = line.create({
|
||||
"product_tmpl_id": app.product_tmpl_id.id,
|
||||
"attribute_id": attr.id,
|
||||
"value_ids": [(6, 0, [
|
||||
patvs_year.id, patvs_month.id,
|
||||
])]
|
||||
})
|
||||
|
||||
ptv_ids = line.product_template_value_ids
|
||||
|
||||
month_ptv = ptv_ids.filtered(lambda x: x.product_attribute_value_id == patvs_month)
|
||||
month_ptv.write({
|
||||
"price_extra": app.month_price
|
||||
})
|
||||
app.month_product_id = month_ptv.ptav_product_variant_ids[:1]
|
||||
|
||||
year_ptv = ptv_ids.filtered(lambda x: x.product_attribute_value_id == patvs_year)
|
||||
year_ptv.write({
|
||||
"price_extra": app.year_price
|
||||
})
|
||||
app.year_product_id = year_ptv.ptav_product_variant_ids[:1]
|
||||
86
kk_odoo_saas/models/sale_order.py
Executable file
@@ -0,0 +1,86 @@
|
||||
from odoo import fields, models
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
build_id = fields.Many2one("kk_odoo_saas.app")
|
||||
is_pkg_pdt = fields.Boolean(default=False)
|
||||
|
||||
def _split_subscription_lines(self):
|
||||
"""Split the order line according to subscription templates that must be created."""
|
||||
self.ensure_one()
|
||||
res = dict()
|
||||
for line in self.order_line:
|
||||
if line.product_id:
|
||||
for p_id, p_name in line.product_id.name_get():
|
||||
if '(Annually)' in p_name:
|
||||
line.product_id.update(
|
||||
{'subscription_template_id': self.env.ref('sale_subscription.yearly_subscription').id})
|
||||
elif '(Monthly)' in p_name:
|
||||
line.product_id.update(
|
||||
{'subscription_template_id': self.env.ref('sale_subscription.monthly_subscription').id})
|
||||
|
||||
new_sub_lines = self.order_line.filtered(lambda
|
||||
l: not l.subscription_id and l.product_id.subscription_template_id and l.product_id.recurring_invoice)
|
||||
templates = new_sub_lines.mapped('product_id').mapped('subscription_template_id')
|
||||
for template in templates:
|
||||
lines = self.order_line.filtered(
|
||||
lambda l: l.product_id.subscription_template_id == template and l.product_id.recurring_invoice)
|
||||
res[template] = lines
|
||||
return res
|
||||
|
||||
def _action_confirm(self):
|
||||
"""Update and/or create subscriptions on order confirmation."""
|
||||
res = super(SaleOrder, self)._action_confirm()
|
||||
# self.create_saas_app_from_subscription()
|
||||
return res
|
||||
|
||||
def create_saas_app_from_subscription(self):
|
||||
for so in self:
|
||||
lines = so.order_line.filtered(lambda l: l.subscription_id is not False)
|
||||
p_ids = so.order_line.mapped('product_id')
|
||||
if lines and p_ids:
|
||||
saas_app_ids = [app.id for app in self.env['saas.app'].search([('year_product_id', 'in', p_ids.ids)])]
|
||||
if not saas_app_ids:
|
||||
saas_app_ids = [app.id for app in self.env['saas.app'].search([('month_product_id', 'in', p_ids.ids)])]
|
||||
line = lines[0]
|
||||
sub_id = line.subscription_id
|
||||
pkg = False
|
||||
if so.is_pkg_pdt:
|
||||
pkg = self.env['saas.package'].search([('year_product_id', 'in', p_ids.ids)])
|
||||
if not pkg:
|
||||
pkg = self.env['saas.package'].search([('month_product_id', 'in', p_ids.ids)])
|
||||
if pkg:
|
||||
saas_app_ids = pkg.module_ids.ids
|
||||
if so and so.build_id and sub_id:
|
||||
so.build_id.update({'subscription_id': sub_id.id,
|
||||
'module_ids': [(6, 0, saas_app_ids)]
|
||||
})
|
||||
sub_id.build_id = so.build_id
|
||||
so.build_id.deploy_app()
|
||||
else:
|
||||
saas_app_env = self.env['kk_odoo_saas.app']
|
||||
def_vals = saas_app_env.default_get(fields_list=['app_name', ])
|
||||
if self.partner_id.user_ids:
|
||||
def_vals['admin_user'] = self.partner_id.user_ids.ids[0]
|
||||
configurations = self.env["kk_odoo_saas.k8s.config"]
|
||||
config = configurations.get_default_config()
|
||||
if config:
|
||||
def_vals['configuration'] = config.id
|
||||
def_vals['sub_domain_name'] = def_vals.get('app_name')
|
||||
def_vals['subscription_id'] = sub_id.id
|
||||
def_vals['module_ids'] = [(6, 0, saas_app_ids)]
|
||||
def_vals['docker_image'] = pkg.docker_image.id
|
||||
def_vals['name'] = '{}\'s SaaS App'.format(self.partner_id.name)
|
||||
saas_app = saas_app_env.create(def_vals)
|
||||
|
||||
sub_id.build_id = saas_app.id
|
||||
self.build_id = saas_app.id
|
||||
|
||||
_logger.info('Going to Deploy SaaS App, Subscription is going to start')
|
||||
saas_app.deploy_app()
|
||||
else:
|
||||
_logger.error('Cant create SaaS App, No K8s configuration found')
|
||||
20
kk_odoo_saas/models/sale_subscription.py
Executable file
@@ -0,0 +1,20 @@
|
||||
from odoo import fields, models, api
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SaleSubscription(models.Model):
|
||||
_inherit = 'sale.subscription'
|
||||
build_id = fields.Many2one("kk_odoo_saas.app", string="Related SaaS Instance")
|
||||
is_saas = fields.Boolean('Is SaaS Subscription')
|
||||
|
||||
def start_subscription(self):
|
||||
res = super(SaleSubscription, self).start_subscription()
|
||||
if self.build_id:
|
||||
self.build_id.deploy_app()
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
res = super(SaleSubscription, self).create(vals)
|
||||
return res
|
||||
17
kk_odoo_saas/security/ir.model.access.csv
Executable file
@@ -0,0 +1,17 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_saas_app_client,kk_odoo_saas.kk_odoo_saas,model_kk_odoo_saas_app,kk_odoo_saas.group_saas_client,1,0,0,0
|
||||
access_kk_odoo_saas_k8s_config,kk_odoo_saas.kk_odoo_saas_k8s_config,model_kk_odoo_saas_k8s_config,base.group_user,1,1,1,1
|
||||
access_kk_odoo_saas_k8s_docker_images_config,kk_odoo_saas.kk_odoo_saas_docker_images_config,model_kk_odoo_saas_k8s_docker_images,base.group_user,1,1,1,1
|
||||
access_saas_app_user,access_saas_app_user,model_saas_app,base.group_user,1,1,1,1
|
||||
access_saas_app_admin,access_saas_app_admin,model_saas_app,kk_odoo_saas.group_saas_manager,1,1,1,1
|
||||
access_kk_odoo_saas_k8s_node,access_kk_odoo_saas.k8s.node,model_kk_odoo_saas_k8s_node,kk_odoo_saas.group_saas_manager,1,1,1,1
|
||||
access_kk_odoo_saas_app_delete_wizard,access_saas_app_delete_wizard,model_kk_odoo_saas_app_delete_wizard,kk_odoo_saas.group_saas_manager,1,1,1,1
|
||||
access_kk_odoo_saas_app_update_dkr_img_wizard,access_kk_odoo_saas_app_update_dkr_img_wizard,model_kk_odoo_saas_app_update_dkr_img_wizard,kk_odoo_saas.group_saas_manager,1,1,1,1
|
||||
access_saas_app_secret_admin,access_saas_app_secret_admin,model_kk_odoo_saas_app_ssl_secret,kk_odoo_saas.group_saas_manager,1,1,1,1
|
||||
access_saas_app_custom_domain,admin_access_saas_app_domain,model_saas_app_custom_domain,kk_odoo_saas.group_saas_manager,1,1,1,1
|
||||
access_saas_app_custom_domain_portal,portal_access_saas_app_domain,model_saas_app_custom_domain,base.group_portal,1,0,0,0
|
||||
access_kk_odoo_saas_master_db_creds,kk_odoo_saas.kk_odoo_saas_master_db_creds,model_kk_odoo_saas_k8s_master_db_creds,kk_odoo_saas.group_saas_manager,1,1,1,1
|
||||
access_saas_package_admin,access_saas_package_admin,model_saas_package,kk_odoo_saas.group_saas_manager,1,1,1,1
|
||||
access_saas_app_container_argument,access_saas_app_container_argument,model_saas_app_container_argument,kk_odoo_saas.group_saas_manager,1,1,1,1
|
||||
access_saas_app_container_env_var,access_saas_app_container_env_var,model_saas_app_container_env_var,kk_odoo_saas.group_saas_manager,1,1,1,1
|
||||
access_saas_app_manager,kk_odoo_saas.kk_odoo_saas,model_kk_odoo_saas_app,kk_odoo_saas.group_saas_manager,1,1,1,1
|
||||
|
36
kk_odoo_saas/security/security.xml
Executable file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.module.category" id="module_category_saas">
|
||||
<field name="name">SaaS Management</field>
|
||||
<field name="sequence">22</field>
|
||||
</record>
|
||||
<record id="group_saas_manager" model="res.groups">
|
||||
<field name="name">SaaS Manager</field>
|
||||
<field name="category_id" ref="module_category_saas" />
|
||||
<field
|
||||
name="users"
|
||||
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
|
||||
/>
|
||||
</record>
|
||||
<record id="group_saas_client" model="res.groups">
|
||||
<field name="name">SaaS Client</field>
|
||||
<field name="category_id" ref="module_category_saas" />
|
||||
</record>
|
||||
<!-- manager rules start-->
|
||||
|
||||
<!-- manager rules end-->
|
||||
|
||||
|
||||
<!-- customer rules start-->
|
||||
<!-- only show its own apps to customers-->
|
||||
<record id="rule_own_saas_apps_only" model="ir.rule">
|
||||
<field name="name">Personal SaaS App Visibility to Customer</field>
|
||||
<field ref="model_kk_odoo_saas_app" name="model_id"/>
|
||||
<field name="domain_force">[('admin_user','=',user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('kk_odoo_saas.group_saas_client'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
</odoo>
|
||||
BIN
kk_odoo_saas/static/description/icon.png
Executable file
|
After Width: | Height: | Size: 116 KiB |
10037
kk_odoo_saas/static/src/css/bootstrap.css
vendored
Executable file
171
kk_odoo_saas/static/src/css/calculator.css
Executable file
@@ -0,0 +1,171 @@
|
||||
.col-lg-12{
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#price{
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.pricing-card-title{
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.users-qty-change-buttons{
|
||||
max-width: 30px;
|
||||
max-height: 30px;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.loader{
|
||||
position: fixed;
|
||||
z-index: 99;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.status{
|
||||
font-size: 26px;
|
||||
color: black;
|
||||
position: fixed;
|
||||
margin-top: -70px;
|
||||
}
|
||||
|
||||
.loader > img {
|
||||
width: 100px;
|
||||
opacity: 0.6;
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
.transition {
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.app, .package{
|
||||
max-height: 95px;
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
min-width: 174px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.app-data{
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
.price-value{
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.period{
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.green-border{
|
||||
border: 2px solid green;
|
||||
}
|
||||
|
||||
.normal-border{
|
||||
border: 2px solid #FFFFFF;
|
||||
}
|
||||
|
||||
.hid{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fnt-24{
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.leftstr, .rightstr {
|
||||
float: left;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.rightstr {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.fnt-larger{
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
#price-window
|
||||
{
|
||||
position: fixed;
|
||||
right: 30%;
|
||||
top: 20%;
|
||||
width: 15%;
|
||||
min-width: 20%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.page-alignment{
|
||||
min-width: 640px;
|
||||
}
|
||||
|
||||
.main-column{
|
||||
margin-right: auto;
|
||||
margin-left: 10%;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 999px) {
|
||||
#price-window
|
||||
{
|
||||
position: fixed;
|
||||
right: 15%;
|
||||
top: 15%;
|
||||
width: 20%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.main-column{
|
||||
margin-right: auto;
|
||||
margin-left: 10%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.page-alignment{
|
||||
min-width: 640px;
|
||||
}
|
||||
|
||||
.app, .package{
|
||||
margin-left: 7px;
|
||||
max-width: 31%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 799px) {
|
||||
#price-window
|
||||
{
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: -6%;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.container{
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
.main-column{
|
||||
margin-right: auto;
|
||||
margin-left: 10%;
|
||||
max-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.col-lg-9{
|
||||
padding-left: 0px;
|
||||
}
|
||||
2338
kk_odoo_saas/static/src/css/font-awesome.css
vendored
Executable file
71
kk_odoo_saas/static/src/css/logs-viewer.css
Executable file
@@ -0,0 +1,71 @@
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
.o-logs-container{
|
||||
height: auto;
|
||||
}
|
||||
.o-logs{
|
||||
word-wrap: break-word;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
line-height: normal;
|
||||
margin: 0;
|
||||
padding-top:80px;
|
||||
}
|
||||
.header{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
background: #f9f9f9;
|
||||
border-bottom: 1px solid #d3d3d3;
|
||||
padding: 6px 15px;
|
||||
box-shadow: 0 0 6px #ddd;
|
||||
}
|
||||
.status-bar {
|
||||
text-align: right;
|
||||
padding-right: 15px;
|
||||
}
|
||||
.button-pause{
|
||||
position: relative;
|
||||
color: #555;
|
||||
font-size: 16px;
|
||||
padding: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.button-pause i{
|
||||
margin-left: 9px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.loader{
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #555;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
z-index: -1;
|
||||
}
|
||||
.loader.loading{
|
||||
-webkit-animation: spin 1s linear infinite;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
.form-inline .form-group {
|
||||
margin-bottom: 0;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
.form-inline .form-group input {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% { -webkit-transform: rotate(0deg); }
|
||||
100% { -webkit-transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
BIN
kk_odoo_saas/static/src/img/add-users.png
Executable file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
kk_odoo_saas/static/src/img/base.png
Executable file
|
After Width: | Height: | Size: 38 KiB |
BIN
kk_odoo_saas/static/src/img/default.png
Executable file
|
After Width: | Height: | Size: 25 KiB |
BIN
kk_odoo_saas/static/src/img/loader.gif
Executable file
|
After Width: | Height: | Size: 24 KiB |
BIN
kk_odoo_saas/static/src/img/starter_pack.png
Executable file
|
After Width: | Height: | Size: 136 KiB |
BIN
kk_odoo_saas/static/src/img/substr-users.png
Executable file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
kk_odoo_saas/static/src/img/user.png
Executable file
|
After Width: | Height: | Size: 13 KiB |
10364
kk_odoo_saas/static/src/js/jquery.js
vendored
Executable file
169
kk_odoo_saas/static/src/js/logs-viewer.js
Executable file
@@ -0,0 +1,169 @@
|
||||
'use strict';
|
||||
var FETCH_SIZE = 10000;
|
||||
var FETCH_INTERVAL_TIME = 5000;
|
||||
var AUTO_PAUSE_TIME = 1000 * 60 * 5;
|
||||
|
||||
function fetch_logs(begin, end) {
|
||||
return $.ajax({
|
||||
dataType: "text",
|
||||
cache: false,
|
||||
headers: {Range: 'bytes=' + (begin === false ? '' : begin) + '-' + (end === false ? '' : end)},
|
||||
}).then(function (data, s, xhr) {
|
||||
data = data.replace(/^\n/, "");
|
||||
data = data.replace(/\n$/, "");
|
||||
var content_range = xhr.getResponseHeader("Content-Range");
|
||||
var bytes = content_range ? /bytes ([0-9]*)-([0-9]*)\/([0-9]*)/.exec(content_range) : undefined;
|
||||
var begin = bytes ? parseInt(bytes[1]) : 0;
|
||||
var end = bytes ? parseInt(bytes[2]) : data.length;
|
||||
var size = bytes ? parseInt(bytes[3]) : data.length + 1;
|
||||
return {
|
||||
data: data,
|
||||
begin: begin,
|
||||
end: end,
|
||||
size: size,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
var fetch_interval;
|
||||
var min;
|
||||
var max;
|
||||
var auto_scroll = true;
|
||||
var def_top;
|
||||
var def_bottom;
|
||||
|
||||
function init() {
|
||||
$('.o-logs span').remove();
|
||||
return fetch_logs(false, 1).then(function (result) {
|
||||
min = max = Math.max(0, result.size - FETCH_SIZE);
|
||||
def_bottom = def_top = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
// borrowed from https://github.com/janl/mustache.js/blob/master/mustache.js
|
||||
function _escapeHTML(string) {
|
||||
var entityMap = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
};
|
||||
return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap(s) {
|
||||
return entityMap[s];
|
||||
});
|
||||
}
|
||||
|
||||
function _prepare_line(line) {
|
||||
var result = '<span class="o-log-line">'
|
||||
line.split('\n').forEach(function (l) {
|
||||
result += '<span style="white-space: pre;">' + _escapeHTML(l) + '</span><br/>';
|
||||
});
|
||||
result += '</span>'
|
||||
result = $(result);
|
||||
filter(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function append_logs() {
|
||||
if (!def_bottom || def_bottom.state() !== 'pending') {
|
||||
def_bottom = fetch_logs(max, false).then(function (result) {
|
||||
if (max !== result.end) {
|
||||
max = result.end;
|
||||
var splits = result.data.split(/\s+(?=[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3})/)
|
||||
splits.forEach(function (line) {
|
||||
$('.o-logs').append(_prepare_line(line));
|
||||
});
|
||||
if (auto_scroll) {
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
}
|
||||
}).fail(function (xhr) {
|
||||
if (xhr.status === 416) {
|
||||
return init();
|
||||
}
|
||||
});
|
||||
}
|
||||
return def_bottom;
|
||||
}
|
||||
|
||||
function prepend_logs() {
|
||||
if (min > 0 && (!def_top || def_top.state() !== 'pending')) {
|
||||
def_top = fetch_logs(Math.max(0, min - FETCH_SIZE), min).then(function (result) {
|
||||
min = result.begin;
|
||||
var lines = result.data.split('\n');
|
||||
var first_line = lines.pop();
|
||||
if (first_line) {
|
||||
$('.o-logs span').first().prepend(_escapeHTML(first_line));
|
||||
}
|
||||
lines.reverse();
|
||||
lines.forEach(function (line) {
|
||||
$('.o-logs').prepend(_prepare_line(line));
|
||||
});
|
||||
window.scrollTo(0, 20);
|
||||
}).fail(function (xhr) {
|
||||
if (xhr.status === 416) {
|
||||
return init();
|
||||
}
|
||||
});
|
||||
}
|
||||
return def_top;
|
||||
}
|
||||
|
||||
function toggle_pause() {
|
||||
$('i').toggle();
|
||||
$('.loader').toggleClass('loading');
|
||||
if (fetch_interval) {
|
||||
clearInterval(fetch_interval);
|
||||
fetch_interval = undefined;
|
||||
} else {
|
||||
fetch_interval = setInterval(append_logs, FETCH_INTERVAL_TIME);
|
||||
setTimeout(toggle_pause, AUTO_PAUSE_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
function filter(elements) {
|
||||
var filter = $('#filter').val();
|
||||
elements.filter(':contains(' + filter + ')').show();
|
||||
elements.filter(':not(:contains(' + filter + '))').hide();
|
||||
}
|
||||
|
||||
$(window).scroll(function () {
|
||||
if ($(window).scrollTop() + $(window).height() === $(document).height()) {
|
||||
auto_scroll = true;
|
||||
} else {
|
||||
auto_scroll = false;
|
||||
}
|
||||
if ($(window).scrollTop() === 0) {
|
||||
prepend_logs();
|
||||
}
|
||||
});
|
||||
|
||||
function fill_page() {
|
||||
if (!def_bottom) {
|
||||
append_logs().then(fill_page);
|
||||
} else if ($(window).height() === $(document).height() && min != 0 && (!def_top || def_top.state() !== 'pending')) {
|
||||
prepend_logs().then(fill_page);
|
||||
} else {
|
||||
$(window).scrollTop($(document).height());
|
||||
}
|
||||
}
|
||||
|
||||
init().then(function () {
|
||||
fill_page();
|
||||
toggle_pause();
|
||||
});
|
||||
$('.button-pause').click(toggle_pause);
|
||||
$('#filter').on('input', function () {
|
||||
filter($('.o-logs .o-log-line'));
|
||||
});
|
||||
$('#filter').keypress(function (event) {
|
||||
if (event.keyCode === 10 || event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
29
kk_odoo_saas/static/src/js/refresh_button.js
Executable file
@@ -0,0 +1,29 @@
|
||||
odoo.define('saas_apps.filter_button', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
var ListController = require('web.ListController');
|
||||
|
||||
ListController.include({
|
||||
renderButtons: function($node) {
|
||||
this._super.apply(this, arguments);
|
||||
if (this.$buttons) {
|
||||
var refresh_apps_button = this.$buttons.find('.refresh_apps_button');
|
||||
if (refresh_apps_button.length) {
|
||||
refresh_apps_button.on("click", this.proxy('refresh_apps_button'));
|
||||
}
|
||||
}
|
||||
},
|
||||
refresh_apps_button: function () {
|
||||
// Loading all modules in saas.line from ir.module.module
|
||||
this._rpc({
|
||||
"model": "saas.app",
|
||||
"method": "action_make_applist_from_local_instance",
|
||||
"args": [],
|
||||
}).then(function (result) {
|
||||
window.location.reload()
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
10
kk_odoo_saas/static/src/xml/base.xml
Executable file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<template xml:space="preserve">
|
||||
<t t-extend="ListView.buttons">
|
||||
<t t-jquery="button.o_list_button_add" t-operation="after">
|
||||
<button t-if="widget.modelName == 'saas.app'" type="button" class="btn btn-primary refresh_apps_button" accesskey="f">
|
||||
Refresh
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
1
kk_odoo_saas/utils/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
from . import k8s_deployment
|
||||
52
kk_odoo_saas/utils/del_git_code.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from kubernetes import config, client
|
||||
from kubernetes.stream import stream
|
||||
from odoo.addons.smile_log.tools import SmileDBLogger
|
||||
from odoo.exceptions import UserError
|
||||
import yaml
|
||||
|
||||
|
||||
# import git_aggregator
|
||||
|
||||
def del_git_dir(self, path):
|
||||
"""
|
||||
It will delete addons directory inside running container
|
||||
"""
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
if self.app_name and path:
|
||||
try:
|
||||
data2 = yaml.safe_load(self.configuration.config_file)
|
||||
config.load_kube_config_from_dict(data2)
|
||||
except config.config_exception.ConfigException as e:
|
||||
_logger.error(str(e))
|
||||
raise UserError("Unable to Connect K8s Cluster")
|
||||
core_v1_api = client.CoreV1Api()
|
||||
|
||||
try:
|
||||
pod = core_v1_api.list_namespaced_pod(namespace='default', label_selector='app={}'.format(self.app_name))
|
||||
except Exception as e:
|
||||
raise UserError("Unable to connect to cluster")
|
||||
resp1 = stream(core_v1_api.connect_get_namespaced_pod_exec,
|
||||
pod.items[0].metadata.name,
|
||||
'default',
|
||||
command=['chmod', '-R', 'ugo+rw', path],
|
||||
stderr=True, stdin=False,
|
||||
stdout=True, tty=False)
|
||||
|
||||
resp = stream(core_v1_api.connect_get_namespaced_pod_exec,
|
||||
pod.items[0].metadata.name,
|
||||
'default',
|
||||
command=['rm', '-rf', path ],
|
||||
stderr=True, stdin=False,
|
||||
stdout=True, tty=False)
|
||||
|
||||
resp3 = stream(core_v1_api.connect_get_namespaced_pod_exec,
|
||||
pod.items[0].metadata.name,
|
||||
'default',
|
||||
command=['mkdir', path ],
|
||||
stderr=True, stdin=False,
|
||||
stdout=True, tty=False)
|
||||
_logger.info(str(resp1))
|
||||
_logger.info(str(resp))
|
||||
_logger.info(str(resp3))
|
||||
_logger.info(str(path))
|
||||
_logger.info(str("code deleted"))
|
||||
167
kk_odoo_saas/utils/deployment.py
Executable file
@@ -0,0 +1,167 @@
|
||||
from kubernetes import client
|
||||
from odoo.addons.smile_log.tools import SmileDBLogger
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_deployment(meta_data, specs, namespace="default", self=False):
|
||||
# Deployment
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
deployment = client.V1Deployment(
|
||||
api_version="apps/v1",
|
||||
kind="Deployment",
|
||||
metadata=meta_data,
|
||||
spec=specs)
|
||||
k8s_apps_v1 = client.AppsV1Api()
|
||||
try:
|
||||
resp = k8s_apps_v1.create_namespaced_deployment(
|
||||
body=deployment, namespace=namespace)
|
||||
_logger.info("Deployment created. name='%s'" % resp.metadata.name)
|
||||
except client.exceptions.ApiException as e:
|
||||
_logger.error(str(e))
|
||||
|
||||
|
||||
def create_docker_repo_secret(app_name, namespace="default", self=False):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
k8s_apps_v1 = client.CoreV1Api()
|
||||
secret = client.V1Secret(
|
||||
metadata=client.V1ObjectMeta(
|
||||
name=app_name+'-dkr-registry-key',
|
||||
labels={
|
||||
"app": app_name,
|
||||
"tier": "backend"
|
||||
}
|
||||
),
|
||||
data={
|
||||
'.dockerconfigjson': self.docker_image.b64_dkr_config
|
||||
},
|
||||
type='kubernetes.io/dockerconfigjson',
|
||||
)
|
||||
try:
|
||||
resp = k8s_apps_v1.create_namespaced_secret(
|
||||
body=secret, namespace=namespace)
|
||||
_logger.info("Secret created. name='%s'" % resp.metadata.name)
|
||||
return True
|
||||
except client.exceptions.ApiException as e:
|
||||
_logger.error(str(e))
|
||||
return False
|
||||
|
||||
|
||||
def delete_docker_repo_secret(app_name, namespace="default", self=False):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
k8s_apps_v1 = client.CoreV1Api()
|
||||
try:
|
||||
resp = k8s_apps_v1.delete_namespaced_secret(app_name+'-dkr-registry-key', namespace=namespace)
|
||||
_logger.info(str(resp))
|
||||
return True
|
||||
except client.exceptions.ApiException as e:
|
||||
_logger.error(str(e))
|
||||
return False
|
||||
|
||||
|
||||
def create_odoo_deployment(app_name, namespace="default", self=False):
|
||||
image = 'odoo:15.0'
|
||||
res_limits = {'ephemeral-storage': '1Gi'}
|
||||
|
||||
image_pull_secrets = []
|
||||
if self.is_custom_image and self.docker_image:
|
||||
image = "{0}:{1}".format(self.docker_image.name, self.docker_image.tag)
|
||||
if self.docker_image.is_pvt_dkr_repo and self.docker_image.b64_dkr_config:
|
||||
if create_docker_repo_secret(app_name, namespace, self):
|
||||
sec_name = app_name+"-dkr-registry-key"
|
||||
image_pull_secrets.append(client.V1LocalObjectReference(name=sec_name))
|
||||
|
||||
meta_data = client.V1ObjectMeta(name=app_name + "-odoo-deployment",
|
||||
labels={"app": app_name})
|
||||
args_odoo = ['--database=' + self.sub_domain_name]
|
||||
# args_odoo = []
|
||||
|
||||
if self.demo_data:
|
||||
args_odoo.append('--without-demo=False')
|
||||
else:
|
||||
args_odoo.append('--without-demo=True')
|
||||
|
||||
if self.module_ids:
|
||||
module_names = ''
|
||||
for module in self.module_ids:
|
||||
module_names = module_names + module.name + ','
|
||||
args_odoo.append("--init={0}".format(module_names))
|
||||
|
||||
if self.db_server_id:
|
||||
_logger.critical('Cant deploy app, PG username or password cant find')
|
||||
UserError("Cant deploy app, PG username or password cant find")
|
||||
|
||||
limits = client.V1ResourceRequirements(limits=res_limits)
|
||||
|
||||
tolerations = []
|
||||
node_selector = {}
|
||||
if self and self.is_dedicated_node and self.node_id:
|
||||
# tolerations = [client.V1Toleration(effect='NoSchedule', key=self.node_key, value=self.node_value, operator='Equal')]
|
||||
# specific for aws clusters
|
||||
node_selector['kubernetes.io/hostname'] = self.node_id.name
|
||||
|
||||
odoo_container = client.V1Container(
|
||||
name="odoo",
|
||||
image=image,
|
||||
env=[
|
||||
client.V1EnvVar(name="HOST", value=self.db_server_id.server_url),
|
||||
client.V1EnvVar(name="USER", value=self.db_server_id.master_username),
|
||||
client.V1EnvVar(name="PASSWORD", value=self.db_server_id.master_pass),
|
||||
client.V1EnvVar(name="PORT", value=self.db_server_id.server_port),
|
||||
client.V1EnvVar(name="ODOO_HTTP_SOCKET_TIMEOUT", value="100"),
|
||||
],
|
||||
ports=[client.V1ContainerPort(container_port=8069, name="odoo-port"),
|
||||
client.V1ContainerPort(container_port=8072, name="longpolling")],
|
||||
args=args_odoo,
|
||||
image_pull_policy='Always',
|
||||
# command=['chown', '-R', '101:101', '/mnt/extra-addons'],
|
||||
resources=limits,
|
||||
# comment following line, if you want to run as odoo user
|
||||
# security_context=client.V1SecurityContext(run_as_user=0, run_as_group=0),
|
||||
volume_mounts=[client.V1VolumeMount(name=app_name + "-odoo-web-pv-storage", mount_path="/var/lib/odoo/")]
|
||||
)
|
||||
# pod Volume Claim
|
||||
volume_claim = client.V1PersistentVolumeClaimVolumeSource(claim_name=app_name + "-odoo-web-pv-claim")
|
||||
# pod volume
|
||||
volume = client.V1Volume(name=app_name + "-odoo-web-pv-storage", persistent_volume_claim=volume_claim)
|
||||
# Strategy
|
||||
strategy = client.V1DeploymentStrategy(type="Recreate")
|
||||
# Template
|
||||
# for running as a odoo user changes instead of stash
|
||||
spec = client.V1PodSpec(containers=[odoo_container], volumes=[volume], image_pull_secrets=image_pull_secrets,
|
||||
security_context=client.V1PodSecurityContext(run_as_group=101, run_as_user=101,
|
||||
fs_group=101, fs_group_change_policy='Always'),
|
||||
node_selector=node_selector)
|
||||
|
||||
template = client.V1PodTemplateSpec(
|
||||
metadata=client.V1ObjectMeta(labels={"app": app_name, "tier": "backend"}),
|
||||
spec=spec
|
||||
)
|
||||
selector = client.V1LabelSelector(match_labels={"app": app_name, "tier": "backend"})
|
||||
|
||||
# Spec
|
||||
specs = client.V1DeploymentSpec(
|
||||
replicas=1,
|
||||
strategy=strategy,
|
||||
selector=selector,
|
||||
template=template,
|
||||
)
|
||||
create_deployment(meta_data, specs, namespace, self=self)
|
||||
|
||||
|
||||
def delete_odoo_deployment(app_name, namespace="default", self=False):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
dep_name = app_name + "-odoo-deployment"
|
||||
core_v1_api = client.AppsV1Api()
|
||||
|
||||
try:
|
||||
deployment = core_v1_api.delete_namespaced_deployment(name=dep_name, namespace=namespace)
|
||||
if self.is_custom_image and self.docker_image:
|
||||
if self.docker_image.is_pvt_dkr_repo and self.docker_image.b64_dkr_config:
|
||||
delete_docker_repo_secret(app_name, namespace, self)
|
||||
_logger.info(str(deployment))
|
||||
|
||||
except client.exceptions.ApiException as e:
|
||||
_logger.error(str(e))
|
||||
235
kk_odoo_saas/utils/ingress.py
Executable file
@@ -0,0 +1,235 @@
|
||||
from kubernetes import client, config
|
||||
from odoo.exceptions import ValidationError
|
||||
import logging
|
||||
from odoo.addons.smile_log.tools import SmileDBLogger
|
||||
|
||||
|
||||
def create_ingress(app_name, self=False):
|
||||
_logger = logging.getLogger(__name__)
|
||||
if self:
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
# create_ingress
|
||||
if not self.domain_name and self.sub_domain_name:
|
||||
return ValidationError('Either Domain name or Subdomain name is not Valid')
|
||||
else:
|
||||
host = self.sub_domain_name + self.domain_name
|
||||
|
||||
networking_v1_api = client.NetworkingV1Api()
|
||||
rules = [client.V1IngressRule(
|
||||
host=host,
|
||||
http=client.V1HTTPIngressRuleValue(
|
||||
paths=[
|
||||
client.V1HTTPIngressPath(
|
||||
path='/',
|
||||
path_type='ImplementationSpecific',
|
||||
backend=client.V1IngressBackend(
|
||||
service=client.V1IngressServiceBackend(
|
||||
port=client.V1ServiceBackendPort(
|
||||
number=80,
|
||||
),
|
||||
name=app_name + '-odoo-service', )
|
||||
)
|
||||
),
|
||||
|
||||
client.V1HTTPIngressPath(
|
||||
path='/longpolling/',
|
||||
path_type='ImplementationSpecific',
|
||||
backend=client.V1IngressBackend(
|
||||
service=client.V1IngressServiceBackend(
|
||||
port=client.V1ServiceBackendPort(
|
||||
number=8072,
|
||||
),
|
||||
name=app_name + '-odoo-service', )
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
]
|
||||
)
|
||||
)]
|
||||
tls_hosts = [host]
|
||||
|
||||
if self and self.custom_domain_ids:
|
||||
for custom_domain in self.custom_domain_ids:
|
||||
rules.append(client.V1IngressRule(
|
||||
host=custom_domain.name,
|
||||
http=client.V1HTTPIngressRuleValue(
|
||||
paths=[
|
||||
client.V1HTTPIngressPath(
|
||||
path='/',
|
||||
path_type='ImplementationSpecific',
|
||||
backend=client.V1IngressBackend(
|
||||
service=client.V1IngressServiceBackend(
|
||||
port=client.V1ServiceBackendPort(
|
||||
number=80,
|
||||
),
|
||||
name=app_name + '-odoo-service', )
|
||||
)
|
||||
),
|
||||
|
||||
client.V1HTTPIngressPath(
|
||||
path='/longpolling/',
|
||||
path_type='ImplementationSpecific',
|
||||
backend=client.V1IngressBackend(
|
||||
service=client.V1IngressServiceBackend(
|
||||
port=client.V1ServiceBackendPort(
|
||||
number=8072,
|
||||
),
|
||||
name=app_name + '-odoo-service', )
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
]
|
||||
|
||||
)
|
||||
|
||||
)
|
||||
)
|
||||
tls_hosts.append(custom_domain.name)
|
||||
|
||||
body = client.V1Ingress(
|
||||
kind='Ingress',
|
||||
metadata=client.V1ObjectMeta(name=app_name + '-ingress',
|
||||
labels={"app": app_name},
|
||||
annotations={'kubernetes.io/ingress.class': 'nginx',
|
||||
'cert-manager.io/cluster-issuer': 'letsencrypt-prod'
|
||||
|
||||
}),
|
||||
spec=client.V1IngressSpec(
|
||||
rules=rules,
|
||||
tls=[client.V1IngressTLS(
|
||||
hosts=tls_hosts,
|
||||
secret_name=self.app_name + 'tls',
|
||||
)]
|
||||
)
|
||||
|
||||
)
|
||||
try:
|
||||
networking_v1_api.create_namespaced_ingress(
|
||||
namespace='default',
|
||||
body=body
|
||||
)
|
||||
except client.exceptions.ApiException as e:
|
||||
_logger.error(str(e))
|
||||
|
||||
|
||||
def delete_odoo_ingress(app_name, namespace="default", self=False):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
networking_v1_api = client.NetworkingV1Api()
|
||||
ing_name = app_name + '-ingress'
|
||||
|
||||
try:
|
||||
ing = networking_v1_api.delete_namespaced_ingress(name=ing_name, namespace=namespace)
|
||||
_logger.info(str(ing))
|
||||
|
||||
except client.exceptions.ApiException as e:
|
||||
_logger.error(str(e))
|
||||
|
||||
|
||||
def update_odoo_ingress(app_name, namespace="default", self=False):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
networking_v1_api = client.NetworkingV1Api()
|
||||
|
||||
ing_name = app_name + '-ingress'
|
||||
|
||||
if not self.domain_name and self.sub_domain_name:
|
||||
return ValidationError('Either Domain name or Subdomain name is not Valid')
|
||||
else:
|
||||
host = self.sub_domain_name + self.domain_name
|
||||
|
||||
rules = [client.V1IngressRule(
|
||||
host=host,
|
||||
http=client.V1HTTPIngressRuleValue(
|
||||
paths=[
|
||||
client.V1HTTPIngressPath(
|
||||
path='/',
|
||||
path_type='ImplementationSpecific',
|
||||
backend=client.V1IngressBackend(
|
||||
service=client.V1IngressServiceBackend(
|
||||
port=client.V1ServiceBackendPort(
|
||||
number=80,
|
||||
),
|
||||
name=app_name + '-odoo-service', )
|
||||
)
|
||||
),
|
||||
|
||||
client.V1HTTPIngressPath(
|
||||
path='/longpolling/',
|
||||
path_type='ImplementationSpecific',
|
||||
backend=client.V1IngressBackend(
|
||||
service=client.V1IngressServiceBackend(
|
||||
port=client.V1ServiceBackendPort(
|
||||
number=8072,
|
||||
),
|
||||
name=app_name + '-odoo-service', )
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
]
|
||||
)
|
||||
)]
|
||||
tls_hosts = [host]
|
||||
|
||||
if self and self.custom_domain_ids:
|
||||
for custom_domain in self.custom_domain_ids:
|
||||
rules.append(client.V1IngressRule(
|
||||
host=custom_domain.name,
|
||||
http=client.V1HTTPIngressRuleValue(
|
||||
paths=[
|
||||
client.V1HTTPIngressPath(
|
||||
path='/',
|
||||
path_type='ImplementationSpecific',
|
||||
backend=client.V1IngressBackend(
|
||||
service=client.V1IngressServiceBackend(
|
||||
port=client.V1ServiceBackendPort(
|
||||
number=80,
|
||||
),
|
||||
name=app_name + '-odoo-service', )
|
||||
)
|
||||
),
|
||||
|
||||
client.V1HTTPIngressPath(
|
||||
path='/longpolling/',
|
||||
path_type='ImplementationSpecific',
|
||||
backend=client.V1IngressBackend(
|
||||
service=client.V1IngressServiceBackend(
|
||||
port=client.V1ServiceBackendPort(
|
||||
number=8072,
|
||||
),
|
||||
name=app_name + '-odoo-service', )
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
tls_hosts.append(custom_domain.name)
|
||||
|
||||
body = client.V1Ingress(
|
||||
kind='Ingress',
|
||||
metadata=client.V1ObjectMeta(name=app_name + '-ingress',
|
||||
labels={"app": app_name},
|
||||
annotations={'kubernetes.io/ingress.class': 'nginx',
|
||||
}),
|
||||
spec=client.V1IngressSpec(
|
||||
rules=rules,
|
||||
tls=[client.V1IngressTLS(
|
||||
hosts=tls_hosts,
|
||||
secret_name=self.app_name + 'tls'
|
||||
)]
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
try:
|
||||
ing = networking_v1_api.patch_namespaced_ingress(name=ing_name, namespace=namespace, body=body)
|
||||
_logger.info(str(ing))
|
||||
except client.exceptions.ApiException as e:
|
||||
_logger.error(str(e))
|
||||
249
kk_odoo_saas/utils/k8s_deployment.py
Executable file
@@ -0,0 +1,249 @@
|
||||
from kubernetes import config, client
|
||||
import yaml
|
||||
from kubernetes.stream import stream
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from .odoo_components import deploy_odoo_components, delete_odoo_components, delete_odoo_components_from_options,\
|
||||
update_odoo_components
|
||||
from .pg_server import delete_databases
|
||||
from odoo.addons.smile_log.tools import SmileDBLogger
|
||||
from .utils import generate_commit_sha
|
||||
|
||||
def create_deployment(app_name, config_file, self=False):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
# Configs can be set in Configuration class directly or using helper
|
||||
# utility. If no argument provided, the config will be loaded from
|
||||
# default location.
|
||||
try:
|
||||
data2 = yaml.safe_load(config_file)
|
||||
config.load_kube_config_from_dict(data2)
|
||||
except config.config_exception.ConfigException as e:
|
||||
_logger.error(str(e))
|
||||
raise UserError("Unable to Connect K8s Cluster")
|
||||
if app_name:
|
||||
deploy_odoo_components(app_name=app_name, namespace="default", self=self)
|
||||
else:
|
||||
_logger.error("Cant find App Name")
|
||||
raise UserError("Cant find App Name")
|
||||
|
||||
|
||||
def delete_app_with_options(self, delete_db, delete_pv, delete_svc, delete_ing, delete_deployment):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
try:
|
||||
data = yaml.safe_load(self.configuration.config_file)
|
||||
config.load_kube_config_from_dict(data)
|
||||
except config.config_exception.ConfigException as e:
|
||||
_logger.error(str(e))
|
||||
raise UserError("Unable to Connect K8s Cluster")
|
||||
if self.app_name:
|
||||
delete_odoo_components_from_options(app_name=self.app_name, namespace="default", self=self, delete_db=delete_db,
|
||||
delete_pv=delete_pv, delete_svc=delete_svc,
|
||||
delete_ing=delete_ing, delete_deployment=delete_deployment)
|
||||
if delete_db:
|
||||
delete_databases(self)
|
||||
else:
|
||||
_logger.error("Cant find App Name")
|
||||
raise UserError("Cant find App Name")
|
||||
|
||||
|
||||
def update_app(self):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
try:
|
||||
data = yaml.safe_load(self.configuration.config_file)
|
||||
config.load_kube_config_from_dict(data)
|
||||
except config.config_exception.ConfigException as e:
|
||||
_logger.error(str(e))
|
||||
raise UserError("Unable to Connect K8s Cluster")
|
||||
if self.app_name:
|
||||
update_odoo_components(app_name=self.app_name, namespace="default", self=self)
|
||||
else:
|
||||
_logger.error("Cant find App Name")
|
||||
raise UserError("Cant find App Name")
|
||||
|
||||
|
||||
def fetch_secrets_from_cluster(self):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
try:
|
||||
data2 = yaml.safe_load(self.configuration.config_file)
|
||||
config.load_kube_config_from_dict(data2)
|
||||
except config.config_exception.ConfigException as e:
|
||||
_logger.error(str(e))
|
||||
raise UserError("Unable to Connect K8s Cluster")
|
||||
|
||||
secs = []
|
||||
if self.app_name:
|
||||
core_v1_api = client.CoreV1Api()
|
||||
secrs = core_v1_api.list_namespaced_secret(namespace='default')
|
||||
for sec in secrs.items:
|
||||
secs.append(sec.metadata.name)
|
||||
return secs
|
||||
|
||||
|
||||
def deploy_apps_from_git(self):
|
||||
"""
|
||||
To pull code from github inside running container
|
||||
"""
|
||||
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
try:
|
||||
data2 = yaml.safe_load(self.configuration.config_file)
|
||||
config.load_kube_config_from_dict(data2)
|
||||
except config.config_exception.ConfigException as e:
|
||||
_logger.error(str(e))
|
||||
raise UserError("Unable to Connect K8s Cluster")
|
||||
|
||||
if self.app_name:
|
||||
core_v1_api = client.CoreV1Api()
|
||||
pod = core_v1_api.list_namespaced_pod(namespace='default', label_selector='app={}'.format(self.app_name))
|
||||
if self.is_extra_addon and self.extra_addons and pod and pod.items:
|
||||
base_version = self.docker_image.base_version
|
||||
clone_path = "/var/lib/odoo/addons/" + str(base_version)
|
||||
if self.is_private_repo and self.git_token:
|
||||
url = self.extra_addons
|
||||
url = url.replace("http://", "")
|
||||
url = url.replace("https://", "")
|
||||
url = url.replace("www.", "")
|
||||
git_url = "https://oauth2:{0}@{1}".format(self.git_token, url)
|
||||
else:
|
||||
git_url = self.extra_addons
|
||||
is_clone_error = False
|
||||
error = ''
|
||||
exec_command = ['git', '-C', clone_path, 'pull']
|
||||
resp = stream(core_v1_api.connect_get_namespaced_pod_exec,
|
||||
pod.items[0].metadata.name,
|
||||
'default',
|
||||
command=exec_command,
|
||||
stderr=True, stdin=True,
|
||||
stdout=True, tty=False,
|
||||
_preload_content=False)
|
||||
while resp.is_open():
|
||||
resp.update(timeout=10)
|
||||
if resp.peek_stdout():
|
||||
_logger.info(str(resp.read_stdout()))
|
||||
if resp.peek_stderr():
|
||||
is_clone_error = True
|
||||
error = resp.read_stderr()
|
||||
_logger.error(str(error))
|
||||
break
|
||||
resp.close()
|
||||
|
||||
if is_clone_error:
|
||||
if error and "not a git repository (or any" in error:
|
||||
resp1 = stream(core_v1_api.connect_get_namespaced_pod_exec,
|
||||
pod.items[0].metadata.name,
|
||||
'default',
|
||||
command=['chmod', '-R', 'ugo+rw', clone_path],
|
||||
stderr=True, stdin=False,
|
||||
stdout=True, tty=False,
|
||||
_preload_content=False)
|
||||
resp = stream(core_v1_api.connect_get_namespaced_pod_exec,
|
||||
pod.items[0].metadata.name,
|
||||
'default',
|
||||
command=['git', 'clone', git_url, clone_path],
|
||||
stderr=True, stdin=False,
|
||||
stdout=True, tty=False,
|
||||
_preload_content=False)
|
||||
while resp.is_open():
|
||||
resp.update(timeout=25)
|
||||
if resp.peek_stdout():
|
||||
_logger.info(str(resp.read_stdout()))
|
||||
if resp.peek_stderr():
|
||||
error = resp.read_stderr()
|
||||
_logger.error(str(error))
|
||||
else:
|
||||
_logger.info(str(
|
||||
"No Response"
|
||||
))
|
||||
resp.close()
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def restart_odoo_service(self):
|
||||
|
||||
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
try:
|
||||
data2 = yaml.safe_load(self.configuration.config_file)
|
||||
config.load_kube_config_from_dict(data2)
|
||||
except config.config_exception.ConfigException as e:
|
||||
_logger.error(str(e))
|
||||
raise UserError("Unable to Connect K8s Cluster")
|
||||
|
||||
if self.app_name:
|
||||
core_v1_api = client.CoreV1Api()
|
||||
pod = core_v1_api.list_namespaced_pod(namespace='default', label_selector='app={}'.format(self.app_name))
|
||||
exec_command = ['./mnt/restart_odoo.sh']
|
||||
resp = stream(core_v1_api.connect_get_namespaced_pod_exec,
|
||||
pod.items[0].metadata.name,
|
||||
'default',
|
||||
command=exec_command,
|
||||
stderr=True, stdin=True,
|
||||
stdout=True, tty=False,
|
||||
_preload_content=False)
|
||||
while resp.is_open():
|
||||
resp.update(timeout=10)
|
||||
if resp.peek_stdout():
|
||||
_logger.info(str(resp.read_stdout()))
|
||||
if resp.peek_stderr():
|
||||
error = resp.read_stderr()
|
||||
_logger.error(str(error))
|
||||
break
|
||||
resp.close()
|
||||
|
||||
|
||||
def read_deployment(self, dep_type='odoo'):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
try:
|
||||
data = yaml.safe_load(self.configuration.config_file)
|
||||
config.load_kube_config_from_dict(data)
|
||||
except config.config_exception.ConfigException as e:
|
||||
_logger.error(str(e))
|
||||
raise UserError("Unable to Connect K8s Cluster")
|
||||
if self.app_name:
|
||||
dep_name = self.app_name + "-odoo-deployment"
|
||||
core_v1_api = client.AppsV1Api()
|
||||
|
||||
try:
|
||||
deployment = core_v1_api.read_namespaced_deployment(name=dep_name, namespace='default')
|
||||
if deployment:
|
||||
return deployment
|
||||
return
|
||||
except Exception as e:
|
||||
pass
|
||||
else:
|
||||
_logger.error("Cant find App Name")
|
||||
raise UserError("Cant find App Name")
|
||||
|
||||
|
||||
def update_deployment(self, container_arguments, dep_type='odoo', env_vars=False):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
try:
|
||||
data = yaml.safe_load(self.configuration.config_file)
|
||||
config.load_kube_config_from_dict(data)
|
||||
except config.config_exception.ConfigException as e:
|
||||
_logger.error(str(e))
|
||||
raise UserError("Unable to Connect K8s Cluster")
|
||||
if self.app_name:
|
||||
core_v1_api = client.AppsV1Api()
|
||||
try:
|
||||
deployment = read_deployment(self=self)
|
||||
if container_arguments:
|
||||
deployment.spec.template.spec.containers[0].args = eval(container_arguments)
|
||||
if env_vars:
|
||||
deployment.spec.template.spec.containers[0].env = env_vars
|
||||
deployment.spec.template.metadata.labels['COMMIT_SHA'] = generate_commit_sha(10)
|
||||
|
||||
patched_deployment = core_v1_api.patch_namespaced_deployment(name=deployment.metadata.name,
|
||||
namespace='default',
|
||||
body=deployment)
|
||||
return patched_deployment
|
||||
except Exception as e:
|
||||
_logger.error(str(e))
|
||||
else:
|
||||
_logger.error("Cant find App Name")
|
||||
raise UserError("Cant find App Name")
|
||||
34
kk_odoo_saas/utils/logs.py
Executable file
@@ -0,0 +1,34 @@
|
||||
from kubernetes import config, client
|
||||
import yaml
|
||||
from kubernetes.client.rest import ApiException
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def read_logs(app_name, self=False, config_file=None, since_seconds=None, previous=False, tail_lines=None):
|
||||
# Configs can be set in Configuration class directly or using helper
|
||||
# utility. If no argument provided, the config will be loaded from
|
||||
# default location.
|
||||
try:
|
||||
data2 = yaml.safe_load(config_file)
|
||||
config.load_kube_config_from_dict(data2)
|
||||
|
||||
except config.config_exception.ConfigException as e:
|
||||
raise UserError("Unable to Connect K8s Cluster")
|
||||
|
||||
try:
|
||||
api_instance = client.CoreV1Api()
|
||||
odoo_pods = api_instance.list_namespaced_pod(namespace='default',
|
||||
label_selector='app={0},tier={1}'.format(str(self.app_name),
|
||||
'backend'))
|
||||
for pod in odoo_pods.items:
|
||||
if pod.metadata and pod.metadata.name and (self.app_name + '-odoo-deployment' in pod.metadata.name):
|
||||
odoo_logs = api_instance.read_namespaced_pod_log(name=pod.metadata.name, namespace='default',
|
||||
tail_lines=tail_lines, since_seconds=since_seconds)
|
||||
return odoo_logs
|
||||
return False
|
||||
except ApiException as e:
|
||||
_logger.error(e)
|
||||
return False
|
||||
37
kk_odoo_saas/utils/odoo_components.py
Executable file
@@ -0,0 +1,37 @@
|
||||
from .service import create_odoo_service, delete_odoo_service
|
||||
from .deployment import create_odoo_deployment, delete_odoo_deployment
|
||||
from .pv_claim import create_odoo_pv_claim, delete_odoo_pv_claim
|
||||
from .ingress import delete_odoo_ingress, update_odoo_ingress
|
||||
from .utils import delete_job_task
|
||||
|
||||
|
||||
def deploy_odoo_components(app_name, namespace, self=False):
|
||||
create_odoo_pv_claim(app_name, namespace, self=self)
|
||||
create_odoo_service(app_name, namespace, self=self)
|
||||
create_odoo_deployment(app_name, namespace, self=self)
|
||||
|
||||
|
||||
def delete_odoo_components(app_name, namespace, self=False):
|
||||
delete_odoo_pv_claim(app_name, namespace, self=self)
|
||||
delete_odoo_service(app_name, namespace, self=self)
|
||||
delete_odoo_deployment(app_name, namespace, self=self)
|
||||
delete_odoo_ingress(app_name, namespace, self=self)
|
||||
delete_job_task(self)
|
||||
|
||||
|
||||
def delete_odoo_components_from_options(app_name, namespace, self=False, delete_db=False,
|
||||
delete_pv=False, delete_svc=False,
|
||||
delete_ing=False, delete_deployment=False):
|
||||
if delete_pv:
|
||||
delete_odoo_pv_claim(app_name, namespace, self=self)
|
||||
if delete_svc:
|
||||
delete_odoo_service(app_name, namespace, self=self)
|
||||
if delete_deployment:
|
||||
delete_odoo_deployment(app_name, namespace, self=self)
|
||||
if delete_ing:
|
||||
delete_odoo_ingress(app_name, namespace, self=self)
|
||||
delete_job_task(self)
|
||||
|
||||
|
||||
def update_odoo_components(app_name, namespace, self=False):
|
||||
update_odoo_ingress(app_name, namespace, self)
|
||||
61
kk_odoo_saas/utils/pg_query.py
Executable file
@@ -0,0 +1,61 @@
|
||||
import psycopg2
|
||||
import sys
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PgQuery(object):
|
||||
"""
|
||||
USAGE:
|
||||
postgresX = ['localhost', 'sadsadsad', 'admin', 'codetuple']
|
||||
pgX = TaskMigration(*postgresX)
|
||||
with pgX:
|
||||
result = pgX.selectQuery(query)
|
||||
"""
|
||||
|
||||
def __init__(self, host, database, user, password, port=5432):
|
||||
self.host = host
|
||||
self.database = database
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.dbConnection = False
|
||||
self.cursor = False
|
||||
self.port = port
|
||||
|
||||
def __enter__(self):
|
||||
try:
|
||||
self.dbConnection = psycopg2.connect(host=self.host, database=self.database, user=self.user,
|
||||
password=self.password, port=self.port)
|
||||
self.cursor = self.dbConnection.cursor()
|
||||
except Exception as e:
|
||||
_logger.info("Error in Postgres Connection: %r" % e)
|
||||
sys.exit()
|
||||
return self.dbConnection
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.dbConnection:
|
||||
# self.dbConnection.close()
|
||||
pass
|
||||
|
||||
def select_query(self, queryString):
|
||||
status = True
|
||||
try:
|
||||
self.cursor.execute(queryString)
|
||||
except Exception as e:
|
||||
status = False
|
||||
return status
|
||||
else:
|
||||
return self.cursor.fetchall()
|
||||
|
||||
def execute_query(self, queryString):
|
||||
status = True
|
||||
try:
|
||||
self.cursor.execute(queryString)
|
||||
self.dbConnection.commit()
|
||||
except Exception as e:
|
||||
_logger.info(queryString)
|
||||
_logger.info(e)
|
||||
status = False
|
||||
finally:
|
||||
return status
|
||||
50
kk_odoo_saas/utils/pg_server.py
Executable file
@@ -0,0 +1,50 @@
|
||||
from psycopg2 import sql, connect
|
||||
import odoo
|
||||
from contextlib import closing
|
||||
import logging
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from .utils import generate_temp_password
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def drop_db(self, db_name):
|
||||
if self:
|
||||
child_conn = self.get_pg_db_connection(db=db_name)
|
||||
child_conn.set_session(autocommit=True)
|
||||
|
||||
with closing(child_conn.cursor()) as cr:
|
||||
odoo.service.db._drop_conn(cr, db_name)
|
||||
try:
|
||||
cr.execute(sql.SQL('DROP DATABASE {}').format(sql.Identifier(db_name)))
|
||||
except Exception as e:
|
||||
_logger.info('DROP DB: %s failed:\n%s', db_name, e)
|
||||
child_conn.close()
|
||||
raise UserError("Couldn't drop database %s: %s" % (db_name, e))
|
||||
else:
|
||||
child_conn.close()
|
||||
_logger.info('DROP DB: %s', db_name)
|
||||
return True
|
||||
|
||||
|
||||
def delete_databases(self):
|
||||
if self and self.client_db_name:
|
||||
# dbs = get_databases(self)
|
||||
drop_db(self, self.client_db_name)
|
||||
|
||||
|
||||
def get_admin_credentials(self):
|
||||
if self and self.client_db_name:
|
||||
# FOR admin user_id = 2
|
||||
child_conn = self.get_pg_db_connection(db=self.client_db_name)
|
||||
query = sql.SQL("SELECT login, COALESCE(password, '') FROM res_users WHERE id=2;")
|
||||
with closing(child_conn.cursor()) as cr:
|
||||
try:
|
||||
cr.execute(query)
|
||||
res = cr.fetchall()
|
||||
child_conn.close()
|
||||
except Exception:
|
||||
_logger.exception('Getting Credentials failed')
|
||||
res = False
|
||||
child_conn.close()
|
||||
return res, self.client_db_name
|
||||
return False, False
|
||||
60
kk_odoo_saas/utils/pv_claim.py
Executable file
@@ -0,0 +1,60 @@
|
||||
from kubernetes import client, config
|
||||
|
||||
import logging
|
||||
from odoo.addons.smile_log.tools import SmileDBLogger
|
||||
|
||||
|
||||
def create_pv_claim(meta_data, specs, namespace="default", self=False):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
|
||||
k8s_apps_v1 = client.CoreV1Api()
|
||||
|
||||
dep = client.V1PersistentVolumeClaim(
|
||||
api_version='v1',
|
||||
kind='PersistentVolumeClaim',
|
||||
metadata=meta_data,
|
||||
spec=specs
|
||||
)
|
||||
try:
|
||||
resp = k8s_apps_v1.create_namespaced_persistent_volume_claim(
|
||||
body=dep, namespace=namespace)
|
||||
_logger.info("Volume created. status='%s'" % resp.metadata.name)
|
||||
except client.exceptions.ApiException as e:
|
||||
_logger.error(msg=str(e))
|
||||
|
||||
|
||||
def create_odoo_pv_claim(app_name, namespace="default", self=False):
|
||||
specs = client.V1PersistentVolumeClaimSpec(
|
||||
access_modes=[
|
||||
'ReadWriteOnce'
|
||||
],
|
||||
storage_class_name="gp2",
|
||||
resources=client.V1ResourceRequirements(
|
||||
requests={
|
||||
'storage': '8Gi'
|
||||
}
|
||||
)
|
||||
)
|
||||
meta_data = client.V1ObjectMeta(
|
||||
name=app_name + "-odoo-web-pv-claim",
|
||||
labels={"app": app_name}
|
||||
)
|
||||
create_pv_claim(meta_data=meta_data, specs=specs, namespace=namespace, self=self)
|
||||
|
||||
|
||||
def delete_odoo_pv_claim(app_name, namespace="default", self=False):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
claim_name = app_name + "-odoo-web-pv-claim"
|
||||
core_v1_api = client.CoreV1Api()
|
||||
|
||||
try:
|
||||
pv = core_v1_api.delete_namespaced_persistent_volume_claim(name=claim_name, namespace=namespace)
|
||||
_logger.info(str(pv))
|
||||
except client.exceptions.ApiException as e:
|
||||
_logger.error(str(e))
|
||||
|
||||
|
||||
|
||||
|
||||
69
kk_odoo_saas/utils/service.py
Executable file
@@ -0,0 +1,69 @@
|
||||
from kubernetes import client, config
|
||||
import random
|
||||
from urllib.parse import urlparse
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
from odoo.addons.smile_log.tools import SmileDBLogger
|
||||
|
||||
|
||||
def create_service(specs, metadata, namespace="default", self=False):
|
||||
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
|
||||
|
||||
core_v1_api = client.CoreV1Api()
|
||||
body = client.V1Service(
|
||||
api_version="v1",
|
||||
kind="Service",
|
||||
metadata=metadata,
|
||||
spec=specs
|
||||
)
|
||||
# Creation of the Deployment in specified namespace
|
||||
try:
|
||||
service = core_v1_api.create_namespaced_service(namespace=namespace, body=body)
|
||||
_logger.info("Service created. status='%s'" % service.metadata.name)
|
||||
|
||||
except client.exceptions.ApiException as e:
|
||||
_logger.error(str(e))
|
||||
|
||||
|
||||
|
||||
|
||||
def create_odoo_service(app_name, namespace, self=False):
|
||||
service_name = app_name + "-odoo-service"
|
||||
specs = client.V1ServiceSpec(
|
||||
selector={"app": app_name, "tier": "backend"},
|
||||
ports=[client.V1ServicePort(
|
||||
name='odoo-port',
|
||||
protocol="TCP",
|
||||
port=80,
|
||||
target_port=8069,
|
||||
),
|
||||
client.V1ServicePort(
|
||||
name='longpolling',
|
||||
protocol="TCP",
|
||||
port=8072,
|
||||
target_port=8072,
|
||||
)
|
||||
],
|
||||
type="NodePort"
|
||||
)
|
||||
metadata = client.V1ObjectMeta(
|
||||
name=service_name,
|
||||
labels={"app": app_name}
|
||||
)
|
||||
create_service(metadata=metadata, specs=specs, namespace=namespace, self=self)
|
||||
|
||||
|
||||
def delete_odoo_service(app_name, namespace, self=False):
|
||||
service_name = app_name + "-odoo-service"
|
||||
core_v1_api = client.CoreV1Api()
|
||||
|
||||
try:
|
||||
service = core_v1_api.delete_namespaced_service(name=service_name, namespace=namespace)
|
||||
_logger.info(service)
|
||||
|
||||
except client.exceptions.ApiException as e:
|
||||
_logger.error(str(e))
|
||||
|
||||
|
||||
|
||||
29
kk_odoo_saas/utils/utils.py
Executable file
@@ -0,0 +1,29 @@
|
||||
def generate_temp_password(length):
|
||||
if not isinstance(length, int) or length < 8:
|
||||
raise ValueError("temp password must have positive length")
|
||||
|
||||
chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*()"
|
||||
from os import urandom
|
||||
# Python 3 (urandom returns bytes)
|
||||
return "".join(chars[c % len(chars)] for c in urandom(length))
|
||||
|
||||
|
||||
def generate_commit_sha(length):
|
||||
if not isinstance(length, int) or length < 8:
|
||||
raise ValueError("sha must have positive length")
|
||||
|
||||
chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||
from os import urandom
|
||||
# Python 3 (urandom returns bytes)
|
||||
return "".join(chars[c % len(chars)] for c in urandom(length))
|
||||
|
||||
|
||||
def delete_job_task(self):
|
||||
if self and self.id:
|
||||
job_q_env = self.env['queue.job']
|
||||
jobs = job_q_env.search([
|
||||
"|", "|", "|", ("state", "=", "pending"), (
|
||||
"state", "=", "enqueued"), ("state", "=", "started"), ("state", "=", "failed"),
|
||||
('func_string', '=', "kk_odoo_saas.app({0},).post_init_tasks()".format(self.id))])
|
||||
for job in jobs:
|
||||
job.button_done()
|
||||
303
kk_odoo_saas/views/app_views.xml
Executable file
@@ -0,0 +1,303 @@
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Sequences for kk_odoo_saas.app -->
|
||||
<record id="kk_odoo_saas_app_sequence" model="ir.sequence">
|
||||
<field name="name">SaaS App Sequence</field>
|
||||
<field name="code">kk_odoo_saas.app</field>
|
||||
<field name="prefix">saas-app</field>
|
||||
<field name="padding">3</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
<data>
|
||||
<record id="kk_odoo_saas_app_view_form" model="ir.ui.view">
|
||||
<field name="name">kk_odoo_saas_app_view_form</field>
|
||||
<field name="model">kk_odoo_saas.app</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="deploy_app" string="Deploy App" type="object"
|
||||
attrs="{'invisible':[('status','not in',['d', 'del'])]}" class="btn-primary"/>
|
||||
<button name="kk_odoo_saas.action_saas_app_delete_wizard"
|
||||
confirm="Are you sure to DELETE this Deployment? it will DELETE all Data of this App ."
|
||||
string="Delete Deployment" type="action" icon="fa-trash"
|
||||
attrs="{'invisible':[('status','not in',['l', 'm'])]}" class="btn-primary"/>
|
||||
<!-- <button name="update_app" string="Update Deployment" type="object"-->
|
||||
<!-- attrs="{'invisible':[('status','not in',['m'])]}" class="btn-primary"/>-->
|
||||
<button name="deploy_apps_from_git" string="Update Git Code" type="object"
|
||||
attrs="{'invisible':[('is_extra_addon','=',False)]}" class="btn-secondary"/>
|
||||
<button name="del_git_dir" string="Delete Git Code" type="object"
|
||||
attrs="{'invisible':[('is_extra_addon','=',False)]}" class="btn-secondary"/>
|
||||
<button name="restart_odoo_service" string="Restart Odoo Service" type="object"
|
||||
class="btn-secondary" icon="fa-refresh"/>
|
||||
<field name="status" widget="statusbar"/>
|
||||
</header>
|
||||
<header>
|
||||
<button name="action_create_subscription"
|
||||
type="object"
|
||||
string="Create Subscription"
|
||||
attrs="{'invisible': [('subscription_id','!=',False)]}"
|
||||
/>
|
||||
</header>
|
||||
|
||||
<sheet>
|
||||
<div class="oe_button_box" style="justify-content: space-between; display: flex;">
|
||||
<div>
|
||||
|
||||
<button name="action_connect_instance"
|
||||
type="object" icon="fa-rocket"
|
||||
class="oe_stat_button pl-3"
|
||||
string="Connect"
|
||||
attrs="{'invisible': [('status','in',['d','del'])]}"
|
||||
/>
|
||||
<button name="kk_odoo_saas.action_saas_app_update_dkr_img_wizard"
|
||||
string="Update Docker Image" type="action" icon="fa-cloud-upload"
|
||||
attrs="{'invisible':[('status','not in',['l', 'm'])]}" class="oe_stat_button"/>
|
||||
</div>
|
||||
|
||||
|
||||
<button name="action_show_subscription"
|
||||
type="object" icon="fa-file"
|
||||
string="Show Subscription"
|
||||
attrs="{'invisible': [('subscription_id','=',False)]}"
|
||||
class="oe_stat_button"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group string="App Info">
|
||||
<field name="app_name" readonly="1" force_save="1"/>
|
||||
<field name="name" required="1"/>
|
||||
</group>
|
||||
<group string="K8s Cluster Configuration">
|
||||
<field name="configuration" options="{'no_create': True, 'no_edit': True}" required="1" attrs="{'readonly': [('status', 'in', ['l','m'])]}"/>
|
||||
</group>
|
||||
<group string="Node Configuration">
|
||||
<field name="is_dedicated_node" required="0" attrs="{'readonly': [('status', 'in', ['l','m'])]}" string="Any Specific Node"/>
|
||||
<field name="node_id"
|
||||
attrs="{'readonly': [('status', 'in', ['l','m'])], 'required': [('is_dedicated_node', '=', True)], 'invisible': [('is_dedicated_node', '=', False)]}"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
<!-- <field name="node_key"-->
|
||||
<!-- attrs="{'readonly': [('status', 'in', ['l','m'])], 'invisible': [('is_dedicated_node', '=', False)]}"-->
|
||||
<!-- />-->
|
||||
<!-- <field name="node_value"-->
|
||||
<!-- attrs="{'readonly': [('status', 'in', ['l','m'])], 'invisible': [('is_dedicated_node', '=', False)]}"-->
|
||||
<!-- />-->
|
||||
<div>
|
||||
<p>
|
||||
<button name="refresh_node_list"
|
||||
attrs="{'invisible': [('is_dedicated_node', '=', False)]}"
|
||||
type="object" string="Click to Refresh Nodes List."
|
||||
class="oe_link" />
|
||||
</p>
|
||||
</div>
|
||||
</group>
|
||||
<group string="Domain Configuration">
|
||||
|
||||
<div>
|
||||
<field name="sub_domain_name" class="oe_inline"/>
|
||||
<field name="domain_name" class="oe_inline"/>
|
||||
</div>
|
||||
</group>
|
||||
<group string="Database Server">
|
||||
<field name="db_server_id" options="{'no_create': True, 'no_edit': True}" domain="[('status', '=', 'connected')]" required="1"/>
|
||||
<field name="client_db_name" required="1"/>
|
||||
</group>
|
||||
<group string="Extra Addons from Git">
|
||||
<field name="is_extra_addon" widget="boolean_toggle"/>
|
||||
<field name="extra_addons"
|
||||
attrs="{'invisible': [('is_extra_addon', '=', False)], 'required':[('is_extra_addon', '=', True)]}"/>
|
||||
<field name="is_private_repo" attrs="{'invisible': [('is_extra_addon', '=', False)]}"/>
|
||||
<field name="git_token" password="True"
|
||||
attrs="{'invisible': ['|', ('is_extra_addon', '=', False), ('is_private_repo', '=', False)], 'required':[('is_private_repo', '=', True)]}"/>
|
||||
</group>
|
||||
<group string="App Version (Docker Image)">
|
||||
<field name="docker_image" required="1" attrs="{'readonly': [('status', 'in', ['l','m'])]}"/>
|
||||
</group>
|
||||
<group string="Demo Data">
|
||||
<field name="demo_data" widget="boolean_toggle" attrs="{'readonly': [('status', 'in', ['l','m'])]}"/>
|
||||
</group>
|
||||
|
||||
|
||||
<group string="Odoo Modules">
|
||||
<field name="module_ids" widget="many2many_tags" attrs="{'readonly': [('status', 'in', ['l','m'])]}"/>
|
||||
</group>
|
||||
<group string="App Credentials">
|
||||
<field name="login_email" string="Email / Username"/>
|
||||
<label for="login_pwd" string="Password"/>
|
||||
<!-- <div>-->
|
||||
<field name="login_pwd" password="True" widget="CopyClipboardChar"/>
|
||||
<!-- <button class="oe_inline btn btn-primary" type="object" name="reset_app_password" string="Reset Credentials"/>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<field name="master_login_email"/>
|
||||
<field name="master_login_pwd" password="True" widget="CopyClipboardChar"/>
|
||||
|
||||
</group>
|
||||
|
||||
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Client Details">
|
||||
<group>
|
||||
<group>
|
||||
<field name="client" force_save="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="country_id" options="{'no_create': True}" attrs="{'readonly': [('status', 'in', ['l','m'])]}"/>
|
||||
<field name="admin_user" context="{'form_view_ref': 'base.view_users_form',}" required="1" attrs="{'readonly': [('status', 'in', ['l','m'])]}"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
</page>
|
||||
<page string="Subscription Details">
|
||||
<group>
|
||||
<group>
|
||||
<field name="subscription_id"/>
|
||||
</group>
|
||||
<group>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
</page>
|
||||
<page name="k8s_logs" string="K8s Logs">
|
||||
<field name="k8s_logs"/>
|
||||
</page>
|
||||
<page name="instance_logs" string="Instance Logs">
|
||||
<button name="get_pod_logs" type="object" string="Download Logs File"/>
|
||||
<button name="action_log_viewer" type="object" string="See Realtime Logs"/>
|
||||
</page>
|
||||
<!-- <page name="redeploy_app" string="Update Docker Image">-->
|
||||
<!-- </page>-->
|
||||
<page name="custom_domains" string="Custom Domains">
|
||||
<field name="custom_domain_ids"/>
|
||||
</page>
|
||||
</notebook>
|
||||
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" groups="base.group_user"/>
|
||||
<field name="activity_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_odoo_saas_app_view_kanban" model="ir.ui.view">
|
||||
<field name="name">kk_odoo_saas_app_view_kanban</field>
|
||||
<field name="model">kk_odoo_saas.app</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban records_draggable="0">
|
||||
<field name="app_name"/>
|
||||
<field name="name"/>
|
||||
<field name="status"/>
|
||||
<field name="domain_name"/>
|
||||
<field name="sub_domain_name"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click container">
|
||||
<field name="name"/>
|
||||
<br/>
|
||||
<field name="app_name"/>
|
||||
<t t-if="record.status.raw_value in ['l', 'm']">
|
||||
<p>
|
||||
<field name="sub_domain_name"/>
|
||||
<field name="domain_name"/>
|
||||
</p>
|
||||
</t>
|
||||
<div>Custom Domains: </div><field name="custom_domain_ids" widget="many2many_tags"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_odoo_saas_app_view_search" model="ir.ui.view">
|
||||
<field name="name">kk_odoo_saas.app.search</field>
|
||||
<field name="model">kk_odoo_saas.app</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Apps">
|
||||
<field name="name" string="Apps"
|
||||
filter_domain="[('name','ilike',self)]"/>
|
||||
<field name="app_name" string="Unique Id"/>
|
||||
<field name="sub_domain_name" string="Domain Name"/>
|
||||
<field name="status" string="State"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="group_status" string="State" domain="[]" context="{'group_by':'status'}"/>
|
||||
<filter name="group_configuration" string="Configuration" domain="[]"
|
||||
context="{'group_by':'configuration'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_odoo_saas_app_view_tree" model="ir.ui.view">
|
||||
<field name="name">kk_odoo_saas_app_view_tree</field>
|
||||
<field name="model">kk_odoo_saas.app</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="app_name"/>
|
||||
<field name="name"/>
|
||||
<field name="configuration"/>
|
||||
<field name="client"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_odoo_saas_app_action" model="ir.actions.act_window">
|
||||
<field name="name">Apps</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">kk_odoo_saas.app</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="context">{'search_default_group_status': True}</field>
|
||||
|
||||
</record>
|
||||
|
||||
|
||||
<!-- Top menu item -->
|
||||
<menuitem name="SaaS" id="menu_root"
|
||||
web_icon="kk_odoo_saas,static/description/icon.png"
|
||||
sequence="-40"
|
||||
/>
|
||||
|
||||
<!-- actions -->
|
||||
<menuitem name="Apps" id="kk_odoo_saas_apps" parent="menu_root"
|
||||
action="kk_odoo_saas.kk_odoo_saas_app_action"/>
|
||||
|
||||
<menuitem id="menu_kk_odoo_saas_smile_log" parent="menu_root" name="Logs" action="smile_log.act_smile_log"/>
|
||||
|
||||
<menuitem id="menu_kk_odoo_saas_customer" parent="menu_root" name="Customers / Orders"/>
|
||||
<menuitem id="menu_kk_odoo_saas_customer_customers" parent="menu_kk_odoo_saas_customer" name="Customers"
|
||||
action="base.action_partner_customer_form" sequence="10"/>
|
||||
|
||||
<menuitem id="menu_kk_odoo_saas_customer_subscriptions" parent="menu_kk_odoo_saas_customer" name="Subscriptions"
|
||||
action="sale_subscription.sale_subscription_action" sequence="20"/>
|
||||
|
||||
<record id="action_orders_ecommerce" model="ir.actions.act_window">
|
||||
<field name="name">Orders</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="view_mode">tree,form,kanban,activity</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="search_view_id" ref="website_sale.view_sales_order_filter_ecommerce"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
There is no confirmed order from the website
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<menuitem id="menu_kk_odoo_saas_customer_orders" parent="menu_kk_odoo_saas_customer" name="Orders"
|
||||
action="action_orders_ecommerce" sequence="30"/>
|
||||
|
||||
<menuitem id="menu_kk_odoo_saas_users" parent="menu_kk_odoo_saas_customer" name="Portal Users"
|
||||
action="base.action_res_users" sequence="30"/>
|
||||
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
10
kk_odoo_saas/views/assets.xml
Executable file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="assets_backend" name="line assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/kk_odoo_saas/static/src/js/refresh_button.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
||||
</odoo>
|
||||
274
kk_odoo_saas/views/config_views.xml
Executable file
@@ -0,0 +1,274 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- explicit list view definition -->
|
||||
<record model="ir.ui.view" id="kk_odoo_saas_k8s_config_list">
|
||||
<field name="name">kk_odoo_saas k8s config list</field>
|
||||
<field name="model">kk_odoo_saas.k8s.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_odoo_saas_k8s_config_kanban" model="ir.ui.view">
|
||||
<field name="name">kk_odoo_saas k8s config kanban</field>
|
||||
<field name="model">kk_odoo_saas.k8s.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban>
|
||||
<field name="name"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click container">
|
||||
<field name="name"/>
|
||||
<!-- <t t-raw="record.name"/>-->
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_odoo_saas_k8s_config_view_form" model="ir.ui.view">
|
||||
<field name="name">kk_odoo_saas_k8s_config_view_form</field>
|
||||
<field name="model">kk_odoo_saas.k8s.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="update_cluster_nodes" string="Update Nodes" type="object"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name" required="1"/>
|
||||
<field name="config_file" required="1" widget="ace" class="oe_edit_only"/>
|
||||
<field name="domain_name"
|
||||
placeholder=".yourdomain.com | A DNS Should be set and configures as *.yourdomain.com"
|
||||
required="1"/>
|
||||
<field name="namespaces" readonly="1"/>
|
||||
<!-- <button name="check_connectivity" string="Check Connection" type="object"/>-->
|
||||
</group>
|
||||
<group>
|
||||
<button name="check_connectivity" string="Check Connection" type="object"/>
|
||||
</group>
|
||||
<!-- <notebook>-->
|
||||
<!-- <page string="NameSpaces" name="ns">-->
|
||||
|
||||
<!-- </page>-->
|
||||
<!-- <page string="Pods" name="pod">-->
|
||||
|
||||
<!-- </page>-->
|
||||
<!-- <page string="deployments" name="deployment">-->
|
||||
|
||||
<!-- </page>-->
|
||||
<!-- <page string="Ingresses" name="ingress">-->
|
||||
|
||||
<!-- </page>-->
|
||||
<!-- <page string="Service" name="service">-->
|
||||
|
||||
<!-- </page>-->
|
||||
<!-- <page string="PVs" name="pv">-->
|
||||
|
||||
<!-- </page>-->
|
||||
<!-- </notebook>-->
|
||||
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_odoo_saas_k8s_config_action" model="ir.actions.act_window">
|
||||
<field name="name">K8s Configuration</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">kk_odoo_saas.k8s.config</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="kk_odoo_saas_k8s_docker_images_list">
|
||||
<field name="name">kk_odoo_saas k8s config list</field>
|
||||
<field name="model">kk_odoo_saas.k8s.docker.images</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="tag"/>
|
||||
<field name="description"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_odoo_saas_k8s_docker_images_kanban" model="ir.ui.view">
|
||||
<field name="name">kk_odoo_saas k8s docker images kanban</field>
|
||||
<field name="model">kk_odoo_saas.k8s.docker.images</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban>
|
||||
<field name="name"/>
|
||||
<field name="tag"/>
|
||||
<field name="description"/>
|
||||
<field name="is_pvt_dkr_repo"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click container">
|
||||
<strong>Image Name:</strong> <field name="name"/>
|
||||
<br/>
|
||||
<strong>Tag Name:</strong> <field name="tag"/>
|
||||
<br/>
|
||||
<strong>Description:</strong> <field name="description"/>
|
||||
<br/>
|
||||
<strong>Is Private Repo?</strong> <field name="is_pvt_dkr_repo"/>
|
||||
<!-- <t t-raw="record.name"/>-->
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_odoo_saas_k8s_docker_images_view_form" model="ir.ui.view">
|
||||
<field name="name">kk_odoo_saas_k8s_docker_images_view_form</field>
|
||||
<field name="model">kk_odoo_saas.k8s.docker.images</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="tag"/>
|
||||
<field name="description"/>
|
||||
<field name="repo_link"/>
|
||||
<field name="base_version"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="is_pvt_dkr_repo" widget="boolean_toggle"/>
|
||||
<field name="b64_dkr_config" class="oe_edit_only" attrs="{'invisible': [('is_pvt_dkr_repo', '=', False)],
|
||||
'required':[('is_pvt_dkr_repo', '=', True)]}"
|
||||
placeholder="Place your .docker/config.json after encoding it in base64"/>
|
||||
|
||||
</group>
|
||||
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_odoo_saas_k8s_docker_images_action" model="ir.actions.act_window">
|
||||
<field name="name">Docker Images</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">kk_odoo_saas.k8s.docker.images</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_odoo_saas_k8s_node_view_tree" model="ir.ui.view">
|
||||
<field name="name">kk_odoo_saas_k8s_node_view_tree</field>
|
||||
<field name="model">kk_odoo_saas.k8s.node</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Cluster Nodes" create="false">
|
||||
<field name="name"/>
|
||||
<field name="create_date"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_odoo_saas_k8s_node_view_form" model="ir.ui.view">
|
||||
<field name="name">kk_odoo_saas_k8s_node_view_form</field>
|
||||
<field name="model">kk_odoo_saas.k8s.node</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Cluster Nodes" create="false">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="labels" widget="ace"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="annotations" widget="ace"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="taints" widget="ace"/>
|
||||
</group>
|
||||
|
||||
</sheet>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="yaml_info" widget="ace"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_master_db_server_view_tree" model="ir.ui.view">
|
||||
<field name="name">kk_master_db_server_view_tree</field>
|
||||
<field name="model">kk_odoo_saas.k8s.master_db_creds</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="DB Server Credentials" >
|
||||
<field name="name"/>
|
||||
<field name="server_url"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_master_db_server_view_form" model="ir.ui.view">
|
||||
<field name="name">kk_master_db_server_view_form</field>
|
||||
<field name="model">kk_odoo_saas.k8s.master_db_creds</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="DB Server Credentials">
|
||||
<header>
|
||||
<button name="check_connectivity" string="Check Connectivity" type="object"/>
|
||||
<field name="status" widget="statusbar"/>
|
||||
</header>
|
||||
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="master_username" />
|
||||
<field name="server_port" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="master_pass" password="True"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="server_url" />
|
||||
</group>
|
||||
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="kk_odoo_saas_k8s_node_action" model="ir.actions.act_window">
|
||||
<field name="name">Cluster Nodes</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">kk_odoo_saas.k8s.node</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_odoo_saas_master_db_creds_action" model="ir.actions.act_window">
|
||||
<field name="name">Master DB Creds</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">kk_odoo_saas.k8s.master_db_creds</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
|
||||
<menuitem name="Configuration" id="kk_odoo_saas_configuration_root" parent="menu_root"/>
|
||||
|
||||
<menuitem name="Clusters Configurations" id="kk_odoo_saas_configuration"
|
||||
parent="kk_odoo_saas_configuration_root"
|
||||
action="kk_odoo_saas.kk_odoo_saas_k8s_config_action"/>
|
||||
|
||||
<menuitem name="Docker Images" id="kk_odoo_saas_docker_images" parent="kk_odoo_saas_configuration_root"
|
||||
action="kk_odoo_saas.kk_odoo_saas_k8s_docker_images_action"/>
|
||||
|
||||
<menuitem name="Cluster Nodes" id="kk_odoo_saas_cluster_nodes" parent="kk_odoo_saas_configuration_root"
|
||||
action="kk_odoo_saas.kk_odoo_saas_k8s_node_action"/>
|
||||
|
||||
<menuitem name="Master DB Server" id="kk_odoo_saas_master_db_server" parent="kk_odoo_saas_configuration_root"
|
||||
action="kk_odoo_saas.kk_odoo_saas_master_db_creds_action"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
49
kk_odoo_saas/views/logs_viewer.xml
Executable file
@@ -0,0 +1,49 @@
|
||||
<odoo>
|
||||
<!-- Header -->
|
||||
<template id="saas_app_log_viewer">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<title>Logs Viewer</title>
|
||||
|
||||
<link rel="stylesheet" href="/kk_odoo_saas/static/src/css/bootstrap.css"/>
|
||||
<link rel="stylesheet" href="/kk_odoo_saas/static/src/css/font-awesome.css"/>
|
||||
<link rel="stylesheet" href="/kk_odoo_saas/static/src/css/logs-viewer.css"/>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<div class="container-fluid o-logs-container">
|
||||
<div class="row">
|
||||
<div class="o-logs col-md-12">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header">
|
||||
<div class="status-bar">
|
||||
<form class="form-inline d-flex justify-content-end" _lpchecked="1">
|
||||
<a class="btn button-pause">
|
||||
<i class="fa fa-pause" aria-hidden="true" style="display: none;"></i>
|
||||
<i class="fa fa-play" aria-hidden="true" style=""></i>
|
||||
<div class="loader"></div>
|
||||
</a>
|
||||
<div class="form-group d-flex">
|
||||
<label for="filter">Filter:</label>
|
||||
<input type="text" class="form-control ml-2" id="filter" placeholder="ERROR"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script type="text/javascript" src="/kk_odoo_saas/static/src/js/jquery.js"></script>
|
||||
<script type="text/javascript" src="/kk_odoo_saas/static/src/js/logs-viewer.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
53
kk_odoo_saas/views/res_config_settings_views.xml
Executable file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.website.apps</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="website.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='website_settings']" position="after">
|
||||
<h2>SaaS pricing page</h2>
|
||||
<div class="row mt16 o_settings_container" id="apps_general_settings">
|
||||
<div class="col-12 col-md-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="show_packages"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="show_packages" string="Show packages"/>
|
||||
<div class="text-muted">
|
||||
Show packages at "price" page
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="show_apps"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="show_apps" string="Show apps"/>
|
||||
<div class="text-muted">
|
||||
Show applications at "price" page
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="show_buy_now_button"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="show_buy_now_button" string="Show 'Buy now' button"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="show_try_trial_button"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="show_try_trial_button" string="Show 'Try trial' button"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
75
kk_odoo_saas/views/saas_app_website.xml
Executable file
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="saas_app_view_form" model="ir.ui.view">
|
||||
<field name="name">saas.app.form</field>
|
||||
<field name="model">saas.app</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="icon_image" widget="image" class="oe_avatar o_field_image"/>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="shortdesc"/>
|
||||
<field name="allow_to_sell"/>
|
||||
<field name="month_price" class="oe_inline" widget='monetary'/>
|
||||
<field name="year_price" class="oe_inline" widget='monetary'/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="dependency_ids"/>
|
||||
<field name="product_tmpl_id"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="saas_app_search" model="ir.ui.view">
|
||||
<field name="name">saas.app.search</field>
|
||||
<field name="model">saas.app</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Redirect">
|
||||
<field name="shortdesc"/>
|
||||
<field name="allow_to_sell"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="saas_app_view_tree" model="ir.ui.view">
|
||||
<field name="name">saas.app.list</field>
|
||||
<field name="model">saas.app</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Manage Apps">
|
||||
<field name="shortdesc"/>
|
||||
<field name="allow_to_sell"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_saas_app_list" model="ir.actions.act_window">
|
||||
<field name="name">Website Apps</field>
|
||||
<field name="res_model">saas.app</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="saas_app_view_tree"/>
|
||||
<field name="target">current</field>
|
||||
<field name="search_view_id" ref="saas_app_search"/>
|
||||
</record>
|
||||
|
||||
<menuitem name="Website"
|
||||
id="menu_website"
|
||||
parent="kk_odoo_saas.menu_root"
|
||||
/>
|
||||
<menuitem name="Manage Apps"
|
||||
id="menu_website_manage_apps_list"
|
||||
action="action_saas_app_list"
|
||||
parent="menu_website"
|
||||
sequence="40"
|
||||
/>
|
||||
<menuitem name="Dashboard"
|
||||
id="menu_website_dashboard"
|
||||
action="website.backend_dashboard"
|
||||
parent="menu_website"
|
||||
sequence="60"
|
||||
/>
|
||||
</data>
|
||||
</odoo>
|
||||
73
kk_odoo_saas/views/saas_package_views.xml
Executable file
@@ -0,0 +1,73 @@
|
||||
<odoo>
|
||||
<record id='saas_package_view_tree' model='ir.ui.view'>
|
||||
<field name="name">saas.package.tree</field>
|
||||
<field name="model">saas.package</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name" string="Package Name"/>
|
||||
<field name="month_price" string="Monthly Price"/>
|
||||
<field name="year_price" string="Yearly Price"/>
|
||||
<field name="month_product_id"/>
|
||||
<field name="year_product_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="saas_package_form_view" model="ir.ui.view">
|
||||
<field name="name">saas.package.form</field>
|
||||
<field name="model">saas.package</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="package_image" widget="image" class="oe_avatar o_field_image"/>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" readonly="0" placeholder="Name"/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group>
|
||||
<field name="is_published"/>
|
||||
<field name="docker_image" required="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="month_price"/>
|
||||
<field name="year_price"/>
|
||||
</group>
|
||||
|
||||
<group>
|
||||
<field name="month_product_id"/>
|
||||
<field name="year_product_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="module_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="product_tmpl_id" string="Product Template"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="stripe_product_id" password="True"/>
|
||||
<field name="subscription_template" required="True"/>
|
||||
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_saas_package_list" model="ir.actions.act_window">
|
||||
<field name="name">Portal Packages</field>
|
||||
<field name="res_model">saas.package</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem name="Manage Packages"
|
||||
id="menu_website_manage_package_list"
|
||||
action="action_saas_package_list"
|
||||
parent="menu_website"
|
||||
sequence="40"
|
||||
/>
|
||||
|
||||
</odoo>
|
||||
15
kk_odoo_saas/views/sale_subscription.xml
Executable file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="sale_subscription_view_form" model="ir.ui.view">
|
||||
<field name="name">sale.subscription.inherit.form.view</field>
|
||||
<field name="model">sale.subscription</field>
|
||||
<field name="inherit_id" ref="sale_subscription.sale_subscription_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="build_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
66
kk_odoo_saas/views/templates.xml
Executable file
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template inherit_id="auth_signup.fields" id="additional_fields" name="Auth Signup additional form fields">
|
||||
|
||||
<xpath expr="//div[hasclass('form-group', 'field-name')]" position="after">
|
||||
<t t-if="not only_passwords">
|
||||
<input type="hidden" name="operator_id" t-att-value="operator_id" />
|
||||
|
||||
<div class="loader hid transition">
|
||||
<img src="/kk_odoo_saas/static/src/img/loader.gif" draggable="false">
|
||||
<p class="status">Creating database...</p>
|
||||
</img>
|
||||
</div>
|
||||
|
||||
<div class="form-group field-company_name">
|
||||
<label for="company_name">Company name</label>
|
||||
<input type="text" name="company_name" id="company_name" class="form-control form-control-sm"
|
||||
t-att-value="company_name"
|
||||
required="required"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group field-database_name">
|
||||
<label for="database_name">Database name</label>
|
||||
<input type="text" name="database_name" id="database_name" class="form-control form-control-sm"
|
||||
t-att-value="database_name"
|
||||
required="required"/>
|
||||
<small id="build-domain-helper">
|
||||
<span class="text-danger build-domain-helper_status build-domain-helper_status-false" style="display: none">Domain is not available</span>
|
||||
<span class="text-success build-domain-helper_status build-domain-helper_status-true" style="display: none">
|
||||
Your domain
|
||||
<span class="domain"></span>
|
||||
</span>
|
||||
<span class="build-domain-helper_status build-domain-helper_status-loading" style="display: none">Detecting is domain available</span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group field-phone">
|
||||
<label for="phone">Phone</label>
|
||||
<input type="text" name="phone" id="phone" class="form-control form-control-sm"
|
||||
t-att-value="phone"
|
||||
required="required"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group field-database_lang">
|
||||
<label for="database_lang">Language</label>
|
||||
<select id="database_lang" name="database_lang" class="form-control" autocomplete="off">
|
||||
<option value=""></option>
|
||||
<t t-foreach="langs" t-as="item">
|
||||
<option t-att-value="item[0]" t-esc="item[1]" t-att-selected="'selected' if database_lang == item[0] else None"/>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group field-country_code">
|
||||
<label for="country_code">Country</label>
|
||||
<select id="country_code" name="country_code" class="form-control" autocomplete="off">
|
||||
<option value=""></option>
|
||||
<t t-foreach="countries" t-as="item">
|
||||
<option t-att-value="item[0]" t-esc="item[1]" t-att-selected="'selected' if country_code == item[0] else None"/>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
2
kk_odoo_saas/wizards/__init__.py
Executable file
@@ -0,0 +1,2 @@
|
||||
from . import saas_app_delete
|
||||
from . import update_docker_image
|
||||
31
kk_odoo_saas/wizards/saas_app_delete.py
Executable file
@@ -0,0 +1,31 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class SaaSAppDelete(models.TransientModel):
|
||||
_name = "kk_odoo_saas.app.delete.wizard"
|
||||
_description = "Wizard to Destroy a SaaS App/Instance"
|
||||
|
||||
delete_database = fields.Boolean('Delete Database?', default=False)
|
||||
delete_pv = fields.Boolean('Delete Attachments and Web Data?', default=False)
|
||||
delete_svc = fields.Boolean('Delete Services and Ingress Rules?', default=True)
|
||||
delete_ing = fields.Boolean('Delete Ingress Rules?', default=True)
|
||||
delete_deployment = fields.Boolean('Delete Container / Pod(s) ?', default=True)
|
||||
|
||||
|
||||
def _default_app_id(self):
|
||||
res = False
|
||||
context = self.env.context
|
||||
if context.get("active_model") == "kk_odoo_saas.app" and context.get("active_id"):
|
||||
res = context["active_id"]
|
||||
return res
|
||||
|
||||
app_id = fields.Many2one(
|
||||
comodel_name="kk_odoo_saas.app", string="SaaS App", default=lambda r: r._default_app_id()
|
||||
)
|
||||
|
||||
def delete_saas_instance(self):
|
||||
if self.app_id:
|
||||
self.app_id.delete_app_from_wizard(delete_db=self.delete_database, delete_pv=self.delete_pv,
|
||||
delete_svc=self.delete_svc, delete_ing=self.delete_ing,
|
||||
delete_deployment=self.delete_deployment)
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
40
kk_odoo_saas/wizards/saas_app_delete.xml
Executable file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_saas_app_delete" model="ir.ui.view">
|
||||
<field name="name">Delete SaaS Instance</field>
|
||||
<field name="model">kk_odoo_saas.app.delete.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Delete SaaS Instance">
|
||||
<group string="Please choose options you want to delete.">
|
||||
<!-- <field name="job_ids" nolabel="1" />-->
|
||||
<field name="delete_database"/>
|
||||
<field name="delete_pv"/>
|
||||
<field name="delete_svc"/>
|
||||
<field name="delete_ing"/>
|
||||
<field name="delete_deployment"/>
|
||||
<field name="app_id" invisible="1"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
name="delete_saas_instance"
|
||||
string="Delete Instance"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_saas_app_delete_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Delete SaaS Instance</field>
|
||||
<field name="res_model">kk_odoo_saas.app.delete.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_saas_app_delete" />
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="kk_odoo_saas.model_kk_odoo_saas_app" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
99
kk_odoo_saas/wizards/update_docker_image.py
Executable file
@@ -0,0 +1,99 @@
|
||||
from odoo import fields, models, api
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class ContainerArgument(models.TransientModel):
|
||||
_name = 'saas.app.container.argument'
|
||||
_description = "Show the Container Arguments while Updating Docker Image"
|
||||
name = fields.Char("Argument Name")
|
||||
value = fields.Char("Argument Value")
|
||||
update_wizard_id = fields.Many2one('kk_odoo_saas.app.update.dkr.img.wizard')
|
||||
|
||||
|
||||
class ContainerEnvVar(models.TransientModel):
|
||||
_name = 'saas.app.container.env.var'
|
||||
_description = "Show Container's Env Vars while Updating Docker Image"
|
||||
name = fields.Char("Variable Name")
|
||||
value = fields.Char("Variable Value")
|
||||
value_from = fields.Char("Value From")
|
||||
update_wizard_id = fields.Many2one('kk_odoo_saas.app.update.dkr.img.wizard')
|
||||
|
||||
|
||||
class SaaSAppUpdateDockerImage(models.TransientModel):
|
||||
_name = "kk_odoo_saas.app.update.dkr.img.wizard"
|
||||
_description = "Wizard to Update Docker Image of a SaaS App/Instance"
|
||||
|
||||
def _default_app_id(self):
|
||||
res = False
|
||||
context = self.env.context
|
||||
if context.get("active_model") == "kk_odoo_saas.app" and context.get("active_id"):
|
||||
res = context["active_id"]
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def default_get(self, default_fields):
|
||||
deployment_yaml = False
|
||||
container_arguments = False
|
||||
context = self.env.context
|
||||
evs = []
|
||||
|
||||
if context.get("active_model") == "kk_odoo_saas.app" and context.get("active_id"):
|
||||
app_id = context["active_id"]
|
||||
if app_id:
|
||||
app_obj = self.env['kk_odoo_saas.app'].browse(app_id)
|
||||
if app_obj:
|
||||
deployment = app_obj.get_odoo_deployment()
|
||||
if deployment:
|
||||
deployment_yaml = str(deployment)
|
||||
container_arguments = deployment.spec.template.spec.containers[0].args
|
||||
container_env_vars = deployment.spec.template.spec.containers[0].env
|
||||
for cev in container_env_vars:
|
||||
evs.append({'name': cev.name, 'value': cev.value, 'value_from': False})
|
||||
|
||||
cas = ['--database=pos', '--without-demo=True']
|
||||
container_argument_ids = []
|
||||
container_env_var_ids = []
|
||||
|
||||
for i in range(len(cas)):
|
||||
key, val = cas[i].split('=')
|
||||
if key == '--database':
|
||||
if val == app_obj.client_db_name:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
# logic when database name is conflict
|
||||
else:
|
||||
container_argument_ids.append((0, 0, {'name': key, 'value': val}))
|
||||
|
||||
for i in range(len(evs)):
|
||||
container_env_var_ids.append((0, 0, evs[i]))
|
||||
|
||||
contextual_self = self.with_context(default_deployment_yaml=deployment_yaml,
|
||||
default_container_arguments=container_arguments or '[]',
|
||||
default_container_argument_ids=container_argument_ids,
|
||||
default_container_env_var_ids=container_env_var_ids,
|
||||
)
|
||||
return super(SaaSAppUpdateDockerImage, contextual_self).default_get(default_fields)
|
||||
|
||||
app_id = fields.Many2one(
|
||||
comodel_name="kk_odoo_saas.app", string="SaaS App", default=lambda r: r._default_app_id()
|
||||
)
|
||||
deployment_yaml = fields.Text('Yaml of kubernetes Deployment')
|
||||
container_arguments = fields.Char()
|
||||
|
||||
container_argument_ids = fields.One2many('saas.app.container.argument', 'update_wizard_id')
|
||||
container_env_var_ids = fields.One2many('saas.app.container.env.var', 'update_wizard_id')
|
||||
|
||||
# container_db = fields.Char('Database name from Container')
|
||||
# app_db = fields.Char('Database name from App')
|
||||
is_cft_db = fields.Char("Is Database name Conflicting")
|
||||
|
||||
def update_docker_image(self):
|
||||
if self.app_id:
|
||||
envs = []
|
||||
for env_var in self.container_env_var_ids:
|
||||
envs.append({'name': env_var.name, 'value': env_var.value})
|
||||
_logger.info(envs)
|
||||
|
||||
self.app_id.update_docker_image(container_arguments=self.container_arguments, env_vars=envs)
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
73
kk_odoo_saas/wizards/update_docker_image.xml
Executable file
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_saas_app_update_dkr_img" model="ir.ui.view">
|
||||
<field name="name">Update Docker Image of Instance</field>
|
||||
<field name="model">kk_odoo_saas.app.update.dkr.img.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Update Docker Image of Instance">
|
||||
<group string="Please choose settings.">
|
||||
<field name="container_arguments"/>
|
||||
<field name="container_argument_ids">
|
||||
<tree string="Options" editable="bottom">
|
||||
<field name="name" required="1"/>
|
||||
<field name="value" required="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
<field name="container_env_var_ids">
|
||||
<tree string="Options" editable="bottom">
|
||||
<field name="name" required="1"/>
|
||||
<field name="value" required="1"/>
|
||||
</tree>
|
||||
|
||||
</field>
|
||||
</group>
|
||||
<group>
|
||||
<div class="accordion md-accordion" id="accordionEx" role="tablist" aria-multiselectable="true">
|
||||
<!-- Accordion card -->
|
||||
<div class="card">
|
||||
<!-- Card header -->
|
||||
<div class="card-header" role="tab" id="headingOne1">
|
||||
<a data-toggle="collapse" data-parent="#accordionEx" href="#collapseOne1"
|
||||
aria-expanded="true"
|
||||
aria-controls="collapseOne1">
|
||||
<h5 class="mb-0">
|
||||
<i class="fa fa-arrow-right"/>
|
||||
Click To See Yaml of Deployment (Advanced)
|
||||
</h5>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Card body -->
|
||||
<div id="collapseOne1" class="collapse " role="tabpanel" aria-labelledby="headingOne1"
|
||||
data-parent="#accordionEx">
|
||||
<field name="deployment_yaml" widget="ace" options="{'mode': 'yaml'}" nolabel="1"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
name="update_docker_image"
|
||||
string="Update Docker Image"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
<button string="Cancel" class="oe_link" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_saas_app_update_dkr_img_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Update Docker Image of Instance</field>
|
||||
<field name="res_model">kk_odoo_saas.app.update.dkr.img.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_saas_app_update_dkr_img"/>
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="kk_odoo_saas.model_kk_odoo_saas_app"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||