Skip to content

Commit

Permalink
Merge pull request #272 from vshn/change/odoo16_billing
Browse files Browse the repository at this point in the history
Add billing for Odoo 16
  • Loading branch information
Kidswiss authored Dec 14, 2023
2 parents 4a68bf3 + 2ea6ace commit 208081c
Show file tree
Hide file tree
Showing 30 changed files with 1,206 additions and 14 deletions.
1 change: 1 addition & 0 deletions component/class/appcat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ parameters:
- ${_base_directory}/component/statefuleset-resize-controller.jsonnet
- ${_base_directory}/component/functions.jsonnet
- ${_base_directory}/component/vshn_appcat_services.jsonnet
- ${_base_directory}/component/billing.jsonnet
input_type: jsonnet
output_path: appcat/

Expand Down
50 changes: 49 additions & 1 deletion component/class/defaults.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ parameters:
registry: xpkg.upbound.io
repository: crossplane-contrib/function-patch-and-transform
tag: v0.1.4
reporting:
registry: ghcr.io
repository: appuio/appuio-reporting
tag: v0.1.1

=_crd_version: ${appcat:images:appcat:tag}

Expand All @@ -64,9 +68,50 @@ parameters:
crossplaneNamespace: ${crossplane:namespace}

appuioManaged: true
tenantID: ${cluster:tenant}
quotasEnabled: false
grpcEndpoint: host.docker.internal:9443

billing:
namespace: ${appcat:namespace}
tenantID: ${cluster:tenant}
instanceUOM: uom_uom_45_1e112771
enableMockOrgInfo: false
salesOrder: "" # TODO: Update once the fact is available in Leutenant
vshn:
# The reporting for VSHN service should only run on APPUiO Cloud LPG2
enableCronjobs: false
schedule: 30 * * * *
# Deploy metering prometheus rules
meteringRules: false
monitoring:
enabled: true
alerts:
APPUiOReportingDatabaseBackfillingFailed:
enabled: true
rule:
annotations:
description: AppCat Reporting backfilling metrics into Odoo failed
message: AppCat Reporting backfilling metrics into Odoo failed.
runbook_url: TBD
summary: AppCat Reporting backfilling metrics into Odoo failed.
expr: |
kube_job_failed{job="kube-state-metrics",namespace="${appcat:namespace}",job_name=~"backfill-.*"} > 0
for: 10m
labels:
severity: warning
syn_team: schedar
network_policies:
target_namespaces: {}
prometheus:
url: http://vshn-appuio-mimir-query-frontend.vshn-appuio-mimir.svc:8080/prometheus
org_id: appuio-cloud-metering-c-appuio-cloudscale-lpg-2|appuio-cloud-metering-c-appuio-exoscale-ch-gva-2-0
odoo:
url: "https://test.central.vshn.ch/api/v2/product_usage_report_POST"
oauth:
url: "https://test.central.vshn.ch/api/v2/authentication/oauth2/token"
clientID: "?{vaultkv:${cluster:tenant}/${cluster:name}/billing-collector-cloudservices/odoo-oauth-client-id}"
clientSecret: "?{vaultkv:${cluster:tenant}/${cluster:name}/billing-collector-cloudservices/odoo-oauth-client-secret}"

