├── LICENSE ├── Makefile ├── README.md ├── assets ├── gif │ ├── failover.gif │ ├── multi-host-networking.gif │ ├── quickstart.gif │ ├── runscript-deploy.each.gif │ ├── runscript-deploy.once.gif │ ├── runscript-deploy.random.gif │ ├── runscript-deploy.scale.1.gif │ ├── runscript-deploy.scale.2.gif │ └── runscript-deploy.single.gif └── img │ ├── Imagotype.png │ ├── Isotype.png │ ├── Logo.png │ ├── Slogan.png │ └── architecture.png ├── jet.sh ├── littlejet.sh ├── make.sh └── share ├── littlejet ├── files │ ├── cpignore │ ├── default.conf │ ├── lib.subr │ └── user.conf └── runscripts │ ├── deploy.all │ ├── deploy.all.seq │ ├── deploy.each │ ├── deploy.once │ ├── deploy.random │ ├── deploy.scale │ ├── deploy.single │ ├── vpn.wg.client │ ├── vpn.wg.client.destroy │ ├── vpn.wg.load-balancer.pen │ ├── vpn.wg.load-balancer.pen.destroy │ ├── vpn.wg.server │ └── vpn.wg.server.destroy └── man ├── man1 └── littlejet.1 └── man5 └── littlejet.conf.5 /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, DtxdF 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MKDIR?=mkdir -p 2 | INSTALL?=install 3 | SED?=sed -i '' 4 | RM?=rm 5 | PREFIX?=/usr/local 6 | FIND?=find 7 | MANDIR?=${PREFIX}/share/man 8 | MANPAGES=man1/littlejet.1 \ 9 | man5/littlejet.conf.5 10 | 11 | LITTLEJET_VERSION?=0.2.0 12 | 13 | all: install 14 | 15 | install: 16 | ${MKDIR} -m 755 -p "${DESTDIR}${PREFIX}/bin" 17 | ${MKDIR} -m 755 -p "${DESTDIR}${PREFIX}/share" 18 | ${MKDIR} -m 755 -p "${DESTDIR}${PREFIX}/share/littlejet" 19 | ${MKDIR} -m 755 -p "${DESTDIR}${MANDIR}/man1" 20 | ${MKDIR} -m 755 -p "${DESTDIR}${MANDIR}/man5" 21 | 22 | ${INSTALL} -m 555 jet.sh "${DESTDIR}${PREFIX}/bin/jet" 23 | ${INSTALL} -m 555 littlejet.sh "${DESTDIR}${PREFIX}/bin/littlejet" 24 | 25 | # files 26 | ${MKDIR} -m 755 -p "${DESTDIR}${PREFIX}/share/littlejet/files" 27 | ${FIND} share/littlejet/files -mindepth 1 -exec ${INSTALL} -m 444 {} "${DESTDIR}${PREFIX}/{}" \; 28 | 29 | # RunScripts 30 | ${MKDIR} -m 755 -p "${DESTDIR}${PREFIX}/share/littlejet/runscripts" 31 | ${FIND} share/littlejet/runscripts -mindepth 1 -exec ${INSTALL} -m 555 {} "${DESTDIR}${PREFIX}/{}" \; 32 | 33 | # Version 34 | ${SED} -e 's|%%LITTLEJET_VERSION%%|${LITTLEJET_VERSION}|' "${DESTDIR}${PREFIX}/bin/jet" 35 | 36 | # man pages 37 | .for manpage in ${MANPAGES} 38 | ${INSTALL} -m 444 share/man/${manpage} "${DESTDIR}${MANDIR}/${manpage}" 39 | .endfor 40 | 41 | # Prefix 42 | .for f in share/littlejet/files/default.conf bin/jet bin/littlejet share/man/man1/littlejet.1 share/man/man5/littlejet.conf.5 43 | ${SED} -i '' -e 's|%%PREFIX%%|${PREFIX}|' "${DESTDIR}${PREFIX}/${f}" 44 | .endfor 45 | 46 | uninstall: 47 | ${RM} -f "${DESTDIR}${PREFIX}/bin/jet" 48 | ${RM} -rf "${DESTDIR}${PREFIX}/share/littlejet" 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | ---- 6 | 7 | # LittleJet - Create, deploy, manage and scale FreeBSD jails anywhere 8 | 9 | LittleJet is an open source, easy-to-use orchestrator for managing, deploying, scaling and interconnecting FreeBSD jails anywhere in the world. 10 | 11 | ## Quickstart 12 | 13 |

