├── examples ├── MinimalVesselFile ├── dinnerrepo │ ├── deploydir │ │ └── dinnerrepo.ini │ ├── README.md │ ├── DinnerVesselFile │ ├── lighttpd │ │ ├── scgi.conf │ │ └── modules.conf │ ├── Makefile │ └── rc.d │ │ └── fossil ├── DevelopmentVesselFile ├── VesselFile.example └── AnsibleVesselFile ├── test ├── jail_options.ini ├── dataset.ini ├── shutdown.ini ├── DeleteFilesVesselFile ├── all.tcl ├── resource.ini ├── s3_repo.test ├── deploydir │ └── postgresql-vessel.ini ├── rctl.test ├── jail.test ├── ini.test ├── run.test ├── filerepo.test └── broken_tests │ └── metadata_db.test ├── src ├── native_deprecated │ ├── src │ │ ├── CMakeLists.txt │ │ ├── app │ │ │ ├── CMakeLists.txt │ │ │ ├── cmdline.h │ │ │ ├── environment.h │ │ │ ├── app_functions.h │ │ │ ├── environment.cpp │ │ │ ├── cmdline.cpp │ │ │ ├── app_containers.cpp │ │ │ └── app_functions.cpp │ │ ├── lib │ │ │ ├── CMakeLists.txt │ │ │ ├── appc_tcl.h │ │ │ ├── container.h │ │ │ ├── mountpoint.h │ │ │ ├── image_archive.h │ │ │ ├── appc_tcl.cpp │ │ │ ├── fs.h │ │ │ ├── container.cpp │ │ │ └── mountpoint.cpp │ │ └── util │ │ │ ├── CMakeLists.txt │ │ │ ├── appc_save.cpp │ │ │ ├── appc_mount.cpp │ │ │ └── appc_compress.cpp │ ├── test │ │ ├── CMakeLists.txt │ │ └── fs_path_test.cpp │ └── CMakeLists.txt ├── lib │ ├── native │ │ ├── devctl.h │ │ ├── url_cmd.h │ │ ├── exec.h │ │ ├── tcl_util.cpp │ │ ├── tcl_kqueue.h │ │ ├── udp_tcl.h │ │ ├── tcl_util.h │ │ ├── tcl_kqueue.cpp │ │ └── pty.cpp │ └── tcl │ │ ├── name-gen.tcl │ │ ├── definition_file.tcl │ │ ├── pkgIndex.tcl │ │ ├── pty_shell.tcl │ │ ├── environment.tcl │ │ ├── bsd.tcl │ │ ├── syslog.tcl │ │ ├── build.tcl │ │ ├── export.tcl │ │ ├── vesseld_client.tcl │ │ ├── deploy.tcl │ │ └── import.tcl ├── network │ ├── notes.md │ └── README.md ├── rc.d │ └── vessel ├── dns │ ├── embdns.h │ └── dns.tcl └── apps │ └── vessel ├── port ├── pkg-descr ├── port-helper.mk ├── Makefile └── pkg-plist ├── dev.sh ├── prototypes ├── apply.tcl ├── devctl_alert.tcl ├── pty_exec.tcl ├── inifile.tcl ├── container-example.json ├── createwithnamespace.tcl ├── logging.tcl ├── appcd.rc ├── coroutines_yieldto.tcl ├── network.tcl ├── interp.tcl ├── prototype.ini ├── constructor_error.tcl ├── jailconf.tcl ├── zfs_diff_with_hardlinks.tcl ├── oo.tcl ├── rctl_devd.cpp ├── ctty_read.tcl ├── ctty_read.cpp ├── ctty.cpp ├── kqueue_process_track.cpp └── appcd ├── .gitignore ├── docs ├── BuildAndInstall.md ├── FeatureIdeas.md ├── QuickStart.md ├── ResourceControl.md ├── Supervisor.md ├── ImageCreation.md └── ExamplesTipsAndTricks.md ├── util ├── generate-name ├── create-network.tcl ├── native │ └── url_test.cpp ├── join-network └── dns-serve ├── pm └── whatsnext.md ├── CMakeLists.txt └── README.md /examples/MinimalVesselFile: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | 3 | FROM FreeBSD:12.2-RELEASE 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/jail_options.ini: -------------------------------------------------------------------------------- 1 | [vessel-supervisor] 2 | 3 | [jail] 4 | allow.raw_sockets=1 5 | osrelease="Shane OS" 6 | host.hostname="re.zeta.investments" -------------------------------------------------------------------------------- /src/native_deprecated/src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include_directories(lib) 2 | 3 | add_subdirectory(lib) 4 | add_subdirectory(app) 5 | add_subdirectory(util) 6 | -------------------------------------------------------------------------------- /test/dataset.ini: -------------------------------------------------------------------------------- 1 | [vessel-supervisor] 2 | 3 | [dataset:upload-images] 4 | dataset=zroot/images 5 | mount=/images 6 | 7 | [dataset:logs] 8 | dataset=zroot/logs 9 | mount=/logs -------------------------------------------------------------------------------- /src/lib/native/devctl.h: -------------------------------------------------------------------------------- 1 | #ifndef DEVCTL_CHANNEL_H 2 | #define DEVCTL_CHANNEL_H 3 | 4 | #include 5 | 6 | int Vessel_DevCtlInit(Tcl_Interp* interp); 7 | 8 | #endif // DEVCTL_CHANNEL_H 9 | -------------------------------------------------------------------------------- /src/native_deprecated/src/app/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(vessel 2 | app_containers.cpp 3 | app_functions.cpp 4 | cmdline.cpp 5 | environment.cpp 6 | ) 7 | target_link_libraries(vessel vessel-priv archive) 8 | -------------------------------------------------------------------------------- /src/native_deprecated/test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | enable_testing() 2 | 3 | include_directories(${CMAKE_SOURCE_DIR}/src/lib) 4 | 5 | add_executable(fs_path_test fs_path_test.cpp) 6 | target_link_libraries(fs_path_test vessel-priv) 7 | -------------------------------------------------------------------------------- /test/shutdown.ini: -------------------------------------------------------------------------------- 1 | #ini file for manually testing shutdown 2 | #Can be run with: 3 | #sudo -E vessel run --ini=test/shutdown.ini --rm minimal:14.2-RELEASE -- sh /etc/rc 4 | 5 | [jail] 6 | exec.stop="sh /etc/rc.shutdown jail" 7 | -------------------------------------------------------------------------------- /src/native_deprecated/src/lib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(vessel-priv STATIC 2 | vessel_tcl.cpp 3 | container.cpp 4 | fs.cpp 5 | image_archive.cpp 6 | mountpoint.cpp) 7 | target_link_libraries(vessel-priv ${TCL_LIBRARY}) 8 | -------------------------------------------------------------------------------- /examples/dinnerrepo/deploydir/dinnerrepo.ini: -------------------------------------------------------------------------------- 1 | [vessel-supervisor] 2 | repository=s3://appc-test1 3 | image=dinner 4 | tag=1.0 5 | command=sh /etc/rc 6 | 7 | [dataset:fossil] 8 | dataset=vessel/volumes/dinnerrepo 9 | mount=/home/dinner/repo -------------------------------------------------------------------------------- /port/pkg-descr: -------------------------------------------------------------------------------- 1 | Vessel is another freebsd container manager. It contains 2 | some unique features like process tracing to detect jail 3 | status. rctl integration including custom actions. 4 | A jail supervisor to manage jails etc. 5 | -------------------------------------------------------------------------------- /src/lib/native/url_cmd.h: -------------------------------------------------------------------------------- 1 | #ifndef URL_CMD_H 2 | #define URL_CMD_H 3 | 4 | #include 5 | 6 | int Vessel_ParseURL(void *clientData, Tcl_Interp *interp, 7 | int objc, struct Tcl_Obj *const *objv); 8 | #endif // URL_CMD_H 9 | -------------------------------------------------------------------------------- /test/DeleteFilesVesselFile: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | 3 | FROM "FreeBSD:12.3-RELEASE" 4 | 5 | RUN env ASSUME_ALWAYS_YES=yes pkg install tcl86 6 | 7 | RUN rm -rvf /usr/share/man 8 | RUN rm -rvf /usr/local/share/man 9 | -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | #Setup the development environment by sourcing this script 2 | 3 | export TCLLIBPATH=$PWD/src/lib/tcl 4 | export LD_LIBRARY_PATH=$PWD/build 5 | export PATH=$PWD/src/apps:$PATH 6 | ln -sf $PWD/build/libvesseltcl.so $PWD/src/lib/tcl/libvesseltcl.so 7 | -------------------------------------------------------------------------------- /examples/DevelopmentVesselFile: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | 3 | FROM FreeBSD:12.1-RELEASE 4 | 5 | RUN env ASSUME_ALWAYS_YES=yes pkg update 6 | RUN env ASSUME_ALWAYS_YES=yes pkg install tcllib curl bash cmake py37-sphinx_rtd_theme 7 | -------------------------------------------------------------------------------- /prototypes/apply.tcl: -------------------------------------------------------------------------------- 1 | proc test {} { 2 | 3 | set test_var jack 4 | set traced_pwd [pwd] 5 | trace add variable traced_pwd unset [list apply {{tv name1 name2 op} { 6 | puts stderr "$name1,$name2,$op,$tv" 7 | }} $test_var] 8 | } 9 | 10 | test 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *\# 3 | .project 4 | .settings 5 | .buildpath 6 | build 7 | port/work 8 | vessel.cflags 9 | vessel.config 10 | vessel.creator 11 | vessel.creator.user 12 | vessel.cxxflags 13 | vessel.files 14 | vessel.includes 15 | src/lib/tcl/libvesseltcl.so 16 | -------------------------------------------------------------------------------- /test/all.tcl: -------------------------------------------------------------------------------- 1 | package require tcltest 2 | namespace import ::tcltest::* 3 | 4 | testConstraint root [expr {$env(USER) eq "root"}] 5 | testConstraint aws [expr {[info exists env(VESSEL_TEST_AWS)] && $env(VESSEL_TEST_AWS)}] 6 | 7 | configure -errfile error.log 8 | runAllTests -------------------------------------------------------------------------------- /prototypes/devctl_alert.tcl: -------------------------------------------------------------------------------- 1 | package require vessel::native 2 | package require vessel::bsd 3 | 4 | proc bgerror {msg} { 5 | puts "bgerror: $msg" 6 | } 7 | 8 | 9 | vessel::devctl_set_callback vessel::bsd::parse_devd_rctl_str 10 | 11 | set _forever_ {} 12 | vwait _forever_ 13 | -------------------------------------------------------------------------------- /src/native_deprecated/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.10.1) 2 | project (AppC) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | find_package(TCL) 8 | include_directories(${TCL_INCLUDE_PATH}) 9 | 10 | add_subdirectory(src) 11 | add_subdirectory(test) 12 | -------------------------------------------------------------------------------- /examples/VesselFile.example: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | 3 | FROM FreeBSD:12.1-RELEASE 4 | 5 | CWD / 6 | 7 | COPY . /app 8 | 9 | #EXPOSE 80 10 | 11 | #Run in a jail when building 12 | RUN env ASSUME_ALWAYS_YES=yes pkg install bash uwsgi curl 13 | RUN env ASSUME_ALWAYS_YES=yes pkg install python 14 | RUN bash 15 | -------------------------------------------------------------------------------- /test/resource.ini: -------------------------------------------------------------------------------- 1 | [resource:maxruntime] 2 | rctl=wallclock:devctl=20 3 | devctl-action=exec=logger -t vesseltest vessel is awesome 4 | 5 | # Because of the way we implement resource control, have two 6 | # resource sections of the same resource (wallclock in this example) 7 | # both will always execute. 8 | [resource:maxruntime2] 9 | rctl=wallclock:devctl=30 10 | devctl-action=shutdown -------------------------------------------------------------------------------- /prototypes/pty_exec.tcl: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env tclsh8.6 2 | 3 | package require appc::native 4 | 5 | set pty_slave_path [lindex $argv 0] 6 | 7 | set pty_chan [open $pty_slave_path RDWR] 8 | 9 | set fd_dict [dict create stdin $pty_chan stdout $pty_chan stderr $pty_chan] 10 | set callback [list puts "shane is awesome"] 11 | 12 | appc::exec $fd_dict $callback {bash} 13 | 14 | vwait __forever__ 15 | -------------------------------------------------------------------------------- /prototypes/inifile.tcl: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env tclsh8.6 2 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 3 | 4 | package require inifile 5 | 6 | set handle [ini::open [file join . prototype.ini]] 7 | 8 | puts [ini::sections $handle] 9 | 10 | puts [ini::keys $handle vessel-supervisor] 11 | 12 | set d [ini::get $handle resource:maxmemoryuse] 13 | 14 | dict for {k v} $d { 15 | puts "$k => $v" 16 | } 17 | -------------------------------------------------------------------------------- /examples/AnsibleVesselFile: -------------------------------------------------------------------------------- 1 | FROM FreeBSD:13.3-RELEASE 2 | 3 | RUN pw useradd -n ec2-user -m -g wheel 4 | RUN env ASSUME_ALWAYS_YES=yes pkg update 5 | RUN env ASSUME_ALWAYS_YES=yes pkg install python sudo 6 | 7 | RUN echo "Port 2222" >> /etc/ssh/sshd_config 8 | 9 | RUN mkdir /home/ec2-user/.ssh 10 | COPY $env(HOME)/.ssh/id_ecdsa_jail.pub /home/ec2-user/.ssh/authorized_keys 11 | 12 | RUN sysrc sshd_enable=YES 13 | 14 | RUN echo "%wheel ALL=(ALL) NOPASSWD: ALL" >> /usr/local/etc/sudoers 15 | -------------------------------------------------------------------------------- /port/port-helper.mk: -------------------------------------------------------------------------------- 1 | # - Run from top level directory 2 | # - Set DISTDIR=/tmp in environment 3 | # - Set _V=`git rev-parse HEAD` 4 | 5 | VERSION?=${_V} 6 | DISTDIR?=/tmp 7 | STAGEDIR?=FBSD 8 | 9 | distdir: .PHONY 10 | mkdir -p ${DISTDIR} 11 | 12 | #ssteidl-vessel-1.0-894fa8fe51401e3c8e10bc67fe86a0e21a63e2ae_GH0.tar.gz 13 | tarball: distdir 14 | git archive -v --format tgz --prefix vessel-${VERSION}/ -o ${DISTDIR}/ssteidl-vessel-${VERSION}_GH0.tar.gz ${VERSION} 15 | make -C port makesum 16 | -------------------------------------------------------------------------------- /prototypes/container-example.json: -------------------------------------------------------------------------------- 1 | { 2 | /*List of files to be deleted*/ 3 | "whiteout": [ 4 | "/path/to/file/1", 5 | "/path/to/file/2" 6 | ], 7 | 8 | /*Commands are executed serially on startup and can be 9 | * overridden via the commandline 10 | */ 11 | "commands": [ 12 | { 13 | /*Commands are NOT executed in a shell*/ 14 | "command": ["executable", "arg1", "arg2", "arg3"] 15 | }, 16 | { 17 | "command": ["executable", "arg1", "arg2", "arg3"] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/s3_repo.test: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | package require vessel::native 3 | package require vessel::env 4 | package require tcltest 5 | 6 | namespace import tcltest::* 7 | 8 | test appc-publish-s3-1 {Verify s3cmd put succeeds when publishing} -constraints aws -setup { 9 | set ::env(VESSEL_REPO_URL) s3://appc-test-1 10 | } -body { 11 | 12 | set minimal_appc_file [file join .. examples MinimalVesselFile] 13 | 14 | } -cleanup { 15 | set ::env(VESSEL_REPO_URL) {} 16 | } -returnCodes ok 17 | 18 | cleanupTests -------------------------------------------------------------------------------- /src/lib/native/exec.h: -------------------------------------------------------------------------------- 1 | #ifndef EXEC_H 2 | #define EXEC_H 3 | #include 4 | 5 | int Vessel_ExecInit(Tcl_Interp* interp); 6 | 7 | int Vessel_Exec_SetSignalHandler(void *clientData, Tcl_Interp *interp, 8 | int objc, struct Tcl_Obj *const *objv); 9 | 10 | int Vessel_Exec(void *clientData, Tcl_Interp *interp, 11 | int objc, struct Tcl_Obj *const *objv); 12 | 13 | int Vessel_Get_Supervisor_Ctrl_Channel(void *clientData, Tcl_Interp *interp, 14 | int objc, struct Tcl_Obj *const *objv); 15 | 16 | #endif // EXEC_H 17 | -------------------------------------------------------------------------------- /test/deploydir/postgresql-vessel.ini: -------------------------------------------------------------------------------- 1 | [vessel-supervisor] 2 | # The name of the image without the tag in the repository 3 | # This section is used to create the vessel commandline 4 | repository=s3://appc1-test/ 5 | image=redb 6 | tag=1.0 7 | command=sh /etc/rc 8 | restart=true 9 | start-delay-ms=5000 10 | 11 | [dataset:pgdata] 12 | dataset=zroot/volumes/redata 13 | mount=/var/db/postgres 14 | 15 | [cpuset] 16 | #The -l parameter to cpuset 17 | list=0,3 18 | 19 | [jail] 20 | # anything in this section is treated as a jail parameter and will 21 | # be added to the generated jail file. 22 | sysvshm=new -------------------------------------------------------------------------------- /port/Makefile: -------------------------------------------------------------------------------- 1 | PORTNAME= vessel 2 | _V?= 1.1.1 3 | DISTVERSION= ${_V} 4 | CATEGORIES= sysutils 5 | 6 | MAINTAINER= shane@zeta.investments 7 | COMMENT= FreeBSD specific container manager 8 | 9 | LICENSE= PD 10 | 11 | USES= cmake:noninja 12 | 13 | USE_GITHUB= yes 14 | GH_ACCOUNT= ssteidl 15 | GH_PROJECT= vessel 16 | GH_TAGNAME= ${DISTVERSION} 17 | #GH_TAGNAME!= git rev-parse HEAD 18 | 19 | LIB_DEPENDS+= libcurl.so:ftp/curl libtcl86.so:lang/tcl86 20 | RUN_DEPENDS+= zip:archivers/zip tcllib>=1.2:devel/tcllib s3cmd:net/py-s3cmd@${PY_FLAVOR} tclsyslog>=2.1:sysutils/tclsyslog 21 | 22 | .include 23 | -------------------------------------------------------------------------------- /src/native_deprecated/test/fs_path_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "fs.h" 4 | 5 | using namespace vessel; 6 | int main(int argc, char** argv) 7 | { 8 | Tcl_FindExecutable(argv[0]); 9 | 10 | fs_path path1 = std::string("/home/shane/"); 11 | std::cerr << "path1: " << path1.str() << std::endl; 12 | 13 | { 14 | fs_path path2 = path1; 15 | path2 += "jack/box"; 16 | std::cerr << "path2: " << path2.str() << std::endl; 17 | 18 | path2.append_extension("tgz"); 19 | std::cerr << "path2: " << path2.str() << std::endl; 20 | } 21 | 22 | Tcl_Exit(0); 23 | } 24 | -------------------------------------------------------------------------------- /port/pkg-plist: -------------------------------------------------------------------------------- 1 | bin/vessel 2 | bin/vessel-supervisor 3 | etc/rc.d/vessel 4 | lib/tclvessel/bsd.tcl 5 | lib/tclvessel/build.tcl 6 | lib/tclvessel/definition_file.tcl 7 | lib/tclvessel/deploy.tcl 8 | lib/tclvessel/environment.tcl 9 | lib/tclvessel/export.tcl 10 | lib/tclvessel/import.tcl 11 | lib/tclvessel/jail.tcl 12 | lib/tclvessel/libvesseltcl.so 13 | lib/tclvessel/metadata_db.tcl 14 | lib/tclvessel/name-gen.tcl 15 | lib/tclvessel/network/network.tcl 16 | lib/tclvessel/pkgIndex.tcl 17 | lib/tclvessel/repos.tcl 18 | lib/tclvessel/run.tcl 19 | lib/tclvessel/syslog.tcl 20 | lib/tclvessel/vessel_file_commands.tcl 21 | lib/tclvessel/zfs.tcl 22 | -------------------------------------------------------------------------------- /src/native_deprecated/src/app/cmdline.h: -------------------------------------------------------------------------------- 1 | #ifndef CMDLINE_H 2 | #define CMDLINE_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace vessel 9 | { 10 | 11 | struct commandline 12 | { 13 | public: 14 | std::string image; 15 | std::string container; 16 | std::vector container_cmd_args; 17 | std::string image_type; 18 | int operation_count; /*Used for validation that only one 19 | * operation is given.*/ 20 | bool do_save; 21 | 22 | static void usage(); 23 | 24 | static std::unique_ptr parse(int argc, char** argv); 25 | }; 26 | } 27 | #endif // CMDLINE_H 28 | -------------------------------------------------------------------------------- /prototypes/createwithnamespace.tcl: -------------------------------------------------------------------------------- 1 | package require TclOO 2 | package require defer 3 | 4 | oo::class create mytestclass { 5 | 6 | self export createWithNamespace 7 | 8 | constructor {} { 9 | puts stderr "constructor: [self]" 10 | puts stderr "[info object namespace [self]]" 11 | } 12 | 13 | destructor { 14 | 15 | puts stderr "Destroying [self]" 16 | } 17 | } 18 | 19 | proc testproc {} { 20 | namespace eval testproc {} 21 | mytestclass createWithNamespace test testproc 22 | mytestclass createWithNamespace test2 testproc 23 | defer::autowith { 24 | puts "deleting" 25 | namespace delete testproc 26 | } 27 | } 28 | 29 | testproc 30 | -------------------------------------------------------------------------------- /src/native_deprecated/src/util/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(APP_DIR ${CMAKE_SOURCE_DIR}/src/app) 2 | include_directories(${APP_DIR}) 3 | 4 | add_executable(vessel_mount 5 | vessel_mount.cpp 6 | ${APP_DIR}/app_functions.cpp 7 | ${APP_DIR}/cmdline.cpp 8 | ${APP_DIR}/environment.cpp 9 | ) 10 | target_link_libraries(vessel_mount vessel-priv archive jail) 11 | 12 | add_executable(vessel_save 13 | vessel_save.cpp 14 | ${APP_DIR}/app_functions.cpp 15 | ${APP_DIR}/cmdline.cpp 16 | ${APP_DIR}/environment.cpp 17 | ) 18 | target_link_libraries(vessel_save vessel-priv archive jail) 19 | 20 | add_executable(vessel_compress 21 | vessel_compress.cpp 22 | ) 23 | target_link_libraries(vessel_compress archive) 24 | -------------------------------------------------------------------------------- /test/rctl.test: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; -*- 2 | 3 | package require tcltest 4 | 5 | package require vessel::bsd 6 | 7 | namespace eval rctl::test { 8 | 9 | namespace import ::tcltest::* 10 | 11 | test parse-rctl-str-1 {Test an rctl string} -body { 12 | 13 | set rctl_str {!system=RCTL subsystem=rule type=matched rule=jail:f55687e0-8475-4e3e-ada9-1197ee857536:wallclock:devctl=5 pid=1273 ruid=0 jail=f55687e0-8475-4e3e-ada9-1197ee857536} 14 | set limit_dict [vessel::bsd::parse_devd_rctl_str $rctl_str] 15 | } -result {rule {subjectid f55687e0-8475-4e3e-ada9-1197ee857536 resource wallclock action devctl=5} pid 1273 ruid 0 jail f55687e0-8475-4e3e-ada9-1197ee857536} 16 | 17 | cleanupTests 18 | } -------------------------------------------------------------------------------- /prototypes/logging.tcl: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | 3 | package require logger 4 | 5 | package require Syslog 6 | 7 | set glog [logger::init vessel] 8 | 9 | namespace eval vessel::shane { 10 | logger::initNamespace [namespace current] debug 11 | variable log [logger::servicecmd [string trimleft [namespace current] :]] 12 | 13 | proc joe {} { 14 | variable log 15 | ${log}::debug "hello my friend" 16 | } 17 | } 18 | 19 | logger::setlevel debug 20 | 21 | proc logtosyslog_debug {txt} { 22 | syslog -facility local1 -ident [lindex $txt 1] debug [lindex $txt 2] 23 | } 24 | 25 | ${glog}::logproc debug logtosyslog_debug 26 | 27 | vessel::shane::joe 28 | -------------------------------------------------------------------------------- /src/native_deprecated/src/lib/appc_tcl.h: -------------------------------------------------------------------------------- 1 | #ifndef VESSEL_TCL_H 2 | #define VESSEL_TCL_H 3 | 4 | #include 5 | #include 6 | 7 | namespace vessel 8 | { 9 | 10 | void delete_interp(Tcl_Interp* interp); 11 | using interp_ptr = std::unique_ptr; 12 | interp_ptr create_tcl_interp(); 13 | 14 | struct tcl_obj_raii 15 | { 16 | Tcl_Obj* obj; 17 | 18 | tcl_obj_raii(Tcl_Obj* obj); 19 | tcl_obj_raii(const tcl_obj_raii& other); 20 | tcl_obj_raii(tcl_obj_raii&& other); 21 | tcl_obj_raii& operator=(const tcl_obj_raii& other); 22 | tcl_obj_raii& operator=(tcl_obj_raii&&); 23 | operator Tcl_Obj*() const; 24 | ~tcl_obj_raii(); 25 | }; 26 | 27 | } 28 | #endif // VESSEL_TCL_H 29 | -------------------------------------------------------------------------------- /src/native_deprecated/src/app/environment.h: -------------------------------------------------------------------------------- 1 | #ifndef ENVIRONMENT_H 2 | #define ENVIRONMENT_H 3 | 4 | #include "fs.h" 5 | 6 | namespace vessel 7 | { 8 | class environment 9 | { 10 | fs_path m_archive_dir; 11 | fs_path m_image_dir; 12 | fs_path m_container_dir; 13 | 14 | environment(environment&& other) = delete; 15 | environment& operator=(environment&& other) = delete; 16 | public: 17 | 18 | environment(); 19 | 20 | fs_path find_image(const std::string& image_name) const; 21 | fs_path find_container(const std::string& container_name) const; 22 | fs_path archive_dir() const; 23 | fs_path image_dir() const; 24 | }; 25 | 26 | } 27 | #endif // ENVIRONMENT_H 28 | -------------------------------------------------------------------------------- /prototypes/appcd.rc: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # PROVIDE: re 4 | # REQUIRE: DAEMON 5 | # KEYWORD: shutdown 6 | # 7 | # Add the following lines to /etc/rc.conf to enable re: 8 | # 9 | 10 | . /etc/rc.subr 11 | 12 | name="appcd" 13 | rcvar=appcd_enable 14 | 15 | load_rc_config $name 16 | 17 | : ${appcd_enable="YES"} 18 | : ${appcd_pidfile="/var/run/${name}.pid"} 19 | : ${appcd_pool="zpool"} 20 | 21 | pidfile=${appcd_pidfile} 22 | command="/usr/sbin/daemon" 23 | appcd_chdir=/var/db/appcd 24 | command_args="-t ${name} -S -p ${pidfile} /usr/local/sbin/appcd" 25 | sig_stop="INT" 26 | command_interpreter=tclsh8.6 27 | procname="/usr/local/sbin/appcd" 28 | start_precmd=start_precmd 29 | appcd_env="APPC_POOL=${appcd_pool}" 30 | start_precmd() 31 | { 32 | 33 | mkdir -p $appcd_chdir 34 | } 35 | 36 | run_rc_command "$1" 37 | -------------------------------------------------------------------------------- /src/native_deprecated/src/app/app_functions.h: -------------------------------------------------------------------------------- 1 | #ifndef APP_FUNCTIONS_H 2 | #define APP_FUNCTIONS_H 3 | 4 | #include "cmdline.h" 5 | #include "environment.h" 6 | #include "mountpoint.h" 7 | 8 | /* 9 | * The app functions are building blocks for 10 | * the different applications and utilities. 11 | * vessel will use all of them and the utilities 12 | * will generally use some subset. 13 | * 14 | * For example, vessel_mount will use 15 | * mount_container_image(). 16 | */ 17 | namespace vessel { namespace funcs { 18 | void auto_unmount(vessel::mountpoint* mnt); 19 | using auto_unmount_ptr = std::unique_ptr; 20 | 21 | auto_unmount_ptr mount_container_image(commandline& cmdline, environment& env); 22 | 23 | void save_container_image(commandline& cmdline, environment& env); 24 | }} 25 | 26 | #endif // APP_FUNCTIONS_H 27 | -------------------------------------------------------------------------------- /src/native_deprecated/src/lib/container.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTAINER_H 2 | #define CONTAINER_H 3 | 4 | #include "fs.h" 5 | 6 | #include 7 | 8 | #include 9 | extern "C" 10 | { 11 | #include 12 | } 13 | 14 | #include 15 | 16 | using sys_jail = struct jail; 17 | namespace vessel 18 | { 19 | class jail 20 | { 21 | sys_jail m_jail; 22 | 23 | jail(const jail& other) = delete; 24 | jail(jail&& other) = delete; 25 | jail& operator=(const jail& other) = delete; 26 | jail& operator=(jail&& other) = delete; 27 | public: 28 | jail(fs_path path, const std::string& hostname); 29 | 30 | // std::tuple fork_jail(); 31 | 32 | std::tuple fork_exec_jail(const std::vector& args); 33 | 34 | ~jail(); 35 | }; 36 | } 37 | 38 | #endif // CONTAINER_H 39 | -------------------------------------------------------------------------------- /examples/dinnerrepo/README.md: -------------------------------------------------------------------------------- 1 | # Context 2 | This example creates a [fossil](https://www2.fossil-scm.org/home/doc/trunk/www/index.wiki) repository served via a lighttpd reverse proxy with scgi 3 | protocol to the fossil server. 4 | 5 | # Notes 6 | Some important notes for this example: 7 | 8 | * A ZFS dataset is defined in the .ini runtime configuration file. This decouples the repository database from the container. In the production environment, the zfs dataset (and thus the fossil database) live on an EBS volume. 9 | * The fossil rc.d script is modified to create the repository if it doesn't exist. This was easier then adding first boot support for such a simple repo. It also changes the ownership of the repo mountpoint. 10 | * The Makefile demonstrates how to build, run, publish and deploy the container. Deploying to AWS is as simple as scp'ing a file to EC2 instance running vessel-supervisor. 11 | -------------------------------------------------------------------------------- /prototypes/coroutines_yieldto.tcl: -------------------------------------------------------------------------------- 1 | package require defer 2 | 3 | proc sub_coro {} { 4 | puts stderr "subcoro init: [info coroutine]" 5 | set cb [yield] 6 | 7 | puts stderr "sub coro after yield: $cb" 8 | after idle $cb 9 | } 10 | 11 | proc main_coro {arg1} { 12 | 13 | defer::with [list arg1] { 14 | puts stderr "main coro exiting: $arg1" 15 | } 16 | 17 | puts stderr "coro1 setup" 18 | 19 | coroutine m2 sub_coro 20 | after idle [info coroutine] 21 | puts "yield in main_coro" 22 | yield 23 | 24 | puts stderr "yielding to sub coro" 25 | yieldto m2 [info coroutine] 26 | 27 | puts stderr "After yieldto" 28 | after idle [list rename [info coroutine] {}] 29 | yield 30 | } 31 | 32 | proc main {} { 33 | 34 | set coro1 [coroutine m1 main_coro "my arg1"] 35 | 36 | puts stderr "after coro1 setup" 37 | } 38 | 39 | main 40 | vwait __forever__ 41 | -------------------------------------------------------------------------------- /docs/BuildAndInstall.md: -------------------------------------------------------------------------------- 1 | # Creating a FreeBSD Package 2 | Vessel has not yet been accepted into the FreeBSD port collection. However it is simple to build a package for installation. 3 | 4 | 1. Ensure that the ports tree is installed. Namely `/usr/ports/Mk/` needs to be available. 5 | 2. Create the tarball with HEAD of the cloned repository: ```sudo env DISTDIR=/tmp make -e -f port/port-helper.mk _V=`git rev-parse HEAD` tarball``` 6 | 3. Build dependencies and package: ```sudo env DISTDIR=/tmp make -e -C port _V=`git rev-parse HEAD` package``` 7 | 8 | # Building and Developing Vessel 9 | 10 | Vessel uses CMake as a build system. To develop vessel, a script `dev.sh` is used to setup the environment. `. dev.sh` will setup the environment so in-tree changes are picked up whenever `vessel` is invoked. 11 | 12 | 1. `mkdir build` 13 | 2. `cd build` 14 | 3. `cmake ..` 15 | 4. `make` 16 | 5. `cd ..` 17 | 6. `. dev.sh` 18 | -------------------------------------------------------------------------------- /src/network/notes.md: -------------------------------------------------------------------------------- 1 | # VLAN Bridging 2 | 3 | The strategy for creating network is: 4 | 5 | 1. Create a single bridge at startup. 6 | 2. Create a single epair interface for vesseld at startup. 7 | We can name the interface vesseld or something to make it 8 | obvious. 9 | 3. Any new network that is requested by the vesseld client should 10 | be mapped to a vlan. All jails with a network interface that 11 | is not inherited from the host should have a side of the epair 12 | that is connected to the vesseld bridge. A vlan interface and 13 | ip address will be set for that vlan interface. 14 | 4. The vesseld epair will have a vlan address for each of the 15 | vlan created. In that way it can serve DNS and other management. 16 | 17 | As a convention, when using epair, we always connect the "b side" to 18 | the bridge. The "a side" is then used as a parent device for one 19 | or more vlan devices. 20 | -------------------------------------------------------------------------------- /prototypes/network.tcl: -------------------------------------------------------------------------------- 1 | #Create a bridge epair devices and vlan 2 | 3 | package require appc::network 4 | package require defer 5 | 6 | 7 | # set bridge_obj [appc::network::bridge new appcd-bridge] 8 | 9 | # set epair_obj [appc::network::epair new testepair] 10 | 11 | # set vlan_obj [appc::network::vlan new 7 [$epair_obj get_aside]] 12 | 13 | # set vlan_obj2 [appc::network::vlan new 7 [$epair_obj get_bside]] 14 | 15 | # $epair_obj add_to_bridge $bridge_obj 16 | 17 | proc create_network {} { 18 | 19 | namespace eval create_networkns {} 20 | 21 | # defer::defer namespace delete create_networkns 22 | set bridge_obj [appc::network::bridge createWithNamespace create_networkns::acbridge create_networkns "acbridge"] 23 | 24 | appc::network::internal_network_vlan createWithNamespace create_networkns::shanenet create_networkns "shanenet" \ 25 | $bridge_obj 7 192.168.101.1 26 | } 27 | 28 | create_network 29 | -------------------------------------------------------------------------------- /src/native_deprecated/src/util/appc_save.cpp: -------------------------------------------------------------------------------- 1 | #include "app_functions.h" 2 | #include "vessel_tcl.h" 3 | #include "cmdline.h" 4 | #include "environment.h" 5 | #include "image_archive.h" 6 | #include 7 | 8 | using namespace vessel; 9 | 10 | int run_main(int argc, char** argv) 11 | { 12 | Tcl_FindExecutable(argv[0]); 13 | std::unique_ptr cmdline = commandline::parse(argc, argv); 14 | 15 | environment env; 16 | 17 | vessel::funcs::save_container_image(*cmdline, env); 18 | return 0; 19 | } 20 | 21 | int main(int argc, char** argv) 22 | { 23 | try 24 | { 25 | return run_main(argc, argv); 26 | } 27 | catch(std::exception& ex) 28 | { 29 | std::cerr << ex.what() << std::endl; 30 | exit(1); 31 | } 32 | catch(...) 33 | { 34 | std::cerr << "Uncaught exception" << std::endl; 35 | exit(1); 36 | } 37 | 38 | return 0; 39 | } 40 | -------------------------------------------------------------------------------- /prototypes/interp.tcl: -------------------------------------------------------------------------------- 1 | #Play around with sub interps so we can 2 | #more safely source appc files 3 | 4 | proc get_some_data {} { 5 | set d [dict create key1 val1 key2 val2] 6 | 7 | return $d 8 | } 9 | 10 | puts stderr "USAGE: interp.tcl " 11 | if {[llength $argv] != 1} { 12 | puts stderr "Exactly one argument required ." 13 | exit 1 14 | } 15 | 16 | set appc_file [lindex $argv 0] 17 | 18 | interp create sub.interp 19 | 20 | # Run commands in a sub-interp 21 | sub.interp eval [list set appc_file $appc_file] 22 | sub.interp eval {puts stderr $appc_file} 23 | 24 | # Pass data to a sub interp via running an alias 25 | # of master command in slave. 26 | sub.interp alias get_data get_some_data 27 | sub.interp eval {puts stderr "d: [get_data]"} 28 | 29 | #Build an image 30 | sub.interp eval { 31 | package require AppcFileCommands 32 | 33 | set cmdline_options [dict create name testimage] 34 | source $appc_file 35 | } 36 | -------------------------------------------------------------------------------- /util/generate-name: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env tclsh8.6 2 | # -*- mode: tcl; -*- 3 | 4 | package require vessel::name-gen 5 | package require cmdline 6 | 7 | set options { 8 | {suffix.arg "" "The suffix of the generated name"} 9 | {prefix.arg "" "The prefix of the generated name"} 10 | {separator.arg "-" "The separator for the name components"} 11 | {components.arg 2 "Number of - separated components in the name"} 12 | } 13 | 14 | set usage {generate-name [--prefix=] [--suffix=] [--components=]} 15 | 16 | try { 17 | array set params [::cmdline::getoptions argv $options $usage] 18 | } trap {CMDLINE USAGE} {msg o} { 19 | # Trap the usage signal, print the message, and exit the application. 20 | # Note: Other errors are not caught and passed through to higher levels! 21 | puts stderr $msg 22 | exit 1 23 | } 24 | 25 | puts [vessel::name-gen::generate-name $params(components) $params(prefix) \ 26 | $params(separator) $params(suffix)] 27 | -------------------------------------------------------------------------------- /prototypes/prototype.ini: -------------------------------------------------------------------------------- 1 | [vessel-supervisor] 2 | # The name of the image without the tag in the repository 3 | # This section is used to create the vessel commandline 4 | repository=s3://reapp/ 5 | image=supercooldjangoapp 6 | tag=1.3.1 7 | command=sh /etc/rc 8 | 9 | [dataset:upload-images] 10 | dataset=upload-images 11 | mount=/var/db/uploaded-images 12 | 13 | [resource:maxprocesses] 14 | #Provide the rctl string without the 'jail:' subject and subject-id. 15 | #Those will be added by vessel. 16 | 17 | #Deny more then concurrent 10 processes 18 | rctl="maxproc:deny=10" 19 | 20 | [resource:maxruntime] 21 | rctl=wallclock:devctl=5 22 | devctl-action=shutdown 23 | 24 | [resource:maxmemoryuse] 25 | rctl=vmemoryuse:devctl=128M 26 | devctl-action=exec=logger -t vessel "this could be any application. Generally used for observability and monitoring" 27 | 28 | [jail] 29 | # anything in this section is treated as a jail parameter and will 30 | # be added to the generated jail file. 31 | sysvshm=new 32 | 33 | -------------------------------------------------------------------------------- /examples/dinnerrepo/DinnerVesselFile: -------------------------------------------------------------------------------- 1 | FROM FreeBSD:12.3-RELEASE 2 | 3 | #Update pkg repo and install packages 4 | RUN env ASSUME_ALWAYS_YES=yes pkg update 5 | RUN env ASSUME_ALWAYS_YES=yes pkg install lighttpd fossil 6 | 7 | #Add the dinner user. The ~dinner/repo directory contains the repo and 8 | #is a mountpoint for the zfs dataset that will hold the fossil sqlite db. 9 | RUN pw useradd -n dinner -m 10 | 11 | COPY ./lighttpd/lighttpd.conf /usr/local/etc/lighttpd/lighttpd.conf 12 | COPY ./lighttpd/modules.conf /usr/local/etc/lighttpd/modules.conf 13 | COPY ./lighttpd/scgi.conf /usr/local/etc/lighttpd/conf.d/scgi.conf 14 | 15 | #Fossil startup script is slightly modified from the default installed 16 | #by pkg. 17 | COPY ./rc.d/fossil /usr/local/etc/rc.d/fossil 18 | 19 | #Setup rc.conf to run lighttpd and fossil 20 | RUN sysrc lighttpd_enable=YES 21 | RUN sysrc fossil_enable=YES 22 | RUN sysrc fossil_directory=/usr/home/dinner/repo/dinner.fossil 23 | RUN sysrc fossil_proto=scgi 24 | RUN sysrc fossil_user=dinner 25 | -------------------------------------------------------------------------------- /util/create-network.tcl: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env tclsh8.6 2 | 3 | package require cmdline 4 | package require vessel::network 5 | 6 | proc create_network {netname} { 7 | 8 | puts stderr "netname: $netname" 9 | 10 | #Intentionally don't destroy these objects so the 11 | #OS objects (interfaces) are not destroyed. 12 | set args_dict [dict create name $netname] 13 | set internal_network_obj [vessel::network::create_network_cmd $args_dict] 14 | } 15 | 16 | set options { 17 | {network.arg "" "Name of the network to create"} 18 | } 19 | 20 | set usage "create-network --network=" 21 | 22 | try { 23 | array set params [::cmdline::getoptions argv $options $usage] 24 | } trap {CMDLINE USAGE} {msg o} { 25 | # Trap the usage signal, print the message, and exit the application. 26 | # Note: Other errors are not caught and passed through to higher levels! 27 | puts stderr $msg 28 | exit 1 29 | } 30 | 31 | parray params 32 | 33 | set netname $params(network) 34 | 35 | create_network $netname 36 | -------------------------------------------------------------------------------- /prototypes/constructor_error.tcl: -------------------------------------------------------------------------------- 1 | package require TclOO 2 | 3 | oo::class create aclass { 4 | 5 | self export createWithNamespace 6 | 7 | variable _str 8 | 9 | constructor {str} { 10 | 11 | set _str $str 12 | } 13 | 14 | destructor { 15 | 16 | puts stderr "aclass destructor $_str" 17 | } 18 | } 19 | 20 | oo::class create container { 21 | 22 | self export createWithNamespace 23 | 24 | variable _inst1 25 | variable _inst2 26 | 27 | constructor {} { 28 | set ns [info object namespace [self]] 29 | set _inst1 [aclass new "first inst"] 30 | puts stderr "inst1: $_inst1" 31 | set _inst2 [aclass new "second inst"] 32 | puts stderr "inst2: $_inst2" 33 | #return -code error "Test error" 34 | 35 | } 36 | 37 | destructor { 38 | puts stderr "container destructor, $_inst1" 39 | #$_inst1 destroy 40 | 41 | #This error gets eaten since there is 42 | #an error already in progress 43 | #$_inst2 destroy 44 | } 45 | } 46 | 47 | namespace eval testns {} 48 | set obj [container createWithNamespace testns::obj testns] 49 | namespace delete testns 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/FeatureIdeas.md: -------------------------------------------------------------------------------- 1 | # Feature Ideas 2 | This page documents some thoughts on ideas that would be nice, useful or fun to implement in the future. 3 | 4 | # Vessel-Supervisor Networking 5 | 6 | There are tons of cool networking features we achieve with a supervisor. 7 | 8 | * Service Discovery by jail hostname. 9 | * Network management via DHCP. 10 | * IP forwarding via pf 11 | * Vessel networking sidecar jail. 12 | * Imagine a rctl controlled jail that ran with each version of vessel-supervisor. 13 | * It could run dnsmasq for dhcp and dns. 14 | * It would also run nginx for tcp forwarding and ssl termination. 15 | * Potentially would be a log sync 16 | * Each instance of vessel-supervisor can have a bridge that supports multiple networks via epairs and vlan. 17 | 18 | # Event Driven Execution (Serverless) 19 | 20 | Listen to kafka topic and feed it into a container. 21 | 22 | * This is realy event driven execution and not serverless as there is no cloud provider managing the servers. 23 | * Generally serverless integrates with the scripting language and calls a callback when a message becomes 24 | available. 25 | 26 | -------------------------------------------------------------------------------- /src/native_deprecated/src/lib/mountpoint.h: -------------------------------------------------------------------------------- 1 | #ifndef MOUNTPOINT_H 2 | #define MOUNTPOINT_H 3 | 4 | #include "fs.h" 5 | #include 6 | #include 7 | 8 | /*Include these so that the MNT_* values 9 | * are automatically available to clients. 10 | */ 11 | #include 12 | #include 13 | 14 | namespace vessel 15 | { 16 | class mountpoint 17 | { 18 | public: 19 | 20 | virtual fs_path from() = 0; 21 | virtual fs_path target() = 0; 22 | 23 | virtual int mount() = 0; 24 | virtual int unmount(int flags) = 0; 25 | 26 | /** 27 | * Error message string from the last mount attempt. 28 | * this may be different then the error message associated 29 | * with errno 30 | */ 31 | virtual std::string error_message() = 0; 32 | 33 | virtual ~mountpoint(); 34 | }; 35 | 36 | std::unique_ptr create_nullfs_mount(fs_path from, 37 | fs_path target, 38 | int mount_flags); 39 | } 40 | #endif // MOUNTPOINT_H 41 | -------------------------------------------------------------------------------- /src/native_deprecated/src/lib/image_archive.h: -------------------------------------------------------------------------------- 1 | #ifndef IMAGE_ARCHIVE_H 2 | #define IMAGE_ARCHIVE_H 3 | 4 | #include 5 | #include 6 | 7 | namespace vessel 8 | { 9 | 10 | class fs_path; 11 | /** 12 | * The term image_archive represents an image that can 13 | * be stored or retrieved from a registry. image_archives are 14 | * somehow packaged so that they can be transported and stored 15 | * in an archive. That package could theoretically be a 16 | * tarball, iso9660 image, compressed ufs image, 17 | * compressed zfs snapshot etc. 18 | */ 19 | 20 | struct compression_progress 21 | { 22 | off_t file_size; 23 | ssize_t bytes_read; 24 | ssize_t bytes_written; 25 | size_t total_read; 26 | size_t total_compressed_written; 27 | }; 28 | 29 | using compression_progress_callback = 30 | std::function; 31 | 32 | int create_image(const fs_path& source, const fs_path& dest); 33 | 34 | void archive_image(const fs_path& image, const fs_path& archive_dir, 35 | const compression_progress_callback& progress_cb); 36 | 37 | } 38 | #endif // IMAGE_ARCHIVE_H 39 | -------------------------------------------------------------------------------- /prototypes/jailconf.tcl: -------------------------------------------------------------------------------- 1 | package require textutil::adjust 2 | package require textutil::expander 3 | 4 | 5 | set mountpoint {/root} 6 | set name {my_jail} 7 | set network_params_dict [dict create "ip4" "inherit"] 8 | set volume_datasets [list "zroot/jails/volumes/redb"] 9 | set quoted_cmd "sh" 10 | textutil::expander jail_file_expander 11 | set jail_conf [jail_file_expander expand { 12 | [set name] { 13 | path="[set mountpoint]"; 14 | host.hostname="[set name]"; 15 | sysvshm=new; 16 | allow.mount; 17 | allow.mount.devfs; 18 | mount.devfs; 19 | allow.mount.zfs; 20 | enforce_statfs=1; 21 | [set network_string {} 22 | dict for {parameter value} ${network_params_dict} { 23 | append network_string "${parameter}=${value};" 24 | } 25 | set network_string] 26 | 27 | 28 | [set volume_string {} 29 | foreach volume $volume_datasets { 30 | set jail_string [subst {exec.created="zfs jail $name $volume";\n}] 31 | append volume_string $jail_string 32 | set mount_string [subst {exec.start+="zfs mount $volume";}] 33 | append volume_string $mount_string 34 | } 35 | set volume_string] 36 | exec.start+="[set quoted_cmd]"; 37 | }}] 38 | 39 | puts $jail_conf 40 | -------------------------------------------------------------------------------- /prototypes/zfs_diff_with_hardlinks.tcl: -------------------------------------------------------------------------------- 1 | package require appc::zfs 2 | if {[llength $argv] < 1} { 3 | puts stderr "Usage: $argv0 " 4 | exit 1 5 | } 6 | 7 | set dataset_mount [lindex $argv 0] 8 | set inode_path_pairs [exec find $dataset_mount -exec stat -f "%i,%N" \{\} \;] 9 | 10 | set dataset [string trim $dataset_mount /] 11 | set output_dict [appc::zfs::diff "${dataset}@a" ${dataset}] 12 | 13 | set inode_dict [dict create] 14 | foreach {inode_path_pair} [split $inode_path_pairs \n] { 15 | 16 | foreach {inode path} [split $inode_path_pair ,] { 17 | dict lappend inode_dict $inode $path 18 | } 19 | } 20 | 21 | set inode_path_pairs [list] 22 | 23 | set modified_file_list [list {*}[dict get $output_dict {M}] {*}[dict get $output_dict {+}]] 24 | 25 | foreach path $modified_file_list { 26 | array set stat_buf {} 27 | 28 | file lstat $path stat_buf 29 | 30 | foreach inode_path [dict get $inode_dict $stat_buf(ino)] { 31 | dict lappend output_dict $stat_buf(ino) $inode_path 32 | } 33 | } 34 | 35 | dict for {key paths} $output_dict { 36 | 37 | if {$key eq {M} || $key eq {+} || $key eq {-} } { 38 | puts "continuing" 39 | continue 40 | } 41 | 42 | foreach path $paths { 43 | puts $path 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /prototypes/oo.tcl: -------------------------------------------------------------------------------- 1 | package require appc::native 2 | 3 | namespace eval _ { 4 | oo::class create repo { 5 | 6 | variable full_url 7 | variable scheme 8 | 9 | constructor {url} { 10 | puts stderr "repo constructor" 11 | #Parse it to ensure it is valid 12 | set url_dict [appc::url::parse ${url}] 13 | set scheme [dict get $url_dict scheme] 14 | set full_url ${url} 15 | } 16 | 17 | method pull_image {image tag} { 18 | return -code error errorcode {INTERFACECALL} \ 19 | "Subclass of repo must implement pull_image" 20 | } 21 | 22 | method put_image {image tag} { 23 | return -code error errorcode {INTERFACECALL} \ 24 | "Subclass of repo must implement put_image" 25 | } 26 | 27 | method get_url {} { 28 | 29 | return $full_url 30 | } 31 | 32 | method get_scheme {} { 33 | 34 | return $scheme 35 | } 36 | } 37 | 38 | oo::class create file_repo { 39 | superclass repo 40 | constructor {url} { 41 | next $url 42 | 43 | puts stderr "file_repo constructor" 44 | } 45 | 46 | method pull_image {image tag} { 47 | 48 | } 49 | 50 | method put_image {image tag} { 51 | #TODO 52 | } 53 | } 54 | } 55 | 56 | _::file_repo create myrepo {file:///usr/home/shane/jack} 57 | 58 | puts stderr "[myrepo get_url]" 59 | -------------------------------------------------------------------------------- /examples/dinnerrepo/lighttpd/scgi.conf: -------------------------------------------------------------------------------- 1 | ####################################################################### 2 | ## 3 | ## SCGI Module 4 | ## --------------- 5 | ## 6 | ## See https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModSCGI 7 | ## 8 | server.modules += ( "mod_scgi" ) 9 | 10 | ## 11 | ## Ruby on Rails Example 12 | ## 13 | ## Normally you only run one Rails application on one vhost. 14 | ## 15 | #$HTTP["host"] == "rails1.example.com" { 16 | # server.document-root = server_root + "/rails/someapp/public" 17 | # server.error-handler-404 = "/dispatch.fcgi" 18 | # scgi.server = ( ".scgi" => 19 | # ("scgi-someapp" => 20 | # ( "socket" => socket_dir + "/someapp-scgi.socket", 21 | # "bin-path" => server_root + "/rails/someapp/public/dispatch.scgi", 22 | # "bin-environment" => ( 23 | # "RAILS_ENV" => "production", 24 | # "TMP" => home_dir + "/rails/someapp", 25 | # ), 26 | # ) 27 | # ) 28 | # ) 29 | #} 30 | 31 | ## 32 | ## Serve fossil 33 | ## 34 | ## 35 | scgi.server = ( "/" => 36 | ( "scgi-tcp" => 37 | ( 38 | "host" => "127.0.0.1", 39 | "port" => 8080, 40 | "check-local" => "disable", 41 | ) 42 | ) 43 | ) 44 | 45 | 46 | ## 47 | ####################################################################### 48 | -------------------------------------------------------------------------------- /src/native_deprecated/src/util/appc_mount.cpp: -------------------------------------------------------------------------------- 1 | #include "app_functions.h" 2 | #include "vessel_tcl.h" 3 | #include "cmdline.h" 4 | #include "environment.h" 5 | #include 6 | #include 7 | #include "mountpoint.h" 8 | #include 9 | 10 | using namespace vessel; 11 | using namespace vessel::funcs; 12 | 13 | int run_main(int argc, char** argv) 14 | { 15 | Tcl_FindExecutable(argv[0]); 16 | 17 | environment env; 18 | std::unique_ptr cmdline = commandline::parse(argc, argv); 19 | 20 | if(!cmdline) 21 | { 22 | return 1; 23 | } 24 | 25 | auto_unmount_ptr mntpoint = mount_container_image(*cmdline, env); 26 | 27 | return 0; 28 | } 29 | 30 | int main(int argc, char** argv) 31 | { 32 | 33 | /* 34 | * Arguments: 35 | * --image: Name of the image. vessel_mount will look in VESSEL_IMAGE_DIR 36 | * to find the image, then nullfs mount the image to --name folder 37 | * in VESSEL_CONTAINER_DIR. 38 | */ 39 | 40 | int exit_code = 1; 41 | try 42 | { 43 | exit_code = run_main(argc, argv); 44 | } 45 | catch(std::exception& ex) 46 | { 47 | std::cerr << ex.what() << std::endl; 48 | } 49 | catch(...) 50 | { 51 | std::cerr << "Unknown error occurred" << std::endl; 52 | } 53 | 54 | exit(exit_code); 55 | } 56 | -------------------------------------------------------------------------------- /util/native/url_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #if !CURL_AT_LEAST_VERSION(7, 62, 0) 5 | #error "this example requires curl 7.62.0 or later" 6 | #endif 7 | 8 | int main(void) 9 | { 10 | CURLU *h; 11 | CURLUcode uc; 12 | char *host; 13 | char *path; 14 | 15 | h = curl_url(); /* get a handle to work with */ 16 | if(!h) 17 | return 1; 18 | 19 | /* parse a full URL */ 20 | uc = curl_url_set(h, CURLUPART_URL, "s3://example.com/path/index.html", CURLU_NON_SUPPORT_SCHEME); 21 | if(uc) 22 | goto fail; 23 | 24 | /* extract scheme name from the parsed URL */ 25 | uc = curl_url_get(h, CURLUPART_SCHEME, &host, 0); 26 | if(!uc) { 27 | printf("Host name: %s\n", host); 28 | curl_free(host); 29 | } 30 | 31 | /* extract the path from the parsed URL */ 32 | uc = curl_url_get(h, CURLUPART_PATH, &path, 0); 33 | if(!uc) { 34 | printf("Path: %s\n", path); 35 | curl_free(path); 36 | } 37 | 38 | /* redirect with a relative URL */ 39 | uc = curl_url_set(h, CURLUPART_URL, "../another/second.html", 0); 40 | if(uc) 41 | goto fail; 42 | 43 | /* extract the new, updated path */ 44 | uc = curl_url_get(h, CURLUPART_PATH, &path, 0); 45 | if(!uc) { 46 | printf("Path: %s\n", path); 47 | curl_free(path); 48 | } 49 | 50 | fail: 51 | curl_url_cleanup(h); /* free url handle */ 52 | return 0; 53 | } 54 | -------------------------------------------------------------------------------- /examples/dinnerrepo/Makefile: -------------------------------------------------------------------------------- 1 | #Makefile to demonstrate how to use fossil for building, publishing and deploying 2 | #images via an S3 image repository. 3 | 4 | #Build the image. Note that the '-E' flag is important for sudo so that 5 | #the artifacts (image manifest) are created in the user's vessel directory and 6 | #not root's. 7 | .PHONY: build-image 8 | build-image: 9 | sudo -E vessel build --file=./DinnerVesselFile --name=dinner --tag=1.0 10 | 11 | #Runs the image using the .ini run configuration to mount the dataset so the 12 | #fossil db is not removed when the container is destroyed. 13 | .PHONY: run 14 | run: 15 | sudo -E vessel run --ini=./deploydir/dinnerrepo.ini --rm dinner:1.0 -- sh /etc/rc 16 | 17 | #Publish to s3. Note that you need to set the VESSEL_REPO_URL environment variable. 18 | #By default VESSEL_REPO_URL is the local filesystem. s3cmd needs to be configured before 19 | #publishing and pulling to s3 20 | .PHONY: publish 21 | publish: 22 | sudo -E env VESSEL_REPO_URL=s3://appc-test1 vessel publish --tag=1.0 dinner 23 | 24 | #After publishing to S3, deploying is trivial as we just need to scp the run configuration 25 | # file to our production server. vessel-supervisor will see the file, pull and run the container. 26 | .PHONY: deploy 27 | deploy: 28 | scp ./deploydir/dinnerrepo.ini prod:/usr/local/etc/vessel/deploy 29 | 30 | .PHONY: clean 31 | clean: 32 | sudo -E vessel image --rm dinner:1.0 33 | -------------------------------------------------------------------------------- /docs/QuickStart.md: -------------------------------------------------------------------------------- 1 | Quickstart 2 | =========== 3 | Let's get started. FBSDAppContainers uses a file to define and build a container 4 | image. The file is similar to a DockerFile. A very simple VesselFile is shown below 5 | 6 | .. code-block:: guess 7 | 8 | FROM FreeBSD:12.1-RELEASE 9 | 10 | RUN env ASSUME_ALWAYS_YES=yes pkg update 11 | RUN env ASSUME_ALWAYS_YES=yes pkg install tcllib curl bash cmake 12 | 13 | This is the VesselFile used for developing *vessel* itself. Now that you have a VesselFile, 14 | let's build the container image 15 | 16 | .. code-block:: shell 17 | 18 | sudo -E vessel build --file=./VesselFile --name=vesseldev --tag=local 19 | 20 | The above command will build an image named *vesseldev* with the special tag local. 21 | Finally, let's run a *sh* in the new container. 22 | 23 | .. code-block:: shell 24 | 25 | sudo -E vessel run --interactive vesseldev:local sh 26 | 27 | There you have it, you have built and are running a persistent application container. 28 | This is really the simplest use case, see the rest of the documentation for more 29 | advanced use cases. 30 | 31 | .. note:: 32 | The program name for FBSDAppContainers is *vessel*. This project started as a prototype 33 | and has continued to grow. The initial name of the program was *appc* but that 34 | has been used by a similar project. So if you still see references to appc in the 35 | source code, that's why. 36 | 37 | -------------------------------------------------------------------------------- /pm/whatsnext.md: -------------------------------------------------------------------------------- 1 | # What's Next 2 | 3 | Document to maintain notes on what is next for the project and current development status. 4 | 5 | ## First Class Environment Setup 6 | 7 | Vessel is very useful for a few things: 8 | 9 | 1. Creating a development environment for jails that mimics a deployment environment. 10 | 11 | 2. Running software in production and integrating with the production environment. 12 | 13 | 3. Creating the production environment. Creating datasets and jail files. 14 | 15 | ### Generating the Production Environment 16 | 17 | To date we have focused mainly on using vessel for development and running containers in production. However, perhaps the most useful feature of vessel is setting up the production environment. This lowers the barrier to entry for vessel so that you don't need long running instances and vessel can just get you started. 18 | 19 | #### Example of Generating Production Environment 20 | 21 | Vessel already creates jail files, populates datasets and mounts volumes. It should have commands for each of these steps. Thus we should have commands for each of: 22 | 23 | 1. Generating jail files that could be committed to source control. These jail files will: 24 | 25 | a. Have startup and shutdown commands that can mount directories and datasets. 26 | b. Setup resource limits and cpu sets 27 | 28 | 2. Creating datasets 29 | 3. Populate datasets 30 | 31 | Each of these commands should be idempotent and integrate well with ansible. -------------------------------------------------------------------------------- /util/join-network: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env tclsh8.6 2 | # -*- mode: tcl; -*- 3 | 4 | package require cmdline 5 | package require vessel::network 6 | 7 | proc join_network {netname jail ip} { 8 | 9 | #Intentionally don't destroy these objects so the 10 | #OS objects (interfaces) are not destroyed. 11 | set bridge_obj [vessel::network::bridge new "${netname}bridge"] 12 | 13 | set internal_network_obj [vessel::network::internal_network new $netname $bridge_obj] 14 | $internal_network_obj add_jail $jail $ip 15 | } 16 | 17 | set options { 18 | {network.arg "" "Name of the network to create"} 19 | {ip.arg "" "Ip address of the host epair"} 20 | {jail.arg "" "Network address for jail"} 21 | } 22 | 23 | set usage {} 24 | try { 25 | array set params [::cmdline::getoptions argv $options $usage] 26 | } trap {CMDLINE USAGE} {msg o} { 27 | # Trap the usage signal, print the message, and exit the application. 28 | # Note: Other errors are not caught and passed through to higher levels! 29 | puts stderr $msg 30 | exit 1 31 | } 32 | 33 | parray params 34 | 35 | set netname $params(network) 36 | if {$netname eq {}} { 37 | puts stderr "network is required" 38 | exit 1 39 | } 40 | 41 | set ip $params(ip) 42 | if {$ip eq {}} { 43 | puts stderr "ip is required" 44 | exit 1 45 | } 46 | 47 | set jail $params(jail) 48 | if {$jail eq {}} { 49 | puts stderr "jail is required" 50 | exit 1 51 | } 52 | join_network $netname $jail $ip 53 | -------------------------------------------------------------------------------- /src/network/README.md: -------------------------------------------------------------------------------- 1 | # Networking Features 2 | The following networking features should be supported 3 | 4 | ## Topologies 5 | * Jail inheriting the host network 6 | * Internal networks with bridge, vlan, epair interfaces 7 | * Nat and rdr networks for exposing internal networks to the 8 | outside world 9 | * No networking 10 | 11 | ## Management 12 | * VLAN per internal network 13 | * Auto generated internal ipv4 address 14 | * Internal network DNS lookup for A records. Hopefully txt in the future. 15 | Does it make sense to use DHCP here? 16 | 17 | # cli design 18 | 19 | * Run with inherited network stack. This is the default and doesn't 20 | require any flags. Still must support internal DNS lookup. 21 | `vessel run devel:0 -- bash` 22 | 23 | * Create an internal network 24 | `vessel create-network --name="shane-net" --dns="postgres-dev:192.168.3.2:" 25 | 26 | * Run with internal network that is nat'd for outside world 27 | access. Network vlan is created if it doesn't yet exist 28 | `vessel run --network "shane-net" devel:0 -- bash` 29 | 30 | * Run with internal network with port 80 of host mapped to 31 | port 8080 of jail. The internal network used will be the 32 | default. Jail will also be nat'd 33 | `vessel run -p 80:8080 devel:0 -- bash` 34 | 35 | * Run with no internal network so internal DNS will not be available 36 | `vessel run --no-internal-net devel:0 -- bash` 37 | 38 | * Run without any network besides localhost 39 | `vessel run --no-net devel:0 --bash` -------------------------------------------------------------------------------- /src/native_deprecated/src/lib/appc_tcl.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "vessel_tcl.h" 3 | #include 4 | 5 | using namespace vessel; 6 | 7 | tcl_obj_raii::tcl_obj_raii(Tcl_Obj* obj) 8 | : obj(obj) 9 | { 10 | if(obj) 11 | { 12 | Tcl_IncrRefCount(obj); 13 | } 14 | } 15 | 16 | tcl_obj_raii::tcl_obj_raii(const tcl_obj_raii& other) 17 | : tcl_obj_raii(Tcl_DuplicateObj(other.obj)) 18 | { 19 | 20 | } 21 | 22 | tcl_obj_raii::tcl_obj_raii(tcl_obj_raii&& other) 23 | : obj(other.obj) 24 | { 25 | other.obj = nullptr; 26 | } 27 | 28 | tcl_obj_raii& tcl_obj_raii::operator=(const tcl_obj_raii& other) 29 | { 30 | if(this != &other) 31 | { 32 | obj = Tcl_DuplicateObj(other.obj); 33 | } 34 | 35 | return *this; 36 | } 37 | 38 | tcl_obj_raii& tcl_obj_raii::operator=(tcl_obj_raii&& other) 39 | { 40 | if(this != &other) 41 | { 42 | obj = other.obj; 43 | other.obj = nullptr; 44 | } 45 | return *this; 46 | } 47 | 48 | tcl_obj_raii::operator Tcl_Obj*() const 49 | { 50 | return obj; 51 | } 52 | 53 | tcl_obj_raii::~tcl_obj_raii() 54 | { 55 | if(obj) 56 | { 57 | Tcl_DecrRefCount(obj); 58 | } 59 | 60 | std::cerr << std::endl; 61 | } 62 | 63 | void vessel::delete_interp(Tcl_Interp* interp) 64 | { 65 | assert(interp); 66 | Tcl_DeleteInterp(interp); 67 | } 68 | 69 | interp_ptr vessel::create_tcl_interp() 70 | { 71 | return vessel::interp_ptr(Tcl_CreateInterp(), vessel::delete_interp); 72 | } 73 | -------------------------------------------------------------------------------- /prototypes/rctl_devd.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | int main(int argc, char** argv) 13 | { 14 | struct sockaddr_un devd_addr; 15 | int s, error; 16 | 17 | /*Connect to devd's seq packet pipe*/ 18 | memset(&devd_addr, 0, sizeof(devd_addr)); 19 | devd_addr.sun_family = PF_LOCAL; 20 | std::string sockpath("/var/run/devd.seqpacket.pipe"); 21 | strlcpy(devd_addr.sun_path, sockpath.c_str(), sizeof(devd_addr.sun_path)); 22 | s = socket(PF_LOCAL, SOCK_SEQPACKET, 0); 23 | error = connect(s, (struct sockaddr*)&devd_addr, SUN_LEN(&devd_addr)); 24 | 25 | if(error == -1) 26 | { 27 | perror("connect"); 28 | exit(1); 29 | } 30 | 31 | int kfd = kqueue(); 32 | 33 | 34 | char event[1024]; 35 | for(;;) 36 | { 37 | memset(&event, 0, sizeof(event)); 38 | /*SEQPACKET is connection oriented but maintains message 39 | * boundaries so only a single message will be received. 40 | */ 41 | ssize_t len = recv(s, event, sizeof(event), 0); 42 | if(len == -1) 43 | { 44 | perror("recv"); 45 | exit(1); 46 | } 47 | 48 | std::string event_msg(event, len); 49 | std::cerr << "Message of length: " << len << " received, msg: " << event_msg << std::endl; 50 | } 51 | 52 | exit(0); 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/tcl/name-gen.tcl: -------------------------------------------------------------------------------- 1 | namespace eval vessel::name-gen { 2 | 3 | namespace eval _ { 4 | variable name_list [list] 5 | 6 | proc load_namelist {} { 7 | variable name_list 8 | set proper_names_file {/usr/share/dict/propernames} 9 | 10 | if {[llength $name_list] > 0} { 11 | return -code ok 12 | } 13 | 14 | if {! [file exists $proper_names_file]} { 15 | return -code error -errorcode {NAMEGEN ENOENT} \ 16 | "Proper names file does not exist" 17 | } 18 | set names_chan [open $proper_names_file] 19 | 20 | set name {} 21 | while {[gets $names_chan name] >= 0} { 22 | lappend name_list $name 23 | } 24 | } 25 | 26 | proc generate_random_name {} { 27 | variable name_list 28 | set name_count [llength $name_list] 29 | set name_offset [expr {int(($name_count + 1) * rand())}] 30 | 31 | set random_name [lindex $name_list $name_offset] 32 | return $random_name 33 | } 34 | } 35 | 36 | proc generate-name {{num_components 2} {prefix {}} {separator -} {suffix {}} } { 37 | 38 | _::load_namelist 39 | 40 | set name_components [list $prefix] 41 | for {set i 0} {$i < $num_components} {incr i} { 42 | lappend name_components [_::generate_random_name] 43 | } 44 | 45 | lappend name_components $suffix 46 | 47 | #Filter out the empty components 48 | set filtered [lmap component $name_components {expr {$component ne {} ? $component : [continue]}}] 49 | return [join $filtered $separator] 50 | } 51 | } 52 | 53 | package provide vessel::name-gen 1.0.0 54 | -------------------------------------------------------------------------------- /src/lib/tcl/definition_file.tcl: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | package require TclOO 3 | package require json::write 4 | 5 | oo::class create definition_file { 6 | 7 | variable m_whiteout_list 8 | 9 | #Each command is a string of values that can 10 | # be expanded. 11 | variable m_commands 12 | 13 | 14 | constructor {} { 15 | 16 | set m_whiteout_list [list] 17 | set m_commands [list] 18 | } 19 | 20 | method add_whiteout_path {path} { 21 | 22 | lappend m_whiteout_list $path 23 | } 24 | 25 | method add_command {command} { 26 | lappend m_commands $command 27 | } 28 | 29 | method serialize {} { 30 | 31 | json::write indented false 32 | json::write aligned false 33 | 34 | return [json::write object \ 35 | "whiteout" [json::write array {*}[lmap whiteout $m_whiteout_list {json::write string $whiteout}]] \ 36 | "commands" [json::write array {*}[lmap command $m_commands {json::write string $command}]]] 37 | } 38 | 39 | method write {path} { 40 | 41 | set container_chan [open $path "w" 0600] 42 | 43 | puts $container_chan [my serialize] 44 | close $container_chan 45 | } 46 | } 47 | 48 | package provide vessel::definition_file 1.0.0 49 | # definition_file create o 50 | # o add_whiteout_path {/yippity/doo/dah} 51 | # o add_whiteout_path {/yippity/doo/dac} 52 | # o add_command {echo 'hello world'} 53 | # puts [o serialize] 54 | # o write ./test.config 55 | -------------------------------------------------------------------------------- /src/native_deprecated/src/app/environment.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "environment.h" 3 | #include 4 | 5 | using namespace vessel; 6 | 7 | namespace 8 | { 9 | std::string get_env_required(const std::string& name) 10 | { 11 | assert(!name.empty()); 12 | 13 | char* value = ::getenv(name.c_str()); 14 | 15 | if(value == nullptr) 16 | { 17 | std::ostringstream msg; 18 | msg << "'" << name << "' is a required environment variable"; 19 | throw std::runtime_error(msg.str()); 20 | } 21 | 22 | return std::string(value); 23 | } 24 | 25 | fs_path get_env_required_dir(const std::string& name) 26 | { 27 | fs_path dir_path = get_env_required(name); 28 | validate_directory(dir_path); 29 | return dir_path; 30 | } 31 | } 32 | 33 | environment::environment() 34 | : m_archive_dir(get_env_required_dir("VESSEL_ARCHIVE_DIR")), 35 | m_image_dir(get_env_required_dir("VESSEL_IMAGE_DIR")), 36 | m_container_dir(get_env_required_dir("VESSEL_CONTAINER_DIR")) 37 | {} 38 | 39 | fs_path environment::find_image(const std::string& image_name) const 40 | { 41 | return m_image_dir.find_dir(image_name); 42 | } 43 | 44 | fs_path environment::find_container(const std::string& container_name) const 45 | { 46 | return m_container_dir.find_dir(container_name); 47 | } 48 | 49 | fs_path environment::archive_dir() const 50 | { 51 | return m_archive_dir; 52 | } 53 | 54 | fs_path environment::image_dir() const 55 | { 56 | return m_image_dir; 57 | } 58 | -------------------------------------------------------------------------------- /src/lib/native/tcl_util.cpp: -------------------------------------------------------------------------------- 1 | #include "tcl_util.h" 2 | #include 3 | 4 | using namespace vessel; 5 | 6 | void vessel::unref_tclobj(Tcl_Obj* obj) 7 | { 8 | if(obj) 9 | { 10 | Tcl_DecrRefCount(obj); 11 | } 12 | } 13 | 14 | void vessel::tclobj_delete_proc(void* client_data, Tcl_Interp* interp) 15 | { 16 | Tcl_DecrRefCount((Tcl_Obj*)client_data); 17 | } 18 | 19 | tclobj_ptr vessel::create_tclobj_ptr(Tcl_Obj* obj) 20 | { 21 | return tclobj_ptr(obj, vessel::unref_tclobj); 22 | } 23 | 24 | int vessel::get_handle_from_channel(Tcl_Interp* interp, Tcl_Obj* chan_name, long& handle) 25 | { 26 | /*Note: handle is of type long because that is the size of a void* which is what 27 | * is used to retrieve the handle. If you pass it an int, bad things happen because 28 | * an int will get dereferenced as a wider void* 29 | */ 30 | 31 | int mode = -1; 32 | 33 | Tcl_Channel chan = Tcl_GetChannel(interp, Tcl_GetString(chan_name), &mode); 34 | if(chan == nullptr) return TCL_ERROR; 35 | 36 | int tcl_error = Tcl_GetChannelHandle(chan, mode, (ClientData*)&handle); 37 | if(tcl_error) 38 | { 39 | Tcl_SetObjResult(interp, Tcl_ObjPrintf("Error getting handle from channel: %s", 40 | Tcl_GetString(chan_name))); 41 | return tcl_error; 42 | } 43 | 44 | return TCL_OK; 45 | } 46 | 47 | 48 | fd_guard::fd_guard(int fd) 49 | : fd(fd) 50 | { 51 | 52 | } 53 | 54 | int fd_guard::release() 55 | { 56 | int tmp_fd = fd; 57 | fd = -1; 58 | return tmp_fd; 59 | } 60 | 61 | fd_guard::~fd_guard() 62 | { 63 | if(fd >= 0) 64 | { 65 | ::close(fd); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/lib/tcl/pkgIndex.tcl: -------------------------------------------------------------------------------- 1 | package ifneeded vessel::bsd 1.0.0 [list source [file join $dir bsd.tcl]] 2 | package ifneeded vessel::build 1.0.0 [list source [file join $dir build.tcl]] 3 | package ifneeded vesseld::client 1.0.0 [list source [file join $dir vesseld_client.tcl]] 4 | package ifneeded vessel::definition_file 1.0.0 [list source [file join $dir definition_file.tcl]] 5 | package ifneeded vessel::deploy 1.0.0 [list source [file join $dir deploy.tcl]] 6 | package ifneeded vessel::dns 1.0.0 [list source [file join $dir dns dns.tcl]] 7 | package ifneeded vessel::env 1.0.0 [list source [file join $dir environment.tcl]] 8 | package ifneeded vessel::export 1.0.0 [list source [file join $dir export.tcl]] 9 | package ifneeded vessel::import 1.0.0 [list source [file join $dir import.tcl]] 10 | package ifneeded vessel::jail 1.0.0 [list source [file join $dir jail.tcl]] 11 | package ifneeded vessel::metadata_db 1.0.0 [list source [file join $dir metadata_db.tcl]] 12 | package ifneeded vessel::name-gen 1.0.0 [list source [file join $dir name-gen.tcl]] 13 | package ifneeded vessel::native 1.0.0 [list load [file join $dir libvesseltcl.so]] 14 | package ifneeded vessel::network 1.0.0 [list source [file join $dir network network.tcl]] 15 | package ifneeded vessel::pty_shell 1.0.0 [list source [file join $dir pty_shell.tcl]] 16 | package ifneeded vessel::repo 1.0.0 [list source [file join $dir repos.tcl]] 17 | package ifneeded vessel::run 1.0.0 [list source [file join $dir run.tcl]] 18 | package ifneeded vessel::syslog 1.0.0 [list source [file join $dir syslog.tcl]] 19 | package ifneeded vessel::zfs 1.0.0 [list source [file join $dir zfs.tcl]] 20 | 21 | package ifneeded VesselFileCommands 1.0.0 [list source [file join $dir vessel_file_commands.tcl]] 22 | -------------------------------------------------------------------------------- /src/lib/native/tcl_kqueue.h: -------------------------------------------------------------------------------- 1 | #ifndef KQUEUE_H 2 | #define KQUEUE_H 3 | #include 4 | #include 5 | #include 6 | #include "tcl_util.h" 7 | 8 | namespace vessel 9 | { 10 | 11 | using tcl_event_ptr = tclalloc_ptr; 12 | 13 | /** 14 | * Utility function to create a Tcl_Event. T must be a 15 | * subclass of Tcl_Event 16 | */ 17 | template 18 | tcl_event_ptr alloc_tcl_event(Ts... args); 19 | 20 | class tcl_event_factory 21 | { 22 | public: 23 | 24 | virtual tcl_event_ptr create_tcl_event(const struct kevent& event) = 0; 25 | 26 | virtual ~tcl_event_factory() 27 | {} 28 | }; 29 | 30 | int Kqueue_Init(Tcl_Interp* interp); 31 | 32 | /** 33 | * @brief Kqueue_Add_Event Add an event to the kqueue with the given tcl event factory. 34 | * @param interp 35 | * @param event 36 | * @param event_factory Object used to create Tcl_Events to handle the given kevent when it is active. Note 37 | * that it is a reference. It is up to the caller to ensure the lifetime of the event factory outlives the lifetime 38 | * of the kevent in kqueue. 39 | * @return 40 | */ 41 | int Kqueue_Add_Event(Tcl_Interp* interp, struct kevent& event, tcl_event_factory& event_factory); 42 | 43 | int Kqueue_Remove_Event(Tcl_Interp* interp, struct kevent& event); 44 | } 45 | 46 | template 47 | vessel::tcl_event_ptr vessel::alloc_tcl_event(Ts... args) 48 | { 49 | /*Tcl alloc function here*/ 50 | T* instance = reinterpret_cast(Tcl_Alloc(sizeof(T))); 51 | new(instance) T(args...); 52 | return tcl_event_ptr(instance, vessel::tclalloc_free); 53 | } 54 | #endif // KQUEUE_H 55 | -------------------------------------------------------------------------------- /src/lib/native/udp_tcl.h: -------------------------------------------------------------------------------- 1 | /* 2 | *---------------------------------------------------------------------- 3 | * UDP Extension for Tcl 8.4 4 | * 5 | * Copyright (c) 1999-2003 by Columbia University; all rights reserved 6 | * Copyright (c) 2003-2005 Pat Thoyts 7 | * 8 | * Written by Xiaotao Wu 9 | * 10 | * $Id: udp_tcl.h,v 1.13 2014/05/02 14:41:24 huubeikens Exp $ 11 | *---------------------------------------------------------------------- 12 | */ 13 | 14 | #ifndef UDP_TCL_H 15 | #define UDP_TCL_H 16 | 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include "tcl.h" 34 | 35 | #ifdef BUILD_udp 36 | #undef TCL_STORAGE_CLASS 37 | #define TCL_STORAGE_CLASS DLLEXPORT 38 | #endif /* BUILD_udp */ 39 | 40 | typedef struct UdpState { 41 | Tcl_Channel channel; 42 | int sock; 43 | char remotehost[256]; /* send packets to */ 44 | uint16_t remoteport; 45 | char peerhost[256]; /* receive packets from */ 46 | uint16_t peerport; 47 | uint16_t localport; 48 | int doread; 49 | short ss_family; /* indicator set for ipv4 or ipv6 usage */ 50 | int multicast; /* indicator set for multicast add */ 51 | Tcl_Obj *groupsObj; /* list of the mcast groups */ 52 | } UdpState; 53 | 54 | 55 | int Udp_Init(Tcl_Interp *interp); 56 | int Udp_SafeInit(Tcl_Interp *interp); 57 | 58 | #endif 59 | -------------------------------------------------------------------------------- /util/dns-serve: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env tclsh8.6 2 | # -*- mode: tcl; -*- 3 | package require cmdline 4 | package require vessel::dns 5 | 6 | set options { 7 | {network.arg "" "Name of the network to create"} 8 | {ip.arg "" "Ip address to bind to"} 9 | {dns.arg "" "dns mappings. A space separated list of 10 | values"} 11 | } 12 | 13 | 14 | proc add_dns_mappings_from_cmdline {server dns_arg} { 15 | # Example DNS cmdline option: --dns="internal.postgres.re.com:192.168.9.3 internal.rabbit.re.com:192.168.9.4" 16 | foreach dns_mapping $dns_arg { 17 | 18 | #Default value for ttl 19 | set ttl 35 20 | 21 | lmap {name ip ttl} [split $dns_mapping ":"] {} 22 | if {$name eq {}} { 23 | puts stderr "Missing name in dns arg: $dns_mapping" 24 | exit 1 25 | } 26 | 27 | if {$ip eq {}} { 28 | puts stderr "Missing ip in dns arg: $dns_mapping" 29 | } 30 | 31 | puts stderr "n: $name, ip: $ip, ttl: $ttl" 32 | $server add_lookup_mapping $name $ip $ttl 33 | } 34 | } 35 | 36 | proc main {} { 37 | global argv 38 | global options 39 | 40 | set usage {} 41 | try { 42 | array set params [::cmdline::getoptions argv $options $usage] 43 | } trap {CMDLINE USAGE} {msg o} { 44 | # Trap the usage signal, print the message, and exit the application. 45 | # Note: Other errors are not caught and passed through to higher levels! 46 | puts stderr $msg 47 | exit 1 48 | } 49 | 50 | set dns_arg $params(dns) 51 | if {$dns_arg eq {}} { 52 | puts stderr "--dns is required" 53 | exit 1 54 | } 55 | set server [vessel::dns::create_server 53 $params(ip)] 56 | add_dns_mappings_from_cmdline $server $params(dns) 57 | 58 | 59 | set _forever_ {} 60 | vwait _forever_ 61 | } 62 | 63 | main 64 | -------------------------------------------------------------------------------- /src/rc.d/vessel: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # vessel startup script 4 | # 5 | # PROVIDE: vessel 6 | # REQUIRE: DAEMON cleanvar 7 | # KEYWORD: shutdown 8 | # 9 | # Add the following to /etc/rc.conf[.local] to enable this service 10 | # 11 | # vessel_enable="YES" 12 | # 13 | # You can fine tune others variables too: 14 | # vessel_directory (str): Path the the vessel deploy directory 15 | # 16 | # Environment Variables 17 | # 18 | # Can be set with vessel_env 19 | # 20 | # VESSEL_POOL Set the zpool used by vessel. 21 | # 22 | # VESSEL_DATASET Set the dataset used for jails within VESSEL_POOL 23 | # 24 | # VESSEL_WORKDIR Set the directory used for managing artifacts 25 | # 26 | # VESSEL_REPO_URL The repo to be used to push and pull artifacts 27 | # 28 | # VESSEL_S3CMD_CONFIG The path to the s3cmd configuration file 29 | 30 | . /etc/rc.subr 31 | 32 | name="vessel" 33 | rcvar=vessel_enable 34 | load_rc_config $name 35 | pidprefix="/var/run/vessel/vessel-supervisor" 36 | pidfile="${pidprefix}.pid" 37 | 38 | procname="/usr/local/bin/vessel-supervisor" 39 | command="/usr/sbin/daemon" 40 | command_interpreter="/usr/local/bin/tclsh8.6" 41 | start_precmd="vessel_precmd" 42 | stop_postcmd="vessel_postcmd" 43 | 44 | vessel_enable=${vessel_enable:-"NO"} 45 | vessel_directory=${vessel_directory:-"/usr/local/etc/vessel/deploy"} 46 | 47 | #I feel like there is a better way to do this. Maybe as part of user 48 | #config or with an env file. 49 | export PATH=$PATH:/usr/local/bin 50 | 51 | command_args="-S -T ${name} -p ${pidfile} ${procname} -d ${vessel_directory}" 52 | 53 | vessel_precmd() 54 | { 55 | install -d -o root -g wheel -m 1777 /var/run/vessel 56 | install -d -o root -g wheel -m 1777 ${vessel_directory} 57 | } 58 | 59 | vessel_postcmd() 60 | { 61 | rm -rf /var/run/vessel 62 | } 63 | 64 | run_rc_command "$1" 65 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(FBSDAppContainers) 2 | cmake_minimum_required(VERSION 3.13) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | 6 | find_package(TCL) 7 | find_package(CURL) 8 | include_directories(${TCL_INCLUDE_PATH} ${CURL_INCLUDE_DIRS}) 9 | 10 | add_library(vesseltcl SHARED 11 | src/lib/native/vessel_native.cpp 12 | src/dns/embdns.cpp 13 | src/lib/native/tcl_util.cpp 14 | src/lib/native/url_cmd.cpp 15 | src/lib/native/pty.cpp 16 | src/lib/native/exec.cpp 17 | src/lib/native/devctl.cpp 18 | src/lib/native/tcl_kqueue.cpp 19 | src/lib/native/udp_tcl.c) 20 | 21 | target_link_libraries(vesseltcl ${CURL_LIBRARIES}) 22 | 23 | add_executable(url_test util/native/url_test.cpp) 24 | target_link_libraries(url_test ${CURL_LIBRARIES}) 25 | 26 | install(TARGETS vesseltcl 27 | LIBRARY 28 | DESTINATION lib/tclvessel) 29 | install(FILES src/lib/tcl/vessel_file_commands.tcl 30 | src/lib/tcl/bsd.tcl 31 | src/lib/tcl/build.tcl 32 | src/lib/tcl/definition_file.tcl 33 | src/lib/tcl/deploy.tcl 34 | src/lib/tcl/environment.tcl 35 | src/lib/tcl/export.tcl 36 | src/lib/tcl/import.tcl 37 | src/lib/tcl/jail.tcl 38 | src/lib/tcl/metadata_db.tcl 39 | src/lib/tcl/name-gen.tcl 40 | src/lib/tcl/pkgIndex.tcl 41 | src/lib/tcl/repos.tcl 42 | src/lib/tcl/run.tcl 43 | src/lib/tcl/syslog.tcl 44 | src/lib/tcl/zfs.tcl 45 | DESTINATION lib/tclvessel) 46 | install(PROGRAMS src/rc.d/vessel 47 | DESTINATION etc/rc.d) 48 | install(DIRECTORY src/network 49 | DESTINATION lib/tclvessel 50 | PATTERN *.md EXCLUDE) 51 | 52 | install(PROGRAMS src/apps/vessel src/apps/vessel-supervisor 53 | DESTINATION bin) 54 | -------------------------------------------------------------------------------- /docs/ResourceControl.md: -------------------------------------------------------------------------------- 1 | Resource Limits 2 | ================== 3 | 4 | Vessel integrates tightly with FreeBSDs resource limits. One or multiple resource limits can be specified 5 | in a vessel ini file. For useful information on freebsd resource limits see [rctl(8)]() 6 | 7 | 8 | > ⚠️ Resource limits must be enabled in the kernel. Before freebsd 12.2 this required adding :code:`kern.racct.enable=1` 9 | > to /boot/loader.conf 10 | 11 | ``` 12 | [resource:maxprocesses] 13 | #Provide the rctl string without the 'jail:' subject and subject-id. 14 | #Those will be added by vessel. 15 | 16 | #Deny more then concurrent 10 processes 17 | rctl="maxproc:deny=10" 18 | 19 | [resource:maxruntime] 20 | rctl=wallclock:devctl=5 21 | devctl-action=shutdown 22 | 23 | [resource:maxmemoryuse] 24 | rctl=vmemoryuse:devctl=128M 25 | devctl-action=exec=logger -t vessel "this could be any application. Generally used for observability and monitoring" 26 | ``` 27 | 28 | # Using Resource Limits 29 | 30 | * Each resource needs to be defined in a separate section named `[resource:]`. 31 | * The `rctl` key can be set to a valid rctl string. The rctl string should not contain the subject or subjectid as those will be 32 | added by vessel. Any of the actions defined in the [rctl(8)]() manual page can be usedin the rctl string. 33 | * There is a second optional key `devctl-action`. If you use devctl as the resource string action then a custom then this key can be used to define that custom action. The supported actions are: 34 | * `shutdown`. Will cleanly shutdown the jail. 35 | * `exec=`. Execute an arbitrary process that can be very useful for monitoring and alerting. 36 | 37 | -------------------------------------------------------------------------------- /test/jail.test: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; -*- 2 | package require tcltest 3 | 4 | package require vessel::native 5 | package require vessel::jail 6 | 7 | namespace eval jail::test { 8 | 9 | namespace import ::tcltest::* 10 | 11 | test generate-jail-conf-1 {Test that the jail conf file is generated correctly} -body { 12 | set jail_options [dict create sysvshm new] 13 | set cpuset {} 14 | set limits {} 15 | set jail_conf [vessel::jail::_::build_jail_conf {myjail} \ 16 | {/zroot/myjail} [list {abs_path /a/b/c abs_jailed_mountpoint /zroot/jails/myjail/opt}] {} {inherit} $limits $cpuset $jail_options sh /etc/rc] 17 | 18 | return $jail_conf 19 | } -match glob -result {*sysvshm=new;*host.hostname=myjail;*sysvshm+=new;*} 20 | 21 | 22 | test generate-jail-conf-2-no-host {Test that the jail conf file is generated correctly with hostname in options} -body { 23 | set jail_options [dict create sysvshm new host.hostname "differenthostname"] 24 | set cpuset {} 25 | set limits {} 26 | set jail_conf [vessel::jail::_::build_jail_conf {myjail} \ 27 | {/zroot/myjail} [list {abs_path /a/b/c abs_jailed_mountpoint /zroot/jails/myjail/opt}] {} {inherit} $limits $cpuset $jail_options sh /etc/rc] 28 | 29 | return $jail_conf 30 | } -match glob -result {*sysvshm=new;*host.hostname+=differenthostname;*} 31 | 32 | test generate-jail-shutdown-cmd {Test that the jail conf file is generated correctly a shutdown command} -body { 33 | set jail_options [dict create sysvshm new exec.stop {"sh /etc/rc.shutdown jail"}] 34 | set cpuset {} 35 | set limits {} 36 | set jail_conf [vessel::jail::_::build_jail_conf {myjail} \ 37 | {/zroot/myjail} [list {abs_path /a/b/c abs_jailed_mountpoint /zroot/jails/myjail/opt}] {} {inherit} $limits $cpuset $jail_options sh /etc/rc] 38 | 39 | return $jail_conf 40 | } -match glob -result {*sysvshm=new;*exec.stop+="sh /etc/rc.shutdown jail"*} 41 | 42 | cleanupTests 43 | } -------------------------------------------------------------------------------- /prototypes/ctty_read.tcl: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env tclsh8.6 2 | 3 | package require appc::native 4 | 5 | #Flag used to exit the event loop when the pty closes 6 | set pty_closed 0 7 | 8 | set ptys [pty::open] 9 | 10 | set master [lindex $ptys 0] 11 | set slave [lindex $ptys 1] 12 | puts $slave 13 | 14 | #Set the size of the new pty to the size of the current 15 | #terminal. This is a good starting point and as long 16 | #as the size of the terminal (or X window terminal emulator) 17 | #isn't changed, then it should work very well. When stdin 18 | #and pty are mismatched in size, it is very obvious when using 19 | #man or typing long commands. 20 | pty::copy_winsz stdin $master 21 | 22 | #Outside of setting blocking to false, the other configurations 23 | #probably aren't necessary. 24 | fconfigure stdin -blocking false -translation binary -buffering none 25 | fconfigure $master -blocking false -translation binary -buffering none 26 | fconfigure stdout -buffering none -translation binary -buffering none 27 | 28 | #Make stdin and stdout raw so that signals are passed though 29 | #to the process on the slave end of the pty 30 | set old_stdin_settings [pty::makeraw stdin] 31 | set old_stdout_settings [pty::makeraw stdout] 32 | 33 | #Proxy input from stdin to the pty 34 | fileevent stdin readable {apply {{input_chan output_chan} { 35 | set data [read $input_chan] 36 | puts -nonewline $output_chan $data 37 | }} stdin $master} 38 | 39 | #Proxy input from the pty to stdout. Closing when the pty closes. 40 | fileevent $master readable {apply {{input_chan output_chan stdin_restore stdout_restore} { 41 | set data [read $input_chan] 42 | if {[eof $input_chan]} { 43 | global pty_closed 44 | 45 | pty::restore stdout $stdout_restore 46 | pty::restore stdin $stdin_restore 47 | close $input_chan 48 | set pty_closed 1 49 | } else { 50 | puts -nonewline $output_chan $data 51 | } 52 | }} $master stdout $old_stdin_settings $old_stdout_settings} 53 | 54 | #Start the event loop 55 | vwait pty_closed 56 | 57 | puts stderr "Exiting container shell" 58 | -------------------------------------------------------------------------------- /src/lib/tcl/pty_shell.tcl: -------------------------------------------------------------------------------- 1 | #Shell implementation for attaching to a pty 2 | #and passing through stdin/stdout to the current 3 | #controlling terminal 4 | 5 | package require vessel::native 6 | 7 | namespace eval vessel::pty_shell { 8 | 9 | proc run {master done_script_prefix} { 10 | 11 | #Set the size of the new pty to the size of the current 12 | #terminal. This is a good starting point and as long 13 | #as the size of the terminal (or X window terminal emulator) 14 | #isn't changed, then it should work very well. When stdin 15 | #and pty are mismatched in size, it is very obvious when using 16 | #man or typing long commands. 17 | pty::copy_winsz stdin $master 18 | 19 | #Outside of setting blocking to false, the other configurations 20 | #probably aren't necessary. 21 | fconfigure stdin -blocking false -translation binary -buffering none 22 | fconfigure $master -blocking false -translation binary -buffering none 23 | fconfigure stdout -buffering none -translation binary -buffering none 24 | 25 | #Make stdin and stdout raw so that signals are passed though 26 | #to the process on the slave end of the pty 27 | set old_stdin_settings [pty::makeraw stdin] 28 | set old_stdout_settings [pty::makeraw stdout] 29 | 30 | #Async proxy input from stdin to the pty 31 | fileevent stdin readable [list apply {{input_chan output_chan} { 32 | set data [read $input_chan] 33 | puts -nonewline $output_chan $data 34 | }} stdin $master] 35 | 36 | #Async proxy input from the pty to stdout. Closing when the pty closes. 37 | fileevent $master readable [list apply {{input_chan output_chan stdin_restore stdout_restore callback} { 38 | set data [read $input_chan] 39 | if {[eof $input_chan]} { 40 | 41 | pty::restore stdout $stdout_restore 42 | pty::restore stdin $stdin_restore 43 | close $input_chan 44 | {*}$callback 45 | 46 | } else { 47 | puts -nonewline $output_chan $data 48 | } 49 | }} $master stdout $old_stdin_settings $old_stdout_settings $done_script_prefix] 50 | } 51 | } 52 | 53 | package provide vessel::pty_shell 1.0.0 54 | -------------------------------------------------------------------------------- /prototypes/ctty_read.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | int main(int argc, char** argv) 9 | { 10 | int master_fd = posix_openpt(O_RDWR); 11 | if(master_fd == -1) 12 | { 13 | perror("posix_openpt"); 14 | exit(1); 15 | } 16 | 17 | 18 | grantpt(master_fd); 19 | unlockpt(master_fd); 20 | 21 | std::cerr << "pts: " << ptsname(master_fd) << std::endl; 22 | char input[512]; 23 | ssize_t bytes_read = 0; 24 | 25 | termios tios; 26 | tcgetattr(0, &tios); 27 | tios.c_lflag &= ~ICANON; 28 | tios.c_lflag &= ~ECHO; 29 | cfmakeraw(&tios); 30 | tcsetattr(0, TCSANOW, &tios); 31 | tcsetattr(1, TCSANOW, &tios); 32 | 33 | cfmakesane(&tios); 34 | tcsetattr(master_fd, TCSANOW, &tios); 35 | for (;;) 36 | { 37 | fd_set set; 38 | FD_ZERO(&set); 39 | 40 | FD_SET(0, &set); 41 | FD_SET(master_fd, &set); 42 | int error = select(master_fd + 1, &set, nullptr, nullptr, nullptr); 43 | 44 | if(error == -1) 45 | { 46 | perror("select"); 47 | exit(1); 48 | } 49 | 50 | if(FD_ISSET(0, &set)) 51 | { 52 | bytes_read = read(0, input, sizeof(input)); 53 | if(bytes_read == -1) 54 | { 55 | perror("read stdin"); 56 | exit(1); 57 | } 58 | 59 | write(master_fd, input, bytes_read); 60 | } 61 | 62 | if(FD_ISSET(master_fd, &set)) 63 | { 64 | bytes_read = read(master_fd, input, sizeof(input)); 65 | 66 | if(bytes_read == 0) 67 | { 68 | std::cerr << "Exiting" << std::endl; 69 | exit(0); 70 | } 71 | 72 | if(bytes_read == -1) 73 | { 74 | perror("read stdin"); 75 | exit(1); 76 | } 77 | 78 | write(1, input, bytes_read); 79 | } 80 | } 81 | 82 | return 0; 83 | } 84 | -------------------------------------------------------------------------------- /test/ini.test: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | 3 | package require tcltest 4 | 5 | package require vessel::deploy 6 | 7 | namespace eval deploy::test { 8 | 9 | namespace import ::tcltest::* 10 | 11 | test validate-ini-file-1 {Test validation of valid deploy file} -setup { 12 | set ini_chan [file tempfile inifilename] 13 | set ini_data { 14 | [vessel-supervisor] 15 | # The name of the image without the tag in the repository 16 | repository=s3://reapp/ 17 | image=supercooldjangoapp 18 | tag=1.3.1 19 | command=sh /etc/rc 20 | 21 | [dataset:upload-images] 22 | pool=reappdata 23 | dataset=upload-images 24 | mount=/var/db/uploaded-images 25 | 26 | [jail] 27 | # anything in this section is treated as a jail parameter and will 28 | # be added to the generated jail file. 29 | sysvshm=new 30 | } 31 | puts $ini_chan [string trim $ini_data] 32 | flush $ini_chan 33 | 34 | } -body { 35 | vessel::deploy::ini::get_deployment_dict $inifilename 36 | } -match exact -result {jail {sysvshm new} dataset:upload-images {dataset upload-images pool reappdata mount /var/db/uploaded-images} vessel-supervisor {command {sh /etc/rc} image supercooldjangoapp tag 1.3.1 repository s3://reapp/}} -cleanup { 37 | close $ini_chan 38 | file delete $inifilename 39 | } 40 | 41 | test validate-ini-file-3 {unknown sections rejected} -setup { 42 | set ini_chan [file tempfile inifilename] 43 | set ini_data { 44 | [gibberish] 45 | } 46 | 47 | puts $ini_chan [string trim $ini_data] 48 | flush $ini_chan 49 | 50 | } -body { 51 | set d [vessel::deploy::ini::get_deployment_dict $inifilename] 52 | } -result {Unexpected section gibberish} -errorCode {VESSEL INI SECTION UNEXPECTED} \ 53 | -cleanup { 54 | close $ini_chan 55 | file delete $inifilename 56 | } 57 | 58 | cleanupTests 59 | } 60 | -------------------------------------------------------------------------------- /src/native_deprecated/src/util/appc_compress.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int main(int argc, char** argv) 9 | { 10 | struct archive *image_archive = archive_write_new(); 11 | 12 | (void)archive_write_set_format_pax(image_archive); 13 | (void)archive_write_add_filter_xz(image_archive); 14 | archive_write_set_bytes_per_block(image_archive, 10); 15 | int archive_error = archive_write_open_filename(image_archive, 16 | "/usr/home/shane/compresstest.xz"); 17 | if(archive_error) 18 | { 19 | std::cerr << "Error opening file for write: " << archive_error_string(image_archive) 20 | << std::endl; 21 | } 22 | 23 | int fd = open("/usr/home/shane/compresstest", O_RDONLY); 24 | if(fd == -1) 25 | { 26 | perror("open"); 27 | exit(1); 28 | } 29 | 30 | char buffer[BUFSIZ]; 31 | memset(buffer, 0, sizeof(buffer)); 32 | 33 | ssize_t bytes_read = 0; 34 | size_t total_bytes_read = 0; 35 | archive_entry* entry = archive_entry_new2(image_archive); 36 | 37 | struct stat sb; 38 | int error = fstat(fd, &sb); 39 | if(error) 40 | { 41 | perror("stat"); 42 | exit(1); 43 | } 44 | 45 | archive_entry_copy_stat(entry, &sb); 46 | archive_entry_set_pathname(entry, "compresstest"); 47 | //TODO: set fflags 48 | int header_error = archive_write_header(image_archive, entry); 49 | assert(header_error == ARCHIVE_OK); 50 | while(true) 51 | { 52 | bytes_read = read(fd, buffer, sizeof(buffer)); 53 | if(bytes_read == -1) 54 | { 55 | perror("read"); 56 | exit(1); 57 | } 58 | else if(bytes_read == 0) 59 | { 60 | break; 61 | } 62 | 63 | ssize_t bytes_written = archive_write_data(image_archive, buffer, bytes_read); 64 | } 65 | 66 | std::cerr << "bytes: " << total_bytes_read << std::endl; 67 | archive_write_free(image_archive); 68 | ::close(fd); 69 | } 70 | -------------------------------------------------------------------------------- /prototypes/ctty.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | int main(int argc, char** argv) 10 | { 11 | std::cerr << "Forking child" << std::endl; 12 | std::string slave_path = argv[1]; 13 | 14 | pid_t fork1_pid = fork(); 15 | 16 | switch(fork1_pid) 17 | { 18 | case -1: 19 | perror("fork1()"); 20 | exit(1); 21 | case 0: 22 | std::cerr << "Child: " << getpid() << std::endl; 23 | break; 24 | default: 25 | 26 | std::cerr << "Waiting for child session leader" << std::endl; 27 | int status = 0; 28 | pid_t wait_pid = wait(&status); 29 | std::cerr << "Waited on: " << wait_pid << ": " << WIFEXITED(status) << "," 30 | << WIFCONTINUED(status) << "," << WIFSIGNALED(status) << "," 31 | << WIFSTOPPED(status)<< std::endl; 32 | std::cerr << "Signal: " << WTERMSIG(status) << std::endl; 33 | exit(0); 34 | } 35 | 36 | int slave_fd = open(argv[1], O_RDWR); 37 | /*Session leader process*/ 38 | 39 | termios tios; 40 | tcgetattr(slave_fd, &tios); 41 | 42 | // tios.c_oflag |= OPOST | ONLCR | ONLRET; 43 | // std::cerr << "oflag" << tios.c_oflag << std::endl; 44 | // tios.c_oflag &= ~ONOCR; 45 | // std::cerr << "oflag" << tios.c_oflag << std::endl; 46 | // tios.c_lflag |= ICANON; 47 | // tcsetattr(master_fd, TCSANOW, &tios); 48 | 49 | pid_t sl_process_grpid = setsid(); 50 | signal(SIGHUP, SIG_DFL); 51 | 52 | int error = tcsetsid(slave_fd, sl_process_grpid); 53 | if(error == -1) 54 | { 55 | perror("tcsetsid"); 56 | exit(1); 57 | } 58 | 59 | dup2(slave_fd, 0); 60 | dup2(slave_fd, 1); 61 | dup2(slave_fd, 2); 62 | error = tcsetpgrp(slave_fd, sl_process_grpid); 63 | if(error == -1) 64 | { 65 | perror("tcsetpgrp"); 66 | } 67 | 68 | // tcgetattr(slave_fd, &tios); 69 | // tios.c_lflag &= ~ICANON; 70 | // tcsetattr(slave_fd, TCSANOW, &tios); 71 | std::vector args = {"bash", nullptr}; 72 | error = execvp("bash", args.data()); 73 | perror("execvp"); 74 | exit(1); 75 | } 76 | -------------------------------------------------------------------------------- /docs/Supervisor.md: -------------------------------------------------------------------------------- 1 | # Vessel Supervisor 2 | 3 | Deploying, managing and supervising vessel containers. 4 | 5 | `vessel-supervisor` can be used to easily deploy containers by monitoring a filesystem directory for 6 | vessel deployment files. This feature was inspired by [uwsgi emperor mode](https://uwsgi-docs.readthedocs.io/en/latest/Emperor.html). It provides similar features to emperor mode (and some supervisord like features) but is more tightly integrated with vessel. 7 | 8 | ## Deployment Workflow 9 | 10 | A container can be quickly deployed and (perhaps more importantly) safely re-deployed using the following workflow: 11 | 12 | 1. Build a container locally or in CI 13 | 2. Publish container to registry (s3 or locally) 14 | 3. Place deployment file in `vessel-supervisor` monitored directory using `ansible` or `scp`. If redeploying, overwrite the existing deployment 15 | file. 16 | 17 | ## Deployment File 18 | 19 | Deployment files are ini files that explain to `vessel-supervisor` how to start, shutdown or reconfigure the container. 20 | They are used to, among other things, generate the jail file used to run a container. 21 | 22 | ## Example 23 | 24 | ``` 25 | [vessel-supervisor] 26 | # The name of the image without the tag in the repository 27 | repository=s3://reapp/ 28 | image=supercooldjangoapp 29 | tag=1.3.1 30 | command=sh /etc/rc 31 | 32 | [dataset:upload-images] 33 | pool=reappdata 34 | dataset=upload-images 35 | mount=/var/db/uploaded-images 36 | 37 | [jail] 38 | # anything in this section is treated as a jail parameter and will 39 | # be added to the generated jail file. 40 | sysvshm=new 41 | ``` 42 | 43 | Vessel deployment files are vessel runtime files which contain a `[vessel-supervisor]` section. The `[vessel-supervisor]` section contains the following keys: 44 | 45 | * **repository:** The repository from which the image will be pulled 46 | * **image:** The name of the image to be pulled 47 | * **tag:** The tag or version of the image 48 | * **command:** The command to run when invoking the container 49 | * **restart:** Boolean that specifies whether or not the container should be restarted if it exits for any reason or the vessel application exits. 50 | * **restart-delay:** The amount of time in seconds to wait before restarting the container 51 | -------------------------------------------------------------------------------- /src/lib/native/tcl_util.h: -------------------------------------------------------------------------------- 1 | #ifndef TCL_UTIL_H 2 | #define TCL_UTIL_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | 11 | namespace vessel 12 | { 13 | 14 | void unref_tclobj(Tcl_Obj* obj); 15 | 16 | template 17 | void tclalloc_free(T* arg); 18 | 19 | template 20 | void tclalloc_destruct(T* arg); 21 | 22 | using tclobj_ptr = std::unique_ptr; 23 | 24 | template 25 | using tclalloc_ptr = std::unique_ptr)>; 26 | 27 | template 28 | void placement_destructor(T* arg); 29 | 30 | template 31 | using placement_ptr = std::unique_ptr)>; 32 | 33 | template 34 | placement_ptr create_placement_ptr(T* arg); 35 | 36 | void tclobj_delete_proc(void* client_data, Tcl_Interp* interp); 37 | 38 | tclobj_ptr create_tclobj_ptr(Tcl_Obj* obj); 39 | 40 | template 41 | int syserror_result(Tcl_Interp* interp, CODES... error_codes) 42 | { 43 | 44 | Tcl_SetResult(interp, (char*)Tcl_ErrnoMsg(errno), TCL_STATIC); 45 | Tcl_SetErrorCode(interp, &error_codes..., Tcl_ErrnoId(), nullptr); 46 | return TCL_ERROR; 47 | } 48 | 49 | int get_handle_from_channel(Tcl_Interp* interp, Tcl_Obj* chan_name, long& handle); 50 | 51 | template 52 | void cpp_delete(void* client_data) 53 | { 54 | delete((T*)client_data); 55 | } 56 | 57 | template 58 | void cpp_delete_with_interp(void* client_data, Tcl_Interp* interp) 59 | { 60 | delete((T*)client_data); 61 | } 62 | 63 | struct fd_guard 64 | { 65 | int fd; 66 | 67 | fd_guard(int fd); 68 | int release(); 69 | ~fd_guard(); 70 | }; 71 | } 72 | 73 | template 74 | void vessel::tclalloc_free(T* arg) 75 | { 76 | Tcl_Free(reinterpret_cast(arg)); 77 | } 78 | 79 | template 80 | void vessel::tclalloc_destruct(T* arg) 81 | { 82 | arg->~T(); 83 | Tcl_Free(reinterpret_cast(arg)); 84 | } 85 | 86 | template 87 | void vessel::placement_destructor(T* arg) 88 | { 89 | arg->~T(); 90 | } 91 | 92 | template 93 | vessel::placement_ptr vessel::create_placement_ptr(T* arg) 94 | { 95 | return placement_ptr(arg, vessel::placement_destructor); 96 | } 97 | #endif // TCL_UTIL_H 98 | -------------------------------------------------------------------------------- /src/native_deprecated/src/lib/fs.h: -------------------------------------------------------------------------------- 1 | #ifndef VESSEL_FS_H 2 | #define VESSEL_FS_H 3 | 4 | #include "vessel_tcl.h" 5 | #include 6 | #include 7 | #include 8 | 9 | namespace vessel 10 | { 11 | 12 | struct path_stat 13 | { 14 | struct stat stat_buf; 15 | bool is_dir() const; 16 | 17 | private: 18 | friend class fs_path; 19 | 20 | path_stat(const std::string& path); 21 | path_stat(const path_stat& other) = delete; 22 | path_stat(path_stat&& other) = delete; 23 | path_stat& operator=(const path_stat& other) = delete; 24 | path_stat& operator=(path_stat&& other) = delete; 25 | 26 | }; 27 | 28 | class resource_fd 29 | { 30 | int m_fd; 31 | 32 | void do_close(); 33 | static int validate_fd(int fd); 34 | 35 | resource_fd(const resource_fd& other) = delete; 36 | resource_fd& operator=(const resource_fd& other) = delete; 37 | public: 38 | 39 | resource_fd(int fd); 40 | resource_fd(resource_fd&& other); 41 | resource_fd& operator=(int fd); 42 | resource_fd& operator=(resource_fd&& other); 43 | operator int(); 44 | ~resource_fd(); 45 | }; 46 | 47 | class fs_path 48 | { 49 | private: 50 | tcl_obj_raii m_path; 51 | 52 | public: 53 | 54 | fs_path(); 55 | fs_path(const std::string& path); 56 | fs_path(const fs_path& other); 57 | fs_path(fs_path&& other); 58 | fs_path& operator=(const fs_path& rhs); 59 | fs_path& operator=(fs_path&& rhs); 60 | 61 | 62 | bool exists() const; 63 | 64 | bool is_dir() const; 65 | 66 | bool is_readable() const; 67 | 68 | bool is_writable() const; 69 | 70 | bool is_executable() const; 71 | 72 | std::unique_ptr stat() const; 73 | 74 | bool operator==(const fs_path& rhs) const; 75 | 76 | fs_path& operator+=(const std::string& path_component); 77 | 78 | std::string basename() const; 79 | 80 | void append_extension(const std::string& extension); 81 | 82 | explicit operator bool() const; 83 | 84 | std::string str() const; 85 | 86 | fs_path find_dir(const std::string& dir_name) const; 87 | 88 | ~fs_path(); 89 | }; 90 | 91 | /** 92 | * @brief validate_directory validates that the path exists and is 93 | * a directory. Throws exception if either test failes. 94 | */ 95 | void validate_directory(const fs_path& path); 96 | 97 | } 98 | #endif // VESSEL_FS_H 99 | -------------------------------------------------------------------------------- /src/native_deprecated/src/app/cmdline.cpp: -------------------------------------------------------------------------------- 1 | #include "cmdline.h" 2 | #include 3 | #include 4 | 5 | using namespace vessel; 6 | 7 | namespace 8 | { 9 | constexpr struct option long_options[] = { 10 | {"name", required_argument, nullptr, 'n'}, 11 | {"image", required_argument, nullptr, 'i'}, 12 | {"save", no_argument, nullptr, 's'}, 13 | {"type", required_argument, nullptr, 't'}, 14 | {nullptr, 0, nullptr, 0} 15 | }; 16 | } 17 | 18 | 19 | void commandline::usage() 20 | { 21 | /*TODO: implement commands instead of just flags 22 | * for example: vessel run OR vessel save 23 | */ 24 | std::cerr << "USAGE: " << std::endl 25 | << "vessel --name= --image=" << std::endl 26 | << "OR" << std::endl 27 | << "vessel --save --type=" << std::endl 28 | << std::endl; 29 | } 30 | 31 | std::unique_ptr commandline::parse(int argc, char** argv) 32 | { 33 | 34 | std::unique_ptr _this = std::make_unique(); 35 | 36 | /*Capture all arguments after '--' to use as a container 37 | * command. Update argc so those arguments are not passed 38 | * to getopt. 39 | */ 40 | for(int i = 0; i < argc; i++) 41 | { 42 | std::string arg = argv[i]; 43 | if(arg == "--") 44 | { 45 | for(int j = i + 1; j < argc; j++) 46 | { 47 | _this->container_cmd_args.push_back(argv[j]); 48 | } 49 | 50 | //Args needs to be null terminated to use with exec. 51 | _this->container_cmd_args.push_back(nullptr); 52 | argc = i; 53 | break; 54 | } 55 | } 56 | 57 | int ch = 0; 58 | while((ch = getopt_long(argc, argv, "n:i:st:", long_options, nullptr)) != -1) 59 | { 60 | switch(ch) 61 | { 62 | case 'n': 63 | _this->container = optarg; 64 | break; 65 | case 'i': 66 | _this->image = optarg; 67 | break; 68 | case 's': 69 | _this->do_save = true; 70 | _this->operation_count++; 71 | break; 72 | case 't': 73 | _this->image_type = optarg; 74 | break; 75 | default: 76 | usage(); 77 | } 78 | } 79 | 80 | return _this; 81 | } 82 | -------------------------------------------------------------------------------- /examples/dinnerrepo/rc.d/fossil: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # fossil startup script 4 | # 5 | # PROVIDE: fossil 6 | # REQUIRE: LOGIN 7 | # KEYWORD: shutdown 8 | # 9 | # Add the following to /etc/rc.conf[.local] to enable this service 10 | # 11 | # fossil_enable="YES" 12 | # 13 | # You can fine tune others variables too: 14 | # fossil_port="8080" 15 | # fossil_directory="/nonexistent" 16 | # fossil_baseurl="" 17 | # fossil_proto="http" 18 | # fossil_listenall="" 19 | # fossil_https="" # force the HTTPS CGI parameter to "on" 20 | # fossil_files="" # comma separated globing patterns of files to serve 21 | # fossil_notfound="" # URI to redirect to in case of 404 22 | # Use fossil_user to run fossil as user 23 | 24 | . /etc/rc.subr 25 | 26 | name="fossil" 27 | rcvar=fossil_enable 28 | load_rc_config $name 29 | pidprefix="/var/run/fossil/fossil" 30 | pidfile="${pidprefix}.pid" 31 | 32 | procname="/usr/local/bin/fossil" 33 | command="/usr/sbin/daemon" 34 | start_precmd="fossil_precmd" 35 | stop_postcmd="fossil_postcmd" 36 | 37 | fossil_enable=${fossil_enable:-"NO"} 38 | fossil_user=${fossil_user:-"nobody"} 39 | fossil_port=${fossil_port:-"8080"} 40 | fossil_proto=${fossil_proto:-"http"} 41 | fossil_directory=${fossil_directory:-"/nonexistent"} 42 | 43 | case "${fossil_proto}" in 44 | http) 45 | # http is the default 46 | ;; 47 | scgi) 48 | fossil_args="--scgi" 49 | ;; 50 | *) 51 | echo "unsupported protocol: ${fossil_proto}, only scgi and http are supported" >&2 52 | exit 1 53 | ;; 54 | esac 55 | 56 | [ -n "${fossil_baseurl}" ] && fossil_args="${fossil_args} --baseurl ${fossil_baseurl}" 57 | [ -z "${fossil_listenall}" ] && fossil_args="${fossil_args} --localhost" 58 | [ -n "${fossil_https}" ] && fossil_args="${fossil_args} --https" 59 | [ -n "${fossil_files}" ] && fossil_args="${fossil_args} --files '${fossil_files}'" 60 | [ -n "${fossil_notfound}" ] && fossil_args="${fossil_args} --notfound \"${fossil_notfound}\"" 61 | [ -n "${fossil_repolist}" ] && fossil_args="${fossil_args} --repolist" 62 | 63 | command_args="-S -T ${name} -p ${pidfile} ${procname} server --create -P ${fossil_port} ${fossil_args} ${fossil_directory}" 64 | 65 | fossil_precmd() 66 | { 67 | #Note: vessel mounts the repo dir as root:wheel. We need to change it to dinner:dinner 68 | chown -R dinner:dinner /usr/home/dinner/repo 69 | install -d -o root -g wheel -m 1777 /var/run/fossil 70 | } 71 | 72 | fossil_postcmd() 73 | { 74 | rm -rf /var/run/fossil 75 | } 76 | 77 | run_rc_command "$1" 78 | -------------------------------------------------------------------------------- /src/native_deprecated/src/lib/container.cpp: -------------------------------------------------------------------------------- 1 | #include "container.h" 2 | #include 3 | #include 4 | 5 | using namespace vessel; 6 | 7 | vessel::jail::jail(fs_path path, const std::string& hostname) 8 | : m_jail() 9 | { 10 | if(!path || !path.is_dir()) 11 | { 12 | std::ostringstream msg; 13 | msg << "Jail path is not valid: " << path.str(); 14 | throw std::logic_error(msg.str()); 15 | } 16 | 17 | if(hostname.empty()) 18 | { 19 | throw std::logic_error("Jail hostname cannot be empty"); 20 | } 21 | 22 | memset(&m_jail, 0, sizeof(m_jail)); 23 | 24 | m_jail.hostname = strdup(hostname.c_str()); 25 | m_jail.path = strdup(path.str().c_str()); 26 | m_jail.jailname = strdup(hostname.c_str()); 27 | } 28 | 29 | 30 | //pid_t vessel::jail::fork_jail() 31 | //{ 32 | // pid_t child_id = fork(); 33 | 34 | // switch(child_id) 35 | // { 36 | // case -1: 37 | // return ret; 38 | // case 0: 39 | // //Child 40 | // ::jail(&m_jail); 41 | // return 0; 42 | // default: 43 | // //Parent 44 | // ret 45 | 46 | // } 47 | //} 48 | 49 | std::tuple vessel::jail::fork_exec_jail(const std::vector& args) 50 | { 51 | if(args.back() != nullptr) 52 | { 53 | throw std::logic_error("jail args must be null terminated"); 54 | } 55 | 56 | pid_t child_id = fork(); 57 | 58 | switch(child_id) 59 | { 60 | case -1: 61 | return {-1, -1}; 62 | 63 | case 0: 64 | //Do jail and exec below 65 | break; 66 | 67 | default: 68 | /*TODO: Should we send the jail id via a pipe so we have 69 | * it in the parent*/ 70 | return {child_id, 0}; 71 | 72 | } 73 | 74 | int jail_id = ::jail(&m_jail); 75 | 76 | if(jail_id == -1) 77 | { 78 | std::ostringstream msg; 79 | msg << "Error jailing child process: " << strerror(errno); 80 | throw std::runtime_error(msg.str()); 81 | } 82 | 83 | (void)execvp(args[0], args.data()); 84 | 85 | //Should never get here. 86 | { 87 | std::ostringstream msg; 88 | msg << "Exec of child process failed: " << strerror(errno); 89 | throw std::runtime_error(msg.str()); 90 | } 91 | } 92 | 93 | vessel::jail::~jail() 94 | { 95 | if(m_jail.hostname) free(m_jail.hostname); 96 | if(m_jail.path) free(m_jail.path); 97 | if(m_jail.jailname) free(m_jail.jailname); 98 | } 99 | -------------------------------------------------------------------------------- /src/lib/tcl/environment.tcl: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | package require uri 3 | 4 | namespace eval vessel::env { 5 | 6 | proc get_from_env {key {_default {}}} { 7 | 8 | global env 9 | set value $_default 10 | if {[info exists env($key)]} { 11 | 12 | set value $env($key) 13 | } 14 | 15 | return $value 16 | } 17 | 18 | proc get_pool {} { 19 | 20 | return [get_from_env VESSEL_POOL "zroot"] 21 | } 22 | 23 | proc get_dataset {} { 24 | #Get the zfs path including pool 25 | 26 | set pool [get_pool] 27 | set dataset [get_from_env VESSEL_DATASET "jails"] 28 | 29 | return "${pool}/${dataset}" 30 | } 31 | 32 | proc get_workdir {} { 33 | return [get_from_env VESSEL_WORKDIR \ 34 | [file join [get_from_env HOME [pwd]] .vessel-workdir]] 35 | } 36 | 37 | proc get_repo_url {} { 38 | 39 | #Defaults to a directory in the workdir 40 | return [get_from_env VESSEL_REPO_URL [uri::join path [get_workdir]/local_repo scheme file]] 41 | } 42 | 43 | proc get_dataset_from_image_name {image_name {tag {}}} { 44 | 45 | set dataset [get_dataset] 46 | set container_path "${dataset}/${image_name}" 47 | if {$tag ne {}} { 48 | set container_path "${container_path}:${tag}" 49 | } 50 | return $container_path 51 | } 52 | 53 | proc copy_resolv_conf {mountpoint} { 54 | # copy resolv.conf 55 | set resolv_file {/etc/resolv.conf} 56 | file copy -force $resolv_file [fileutil::jail $mountpoint $resolv_file] 57 | } 58 | 59 | proc remove_resolv_conf {mountpoint} { 60 | set resolve_file {/etc/resolv.conf} 61 | #TODO: Commented this out because it was getting deleted multiple times 62 | #due to multiple run commands. Need to fix the vessel_file_command 63 | #file delete $resolve_file [fileutil::jail $mountpoint $resolve_file] 64 | } 65 | 66 | proc s3cmd_config_file {} { 67 | 68 | return [get_from_env VESSEL_S3CMD_CONFIG [file normalize ~/.s3cfg]] 69 | } 70 | 71 | proc image_download_dir {} { 72 | set workdir [get_workdir] 73 | return [get_from_env VESSEL_DOWNLOAD_DIR [file join $workdir {downloaded_images}]] 74 | } 75 | 76 | proc metadata_db_dir {} { 77 | 78 | set workdir [get_workdir] 79 | return [get_from_env VESSEL_METADATA_DB_DIR [file join $workdir {db}]] 80 | } 81 | 82 | proc vessel_run_dir {} { 83 | return [get_from_env VESSEL_RUN_DIR {/var/run/vessel/jails}] 84 | } 85 | 86 | proc jail_confs_dir {} { 87 | 88 | return [get_from_env VESSEL_VAR_RUN_DIR {/var/run/vessel/jails}] 89 | } 90 | } 91 | package provide vessel::env 1.0.0 92 | -------------------------------------------------------------------------------- /test/run.test: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | package require tcltest 3 | 4 | package require vessel::native 5 | package require vessel::run 6 | 7 | namespace eval deploy::test { 8 | 9 | namespace import ::tcltest::* 10 | 11 | test run-dict {Test validation of valid deploy file} -setup { 12 | set ini_chan [file tempfile inifilename] 13 | set ini_data { 14 | [vessel-supervisor] 15 | # The name of the image without the tag in the repository 16 | repository=s3://reapp/ 17 | image=supercooldjangoapp 18 | tag=1.3.1 19 | command=sh /etc/rc 20 | 21 | [dataset:upload-images] 22 | dataset=upload-images 23 | mount=/var/db/uploaded-images 24 | 25 | [nullfs:projectdir] 26 | directory=/usr/home/joe/projectdir 27 | mount=/re 28 | 29 | [jail] 30 | # anything in this section is treated as a jail parameter and will 31 | # be added to the generated jail file. 32 | sysvshm=new 33 | host.hostname=re.zeta.investments 34 | } 35 | puts $ini_chan [string trim $ini_data] 36 | flush $ini_chan 37 | 38 | set options_dict [vessel::parse_options \ 39 | [list run --interactive \ 40 | -v /test/joe:/mnt \ 41 | --ini=$inifilename \ 42 | --rm \ 43 | --dataset=reappdata:/var/re/data \ 44 | testimage:1.0]] 45 | } -body { 46 | set d [vessel::run::_::create_run_dict [dict get $options_dict args]] 47 | 48 | #ini_file is random so just set it to a known value for comparison 49 | dict set d ini_file {} 50 | return $d 51 | } -match exact -result {datasets {reappdata:/var/re/data upload-images:/var/db/uploaded-images} volumes {/test/joe:/mnt /usr/home/joe/projectdir:/re} resources {} remove 1 interactive 1 network inherit ini_file {} image testimage:1.0 jail {sysvshm new host.hostname re.zeta.investments} limits {} cpuset {}} \ 52 | -cleanup { 53 | close $ini_chan 54 | file delete $inifilename 55 | } 56 | 57 | test supervisor-ctrl-chan-stop {test the stop supervisor cmd} -setup { 58 | set supervisor_chan_list [chan pipe] 59 | set supervisor_chan_read [lindex $supervisor_chan_list 0] 60 | set supervisor_chan_write [lindex $supervisor_chan_list 1] 61 | 62 | chan configure $supervisor_chan_write -buffering line 63 | } -cleanup { 64 | close $supervisor_chan_read 65 | close $supervisor_chan_write 66 | } -body { 67 | 68 | puts $supervisor_chan_write "stop" 69 | vessel::run::supervisor_event_handler $supervisor_chan_read "test_jail" "/tmp/test_jail.conf" 70 | } -errorCode {JAIL RUN REMOVE} -match glob -result "*" 71 | 72 | cleanupTests 73 | } 74 | -------------------------------------------------------------------------------- /docs/ImageCreation.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | There is an [example VesselFile and invocation](/README.md#quickstart) in the quick start guide. 4 | 5 | # Supported Commands 6 | 7 | VesselFiles support a small but powerful number of commands. It is formatted similarly to a DockerFile file. 8 | 9 | > 🕵️ Fun fact... DockerFiles are valid tcl files. Commands in Vessel are actually implemented using a tcl sub interpreter. 10 | 11 | ## 'FROM' Command 12 | 13 | The `From` command tells vessel the base image to use. Currently this image needs to be a FreeBSD. 14 | 15 | **Example** 16 | 17 | `FROM FreeBSD:12.3-RELEASE` 18 | 19 | > 🕵️ The FROM command pulls a base tarball from "https://ftp.freebsd.org/pub/FreeBSD/releases/$arch/$version/base.txz". This base image is extracted into a new 20 | > dataset and a snapshot is created. 21 | 22 | ## 'COPY' Command 23 | 24 | The copy command accepts a `source` and `dest`. The source is relative to the current working directory of the host system. The dest is relative to the current 25 | working directory in the jail which defaults to /. 26 | 27 | **Example** 28 | 29 | `COPY . /app` 30 | 31 | ## 'CWD' Command 32 | 33 | Change the current working directory of the jail. This is a stateful command that only affects the COPY command. It does not carry over when an image is run using `vessel run` (this is different then docker). 34 | 35 | **Example** 36 | 37 | `CWD /app` 38 | 39 | ## 'RUN' Command 40 | 41 | Execute a command in the new image. 42 | 43 | **Example** 44 | `RUN env ASSUME_ALWAYS_YES=yes pkg install nginx` 45 | 46 | > 🕵️ Each run command runs within a separate container (jail) on the new images dataset. So commands use the dataset of the image not the host filesystem. The 47 | > jail inherits the host networking stack and the resolv.conf file is copied from the host into the jail. 48 | 49 | # Impage Publish and Pull 50 | 51 | `vessel` supports `publish` and `pull` commands to transfer images to an image repository. Vessel's image repositories are configured using the `VESSEL_REPO_URL` environment variable. Vessel supports the following repository schemas: 52 | 53 | * `file://` - The schema of the default repository. 54 | * `s3://` - Uses an s3 bucket and key prefix for the image repository. 55 | 56 | > 🕵️ When a repository uses the s3 schema, The `s3cmd` program is used to interface with the bucket. Therefore, it's not only amazon's s3 object storage that can 57 | > be used. It's any object storage that s3cmd can interface with (including digital ocean). Note `s3cmd` must be configured outside of the context of vessel 58 | > before the s3 repository schema can be used. 59 | 60 | After an image is published to a repository, it can then be pulled from another machine. 61 | 62 | **Example** 63 | 64 | ``` 65 | env VESSEL_REPO_URL=s3://reweb-1234/images sudo -E vessel publish --tag=1.0 reweb 66 | ``` 67 | 68 | and from a different machine: 69 | 70 | ``` 71 | env VESSEL_REPO_URL=s3://reweb-1234/images sudo -E vessel pull --tag=1.0 reweb 72 | ``` 73 | 74 | or to export an image for manual transfer 75 | 76 | ``` 77 | env VESSEL_REPO_URL=file:///usr/home/shane/images sudo -E vessel pull --tag=1.0 reweb 78 | ``` 79 | -------------------------------------------------------------------------------- /test/filerepo.test: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; -*- 2 | 3 | package require vessel::bsd 4 | package require vessel::env 5 | package require vessel::metadata_db 6 | package require vessel::repo 7 | package require vessel::zfs 8 | package require defer 9 | package require logger 10 | package require tcltest 11 | 12 | namespace eval file_repo::test { 13 | 14 | namespace import ::tcltest::* 15 | 16 | variable test_image_dataset_name {test_minimal} 17 | 18 | variable repo_dir_path [file join [pwd] test_repo] 19 | 20 | variable test_dir [file normalize testrun] 21 | 22 | proc create_test_image_dataset {tag} { 23 | 24 | #TODO: this was stolen from the vessel init command and needs to be 25 | #extracted into an initializaation module 26 | set from_version [vessel::bsd::host_version_without_patch] 27 | set minimal_build_file {} 28 | set minimal_build_file_chan [file tempfile minimal_build_file] 29 | defer::defer close $minimal_build_file_chan 30 | puts $minimal_build_file_chan "FROM FreeBSD:${from_version}" 31 | flush $minimal_build_file_chan 32 | 33 | set build_dict [dict create name minimal tag $from_version file $minimal_build_file] 34 | 35 | exec -ignorestderr vessel build --name=minimal --tag=${tag} --file=$minimal_build_file >&@ stderr 36 | } 37 | 38 | logger::setlevel debug 39 | 40 | #Create the repository directory 41 | file mkdir [file join [pwd] $repo_dir_path] 42 | 43 | #Repository tests 44 | test publish-file-repo-1 {Test file repo publish} -constraints {root} -setup { 45 | 46 | variable test_dir 47 | set test_tag {publishtest} 48 | 49 | #Create the image for testing 50 | create_test_image_dataset $test_tag 51 | 52 | set tmp_repo_path [file normalize [makeDirectory {publish_file_repo_1} $test_dir]] 53 | puts stderr $tmp_repo_path 54 | set env(VESSEL_REPO_URL) "file://${tmp_repo_path}" 55 | 56 | } -body { 57 | 58 | vessel::repo::repo_cmd publish [dict create image minimal tag $test_tag] 59 | 60 | } -returnCodes ok -cleanup { 61 | set env(VESSEL_REPO_URL) {} 62 | vessel::metadata_db::image_command [dict create rm minimal:${test_tag}] 63 | } 64 | 65 | test pull-file-repo-1 {Test file repo pull} -constraints {root} -setup { 66 | variable test_dir 67 | set test_tag {pulltest} 68 | 69 | set tmp_repo_path [file normalize [makeDirectory {pull_file_repo_1} $test_dir]] 70 | puts stderr $tmp_repo_path 71 | 72 | set env(VESSEL_REPO_URL) "file://${tmp_repo_path}" 73 | set env(VESSEL_WORKDIR) [makeDirectory {workdir_pull_file_repo_1} $test_dir] 74 | 75 | makeDirectory [vessel::env::metadata_db_dir] 76 | makeDirectory [vessel::env::image_download_dir] 77 | create_test_image_dataset $test_tag 78 | 79 | 80 | vessel::repo::repo_cmd publish [dict create image minimal tag $test_tag] 81 | vessel::metadata_db::image_command [dict create rm minimal:${test_tag}] 82 | 83 | } -body { 84 | 85 | vessel::repo::repo_cmd pull [dict create image minimal tag $test_tag] 86 | set result {} 87 | } -returnCodes ok -cleanup { 88 | set env(VESSEL_REPO_URL) {} 89 | vessel::metadata_db::image_command [dict create rm minimal:${test_tag}] 90 | } 91 | 92 | cleanupTests 93 | } 94 | -------------------------------------------------------------------------------- /docs/ExamplesTipsAndTricks.md: -------------------------------------------------------------------------------- 1 | 2 | # Tips and Tricks 3 | 4 | Some tips and tricks for working with vessel. 5 | 6 | # How to Develop Images to Run Applications 7 | 8 | Developing a VesselFile is straight forward: 9 | 10 | 1. Start an interactive prompt in a minimal image null mounting the application folder: `sudo -E vessel run --interactive --rm -v $PWD:/app minimal:12.3-RELEASE` 11 | 2. Install the application into the container following the installation process. Let's say `make install` for this example. 12 | 3. Install the packages needed to run the application. Note the packages in the VesselFile. Generally they are listed in a single `pkg install` command. 13 | ``` 14 | RUN env ASSUME_ALWAYS_YES=yes pkg update 15 | RUN env ASSUME_ALWAYS_YES=yes pkg install 16 | ``` 17 | 4. Run `sudo -E vessel build --file=./VesselFile --tag=latest --name=myapp` 18 | 5. Run the app command in the newly built container and verify that your applications runs properly. If your app doesn't run properly, start a shell in your newly built container and fix it (and update your VesselFile) 19 | 6. Generate a runtime definition file with the appropriate resources, volumes, jail options and cpusets 20 | 21 | # Dependencies with Vessel Supervisor 22 | 23 | Vessel supervisor does not yet have great container dependency support. The only way to simulate this is by setting `start-delay` values in the vessel-supervisor section of the runtime file. 24 | 25 | # Minimize Images 26 | 27 | When generating a vessel image, it's good practice to delete unnecessary files that are installed with packages. For instance the man pages like man pages. This allows for smaller file transfers when publishing and pulling images. It also takes less disk space. For tiny images, learn how to use FreeBSDs support for firstboot services. 28 | 29 | # FreeBSD Init System 30 | 31 | The most powerful tool to learn when working with vessel containers is the FreeBSD init system. Images should be started with rc files using the standard init tools. This includes `rcorder` and all of the functions defined in 32 | 33 | **Firstboot Service File Example** 34 | The following firstboot file installs and starts a postgres instance the first time a container is started. 35 | ``` 36 | #!/bin/sh 37 | 38 | # PROVIDE: postgresql_firstboot 39 | # REQUIRE: DAEMON 40 | # BEFORE: postgresql 41 | # KEYWORD: firstboot 42 | 43 | . /etc/rc.subr 44 | 45 | name="postgresql_firstboot" 46 | rcvar="postgresql_firstboot_enable" 47 | start_cmd=do_start 48 | 49 | do_start() 50 | { 51 | env ASSUME_ALWAYS_YES=yes pkg update 52 | env ASSUME_ALWAYS_YES=yes pkg install postgresql12-server 53 | chown postgres:postgres /var/db/postgres 54 | service postgresql initdb 55 | service postgresql start 56 | /usr/local/bin/psql -h localhost -U postgres -c "create database re;" 57 | } 58 | 59 | load_rc_config $name 60 | run_rc_command "$1" 61 | ``` 62 | 63 | # Debugging, Reviewing and Using jail.conf file 64 | 65 | Vessel attempts to work with existing system tools instead of replacing them. To work with the `jail` command vessel must generate jail.conf files. The jail.conf file for a jail is stored in `/var/run/vessel/jails/`. If your container fails to start, you can see the generated jail.conf file via stderr of vessel by using the `--debug` provided to the vessel command `vessel --debug run ...` 66 | 67 | **Example** 68 | `jail -r -f /var/run/vessel/jails/ ` 69 | -------------------------------------------------------------------------------- /src/native_deprecated/src/lib/mountpoint.cpp: -------------------------------------------------------------------------------- 1 | #include "mountpoint.h" 2 | 3 | #include "fs.h" 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | using namespace vessel; 12 | 13 | namespace 14 | { 15 | class base_mountpoint : public mountpoint 16 | { 17 | fs_path m_from; 18 | fs_path m_target; 19 | 20 | using options = std::map; 21 | options m_opts; 22 | 23 | int m_flags; 24 | 25 | std::string m_last_mount_err; 26 | 27 | protected: 28 | 29 | void add_option(const std::string& option, const std::string& value) 30 | { 31 | m_opts[option] = value; 32 | } 33 | 34 | public: 35 | 36 | std::string fspath() { 37 | 38 | return m_opts["fspath"]; 39 | } 40 | 41 | /** NOTE: I don't yet handle ufs which may require "from" 42 | * instead of "target" 43 | */ 44 | base_mountpoint(const fs_path& from, const fs_path& target, 45 | const std::string fs_type, int flags) 46 | : m_from(from), 47 | m_target(target), 48 | m_opts({{"target", m_from.str()}, 49 | {"fspath", m_target.str()}, 50 | {"fstype", fs_type}}), 51 | m_flags(flags), 52 | m_last_mount_err() 53 | {} 54 | 55 | fs_path from() override 56 | { 57 | return m_from; 58 | } 59 | 60 | fs_path target() override 61 | { 62 | return m_target; 63 | } 64 | 65 | int mount() override 66 | { 67 | char errmsg[255]; 68 | memset(errmsg, 0, sizeof(errmsg)); 69 | std::vector iovs; 70 | 71 | struct iovec iov; 72 | for(auto& option : m_opts) { 73 | 74 | iov.iov_base = (void*)option.first.c_str(); 75 | iov.iov_len = option.first.length() + 1; 76 | iovs.push_back(iov); 77 | 78 | iov.iov_base = (void*)option.second.c_str(); 79 | iov.iov_len = option.second.length() + 1; 80 | iovs.push_back(iov); 81 | 82 | std::cerr << option.first << "=" << option.second << std::endl; 83 | } 84 | 85 | iov.iov_base = (void*)"errmsg"; 86 | iov.iov_len = sizeof("errmsg"); 87 | iovs.push_back(iov); 88 | 89 | iov.iov_base = errmsg; 90 | iov.iov_len = sizeof(errmsg); 91 | iovs.push_back(iov); 92 | 93 | int err = ::nmount(iovs.data(), iovs.size(), m_flags); 94 | if(err == -1 && errmsg[0]) { 95 | m_last_mount_err = errmsg; 96 | } 97 | 98 | return err; 99 | } 100 | 101 | int unmount(int flags) override 102 | { 103 | return ::unmount(m_target.str().c_str(), flags); 104 | } 105 | 106 | std::string error_message() override 107 | { 108 | return m_last_mount_err; 109 | } 110 | }; 111 | } 112 | 113 | mountpoint::~mountpoint() 114 | {} 115 | 116 | std::unique_ptr vessel::create_nullfs_mount(fs_path from, 117 | fs_path target, 118 | int flags) 119 | { 120 | return std::make_unique(from, target, "nullfs", flags); 121 | } 122 | -------------------------------------------------------------------------------- /src/native_deprecated/src/app/app_containers.cpp: -------------------------------------------------------------------------------- 1 | #include "app_functions.h" 2 | #include "vessel_tcl.h" 3 | #include "cmdline.h" 4 | #include "container.h" 5 | #include "environment.h" 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | using namespace vessel; 12 | 13 | namespace { 14 | 15 | /** 16 | * @brief The registry class is responsible for fetching the 17 | * image stack from some source. That source may be filesystem, 18 | * ftp, https or some other source. 19 | * 20 | * NOTE: Remote zfs support would be a killer feature. i.e. using zsend 21 | * and zrecv to retrieve images. 22 | */ 23 | class registry 24 | { 25 | 26 | }; 27 | 28 | /** 29 | * @brief The image_stack class represents a stack of images 30 | * that need to be mounted ontop of each other. 31 | */ 32 | class image_stack 33 | { 34 | /* - stack of images that will be mounted 35 | * - mount(): mount the image stack using unionfs 36 | */ 37 | }; 38 | 39 | void validate_save_cmdline(commandline& cmdline) 40 | { 41 | 42 | } 43 | 44 | int run_main(int argc, char** argv) 45 | { 46 | Tcl_FindExecutable(argv[0]); 47 | 48 | /*Process commandline arguments 49 | * - name: name of the container 50 | * - image: Name of the filesystem image in the registry 51 | */ 52 | std::unique_ptr cmdline = commandline::parse(argc, argv); 53 | if(!cmdline) 54 | { 55 | exit(1); 56 | } 57 | 58 | /*VESSEL_REGISTRY is the url to the registry. It could potentially 59 | * support multiple different protocols 60 | * TODO: enable when we support the registry 61 | */ 62 | 63 | /*VESSEL_IMAGE_DIR is the directory where images are extracted to after 64 | * being downloaded from a registry*/ 65 | 66 | 67 | /*VESSEL_CONTAINER_DIR is where container directories are created. Inside 68 | * container directories lives the mounted image(s). The mount may be 69 | * a null mount from the image contained in VESSEL_IMAGE_DIR or it could be 70 | * vnode back mem disk, or zfs clone or any number of mount options. 71 | */ 72 | environment env; 73 | 74 | /*Next steps: 75 | * 1. bind the image to wherever it needs to go in the container 76 | * directory. 77 | * 2. Run tcl script to setup the jail (get script from command line. can be empty) 78 | * 3. Start the jail 79 | */ 80 | 81 | vessel::funcs::auto_unmount_ptr mountpoint = vessel::funcs::mount_container_image(*cmdline, env); 82 | 83 | vessel::jail the_jail(mountpoint->target(), cmdline->container); 84 | std::tuple child_ids = the_jail.fork_exec_jail(cmdline->container_cmd_args); 85 | 86 | pid_t child_pid = std::get<0>(child_ids); 87 | if(child_pid > 0) 88 | { 89 | int status = 0; 90 | pid_t wait_child_id = waitpid(child_pid, &status, 0); 91 | if(wait_child_id == -1) 92 | { 93 | std::cerr << "Error when waiting for child: " << strerror(errno) << std::endl; 94 | return 1; 95 | } 96 | } 97 | 98 | return 0; 99 | } 100 | } 101 | 102 | int main(int argc, char** argv) 103 | { 104 | int exit_code = 1; 105 | try 106 | { 107 | exit_code = run_main(argc, argv); 108 | } 109 | catch(std::exception& e) 110 | { 111 | std::cerr << "Fatal Error: " << e.what() << std::endl; 112 | } 113 | catch(...) 114 | { 115 | std::cerr << "Fatal Error: " << "Unspecified" << std::endl; 116 | } 117 | 118 | exit(exit_code); 119 | } 120 | -------------------------------------------------------------------------------- /src/dns/embdns.h: -------------------------------------------------------------------------------- 1 | #ifndef EMBDNS_H 2 | #define EMBDNS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace embdns { 10 | 11 | struct dns_header 12 | { 13 | static const int SIZE = 12; /**< Num bytes in a header*/ 14 | 15 | uint16_t id; 16 | bool qr; 17 | uint8_t opcode; 18 | bool aa; 19 | bool tc; 20 | bool rd; 21 | bool ra; 22 | uint8_t z; 23 | uint8_t rcode; 24 | uint16_t qdcount; 25 | uint16_t ancount; 26 | uint16_t nscount; 27 | uint16_t arcount; 28 | 29 | dns_header(); 30 | dns_header(const unsigned char* data, size_t size); 31 | bool is_query() const; 32 | 33 | dns_header& set_id(uint16_t id); 34 | 35 | dns_header& set_response(bool is_response); 36 | 37 | dns_header& set_opcode(uint8_t opcode); 38 | 39 | dns_header& set_authoritative(bool authoritative); 40 | 41 | dns_header& set_truncation(bool truncated); 42 | 43 | dns_header& set_recursion_desired(bool recursion); 44 | 45 | dns_header& set_recursion_available(bool recursion_available); 46 | 47 | dns_header& set_response_code(uint8_t rcode); 48 | 49 | dns_header& set_question_count(uint16_t qdcount); 50 | 51 | dns_header& set_answer_count(uint16_t ancount); 52 | 53 | dns_header& set_nscount(uint16_t nscount); 54 | 55 | dns_header& set_additional_records_count(uint16_t arcount); 56 | 57 | void serialize(unsigned char* buf, size_t buf_size); 58 | }; 59 | 60 | class dns_message 61 | { 62 | public: 63 | static const int MAX_SIZE = 512; 64 | protected: 65 | std::vector raw_bytes; 66 | 67 | dns_header m_header; 68 | 69 | dns_message(const unsigned char* buf, size_t msg_size); 70 | public: 71 | 72 | dns_message(); 73 | 74 | const dns_header& header() const; 75 | }; 76 | 77 | struct dns_query : public dns_message 78 | { 79 | std::string qname; /**< Queried name*/ 80 | 81 | uint16_t qtype; /**< Query type*/ 82 | 83 | uint16_t qclass; /**< Query class*/ 84 | 85 | dns_query(const unsigned char* data, size_t size); 86 | 87 | const std::vector& raw() const; 88 | }; 89 | 90 | struct dns_A_response : public dns_message 91 | { 92 | std::string name; 93 | in_addr_t addr; 94 | uint32_t ttl; 95 | 96 | /**Empty response*/ 97 | dns_A_response(uint16_t id, 98 | const std::string& name); 99 | 100 | dns_A_response(uint16_t id, 101 | const std::string& name, 102 | in_addr_t result_addr, 103 | uint32_t ttl); 104 | 105 | size_t serialize(std::array& msg_buf, 106 | const unsigned char* query, size_t query_size); 107 | }; 108 | 109 | dns_query parse_packet(const uint8_t* pkt, size_t size); 110 | 111 | 112 | size_t generate_response(std::array& pkt_buf, 113 | dns_query& assoc_query); 114 | 115 | size_t generate_response(std::array& pkt_buf, 116 | dns_query& assoc_query, /*Corresponding query*/ 117 | const std::string& address, 118 | uint32_t ttl); 119 | } 120 | #endif // EMBDNS_H 121 | -------------------------------------------------------------------------------- /prototypes/kqueue_process_track.cpp: -------------------------------------------------------------------------------- 1 | /*Quick prototype to ensure I understand how 2 | * kqueue works when tracking children*/ 3 | 4 | /*Takeaway: 5 | * 1. It is safe to set the trac event in the parent and ignore the race 6 | * condition of child exit. It appears the child exit event will still 7 | * be generated even if the child exits before the event is created. I 8 | * imagine this is because a child hasn't been reaped yet. 9 | * 10 | * 2. Any grandchild process that has exited before the child event was added 11 | * in the top level process, will produce events. IOW, the kernel does not 12 | * maintain a history of old processes and backfill the events (obviously). 13 | * 14 | * 3. Exit status is returned in data. 15 | * 16 | * 4. The flags field of the event have ONESHOT, CLEAR and EOF bits set. 17 | * 18 | * 5. User data is maintained across new processes. 19 | * 20 | * Event: pid=899,filter=fffb,flags=30,fflags=4,data=898,userdata=898 21 | * Event: pid=89a,filter=fffb,flags=8030,fflags=4,data=899,userdata=898 22 | * Event: pid=89a,filter=fffb,flags=8030,fflags=80000000,data=700,userdata=898 23 | * Event: pid=898,filter=fffb,flags=8030,fflags=80000000,data=700,userdata=898 24 | * Event: pid=899,filter=fffb,flags=8030,fflags=80000000,data=700,userdata=898 25 | */ 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | namespace 35 | { 36 | void fork_child(bool second_fork) 37 | { 38 | pid_t pid = fork(); 39 | switch(pid) 40 | { 41 | case -1: 42 | perror("fork_child"); 43 | break; 44 | case 0: 45 | if(second_fork) 46 | { 47 | sleep(1); 48 | fork_child(false); 49 | } 50 | break; 51 | default: 52 | /*parent*/ 53 | sleep(2); 54 | break; 55 | } 56 | 57 | exit(7); 58 | } 59 | 60 | void parent_main(int kq_fd, int child_pid) 61 | { 62 | std::cerr << "main child pid: " << child_pid << std::endl; 63 | while(true) 64 | { 65 | std::array events; 66 | int num_events = kevent(kq_fd, nullptr, 0, events.data(), events.size(), nullptr); 67 | 68 | std::cerr << std::hex; 69 | for(int i = 0; i < num_events; ++i) 70 | { 71 | std::cerr << "Event: pid=" << events[i].ident << ",filter=" << events[i].filter 72 | << ",flags=" << events[i].flags << ",fflags="<< std::hex << events[i].fflags 73 | << ",data=" << events[i].data << ",userdata=" << *(pid_t*)events[i].udata << std::endl; 74 | } 75 | } 76 | } 77 | } 78 | 79 | int main(int argc, char** argv) 80 | { 81 | int kq_fd = kqueue(); 82 | if(kq_fd == -1) 83 | { 84 | perror("kqueue"); 85 | exit(1); 86 | } 87 | 88 | struct kevent ev; 89 | 90 | pid_t pid = fork(); 91 | int num_events = 0; 92 | switch(pid) 93 | { 94 | case 0: 95 | /*child*/ 96 | break; 97 | default: 98 | 99 | EV_SET(&ev, pid, EVFILT_PROC, EV_ADD, NOTE_EXIT|NOTE_TRACK, 0, &pid); 100 | num_events = kevent(kq_fd, &ev, 1, nullptr, 0, nullptr); 101 | if(num_events == -1) 102 | { 103 | perror("kevent add"); 104 | exit(1); 105 | } 106 | parent_main(kq_fd, pid); 107 | break; 108 | case -1: 109 | 110 | break; 111 | } 112 | 113 | /*child*/ 114 | 115 | for(int i = 0; i < 2; ++i) 116 | { 117 | fork_child(true); 118 | sleep(2); 119 | } 120 | 121 | return 0; 122 | } 123 | -------------------------------------------------------------------------------- /src/lib/tcl/bsd.tcl: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | 3 | package require json 4 | package require dicttool 5 | 6 | namespace eval vessel::bsd { 7 | 8 | proc host_architecture {} { 9 | 10 | return [exec uname -m] 11 | } 12 | 13 | proc host_version {} { 14 | 15 | return [exec uname -r] 16 | } 17 | 18 | proc host_version_without_patch {} { 19 | 20 | #Example: 12.2-RELEASE-p3 21 | set complete_version [host_version] 22 | 23 | if {[string match *-*-p* $complete_version]} { 24 | 25 | set last_hyphen [string last - $complete_version] 26 | return [string range $complete_version 0 [expr $last_hyphen - 1]] 27 | 28 | } elseif {[string match *-* $complete_version]} { 29 | 30 | #Patch version is optional. The equivalent of -p0 is left off the version 31 | return $complete_version 32 | } else { 33 | return -code error -errorcode {SYS VERSION INVALID} "Invalid system version: $complete_version" 34 | } 35 | } 36 | 37 | proc null_mount {source_dir dest_dir} { 38 | 39 | if {$source_dir eq {}} { 40 | 41 | return -code error -errorcode {BSD MOUNT PARAM} "source_dir parameter is empty for nullfs mount" 42 | } 43 | 44 | if {$dest_dir eq {}} { 45 | 46 | return -code error -errorcode {BSD MOUNT PARAM} "dest_dir parameter is empty for nullfs mount" 47 | } 48 | file mkdir $dest_dir 49 | 50 | exec mount -t nullfs $source_dir $dest_dir 51 | } 52 | 53 | proc umount {path} { 54 | 55 | if {$path ne {} && [file exists $path]} { 56 | exec umount $path 57 | } 58 | } 59 | 60 | proc is_mountpoint {path} { 61 | set fs_json [exec df --libxo=json $path] 62 | set mountpoint_dicts [json::json2dict $fs_json] 63 | 64 | set mp_list [dict getnull $mountpoint_dicts storage-system-information filesystem] 65 | foreach mp_dict $mp_list { 66 | if {[dict get $mp_dict "mounted-on"] eq $path} { 67 | return true 68 | } 69 | } 70 | 71 | return false 72 | } 73 | 74 | proc mount_procfs {} { 75 | exec mount -t procfs /proc /proc 76 | } 77 | 78 | proc parse_devd_rctl_str {rctl_str} { 79 | # Parse the devd string and break it down into a dictionary if it 80 | # is from the rctl subsystem. Examples of devd strings are below 81 | 82 | # !system=ACPI subsystem=CMBAT type=\_SB_.PCI0.BAT0 notify=0x80 83 | # !system=CAM subsystem=periph type=error device=cd0 serial="VB2-01700376" cam_status="0xcc" scsi_status=2 scsi_sense="70 02 3a 00" CDB="00 00 00 00 00 00 " 84 | # !system=CAM subsystem=periph type=error device=cd0 serial="VB2-01700376" cam_status="0xcc" scsi_status=2 scsi_sense="70 02 3a 00" CDB="00 00 00 00 00 00 " 85 | # !system=RCTL subsystem=rule type=matched rule=jail:f55687e0-8475-4e3e-ada9-1197ee857536:wallclock:devctl=5 pid=1273 ruid=0 jail=f55687e0-8475-4e3e-ada9-1197ee857536 86 | 87 | set rctl_str [string trim $rctl_str] 88 | set matched [regexp {^!system=RCTL subsystem=rule type=matched rule=(jail:.*:.*:.*) pid=(\d+) ruid=(\d) jail=(.*)$} $rctl_str matched_str rule pid ruid jail] 89 | set rctl_dict [dict create] 90 | if {$matched} { 91 | 92 | set matched [regexp {^jail:(.*):(.*):(.*)$} $rule matched_str subjectid resource action] 93 | if (!$matched) { 94 | return -code error -errorCode {VESSEL RCTL RULE EINVAL} 95 | } 96 | set rule_dict [dict create "subjectid" $subjectid "resource" $resource "action" $action] 97 | set rctl_dict [dict create "rule" $rule_dict "pid" $pid "ruid" $ruid "jail" $jail] 98 | } 99 | 100 | return $rctl_dict 101 | } 102 | } 103 | 104 | package provide vessel::bsd 1.0.0 105 | -------------------------------------------------------------------------------- /src/native_deprecated/src/app/app_functions.cpp: -------------------------------------------------------------------------------- 1 | #include "app_functions.h" 2 | #include "fs.h" 3 | #include 4 | #include 5 | 6 | using namespace vessel::funcs; 7 | 8 | void vessel::funcs::auto_unmount(vessel::mountpoint* mnt) 9 | { 10 | if(mnt == nullptr) 11 | { 12 | return; 13 | } 14 | 15 | int err = mnt->unmount(MNT_WAIT); 16 | if(err) 17 | { 18 | std::cerr << "Error unmounting: " << strerror(errno) << std::endl; 19 | } 20 | } 21 | 22 | auto_unmount_ptr 23 | vessel::funcs::mount_container_image(commandline &cmdline, environment& env) 24 | { 25 | if(cmdline.container.empty()) 26 | { 27 | std::cerr << "ERROR: 'name' is required" << std::endl; 28 | commandline::usage(); 29 | exit(1); 30 | } 31 | 32 | if(cmdline.image.empty()) 33 | { 34 | std::cerr << "ERROR: 'image' is required" << std::endl; 35 | commandline::usage(); 36 | exit(1); 37 | } 38 | 39 | fs_path image_path = env.find_image(cmdline.image); 40 | if(image_path) 41 | { 42 | std::cerr << "Image found: " << image_path.str() << std::endl; 43 | } 44 | else 45 | { 46 | std::cerr << "No image found" << std::endl; 47 | } 48 | 49 | std::cerr << "container: " << cmdline.container << std::endl; 50 | fs_path container_dir = env.find_container(cmdline.container); 51 | if(container_dir) 52 | { 53 | std::cerr << "Container dir exists: " << container_dir.str() << std::endl; 54 | } 55 | else 56 | { 57 | throw std::runtime_error("NYI: Create container directory if it doesn't exist"); 58 | } 59 | 60 | auto_unmount_ptr mntpoint(create_nullfs_mount(image_path, container_dir, 0).release(), 61 | auto_unmount); 62 | 63 | std::cerr << "Mounting" << std::endl; 64 | int error = mntpoint->mount(); 65 | if(error) 66 | { 67 | std::cerr << "Mount failed: " << mntpoint->error_message() << std::endl 68 | << "system error: " << strerror(errno) << std::endl; 69 | exit(1); 70 | } 71 | 72 | return mntpoint; 73 | } 74 | 75 | void vessel::funcs::save_container_image(commandline& cmdline, environment& env) 76 | { 77 | if(!cmdline.do_save) 78 | { 79 | throw std::logic_error("do_save has not been requested"); 80 | } 81 | 82 | if(cmdline.container.empty()) 83 | { 84 | std::cerr << "--name required." << std::endl; 85 | commandline::usage(); 86 | exit(1); 87 | } 88 | 89 | fs_path container_dir = env.find_container(cmdline.container); 90 | if(!container_dir) 91 | { 92 | std::cerr << "container not found: " << cmdline.container << std::endl; 93 | exit(1); 94 | } 95 | 96 | fs_path image_dest{env.image_dir()}; 97 | image_dest += container_dir.basename(); 98 | image_dest.append_extension("img"); 99 | 100 | int ret = create_image(container_dir, image_dest); 101 | if(ret) 102 | { 103 | std::cerr << "Error creating image" << std::endl; 104 | exit(1); 105 | } 106 | 107 | int count = 0; 108 | archive_image(image_dest, env.archive_dir(), 109 | [&count](const compression_progress& progress) { 110 | if((count % 1000) == 0) { 111 | std::cerr << "File size: " << progress.file_size << std::endl 112 | << "Total read: " << progress.total_read << std::endl 113 | << "Total written: " << progress.total_compressed_written << std::endl 114 | << "Read: " << progress.bytes_read << std::endl 115 | << "Written: " << progress.bytes_written << std::endl 116 | << "% complete: " << ((float)progress.total_read / (float)progress.file_size) * 100 << std::endl 117 | << "Count: " << count << std::endl; 118 | 119 | } 120 | ++count; 121 | }); 122 | 123 | exit(ret); 124 | } 125 | -------------------------------------------------------------------------------- /src/lib/tcl/syslog.tcl: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | 3 | package require logger 4 | package require Syslog 5 | 6 | #NOTE: There's some copy and paste in this module but I'm not going 7 | # to worry about it now. 8 | 9 | namespace eval vessel::syslog { 10 | 11 | variable _ident {} 12 | 13 | namespace eval _ { 14 | 15 | proc log {level text} { 16 | #Text is actually a list from the logger subsystem. Example is: 17 | # 18 | # prototypes/logging.tcl[91636]: -_logger::service vessel::shane {hello my friend} 19 | 20 | variable ::vessel::syslog::_ident 21 | 22 | set ident {} 23 | if {${_ident} ne {}} { 24 | set ident ${_ident} 25 | } else { 26 | set ident [lindex $text 1] 27 | } 28 | 29 | syslog -facility local1 -ident $ident $level [lindex $text 2] 30 | } 31 | 32 | proc log_debug {txt} { 33 | log debug $txt 34 | } 35 | 36 | proc log_info {txt} { 37 | log info $txt 38 | } 39 | 40 | proc log_notice {txt} { 41 | log notice $txt 42 | } 43 | 44 | proc log_warn {txt} { 45 | log warning $txt 46 | } 47 | 48 | proc log_error {txt} { 49 | log error $txt 50 | } 51 | 52 | proc log_critical {txt} { 53 | log critical $txt 54 | } 55 | 56 | proc log_alert {txt} { 57 | log alert $txt 58 | } 59 | 60 | proc log_emergency {txt} { 61 | log emergency $txt 62 | } 63 | } 64 | 65 | proc enable {} { 66 | set log [logger::servicecmd vessel] 67 | ${log}::logproc debug [namespace current]::_::log_debug 68 | ${log}::logproc info [namespace current]::_::log_info 69 | ${log}::logproc notice [namespace current]::_::log_notice 70 | ${log}::logproc warn [namespace current]::_::log_warn 71 | ${log}::logproc error [namespace current]::_::log_error 72 | ${log}::logproc critical [namespace current]::_::log_critical 73 | ${log}::logproc alert [namespace current]::_::log_alert 74 | ${log}::logproc emergency [namespace current]::_::log_emergency 75 | } 76 | 77 | proc set_ident {ident} { 78 | variable _ident 79 | 80 | set _ident $ident 81 | } 82 | } 83 | 84 | namespace eval vessel::stderrlog { 85 | namespace eval _ { 86 | 87 | proc log {level text} { 88 | puts stderr [lindex $text 2] 89 | } 90 | 91 | proc log_debug {txt} { 92 | log debug $txt 93 | } 94 | 95 | proc log_info {txt} { 96 | log info $txt 97 | } 98 | 99 | proc log_notice {txt} { 100 | log notice $txt 101 | } 102 | 103 | proc log_warn {txt} { 104 | log warning $txt 105 | } 106 | 107 | proc log_error {txt} { 108 | log error $txt 109 | } 110 | 111 | proc log_critical {txt} { 112 | log critical $txt 113 | } 114 | 115 | proc log_alert {txt} { 116 | log alert $txt 117 | } 118 | 119 | proc log_emergency {txt} { 120 | log emergency $txt 121 | } 122 | } 123 | 124 | proc enable {} { 125 | set log [logger::servicecmd vessel] 126 | ${log}::logproc debug [namespace current]::_::log_debug 127 | ${log}::logproc info [namespace current]::_::log_info 128 | ${log}::logproc notice [namespace current]::_::log_notice 129 | ${log}::logproc warn [namespace current]::_::log_warn 130 | ${log}::logproc error [namespace current]::_::log_error 131 | ${log}::logproc critical [namespace current]::_::log_critical 132 | ${log}::logproc alert [namespace current]::_::log_alert 133 | ${log}::logproc emergency [namespace current]::_::log_emergency 134 | } 135 | } 136 | 137 | package provide vessel::syslog 1.0.0 -------------------------------------------------------------------------------- /src/dns/dns.tcl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tclsh8.6 2 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 3 | 4 | package require TclOO 5 | package require dicttool 6 | package require vessel::native 7 | 8 | namespace eval vessel::dns { 9 | 10 | namespace export create_server 11 | namespace ensemble create 12 | 13 | namespace eval _ { 14 | 15 | variable domain_store [dict create] 16 | 17 | oo::class create transform { 18 | 19 | constructor {} { 20 | 21 | } 22 | 23 | method initialize {handle mode} { 24 | 25 | return "initialize clear finalize read write" 26 | } 27 | 28 | method clear {} { 29 | 30 | } 31 | 32 | method finalize {handle} { 33 | 34 | } 35 | 36 | method limit {handle} { 37 | return 0 38 | } 39 | 40 | method read {handle buffer} { 41 | 42 | set query [vessel::dns::parse_query $buffer] 43 | return $query 44 | } 45 | 46 | method write {handle buffer} { 47 | 48 | if {![dict exists $buffer raw_query] || 49 | ![dict exists $buffer address] || 50 | ![dict exists $buffer ttl]} { 51 | return -code error -errorcode {DNS RESPONSE INVALID} \ 52 | "writing response to dns channel must be a dict with 'raw_query', 'address' and 'ttl' keys" 53 | } 54 | 55 | set addr [dict get $buffer address] 56 | set ttl [dict get $buffer ttl] 57 | set raw_query [dict get $buffer raw_query] 58 | # buffer should be an ipv4 address string or an empty string 59 | set response [vessel::dns::generate_A_response $addr $ttl $raw_query] 60 | return $response 61 | } 62 | 63 | destructor { 64 | 65 | puts "Transform destructor" 66 | } 67 | } 68 | 69 | oo::class create DNSServer { 70 | 71 | variable store [dict create] 72 | variable udp_channel 73 | variable message_channel 74 | 75 | constructor {port ip} { 76 | 77 | #Add support for listening on a specific ip 78 | #address 79 | set udp_channel [vessel::udp_open -myaddr $ip $port] 80 | fconfigure $udp_channel -translation binary -buffering none -blocking false 81 | 82 | #TODO: Proper way to delete this 83 | set dns_transform [vessel::dns::_::transform new] 84 | set message_channel [chan push $udp_channel $dns_transform] 85 | fileevent $udp_channel readable [list [self] pkt_ready] 86 | 87 | } 88 | 89 | method pkt_ready {} { 90 | 91 | #Callback method invoked when the udp channel has a packet ready 92 | 93 | #Read the packet 94 | set pkt [read $message_channel] 95 | 96 | #Configure the channel for response 97 | fconfigure $udp_channel -remote [fconfigure $udp_channel -peer] 98 | 99 | #Create the response from the stored entries 100 | set address {} 101 | set qname [dict getnull $pkt "qname"] 102 | if {$qname ne {}} { 103 | set entry [dict getnull $store $qname] 104 | set address [lindex $entry 0] 105 | set ttl [lindex $entry 1] 106 | } 107 | 108 | if {$ttl eq {}} { 109 | set ttl 0 110 | } 111 | 112 | set response [dict create] 113 | dict set response address $address 114 | dict set response raw_query [dict get $pkt raw_query] 115 | dict set response ttl $ttl 116 | 117 | #Respond 118 | puts -nonewline $message_channel $response 119 | flush $message_channel 120 | } 121 | 122 | method add_lookup_mapping {name ip {ttl 0}} { 123 | set store [dict set store $name [list $ip $ttl]] 124 | } 125 | 126 | destructor { 127 | close $udp_channel 128 | } 129 | 130 | } 131 | } 132 | 133 | proc create_server {{port 53} {ip {0.0.0.0}}} { 134 | 135 | return [_::DNSServer new $port $ip] 136 | } 137 | } 138 | 139 | package provide vessel::dns 1.0.0 140 | -------------------------------------------------------------------------------- /examples/dinnerrepo/lighttpd/modules.conf: -------------------------------------------------------------------------------- 1 | ####################################################################### 2 | ## 3 | ## Modules to load 4 | ## ----------------- 5 | ## 6 | ## Load only the modules needed in order to keep things simple. 7 | ## 8 | ## lighttpd automatically adds the following default modules 9 | ## to server.modules, if not explicitly listed in server.modules: 10 | ## "mod_indexfile", "mod_dirlisting", "mod_staticfile" 11 | ## 12 | ## You may disable automatic loading of default modules by setting 13 | ## server.compat-module-load = "disable" 14 | ## 15 | ## lighttpd provides many modules, and not all are listed below. Please see: 16 | ## https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ConfigurationOptions 17 | ## 18 | ## Modules, which are pulled in via conf.d/*.conf 19 | ## 20 | ## - mod_accesslog -> conf.d/access_log.conf 21 | ## - mod_deflate -> conf.d/deflate.conf 22 | ## - mod_status -> conf.d/status.conf 23 | ## - mod_webdav -> conf.d/webdav.conf 24 | ## - mod_evhost -> conf.d/evhost.conf 25 | ## - mod_simple_vhost -> conf.d/simple_vhost.conf 26 | ## - mod_userdir -> conf.d/userdir.conf 27 | ## - mod_rrdtool -> conf.d/rrdtool.conf 28 | ## - mod_ssi -> conf.d/ssi.conf 29 | ## - mod_cgi -> conf.d/cgi.conf 30 | ## - mod_scgi -> conf.d/scgi.conf 31 | ## - mod_fastcgi -> conf.d/fastcgi.conf 32 | ## - mod_proxy -> conf.d/proxy.conf 33 | ## - mod_secdownload -> conf.d/secdownload.conf 34 | ## - mod_expire -> conf.d/expire.conf 35 | ## 36 | ## NOTE: The order of modules in server.modules is important. 37 | ## 38 | ## Modules which gate requests (e.g. mod_access, mod_auth) or modify 39 | ## requests (e.g. mod_alias, mod_setenv) should be listed before 40 | ## modules which complete requests (e.g. mod_redirect, mod_rewrite), 41 | ## and which, in turn, should be listed before dynamic handlers 42 | ## (e.g. mod_cgi, mod_fastcgi, mod_proxy, mod_scgi, ...) 43 | ## 44 | ## DO NOT alphabetize modules. 45 | ## Alphabetizing may break expected functionality. See explanation above. 46 | ## 47 | 48 | server.modules = ( 49 | # "mod_rewrite", 50 | "mod_access", 51 | # "mod_evasive", 52 | # "mod_auth", 53 | # "mod_authn_file", 54 | # "mod_redirect", 55 | # "mod_setenv", 56 | # "mod_alias", 57 | ) 58 | 59 | ## 60 | ####################################################################### 61 | 62 | ####################################################################### 63 | ## 64 | ## Config for various Modules 65 | ## 66 | 67 | ## 68 | ## mod_expire 69 | ## 70 | #include conf_dir + "/conf.d/expire.conf" 71 | 72 | ## 73 | ## mod_deflate 74 | ## 75 | #include conf_dir + "/conf.d/deflate.conf" 76 | 77 | ## 78 | ## mod_magnet 79 | ## 80 | #include conf_dir + "/conf.d/magnet.conf" 81 | 82 | ## 83 | ## mod_ssi 84 | ## 85 | #include conf_dir + "/conf.d/ssi.conf" 86 | 87 | ## 88 | ## mod_status 89 | ## 90 | #include conf_dir + "/conf.d/status.conf" 91 | 92 | ## 93 | ## mod_webdav 94 | ## 95 | #include conf_dir + "/conf.d/webdav.conf" 96 | 97 | ## 98 | ## mod_userdir 99 | ## 100 | #include conf_dir + "/conf.d/userdir.conf" 101 | 102 | ## 103 | ## mod_rrdtool 104 | ## 105 | #include conf_dir + "/conf.d/rrdtool.conf" 106 | 107 | ## 108 | ## mod_secdownload 109 | ## 110 | #include conf_dir + "/conf.d/secdownload.conf" 111 | 112 | ## 113 | ####################################################################### 114 | 115 | ####################################################################### 116 | ## 117 | ## CGI/proxy modules 118 | ## 119 | 120 | ## 121 | ## mod_proxy 122 | ## 123 | #include conf_dir + "/conf.d/proxy.conf" 124 | 125 | ## 126 | ## SCGI (mod_scgi) 127 | ## 128 | include conf_dir + "/conf.d/scgi.conf" 129 | 130 | ## 131 | ## FastCGI (mod_fastcgi) 132 | ## 133 | #include conf_dir + "/conf.d/fastcgi.conf" 134 | 135 | ## 136 | ## plain old CGI (mod_cgi) 137 | ## 138 | #include conf_dir + "/conf.d/cgi.conf" 139 | 140 | ## 141 | ####################################################################### 142 | 143 | ####################################################################### 144 | ## 145 | ## VHost Modules 146 | ## 147 | ## Only load ONE of them! 148 | ## ======================== 149 | ## 150 | 151 | ## 152 | ## You can use conditionals for vhosts aswell. 153 | ## 154 | ## see https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_Configuration 155 | ## 156 | 157 | ## 158 | ## mod_evhost 159 | ## 160 | #include conf_dir + "/conf.d/evhost.conf" 161 | 162 | ## 163 | ## mod_simple_vhost 164 | ## 165 | #include conf_dir + "/conf.d/simple_vhost.conf" 166 | 167 | ## 168 | ####################################################################### 169 | -------------------------------------------------------------------------------- /test/broken_tests/metadata_db.test: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | 3 | package require vessel::env 4 | package require vessel::metadata_db 5 | package require vessel::zfs 6 | package require json 7 | package require json::write 8 | package require tcltest 9 | 10 | namespace eval metadata_db::test { 11 | namespace import ::tcltest::* 12 | 13 | #Use a specific dataset for testing 14 | set env(VESSEL_DATASET) vessel-test 15 | #set env(VESSEL_METADATA_DB_DIR) {/tmp/vessel-test/metadata_db} 16 | proc clean_dataset {} { 17 | 18 | set vessel_dataset [vessel::env::get_dataset] 19 | 20 | set dataset_children [vessel::zfs::dataset_children $vessel_dataset] 21 | 22 | set child_datasets_list [lrange $dataset_children 1 end] 23 | 24 | set vessel_children [list] 25 | #Destroy all child datasets except for the base FBSD dataset 26 | foreach dataset_props $child_datasets_list { 27 | set dataset_name [dict get $dataset_props name] 28 | set freebsd_index [string first "${vessel_dataset}/FreeBSD" $dataset_name 0] 29 | if {$freebsd_index == 0} { 30 | #Skip FreeBSD dataset 31 | continue 32 | } 33 | 34 | lappend vessel_children $dataset_name 35 | } 36 | 37 | set sorted_vessel_children [lsort -dictionary -unique -decreasing $vessel_children] 38 | 39 | foreach ds $sorted_vessel_children { 40 | 41 | #It's an vessel dataset but not the FreeBSD dataset. 42 | vessel::zfs::destroy_recursive $ds 43 | } 44 | } 45 | 46 | proc build_image {image tag} { 47 | set test_image_vessel_file {FROM FreeBSD:12.3-RELEASE 48 | CMD /usr/local/bin/bash -c {echo "fbsd is cool"}} 49 | set test_image_chan [file tempfile vessel_file /tmp/TestVesselFile] 50 | puts $test_image_chan $test_image_vessel_file 51 | close $test_image_chan 52 | 53 | exec vessel build --file $vessel_file --name $image --tag $tag >&@ stderr 54 | } 55 | 56 | proc init_runtime_environment {} { 57 | # Initialize runtime environment based on environment variables. This should 58 | # probably be a standalone module. 59 | 60 | file mkdir [vessel::env::image_download_dir] 61 | file mkdir [vessel::env::metadata_db_dir] 62 | } 63 | 64 | 65 | #configure -skip {image-list-output-1} 66 | 67 | init_runtime_environment 68 | test image-list-1 {Verify output from the image iteration proc} { 69 | 70 | } 71 | 72 | #Create a background container 73 | test image-list-output-1 {List all available images and their metadata} -setup { 74 | catch {clean_dataset} 75 | build_image list-images-test 1 76 | } -body { 77 | 78 | exec vessel image --list 79 | 80 | #ID PARENT CMD 81 | } -result {DATASET CMD PARENT 82 | zroot/vessel-test/FreeBSD:12.3-RELEASE /etc/rc 83 | zroot/vessel-test/list-images-test:1 /usr/local/bin/bash -c {echo "fbsd is cool"} FreeBSD:12.3-RELEASE} -cleanup { 84 | 85 | } 86 | 87 | test metadata_db-json-1 {Verify json format} -setup { 88 | json::write indented false 89 | } -body { 90 | vessel::metadata_db::_::create_metadata_json \ 91 | [dict create \ 92 | "name" "test-image" \ 93 | "tag" "1.0" \ 94 | "command" "bash" \ 95 | "cwd" "/home/joe" \ 96 | "parent_images" [list FreeBSD:11.1]] 97 | 98 | } -result {{"name":"test-image","tag":"1.0","command":["bash"],"cwd":"/home/joe","parent_images":["FreeBSD:11.1"]}} -cleanup { 99 | json::write indented true 100 | } 101 | 102 | test metadata_db-json-2 {Verify valid json is written to the metadata file} -body { 103 | set metadata_file [vessel::metadata_db::write_metadata_file \ 104 | test_image test {/} {/etc/rc} [list]] 105 | 106 | 107 | set d [json::json2dict [fileutil::cat $metadata_file]] 108 | expr [dict size $d] > 0 109 | } -result [expr 1] 110 | 111 | test metadata_db-json-2 {Verify json is read correctly} -setup { 112 | clean_dataset 113 | build_image list-images-test 1 114 | } -body { 115 | 116 | #NOTE: Working on getting command output right. 117 | vessel::metadata_db::_::list_images stdout 118 | } -output {DATASET CMD PARENT 119 | zroot/vessel-test/FreeBSD:12.3-RELEASE /etc/rc 120 | zroot/vessel-test/list-images-test:1 /usr/local/bin/bash -c {echo "fbsd is cool"} FreeBSD:12.3-RELEASE 121 | } -cleanup { 122 | 123 | } 124 | 125 | test {metadata_db-get-image-names-1} {Get a list of existing images} -setup { 126 | 127 | } -body { 128 | 129 | vessel::metadata_db::_::get_image_names 130 | } -result {zroot/vessel-test/FreeBSD:12.3-RELEASE zroot/vessel-test/list-images-test:1} 131 | 132 | cleanupTests 133 | } -------------------------------------------------------------------------------- /src/lib/tcl/build.tcl: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | package require uuid 3 | package require fileutil 4 | package require vessel::definition_file 5 | package require vessel::env 6 | package require vessel::import 7 | package require vessel::jail 8 | package require vessel::zfs 9 | 10 | namespace eval vessel::build { 11 | 12 | namespace eval _ { 13 | 14 | proc build_context_get {build_context build_interp key} { 15 | set existing_val [dict get $build_context $key] 16 | set val [$build_interp eval [list set $key]] 17 | if {$val eq {}} { 18 | set val $existing_val 19 | } 20 | 21 | return $val 22 | } 23 | 24 | proc transfer_build_context_from_interp {build_context build_interp} { 25 | 26 | dict set build_context current_dataset \ 27 | [$build_interp eval {set current_dataset}] 28 | 29 | dict set build_context guid [build_context_get $build_context $build_interp {guid}] 30 | 31 | dict set build_context cmd [build_context_get $build_context $build_interp {cmd}] 32 | 33 | dict set build_context cwd [build_context_get $build_context $build_interp {cwd}] 34 | 35 | dict set build_context parent_image [build_context_get $build_context $build_interp {parent_image}] 36 | 37 | return $build_context 38 | } 39 | 40 | proc execute_vessel_file {build_context vessel_file status_channel} { 41 | 42 | #Sourcing the vessel file calls the functions like RUN 43 | # and copy at a global level. We definitely need to do 44 | # that in a jail. 45 | set interp_name {build_interp} 46 | 47 | #Create the interp and setup the necessary global options 48 | interp create $interp_name 49 | defer::with [list interp_name] { 50 | interp delete $interp_name 51 | } 52 | interp share {} $status_channel $interp_name 53 | $interp_name eval { 54 | package require VesselFileCommands 55 | } 56 | $interp_name eval [list set ::status_channel $status_channel] 57 | $interp_name eval [list set ::cmdline_options [dict get $build_context cmdline_options]] 58 | $interp_name eval {source [dict get $::cmdline_options file]} 59 | set build_context [transfer_build_context_from_interp $build_context $interp_name] 60 | 61 | return [transfer_build_context_from_interp $build_context $interp_name] 62 | } 63 | } 64 | 65 | proc build_command {args_dict status_channel} { 66 | 67 | #build_context maintains the state for this build. All 68 | #of the global variables set by sourcing the vessel file 69 | #are copied into this dict. 70 | set build_context [dict create \ 71 | cmdline_options $args_dict \ 72 | from_called false \ 73 | parent_image {} \ 74 | current_dataset {} \ 75 | cwd {/} \ 76 | mountpoint {} \ 77 | name {} \ 78 | guid {} \ 79 | cmd {sh /etc/rc} \ 80 | status_channel $status_channel] 81 | 82 | set vessel_file [dict get $build_context cmdline_options {file}] 83 | 84 | #Clones the new dataset, runs commands and creates the '@a' snapshot 85 | set build_context [_::execute_vessel_file $build_context $vessel_file $status_channel] 86 | 87 | #current_dataset is set by the FROM command 88 | set current_dataset [dict get $build_context current_dataset] 89 | 90 | #Create the b snapshot that will be used to clone children containers 91 | set bsnapshot "${current_dataset}@b" 92 | if {[vessel::zfs::snapshot_exists $bsnapshot]} { 93 | vessel::zfs::destroy $bsnapshot 94 | } 95 | vessel::zfs::create_snapshot $current_dataset {b} 96 | 97 | set guid [dict get $build_context guid] 98 | set name $guid 99 | if {[dict exists $build_context cmdline_options name]} { 100 | set name [dict get $build_context cmdline_options name] 101 | } 102 | 103 | set tag {latest} 104 | if {[dict exists $build_context cmdline_options tag]} { 105 | set tag [dict get $build_context cmdline_options tag] 106 | } 107 | 108 | set cmd [dict get $build_context cmd] 109 | set cwd [dict get $build_context cwd] 110 | set parent_image [dict get $build_context parent_image] 111 | 112 | vessel::import::import_image_metadata $name $tag $cwd $cmd $parent_image 113 | } 114 | } 115 | 116 | package provide vessel::build 1.0.0 117 | -------------------------------------------------------------------------------- /src/lib/tcl/export.tcl: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | package require vessel::env 3 | package require vessel::metadata_db 4 | package require vessel::zfs 5 | 6 | package require fileutil 7 | package require logger 8 | package require uuid 9 | 10 | namespace eval vessel::export { 11 | 12 | logger::initNamespace [namespace current] debug 13 | variable log [logger::servicecmd [string trimleft [namespace current] :]] 14 | 15 | namespace eval _ { 16 | 17 | proc create_layer {dataset guid status_channel} { 18 | 19 | #Create the layer by zfs diff'ing the 'a' snapshot with the 20 | # dataset filesystem. A layer is a zip archive of 21 | # the differences in the snapshots. 22 | 23 | set mountpoint [vessel::zfs::get_mountpoint $dataset] 24 | set diff_dict [vessel::zfs::diff ${dataset}@a ${dataset}] 25 | 26 | set workdir [vessel::env::get_workdir] 27 | set build_dir [file join $workdir $guid] 28 | 29 | file mkdir $build_dir 30 | set old_dir [pwd] 31 | cd $build_dir 32 | set whiteout_file [open {whiteouts.txt} {w}] 33 | 34 | if {[dict exists $diff_dict {-}]} { 35 | foreach deleted_file [dict get $diff_dict {-}] { 36 | puts $whiteout_file [fileutil::stripPath $mountpoint $deleted_file] 37 | } 38 | } 39 | 40 | set modified_links_dict [vessel::zfs::diff_hardlinks $diff_dict $mountpoint] 41 | 42 | set tar_list_file [open {files.txt} {w}] 43 | 44 | #If the first line of the file is a '-C' then tar will chdir to the directory 45 | # specified in the second line of the file. 46 | puts $tar_list_file {-C} 47 | puts $tar_list_file "$mountpoint" 48 | 49 | dict for {inode paths} $modified_links_dict { 50 | 51 | #Put all the paths in the file to be tar'd up. 52 | foreach path $paths { 53 | puts $tar_list_file [fileutil::stripPath "${mountpoint}" $path] 54 | } 55 | } 56 | 57 | flush $tar_list_file 58 | close $tar_list_file 59 | exec -ignorestderr tar -cavf ${guid}-layer.tgz -n -T {files.txt} >&@ $status_channel 60 | 61 | #TODO: Use a trace to return to the old directory. Or better 62 | # yet, don't change directory 63 | cd $old_dir 64 | return $build_dir 65 | } 66 | 67 | proc create_image {image_name image_tag output_dir status_channel} { 68 | 69 | #Create the image metadata and zip it with the layer. An image 70 | # is the layer with the metadata zipped together. 71 | 72 | set output_image_path [file join $output_dir "${image_name}:${image_tag}.zip"] 73 | if {[file exists $output_image_path]} { 74 | #Short circuit if the image already exists. 75 | return $output_image_path 76 | } 77 | #Create the image layer. 78 | set dataset [vessel::env::get_dataset_from_image_name $image_name $image_tag] 79 | set guid [uuid::uuid generate] 80 | set image_dir [create_layer $dataset $guid $status_channel] 81 | 82 | set metadata_file [vessel::metadata_db::metadata_file_path $image_name $image_tag] 83 | file copy $metadata_file $image_dir 84 | set workdir [vessel::env::get_workdir] 85 | 86 | #Ensure we cd back to the initial directory. Should probably just 87 | # use tcllib::defer 88 | trace add variable workdir unset [list apply {{old_pwd _1 _2 _3} { 89 | cd $old_pwd 90 | } } [pwd]] 91 | 92 | cd $image_dir 93 | 94 | exec zip -v -r -m $output_image_path . >&@ $status_channel 95 | return $output_image_path 96 | } 97 | } 98 | 99 | proc export_image {image tag output_dir} { 100 | 101 | return [_::create_image $image $tag $output_dir stderr] 102 | } 103 | 104 | proc export_command {args_dict} { 105 | 106 | #TODO: Audit the code base and move all of this code into an 107 | # image class 108 | set image_w_tag [dict get $args_dict image] 109 | set image_components [split $image_w_tag :] 110 | set image_name [lindex $image_components 0] 111 | set image_tag {latest} 112 | if {[llength $image_components] > 1} { 113 | set image_tag [lindex $image_components 1] 114 | } 115 | 116 | set output_dir [dict get $args_dict dir] 117 | return [export_image $image_name $image_tag $output_dir] 118 | 119 | #TODO: We should get rid of the export command. It should just be part of 120 | #the publish command with a file:// scheme 121 | } 122 | } 123 | 124 | package provide vessel::export 1.0.0 125 | -------------------------------------------------------------------------------- /src/lib/tcl/vesseld_client.tcl: -------------------------------------------------------------------------------- 1 | #NOTE: This module is not used for anything, it should be 2 | #removed. It was written when vessel was going to be a daemon. 3 | 4 | #Client support for communicating with vesseld 5 | package require debug 6 | package require vessel::native 7 | package require vessel::pty_shell 8 | 9 | namespace eval vesseld::client { 10 | debug define client 11 | debug on client 1 stderr 12 | 13 | namespace eval _ { 14 | 15 | variable conn {} 16 | variable vwait_var 0 17 | 18 | #Connects to vesseld and returns a non-blocking channel 19 | proc connect {port} { 20 | 21 | debug.client "Connecting..." 22 | 23 | #TODO: Use unix socket so we can check permissions 24 | set vesseld_channel [socket {localhost} $port] 25 | debug.client "Connected" 26 | 27 | fconfigure $vesseld_channel -encoding {utf-8} \ 28 | -buffering line \ 29 | -translation crlf \ 30 | -blocking 0 31 | 32 | return $vesseld_channel 33 | } 34 | 35 | proc run_coroutine {options} { 36 | set args [dict get $options args] 37 | set interactive [dict get $args interactive] 38 | set vesseld_chan [connect 6432] 39 | set vesseld_msg [dict create cli_options $options pty {}] 40 | 41 | if {$interactive} { 42 | debug.client "Opening pty for interactive use" 43 | set ptys [pty::open] 44 | set master_chan [lindex $ptys 0] 45 | set slave_path [lindex $ptys 1] 46 | set vesseld_msg [dict set vesseld_msg pty $slave_path] 47 | } 48 | 49 | puts $vesseld_chan $vesseld_msg 50 | 51 | if {$interactive} { 52 | 53 | debug.client "Starting pty shell [info coroutine]" 54 | vessel::pty_shell::run $master_chan [info coroutine] 55 | 56 | debug.client "Yielding for shell to finish" 57 | yield 58 | 59 | debug.client "pty shell exited. Exiting event loop." 60 | variable vwait_var 61 | 62 | #TODO: Increment vwait var instead of directly exiting. 63 | exit 0 64 | } 65 | } 66 | 67 | proc pull_coroutine {options} { 68 | set vesseld_chan [connect 6432] 69 | set vesseld_msg [dict create cli_options $options] 70 | fileevent $vesseld_chan readable [info coroutine] 71 | 72 | #Send the request 73 | puts $vesseld_chan $vesseld_msg 74 | 75 | #Response loop 76 | while {true} { 77 | #Wait for a response or the connection to close 78 | yield 79 | 80 | gets $vesseld_chan msg 81 | 82 | if {$msg ne {}} { 83 | puts stdout $msg 84 | } else { 85 | if {[fblocked $vesseld_chan]} { 86 | continue 87 | } elseif {[eof $vesseld_chan]} { 88 | break 89 | } 90 | } 91 | } 92 | puts stderr "exiting" 93 | exit 0 94 | } 95 | 96 | proc build_coroutine {options} { 97 | set vesseld_chan [connect 6432] 98 | set vesseld_msg [dict create cli_options $options] 99 | fileevent $vesseld_chan readable [info coroutine] 100 | 101 | #Send the request 102 | puts $vesseld_chan $vesseld_msg 103 | 104 | fconfigure $vesseld_chan -blocking 0 -buffering none -translation binary 105 | 106 | #Response loop 107 | while {true} { 108 | #Wait for a response or the connection to close 109 | yield 110 | 111 | gets $vesseld_chan msg 112 | 113 | if {$msg ne {}} { 114 | puts stderr $msg 115 | } else { 116 | if {[fblocked $vesseld_chan]} { 117 | continue 118 | } elseif {[eof $vesseld_chan]} { 119 | break 120 | } 121 | } 122 | } 123 | puts stderr "exiting" 124 | exit 0 125 | } 126 | 127 | proc publish_coroutine {options} { 128 | set vesseld_chan [connect 6432] 129 | set vesseld_msg [dict create cli_options $options] 130 | fileevent $vesseld_chan readable [info coroutine] 131 | 132 | #Send the request 133 | puts $vesseld_chan $vesseld_msg 134 | 135 | fconfigure $vesseld_chan -blocking 0 -buffering none -translation binary 136 | 137 | #Response loop 138 | while {true} { 139 | #Wait for a response or the connection to close 140 | yield 141 | 142 | gets $vesseld_chan msg 143 | 144 | if {$msg ne {}} { 145 | puts stderr $msg 146 | } else { 147 | if {[fblocked $vesseld_chan]} { 148 | continue 149 | } elseif {[eof $vesseld_chan]} { 150 | break 151 | } 152 | } 153 | } 154 | puts stderr "exiting" 155 | exit 0 156 | } 157 | } 158 | 159 | proc main {options} { 160 | variable _::vwait_var 161 | 162 | if {[dict exists $options args help]} { 163 | puts stderr [dict get $options args help] 164 | exit 0 165 | } 166 | set command [dict get $options command] 167 | switch -exact $command { 168 | build { 169 | coroutine client_coro _::build_coroutine $options 170 | } 171 | publish { 172 | coroutine client_coro _::publish_coroutine $options 173 | } 174 | pull { 175 | coroutine client_coro _::pull_coroutine $options 176 | } 177 | run { 178 | coroutine client_coro _::run_coroutine $options 179 | } 180 | 181 | default { 182 | puts stderr "Unknown command: $command" 183 | } 184 | } 185 | 186 | vwait vwait_var 187 | } 188 | } 189 | 190 | package provide vesseld::client 1.0.0 191 | -------------------------------------------------------------------------------- /src/lib/tcl/deploy.tcl: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | 3 | package require defer 4 | package require inifile 5 | package require logger 6 | package require struct::set 7 | 8 | namespace eval vessel::deploy { 9 | 10 | namespace export poll_deploy_dir 11 | 12 | logger::initNamespace [namespace current] debug 13 | variable log [logger::servicecmd [string trimleft [namespace current] :]] 14 | 15 | namespace eval _ { 16 | 17 | #dictionary of statbuf arrays of deployment files 18 | variable deploy_files_dict [dict create] 19 | 20 | proc reset {} { 21 | #Reset the state of the module. used for testing, not public interface 22 | 23 | variable deploy_files_dict 24 | 25 | set deploy_files_dict [dict create] 26 | } 27 | } 28 | 29 | namespace eval ini { 30 | 31 | namespace eval _ { 32 | proc validate_section_name {name} { 33 | if {$name eq {vessel-supervisor}} { 34 | return 35 | } 36 | 37 | if {[string first {dataset:} $name] != -1} { 38 | return 39 | } 40 | 41 | if {[string first {nullfs:} $name] != -1} { 42 | return 43 | } 44 | 45 | if {$name eq {jail}} { 46 | return 47 | } 48 | 49 | if {[string first {resource:} $name] != -1} { 50 | return 51 | } 52 | 53 | if {$name eq {cpuset}} { 54 | return 55 | } 56 | 57 | return -code error -errorcode {VESSEL INI SECTION UNEXPECTED} \ 58 | "Unexpected section $name" 59 | } 60 | } 61 | 62 | proc get_deployment_dict {ini_file} { 63 | set fh [ini::open $ini_file] 64 | defer::with [list fh] { 65 | ini::close $fh 66 | } 67 | set sections [ini::sections $fh] 68 | 69 | set deployment_dict [dict create] 70 | foreach section $sections { 71 | dict set deployment_dict $section [ini::get $fh $section] 72 | _::validate_section_name $section 73 | } 74 | 75 | return $deployment_dict 76 | } 77 | 78 | #Return a list of parameters to be executed. 79 | proc parse_devctl_exec_string {devctl_str} { 80 | 81 | set matched [regexp {^exec[[:space:]]*=[[:space:]]*(.*)} $devctl_str matched_str cmd] 82 | 83 | if {!$matched} { 84 | return -code error -errorcode {INI DEVCTL PARSE} \ 85 | "Could not parse rctl exec string: $devctl_str" 86 | } 87 | 88 | return $cmd 89 | } 90 | } 91 | 92 | proc poll_deploy_dir {deploy_dir} { 93 | 94 | # Returns a dictionary with 'start', 'stop' and 'modified' keys. 95 | # Each key has a list of ini files to handle. 96 | 97 | variable log 98 | 99 | set change_dict [dict create "start" [list] "stop" [list] "modified" [list]] 100 | 101 | variable _::deploy_files_dict 102 | 103 | set ini_files [glob -nocomplain [file join $deploy_dir]/*.ini] 104 | #${log}::debug "Polling ini files: ${ini_files}" 105 | 106 | #${log}::debug "Deploy files dict: [dict keys $deploy_files_dict]" 107 | foreach f $ini_files { 108 | 109 | set deploy_file [file normalize $f] 110 | file stat $deploy_file statbuf 111 | 112 | if {$statbuf(type) eq {file}} { 113 | 114 | if {[dict exists $deploy_files_dict $deploy_file]} { 115 | 116 | set existing_mtime [dict get $deploy_files_dict $deploy_file "mtime"] 117 | 118 | if {$existing_mtime < $statbuf(mtime)} { 119 | dict lappend change_dict "modified" $deploy_file 120 | } elseif {$existing_mtime == $statbuf(mtime)} { 121 | #${log}::debug "File exists but hasn't changed" 122 | } else { 123 | ${log}::warn "Existing statbuf is newer then updated modification time... ignoring." 124 | } 125 | } else { 126 | 127 | dict lappend change_dict "start" $deploy_file 128 | } 129 | 130 | 131 | #save the deploy file to the deploy_files_dict so we can 132 | # check if anything changed in the next invocation. 133 | dict set deploy_files_dict $deploy_file [array get statbuf] 134 | } 135 | } 136 | 137 | # Figure out which files have been deleted by doing a set difference between 138 | # ini files and known ini files. 139 | set known_deploy_files [list] 140 | struct::set add known_deploy_files [dict keys $deploy_files_dict] 141 | set deleted_files [struct::set difference $known_deploy_files $ini_files] 142 | 143 | #${log}::debug "Known deploy files: $known_deploy_files" 144 | #${log}::debug "ini files: $ini_files" 145 | #${log}::debug "deleted files: $deleted_files" 146 | 147 | #Remove the deleted files from the deploy_files_dict 148 | foreach filename $deleted_files { 149 | dict lappend change_dict "stop" $filename 150 | dict unset deploy_files_dict $filename 151 | } 152 | return $change_dict 153 | } 154 | } 155 | 156 | package provide vessel::deploy 1.0.0 157 | -------------------------------------------------------------------------------- /src/apps/vessel: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/tclsh8.6 2 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 3 | # vim: set ft=tcl 4 | package require vessel::build 5 | package require vessel::env 6 | package require vessel::export 7 | package require vessel::native 8 | package require vessel::import 9 | package require vessel::metadata_db 10 | package require vessel::repo 11 | package require vessel::run 12 | package require vessel::syslog 13 | package require vessel::zfs 14 | 15 | package require logger 16 | package require defer 17 | 18 | package require fileutil 19 | 20 | set log [logger::init vessel] 21 | 22 | logger::setlevel info 23 | set options [vessel::parse_options $argv] 24 | 25 | if {[dict get $options pre_command_flags debug]} { 26 | logger::setlevel debug 27 | } 28 | 29 | if {[dict get $options pre_command_flags no-annotate-log]} { 30 | vessel::stderrlog::enable 31 | } elseif {[dict get $options pre_command_flags syslog]} { 32 | vessel::syslog::enable 33 | } 34 | 35 | set command [dict get $options command] 36 | set args [dict get $options args] 37 | 38 | if {[dict exists $args help]} { 39 | puts stderr [dict get $args help] 40 | exit 0 41 | } 42 | 43 | set pool [vessel::env::get_pool] 44 | if {![vessel::zfs::is_mounted $pool]} { 45 | 46 | if {[vessel::zfs::can_mount $pool]} { 47 | ${log}::debug "ZFS pool '$pool' is not mounted. Attempting to mount now" 48 | if {[catch {vessel::zfs::mount $pool} msg]} { 49 | ${log}::debug "Failed to mount pool: ${msg}. Moving on..." 50 | } 51 | } else { 52 | ${log}::debug "ZFS pool '$pool' is not mounted and canmount is off... moving on" 53 | } 54 | } 55 | 56 | if {![vessel::bsd::is_mountpoint /proc]} { 57 | ${log}::info "Mounting /proc" 58 | vessel::bsd::mount_procfs 59 | } 60 | 61 | #Make the runtime directories. 62 | file mkdir [vessel::env::vessel_run_dir] 63 | file mkdir [vessel::env::jail_confs_dir] 64 | 65 | set run_done_flag [expr false] 66 | 67 | #TODO: This belongs in the 'vessel init' command whenever 68 | # we decide to implement that.. which in turn belongs in the init module 69 | proc create_workdir {} { 70 | 71 | set workdir [vessel::env::get_workdir] 72 | if {![file exists $workdir]} { 73 | file mkdir $workdir 74 | file mkdir [file join $workdir local_repo] 75 | file mkdir [file join $workdir db] 76 | file mkdir [file join $workdir downloaded_images] 77 | } 78 | } 79 | 80 | proc create_ctrl_channel_from_env {} { 81 | global env 82 | 83 | set ctrl_channel {} 84 | if {[info exists env(VESSEL_CTRL_FD)]} { 85 | set ctrl_channel [vessel::get_supervisor_ctrl_channel] 86 | } 87 | 88 | return $ctrl_channel 89 | } 90 | 91 | #Creates the necessary constructs like channel dictionary 92 | # to invoke the run_command coroutine. 93 | proc run_container_coro {options} { 94 | global run_done_flag 95 | global log 96 | 97 | set interactive [dict get $options args interactive] 98 | 99 | set ctrl_channel [create_ctrl_channel_from_env] 100 | #Initialization yield 101 | yield 102 | 103 | if {$ctrl_channel ne {}} { 104 | ${log}::debug "ctrl channel: $ctrl_channel" 105 | puts $ctrl_channel "Testing ctrl channel" 106 | flush $ctrl_channel 107 | } 108 | 109 | if {$interactive} { 110 | set chan_dict [dict create stdin stdin stdout stdout stderr stderr ctrl $ctrl_channel] 111 | } else { 112 | set devnull [open /dev/null w] 113 | set chan_dict [dict create stdin $devnull stdout stdout stderr stderr ctrl $ctrl_channel] 114 | defer::with [list devnull] { 115 | close $devnull 116 | } 117 | } 118 | 119 | #Setup and yieldto the run coroutine. It will callback this coroutine after exiting 120 | coroutine runcoro vessel::run::run_command $chan_dict [dict get $options args] \ 121 | [info coroutine] 122 | yieldto runcoro 123 | set run_done_flag [expr true] 124 | } 125 | 126 | create_workdir 127 | 128 | proc usage {} { 129 | puts stderr \ 130 | { 131 | vessel [options] 132 | 133 | COMMANDS 134 | 135 | init Initialize the system to work with vessel. Including the installation of a base image 136 | build Build a container image from a vessel file 137 | run Run a container that has been built or can be pulled from a repository 138 | publish Export and push a container to a container repository 139 | pull Download and install a container from a container repository 140 | image Output image metadata 141 | help Print this help message 142 | } 143 | } 144 | 145 | switch -regexp -matchvar parsed_cmd $command { 146 | 147 | init { 148 | set from_version [vessel::bsd::host_version_without_patch] 149 | set minimal_build_file {} 150 | set minimal_build_file_chan [file tempfile minimal_build_file] 151 | puts $minimal_build_file_chan "FROM FreeBSD:${from_version}" 152 | flush $minimal_build_file_chan 153 | 154 | set build_dict [dict create name minimal tag $from_version file $minimal_build_file] 155 | vessel::build::build_command $build_dict stderr 156 | 157 | } 158 | build { 159 | #Building creates the image as a tagged dataset. 160 | #TODO: currently it doesn't tag the dataset 161 | vessel::build::build_command $args stderr 162 | } 163 | run { 164 | #Create a new container from a tagged dataset. 165 | set interactive [dict get $args interactive] 166 | coroutine toplevel_run_coro run_container_coro $options 167 | after idle toplevel_run_coro 168 | vwait run_done_flag 169 | } 170 | publish|pull { 171 | ${log}::debug "push pull args: $args" 172 | vessel::repo::repo_cmd $parsed_cmd $args 173 | } 174 | image { 175 | vessel::metadata_db::image_command $args 176 | } 177 | export { 178 | vessel::export::export_command $args 179 | } 180 | help { 181 | usage 182 | exit 0 183 | } 184 | {} { 185 | puts stderr "Command is required" 186 | usage 187 | exit 1 188 | } 189 | default { 190 | puts stderr "Unknown command: $command" 191 | usage 192 | exit 1 193 | } 194 | 195 | exit 0 196 | } 197 | -------------------------------------------------------------------------------- /prototypes/appcd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tclsh8.6 2 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 3 | package require Tcl 8.6 4 | package require TclOO 5 | package require appc::build 6 | package require appc::env 7 | package require appc::network 8 | package require appc::publish 9 | package require appc::pull 10 | package require appc::run 11 | package require debug 12 | package require debug::heartbeat 13 | package require defer 14 | package require dicttool 15 | 16 | #NOTES: Quick notes on first features of the daemon 17 | #since i'm in a plane and can't get online. 18 | # The first features we need: 19 | # 1. Pull an image 20 | # 2. Run a pulled image in the background and on a pty 21 | # 2a. Standard out and/or standard err should go 22 | # to syslog (or some log) 23 | # 2b. We don't yet support executing the command file that is 24 | # part of the package. We also don't delete the whitelist 25 | # files 26 | # 3. Run multiple containers and setup the networking 27 | # between them. 28 | # 3a. Run the dns server 29 | # 4. Monitor and close containers (cleanup) 30 | # 5. Other features like capsicum and ractl 31 | # 32 | # NOTE: The -h flag is going to be huge for this because it can use 33 | # dns to resolve the hostname of a jail to addresses 34 | # 35 | # NOTE: Name must be part of the jail. If it isn't provided then use the uid 36 | 37 | namespace eval appcd::_ { 38 | 39 | debug define appcd 40 | debug on appcd 1 stderr 41 | 42 | 43 | proc read_msgs_coro {channel clientaddr clientport cb_coro} { 44 | #Read a message from the channel and yield to the callback coroutine 45 | 46 | yield 47 | while {true} { 48 | set msg {} 49 | gets $channel msg 50 | if {[fblocked $channel]} { 51 | debug.appcd "Full msg not available. Waiting for more data" 52 | 53 | #No message available, setup the callback for when data becomes available 54 | chan event $channel readable [info coroutine] 55 | yield 56 | continue 57 | } elseif {[eof $channel]} { 58 | debug.appcd "EOF for channel found (${clientaddr}:${clientport}). Closing connection." 59 | 60 | chan event $channel readable {} 61 | 62 | #Yield an empty message which signals the connection has closed. The 63 | #receiving coroutine is responsible for deleting this coroutine 64 | yieldto $cb_coro {} 65 | } else { 66 | #Message available. 67 | 68 | #Delete the handle until we are ready for another message 69 | chan event $channel readable {} 70 | 71 | #Give the msg to the interested coroutine 72 | yieldto $cb_coro $msg 73 | } 74 | } 75 | 76 | return -code error -errorcode {APPCD UNEXPECTED} "Reached the end of message generator" 77 | } 78 | 79 | 80 | #For the first iteration, commands will just be line delimited tcl dicts 81 | # send over localhost. 82 | proc request_coroutine {channel clientaddr clientport} { 83 | 84 | set msg_generator_name ${channel}_msg_generator 85 | coroutine $msg_generator_name read_msgs_coro $channel $clientaddr $clientport [info coroutine] 86 | defer::with [list channel msg_generator_name] { 87 | close $channel 88 | rename $msg_generator_name {} 89 | } 90 | 91 | fconfigure $channel \ 92 | -encoding {utf-8} \ 93 | -blocking 0 \ 94 | -buffering line \ 95 | -buffersize 2048 \ 96 | -translation crlf 97 | 98 | #Initialization yield 99 | yield 100 | 101 | debug.appcd "Yielding to message generator" 102 | set msg [yieldto $msg_generator_name] 103 | 104 | #I'm not sure why but for some reason the message is a list 105 | #of length one containing a string that is the message 106 | set msg [lindex $msg 0] 107 | debug.appcd "Message received: $msg" 108 | if {$msg eq {}} { 109 | 110 | debug.appcd "No more messages available on channel. Exiting processing coroutine" 111 | return -code ok 112 | } 113 | 114 | set command [dict get $msg cli_options command] 115 | set args [dict get $msg cli_options args] 116 | set request_tag ${clientaddr}.${clientport}.${command} 117 | switch -exact $command { 118 | build { 119 | appc::build::build_command $args $request_tag $channel 120 | } 121 | publish { 122 | appc::publish::publish_command $args $request_tag $channel 123 | } 124 | pull { 125 | appc::pull::pull_command $args $channel 126 | } 127 | run { 128 | set pty [dict get $msg pty] 129 | set run_coro_name ${channel}.${clientaddr}.{clientport}_run_coro 130 | coroutine $run_coro_name appc::run::run_command $pty $args [info coroutine] 131 | yieldto $run_coro_name 132 | } 133 | create-network { 134 | appc::network::create_network $args 135 | } 136 | default { 137 | debug.appcd "Unknown command $command" 138 | } 139 | } 140 | } 141 | } 142 | 143 | proc new_connection {channel clientaddr clientport} { 144 | 145 | 146 | debug.appcd "New connection found" 147 | set coro_name ${channel}.${clientaddr}.${clientport}-request_coro 148 | coroutine $coro_name appcd::_::request_coroutine $channel $clientaddr $clientport 149 | 150 | #Start the coroutine outside of the context of this proc 151 | after idle $coro_name 152 | } 153 | 154 | #Needed for the heartbeat module 155 | proc every {ms body} { 156 | eval $body 157 | variable debug::timer [after $ms [info level 0]] 158 | return 159 | } 160 | 161 | proc bgerror {message} { 162 | debug.appcd "bgerror: $message" 163 | } 164 | 165 | proc init_zpool {} { 166 | #Init ZFS 167 | set pool [appc::env::get_pool] 168 | if {![appc::zfs::is_mounted $pool]} { 169 | debug.appcd "ZFS pool '$pool' is not mounted. Attempting to mount now" 170 | appc::zfs::mount $pool 171 | } 172 | } 173 | 174 | #TODO Unix socket so we can restrict connections based on 175 | #group permissions 176 | socket -server [list new_connection] -myaddr 127.0.0.1 6432 177 | debug::heartbeat [expr 1000 * 60] 178 | vwait _forever_ 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vessel 2 | Application containers for FreeBSD. 3 | 4 | The goal of vessel is to expose the powerful features of the FreeBSD operating system to application, operations and test engineers. Vessel accomplishes this goal by integrating tightly with FreeBSD's system level interfaces to provide a "docker-like" interface that feels familiar to most developers. Vessel differs from other jail management systems in that it runs alongside the container listening for system events that can be used for container management and observability. 5 | 6 | *Example Integrations* 7 | 8 | * Jail process tracing with kqueue. This allows us to run "fat jails" and non-interactive jails in the foreground. 9 | * Signal handling with kqueue. Trigger jail events (like shutdown) with signals. 10 | * Resource control events from `devd.seqpacket`. Execute custom commands based on resource events. 11 | * Container supervisor. Start, stop and restart jails by changing runtime files in deployment directory. 12 | * And more... 13 | 14 | # Feature Highlights 15 | 16 | |Feature | Implemented| 17 | |-----------------------------------------------------------------------------------|------------| 18 | | [VesselFile (similar to Dockerfile)](docs/ImageCreation.md) | Yes | 19 | | [Run configuration files (ini)](docs/RunningContainer.md#runtime-file) | Yes | 20 | | [Volume Management](docs/RunningContainer.md#volumes-and-datasets) | Yes | 21 | | [Image Push/Pull Repositories (s3)](docs/ImageCreation.md#impage-publish-and-pull)| Yes | 22 | | [Jail Management](docs/RunningContainer.md#jail-management) | Yes | 23 | | [Container Supervisor](docs/Supervisor.md) | Yes | 24 | | [Resource Control](docs/ResourceControl.md) | Yes | 25 | | [CPU Sets](docs/RunningContainer.md#cpu-sets) | Yes | 26 | | Internal (Bridged) Networking | In Progress| 27 | | DNS Service Discovery | In Progress| 28 | | Multi-node container orchestration | Not yet | 29 | | VNET Routing via PF | In Progress| 30 | 31 | * [Tips and Tricks](docs/ExamplesTipsAndTricks.md) 32 | * [Build, Install and Develop](docs/BuildAndInstall.md) 33 | 34 | # Installation 35 | 36 | Vessel follows a similar branching strategy as FreeBSD. The `master` branch is equivalent to FreeBSD's CURRENT branch. We also maintain a stable branch. The branch you build from depends on if you want cutting edge (master) features or the most stable (stable-) features. 37 | 38 | ## Building master from source (also works for stable): 39 | 40 | 1. Download the source from github (or clone the repository). 41 | 2. Building all dependencies (including cmake) can take a long time. To expediate the process it can be useful to install the build and runtime dependencies with `pkg`. An up-to-date list of dependencies can be found in the `ports/Makefile` file. `pkg update && pkg install curl tcl86 cmake tcllib py39-s3cmd tclsyslog` 42 | 3. From the source directory make the build directory: `mkdir build` 43 | 4. Change directory into the build dir and run cmake: `cd build` and `cmake ..` 44 | 5. Make and install vessel `make && sudo make install` 45 | 46 | ## Building stable port 47 | 48 | The port is maintained on the stable- branch. Building the port and all dependencies takes about 2 hours on a t3.micro so I would recommend installing the dependencies via packages. Port install requires a copy of `/usr/ports` to be populated. You can use `portsnap` for this. Once the `/usr/ports` is populated, you can build from the `/port` directory with the standard `sudo make install` or `sudo make pkg`. 49 | 50 | # Quickstart 51 | 52 | Initialize your user environment to work with vessel. The init command will create a minimal image by downloading the base tarball of the currently running container and installing it into a dedicated dataset. By default, vessel uses `zroot` as the pool for containers. To change the pool, you can set the `VESSEL_POOL` environment variable. 53 | 54 | `sudo -E vessel init` 55 | 56 | Run a shell in an ephemeral container using the previously created minimal image: 57 | 58 | `sudo -E vessel run --interactive --rm minimal:12.3-RELEASE -- sh` 59 | 60 | ## Vessel Files 61 | 62 | While quickly running a minimal container can be useful, it's generally more useful to create a customized container. For this, we can use a VesselFile which is similar to a DockerFile. Let's look at a VesselFile that creates a container to run a custom django application. Note that the modeline is set to tcl to allow for syntax highlighting 63 | 64 | ``` 65 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 66 | 67 | FROM FreeBSD:12.2-RELEASE 68 | 69 | RUN mkdir -p /usr/local/etc/pkg/repos 70 | COPY ./deployment/FreeBSD.conf /usr/local/etc/pkg/repos/FreeBSD.conf 71 | RUN env ASSUME_ALWAYS_YES=yes pkg update -f 72 | RUN env ASSUME_ALWAYS_YES=yes pkg install python 73 | RUN env ASSUME_ALWAYS_YES=yes pkg install py37-django-extensions \ 74 | py37-pandas uwsgi gnuplot-lite mime-support \ 75 | py37-django-bootstrap4 py37-Jinja2 py37-psycopg2 \ 76 | nginx 77 | 78 | RUN sysrc syslogd_enable=YES 79 | RUN sysrc uwsgi_enable=YES 80 | RUN sysrc uwsgi_configfile=/re/freebsd/uwsgi.ini 81 | RUN sysrc zfs_enable=YES 82 | RUN sysrc nginx_enable=YES 83 | RUN sysrc clear_tmp_enable=YES 84 | 85 | # We need to enable this kernel module via ansible first 86 | # RUN sysrc nginx_http_accept_enable=YES 87 | 88 | RUN sh -c "touch /var/log/all.log && chmod 600 /var/log/all.log" 89 | RUN mkdir -p /var/log/nginx 90 | COPY ./freebsd/syslog.conf /etc 91 | COPY ./freebsd/nginx.conf /usr/local/etc/nginx 92 | COPY . /re 93 | 94 | ``` 95 | 96 | To build the above image run: 97 | 98 | `sudo -E vessel build --file=./DjangoAppVesselFile --tag=1.0 --name=djangoapp` 99 | 100 | Once the image is built, it can be run with: 101 | 102 | `sudo -E vessel run --rm djangoapp:1.0 -- sh /etc/rc` 103 | 104 | This will start the init process in a new container running in the foreground. 105 | 106 | # Note On Networking 107 | 108 | The current version of vessel uses ipv4 inherited networking (IOW, the host network stack). In the future a bridged and vlan networking system with VNET will be implemented. For now, only inherited networking is used. 109 | 110 | > ℹ️ We have found that using inherited networking is not the major limitation that it seems. While a vnet based network would/could have it's uses, inherited network has met the majority of our use cases. 111 | 112 | -------------------------------------------------------------------------------- /src/lib/tcl/import.tcl: -------------------------------------------------------------------------------- 1 | # -*- mode: tcl; indent-tabs-mode: nil; tab-width: 4; -*- 2 | 3 | package require vessel::env 4 | package require vessel::metadata_db 5 | package require vessel::native 6 | package require defer 7 | package require fileutil 8 | package require json 9 | package require logger 10 | package require uri 11 | package require TclOO 12 | 13 | 14 | namespace eval vessel::import { 15 | #vessel::import is responsible for unpacking and "importing" 16 | #vessel images into the zfs pool 17 | 18 | logger::initNamespace [namespace current] debug 19 | variable log [logger::servicecmd [string trimleft [namespace current] :]] 20 | 21 | namespace eval _ { 22 | 23 | proc create_layer {image tag extracted_path status_channel} { 24 | variable ::vessel::import::log 25 | 26 | # We don't know the uuid so we glob in the image_dir and 27 | # use what should be the only thing there 28 | set layer_file_glob [glob "${extracted_path}/*-layer.tgz"] 29 | if {[llength $layer_file_glob] != 1} { 30 | return -code error -errorcode {VESSEL IMPORT LAYER} \ 31 | "Unexpected number of layer files in image: $layer_file_glob" 32 | } 33 | set layer_file [lindex $layer_file_glob 0] 34 | 35 | #Read parent image from metadata file 36 | set extracted_metadata_file [file join ${extracted_path} "${image}:${tag}.json"] 37 | set metadata_json [fileutil::cat $extracted_metadata_file] 38 | set metadata_dict [json::json2dict $metadata_json] 39 | 40 | #NOTE: The metadata file allows a list of parent images. 41 | #We current only support one layer of parent images. In 42 | #the future we can use arbitrarily long chain of parent 43 | #images. 44 | ${log}::debug "parsing metadata file" 45 | set parent_image [lindex [dict get $metadata_dict parent_images] 0] 46 | set parent_image_components [split $parent_image :] 47 | set parent_image_name [lindex $parent_image_components 0] 48 | set parent_image_version [lindex $parent_image_components 1] 49 | set parent_image_snapshot [vessel::env::get_dataset_from_image_name $parent_image_name $parent_image_version] 50 | set parent_image_snapshot "${parent_image_snapshot}@${parent_image_version}" 51 | 52 | ${log}::debug "parent image: $parent_image_snapshot" 53 | 54 | #TODO: fetch the parent image if it doesn't exist. We can 55 | #move the fetch_image command from the vessel_file_commands 56 | if {![vessel::zfs::snapshot_exists $parent_image_snapshot]} { 57 | return -code error -errorcode {NYI} \ 58 | "Pulling parent image is not yet implemented: '$parent_image_snapshot'" 59 | } 60 | 61 | #Clone parent filesystem 62 | set new_dataset [vessel::env::get_dataset_from_image_name $image $tag] 63 | if {![vessel::zfs::dataset_exists $new_dataset]} { 64 | ${log}::debug "Cloning parent image snapshot: '$parent_image_snapshot' to '$new_dataset'" 65 | vessel::zfs::clone_snapshot $parent_image_snapshot $new_dataset 66 | } else { 67 | ${log}::debug "New dataset already exists: $new_dataset" 68 | } 69 | 70 | if {![vessel::zfs::snapshot_exists ${new_dataset}@a]} { 71 | vessel::zfs::create_snapshot ${new_dataset} a 72 | } 73 | 74 | #Untar layer on top of parent file system 75 | set mountpoint [vessel::zfs::get_mountpoint $new_dataset] 76 | exec tar -C $mountpoint -xvf $layer_file >&@ $status_channel 77 | flush $status_channel 78 | 79 | #Read whiteout file and delete the listed files. 80 | # The whiteout file is the list of files from the base image that 81 | # need to be deleted. It's called whiteouts because that's how 82 | # unionfs works as it can't delete files in the lower layers. 83 | set whiteouts_file_path [file join ${extracted_path} {whiteouts.txt}] 84 | set whiteout_chan [open $whiteouts_file_path RDONLY] 85 | while {[gets $whiteout_chan deleteme_path] >= 0} { 86 | 87 | set jailed_path [fileutil::jail $mountpoint $deleteme_path] 88 | try { 89 | ${log}::debug "Deleting file $jailed_path" 90 | file delete -force $deleteme_path 91 | } on error {msg} { 92 | ${log}::debug "Failed to delete file $jailed_path: $msg" 93 | } 94 | } 95 | 96 | if {[vessel::zfs::snapshot_exists ${new_dataset}@b]} { 97 | #If the b snapshot already exists then we need to delete it and 98 | #make a new one. 99 | vessel::zfs::destroy ${new_dataset}@b 100 | } 101 | vessel::zfs::create_snapshot $new_dataset b 102 | 103 | #Very last thing is importing the image metadata. We import 104 | #instead of copying the file to safeguard against mismatching versions 105 | vessel::import::import_image_metadata_dict $metadata_dict 106 | } 107 | } 108 | 109 | proc import_image_metadata {name tag cwd cmd parent_images} { 110 | #Used when the image already exists (maybe it was built) and 111 | # we just need to store the metadata. 112 | 113 | vessel::metadata_db::write_metadata_file $name $tag $cwd $cmd $parent_images 114 | } 115 | 116 | proc import_image_metadata_dict {metadata_dict} { 117 | set name [dict get $metadata_dict name] 118 | set tag [dict get $metadata_dict tag] 119 | set command [dict get $metadata_dict command] 120 | set cwd [dict get $metadata_dict cwd] 121 | set parent_images [dict get $metadata_dict parent_images] 122 | 123 | import_image_metadata $name $tag $cwd $command $parent_images 124 | } 125 | 126 | # Import an image into the vessel environment. 127 | # 128 | # params: 129 | # 130 | # image_w_tag: The name of the image without tag appended 131 | # tag: The tag of the image 132 | # image_dir: The directory where the image file resides 133 | proc import {image tag image_dir status_channel} { 134 | variable log 135 | 136 | ${log}::debug "import{}: ${image},${tag}" 137 | 138 | set extracted_path [file join $image_dir "${image}:${tag}"] 139 | 140 | #Extract files into extracted_path overriding files that already exist. 141 | exec unzip -o -d $extracted_path [file join $image_dir "${image}:${tag}.zip"] >&@ $status_channel 142 | _::create_layer $image $tag $extracted_path $status_channel 143 | } 144 | } 145 | 146 | package provide vessel::import 1.0.0 147 | -------------------------------------------------------------------------------- /src/lib/native/tcl_kqueue.cpp: -------------------------------------------------------------------------------- 1 | #include "tcl_kqueue.h" 2 | #include "tcl_util.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | using namespace vessel; 14 | 15 | namespace 16 | { 17 | /** 18 | * @brief The kqueue_state struct simply manages if there is an event ready to be 19 | * processed in the kqueue. 20 | */ 21 | struct kqueue_state 22 | { 23 | int kqueue_fd; 24 | bool ready; 25 | 26 | kqueue_state(int kqueue_fd) 27 | : kqueue_fd(kqueue_fd), 28 | ready(false) 29 | {} 30 | 31 | ~kqueue_state() 32 | { 33 | if(kqueue_fd != -1) 34 | { 35 | close(kqueue_fd); 36 | } 37 | } 38 | }; 39 | 40 | 41 | /** 42 | * @brief KqueueEventReady The callback for the tcl file handler (fd notifier). 43 | * @param clientData 44 | * @param flags 45 | */ 46 | void KqueueEventReady(void *clientData, int flags) 47 | { 48 | kqueue_state* state = reinterpret_cast(clientData); 49 | state->ready = true; 50 | } 51 | 52 | /** 53 | * @brief KqueueEventSetupProc The setup proc for the tcl event loop integration. 54 | * @param clientData 55 | * @param flags 56 | */ 57 | void KqueueEventSetupProc(void *clientData, int flags) 58 | { 59 | (void)flags; 60 | 61 | kqueue_state* state = (kqueue_state*)clientData; 62 | if(state == nullptr || state->kqueue_fd == -1) 63 | { 64 | return; 65 | } 66 | 67 | Tcl_CreateFileHandler(state->kqueue_fd, TCL_READABLE, KqueueEventReady, state); 68 | } 69 | 70 | 71 | /** 72 | * @brief KqueueEventCheckProc Handles available kqueue events. We know if 73 | * an event is ready based on the kqueue_state which is set by the file event 74 | * handler. 75 | * @param clientData 76 | * @param flags 77 | */ 78 | void KqueueEventCheckProc(void *clientData, int flags) 79 | { 80 | (void)flags; 81 | 82 | kqueue_state* state = reinterpret_cast(clientData); 83 | assert(state); 84 | 85 | if(!state->ready) 86 | { 87 | return; 88 | } 89 | state->ready = false; 90 | 91 | /*Kqueue is ready. Get the available events and process them.*/ 92 | std::array events = {}; 93 | 94 | /*Use timespec instead of nullptr so kevent doesn't block*/ 95 | timespec spec; 96 | spec.tv_sec = 0; 97 | spec.tv_nsec = 0; 98 | ssize_t event_count = kevent(state->kqueue_fd, nullptr, 0, 99 | events.data(), events.size(), 100 | &spec); 101 | 102 | if(event_count == -1) 103 | { 104 | /*TODO: We should use tcl background errors instead of c++ exceptions*/ 105 | std::ostringstream msg; 106 | msg << "Error occurred while retrieving kevents: " << strerror(errno); 107 | std::cerr << msg.str() << std::endl; 108 | throw std::runtime_error(msg.str()); 109 | } 110 | 111 | for(int i = 0; i < event_count; ++i) 112 | { 113 | if(events[i].udata == nullptr) 114 | { 115 | throw std::runtime_error("Missing user data in kevent"); 116 | } 117 | 118 | /*NOTE: Events need to be allocated by Tcl_Alloc. Copy the enqueue_signal_event for how to 119 | * add do new placement and allocation correctly.*/ 120 | tcl_event_factory* factory = reinterpret_cast(events[i].udata); 121 | tcl_event_ptr event = factory->create_tcl_event(events[i]); 122 | Tcl_QueueEvent(event.release(), TCL_QUEUE_TAIL); 123 | } 124 | } 125 | 126 | void KqueueDeleteProc(void *clientData, Tcl_Interp *interp) 127 | { 128 | int kqueue_fd = -1; 129 | Tcl_Obj* fd_obj = (Tcl_Obj*)clientData; 130 | int tcl_error = Tcl_GetIntFromObj(interp, fd_obj, &kqueue_fd); 131 | 132 | if(!tcl_error) 133 | { 134 | Tcl_DecrRefCount(fd_obj); 135 | close(kqueue_fd); 136 | } 137 | } 138 | 139 | class tcl_kqueue 140 | { 141 | 142 | kqueue_state m_kq_state; 143 | Tcl_Interp* m_interp; 144 | 145 | public: 146 | tcl_kqueue(Tcl_Interp* interp) 147 | : m_kq_state(kqueue()), 148 | m_interp(interp) 149 | { 150 | if(m_kq_state.kqueue_fd != -1) 151 | { 152 | Tcl_CreateEventSource(KqueueEventSetupProc, KqueueEventCheckProc, this); 153 | } 154 | } 155 | 156 | /* Note that the consumer should not use event.udata, they should use the subclass of Tcl_Event 157 | * for any context they need.*/ 158 | int add_kevent(struct kevent& event, tcl_event_factory& event_factory) 159 | { 160 | /*NOTE it's up to the consumer to ensure the event_factory object lifetime 161 | * is longer then the event lives in kqueue*/ 162 | event.udata = &event_factory; 163 | int error = kevent(m_kq_state.kqueue_fd, &event, 1, nullptr, 0, nullptr); 164 | if(error == -1) 165 | { 166 | return vessel::syserror_result(m_interp, "EXEC", "MONITOR", "KQUEUE"); 167 | } 168 | return TCL_OK; 169 | } 170 | 171 | int remove_kevent(struct kevent& event) 172 | { 173 | int error = kevent(m_kq_state.kqueue_fd, &event, 1, nullptr, 0, nullptr); 174 | if(error == -1) 175 | { 176 | return vessel::syserror_result(m_interp, "EXEC", "MONITOR", "KQUEUE"); 177 | } 178 | return TCL_OK; 179 | } 180 | 181 | ~tcl_kqueue() 182 | { 183 | /*Tcl ignores the case when the event source doesn't exist.*/ 184 | Tcl_DeleteEventSource(KqueueEventSetupProc, KqueueEventCheckProc, this); 185 | } 186 | }; 187 | } 188 | 189 | int vessel::Kqueue_Init(Tcl_Interp* interp) 190 | { 191 | Tcl_SetAssocData(interp, "TclKqueueContext", vessel::cpp_delete_with_interp, new tcl_kqueue(interp)); 192 | return TCL_OK; 193 | } 194 | 195 | int vessel::Kqueue_Add_Event(Tcl_Interp* interp, struct kevent& event, tcl_event_factory& event_factory) 196 | { 197 | tcl_kqueue* kq = reinterpret_cast(Tcl_GetAssocData(interp, "TclKqueueContext", nullptr)); 198 | kq->add_kevent(event, event_factory); 199 | return TCL_OK; 200 | } 201 | 202 | int vessel::Kqueue_Remove_Event(Tcl_Interp* interp, struct kevent& event) 203 | { 204 | tcl_kqueue* kq = reinterpret_cast(Tcl_GetAssocData(interp, "TclKqueueContext", nullptr)); 205 | return kq->remove_kevent(event); 206 | } 207 | -------------------------------------------------------------------------------- /src/lib/native/pty.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Pseudo TTY library 3 | * 4 | * Copyright (C) 2015 Lawrence Woodman 5 | * 6 | * Licensed under an MIT licence. Please see LICENCE.md for details. 7 | * 8 | */ 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include "tcl_util.h" 18 | #include 19 | 20 | namespace { 21 | 22 | int 23 | Pty_Open(ClientData clientData, 24 | Tcl_Interp *interp, 25 | int objc, 26 | Tcl_Obj *CONST objv[]) 27 | { 28 | intptr_t masterfd; 29 | char* slaveName; 30 | Tcl_Channel masterChan; 31 | char *argStr; 32 | int lengthArgStr; 33 | int argNum; 34 | int nogrant = 0; 35 | int numArgsLeft; 36 | Tcl_Obj *result; 37 | 38 | for (argNum = 1; argNum < objc; argNum++) { 39 | argStr = Tcl_GetStringFromObj (objv[argNum], &lengthArgStr); 40 | if (argStr[0] != '-') { 41 | break; 42 | } 43 | 44 | if (strncmp(argStr, "-nogrant", lengthArgStr) == 0) { 45 | nogrant = 1; 46 | } else { 47 | Tcl_AppendResult(interp, 48 | "bad option \"", argStr, 49 | "\": must be -nogrant", 50 | (char *) NULL); 51 | return TCL_ERROR; 52 | } 53 | } 54 | 55 | numArgsLeft = objc - argNum; 56 | 57 | if (numArgsLeft != 0) { 58 | Tcl_WrongNumArgs(interp, 1, objv, "?options?"); 59 | return TCL_ERROR; 60 | } 61 | 62 | masterfd = posix_openpt(O_RDWR | O_NOCTTY); 63 | 64 | if (masterfd == -1) { 65 | Tcl_AppendResult(interp, 66 | "posix_openpt failed \"", 67 | Tcl_ErrnoMsg(Tcl_GetErrno()), "\"", 68 | (char *) NULL); 69 | return TCL_ERROR; 70 | } 71 | 72 | if (!nogrant) { 73 | if (grantpt(masterfd) != 0) { 74 | Tcl_AppendResult(interp, 75 | "grantpt failed \"", 76 | Tcl_ErrnoMsg(Tcl_GetErrno()), "\"", 77 | (char *) NULL); 78 | return TCL_ERROR; 79 | } 80 | } 81 | 82 | if (unlockpt(masterfd) != 0) { 83 | Tcl_AppendResult(interp, 84 | "unlockpt failed \"", 85 | Tcl_ErrnoMsg(Tcl_GetErrno()), "\"", 86 | (char *) NULL); 87 | return TCL_ERROR; 88 | } 89 | 90 | slaveName = ptsname(masterfd); 91 | if (slaveName == NULL) { 92 | Tcl_AppendResult(interp, 93 | "ptsname failed \"", 94 | Tcl_ErrnoMsg(Tcl_GetErrno()), "\"", 95 | (char *) NULL); 96 | return TCL_ERROR; 97 | } 98 | Tcl_Obj* slaveNameObj = Tcl_NewStringObj(slaveName, -1); 99 | masterChan = 100 | Tcl_MakeFileChannel((ClientData)masterfd, TCL_READABLE | TCL_WRITABLE); 101 | 102 | if (masterChan == (Tcl_Channel)NULL) { 103 | return TCL_ERROR; 104 | } 105 | 106 | Tcl_RegisterChannel(interp, masterChan); 107 | 108 | result = Tcl_NewListObj(0, NULL); 109 | 110 | Tcl_ListObjAppendElement(interp, 111 | result, 112 | Tcl_NewStringObj(Tcl_GetChannelName(masterChan), 113 | -1)); 114 | Tcl_ListObjAppendElement(interp, result, slaveNameObj); 115 | Tcl_SetObjResult(interp, result); 116 | 117 | return TCL_OK; 118 | } 119 | 120 | 121 | int 122 | Pty_MakeRaw(ClientData clientData, 123 | Tcl_Interp *interp, 124 | int objc, 125 | Tcl_Obj *CONST objv[]) 126 | { 127 | if(objc != 2) 128 | { 129 | Tcl_WrongNumArgs(interp, objc, objv, "channel"); 130 | return TCL_ERROR; 131 | } 132 | 133 | Tcl_Obj* chan_name = objv[1]; 134 | 135 | long handle = -1; 136 | int tcl_error = vessel::get_handle_from_channel(interp, chan_name, handle); 137 | if(tcl_error) return tcl_error; 138 | 139 | termios tios; 140 | int error = tcgetattr((int)handle, &tios); 141 | if(error == -1) 142 | { 143 | return vessel::syserror_result(interp, "PTY", "TCGETATTR"); 144 | } 145 | 146 | /*Create byte array that can be used for restoring settings*/ 147 | vessel::tclobj_ptr old_tios = vessel::create_tclobj_ptr(Tcl_NewByteArrayObj((unsigned char*)&tios, sizeof(tios))); 148 | 149 | cfmakeraw(&tios); 150 | error = tcsetattr((int)handle, TCSANOW, &tios); 151 | if(error == -1) 152 | { 153 | return vessel::syserror_result(interp, "PTY", "TCSETATTR"); 154 | } 155 | 156 | Tcl_IncrRefCount(old_tios.get()); 157 | Tcl_SetObjResult(interp, old_tios.release()); 158 | return TCL_OK; 159 | } 160 | 161 | int 162 | Pty_CopyWindowSize(ClientData clientData, 163 | Tcl_Interp *interp, 164 | int objc, 165 | Tcl_Obj *CONST objv[]) 166 | { 167 | if(objc != 3) 168 | { 169 | Tcl_WrongNumArgs(interp, objc, objv, "src_chan dest_chan"); 170 | return TCL_ERROR; 171 | } 172 | 173 | Tcl_Obj* src_chan_name = objv[1]; 174 | long src_handle = -1; 175 | int tcl_error = vessel::get_handle_from_channel(interp, src_chan_name, src_handle); 176 | if(tcl_error) return tcl_error; 177 | 178 | Tcl_Obj* dest_chan_name = objv[2]; 179 | long dest_handle = -1; 180 | tcl_error = vessel::get_handle_from_channel(interp, dest_chan_name, dest_handle); 181 | if(tcl_error) return tcl_error; 182 | 183 | winsize ws; 184 | memset(&ws, 0, sizeof(winsize)); 185 | int error = ioctl((int)src_handle, TIOCGWINSZ, &ws); 186 | if(error == -1) 187 | { 188 | return vessel::syserror_result(interp, "PTY", "WINSZ", "TIOCGWINSZ"); 189 | } 190 | 191 | error = ioctl((int)dest_handle, TIOCSWINSZ, &ws); 192 | if(error == -1) 193 | { 194 | return vessel::syserror_result(interp, "PTY", "WINSZ", "TIOCSWINSZ"); 195 | } 196 | return TCL_OK; 197 | } 198 | 199 | int 200 | Pty_TermiosRestore(ClientData clientData, 201 | Tcl_Interp *interp, 202 | int objc, 203 | Tcl_Obj *CONST objv[]) 204 | { 205 | if(objc != 3) 206 | { 207 | Tcl_WrongNumArgs(interp, objc, objv, "channel termios_binary"); 208 | return TCL_ERROR; 209 | } 210 | 211 | long handle = 0; 212 | int tcl_error = vessel::get_handle_from_channel(interp, objv[1], handle); 213 | if(tcl_error) return tcl_error; 214 | 215 | int length = 0; 216 | unsigned char* termios_bytes = Tcl_GetByteArrayFromObj(objv[2], &length); 217 | int error = tcsetattr((int)handle, TCSANOW, (termios*)termios_bytes); 218 | if(error == -1) 219 | { 220 | return vessel::syserror_result(interp, "PTY", "TCSETATTR"); 221 | } 222 | 223 | return TCL_OK; 224 | } 225 | } 226 | 227 | extern "C" 228 | { 229 | int 230 | Pty_Init(Tcl_Interp *interp) 231 | { 232 | Tcl_CreateObjCommand(interp, 233 | "pty::open", 234 | Pty_Open, 235 | (ClientData) NULL, 236 | (Tcl_CmdDeleteProc *) NULL); 237 | 238 | Tcl_CreateObjCommand(interp, 239 | "pty::makeraw", 240 | Pty_MakeRaw, 241 | (ClientData) NULL, 242 | (Tcl_CmdDeleteProc *) NULL); 243 | 244 | Tcl_CreateObjCommand(interp, 245 | "pty::copy_winsz", 246 | Pty_CopyWindowSize, 247 | (ClientData) NULL, 248 | (Tcl_CmdDeleteProc *) NULL); 249 | 250 | Tcl_CreateObjCommand(interp, 251 | "pty::restore", 252 | Pty_TermiosRestore, 253 | (ClientData) NULL, 254 | (Tcl_CmdDeleteProc *) NULL); 255 | 256 | if ( Tcl_PkgProvide(interp, "pty", "0.1") != TCL_OK ) { 257 | return TCL_ERROR; 258 | } 259 | 260 | return TCL_OK; 261 | } 262 | } 263 | --------------------------------------------------------------------------------