@flowfuse/driver-kubernetes
Advanced tools
Comparing version 2.4.1-d12c406-202406041336.0 to 2.5.0
@@ -0,1 +1,8 @@ | ||
#### 2.5.0: Release | ||
- Custom Hostname support (#151) @hardillb | ||
- Bump actions/setup-node from 1 to 4 (#159) @app/dependabot | ||
- Bump actions/checkout from 1 to 4 (#158) @app/dependabot | ||
- Enable dependabot for github actions (#154) @ppawlowski | ||
#### 2.4.0: Release | ||
@@ -2,0 +9,0 @@ |
@@ -134,2 +134,32 @@ const got = require('got') | ||
const customIngressTemplate = { | ||
apiVersion: 'networking.k8s.io/v1', | ||
kind: 'Ingress', | ||
metadata: { | ||
annotations: {} | ||
}, | ||
spec: { | ||
rules: [ | ||
{ | ||
http: { | ||
paths: [ | ||
{ | ||
pathType: 'Prefix', | ||
path: '/', | ||
backend: { | ||
service: { | ||
port: { number: 1880 } | ||
} | ||
} | ||
} | ||
] | ||
} | ||
} | ||
], | ||
tls: [ | ||
] | ||
} | ||
} | ||
const createDeployment = async (project, options) => { | ||
@@ -344,2 +374,46 @@ const stack = project.ProjectStack.properties | ||
const createCustomIngress = async (project, hostname, options) => { | ||
const prefix = project.safeName.match(/^[0-9]/) ? 'srv-' : '' | ||
const url = new URL(project.url) | ||
url.host = hostname | ||
// exposedData available for annotation replacements | ||
const exposedData = { | ||
serviceName: `${prefix}${project.safeName}`, | ||
instanceURL: url.href, | ||
instanceHost: url.host, | ||
instanceProtocol: url.protocol | ||
} | ||
this._app.log.info('K8S DRIVER: start custom hostname ingress template') | ||
const customIngress = JSON.parse(JSON.stringify(customIngressTemplate)) | ||
customIngress.metadata.name = `${project.safeName}-custom` | ||
customIngress.spec.rules[0].host = hostname | ||
customIngress.spec.rules[0].http.paths[0].backend.service.name = `${prefix}${project.safeName}` | ||
if (this._customHostname?.certManagerIssuer) { | ||
customIngress.metadata.annotations['cert-manager.io/cluster-issuer'] = this._customHostname.certManagerIssuer | ||
customIngress.spec.tls = [ | ||
{ | ||
hosts: [ | ||
hostname | ||
], | ||
secretName: `${project.safeName}-custom` | ||
} | ||
] | ||
} | ||
// process annotations with potential replacements | ||
Object.keys(customIngress.metadata.annotations).forEach((key) => { | ||
customIngress.metadata.annotations[key] = mustache(customIngress.metadata.annotations[key], exposedData) | ||
}) | ||
if (this._customHostname?.ingressClass) { | ||
customIngress.spec.ingressClassName = `${this._customHostname.ingressClass}` | ||
} | ||
return customIngress | ||
} | ||
const createProject = async (project, options) => { | ||
@@ -357,3 +431,3 @@ const namespace = this._app.config.driver.options.projectNamespace || 'flowforge' | ||
// If deployment exists, perform an upgrade | ||
this._app.log.warn(`[k8s] Deployment for project ${project.id} already exists. Upgrading deployment`) | ||
this._app.log.warn(`[k8s] Deployment for instance ${project.id} already exists. Upgrading deployment`) | ||
const result = await this._k8sAppApi.readNamespacedDeployment(project.safeName, namespace) | ||
@@ -369,3 +443,3 @@ | ||
// Log other errors and rethrow them for additional higher-level handling | ||
this._app.log.error(`[k8s] Unexpected error creating deployment for project ${project.id}.`) | ||
this._app.log.error(`[k8s] Unexpected error creating deployment for instance ${project.id}.`) | ||
this._app.log.error(`[k8s] deployment ${JSON.stringify(localDeployment, undefined, 2)}`) | ||
@@ -390,3 +464,3 @@ this._app.log.error(err) | ||
clearInterval(pollInterval) | ||
this._app.log.error(`[k8s] Project ${project.id} - timeout waiting for Deployment`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - timeout waiting for Deployment`) | ||
reject(new Error('Timed out to creating Deployment')) | ||
@@ -402,6 +476,6 @@ } | ||
if (err.statusCode === 409) { | ||
this._app.log.warn(`[k8s] Service for project ${project.id} already exists, proceeding...`) | ||
this._app.log.warn(`[k8s] Service for instance ${project.id} already exists, proceeding...`) | ||
} else { | ||
if (project.state !== 'suspended') { | ||
this._app.log.error(`[k8s] Project ${project.id} - error creating service: ${err.toString()}`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - error creating service: ${err.toString()}`) | ||
throw err | ||
@@ -424,3 +498,3 @@ } | ||
clearInterval(pollInterval) | ||
this._app.log.error(`[k8s] Project ${project.id} - timeout waiting for Service`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - timeout waiting for Service`) | ||
reject(new Error('Timed out to creating Service')) | ||
@@ -436,6 +510,6 @@ } | ||
if (err.statusCode === 409) { | ||
this._app.log.warn(`[k8s] Ingress for project ${project.id} already exists, proceeding...`) | ||
this._app.log.warn(`[k8s] Ingress for instance ${project.id} already exists, proceeding...`) | ||
} else { | ||
if (project.state !== 'suspended') { | ||
this._app.log.error(`[k8s] Project ${project.id} - error creating ingress: ${err.toString()}`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - error creating ingress: ${err.toString()}`) | ||
throw err | ||
@@ -445,2 +519,20 @@ } | ||
} | ||
if (this._customHostname?.enabled) { | ||
const customHostname = await project.getSetting('customHostname') | ||
if (customHostname) { | ||
const customHostnameIngress = await createCustomIngress(project, customHostname, options) | ||
try { | ||
await this._k8sNetApi.createNamespacedIngress(namespace, customHostnameIngress) | ||
} catch (err) { | ||
if (err.statusCode === 409) { | ||
this._app.log.warn(`[k8s] Custom Hostname Ingress for instance ${project.id} already exists, proceeding...`) | ||
} else { | ||
if (project.state !== 'suspended') { | ||
this._app.log.error(`[k8s] Instance ${project.id} - error creating custom hostname ingress: ${err.toString()}`) | ||
throw err | ||
} | ||
} | ||
} | ||
} | ||
} | ||
@@ -458,3 +550,3 @@ await new Promise((resolve, reject) => { | ||
clearInterval(pollInterval) | ||
this._app.log.error(`[k8s] Project ${project.id} - timeout waiting for Ingress`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - timeout waiting for Ingress`) | ||
reject(new Error('Timed out to creating Ingress')) | ||
@@ -492,7 +584,7 @@ } | ||
/** | ||
* Initialises this driver | ||
* @param {string} app - the Vue application | ||
* @param {object} options - A set of configuration options for the driver | ||
* @return {forge.containers.ProjectArguments} | ||
*/ | ||
* Initialises this driver | ||
* @param {string} app - the Vue application | ||
* @param {object} options - A set of configuration options for the driver | ||
* @return {forge.containers.ProjectArguments} | ||
*/ | ||
init: async (app, options) => { | ||
@@ -509,2 +601,6 @@ this._app = app | ||
this._cloudProvider = this._app.config.driver.options?.cloudProvider | ||
if (this._app.config.driver.options?.customHostname?.enabled) { | ||
this._app.log.info('[k8s] Enabling Custom Hostname Support') | ||
this._customHostname = this._app.config.driver.options?.customHostname | ||
} | ||
@@ -585,3 +681,3 @@ const kc = new k8s.KubeConfig() | ||
this._app.log.error(`[k8s] Error while reading namespaced deployment for project '${project.safeName}' ${project.id}. Error msg=${err.message}, stack=${err.stack}`) | ||
this._app.log.info(`[k8s] Project ${project.id} - recreating deployment`) | ||
this._app.log.info(`[k8s] Instance ${project.id} - recreating deployment`) | ||
const fullProject = await this._app.db.models.Project.byId(project.id) | ||
@@ -597,3 +693,3 @@ await createProject(fullProject, options) | ||
} catch (err) { | ||
this._app.log.debug(`[k8s] Project ${project.id} - recreating deployment`) | ||
this._app.log.debug(`[k8s] Instance ${project.id} - recreating deployment`) | ||
const fullProject = await this._app.db.models.Project.byId(project.id) | ||
@@ -604,3 +700,3 @@ await createProject(fullProject, options) | ||
} catch (err) { | ||
this._app.log.error(`[k8s] Project ${project.id} - error resuming project: ${err.stack}`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - error resuming project: ${err.stack}`) | ||
} | ||
@@ -670,3 +766,3 @@ }) | ||
} catch (err) { | ||
this._app.log.error(`[k8s] Project ${project.id} - error deleting ingress: ${err.toString()}`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - error deleting ingress: ${err.toString()}`) | ||
} | ||
@@ -678,6 +774,22 @@ | ||
} catch (err) { | ||
this._app.log.error(`[k8s] Project ${project.id} - error deleting tls secret: ${err.toString()}`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - error deleting tls secret: ${err.toString()}`) | ||
} | ||
} | ||
if (this._customHostname?.enabled) { | ||
try { | ||
await this._k8sNetApi.deleteNamespacedIngress(`${project.safeName}-custom`, this._namespace) | ||
} catch (err) { | ||
this._app.log.error(`[k8s] Instance ${project.id} - error deleting custom ingress: ${err.toString()}`) | ||
} | ||
if (this._customHostname?.certManagerIssuer) { | ||
try { | ||
await this._k8sApi.deleteNamespacedSecret(`${project.safeName}-custom`, this._namespace) | ||
} catch (err) { | ||
this._app.log.error(`[k8s] Instance ${project.id} - error deleting custom tls secret: ${err.toString()}`) | ||
} | ||
} | ||
} | ||
// Note that, regardless, the main objective is to delete deployment (runnable) | ||
@@ -699,3 +811,3 @@ // Even if some k8s resources like ingress or service are still not deleted (maybe because of | ||
clearInterval(pollInterval) | ||
this._app.log.error(`[k8s] Project ${project.id} - timed out deleting ingress`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - timed out deleting ingress`) | ||
reject(new Error('Timed out to deleting Ingress')) | ||
@@ -706,3 +818,3 @@ } | ||
} catch (err) { | ||
this._app.log.error(`[k8s] Project ${project.id} - Ingress was not deleted: ${err.toString()}`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - Ingress was not deleted: ${err.toString()}`) | ||
} | ||
@@ -714,3 +826,3 @@ | ||
} catch (err) { | ||
this._app.log.error(`[k8s] Project ${project.id} - error deleting service: ${err.toString()}`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - error deleting service: ${err.toString()}`) | ||
} | ||
@@ -731,3 +843,3 @@ | ||
clearInterval(pollInterval) | ||
this._app.log.error(`[k8s] Project ${project.id} - timed deleting service`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - timed deleting service`) | ||
reject(new Error('Timed out to deleting Service')) | ||
@@ -738,3 +850,3 @@ } | ||
} catch (err) { | ||
this._app.log.error(`[k8s] Project ${project.id} - Service was not deleted: ${err.toString()}`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - Service was not deleted: ${err.toString()}`) | ||
} | ||
@@ -764,3 +876,3 @@ | ||
clearInterval(pollInterval) | ||
this._app.log.error(`[k8s] Project ${project.id} - timed deleting ${pod ? 'Pod' : 'Deployment'}`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - timed deleting ${pod ? 'Pod' : 'Deployment'}`) | ||
reject(new Error('Timed out to deleting Deployment')) | ||
@@ -785,3 +897,3 @@ } | ||
} catch (err) { | ||
this._app.log.error(`[k8s] Project ${project.id} - error deleting ingress: ${err.toString()}`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - error deleting ingress: ${err.toString()}`) | ||
} | ||
@@ -792,5 +904,19 @@ if (this._certManagerIssuer) { | ||
} catch (err) { | ||
this._app.log.error(`[k8s] Project ${project.id} - error deleting tls secret: ${err.toString()}`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - error deleting tls secret: ${err.toString()}`) | ||
} | ||
} | ||
if (this._customHostname?.enabled) { | ||
try { | ||
await this._k8sNetApi.deleteNamespacedIngress(`${project.safeName}-custom`, this._namespace) | ||
} catch (err) { | ||
this._app.log.error(`[k8s] Instance ${project.id} - error deleting custom ingress: ${err.toString()}`) | ||
} | ||
if (this._customHostname?.certManagerIssuer) { | ||
try { | ||
await this._k8sApi.deleteNamespacedSecret(`${project.safeName}-custom`, this._namespace) | ||
} catch (err) { | ||
this._app.log.error(`[k8s] Instance ${project.id} - error deleting custom tls secret: ${err.toString()}`) | ||
} | ||
} | ||
} | ||
try { | ||
@@ -803,3 +929,3 @@ if (project.safeName.match(/^[0-9]/)) { | ||
} catch (err) { | ||
this._app.log.error(`[k8s] Project ${project.id} - error deleting service: ${err.toString()}`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - error deleting service: ${err.toString()}`) | ||
} | ||
@@ -818,5 +944,5 @@ const currentType = await project.getSetting('k8sType') | ||
if (currentType === 'deployment') { | ||
this._app.log.error(`[k8s] Project ${project.id} - error deleting deployment: ${err.toString()}`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - error deleting deployment: ${err.toString()}`) | ||
} else { | ||
this._app.log.error(`[k8s] Project ${project.id} - error deleting pod: ${err.toString()}`) | ||
this._app.log.error(`[k8s] Instance ${project.id} - error deleting pod: ${err.toString()}`) | ||
} | ||
@@ -871,3 +997,3 @@ } | ||
} catch (err) { | ||
this._app.log.debug(`error getting state from project ${project.id}: ${err}`) | ||
this._app.log.debug(`error getting state from instance ${project.id}: ${err}`) | ||
return { | ||
@@ -905,3 +1031,3 @@ id: project.id, | ||
} catch (err) { | ||
this._app.log.debug(`error getting state from project ${project.id}: ${err}`) | ||
this._app.log.debug(`error getting state from instance ${project.id}: ${err}`) | ||
return { | ||
@@ -923,3 +1049,3 @@ id: project.id, | ||
} catch (err) { | ||
this._app.log.debug(`error getting pod status for project ${project.id}: ${err}`) | ||
this._app.log.debug(`error getting pod status for instance ${project.id}: ${err}`) | ||
return { | ||
@@ -1046,3 +1172,3 @@ id: project?.id, | ||
revokeUserToken: async (project, token) => { // logout:nodered(step-3) | ||
this._app.log.debug(`[k8s] Project ${project.id} - logging out node-red instance`) | ||
this._app.log.debug(`[k8s] Instance ${project.id} - logging out node-red instance`) | ||
const endpoints = await getEndpoints(project) | ||
@@ -1049,0 +1175,0 @@ const commands = [] |
{ | ||
"name": "@flowfuse/driver-kubernetes", | ||
"version": "2.4.1-d12c406-202406041336.0", | ||
"version": "2.5.0", | ||
"description": "Kubernetes driver for FlowFuse", | ||
@@ -5,0 +5,0 @@ "main": "kubernetes.js", |
@@ -24,2 +24,7 @@ # FlowForge Docker Container Driver | ||
logPassthrough: true | ||
customHostname: | ||
enabled: true | ||
cnameTarget: custom-loadbalancer.example.com | ||
certManagerIssuer: lets-encrypt | ||
ingressClass: custom-nginx | ||
``` | ||
@@ -38,2 +43,7 @@ | ||
- `logPassthrough` Have Node-RED logs printed in JSON format to container stdout (default false) | ||
- `customHostname` Settings linked to allowing instances to have a second hostname | ||
- `customHostname.enabled` (default false) | ||
- `customHostname.cnameTarget` The hostname users should configure their DNS entries to point at. Required. (default not set) | ||
- `customHostname.certManagerIssuer` Name of the Cluster issuer to use to create HTTPS certs for the custom hostname (default not set) | ||
- `customHostname.ingressClass` Name of the IngressClass to use to expose the custom hostname (default not set) | ||
@@ -40,0 +50,0 @@ Expects to pick up K8s credentials from the environment |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
68755
1086
1
58