Skip to content

Commit 63f31ae

Browse files
committed
Initial Commit of the n8n automation framework
1 parent cd83891 commit 63f31ae

File tree

2 files changed

+358
-0
lines changed

2 files changed

+358
-0
lines changed

.templates/n8n/build.py

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
#!/usr/bin/env python3
2+
3+
issues = {} # Returned issues dict
4+
buildHooks = {} # Options, and others hooks
5+
haltOnErrors = True
6+
7+
# Main wrapper function. Required to make local vars work correctly
8+
def main():
9+
import os
10+
import time
11+
import ruamel.yaml
12+
import signal
13+
import sys
14+
from blessed import Terminal
15+
16+
from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText
17+
from deps.consts import servicesDirectory, templatesDirectory
18+
from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail
19+
20+
yaml = ruamel.yaml.YAML()
21+
yaml.preserve_quotes = True
22+
23+
global dockerComposeServicesYaml # The loaded memory YAML of all checked services
24+
global toRun # Switch for which function to run when executed
25+
global buildHooks # Where to place the options menu result
26+
global currentServiceName # Name of the current service
27+
global issues # Returned issues dict
28+
global haltOnErrors # Turn on to allow erroring
29+
global hideHelpText # Showing and hiding the help controls text
30+
global serviceService
31+
32+
serviceService = servicesDirectory + currentServiceName
33+
serviceTemplate = templatesDirectory + currentServiceName
34+
35+
try: # If not already set, then set it.
36+
hideHelpText = hideHelpText
37+
except:
38+
hideHelpText = False
39+
40+
documentationHint = 'https://docs.n8n.io/quickstart/'
41+
42+
# runtime vars
43+
portConflicts = []
44+
45+
# This lets the menu know whether to put " >> Options " or not
46+
# This function is REQUIRED.
47+
def checkForOptionsHook():
48+
try:
49+
buildHooks["options"] = callable(runOptionsMenu)
50+
except:
51+
buildHooks["options"] = False
52+
return buildHooks
53+
return buildHooks
54+
55+
# This function is REQUIRED.
56+
def checkForPreBuildHook():
57+
try:
58+
buildHooks["preBuildHook"] = callable(preBuild)
59+
except:
60+
buildHooks["preBuildHook"] = False
61+
return buildHooks
62+
return buildHooks
63+
64+
# This function is REQUIRED.
65+
def checkForPostBuildHook():
66+
try:
67+
buildHooks["postBuildHook"] = callable(postBuild)
68+
except:
69+
buildHooks["postBuildHook"] = False
70+
return buildHooks
71+
return buildHooks
72+
73+
# This function is REQUIRED.
74+
def checkForRunChecksHook():
75+
try:
76+
buildHooks["runChecksHook"] = callable(runChecks)
77+
except:
78+
buildHooks["runChecksHook"] = False
79+
return buildHooks
80+
return buildHooks
81+
82+
# This service will not check anything unless this is set
83+
# This function is optional, and will run each time the menu is rendered
84+
def runChecks():
85+
checkForIssues()
86+
return []
87+
88+
# This function is optional, and will run after the docker-compose.yml file is written to disk.
89+
def postBuild():
90+
return True
91+
92+
# This function is optional, and will run just before the build docker-compose.yml code.
93+
def preBuild():
94+
global dockerComposeServicesYaml
95+
global currentServiceName
96+
97+
# Setup service directory
98+
if not os.path.exists(serviceService):
99+
os.makedirs(serviceService, exist_ok=True)
100+
os.makedirs(serviceService + '/etc_n8n', exist_ok=True)
101+
os.makedirs(serviceService + '/var_lib_n8n', exist_ok=True)
102+
return True
103+
104+
# #####################################
105+
# Supporting functions below
106+
# #####################################
107+
108+
def checkForIssues():
109+
for (index, serviceName) in enumerate(dockerComposeServicesYaml):
110+
if not currentServiceName == serviceName: # Skip self
111+
currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml)
112+
portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml)
113+
if (len(portConflicts) > 0):
114+
issues["portConflicts"] = portConflicts
115+
116+
# #####################################
117+
# End Supporting functions
118+
# #####################################
119+
120+
############################
121+
# Menu Logic
122+
############################
123+
124+
global currentMenuItemIndex
125+
global selectionInProgress
126+
global menuNavigateDirection
127+
global needsRender
128+
129+
selectionInProgress = True
130+
currentMenuItemIndex = 0
131+
menuNavigateDirection = 0
132+
needsRender = 1
133+
term = Terminal()
134+
hotzoneLocation = [((term.height // 16) + 6), 0]
135+
136+
def goBack():
137+
global selectionInProgress
138+
global needsRender
139+
selectionInProgress = False
140+
needsRender = 1
141+
return True
142+
143+
def enterPortNumberExec():
144+
# global term
145+
global needsRender
146+
global dockerComposeServicesYaml
147+
externalPort = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0]
148+
internalPort = getInternalPorts(currentServiceName, dockerComposeServicesYaml)[0]
149+
newPortNumber = enterPortNumberWithWhiptail(term, dockerComposeServicesYaml, currentServiceName, hotzoneLocation, externalPort)
150+
151+
if newPortNumber > 0:
152+
dockerComposeServicesYaml[currentServiceName]["ports"][0] = "{newExtPort}:{oldIntPort}".format(
153+
newExtPort = newPortNumber,
154+
oldIntPort = internalPort
155+
)
156+
createMenu()
157+
needsRender = 1
158+
159+
def onResize(sig, action):
160+
global n8nBuildOptions
161+
global currentMenuItemIndex
162+
mainRender(1, n8nBuildOptions, currentMenuItemIndex)
163+
164+
n8nBuildOptions = []
165+
166+
def createMenu():
167+
global n8nBuildOptions
168+
try:
169+
n8nBuildOptions = []
170+
portNumber = getExternalPorts(currentServiceName, dockerComposeServicesYaml)[0]
171+
n8nBuildOptions.append([
172+
"Change external WUI Port Number from: {port}".format(port=portNumber),
173+
enterPortNumberExec
174+
])
175+
except: # Error getting port
176+
pass
177+
n8nBuildOptions.append(["Go back", goBack])
178+
179+
def runOptionsMenu():
180+
createMenu()
181+
menuEntryPoint()
182+
return True
183+
184+
def renderHotZone(term, menu, selection, hotzoneLocation):
185+
lineLengthAtTextStart = 71
186+
print(term.move(hotzoneLocation[0], hotzoneLocation[1]))
187+
for (index, menuItem) in enumerate(menu):
188+
toPrint = ""
189+
if index == selection:
190+
toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"]))
191+
else:
192+
toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"]))
193+
194+
for i in range(lineLengthAtTextStart - len(menuItem[0])):
195+
toPrint += " "
196+
197+
toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"])
198+
199+
toPrint = term.center(toPrint)
200+
201+
print(toPrint)
202+
203+
def mainRender(needsRender, menu, selection):
204+
term = Terminal()
205+
206+
if needsRender == 1:
207+
print(term.clear())
208+
print(term.move_y(term.height // 16))
209+
print(term.black_on_cornsilk4(term.center('IOTstack n8n Options')))
210+
print("")
211+
print(term.center(commonTopBorder(renderMode)))
212+
print(term.center(commonEmptyLine(renderMode)))
213+
print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"])))
214+
print(term.center(commonEmptyLine(renderMode)))
215+
216+
if needsRender >= 1:
217+
renderHotZone(term, menu, selection, hotzoneLocation)
218+
219+
if needsRender == 1:
220+
print(term.center(commonEmptyLine(renderMode)))
221+
print(term.center(commonEmptyLine(renderMode)))
222+
if not hideHelpText:
223+
print(term.center(commonEmptyLine(renderMode)))
224+
print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"])))
225+
print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"])))
226+
print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"])))
227+
print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"])))
228+
print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"])))
229+
print(term.center(commonEmptyLine(renderMode)))
230+
if len(documentationHint) > 1:
231+
if len(documentationHint) > 56:
232+
documentationAndPadding = padText(documentationHint, 71)
233+
print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"])))
234+
print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding)))
235+
else:
236+
documentationAndPadding = padText(documentationHint, 56)
237+
print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding)))
238+
print(term.center(commonEmptyLine(renderMode)))
239+
print(term.center(commonEmptyLine(renderMode)))
240+
print(term.center(commonBottomBorder(renderMode)))
241+
242+
def runSelection(selection):
243+
import types
244+
global n8nBuildOptions
245+
if len(n8nBuildOptions[selection]) > 1 and isinstance(n8nBuildOptions[selection][1], types.FunctionType):
246+
n8nBuildOptions[selection][1]()
247+
else:
248+
print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0])))
249+
250+
def isMenuItemSelectable(menu, index):
251+
if len(menu) > index:
252+
if len(menu[index]) > 2:
253+
if menu[index][2]["skip"] == True:
254+
return False
255+
return True
256+
257+
def menuEntryPoint():
258+
# These need to be reglobalised due to eval()
259+
global currentMenuItemIndex
260+
global selectionInProgress
261+
global menuNavigateDirection
262+
global needsRender
263+
global hideHelpText
264+
global n8nBuildOptions
265+
term = Terminal()
266+
with term.fullscreen():
267+
menuNavigateDirection = 0
268+
mainRender(needsRender, n8nBuildOptions, currentMenuItemIndex)
269+
selectionInProgress = True
270+
with term.cbreak():
271+
while selectionInProgress:
272+
menuNavigateDirection = 0
273+
274+
if needsRender: # Only rerender when changed to prevent flickering
275+
mainRender(needsRender, n8nBuildOptions, currentMenuItemIndex)
276+
needsRender = 0
277+
278+
key = term.inkey(esc_delay=0.05)
279+
if key.is_sequence:
280+
if key.name == 'KEY_TAB':
281+
menuNavigateDirection += 1
282+
if key.name == 'KEY_DOWN':
283+
menuNavigateDirection += 1
284+
if key.name == 'KEY_UP':
285+
menuNavigateDirection -= 1
286+
if key.name == 'KEY_LEFT':
287+
goBack()
288+
if key.name == 'KEY_ENTER':
289+
runSelection(currentMenuItemIndex)
290+
if key.name == 'KEY_ESCAPE':
291+
return True
292+
elif key:
293+
if key == 'h': # H pressed
294+
if hideHelpText:
295+
hideHelpText = False
296+
else:
297+
hideHelpText = True
298+
mainRender(1, n8nBuildOptions, currentMenuItemIndex)
299+
300+
if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item
301+
currentMenuItemIndex += menuNavigateDirection
302+
currentMenuItemIndex = currentMenuItemIndex % len(n8nBuildOptions)
303+
needsRender = 2
304+
305+
while not isMenuItemSelectable(n8nBuildOptions, currentMenuItemIndex):
306+
currentMenuItemIndex += menuNavigateDirection
307+
currentMenuItemIndex = currentMenuItemIndex % len(n8nBuildOptions)
308+
return True
309+
310+
####################
311+
# End menu section
312+
####################
313+
314+
315+
if haltOnErrors:
316+
eval(toRun)()
317+
else:
318+
try:
319+
eval(toRun)()
320+
except:
321+
pass
322+
323+
# This check isn't required, but placed here for debugging purposes
324+
global currentServiceName # Name of the current service
325+
if currentServiceName == 'n8n':
326+
main()
327+
else:
328+
print("Error. '{}' Tried to run 'n8n' config".format(currentServiceName))

