Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Hostname support #151

Merged
merged 18 commits into from
Jun 5, 2024
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ driver:
k8sDelay: 1000
k8sRetries: 10
logPassthrough: true
customHostname:
enabled: true
certManagerIssuer: lets-encrypt
ingressClass: custom-nginx
```

- `registry` is the Docker Registry to load Stack Containers from
Expand All @@ -34,6 +38,10 @@ AWS EKS specific annotation for ALB Ingress. or `openshift` to allow running on
- `k8sRetries` how many times to retry actions against the K8s API
- `k8sDelay` how long to wait (in ms) between retries to the K8s API
- `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.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)

Expects to pick up K8s credentials from the environment

Expand Down
136 changes: 131 additions & 5 deletions kubernetes.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,36 @@ const ingressTemplate = {
}
}

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) => {
const stack = project.ProjectStack.properties

Expand Down Expand Up @@ -340,6 +370,50 @@ const createIngress = async (project, options) => {
return localIngress
}

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) => {
const namespace = this._app.config.driver.options.projectNamespace || 'flowforge'

Expand Down Expand Up @@ -434,6 +508,24 @@ const createProject = async (project, options) => {
}
}
}
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 project ${project.id} already exists, proceeding...`)
knolleary marked this conversation as resolved.
Show resolved Hide resolved
} else {
if (project.state !== 'suspended') {
this._app.log.error(`[k8s] Project ${project.id} - error creating custom hostname ingress: ${err.toString()}`)
throw err
}
}
}
}
}

await new Promise((resolve, reject) => {
let counter = 0
Expand Down Expand Up @@ -479,11 +571,11 @@ const getEndpoints = async (project) => {

module.exports = {
/**
* 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) => {
this._app = app
this._projects = {}
Expand All @@ -495,6 +587,10 @@ module.exports = {
this._certManagerIssuer = this._app.config.driver.options?.certManagerIssuer
this._logPassthrough = this._app.config.driver.options?.logPassthrough || false
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
}

const kc = new k8s.KubeConfig()

Expand Down Expand Up @@ -663,6 +759,22 @@ module.exports = {
}
}

if (this._customHostname?.enabled) {
try {
await this._k8sNetApi.deleteNamespacedIngress(`${project.safeName}-custom`, this._namespace)
} catch (err) {
this._app.log.error(`[k8s] Project ${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] Project ${project.id} - error deleting custom tls secret: ${err.toString()}`)
}
}
}

// Note that, regardless, the main objective is to delete deployment (runnable)
// Even if some k8s resources like ingress or service are still not deleted (maybe because of
// k8s service latency), the most important thing is to get to deployment.
Expand Down Expand Up @@ -768,6 +880,20 @@ module.exports = {
this._app.log.error(`[k8s] Project ${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] Project ${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] Project ${project.id} - error deleting custom tls secret: ${err.toString()}`)
}
}
}
try {
if (project.safeName.match(/^[0-9]/)) {
await this._k8sApi.deleteNamespacedService('srv-' + project.safeName, this._namespace)
Expand Down
Loading