From 327177a7e53a0cb7f0ea11e4145cafd97e8f3e5d Mon Sep 17 00:00:00 2001 From: FLYBYME Date: Sun, 5 Nov 2023 18:24:34 -0500 Subject: [PATCH] build --- services/cluster.service.js | 94 +++++-- services/nodes.service.js | 534 ++++++++++++++++++++++++++++++++++++ 2 files changed, 605 insertions(+), 23 deletions(-) create mode 100644 services/nodes.service.js diff --git a/services/cluster.service.js b/services/cluster.service.js index 83393a8..f6af165 100644 --- a/services/cluster.service.js +++ b/services/cluster.service.js @@ -63,12 +63,6 @@ module.exports = { name: { type: "string", required: true, - unique: true, - min: 3, - max: 255, - trim: true, - lowercase: true, - index: true, }, // the description of the cluster @@ -83,32 +77,90 @@ module.exports = { endpoint: { type: "string", required: true, - unique: true, - min: 3, - max: 255, - trim: true, - lowercase: true, - index: true, }, - // cluster feature set - features: { + // cluster routers + routers: { type: "array", required: false, default: [], items: { - type: "string", - enum: [ - - ] + type: "object", + props: { + // the name of the router + name: { + type: "string", + required: true, + }, + // the description of the router + description: { + type: "string", + required: false, + max: 255, + trim: true, + }, + // the router ipv4 address + ipv4: { + type: "string", + required: true, + }, + // the router ipv6 address + ipv6: { + type: "string", + required: false, + }, + // the zone of the router (e.g. ca, usa etc) + zone: { + type: "string", + required: false, + }, + } } }, + // cluster storage devices + storage: { + type: "array", + required: false, + default: [], + items: "string", + populate: { + action: "v1.storage.nfs.get", + } + }, + + // cluster monitoring endpoint + monitoring: { + type: "string", + required: false, + }, + + // cluster logging endpoint + logging: { + type: "string", + required: false, + }, + + // cluster feature sets + features: { + type: "array", + required: false, + default: [], + items: "string", + enum:[ + "k8s", + "router", + "storage", + "monitoring", + "logging", + "registry", + "dns", + ] + }, ...DbService.FIELDS,// inject dbservice fields - //...Membership.FIELDS,// inject membership fields }, // default database populates @@ -117,13 +169,11 @@ module.exports = { // database scopes scopes: { ...DbService.SCOPE,// inject dbservice scope - //...Membership.SCOPE,// inject membership scope }, // default database scope defaultScopes: [ ...DbService.DSCOPE,// inject dbservice dscope - //...Membership.DSCOPE,// inject membership dscope ], // default init config settings @@ -136,8 +186,6 @@ module.exports = { * service actions */ actions: { - //...Membership.ACTIONS,// inject membership actions - }, diff --git a/services/nodes.service.js b/services/nodes.service.js new file mode 100644 index 0000000..c37712e --- /dev/null +++ b/services/nodes.service.js @@ -0,0 +1,534 @@ + +// External Modules +const Membership = require("membership-mixin"); +const DbService = require("db-mixin"); +const ConfigLoader = require("config-mixin"); +const { Context } = require("moleculer"); +const { MoleculerClientError } = require("moleculer").Errors; + +/** + * k8s cluster node service manages the nodes in a cluster. + */ + +module.exports = { + // name of service + name: "k8s.nodes", + // version of service + version: 1, + + /** + * Service Mixins + * + * @type {Array} + * @property {DbService} DbService - Database mixin + * @property {Membership} Membership - Membership mixin + * @property {ConfigLoader} ConfigLoader - Config loader mixin + */ + mixins: [ + DbService({ + permissions: "k8s.nodes" + }), + ConfigLoader(['k8s.**']), + ], + + /** + * Service dependencies + */ + dependencies: [ + "k8s.cluster" + ], + + /** + * Service settings + * + * @type {Object} + */ + settings: { + rest: "v1/k8s/nodes", + + fields: { + cluster: { + type: "string", + required: true, + description: "The cluster id that this node belongs to." + }, + uid: { + type: "string", + required: true, + description: "The unique id of the node." + }, + name: { + type: "string", + required: true, + description: "The name of the node." + }, + online: { + type: "boolean", + description: "True if the node is online.", + default: false + }, + zone: { + type: "string", + description: "The zone of the node.", + default: "default", + required: false, + }, + roles: { + type: "array", + description: "The roles of the node.", + default: [], + required: false, + items: "string" + }, + cordon: { + type: "boolean", + description: "True if the node is cordoned.", + default: false, + required: false, + readonly: true, + }, + addresses: { + type: "array", + description: "The addresses for the node.", + default: [], + required: false, + items: { + type: "object", + properties: { + type: { + type: "string", + description: "The type of address.", + enum: ["InternalIP", "ExternalIP", "Hostname"] + }, + address: { + type: "string", + description: "The address." + } + } + } + }, + status: { + type: "object", + description: "The status of the node.", + readonly: true, + populate: { + action: function (ctx, values, entities, field) { + return Promise.all(entities.map(async (entity) => { + return ctx.call("v1.kube.findOne", { + metadata: { + uid: entity.uid + }, + fields: ["status"] + }); + })); + } + } + }, + + + ...DbService.FIELDS,// inject dbservice fields + }, + + // default database populates + defaultPopulates: [], + + // database scopes + scopes: { + ...DbService.SCOPE,// inject dbservice scope + }, + + // default database scope + defaultScopes: [ + ...DbService.DSCOPE,// inject dbservice dscope + ], + + // default init config settings + config: { + + } + }, + + /** + * service actions + */ + actions: { + /** + * add role to node + * + * @actions + * @param {String} id - The id of the node. + * @param {String} role - The role to add to the node. + * + * @returns {Object} - The updated node. + */ + addRole: { + params: { + id: { + type: "string", + description: "The id of the node." + }, + role: { + type: "string", + description: "The role to add to the node." + } + }, + async handler(ctx) { + // get node + const node = await this.getById(ctx, ctx.params.id); + + // check if node exists + if (!node) { + throw new MoleculerClientError("Node not found.", 404); + } + + // update node + return this.updateEntity(ctx, { + id: node.id, + $addToSet: { + roles: ctx.params.role + } + }, { raw: true }); + } + }, + + /** + * remove role from node + * + * @actions + * @param {String} id - The id of the node. + * @param {String} role - The role to remove from the node. + * + * @returns {Object} - The updated node. + */ + removeRole: { + params: { + id: { + type: "string", + description: "The id of the node." + }, + role: { + type: "string", + description: "The role to remove from the node." + } + }, + async handler(ctx) { + // get node + const node = await this.getById(ctx, ctx.params.id); + + // check if node exists + if (!node) { + throw new MoleculerClientError("Node not found.", 404); + } + + // update node + return this.updateEntity(ctx, { + id: node.id, + $pull: { + roles: ctx.params.role + } + }, { raw: true }); + } + }, + + /** + * cordoned node + * + * @actions + * @param {String} id - The id of the node. + * + * @returns {Object} - The updated node. + */ + cordon: { + params: { + id: { + type: "string", + description: "The id of the node." + } + }, + async handler(ctx) { + // get node + const node = await this.getById(ctx, ctx.params.id); + + // check if node exists + if (!node) { + throw new MoleculerClientError("Node not found.", 404); + } + + // check if node is online + if (!node.online) { + throw new MoleculerClientError("Node is offline.", 409); + } + + // check if node is already cordoned + if (node.cordon) { + throw new MoleculerClientError("Node is already cordoned.", 409); + } + + // get cluster + const cluster = await ctx.call("v1.k8s.cluster.get", { id: node.cluster }); + + // patch node + await ctx.call("v1.kube.patchNode", { + name: node.name, + body: { + spec: { + unschedulable: true + } + }, + cluster: cluster.name + }); + + // update node + return this.updateEntity(ctx, { + id: node.id, + cordon: true + }, { raw: true }); + } + }, + + /** + * uncordoned node + * + * @actions + * @param {String} id - The id of the node. + * + * @returns {Object} - The updated node. + */ + uncordon: { + params: { + id: { + type: "string", + description: "The id of the node." + } + }, + async handler(ctx) { + // get node + const node = await this.getById(ctx, ctx.params.id); + + // check if node exists + if (!node) { + throw new MoleculerClientError("Node not found.", 404); + } + + // check if node is online + if (!node.online) { + throw new MoleculerClientError("Node is offline.", 409); + } + + // check if node is already uncordoned + if (!node.cordon) { + throw new MoleculerClientError("Node is already uncordoned.", 409); + } + + // get cluster + const cluster = await ctx.call("v1.k8s.cluster.get", { id: node.cluster }); + + // patch node + await ctx.call("v1.kube.patchNode", { + name: node.name, + body: { + spec: { + unschedulable: false + } + }, + cluster: cluster.name + }); + + // update node + return this.updateEntity(ctx, { + id: node.id, + cordon: false + }, { raw: true }); + } + } + }, + + /** + * service events + */ + events: { + async "kube.nodes.added"(ctx) { + const node = ctx.params; + // check if node exists + await this.updateNode(ctx, node) + .then(node => { + this.logger.info(`Node ${node.name} added.`); + }) + .catch(err => { + this.logger.error(err); + }); + + }, + async "kube.nodes.modified"(ctx) { + const node = ctx.params; + // check if node exists + await this.updateNode(ctx, node) + .then(node => { + this.logger.info(`Node ${node.name} modified.`); + }) + .catch(err => { + this.logger.error(err); + }); + + }, + async "kube.nodes.deleted"(node) { + const node = ctx.params; + // check if node exists + await this.deleteNode(ctx, node) + .then(node => { + this.logger.info(`Node ${node.name} added.`); + }) + .catch(err => { + this.logger.error(err); + }); + + }, + }, + + /** + * service methods + */ + methods: { + /** + * Get the node by id. + * + * @param {Context} ctx - The context of the request. + * @param {String} id - The id of the node. + * + * @returns {Object} - The node. + */ + async getById(ctx, id) { + return this.resolveEntities(null, { id: id }); + }, + /** + * Get the node by uid. + * + * @param {String} uid - The uid of the node. + * + * @returns {Object} - The node. + */ + async getByUID(uid) { + return this.findEntity(null, { query: { uid: uid } }); + }, + + /** + * Get the node by name. + * + * @param {String} name - The name of the node. + * + * @returns {Object} - The node. + */ + async getByName(name) { + return this.findEntity(null, { query: { name: name } }); + }, + + /** + * Check if the node exists. + * + * @param {String} uid - The uid of the node. + * + * @returns {Boolean} - True if the node exists. + */ + async exists(uid) { + return this.getByUID(uid).then(node => { + return !!node; + }); + }, + + /** + * create new node + * + * @param {Context} ctx - The context of the request.s + * @param {Object} node - The node to create. + * + * @returns {Object} - The created node. + */ + async createNode(ctx, node) { + // check if node exists + const exists = await this.exists(node.metadata.uid); + if (exists) { + throw new MoleculerClientError("Node already exists.", 409); + } + + // lookup cluster + const cluster = await ctx.call("k8s.cluster.lookup", { name: node.cluster }); + + // check if cluster exists + if (!cluster) { + throw new MoleculerClientError("Cluster not found.", 404); + } + + // create node + return this.createEntity(ctx, { + cluster: cluster.id, + uid: node.metadata.uid, + name: node.metadata.name + }); + }, + + /** + * update node + * + * @param {Context} ctx - The context of the request. + * @param {Object} node - The node to update. + * + * @returns {Object} - The updated node. + */ + async updateNode(ctx, node) { + // check if node exists + const exists = await this.exists(node.metadata.uid); + if (!exists) { + await this.createNode(ctx, node); + } + const entity = await this.getByUID(node.metadata.uid); + + // update node + return this.updateEntity(ctx, { + id: entity.id, + addresses: node.status.addresses, + online: node.status.conditions.filter(c => c.type === "Ready").every(c => c.status === "True") + }); + }, + + /** + * delete node + * + * @param {Context} ctx - The context of the request. + * @param {Object} node - The node to delete. + * + * @returns {Object} - The deleted node. + */ + async deleteNode(ctx, node) { + // check if node exists + const exists = await this.exists(node.metadata.uid); + if (!exists) { + throw new MoleculerClientError("Node not found.", 404); + } + + const entity = await this.getByUID(node.metadata.uid); + + // delete node + return this.deleteEntity(ctx, { + id: entity.id, + }); + }, + }, + + created() { }, + + async started() { + return this.broker.call('v1.kube.find', { + kind: "Node", + fields: ["metadata.uid", "metadata.name", "status.addresses", "status.conditions"], + }) + .then(async (nodes) => { + await Promise.all(nodes.map(async (node) => + this.updateNode(this.broker, node) + )); + }); + }, + + async stopped() { }, + +} +