diff --git a/lib/kubevela.js b/lib/kubevela.js index f21a8fe..cba738b 100644 --- a/lib/kubevela.js +++ b/lib/kubevela.js @@ -1,14 +1,16 @@ const slugify = require("slugify"); const mathutils = require("./math"); const _ = require("lodash"); +const transliterate = require('transliteration').transliterate; module.exports = { json: (doc) =>{ let object = _.clone(doc) + object['title'] = transliterate(object['title']) object['variables'] = _.map(doc['variables'], (v)=>{ return { - 'key': slugify(v['name'].replaceAll('/','_'),'_'), + 'key': slugify(transliterate(v['name']).replaceAll("\s+"," ").replaceAll('/','_'),'_'), 'path': '/'+v['name'], 'type': 'float', 'meaning': v['name'].split('/').pop(), diff --git a/lib/metric_model.js b/lib/metric_model.js index 7e42d5e..3aa69de 100644 --- a/lib/metric_model.js +++ b/lib/metric_model.js @@ -1,10 +1,12 @@ const slugify = require("slugify"); const _ = require("lodash"); const yaml = require('yaml'); +const {transliterate} = require("transliteration"); module.exports = { yaml: (doc) => { let object = _.cloneDeep(doc); + object['title'] = transliterate(object['title']) let componentsForAppSpecComp = []; let componentsForAppWideScope = []; diff --git a/modules/application/index.js b/modules/application/index.js index 7a8039c..c598ff3 100644 --- a/modules/application/index.js +++ b/modules/application/index.js @@ -11,6 +11,7 @@ const OpenAI = require("openai"); const projection = { title: 1, uuid: 1, + token: 1, status: 1, organization: 1, content: 1, @@ -40,6 +41,10 @@ module.exports = { type: 'string', label: 'UUID' }, + token:{ + type: 'string', + label: 'Token' + }, status: { type: 'string', label: 'Status', @@ -516,6 +521,9 @@ module.exports = { }; }, methods(self) { + validateMetaConstraints = (uuid,doc) => { + return self.apos.modules.exn.bqa_application_validate(uuid,doc) + }; const contentSchema = Joi.string().custom((value, helpers) => { try { yaml.parse(value); @@ -850,7 +858,9 @@ module.exports = { async updateWithRegions(req, doc) { return new Promise(async (resolve) => { - + if(!doc.resources){ + doc.resources = [] + } const resource_uuids = doc.resources.map(r => { return r.uuid }) @@ -945,24 +955,22 @@ module.exports = { const doc = req.body; let errorResponses = self.validateDocument(doc) || []; + + if(doc.uuid){ + const metaConstraintValidation = await validateMetaConstraints(doc.uuid, doc) + if(!metaConstraintValidation.valid){ + errorResponses.push({ + path: `slMetaConstraint`, + index: 90, + key: `slMetaConstraint`, + message: metaConstraintValidation.message || 'Please check the SL Meta Constraints' + }) + } + } if (errorResponses.length > 0) { throw self.apos.error('required', 'Validation failed', {error: errorResponses}); } }, - async validateConstraints(req) { - if (!self.apos.permission.can(req, 'edit')) { - throw self.apos.error('forbidden', 'Insufficient permissions'); - } - const slMetaContraints = req.body; - const valid = await new Promise((resolve) => { - setTimeout(() => { - const randomBoolean = Math.random() < 0.5; - resolve(randomBoolean); - }, 5000); - }); - console.log("Returning valid for ", slMetaContraints, valid); - return valid; - }, async 'generate'(req) { if (!self.apos.permission.can(req, 'edit')) { throw self.apos.error('forbidden', 'Insufficient permissions'); @@ -1041,6 +1049,28 @@ module.exports = { throw self.apos.error(error.name, error.message); } }, + async ':uuid/uuid/debug'(req) { + + const uuid = req.params.uuid; + + // let errorResponses = self.validateDocument(updateData, true) || []; + // if (errorResponses.length > 0) { + // throw self.apos.error('required', 'Validation failed', { error: errorResponses }); + // } + const currentUser = req.user; + const adminOrganization = currentUser.organization; + + const existingApp = await self.apos.doc.db.findOne({uuid: uuid, organization: adminOrganization}); + if (!existingApp) { + throw self.apos.error('notfound', 'Application not found'); + } + + try { + await self.apos.modules.exn.send_application_dsl(uuid) + } catch (error) { + throw self.apos.error(error.name, error.message); + } + }, async ':uuid/uuid/undeploy'(req) { const uuid = req.params.uuid; const currentUser = req.user; @@ -1128,6 +1158,38 @@ module.exports = { } catch (error) { throw self.apos.error('error', error.message); } + }, + async ':uuid/vr/token'(req) { + + const uuid = req.params.uuid; + + if (!self.apos.permission.can(req, 'view')) { + throw self.apos.error('forbidden', 'Insufficient permissions'); + } + + try { + const currentUser = req.user; + const adminOrganization = currentUser.organization; + + const doc = await self.find(req, { + uuid: uuid, + organization: adminOrganization + }).toObject(); + if (!doc) { + throw self.apos.error('notfound', 'Application not found'); + } + + if (doc.organization !== adminOrganization) { + throw self.apos.error('forbidden', 'Access denied'); + } + + const token = uuidv4(); + doc.token = token; + await self.update(req,doc) + return token; + } catch (error) { + throw self.apos.error(error.name, error.message); + } } }, @@ -1303,8 +1365,50 @@ module.exports = { } catch (error) { throw self.apos.error('error', error.message); } + }, + async 'vr/data/:token'(req) { + + const token = req.params.token; + + const doc = await self.find(req, { + token: token, + }).project(projection).toObject(); + + if (!doc) { + throw self.apos.error('notfound', 'Application not found'); + } + + try { + const measurements = req.query.measurement || [] + const interval = req.query.interval || '-30d' + const range = req.query.range || 10 + const slice = req.query.slice || 5 + + const res = await self.apos.modules.influxdb.getTimeSeriesForMeasurements(doc.uuid, measurements, interval) + return { + application: doc.title, + uuid: doc.uuid, + charts: res.map(chart => { + const slicedValues = chart.config.datasets[0].data.slice(0, slice); + const maxValue = Math.max(...slicedValues); + + return { + title: chart.title, + points: chart.config.labels.slice(0, slice).map((label, index) => ({ + title: label, + value: maxValue > 0 ? (chart.config.datasets[0].data[index] / maxValue) * range : 0, + raw: chart.config.datasets[0].data[index] + })) + + } + }) + }; + } catch (error) { + throw self.apos.error('error', error.message); + } } + }, delete: { async ':uuid/uuid'(req) { diff --git a/modules/exn/index.js b/modules/exn/index.js index 449f230..3edfedb 100644 --- a/modules/exn/index.js +++ b/modules/exn/index.js @@ -44,6 +44,8 @@ let sender_ui_application_dsl_metric; let sender_ui_policies_rule_upsert; let sender_ui_policies_model_upsert; +let sender_bqa_validate_slos; + let sender_ui_application_user_info; let sender_ui_application_info; @@ -108,6 +110,11 @@ module.exports = { await aposSelf.update_application_state(context.message.application_properties.application, context.message.body); } + if (context.message.to === "topic://eu.nebulouscloud.ontology.bqa.reply") { + correlations[context.message.correlation_id]['resolve'](context.message.body) + return + } + if (context.message.correlation_id in correlations) { if (context.message.body.metaData['status'] >= 400) { correlations[context.message.correlation_id]['reject'](context.message.body['message']) @@ -127,6 +134,7 @@ module.exports = { context.connection.open_receiver('topic://eu.nebulouscloud.optimiser.controller.app_state') context.connection.open_receiver('topic://eu.nebulouscloud.ui.user.get') context.connection.open_receiver('topic://eu.nebulouscloud.ui.app.get') + context.connection.open_receiver('topic://eu.nebulouscloud.ontology.bqa.reply') sender_sal_nodecandidate_get = context.connection.open_sender('topic://eu.nebulouscloud.exn.sal.nodecandidate.get'); sender_sal_cloud_get = context.connection.open_sender('topic://eu.nebulouscloud.exn.sal.cloud.get'); @@ -147,6 +155,8 @@ module.exports = { sender_ui_application_user_info = context.connection.open_sender('topic://eu.nebulouscloud.ui.user.get.reply'); sender_ui_application_info = context.connection.open_sender('topic://eu.nebulouscloud.ui.app.get.reply'); + sender_bqa_validate_slos = context.connection.open_sender('topic://eu.nebulouscloud.ontology.bqa'); + }); if (process.env.EXN_DISABLE == "True") { @@ -350,6 +360,33 @@ module.exports = { }) } , + async bqa_application_validate(uuid) { + return new Promise(async (resolve, reject) => { + + const correlation_id = uuidv4() + correlations[correlation_id] = { + 'resolve': resolve, 'reject': reject, + }; + const req = aposSelf.apos.task.getReq() + const dsl = await aposSelf.apos.modules.application.getDSL(req, uuid) + const message = { + to: sender_bqa_validate_slos.options.target.address, + correlation_id: correlation_id, + message_annotations: {application: uuid}, + application_properties: {application: uuid}, + body: dsl.json + } + const timer = setTimeout(() => { + console.warn("SLO Validator timeout") + resolve({ + 'valid':true + }) + }, 7000); + + console.log("[bqa_application_validate] Send ", JSON.stringify( message)) + sender_bqa_validate_slos.send(message) + }) + }, get_cloud_candidates() { return new Promise((resolve, reject) => { diff --git a/modules/influxdb/index.js b/modules/influxdb/index.js index 4e592a9..29de653 100644 --- a/modules/influxdb/index.js +++ b/modules/influxdb/index.js @@ -49,101 +49,108 @@ module.exports = { }) }, getTimeSeriesForMeasurements(uuid, measurements = [], time = '-3h') { - const influxDB = new InfluxDB({ - url: connection_options.url, - token: connection_options.token, - }); - - const queryApi = influxDB.getQueryApi(connection_options.organization); - return new Promise((resolve, reject) => { - const timeSeriesData = []; - // Build the measurement filter - let measurementFilter = ''; - if (measurements.length > 0) { - const measurementList = measurements.map(m => `r._measurement == "${m}"`).join(' or '); - measurementFilter = `|> filter(fn: (r) => ${measurementList})`; - } - // Query to get time series data - const fluxQuery = ` - from(bucket: "nebulous_${uuid}_bucket") - |> range(start: ${time}) - ${measurementFilter} - |> filter(fn: (r) => r._field == "metricValue") - |> sort(columns: ["_time"]) - `; - - queryApi.queryRows(fluxQuery, { - next(row, tableMeta) { - const o = tableMeta.toObject(row); - timeSeriesData.push({ - time: o._time, - measurement: o._measurement, - value: o._value, - }); - }, - error(error) { - console.error('Query error:', error); - reject(error); - }, - complete() { - // Group data by measurement - const groupedData = timeSeriesData.reduce((acc, item) => { - if (!acc[item.measurement]) { - acc[item.measurement] = []; - } - acc[item.measurement].push(item); - return acc; - }, {}); - - // Transform into metrics format - const metrics = Object.entries(groupedData).map(([measurement, data]) => { - // Sort by time - const sortedData = data.sort((a, b) => - new Date(a.time).getTime() - new Date(b.time).getTime() - ); - - // Extract values and create labels - const values = sortedData.map(d => d.value); - const labels = sortedData.map(d => - new Date(d.time).toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit' - }) - ); - - // Calculate statistics - const latestValue = values[values.length - 1] || 0; - const avgValue = values.reduce((a, b) => a + b, 0) / values.length; - - // Determine chart type based on measurement name - let type = 'line'; - if (measurement.includes('constraint') || measurement.includes('slo')) { - type = 'bar'; - } - - return { - title: measurement.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), - summary: `${latestValue.toFixed(2)} (avg: ${avgValue.toFixed(2)})`, - type: type, - config: { - labels: labels, - datasets: [{ - label: measurement, - data: values - }] - }, - data: { - value: latestValue + try { + const influxDB = new InfluxDB({ + url: connection_options.url, + token: connection_options.token, + }); + + const queryApi = influxDB.getQueryApi(connection_options.organization); + + const timeSeriesData = []; + + // Build the measurement filter + let measurementFilter = ''; + if (measurements.length > 0) { + const measurementList = measurements.map(m => `r._measurement == "${m}"`).join(' or '); + measurementFilter = `|> filter(fn: (r) => ${measurementList})`; + } + + // Query to get time series data + const fluxQuery = ` + from(bucket: "nebulous_${uuid}_bucket") + |> range(start: ${time}) + ${measurementFilter} + |> filter(fn: (r) => r._field == "metricValue") + |> sort(columns: ["_time"]) + `; + + queryApi.queryRows(fluxQuery, { + next(row, tableMeta) { + const o = tableMeta.toObject(row); + timeSeriesData.push({ + time: o._time, + measurement: o._measurement, + value: o._value, + }); + }, + error(error) { + console.error('Query error:', error); + resolve([]); + }, + complete() { + // Group data by measurement + const groupedData = timeSeriesData.reduce((acc, item) => { + if (!acc[item.measurement]) { + acc[item.measurement] = []; + } + acc[item.measurement].push(item); + return acc; + }, {}); + + // Transform into metrics format + const metrics = Object.entries(groupedData).map(([measurement, data]) => { + // Sort by time + const sortedData = data.sort((a, b) => + new Date(a.time).getTime() - new Date(b.time).getTime() + ); + + // Extract values and create labels + const values = sortedData.map(d => d.value); + const labels = sortedData.map(d => + new Date(d.time).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit' + }) + ); + + // Calculate statistics + const latestValue = values[values.length - 1] || 0; + const avgValue = values.reduce((a, b) => a + b, 0) / values.length; + + // Determine chart type based on measurement name + let type = 'line'; + if (measurement.includes('constraint') || measurement.includes('slo')) { + type = 'bar'; } - }; - }); - resolve(metrics); - }, - }); + return { + title: measurement.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), + summary: `${latestValue.toFixed(2)} (avg: ${avgValue.toFixed(2)})`, + type: type, + config: { + labels: labels, + datasets: [{ + label: measurement, + data: values + }] + }, + data: { + value: latestValue + } + }; + }); + + resolve(metrics); + }, + }); + } catch (e) { + console.error('[InfluxDB] Query error:', e); + resolve([]); + } }); } } diff --git a/modules/resources/index.js b/modules/resources/index.js index b98ab9b..5d277a2 100644 --- a/modules/resources/index.js +++ b/modules/resources/index.js @@ -48,7 +48,8 @@ const credentialsSchema = Joi.object({ module.exports = { extend: '@apostrophecms/piece-type', options: { - label: 'Resource' + label: 'Resource', + autopublish: true }, fields: { add: { diff --git a/package.json b/package.json index 7accc22..ce9d17b 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "slugify": "^1.6.6", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", + "transliteration": "^2.3.5", "uuid": "^9.0.1", "yaml": "^2.3.4" },