14 | 15 |
16 | Just a few commands and you will get deployed the project on all nodes. 17 |

18 | 19 | ```sh 20 | git clone https://github.com/DtxdF/hello-http 21 | cd ./hello-http/ 22 | jet create hello 23 | jet add-node 24 | jet add-node # optional 25 | jet add-node # optional 26 | jet run-script -p hello deploy.all 27 | jet show hello 28 | jet run-appjail -Pp hello cmd jexec hello-http fetch -qo - http://localhost 29 | jet destroy hello 30 | ``` 31 | 32 | ## How LittleJet works: architecture 33 | 34 |

35 | 36 |
37 | Sample architecture: Load balancing two web server replicas on nodes #1 and #2. 38 |

39 | 40 | Although it may be much more basic than the image above, showing all the toys is better to demonstrate what you can do with LittleJet. 41 | 42 | In the image above there are four nodes. The first and second nodes have a replica of the same web server, so they provide the same service. The fourth node provides the load balancing software. The question is: how does the load balancer on the fourth node send and receive packets to and from the first and second nodes? Easy: They are all connected to the same VPN server on the third node, but the difference is how they connect to that node. The first and second nodes use a jail called *connector* that has the VPN client and some packet filter rules configured to forward packets to the web server, so nodes on the same VPN can make HTTP requests. The load balancer itself has two pieces of software, the load balancer and the VPN client, and you only need to have all the connector's IP addresses to load balance them all. 43 | 44 | All of these pieces are created, configured and deployed using LittleJet with just a few commands from Manager, the host that can connect to all nodes in the cluster. 45 | 46 | ## What you can do with LittleJet: features 47 | 48 | ### Projects instead of jails 49 | 50 | Instead of simply dealing with jails, we exploit the concept of [Director](https://github.com/DtxdF/director) projects. A project is simply a group of jails and this is very useful because you can deploy one or more jails on the same node to take advantage of locality. Also, since there are many projects already created, you can simply copy them, edit them to suit your environment, and simply deploy them. 51 | 52 | ### RunScripts 53 | 54 | We can just strictly implement all the things in LittleJet, such as the connector, load balancer, deployment algorithms, etc. There is another way to implement those things: through RunScripts. 55 | 56 | A RunScript is a form of automation that LittleJet uses to perform more tasks than it was initially designed for. In this way, LittleJet is very modular and can be integrated with any other system. Even better: you don't need to write a RunScript in the POSIX shell, you can use the language of your choice, for example, Python, Golang, Rust, etc. 57 | 58 | Some RunScripts already implemented: 59 | 60 |

61 | 62 |
63 | deploy.random: Deploy a project to a randomly chosen node. 64 |

65 | 66 |

67 | 68 |
69 | deploy.once: Deploy a project to a node if it is not already deployed to any of them. 70 |

71 | 72 |

73 | 74 |
75 | deploy.each: For each run, deploy to any of the nodes. 76 |

77 | 78 |

79 | 80 |
81 | deploy.single: Deploy a project to the given node. 82 |

83 | 84 | ### Scaling 85 | 86 | An orchestrator that cannot automatically scale to other nodes is not that useful. LittleJet scales your project easily and effortlessly with —surprise— a RunScript. 87 | 88 |

89 | 90 |
91 | Deploying a project with a minimum of two replicas. 92 |

93 | 94 | Very simple, but in real life the web server will be overloaded, can LittleJet auto-scale the project using jail or project metrics? 95 | 96 |

97 | 98 |
99 | Yes! 100 |

101 | 102 | ### Load balancing / Failover / Multi-host networking 103 | 104 | You say that you have a replica of a web server on many nodes around the world, in several countries, but you want to access it using your favorite web browser on your laptop. 105 | 106 |

