${json}
`)
96 | })
97 |
98 | app.delete('/api/dev/agent/:ip/containers/:id', async (req, res) => {
99 | try {
100 | const { ip, id } = req.params
101 | const response = await axios.delete(`http://${ip}:9501/containers/${id}`)
102 | res.send(response.data)
103 | } catch (error: any) {
104 | res.status(500).send(error.message)
105 | }
106 | })
107 |
108 | app.get('/api/dev/system/df', async (req, res) => {
109 | const system = await systemDF()
110 | try {
111 | return res.json(system)
112 | } catch (err: any) {
113 | return res.status(500).send(err.message)
114 | }
115 | })
116 |
117 | app.get('/api/dev/nodes', async (req, res) => {
118 | const nodes = await nodesInfo()
119 | try {
120 | return res.json(nodes)
121 | } catch (err: any) {
122 | return res.status(500).send(err.message)
123 | }
124 | })
125 |
126 | app.get('/api/dev/agents/dns', async (req, res) => {
127 | try {
128 | const dns = await agentDNSLookup()
129 | return res.json(dns)
130 | } catch (err: any) {
131 | return res.status(500).send(err.message)
132 | }
133 | })
134 |
135 | app.get('/api/dev/container/:hash', async (req, res) => {
136 | try {
137 | const { hash } = req.params
138 | const _container = await containerStats(hash)
139 | return res.json(_container)
140 | } catch (err: any) {
141 | return res.status(500).send(err.message)
142 | }
143 | })
144 |
145 | app.get('/api/dev/:node/info', async (req, res) => {
146 | try {
147 | const { node } = req.params
148 | const result = await fetch(`http://${node}:9501/info`)()
149 | return res.json(result)
150 | } catch (err: any) {
151 | return res.status(500).send(err.message)
152 | }
153 | })
154 |
155 | app.get('/api/dev/:node/containers', async (req, res) => {
156 | try {
157 | const { node } = req.params
158 | const result = await fetch(`http://${node}:9501/containers`)()
159 | return res.json(result)
160 | } catch (err: any) {
161 | return res.status(500).send(err.message)
162 | }
163 | })
164 |
165 | app.get('/api/dev', async (req, res) => {
166 | const nodes = await nodesInfo()
167 | const addrs = nodes.map(n => n.Addr)
168 |
169 | const promises: any[] = []
170 |
171 | addrs.forEach(addr => promises.push(fetch(`http://${addr}:9501/`)()))
172 |
173 | try {
174 | const results = await Promise.allSettled(promises)
175 | return res.json({ nodes, results })
176 | } catch (err: any) {
177 | return res.status(500).send(err.message)
178 | }
179 | })
180 |
181 | app.get('/healthcheck', (req, res) => {
182 | res.send('OK')
183 | })
184 |
185 | app.get('*', (req, res) => {
186 | return res.status(404).send('nothing here')
187 | })
188 |
189 | app.listen(port, () => {
190 | console.log(`[manager] listening at http://127.0.0.1:${port}`)
191 | })
192 |
--------------------------------------------------------------------------------
/src/tasks/agent.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Yannick Deubel (https://github.com/yandeu)
3 | * @copyright Copyright (c) 2021 Yannick Deubel
4 | * @license {@link https://github.com/yandeu/docker-swarm-visualizer/blob/main/LICENSE LICENSE}
5 | */
6 |
7 | import { Router } from 'express'
8 | import { curlWithCron } from '../lib/cron.js'
9 | import { generateId } from '../lib/misc.js'
10 | import { checkContainersForTasks, gaterInformationForContainerTasks } from './task.autoscale.js'
11 | import { getTasks } from './task.autoscale.js'
12 |
13 | const SECRET = generateId()
14 |
15 | const router = Router()
16 |
17 | let url = ''
18 | let cmd = ''
19 |
20 | const TASK_ENABLED = process.env.VISUALIZER_TASK === 'true' ? true : false
21 | console.log(TASK_ENABLED ? '[agent] tasks are enabled' : '[agent] tasks are disabled')
22 |
23 | // at startup
24 | if (TASK_ENABLED) setTimeout(checkContainersForTasks, 5_000)
25 |
26 | /** the tasks this node wishes to perform */
27 | router.get('/', (req, res) => {
28 | if (!TASK_ENABLED) return res.json([])
29 | return res.json(getTasks())
30 | })
31 |
32 | router.get('/checkContainersForTasks', async (req, res) => {
33 | if (!TASK_ENABLED) return res.send()
34 | const { secret } = req.query
35 | if (secret === SECRET) checkContainersForTasks()
36 | return res.send()
37 | })
38 |
39 | router.get('/gaterInformationForContainerTasks', async (req, res) => {
40 | if (!TASK_ENABLED) return res.send()
41 | const { secret } = req.query
42 | if (secret === SECRET) gaterInformationForContainerTasks()
43 | return res.send()
44 | })
45 |
46 | export { router as tasksRouter }
47 |
48 | if (TASK_ENABLED) {
49 | ;(async () => {
50 | if (process.env.VISUALIZER_TASK_SUBNET === 'true') {
51 | import('./task.subnet.js')
52 | .then(module => {
53 | console.log('[agent] task.subnet.js loaded')
54 | module.addSubnetLabel().catch(err => {
55 | console.log('[agent] Something went wrong in [addSubnetLabel()]: ', err.message)
56 | })
57 | })
58 | .catch(err => {
59 | console.log('[agent] task.subnet.js failed', err.message)
60 | })
61 | }
62 |
63 | // keep track which containers have tasks
64 | url = `http://127.0.0.1:9501/tasks/checkContainersForTasks?secret=${SECRET}`
65 | cmd = `curl --silent ${url}`
66 | await curlWithCron({ url, cron: `* * * * * ${cmd}`, interval: 60_000 })
67 |
68 | // collect and process tasks from containers
69 | url = `http://127.0.0.1:9501/tasks/gaterInformationForContainerTasks?secret=${SECRET}`
70 | cmd = `curl --silent ${url}`
71 | await curlWithCron({
72 | url,
73 | cron: `* * * * * for i in 0 1 2; do ${cmd} & sleep 15; done; echo ${cmd}`,
74 | interval: 15_000
75 | })
76 | })()
77 | }
78 |
--------------------------------------------------------------------------------
/src/tasks/manager.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Yannick Deubel (https://github.com/yandeu)
3 | * @copyright Copyright (c) 2021 Yannick Deubel
4 | * @license {@link https://github.com/yandeu/docker-swarm-visualizer/blob/main/LICENSE LICENSE}
5 | */
6 |
7 | import { Router } from 'express'
8 | import { curlWithCron } from '../lib/cron.js'
9 | import { generateId } from '../lib/misc.js'
10 | import { checkAgentsForNewTasks } from './task.autoscale.js'
11 |
12 | const SECRET = generateId()
13 |
14 | const router = Router()
15 |
16 | let url = ''
17 | let cmd = ''
18 |
19 | const TASK_ENABLED = process.env.VISUALIZER_TASK === 'true' ? true : false
20 | console.log(TASK_ENABLED ? '[manager] tasks are enabled' : '[manager] tasks are disabled')
21 |
22 | router.get('/checkAgentsForNewTasks', async (req, res) => {
23 | if (!TASK_ENABLED) return res.send()
24 | const { secret } = req.query
25 | if (secret === SECRET) checkAgentsForNewTasks()
26 | return res.send()
27 | })
28 |
29 | router.get('/checkForImageUpdate', async (req, res) => {
30 | if (!TASK_ENABLED) return res.send()
31 | const { secret } = req.query
32 | if (secret === SECRET)
33 | import('./task.autoupdate.js').then(module => {
34 | module.checkImageUpdate()
35 | })
36 | return res.send()
37 | })
38 |
39 | export { router as tasksRouter }
40 |
41 | if (TASK_ENABLED) {
42 | ;(async () => {
43 | if (process.env.VISUALIZER_TASK_AUTOUPDATE === 'true') {
44 | let cron = process.env.VISUALIZER_TASK_AUTOUPDATE_CRON
45 | if (!cron || cron.split(' ').length !== 5) {
46 | cron = '0 */6 * * *'
47 | console.log('VISUALIZER_TASK_AUTOUPDATE_CRON is invalid or not present. Fallback to', cron)
48 | } else {
49 | console.log('Starting VISUALIZER_TASK_AUTOUPDATE with cron', cron)
50 | }
51 |
52 | // k// check every agent for new tasks and process/apply them
53 | url = `http://127.0.0.1:3500/tasks/checkForImageUpdate?secret=${SECRET}`
54 | cmd = `curl --silent ${url}`
55 | await curlWithCron({
56 | url,
57 | cron: cron,
58 | interval: -1
59 | })
60 | }
61 |
62 | if (process.env.VISUALIZER_TASK_AUTOSCALE === 'true') {
63 | // k// check every agent for new tasks and process/apply them
64 | url = `http://127.0.0.1:3500/tasks/checkAgentsForNewTasks?secret=${SECRET}`
65 | cmd = `curl --silent ${url}`
66 | await curlWithCron({
67 | url,
68 | cron: `* * * * * for i in 0 1 2 3 4; do ${cmd} & sleep 10; done; echo ${cmd}`,
69 | interval: 10_000
70 | })
71 | }
72 | })()
73 | }
74 |
--------------------------------------------------------------------------------
/src/tasks/task.autoscale.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Yannick Deubel (https://github.com/yandeu)
3 | * @copyright Copyright (c) 2021 Yannick Deubel
4 | * @license {@link https://github.com/yandeu/docker-swarm-visualizer/blob/main/LICENSE LICENSE}
5 | */
6 |
7 | import type { CPUUsage, Tasks, AutoscalerSettings } from '../types.js'
8 |
9 | import { containers, containerStats } from '../lib/api.js'
10 | import { docker } from '../lib/docker.js'
11 | import { agentDNSLookup } from '../lib/dns.js'
12 | import { fetch } from '../lib/fetch.js'
13 |
14 | const CPU_USAGE: any = {}
15 | const LABEL_VISUALIZER = /^visualizer\./
16 |
17 | let CONTAINERS_WITH_TASKS: any[] = []
18 | let TASKS: any[] = []
19 | let DEBUG = false
20 |
21 | export const getTasks = () => TASKS
22 |
23 | export const executeTask = async (service: any, task: Tasks) => {
24 | if (DEBUG) console.log('[executeTask]')
25 |
26 | const { name, autoscaler = { min: 1, max: 2 } } = task
27 | const { min, max } = autoscaler
28 |
29 | if (DEBUG) console.log('Task, Try:', name)
30 |
31 | if (name !== 'SCALE_UP' && name !== 'SCALE_DOWN') return
32 |
33 | const serviceId = service.ID
34 | const serviceVersion = service.Version.Index
35 |
36 | // update the service specs
37 | service.Spec.Mode.Replicated.Replicas += name === 'SCALE_UP' ? 1 : -1
38 |
39 | if (name === 'SCALE_DOWN' && service.Spec.Mode.Replicated.Replicas < min) return
40 | if (name === 'SCALE_UP' && service.Spec.Mode.Replicated.Replicas > max) return
41 |
42 | if (DEBUG) console.log('Task, Do:', name)
43 |
44 | try {
45 | await docker(`services/${serviceId}/update?version=${serviceVersion}`, 'POST', service.Spec)
46 | } catch (error: any) {
47 | console.log('error', error.message)
48 | }
49 | }
50 |
51 | /**
52 | * Check one per minute which containers have tasks
53 | */
54 | export const checkContainersForTasks = async () => {
55 | if (DEBUG) console.log('[checkContainersForTasks]')
56 |
57 | let _containers: any = await containers(false)
58 |
59 | CONTAINERS_WITH_TASKS = _containers
60 | // only "running" containers
61 | .filter(c => c.State === 'running')
62 | // only containers with at least one label starting with "visualizer."
63 | .filter(c => Object.keys(c.Labels).some(key => LABEL_VISUALIZER.test(key)))
64 | // return container id and all its labels
65 | .map(c => ({
66 | id: c.Id,
67 | labels: c.Labels
68 | }))
69 | }
70 |
71 | /**
72 | * Gather information about container tasks
73 | */
74 | export const gaterInformationForContainerTasks = async () => {
75 | if (DEBUG) console.log('[gaterInformationForContainerTasks]')
76 |
77 | const batch = CONTAINERS_WITH_TASKS.map(async container => {
78 | const _stats: any = await containerStats(container.id)
79 |
80 | if (_stats && _stats.cpu_stats && _stats.precpu_stats && _stats.memory_stats) {
81 | const stats = {
82 | cpu_stats: _stats.cpu_stats,
83 | precpu_stats: _stats.precpu_stats,
84 | memory_stats: _stats.memory_stats
85 | }
86 | const cpuUsage = calculateCPUUsageOneMinute({ Id: container.id, Stats: stats })
87 | return { ...container, cpuUsage }
88 | }
89 |
90 | return container
91 | })
92 |
93 | const result: any[] = await Promise.all(batch)
94 |
95 | // TODO(yandeu): Decide what to do
96 | // publish each task only once per 5 Minute?
97 |
98 | const tasks: Tasks[] = []
99 | result.forEach(r => {
100 | const cpuUsage = r.cpuUsage as CPUUsage
101 |
102 | // service
103 | const service = r.labels['com.docker.swarm.service.name']
104 |
105 | // autoscaler
106 | const cpuUp = parseFloat(r.labels['visualizer.autoscale.up.cpu'])
107 | const cpuDown = parseFloat(r.labels['visualizer.autoscale.down.cpu'])
108 | const max = parseInt(r.labels['visualizer.autoscale.max'])
109 | const min = parseInt(r.labels['visualizer.autoscale.min'])
110 |
111 | // updates
112 | // TODO
113 |
114 | const autoscaler: AutoscalerSettings = { min, max, up: { cpu: cpuUp }, down: { cpu: cpuDown } }
115 |
116 | if (cpuUsage && cpuUsage.cpu >= 0 && service && max > 0 && min > 0) {
117 | if (cpuUsage.cpu > cpuUp * 100) {
118 | tasks.push({ name: 'SCALE_UP', service: service, autoscaler, cpuUsage })
119 | }
120 | if (cpuUsage.cpu < cpuDown * 100) {
121 | tasks.push({ name: 'SCALE_DOWN', service: service, autoscaler, cpuUsage })
122 | }
123 | }
124 | })
125 |
126 | // console.log('agent, tasks:', tasks.length)
127 |
128 | TASKS = tasks
129 | }
130 |
131 | export const calculateCPUUsageOneMinute = (container): CPUUsage => {
132 | if (DEBUG) console.log('[calculateCPUUsageOneMinute]')
133 |
134 | const { Stats, Id } = container
135 |
136 | if (!CPU_USAGE[Id]) CPU_USAGE[Id] = []
137 |
138 | let cpuPercent = 0.0
139 |
140 | CPU_USAGE[Id].push({
141 | time: new Date().getTime(),
142 | usage: Stats.precpu_stats.cpu_usage.total_usage,
143 | systemUsage: Stats.precpu_stats.system_cpu_usage
144 | })
145 |
146 | try {
147 | const cpuDelta = Stats.cpu_stats.cpu_usage.total_usage - CPU_USAGE[Id][0].usage
148 |
149 | const systemDelta = Stats.cpu_stats.system_cpu_usage - CPU_USAGE[Id][0].systemUsage
150 |
151 | if (systemDelta > 0.0 && cpuDelta > 0.0) cpuPercent = (cpuDelta / systemDelta) * Stats.cpu_stats.online_cpus * 100.0
152 |
153 | // 2 time 10 seconds = 20 seconds
154 | // give the average of 20 second cpu
155 | if (CPU_USAGE[Id].length > 2) {
156 | const data = { cpu: cpuPercent, time: new Date().getTime() - CPU_USAGE[Id][0].time }
157 | CPU_USAGE[Id].shift()
158 | return data
159 | }
160 |
161 | return { cpu: -1, time: -1 }
162 | } catch (error) {
163 | return { cpu: -1, time: -1 }
164 | }
165 | }
166 |
167 | export const checkAgentsForNewTasks = async () => {
168 | if (DEBUG) console.log('[checkAgentsForNewTasks]')
169 |
170 | const tasks: { [key: string]: Tasks[] } = {}
171 |
172 | const dns = await agentDNSLookup()
173 | if (dns.length === 0) return tasks
174 |
175 | const agents: Tasks[][] = await Promise.all(dns.map(addr => fetch(`http://${addr}:9501/tasks`)()))
176 |
177 | // console.log('check tasks', agents?.length)
178 |
179 | agents.forEach(agentTasks => {
180 | agentTasks.forEach(task => {
181 | const { service } = task
182 | if (!tasks[service]) tasks[service] = [task]
183 | else tasks[service].push(task)
184 | })
185 | })
186 |
187 | // first task of first agent
188 | // console.log(agents[0][0])
189 |
190 | // console.log('tasks', tasks)
191 |
192 | // NOTE:
193 | // We need a agent quorum to perform a task (> 50%)
194 |
195 | Object.keys(tasks).forEach(async key => {
196 | // console.log('new tasks', Object.keys(tasks).length)
197 | const task = tasks[key]
198 |
199 | const scaleUp = task.filter(t => t.name === 'SCALE_UP')
200 | const scaleDown = task.filter(t => t.name === 'SCALE_DOWN')
201 |
202 | const service: any = await docker(`services/${task[0].service}`)
203 | const replicas = service?.Spec?.Mode?.Replicated?.Replicas as number
204 |
205 | if (typeof replicas === 'number' && replicas > 0) {
206 | let ratio
207 |
208 | ratio = scaleUp.length / replicas
209 | if (ratio - 0.5 > 0) executeTask(service, scaleUp[0])
210 |
211 | ratio = scaleDown.length / replicas
212 | if (ratio - 0.5 > 0) executeTask(service, scaleDown[0])
213 | }
214 | })
215 |
216 | // console.log(service.Spec.Mode.Replicated.Replicas)
217 |
218 | // TODO(yandeu): If some agents request some tasks, process them.
219 | }
220 |
--------------------------------------------------------------------------------
/src/tasks/task.autoupdate.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Yannick Deubel (https://github.com/yandeu)
3 | * @copyright Copyright (c) 2021 Yannick Deubel
4 | * @license {@link https://github.com/yandeu/docker-swarm-visualizer/blob/main/LICENSE LICENSE}
5 | */
6 |
7 | import { services as getServices, serviceUpdateImage, Image } from '../lib/api.js'
8 | import { Registry } from '../lib/registry.js'
9 |
10 | export const checkImageUpdate = async () => {
11 | const tmp = await getServices()
12 | if (!tmp) return
13 |
14 | const AUTOUPDATE_LABEL = 'visualizer.autoupdate'
15 |
16 | const services = tmp
17 | .map(s => ({
18 | ID: s.ID,
19 | Labels: s.Spec.TaskTemplate.ContainerSpec.Labels,
20 | Image: s.Spec.TaskTemplate.ContainerSpec.Image
21 | }))
22 | .filter(s => {
23 | return (
24 | s.Labels &&
25 | Object.keys(s.Labels).some(key => new RegExp(AUTOUPDATE_LABEL).test(key)) &&
26 | s.Labels[AUTOUPDATE_LABEL] === 'true'
27 | )
28 | })
29 |
30 | for (const service of services) {
31 | try {
32 | const { Image, ID } = service
33 |
34 | // TODO(yandeu): Let the user chose which registry and the auth
35 | const REGISTRY = 'DOCKER' // or 'GITHUB'
36 |
37 | // parse local digest
38 | const [_, IMG, TAG, localDigest] = /([\w\/-]+):([\w-]+)@(sha256:.+)/.exec(Image) as any
39 | const IMAGE = /\//.test(IMG) ? IMG : `library/${IMG}`
40 |
41 | // get digest of remote images
42 | const registry = new Registry(REGISTRY)
43 | console.log(IMAGE, TAG)
44 | registry.requestImage(IMAGE, TAG)
45 | const auth = await registry.Auth()
46 | const remoteDigest = await registry.getDigest(auth)
47 | registry.Clear()
48 |
49 | // check if digest of remote image is different from the local one
50 | // console.log({ localDigest, remoteDigest })
51 | if (localDigest && remoteDigest && localDigest !== remoteDigest) serviceUpdateImage(ID, IMAGE, TAG, remoteDigest)
52 | } catch (error: any) {
53 | console.log('Error while autoupdate', error.message)
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/tasks/task.subnet.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Yannick Deubel (https://github.com/yandeu)
3 | * @copyright Copyright (c) 2021 Yannick Deubel
4 | * @license {@link https://github.com/yandeu/docker-swarm-visualizer/blob/main/LICENSE LICENSE}
5 | */
6 |
7 | /**
8 | * @description
9 | * Automatically add a node label based on in which subnet the node is.
10 | *
11 | * @version
12 | * Works only in Node.js >= v15.0.0
13 | *
14 | * @example
15 | *
16 | # whatever service you want to deploy
17 | services:
18 | nginx:
19 | image: nginx:latest
20 | ports:
21 | - 8080:80
22 | deploy:
23 | placement:
24 | preferences:
25 | # spread this service out over the "subnet" label
26 | spread:
27 | - node.labels.subnet
28 |
29 | # the visualizer_agent service
30 | services
31 | agent:
32 | labels:
33 | - visualizer.subnet.az1=172.31.0.0/20
34 | - visualizer.subnet.az2=172.31.16.0/20
35 | - visualizer.subnet.az3=172.31.32.0/20
36 |
37 | if the node has the internal IP 127.31.18.5, the label "az2" should be added to that node.
38 |
39 | */
40 |
41 | import net from 'net'
42 | import { containers as getContainers, info, node } from '../lib/api.js'
43 | import { docker } from '../lib/docker.js'
44 |
45 | export const addSubnetLabel = async () => {
46 | const { NodeAddr, NodeID } = await info()
47 |
48 | let containers = await getContainers(false)
49 | containers = containers.filter(c => 'visualizer.agent' in c.Labels && c.State === 'running')
50 | if (containers.length === 0) return
51 |
52 | // check if there are any subnet labels
53 | const subnetRegex = /^visualizer.subnet./
54 | const subnets = Object.entries(containers[0].Labels)
55 | .filter(([key]) => subnetRegex.test(key))
56 | .map(entry => `${entry[0].replace(subnetRegex, '')}=${entry[1]}`)
57 |
58 | // const subnets = ['az1=172.31.0.0/20', 'az2=172.31.16.0/20', 'az3=172.31.32.0/20']
59 | console.log('available subnets', subnets)
60 |
61 | let found
62 | while (subnets.length > 0 && !found) {
63 | const subnet = subnets.pop() as string
64 | const reg = /(\w+)=([\d\.]+)\/([\d]+)/gm
65 |
66 | try {
67 | const [_, label, ip, prefix] = reg.exec(subnet) as any
68 |
69 | const list = new net.BlockList()
70 | list.addSubnet(ip, parseInt(prefix))
71 | const match = list.check(NodeAddr)
72 |
73 | if (match) found = label
74 | } catch (error: any) {
75 | console.log(error.message)
76 | }
77 | }
78 |
79 | if (found) {
80 | // get current node spec
81 | let { Spec, Version } = await node(NodeID)
82 |
83 | // update current node spec
84 | Spec.Labels = { ...Spec.Labels, subnet: found }
85 | await docker(`nodes/${NodeID}/update?version=${Version.Index}`, 'POST', Spec)
86 |
87 | console.log('node subnet is:', found)
88 |
89 | // this node should now have a label called "subnet"
90 |
91 | // VERIFY (DEV)
92 | // let { Spec: tmp } = await node(NodeID)
93 | // console.log(tmp)
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | /** The Internal IP of a Agent. */
2 | export type DNS = string
3 |
4 | /** The name of a Docker Service. */
5 | export type ServiceName = string
6 |
7 | export interface CPUUsage {
8 | cpu: number
9 | time: number
10 | }
11 |
12 | export interface AutoscalerSettings {
13 | min: number
14 | max: number
15 | up: { cpu: number }
16 | down: { cpu: number }
17 | }
18 |
19 | export interface Tasks {
20 | /** For what Service is this task. */
21 | service: ServiceName
22 | /** What is the task? */
23 | name: 'SCALE_UP' | 'SCALE_DOWN'
24 | /** Current CPU usage. */
25 | cpuUsage: CPUUsage
26 | /** Autoscaler options (if available). */
27 | autoscaler?: AutoscalerSettings
28 | }
29 |
--------------------------------------------------------------------------------
/src/www/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yandeu/docker-swarm-visualizer/6ec0129b4d2ff8a82198f961388dfafa9d4187c8/src/www/favicon.ico
--------------------------------------------------------------------------------
/src/www/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Developed and maintained by @yandeu
15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/www/js/elements.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Yannick Deubel (https://github.com/yandeu) 3 | * @copyright Copyright (c) 2021 Yannick Deubel 4 | * @license {@link https://github.com/yandeu/docker-swarm-visualizer/blob/main/LICENSE LICENSE} 5 | */ 6 | 7 | import { toPercent, toMb, toGb, ipToId, calculateCPUUsage, toMiB } from './misc.js' 8 | 9 | // keep track of services 10 | const services: string[] = [] 11 | // keep track of deleted containers 12 | const deletedContainers: string[] = [] 13 | 14 | const node = node => { 15 | const { Addr, Role, Availability, State, Hostname} = node 16 | 17 | const status = State === 'down' ? 'red' : 'yellow blink' 18 | 19 | const placeholders = `
7 | Drop Here
8 |
9 |