.templates/n8n/service.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
n8n:
2+
container_name: "n8n"
3+
restart: unless-stopped
4+
ports:
5+
- "5678:5678"
6+
image: n8nio/n8n
7+
stdin_open: true
8+
volumes:
9+
- ./volumes/n8n:/home/node/.n8n
10+
networks:
11+
- iotstack_nw
12+
# Optional DB and Timezone configs.
13+
# environment:
14+
# - DB_TYPE=mysqldb
15+
# - DB_MYSQLDB_DATABASE=<MYSQLDB_DATABASE>
16+
# - DB_MYSQLDB_HOST=<MYSQLDB_HOST>
17+
# - DB_MYSQLDB_PORT=<MYSQLDB_PORT>
18+
# - DB_MYSQLDB_USER=<MYSQLDB_USER>
19+
# - DB_MYSQLDB_PASSWORD=<MYSQLDB_PASSWORD>
20+
# - GENERIC_TIMEZONE="Europe/Berlin"
21+
# - TZ="Europe/Berlin"
22+
# Uncomment to enable authentication
23+
# - N8N_BASIC_AUTH_ACTIVE=true
24+
# - N8N_BASIC_AUTH_USER=<USER>
25+
# - N8N_BASIC_AUTH_PASSWORD=<PASSWORD>
26+
27+
# - PGID=1000
28+
# - PUID=1000
29+
# - USBDEVICES=/dev/ttyAMA0
30+
# - PACKAGES=mc

0 commit comments

Comments
 (0)