defaultRestoreRoleRules:
- apiGroups:
- vshn.appcat.vshn.io
Expand Down Expand Up @@ -306,6 +351,7 @@ parameters:
stsResizer:
enabled: true
postgres:
billing: true
# bucket_region: 'lpg' || 'ch-gva-2'
bucket_region: ""
# bucket_endpoint: 'https://objects.lpg.cloudscale.ch' || 'https://sos-ch-gva-2.exo.io'
Expand Down Expand Up @@ -438,6 +484,7 @@ parameters:
cpu: "250m"
memory: "256Mi"
redis:
billing: true
enabled: true
enableNetworkPolicy: true
secretNamespace: ${appcat:services:vshn:secretNamespace}
Expand Down Expand Up @@ -480,6 +527,7 @@ parameters:
memory: "8Gi"
disk: 16Gi
minio:
billing: true
enabled: false
enableNetworkPolicy: true
secretNamespace: ${appcat:services:vshn:secretNamespace}
Expand Down
200 changes: 200 additions & 0 deletions component/component/billing.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// main template for appuio-reporting
local alerts = import 'billing_alerts.libsonnet';
local common = import 'billing_cronjob.libsonnet';
local netPol = import 'billing_netpol.libsonnet';
local kap = import 'lib/kapitan.libjsonnet';
local kube = import 'lib/kube.libjsonnet';
local inv = kap.inventory();
// The hiera parameters for the component
local params = inv.parameters.appcat;

local formatImage = function(ref) '%(registry)s/%(repository)s:%(tag)s' % ref;

// escape any non-valid characters and replace them with -
local escape = function(str)
std.join('',
std.map(
function(c)
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) then c else '-'
, str
));

local odooSecret = kube.Secret('odoo-credentials') {
metadata+: {
namespace: params.billing.namespace,
labels+: common.Labels,
},
stringData: {
client_id: params.billing.odoo.oauth.clientID,
client_secret: params.billing.odoo.oauth.clientSecret,
token_endpoint: params.billing.odoo.oauth.url,
},
};

local commonEnv = std.prune([
{
name: 'AR_ODOO_OAUTH_TOKEN_URL',
valueFrom: {
secretKeyRef: {
name: odooSecret.metadata.name,
key: 'token_endpoint',
},
},
},
{
name: 'AR_ODOO_OAUTH_CLIENT_ID',
valueFrom: {
secretKeyRef: {
name: odooSecret.metadata.name,
key: 'client_id',
},
},
},
{
name: 'AR_ODOO_OAUTH_CLIENT_SECRET',
valueFrom: {
secretKeyRef: {
name: odooSecret.metadata.name,
key: 'client_secret',
},
},
},
{
name: 'AR_ODOO_URL',
value: params.billing.odoo.url,
},
{
name: 'AR_PROM_URL',
value: params.billing.prometheus.url,
},
if params.billing.prometheus.org_id != null then {
name: 'AR_ORG_ID',
value: params.billing.prometheus.org_id,
},
]);

local backfillCJ = function(name, query, sla, type)

local nameSLA = name + ' by VSHN ' + sla;

local itemDescJsonnet = 'local labels = std.extVar("labels"); "%s" %% labels' % nameSLA;

local clusterID = if params.billing.enableMockOrgInfo then 'kind' else '%(cluster_id)s';

local itemGroupDesc = nameSLA + ' - Zone: ' + clusterID + ' / Namespace: %(label_appcat_vshn_io_claim_namespace)s';

local itemGroupDescJsonnet = 'local labels = std.extVar("labels"); "%s" %% labels' % itemGroupDesc;

local instanceJsonnet = 'local labels = std.extVar("labels"); "%s" %% labels' % '%(label_appcat_vshn_io_claim_namespace)s/%(label_appcat_vshn_io_claim_name)s';

local productID = 'appcat-vshn-%(name)s-%(sla)s' % { name: name, sla: sla };

local jobEnv = std.prune([
{
name: 'AR_PRODUCT_ID',
value: productID,
},
{
name: 'AR_QUERY',
value: query,
},
{
name: 'AR_INSTANCE_JSONNET',
value: instanceJsonnet,
},
if itemGroupDescJsonnet != null then {
name: 'AR_ITEM_GROUP_DESCRIPTION_JSONNET',
value: itemGroupDescJsonnet,
},
if itemDescJsonnet != null then {
name: 'AR_ITEM_DESCRIPTION_JSONNET',
value: itemDescJsonnet,
},
{
name: 'AR_UNIT_ID',
value: params.billing.instanceUOM,
},
]);
common.CronJob('%(product)s-%(type)s' % { product: escape(productID), type: type }, 'backfill', {
containers: [
{
name: 'backfill',
image: formatImage(params.images.reporting),
env+: commonEnv + jobEnv,
command: [ 'sh', '-c' ],
args: [
'appuio-reporting report --timerange 1h --begin=$(date -d "now -3 hours" -u +"%Y-%m-%dT%H:00:00Z") --repeat-until=$(date -u -Iseconds)',
],
resources: {},
},
],
}) {
metadata+: {
annotations+: {
'product-id': productID,
},
},
spec+: {
jobTemplate+: {
metadata+: {
annotations+: {
'product-id': productID,
},
},
},
failedJobsHistoryLimit: 10,
},
};