107 | 108 |
109 | Load balancing three replicas of a web server. 110 |

111 | 112 |

113 | 114 |
115 | Failover. 116 |

117 | 118 | ### And much more... 119 | 120 | LittleJet is very, very simple: it depends on the lower layers to do its job, i.e. it depends on [AppJail](https://github.com/DtxdF/AppJail), [Director](https://github.com/DtxdF/director), etc., so check out those projects to see what crazy combinations you can make. 121 | 122 | ## Dependencies 123 | 124 | ### Manager 125 | 126 | * [textproc/jq](https://freshports.org/textproc/jq) 127 | * [sysutils/cpdup](https://freshports.org/sysutils/cpdup) 128 | * [textproc/sansi](https://freshports.org/textproc/sansi) 129 | 130 | ### Nodes 131 | 132 | * [sysutils/cpdup](https://freshports.org/sysutils/cpdup) 133 | * [sysutils/py-director](https://freshports.org/sysutils/py-director) 134 | * [sysutils/appjail](https://freshports.org/sysutils/appjail) or [sysutils/appjail-devel](https://freshports.org/sysutils/appjail-devel) 135 | 136 | ## Documentation 137 | 138 | * [wiki](https://github.com/DtxdF/LittleJet/wiki) 139 | * `man 1 littlejet` 140 | * `man 5 littlejet.conf` 141 | 142 | ## Recommendations 143 | 144 | Configuring each node can be painful if there are a lot of nodes, so use a tool like Ansible or Puppet to suit your environment. 145 | 146 | ## Contributing 147 | 148 | Here is a list of some things you can contribute to LittleJet: 149 | 150 | * Report or fix bugs. 151 | * Create a new RunScript. You don't need to submit a PR to this repository, you can create your own repository and share it, so I can create a new section on the wiki called *"User RunScripts"*. Of course, if you want to send me a PR with your RunScript, I have no problem. 152 | * Contribute to projects this project depends on, such as [AppJail](https://github.com/DtxdF/AppJail) or [Director](https://github.com/DtxdF/director). 153 | * ... 154 | 155 | ## Notes 156 | 157 | 1. `BatchMode` is set to `yes`, which means, quoting an excerpt from `ssh_config(5)`, *“... user interaction, such as password prompt and host key confirmation prompts, will be disabled.“* 158 | 159 | If you have your SSH private key with a password, use `ssh-add(1)` and `ssh-agent(1)` before using LittleJet. 160 | 2. The `-t` parameter of `ssh(1)` is set, which means that if you want to process some text, you cannot do so because the text will be mangled. This note is when using one of the `run-*` subcommands. A simple workaround is the `-C` flag in one of the `run-*` subcommands that use sansi to remove such control characters. 161 | 4. If you installed Director using pipx, note that it cannot be used over SSH when installed in `~/.local/bin/appjail-director` because `~/.local/bin` is not yet in the PATH environment variable that is loaded by `~/.profile`: 162 | 163 | ```sh 164 | $ ssh which appjail-director 165 | $ echo $? 166 | 1 167 | $ ssh ls .local/bin/appjail-director 168 | .local/bin/appjail-director 169 | $ echo $? 170 | 0 171 | ``` 172 | 173 | A simple workaround is to add the following script in your `/usr/local/bin`. 174 | 175 | Note that this is not necessary when installed using [sysutils/py-director](https://freshports.org/sysutils/py-director). 176 | 5. LittleJet is designed to run as a non-root user, but on the remote site, AppJail needs privileges. If you are not using root on the remote site, [configure AppJail to use a trusted user](https://appjail.readthedocs.io/en/latest/trusted-users/). 177 | 6. Do not put your volumes in the same directory as the project because they can be overwritten when redeploying or simply destroyed when destroying a project. Use an external directory on each node. 178 | 7. The remote user must use the `sh(1)` shell. 179 | 8. Allowed characters: 180 | 181 | - Labels: `^[a-z][a-z0-9]*((\.|-)?[a-z][a-z0-9]*)*$` 182 | - Nodes: `^[a-zA-Z0-9._@-]+'` 183 | - Projects: `^[a-zA-Z0-9._-]+$` 184 | 9. Keep in-sync AppJail, Director and LittleJet. 185 | -------------------------------------------------------------------------------- /assets/gif/failover.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DtxdF/LittleJet/342e8abd015e84c0f9f39ce0fe4113fc8afdcbe5/assets/gif/failover.gif -------------------------------------------------------------------------------- /assets/gif/multi-host-networking.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DtxdF/LittleJet/342e8abd015e84c0f9f39ce0fe4113fc8afdcbe5/assets/gif/multi-host-networking.gif -------------------------------------------------------------------------------- /assets/gif/quickstart.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DtxdF/LittleJet/342e8abd015e84c0f9f39ce0fe4113fc8afdcbe5/assets/gif/quickstart.gif -------------------------------------------------------------------------------- /assets/gif/runscript-deploy.each.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DtxdF/LittleJet/342e8abd015e84c0f9f39ce0fe4113fc8afdcbe5/assets/gif/runscript-deploy.each.gif -------------------------------------------------------------------------------- /assets/gif/runscript-deploy.once.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DtxdF/LittleJet/342e8abd015e84c0f9f39ce0fe4113fc8afdcbe5/assets/gif/runscript-deploy.once.gif -------------------------------------------------------------------------------- /assets/gif/runscript-deploy.random.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DtxdF/LittleJet/342e8abd015e84c0f9f39ce0fe4113fc8afdcbe5/assets/gif/runscript-deploy.random.gif -------------------------------------------------------------------------------- /assets/gif/runscript-deploy.scale.1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DtxdF/LittleJet/342e8abd015e84c0f9f39ce0fe4113fc8afdcbe5/assets/gif/runscript-deploy.scale.1.gif -------------------------------------------------------------------------------- /assets/gif/runscript-deploy.scale.2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DtxdF/LittleJet/342e8abd015e84c0f9f39ce0fe4113fc8afdcbe5/assets/gif/runscript-deploy.scale.2.gif -------------------------------------------------------------------------------- /assets/gif/runscript-deploy.single.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DtxdF/LittleJet/342e8abd015e84c0f9f39ce0fe4113fc8afdcbe5/assets/gif/runscript-deploy.single.gif -------------------------------------------------------------------------------- /assets/img/Imagotype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DtxdF/LittleJet/342e8abd015e84c0f9f39ce0fe4113fc8afdcbe5/assets/img/Imagotype.png -------------------------------------------------------------------------------- /assets/img/Isotype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DtxdF/LittleJet/342e8abd015e84c0f9f39ce0fe4113fc8afdcbe5/assets/img/Isotype.png -------------------------------------------------------------------------------- /assets/img/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DtxdF/LittleJet/342e8abd015e84c0f9f39ce0fe4113fc8afdcbe5/assets/img/Logo.png -------------------------------------------------------------------------------- /assets/img/Slogan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DtxdF/LittleJet/342e8abd015e84c0f9f39ce0fe4113fc8afdcbe5/assets/img/Slogan.png -------------------------------------------------------------------------------- /assets/img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DtxdF/LittleJet/342e8abd015e84c0f9f39ce0fe4113fc8afdcbe5/assets/img/architecture.png -------------------------------------------------------------------------------- /littlejet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . "%%PREFIX%%/bin/jet" 4 | -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Script designed to be run for development purposes only. 5 | # 6 | 7 | "${SUEXEC:-doas}" make LITTLEJET_VERSION=`make -V LITTLEJET_VERSION`+`git rev-parse HEAD` 8 | -------------------------------------------------------------------------------- /share/littlejet/files/cpignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /share/littlejet/files/default.conf: -------------------------------------------------------------------------------- 1 | # Home directory. 2 | UID=`id -u` 3 | HOMEDIR=`getent passwd ${UID} | cut -d: -f6` || exit $? 4 | 5 | if [ ! -d "${HOMEDIR}" ]; then 6 | err "Cannot find home directory '${HOMEDIR}'" 7 | fi 8 | 9 | PREFIX="%%PREFIX%%" 10 | SHAREDIR="${PREFIX}/share/littlejet" 11 | FILESDIR="${SHAREDIR}/files" 12 | DATADIR="${PREFIX}/littlejet" 13 | LIB_SUBR="${FILESDIR}/lib.subr" 14 | 15 | LITTLEJETDIR="${HOMEDIR}/.littlejet" 16 | PROJECTSDIR="${LITTLEJETDIR}/projects" 17 | RUNSCRIPTS="${LITTLEJETDIR}/runscripts ${SHAREDIR}/runscripts" 18 | SOCKETSDIR="${LITTLEJETDIR}/sockets" 19 | NODESDIR="${LITTLEJETDIR}/nodes" 20 | CONTROLPATH="%r.%h.%p" 21 | CONTROLPERSIST="8m" 22 | CPIGNORE="${FILESDIR}/cpignore" 23 | DEBUG="NO" 24 | REMOTE_DATADIR="${DATADIR}" 25 | REMOTE_PROJECTSDIR="${REMOTE_DATADIR}/projects" 26 | NCPU=`sysctl -n hw.ncpu` 27 | SSH_LOGLEVEL="ERROR" 28 | SHOW_HEALTHCHECKERS=0 29 | SHOW_LIMITS=0 30 | SHOW_STATS=0 31 | -------------------------------------------------------------------------------- /share/littlejet/files/lib.subr: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024, Jesús Daniel Colmenares Oviedo 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | # 8 | # * Redistributions of source code must retain the above copyright notice, this 9 | # list of conditions and the following disclaimer. 10 | # 11 | # * Redistributions in binary form must reproduce the above copyright notice, 12 | # this list of conditions and the following disclaimer in the documentation 13 | # and/or other materials provided with the distribution. 14 | # 15 | # * Neither the name of the copyright holder nor the names of its 16 | # contributors may be used to endorse or promote products derived from 17 | # this software without specific prior written permission. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | set -T 31 | 32 | # Default name. 33 | NAME="main" 34 | 35 | # Colors. 36 | COLOR_DEFAULT="\033[39;49m" 37 | COLOR_RED="\033[0;31m" 38 | COLOR_LIGHT_YELLOW="\033[0;93m" 39 | COLOR_LIGHT_BLUE="\033[0;94m" 40 | COLOR_GRAY="\033[0;90m" 41 | 42 | # See sysexits(3). 43 | EX_OK=0 44 | EX_USAGE=64 45 | EX_DATAERR=65 46 | EX_NOINPUT=66 47 | EX_NOUSER=67 48 | EX_NOHOST=68 49 | EX_UNAVAILABLE=69 50 | EX_SOFTWARE=70 51 | EX_OSERR=71 52 | EX_OSFILE=72 53 | EX_CANTCREAT=73 54 | EX_IOERR=74 55 | EX_TEMPFAIL=75 56 | EX_PROTOCOL=76 57 | EX_NOPERM=77 58 | EX_CONFIG=78 59 | 60 | # usage: 61 | # setname 62 | # args: 63 | # : Module name. 64 | # description: 65 | # The name is displayed for each execution of logging functions, such as debug, 66 | # info, err, and warn. 67 | # 68 | setname() 69 | { 70 | NAME="$1" 71 | } 72 | 73 | # usage: 74 | # checkdependency 75 | # description: 76 | # Check if a program can be located using the PATH environment variable and if not 77 | # it exists with exit status EX_UNAVAILABLE. 78 | # 79 | checkdependency() 80 | { 81 | if ! which -s "$1"; then 82 | err "$1: dependency required but cannot be found." 83 | exit ${EX_UNAVAILABLE} 84 | fi 85 | } 86 | 87 | # usage: 88 | # checklabelname 89 | # description: 90 | # Check if a label is valid.. 91 | # 92 | checklabelname() 93 | { 94 | if printf "%s" "$1" | grep -qEe '^[a-z][a-z0-9]*((\.|-)?[a-z][a-z0-9]*)*$'; then 95 | return 0 96 | else 97 | return 1 98 | fi 99 | } 100 | 101 | # usage: 102 | # checklabel