local generateCloudAndManaged = function(name)

// For postgresql we have a missmatch between the label and the name in our definition.
local queryName = if name == 'postgres' then name + 'ql' else name;

local managedQuery = 'appcat:metering{label_appuio_io_billing_name="appcat-' + queryName + '",label_appcat_vshn_io_sla="%s"}';
local cloudQuery = managedQuery + ' * on(label_appuio_io_organization) group_left(sales_order) label_replace(appuio_control_organization_info, "label_appuio_io_organization", "$1", "organization", "(.*)")';

local permutations = [
{
query: cloudQuery % 'besteffort',
sla: 'besteffort',
type: 'cloud',
},
{
query: cloudQuery % 'guaranteed',
sla: 'guaranteed',
type: 'cloud',
},
// Currently appcat on appuio managed isn't billed, so we don't need the permutations
// {
// query: managedQuery % 'besteffort',
// sla: 'besteffort',
// type: 'managed',
// },
// {
// query: managedQuery % 'guaranteed',
// sla: 'guaranteed',
// type: 'managed',
// },
];

std.flatMap(function(r) [ backfillCJ(name, r.query, r.sla, r.type) ], permutations);

local keysAndValues(obj) = std.map(function(x) { name: x, value: obj[x] }, std.objectFields(obj));
local vshnServices = std.filter(function(r) std.type(r.value) == 'object' && std.objectHas(r.value, 'billing') && r.value.billing, keysAndValues(params.services.vshn));
local billingCronjobs = std.flattenArrays(std.flatMap(function(r) [ generateCloudAndManaged(r.name) ], vshnServices));

if params.billing.vshn.enableCronjobs then
{
'billing/00_namespace': kube.Namespace(params.billing.namespace) {
metadata+: {
labels+: common.Labels {
'openshift.io/cluster-monitoring': 'true',
},
},
},
[if std.length(params.billing.network_policies.target_namespaces) != 0 then 'billing/01_netpol']: netPol.Policies,
'billing/10_odoo_secret': odooSecret,
'billing/11_backfill': billingCronjobs,
[if params.billing.monitoring.enabled then 'billing/50_alerts']: alerts.Alerts,
} else {}
42 changes: 42 additions & 0 deletions component/component/billing_alerts.libsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
local common = import 'billing_cronjob.libsonnet';
local com = import 'lib/commodore.libjsonnet';
local kap = import 'lib/kapitan.libjsonnet';
local kube = import 'lib/kube.libjsonnet';

local inv = kap.inventory();
local params = inv.parameters.appcat.billing;

local alertlabels = {
syn: 'true',
syn_component: 'appuio-reporting',
};

local alertParams = params.monitoring.alerts;

local alerts =
kube._Object('monitoring.coreos.com/v1', 'PrometheusRule', 'appuio-reporting') {
metadata+: {
namespace: params.namespace,
labels+: common.Labels,
},
spec+: {
groups+: [
{
name: 'appuio-reporting.alerts',
rules:
std.filterMap(
function(field) alertParams[field].enabled == true,
function(field) alertParams[field].rule {
alert: field,
labels+: alertlabels,
},
std.sort(std.objectFields(alertParams))
),
},
],
},
};

{
Alerts: alerts,
}
Loading

0 comments on commit 208081c

Please sign in to comment.