Compare commits
3 Commits
main
...
3f3ad43cb2
Author | SHA1 | Date | |
---|---|---|---|
|
3f3ad43cb2 | ||
|
15c5c904a2 | ||
|
81d4a35b24 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +1,4 @@
|
|||||||
*-bin
|
*-bin
|
||||||
*admin.yml*
|
*-admin.tgz*
|
||||||
*bootstrap.yml*
|
*-bootstrap.tgz
|
||||||
result
|
result
|
||||||
|
4
AppDir/AppRun
Executable file
4
AppDir/AppRun
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
export PATH=$APPDIR/bin
|
||||||
|
exec cryptic-net-main entrypoint "$@"
|
9
AppDir/bin/wait-for-ip
Normal file
9
AppDir/bin/wait-for-ip
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
ip="$1"
|
||||||
|
shift;
|
||||||
|
|
||||||
|
echo "waiting for $ip to become available..."
|
||||||
|
|
||||||
|
while true; do ping -c1 -W1 "$ip" &> /dev/null && break; done
|
||||||
|
|
||||||
|
exec "$@"
|
@ -1,6 +1,6 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=Isle
|
Name=Cryptic Net
|
||||||
Name[en]=Isle
|
Name[en]=Cryptic Net
|
||||||
Exec=AppRun
|
Exec=AppRun
|
||||||
|
|
||||||
Icon=cryptic-logo
|
Icon=cryptic-logo
|
76
AppDir/etc/daemon.yml
Normal file
76
AppDir/etc/daemon.yml
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
|
||||||
|
#
|
||||||
|
# This file defines all configuration directives which can be modified for
|
||||||
|
# the cryptic-net daemon at runtime. All values specified here are the
|
||||||
|
# default values.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
# A DNS service runs as part of every cryptic-net process.
|
||||||
|
dns:
|
||||||
|
|
||||||
|
# list of IPs that the DNS service will use to resolve non-cryptic.io
|
||||||
|
# hostnames.
|
||||||
|
resolvers:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
|
|
||||||
|
# A VPN service runs as part of every cryptic-net process.
|
||||||
|
vpn:
|
||||||
|
|
||||||
|
# Enable this field if the vpn will be made to be publicly accessible at a
|
||||||
|
# particular IP or hostname. At least one host must have a publicly accessible
|
||||||
|
# VPN process at any given moment.
|
||||||
|
#public_addr: "host:port"
|
||||||
|
|
||||||
|
# Firewall directives, as described here:
|
||||||
|
# https://github.com/slackhq/nebula/blob/v1.6.1/examples/config.yml#L260
|
||||||
|
firewall:
|
||||||
|
|
||||||
|
conntrack:
|
||||||
|
tcp_timeout: 12m
|
||||||
|
udp_timeout: 3m
|
||||||
|
default_timeout: 10m
|
||||||
|
max_connections: 100000
|
||||||
|
|
||||||
|
outbound:
|
||||||
|
|
||||||
|
# Allow all outbound traffic from this node.
|
||||||
|
- port: any
|
||||||
|
proto: any
|
||||||
|
host: any
|
||||||
|
|
||||||
|
inbound:
|
||||||
|
|
||||||
|
# If any storage allocations are declared below, the ports used will be
|
||||||
|
# allowed here automatically.
|
||||||
|
|
||||||
|
# Allow ICMP between hosts.
|
||||||
|
- port: any
|
||||||
|
proto: icmp
|
||||||
|
host: any
|
||||||
|
|
||||||
|
# That's it.
|
||||||
|
|
||||||
|
storage:
|
||||||
|
|
||||||
|
# Allocations defined here are used to store data in the distributed storage
|
||||||
|
# network. If no allocations are defined then no data is replicated to this
|
||||||
|
# node.
|
||||||
|
#
|
||||||
|
# The data directory of each allocation should be on a different drive, while
|
||||||
|
# the meta directories can be anywhere (ideally on an SSD).
|
||||||
|
#
|
||||||
|
# Capacity declares how many gigabytes can be stored in each allocation, and
|
||||||
|
# is required. It must be a multiple of 100.
|
||||||
|
#
|
||||||
|
# The various ports are all required and must all be unique within and across
|
||||||
|
# allocations.
|
||||||
|
allocations:
|
||||||
|
|
||||||
|
#- data_path: /foo/bar/data
|
||||||
|
# meta_path: /foo/bar/meta
|
||||||
|
# capacity: 1200
|
||||||
|
# api_port: 3900
|
||||||
|
# rpc_port: 3901
|
||||||
|
# web_port: 3902
|
661
LICENSE.txt
661
LICENSE.txt
@ -1,661 +0,0 @@
|
|||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 19 November 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works, specifically designed to ensure
|
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
|
||||||
you this License which gives you legal permission to copy, distribute
|
|
||||||
and/or modify the software.
|
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
|
||||||
improvements made in alternate versions of the program, if they
|
|
||||||
receive widespread use, become available for other developers to
|
|
||||||
incorporate. Many developers of free software are heartened and
|
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
|
||||||
ensure that, in such cases, the modified source code becomes available
|
|
||||||
to the community. It requires the operator of a network server to
|
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
|
||||||
this license.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the work with which it is combined will remain governed by version
|
|
||||||
3 of the GNU General Public License.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
|
||||||
network, you should also make sure that it provides a way for users to
|
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
|
||||||
of the code. There are many ways you could offer source, and different
|
|
||||||
solutions will be better for different programs; see section 13 for the
|
|
||||||
specific requirements.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
123
README.md
123
README.md
@ -4,43 +4,110 @@ rely on it for anything._**
|
|||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
# Isle
|
# cryptic-net
|
||||||
|
|
||||||
Welcome to Isle's technical documentation. You can find a less technical
|
The cryptic-net project provides the foundation for an **autonomous community
|
||||||
entrypoint to Isle on [the Micropelago website][isle].
|
cloud infrastructure**.
|
||||||
|
|
||||||
Isle runs on a host as a server daemon, and connects to other isle instances to
|
This project targets communities of individuals, where certain members of the
|
||||||
form a peer-to-peer network. Isle networks are completely self-hosted; no
|
community would like to host services and applications from servers running in
|
||||||
third-parties are required for a network to function.
|
their homes or offices. These servers can range from simple Raspberry Pis to
|
||||||
|
full-sized home PCs.
|
||||||
|
|
||||||
Members of a network are able to build upon the capabilities provided by Isle to
|
The core components of cryptic-net, currently, are:
|
||||||
host services for themselves and others. These capabilities include:
|
|
||||||
|
|
||||||
* A VPN which enables direct peer-to-peer communication between network members.
|
* A VPN which enables direct peer-to-peer communication. Even if most hosts in
|
||||||
Even if most hosts in the network are on a private LAN (e.g. their home WiFi
|
the network are on a private LAN (e.g. their home WiFi network) or have a
|
||||||
network) or have a dynamic IP, they can still communicate directly with each
|
dynamic IP, they can still communicate directly with each other.
|
||||||
other.
|
|
||||||
|
|
||||||
* An S3-compatible network filesystem. Each member can provide as much storage
|
* An S3-compatible network filesystem. Each participant can provide as much
|
||||||
as they care to, if any. Stored data is sharded and replicated across all
|
storage as they care to, if any. Stored data is sharded and replicated across
|
||||||
hosts that choose to provide storage.
|
all hosts that choose to provide storage.
|
||||||
|
|
||||||
* A DNS server which provides automatic host discovery within the network.
|
These components are wrapped into a single binary, with all setup being
|
||||||
|
automated. cryptic-net takes "just works" very seriously.
|
||||||
|
|
||||||
Every isle daemon is able to create or join multiple independent networks. In
|
Participants are able to build upon these foundations to host services for
|
||||||
this case the networks remain siloed from each other, such that members of one
|
themselves and others. They can be assured that their communications are private
|
||||||
network are unable to access resources or communicate with members of the other.
|
and their storage is reliable, all with zero administrative overhead and zero
|
||||||
|
third parties involved.
|
||||||
|
|
||||||
[isle]: https://micropelago.net/isle/
|
[nebula]: https://github.com/slackhq/nebula
|
||||||
|
[garage]: https://garagehq.deuxfleurs.fr/documentation/quick-start/
|
||||||
|
|
||||||
## Getting Started
|
## Documentation
|
||||||
|
|
||||||
The following pages will guide you through setup of Isle, joining an existing
|
_NOTE: There is currently only a single live cryptic-net which can be joined,
|
||||||
network, and all other functionality available via the command-line.
|
though generalizing the bootstrap process so others can create their own network
|
||||||
|
is [planned][roadmap]. If you do not know the admins of this cryptic-net then
|
||||||
|
unfortunately there's not much you can do right now._
|
||||||
|
|
||||||
* [Installation](./docs/install.md)
|
cryptic-net users fall into different roles, depending on their level of
|
||||||
* [Command-line Usage](./docs/command-line.md)
|
involvement and expertise within their particular network. The documentation for
|
||||||
* [Join a Network](./docs/user/join-a-network.md)
|
cryptic-net is broken down by these categories, so that the reader can easily
|
||||||
|
know which documents they need to care about.
|
||||||
|
|
||||||
Those who want to dive in and contribute to the Isle codebase should check out
|
### User Docs
|
||||||
the [Developer Documentation](./docs/dev/index.md).
|
|
||||||
|
Users are participants who use cryptic-net resources, but do not provide any
|
||||||
|
network or storage resources themselves. Users may be accessing the network from
|
||||||
|
a laptop, and so are not expected to be online at any particular moment.
|
||||||
|
|
||||||
|
Documentation for users:
|
||||||
|
|
||||||
|
* [Getting Started](docs/user/getting-started.md)
|
||||||
|
* [Creating a daemon.yml File](docs/user/creating-a-daemonyml-file.md)
|
||||||
|
* [Using DNS](docs/user/using-dns.md) (advanced)
|
||||||
|
* Restic example (TODO)
|
||||||
|
|
||||||
|
### Operator Docs
|
||||||
|
|
||||||
|
Operators are participants who own a dedicated host which they can expect to be
|
||||||
|
always-online (to the extent that's possible in a residential environment).
|
||||||
|
Operator hosts will need at least one of the following to be useful:
|
||||||
|
|
||||||
|
* A static public IP, or a dynamic public IP with [dDNS][ddns] set up.
|
||||||
|
|
||||||
|
* At least 100GB of unused storage which can be reserved for the network.
|
||||||
|
|
||||||
|
Operators are expected to be familiar with server administration, and to not be
|
||||||
|
afraid of a terminal.
|
||||||
|
|
||||||
|
Documentation for operators:
|
||||||
|
|
||||||
|
* [Contributing Storage](docs/operator/contributing-storage.md)
|
||||||
|
* [Contributing a Lighthouse](docs/operator/contributing-a-lighthouse.md)
|
||||||
|
* [Managing garage](docs/operator/managing-garage.md)
|
||||||
|
|
||||||
|
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/
|
||||||
|
|
||||||
|
### Admin Docs
|
||||||
|
|
||||||
|
Admins are participants who control membership within the network. They are
|
||||||
|
likely operators as well.
|
||||||
|
|
||||||
|
Documentation for admins:
|
||||||
|
|
||||||
|
* [Adding a Host to the Network](docs/admin/adding-a-host-to-the-network.md)
|
||||||
|
* Removing a Host From the Network (TODO)
|
||||||
|
|
||||||
|
### Dev Docs
|
||||||
|
|
||||||
|
Dev may or may not be participants in any particular cryptic-net. They instead
|
||||||
|
are those who work on the actual code for cryptic-net.
|
||||||
|
|
||||||
|
Documentation for devs:
|
||||||
|
|
||||||
|
* [Design Principles](docs/dev/design-principles.md)
|
||||||
|
* [`cryptic-net daemon` process tree](docs/dev/daemon-process-tree.svg): Diagram
|
||||||
|
describing the [pmux](https://github.com/cryptic-io/pmux) process tree created
|
||||||
|
by `cryptic-net daemon` at runtime.
|
||||||
|
* [Rebuilding Documentation](docs/dev/rebuilding-documentation.md)
|
||||||
|
|
||||||
|
## Misc
|
||||||
|
|
||||||
|
Besides documentation, there are a few other pages which might be useful:
|
||||||
|
|
||||||
|
* [Roadmap][roadmap]
|
||||||
|
|
||||||
|
[roadmap]: docs/roadmap.md
|
||||||
|
202
default.nix
202
default.nix
@ -1,170 +1,116 @@
|
|||||||
{
|
{
|
||||||
buildSystem ? builtins.currentSystem,
|
|
||||||
hostSystem ? buildSystem,
|
|
||||||
pkgsNix ? (import ./nix/pkgs.nix),
|
|
||||||
|
|
||||||
revision ? "dev",
|
pkgs ? (import ./nix/pkgs.nix).stable,
|
||||||
releaseName ? "dev",
|
bootstrap ? null,
|
||||||
}: let
|
|
||||||
|
|
||||||
pkgs = pkgsNix.default {
|
}: rec {
|
||||||
inherit buildSystem hostSystem;
|
|
||||||
|
rootedBootstrap = pkgs.stdenv.mkDerivation {
|
||||||
|
name = "cryptic-net-rooted-bootstrap";
|
||||||
|
|
||||||
|
src = bootstrap;
|
||||||
|
|
||||||
|
builder = builtins.toFile "builder.sh" ''
|
||||||
|
source $stdenv/setup
|
||||||
|
mkdir -p "$out"/share
|
||||||
|
cp "$src" "$out"/share/bootstrap.tgz
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
pkgsNative = pkgsNix.default {
|
|
||||||
inherit buildSystem;
|
|
||||||
hostSystem = buildSystem;
|
|
||||||
};
|
|
||||||
|
|
||||||
garageNix = (import ./nix/garage.nix);
|
|
||||||
|
|
||||||
in rec {
|
|
||||||
|
|
||||||
version = pkgs.stdenv.mkDerivation {
|
version = pkgs.stdenv.mkDerivation {
|
||||||
name = "isle-version";
|
name = "cryptic-net-version";
|
||||||
|
|
||||||
inherit buildSystem hostSystem revision releaseName;
|
buildInputs = [ pkgs.git pkgs.go ];
|
||||||
|
src = ./.;
|
||||||
nativeBuildInputs = [ pkgsNative.git ];
|
inherit bootstrap;
|
||||||
|
|
||||||
goVersion = pkgs.go.version;
|
|
||||||
garageVersion = garageNix.version;
|
|
||||||
nixpkgsVersion = pkgsNix.version;
|
|
||||||
|
|
||||||
builder = builtins.toFile "builder.sh" ''
|
builder = builtins.toFile "builder.sh" ''
|
||||||
source $stdenv/setup
|
source $stdenv/setup
|
||||||
|
|
||||||
versionFile=version
|
versionFile=version
|
||||||
|
|
||||||
echo "Release: $releaseName" >> "$versionFile"
|
if [ "$bootstrap" != "" ]; then
|
||||||
echo "Platform: $hostSystem" >> "$versionFile"
|
hostName=$(tar -xzf "$bootstrap" --to-stdout ./hostname)
|
||||||
echo "Git Revision: $revision" >> "$versionFile"
|
echo "Built for host: $hostName" >> "$versionFile"
|
||||||
echo "Go Version: $goVersion" >> "$versionFile"
|
fi
|
||||||
echo "Garage Version: $garageVersion" >> "$versionFile"
|
|
||||||
echo "NixPkgs Version: $nixpkgsVersion" >> "$versionFile"
|
echo "Build date: $(date)" >> "$versionFile"
|
||||||
echo "Build Platform: $buildSystem" >> "$versionFile"
|
echo "Git status: $(cd "$src" && git describe --always --long --dirty=' (dirty)')" >> "$versionFile"
|
||||||
|
echo "Go version: $(go version)" >> "$versionFile"
|
||||||
|
echo "Build host info: $(uname -srvm)" >> "$versionFile"
|
||||||
|
|
||||||
mkdir -p "$out"/share
|
mkdir -p "$out"/share
|
||||||
cp "$versionFile" "$out"/share
|
cp "$versionFile" "$out"/share
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
goBinaries = pkgs.buildGoModule {
|
goWorkspace = pkgs.callPackage ./go-workspace {};
|
||||||
pname = "isle-go-binaries";
|
|
||||||
version = "unstable";
|
|
||||||
|
|
||||||
# If this seems pointless, that's because it is! buildGoModule doesn't like
|
dnsmasq = (pkgs.callPackage ./dnsmasq {
|
||||||
# it if the src derivation's name ends in "-go". So this mkDerivation here
|
glibcStatic = pkgs.glibc.static;
|
||||||
# only serves to give buildGoModule a src derivation with a name it likes.
|
}).env;
|
||||||
src = pkgs.stdenv.mkDerivation {
|
|
||||||
name = "isle-go-src";
|
|
||||||
src = ./go;
|
|
||||||
builder = builtins.toFile "builder.sh" ''
|
|
||||||
source $stdenv/setup
|
|
||||||
cp -r "$src" "$out"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
vendorHash = "sha256-CYnZNk1wTw/88L6SxNJTUojWartbGdL44c4GKFc8s2k=";
|
garage = (pkgs.callPackage ./garage {}).env;
|
||||||
|
|
||||||
subPackages = [
|
waitFor = pkgs.callPackage ./nix/wait-for.nix {};
|
||||||
"./cmd/entrypoint"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
dnsmasq = (pkgs.callPackage ./nix/dnsmasq.nix {
|
appDir = pkgs.buildEnv {
|
||||||
stdenv = pkgs.pkgsStatic.stdenv;
|
name = "cryptic-net-AppDir";
|
||||||
});
|
|
||||||
|
|
||||||
nebula = pkgs.callPackage ./nix/nebula.nix {};
|
|
||||||
|
|
||||||
garage = let
|
|
||||||
hostPlatform = pkgs.stdenv.hostPlatform.parsed;
|
|
||||||
in pkgs.callPackage garageNix.package {
|
|
||||||
inherit buildSystem;
|
|
||||||
hostSystem = "${hostPlatform.cpu.name}-unknown-${hostPlatform.kernel.name}-musl";
|
|
||||||
pkgsSrc = pkgsNix.src;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
appDirBase = pkgs.buildEnv {
|
|
||||||
name = "isle-AppDir-base";
|
|
||||||
paths = [
|
paths = [
|
||||||
|
|
||||||
|
pkgs.pkgsStatic.bash
|
||||||
|
pkgs.pkgsStatic.coreutils
|
||||||
|
pkgs.pkgsStatic.unixtools.ping
|
||||||
|
pkgs.pkgsStatic.netcat # required by waitFor
|
||||||
|
pkgs.pkgsStatic.gnutar
|
||||||
|
pkgs.pkgsStatic.gzip
|
||||||
|
|
||||||
|
# custom packages from ./pkgs.nix
|
||||||
|
pkgs.yq-go
|
||||||
|
pkgs.nebula
|
||||||
|
|
||||||
./AppDir
|
./AppDir
|
||||||
version
|
version
|
||||||
dnsmasq
|
dnsmasq
|
||||||
nebula
|
|
||||||
garage
|
garage
|
||||||
pkgs.minio-client
|
waitFor
|
||||||
];
|
goWorkspace.crypticNetMain
|
||||||
|
|
||||||
|
] ++ (if bootstrap != null then [ rootedBootstrap ] else []);
|
||||||
};
|
};
|
||||||
|
|
||||||
appDir = pkgs.stdenv.mkDerivation {
|
appimagetool = pkgs.callPackage ./nix/appimagetool.nix {};
|
||||||
name = "isle-AppDir";
|
|
||||||
|
|
||||||
src = appDirBase;
|
|
||||||
inherit goBinaries;
|
|
||||||
|
|
||||||
builder = builtins.toFile "build.sh" ''
|
|
||||||
source $stdenv/setup
|
|
||||||
cp -rL "$src" "$out"
|
|
||||||
chmod +w "$out" -R
|
|
||||||
|
|
||||||
cd "$out"
|
|
||||||
cp $goBinaries/bin/entrypoint ./AppRun
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
devShell = pkgs.mkShell {
|
|
||||||
buildInputs = [
|
|
||||||
pkgs.go
|
|
||||||
pkgs.golangci-lint
|
|
||||||
pkgs.gopls
|
|
||||||
(pkgs.callPackage ./nix/gowrap.nix {})
|
|
||||||
pkgs.go-mockery
|
|
||||||
];
|
|
||||||
shellHook = ''
|
|
||||||
true # placeholder
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
testShell = pkgs.mkShell {
|
|
||||||
APPDIR = appDirBase;
|
|
||||||
buildInputs = [
|
|
||||||
pkgs.go
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
build = rec {
|
|
||||||
appImage = pkgs.stdenv.mkDerivation {
|
appImage = pkgs.stdenv.mkDerivation {
|
||||||
name = "isle-AppImage";
|
name = "cryptic-net-AppImage";
|
||||||
src = appDir;
|
src = appDir;
|
||||||
|
|
||||||
nativeBuildInputs = [
|
buildInputs = [ appimagetool ];
|
||||||
(pkgsNative.callPackage ./nix/appimagetool.nix {})
|
|
||||||
];
|
|
||||||
|
|
||||||
ARCH = pkgs.stdenv.hostPlatform.parsed.cpu.name;
|
ARCH = "x86_64";
|
||||||
|
|
||||||
builder = builtins.toFile "build.sh" ''
|
builder = builtins.toFile "build.sh" ''
|
||||||
source $stdenv/setup
|
source $stdenv/setup
|
||||||
cp -rL "$src" isle.AppDir
|
cp -rL "$src" cryptic-net
|
||||||
chmod +w isle.AppDir -R
|
chmod +w cryptic-net -R
|
||||||
|
mkdir $out
|
||||||
export VERSION=debug
|
appimagetool cryptic-net "$out/cryptic-net"
|
||||||
|
|
||||||
# https://github.com/probonopd/go-appimage/issues/155
|
|
||||||
unset SOURCE_DATE_EPOCH
|
|
||||||
|
|
||||||
appimagetool ./isle.AppDir
|
|
||||||
mkdir -p "$out"/bin
|
|
||||||
mv Isle-* "$out"/bin/isle
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
archPkg = ((import ./dist/linux/arch) {
|
service = pkgs.writeText "cryptic-service" ''
|
||||||
inherit hostSystem releaseName appImage;
|
[Unit]
|
||||||
pkgs = pkgsNative;
|
Description=cryptic nebula
|
||||||
});
|
Requires=network.target
|
||||||
};
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=always
|
||||||
|
RestartSec=1s
|
||||||
|
User=root
|
||||||
|
ExecStart=${appImage}/cryptic-net
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
'';
|
||||||
}
|
}
|
||||||
|
86
dist/linux/arch/default.nix
vendored
86
dist/linux/arch/default.nix
vendored
@ -1,86 +0,0 @@
|
|||||||
{
|
|
||||||
pkgs,
|
|
||||||
hostSystem,
|
|
||||||
releaseName,
|
|
||||||
appImage,
|
|
||||||
}: let
|
|
||||||
|
|
||||||
cpuArch = (pkgs.lib.systems.parse.mkSystemFromString hostSystem).cpu.name;
|
|
||||||
|
|
||||||
pkgbuild = pkgs.writeText "isle-arch-PKGBUILD-${releaseName}-${cpuArch}" ''
|
|
||||||
pkgname=isle
|
|
||||||
pkgver=${builtins.replaceStrings ["-"] ["_"] releaseName}
|
|
||||||
pkgrel=0
|
|
||||||
pkgdesc="The foundation for an autonomous community cloud infrastructure."
|
|
||||||
arch=('${cpuArch}')
|
|
||||||
url="https://code.betamike.com/micropelago/isle"
|
|
||||||
license=('AGPL-3.0-or-later')
|
|
||||||
|
|
||||||
depends=(
|
|
||||||
'fuse2'
|
|
||||||
)
|
|
||||||
|
|
||||||
# The appImage is deliberately kept separate from the src.tar.zst. For some
|
|
||||||
# reason including the appImage within the archive results in a large part
|
|
||||||
# of the binary being stripped away and some weird skeleton appImage comes
|
|
||||||
# out the other end.
|
|
||||||
source=('isle' 'src.tar.zst')
|
|
||||||
md5sums=('SKIP' 'SKIP')
|
|
||||||
noextract=('isle')
|
|
||||||
|
|
||||||
package() {
|
|
||||||
cp -r etc "$pkgdir"/etc
|
|
||||||
cp -r usr "$pkgdir"/usr
|
|
||||||
|
|
||||||
mkdir -p "$pkgdir"/usr/bin/
|
|
||||||
cp isle "$pkgdir"/usr/bin/
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
|
|
||||||
in
|
|
||||||
pkgs.stdenv.mkDerivation {
|
|
||||||
name = "isle-arch-pkg-${releaseName}-${cpuArch}";
|
|
||||||
|
|
||||||
nativeBuildInputs = [
|
|
||||||
pkgs.zstd
|
|
||||||
pkgs.pacman
|
|
||||||
pkgs.fakeroot
|
|
||||||
pkgs.libarchive
|
|
||||||
];
|
|
||||||
|
|
||||||
inherit pkgbuild;
|
|
||||||
src = appImage;
|
|
||||||
defaultDaemonYml = ../../../go/daemon/daecommon/daemon.yml;
|
|
||||||
systemdService = ../isle.service;
|
|
||||||
dontUnpack = true;
|
|
||||||
|
|
||||||
buildPhase = ''
|
|
||||||
mkdir -p root/etc/isle/
|
|
||||||
cp "$defaultDaemonYml" root/etc/isle/daemon.yml
|
|
||||||
|
|
||||||
mkdir -p root/usr/lib/sysusers.d/
|
|
||||||
cat >root/usr/lib/sysusers.d/isle.conf <<EOF
|
|
||||||
u isle - "isle Daemon"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
mkdir -p root/usr/lib/systemd/system
|
|
||||||
cp "$systemdService" root/usr/lib/systemd/system/isle.service
|
|
||||||
|
|
||||||
cp $pkgbuild PKGBUILD
|
|
||||||
|
|
||||||
tar -cf src.tar.zst --zstd --mode=a+rX,u+w -C root .
|
|
||||||
cp "$src"/bin/isle isle
|
|
||||||
|
|
||||||
PKGEXT=".pkg.tar.zst" CARCH="${cpuArch}" makepkg \
|
|
||||||
--nodeps \
|
|
||||||
--config ${pkgs.pacman}/etc/makepkg.conf
|
|
||||||
'';
|
|
||||||
|
|
||||||
installPhase = ''
|
|
||||||
mkdir -p $out
|
|
||||||
cp *.pkg.tar.zst $out/
|
|
||||||
'';
|
|
||||||
|
|
||||||
# NOTE if https://github.com/NixOS/nixpkgs/issues/241911 is ever addressed
|
|
||||||
# it'd be nice to add an automatic check using namcap here.
|
|
||||||
}
|
|
16
dist/linux/isle.service
vendored
16
dist/linux/isle.service
vendored
@ -1,16 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Isle
|
|
||||||
Requires=network.target
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
User=isle
|
|
||||||
ExecStart=/usr/bin/isle daemon -c /etc/isle/daemon.yml
|
|
||||||
RuntimeDirectory=isle
|
|
||||||
RuntimeDirectoryMode=0700
|
|
||||||
StateDirectory=isle
|
|
||||||
StateDirectoryMode=0700
|
|
||||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
36
dnsmasq/bin/dnsmasq-entrypoint
Normal file
36
dnsmasq/bin/dnsmasq-entrypoint
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# TODO implement this in go
|
||||||
|
|
||||||
|
set -e -o pipefail
|
||||||
|
cd "$APPDIR"
|
||||||
|
|
||||||
|
conf_path="$_RUNTIME_DIR_PATH"/dnsmasq.conf
|
||||||
|
|
||||||
|
cat etc/dnsmasq/base.conf > "$conf_path"
|
||||||
|
|
||||||
|
tmp="$(mktemp -d -t cryptic-net-dnsmasq-entrypoint-XXX)"
|
||||||
|
|
||||||
|
( trap "rm -rf '$tmp'" EXIT
|
||||||
|
|
||||||
|
tar xzf "$_BOOTSTRAP_PATH" -C "$tmp" ./hosts
|
||||||
|
|
||||||
|
thisHostName=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./hostname)
|
||||||
|
thisHostIP=$(cat "$tmp"/hosts/"$thisHostName".yml | yq '.nebula.ip')
|
||||||
|
|
||||||
|
domain=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./admin/creation-params.yml | yq '.domain')
|
||||||
|
|
||||||
|
echo "listen-address=$thisHostIP" >> "$conf_path"
|
||||||
|
|
||||||
|
ls -1 "$tmp"/hosts | while read hostYml; do
|
||||||
|
|
||||||
|
hostName=$(echo "$hostYml" | cut -d. -f1)
|
||||||
|
hostIP=$(cat "$tmp"/hosts/"$hostYml" | yq '.nebula.ip')
|
||||||
|
echo "address=/${hostName}.hosts.$domain/$hostIP" >> "$conf_path"
|
||||||
|
|
||||||
|
done
|
||||||
|
)
|
||||||
|
|
||||||
|
cat "$_DAEMON_YML_PATH" | \
|
||||||
|
yq '.dns.resolvers | .[] | "server=" + .' \
|
||||||
|
>> "$conf_path"
|
||||||
|
|
||||||
|
exec bin/dnsmasq -d -C "$conf_path"
|
39
dnsmasq/default.nix
Normal file
39
dnsmasq/default.nix
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
|
||||||
|
stdenv,
|
||||||
|
buildEnv,
|
||||||
|
glibcStatic,
|
||||||
|
rebase,
|
||||||
|
|
||||||
|
}: rec {
|
||||||
|
|
||||||
|
dnsmasq = stdenv.mkDerivation rec {
|
||||||
|
pname = "dnsmasq";
|
||||||
|
version = "2.85";
|
||||||
|
|
||||||
|
src = builtins.fetchurl {
|
||||||
|
url = "https://www.thekelleys.org.uk/dnsmasq/${pname}-${version}.tar.xz";
|
||||||
|
sha256 = "sha256-rZjTgD32h+W5OAgPPSXGKP5ByHh1LQP7xhmXh/7jEvo=";
|
||||||
|
};
|
||||||
|
|
||||||
|
nativeBuildInputs = [ glibcStatic ];
|
||||||
|
|
||||||
|
makeFlags = [
|
||||||
|
"LDFLAGS=-static"
|
||||||
|
"DESTDIR="
|
||||||
|
"BINDIR=$(out)/bin"
|
||||||
|
"MANDIR=$(out)/man"
|
||||||
|
"LOCALEDIR=$(out)/share/locale"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
env = buildEnv {
|
||||||
|
name = "cryptic-net-dnsmasq";
|
||||||
|
paths = [
|
||||||
|
(rebase "cryptic-net-dnsmasq-bin" ./bin "bin")
|
||||||
|
(rebase "cryptic-net-dnsmasq-etc" ./etc "etc/dnsmasq")
|
||||||
|
dnsmasq
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
41
dnsmasq/etc/base.conf
Normal file
41
dnsmasq/etc/base.conf
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Configuration file for dnsmasq.
|
||||||
|
#
|
||||||
|
# Format is one option per line, legal options are the same
|
||||||
|
# as the long options legal on the command line. See
|
||||||
|
# "/usr/sbin/dnsmasq --help" or "man 8 dnsmasq" for details.
|
||||||
|
|
||||||
|
# Listen on this specific port instead of the standard DNS port
|
||||||
|
# (53). Setting this to zero completely disables DNS function,
|
||||||
|
# leaving only DHCP and/or TFTP.
|
||||||
|
port=53
|
||||||
|
|
||||||
|
# If you don't want dnsmasq to read /etc/resolv.conf or any other
|
||||||
|
# file, getting its servers from this file instead (see below), then
|
||||||
|
# uncomment this.
|
||||||
|
no-resolv
|
||||||
|
|
||||||
|
# On systems which support it, dnsmasq binds the wildcard address,
|
||||||
|
# even when it is listening on only some interfaces. It then discards
|
||||||
|
# requests that it shouldn't reply to. This has the advantage of
|
||||||
|
# working even when interfaces come and go and change address. If you
|
||||||
|
# want dnsmasq to really bind only the interfaces it is listening on,
|
||||||
|
# uncomment this option. About the only time you may need this is when
|
||||||
|
# running another nameserver on the same machine.
|
||||||
|
bind-interfaces
|
||||||
|
|
||||||
|
# If you don't want dnsmasq to read /etc/hosts, uncomment the
|
||||||
|
# following line.
|
||||||
|
no-hosts
|
||||||
|
|
||||||
|
# Unset user and group so that dnsmasq doesn't drop privileges to another user.
|
||||||
|
# If this isn't done then dnsmasq fails to start up, since it fails to access
|
||||||
|
# /etc/passwd correctly, probably due to nix.
|
||||||
|
user=
|
||||||
|
group=
|
||||||
|
|
||||||
|
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
#
|
||||||
|
# Everything below is generated dynamically based on runtime configuration
|
||||||
|
#
|
||||||
|
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
|
@ -4,6 +4,20 @@ This document guides an admin through adding a single host to the network. Keep
|
|||||||
in mind that the steps described here must be done for _each_ host the user
|
in mind that the steps described here must be done for _each_ host the user
|
||||||
wishes to add.
|
wishes to add.
|
||||||
|
|
||||||
|
There are two ways for a user to add a host to the cryptic-net network.
|
||||||
|
|
||||||
|
- If the user is savy enough to obtain their own `cryptic-net` binary, they can
|
||||||
|
do so. The admin can then generate a `bootstrap.tgz` file for their host,
|
||||||
|
give that to the user, and the user can run `cryptic-net daemon` using that
|
||||||
|
bootstrap file.
|
||||||
|
|
||||||
|
- If the user is not so savy, the admin can generate a custom `cryptic-net`
|
||||||
|
binary with the `bootstrap.tgz` embedded into it. The user can be given this
|
||||||
|
binary and run `cryptic-net daemon` without any configuration on their end.
|
||||||
|
|
||||||
|
From the admin's perspective the only difference between these cases is one
|
||||||
|
extra step.
|
||||||
|
|
||||||
## Step 1: Choose Hostname
|
## Step 1: Choose Hostname
|
||||||
|
|
||||||
The user will need to provide you with a name for their host. The name should
|
The user will need to provide you with a name for their host. The name should
|
||||||
@ -15,20 +29,66 @@ conform to the following rules:
|
|||||||
|
|
||||||
* It should end with a letter or number.
|
* It should end with a letter or number.
|
||||||
|
|
||||||
## Step 2: Create a `bootstrap.json` File
|
## Step 2: Add Host to Network
|
||||||
|
|
||||||
To create a `bootstrap.json` file for the new host, the admin should perform the
|
The admin should choose an IP for the host. The IP you choose for the new host
|
||||||
|
should be one which is not yet used by any other host, and which is in the VPN's
|
||||||
|
set of allowed IPs.
|
||||||
|
|
||||||
|
The admin should perform the following command from their own host:
|
||||||
|
|
||||||
|
```
|
||||||
|
cryptic-net hosts add --name <name> --ip <ip>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Create a `bootstrap.tgz` File
|
||||||
|
|
||||||
|
Access to an `admin.tgz` file is required for this step.
|
||||||
|
|
||||||
|
To create a `bootstrap.tgz` file for the new host, the admin should perform the
|
||||||
following command from their own host:
|
following command from their own host:
|
||||||
|
|
||||||
```
|
```
|
||||||
isle hosts create --hostname <name> >bootstrap.json
|
cryptic-net hosts make-bootstrap \
|
||||||
|
--name <name> \
|
||||||
|
--admin-path <path to admin.tgz> \
|
||||||
|
> bootstrap.tgz
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting `bootstrap.json` file should be treated as a secret file and
|
The resulting `bootstrap.tgz` file should be treated as a secret file that is
|
||||||
shared only with the user it was generated for. The `bootstrap.json` file should
|
shared only with the user it was generated for. The `bootstrap.tgz` file should
|
||||||
not be re-used between hosts.
|
not be re-used between hosts either.
|
||||||
|
|
||||||
The user can now proceed with calling `isle network join`, as described in the
|
If the user already has access to a `cryptic-net` binary then the new
|
||||||
[Getting Started][getting-started] document.
|
`bootstrap.tgz` file can be given to them as-is, and they can proceed with
|
||||||
|
running their host's `cryptic-net daemon`.
|
||||||
|
|
||||||
[getting-started]: ../user/getting-started.md
|
### Encrypted `admin.tgz`
|
||||||
|
|
||||||
|
If `admin.tgz` is kept in an encrypted format on disk (it should be!) then the
|
||||||
|
decrypted form can be piped into `make-bootstrap` over stdin. For example, if
|
||||||
|
GPG is being used to secure `admin.tgz` then the following could be used to
|
||||||
|
generate a `bootstrap.tgz`:
|
||||||
|
|
||||||
|
```
|
||||||
|
gpg -d <path to admin.tgz.gpg> | cryptic-net hosts make-boostrap \
|
||||||
|
--name <name> \
|
||||||
|
--admin-path - \
|
||||||
|
> bootstrap.tgz
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the value of `--admin-path` is `-`, indicating that `admin.tgz` should
|
||||||
|
be read from stdin.
|
||||||
|
|
||||||
|
## Step 4: Optionally, Build Binary
|
||||||
|
|
||||||
|
If you wish to embed the `bootstrap.tgz` into a custom binary for the user (to
|
||||||
|
make installation _extremely_ easy for them) then you can run the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-build --arg bootstrap <path to bootstrap.tgz> -A appImage
|
||||||
|
```
|
||||||
|
|
||||||
|
The resulting binary can be found in the `result` directory which is created.
|
||||||
|
Note that this binary should be treated like a `bootstrap.tgz` in terms of its
|
||||||
|
uniqueness and sensitivity.
|
||||||
|
@ -1,101 +0,0 @@
|
|||||||
# Creating a New Network
|
|
||||||
|
|
||||||
This guide is for those who wish to start a new isle network of their own.
|
|
||||||
|
|
||||||
By starting a new isle network, you are becoming the administrator of a
|
|
||||||
network. Be aware that being a network administrator is not necessarily easy,
|
|
||||||
and the users of your network will frequently need your help in order to have a
|
|
||||||
good experience. It can be helpful to have others with which you are
|
|
||||||
administering the network, in order to share responsibilities.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
Creating a network is done using a single host, which will become the first host
|
|
||||||
in the network.
|
|
||||||
|
|
||||||
The configuration used during network creation will be identical to that used
|
|
||||||
during normal operation of the host, so be prepared to commit to that
|
|
||||||
configuration for a non-trivial amount of time.
|
|
||||||
|
|
||||||
The requirements for this host are:
|
|
||||||
|
|
||||||
* A public static IP, or a dynamic public IP with [dDNS][ddns] set up.
|
|
||||||
|
|
||||||
* There should be UDP port which is accessible publicly over that IP/DNS name.
|
|
||||||
This may involve forwarding the UDP port in your gateway if the host is
|
|
||||||
behind a NAT, and/or allowing traffic on that UDP port in your hosts
|
|
||||||
firewall.
|
|
||||||
|
|
||||||
* At least 3 GB of disk storage space.
|
|
||||||
|
|
||||||
* At least 3 directories should be chosen, each of which will be committing at
|
|
||||||
least 1GB. Ideally these directories should be on different physical disks,
|
|
||||||
but if that's not possible it's ok. See the Next Steps section.
|
|
||||||
|
|
||||||
* None of the resources being used for this network (the UDP port or storage
|
|
||||||
locations) should be being used by other networks.
|
|
||||||
|
|
||||||
## Step 1: Configure the isle Daemon
|
|
||||||
|
|
||||||
Open `/etc/isle/daemon.yml` in a text editor and perform the following changes:
|
|
||||||
|
|
||||||
* Set the `vpn.public_addr` field to the `host:port` your host is accessible on,
|
|
||||||
where `host` is the static public IP/DNS name of your host, and `port` is the
|
|
||||||
UDP port which is publicly accessible.
|
|
||||||
|
|
||||||
* Configure 3 (at least) allocations in the `storage.allocations` section.
|
|
||||||
|
|
||||||
Save and close the file.
|
|
||||||
|
|
||||||
Run the following to restart the daemon with the new configuration:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo systemctl restart isle
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 2: Choose Parameters
|
|
||||||
|
|
||||||
There are some key parameters which must be chosen when creating a new network.
|
|
||||||
These will remain constant throughout the lifetime of the network, and so should
|
|
||||||
be chosen with care.
|
|
||||||
|
|
||||||
* Name: A human-readable name for the network. This will only be used for
|
|
||||||
display purposes.
|
|
||||||
|
|
||||||
* Subnet: The IP subnet (or CIDR) will look something like `10.10.0.0/16`, where
|
|
||||||
the `/16` indicates that all IPs from `10.10.0.0` to `10.10.255.255` are
|
|
||||||
included. It's recommended to choose from the [ranges reserved for private
|
|
||||||
networks](https://en.wikipedia.org/wiki/IPv4#Private_networks), but within
|
|
||||||
that selection the choice is up to you.
|
|
||||||
|
|
||||||
* Domain: isle is shipped with a DNS server which will automatically
|
|
||||||
configure itself with all hosts in the network, with each DNS entry taking the
|
|
||||||
form of `hostname.hosts.domain`, where `domain` is the domain chosen in this
|
|
||||||
step. The domain may be a valid public domain or not, it's up to you.
|
|
||||||
|
|
||||||
* Hostname: The hostname of your host, which will be the first host in the
|
|
||||||
network, must be chosen at this point. You can reference the [Adding a Host to
|
|
||||||
the Network](./adding-a-host-to-the-network.md) document for the constraints
|
|
||||||
on the hostname.
|
|
||||||
|
|
||||||
* IP: The IP of your host, which will be the first host in the network. This IP
|
|
||||||
must be within the chosen subnet range.
|
|
||||||
|
|
||||||
## Step 3: Create the Network
|
|
||||||
|
|
||||||
To create the network, run:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo isle network create \
|
|
||||||
--name <name> \
|
|
||||||
--ip-net <subnet> \
|
|
||||||
--domain <domain> \
|
|
||||||
--hostname <hostname>
|
|
||||||
```
|
|
||||||
|
|
||||||
At this point your host, and your network, are ready to go! To add other hosts
|
|
||||||
to the network you can reference the [Adding a Host to the Network][add-host]
|
|
||||||
document.
|
|
||||||
|
|
||||||
[add-host]: ./adding-a-host-to-the-network.md
|
|
||||||
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/
|
|
@ -1,12 +0,0 @@
|
|||||||
# Removing a Host from the Network
|
|
||||||
|
|
||||||
Removing a host from the network is as simple as
|
|
||||||
|
|
||||||
```
|
|
||||||
isle host remove --hostname <name>
|
|
||||||
```
|
|
||||||
|
|
||||||
Keep in mind that if the host being removed had storage allocations set on it
|
|
||||||
then the network will require some time to rebalance the stored data across the
|
|
||||||
remaining storage hosts. This means that when removing multiple storage hosts
|
|
||||||
from the network it is a good idea to wait a while in between each removal.
|
|
@ -1,54 +0,0 @@
|
|||||||
# Command-line Usage
|
|
||||||
|
|
||||||
This documents provides examples of how to accomplish various tasks from Isle's
|
|
||||||
command-line interface.
|
|
||||||
|
|
||||||
Isle network members fall into different roles, depending on their level of
|
|
||||||
involvement and expertise within their particular network. The documentation is
|
|
||||||
broken down by these categories, so that the reader can easily decide which
|
|
||||||
documents they need to care about.
|
|
||||||
|
|
||||||
### User Docs
|
|
||||||
|
|
||||||
Users are members who use network resources, but do not provide any network
|
|
||||||
resources themselves. Users may be accessing the network from a mobile device,
|
|
||||||
and so are not expected to be online at any particular moment.
|
|
||||||
|
|
||||||
Documentation for users:
|
|
||||||
|
|
||||||
* [Joining a Network](./user/join-a-network.md)
|
|
||||||
* [Using DNS](./user/using-dns.md) (advanced)
|
|
||||||
* Restic example (TODO)
|
|
||||||
|
|
||||||
### Operator Docs
|
|
||||||
|
|
||||||
Operators are members who own a dedicated host which they can expect to be
|
|
||||||
always-online (to the extent that's possible in a residential environment).
|
|
||||||
Operator hosts will need at least one of the following to be useful:
|
|
||||||
|
|
||||||
* A static public IP, or a dynamic public IP with [dDNS][ddns] set up.
|
|
||||||
|
|
||||||
* At least 1GB of unused storage which can be reserved for the network.
|
|
||||||
|
|
||||||
Operators are expected to be familiar with server administration, and to not be
|
|
||||||
afraid of a terminal.
|
|
||||||
|
|
||||||
Documentation for operators:
|
|
||||||
|
|
||||||
* [Contributing Storage](./operator/contributing-storage.md)
|
|
||||||
* [Contributing a Lighthouse](./operator/contributing-a-lighthouse.md)
|
|
||||||
* [Managing garage](./operator/managing-garage.md)
|
|
||||||
* [Firewalls](./operator/firewall.md)
|
|
||||||
|
|
||||||
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/
|
|
||||||
|
|
||||||
### Admin Docs
|
|
||||||
|
|
||||||
Admins are members who control membership within the network. They are likely
|
|
||||||
operators as well.
|
|
||||||
|
|
||||||
Documentation for admins:
|
|
||||||
|
|
||||||
* [Creating a New Network](./admin/creating-a-new-network.md)
|
|
||||||
* [Adding a Host to the Network](./admin/adding-a-host-to-the-network.md)
|
|
||||||
* [Removing a Host from the Network](./admin/removing-a-host-from-the-network.md)
|
|
@ -1,25 +0,0 @@
|
|||||||
# Architecture
|
|
||||||
|
|
||||||
The isle daemon is the central point around which all operations occur. Users of
|
|
||||||
the isle command-line tool (or, soon, a GUI) communicate with the daemon over a
|
|
||||||
local RPC socket to issue commands, for example to tell it to join a new network
|
|
||||||
or to retrieve the names of hosts in an already-joined network.
|
|
||||||
|
|
||||||
For every network which is joined, the isle daemon will create and manage
|
|
||||||
sub-processes to provide certain parts of its functionality. These include:
|
|
||||||
|
|
||||||
* A [nebula][nebula] instance to provide VPN functionality.
|
|
||||||
* A [dnsmasq][dnsmasq] instance to act as DNS server.
|
|
||||||
* Zero or more [garage][garage] instances, depending on how storage is
|
|
||||||
configured on the host, to provide the S3 storage layer.
|
|
||||||
|
|
||||||
The isle daemon considers the configuration and management of these
|
|
||||||
sub-processes to be an implementation detail. In other words, its RPC interface,
|
|
||||||
and therefore all user interfaces, do not expose members to the fact that these
|
|
||||||
sub-processes exist, or even that these projects are being used under the hood.
|
|
||||||
|
|
||||||
![Architectural diagram](./architecture.svg)
|
|
||||||
|
|
||||||
[nebula]: https://github.com/slackhq/nebula
|
|
||||||
[garage]: https://garagehq.deuxfleurs.fr/
|
|
||||||
[dnsmasq]: https://dnsmasq.org/doc.html
|
|
@ -1,49 +0,0 @@
|
|||||||
@startuml
|
|
||||||
skinparam componentStyle rectangle
|
|
||||||
|
|
||||||
[isle command line] as isleCommand
|
|
||||||
() "daemon.RPC" as isleDaemonRPC
|
|
||||||
[daemon.Client] as daemonClient
|
|
||||||
|
|
||||||
frame "isle daemon process" {
|
|
||||||
portin "RPC Socket" as rpcSocket
|
|
||||||
[RPC Server] as rpcServer
|
|
||||||
|
|
||||||
() "daemon.RPC" as daemonRPC
|
|
||||||
[daemon.Daemon] as daemon
|
|
||||||
[network.Network (A)] as networkA
|
|
||||||
[network.Network (B)] as networkB
|
|
||||||
|
|
||||||
|
|
||||||
rpcServer --> rpcSocket : handle
|
|
||||||
rpcServer ..> daemonRPC : dispatch
|
|
||||||
|
|
||||||
daemon - daemonRPC
|
|
||||||
daemon --> networkA
|
|
||||||
daemon --> networkB
|
|
||||||
}
|
|
||||||
|
|
||||||
isleCommand ..> isleDaemonRPC : issue commands
|
|
||||||
daemonClient - isleDaemonRPC
|
|
||||||
daemonClient --> rpcSocket
|
|
||||||
|
|
||||||
package "network A child processes" {
|
|
||||||
[nebula] as networkANebula
|
|
||||||
[garage (alloc 1)] as networkAGarage1
|
|
||||||
[garage (alloc 2)] as networkAGarage2
|
|
||||||
[dnsmasq] as networkADNSMasq
|
|
||||||
}
|
|
||||||
|
|
||||||
networkA --> networkANebula
|
|
||||||
networkA --> networkAGarage1
|
|
||||||
networkA --> networkAGarage2
|
|
||||||
networkA --> networkADNSMasq
|
|
||||||
|
|
||||||
package "network B child processes" {
|
|
||||||
[nebula] as networkBNebula
|
|
||||||
[dnsmasq] as networkBDNSMasq
|
|
||||||
}
|
|
||||||
|
|
||||||
networkB --> networkBNebula
|
|
||||||
networkB --> networkBDNSMasq
|
|
||||||
@enduml
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 13 KiB |
@ -1,47 +0,0 @@
|
|||||||
# Building Isle
|
|
||||||
|
|
||||||
Building from source requires [nix][nix].
|
|
||||||
|
|
||||||
(*NOTE* The first time you run some of these builds a lot of things will be
|
|
||||||
built from scratch. If you have not otherwise configured it, nix might be using
|
|
||||||
a tmpfs as its build directory, and the capacity of this tmpfs will probably be
|
|
||||||
exceeded by this build. You can change your build directory to somewhere on-disk
|
|
||||||
by setting the TMPDIR environment variable for `nix-daemon` (see [this github
|
|
||||||
issue][tmpdir-gh].))
|
|
||||||
|
|
||||||
[nix]: https://nixos.wiki/wiki/Nix_package_manager
|
|
||||||
[tmpdir-gh]: https://github.com/NixOS/nix/issues/2098#issuecomment-383243838
|
|
||||||
|
|
||||||
## Current System
|
|
||||||
|
|
||||||
You can build an AppImage for your current system by running the following from
|
|
||||||
the project's root:
|
|
||||||
|
|
||||||
```
|
|
||||||
nix-build -A build.appImage
|
|
||||||
```
|
|
||||||
|
|
||||||
The resulting binary can be found under `result/bin`.
|
|
||||||
|
|
||||||
## Cross-Compile
|
|
||||||
|
|
||||||
An AppImage can be cross-compiled by passing the `hostSystem` argument:
|
|
||||||
|
|
||||||
```
|
|
||||||
nix-build --argstr hostSystem "x86_64-linux" -A build.appImage
|
|
||||||
```
|
|
||||||
|
|
||||||
Supported system strings are enumerated in `nix/pkgs.nix`.
|
|
||||||
|
|
||||||
## Alternative Build Targets
|
|
||||||
|
|
||||||
Besides AppImage, Isle supports building alternative package types which are
|
|
||||||
targetted at different specific operating systems. Each of these has its own
|
|
||||||
`build.*` target.
|
|
||||||
|
|
||||||
* AppImage (all OSs): `nix-build -A build.appImage`
|
|
||||||
* Arch Linux package: `nix-build -A build.archPkg`
|
|
||||||
|
|
||||||
All targets theoretically support passing in a `hostSystem` argument to
|
|
||||||
cross-compile to a different architecture or OS, but some targets may not make
|
|
||||||
sense with some `hostSystem` values.
|
|
68
docs/dev/daemon-process-tree.plantuml
Normal file
68
docs/dev/daemon-process-tree.plantuml
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
@startuml
|
||||||
|
hide empty description
|
||||||
|
|
||||||
|
state "./cryptic-net daemon -c ./daemon.yml" as init
|
||||||
|
|
||||||
|
state AppDir {
|
||||||
|
|
||||||
|
note "All relative paths are relative to the root of the AppDir" as N1
|
||||||
|
|
||||||
|
state "./AppRun" as AppRun {
|
||||||
|
AppRun : * Set PATH to APPDIR/bin
|
||||||
|
}
|
||||||
|
|
||||||
|
state "./bin/cryptic-net-main entrypoint daemon -c ./daemon.yml" as entrypoint {
|
||||||
|
entrypoint : * Create runtime dir at $_RUNTIME_DIR_PATH
|
||||||
|
entrypoint : * Lock runtime dir
|
||||||
|
entrypoint : * Merge given and default daemon.yml files
|
||||||
|
entrypoint : * Copy bootstrap.tgz into $_DATA_DIR_PATH, if it's not there
|
||||||
|
entrypoint : * Merge daemon.yml config into bootstrap.tgz
|
||||||
|
entrypoint : * Run child processes
|
||||||
|
}
|
||||||
|
|
||||||
|
init --> AppRun : exec
|
||||||
|
AppRun --> entrypoint : exec
|
||||||
|
|
||||||
|
state "./bin/dnsmasq-entrypoint" as dnsmasqEntrypoint {
|
||||||
|
dnsmasqEntrypoint : * Create $_RUNTIME_DIR_PATH/dnsmasq.conf
|
||||||
|
}
|
||||||
|
|
||||||
|
state "./bin/dnsmasq -d -C $_RUNTIME_DIR_PATH/dnsmasq.conf" as dnsmasq
|
||||||
|
|
||||||
|
entrypoint --> dnsmasqEntrypoint : child
|
||||||
|
dnsmasqEntrypoint --> dnsmasq : exec
|
||||||
|
|
||||||
|
state "./bin/cryptic-net-main nebula-entrypoint" as nebulaEntrypoint {
|
||||||
|
nebulaEntrypoint : * Create $_RUNTIME_DIR_PATH/nebula.yml
|
||||||
|
}
|
||||||
|
|
||||||
|
state "./bin/nebula -config $_RUNTIME_DIR_PATH/nebula.yml" as nebula
|
||||||
|
|
||||||
|
entrypoint --> nebulaEntrypoint : child
|
||||||
|
nebulaEntrypoint --> nebula : exec
|
||||||
|
|
||||||
|
state "./bin/cryptic-net-main garage-entrypoint" as garageEntrypoint {
|
||||||
|
garageEntrypoint : * Create $_RUNTIME_DIR_PATH/garage-N.toml\n (one per storage allocation)
|
||||||
|
garageEntrypoint : * Run child processes
|
||||||
|
}
|
||||||
|
|
||||||
|
state "./bin/garage -c $_RUNTIME_DIR_PATH/garage-N.toml server" as garage
|
||||||
|
state "./bin/garage-apply-layout-diff" as garageApplyLayoutDiff {
|
||||||
|
garageApplyLayoutDiff : * Runs once then exits
|
||||||
|
garageApplyLayoutDiff : * Updates cluster topo
|
||||||
|
}
|
||||||
|
|
||||||
|
entrypoint --> garageEntrypoint : child (only if >1 storage allocation defined in daemon.yml)
|
||||||
|
garageEntrypoint --> garage : child (one per storage allocation)
|
||||||
|
garageEntrypoint --> garageApplyLayoutDiff : child
|
||||||
|
|
||||||
|
state "./bin/cryptic-net-main update-global-bucket" as updateGlobalBucket {
|
||||||
|
updateGlobalBucket : * Runs once then exits
|
||||||
|
updateGlobalBucket : * Updates the bootstrap data for the host in garage for other hosts to query
|
||||||
|
}
|
||||||
|
|
||||||
|
entrypoint --> updateGlobalBucket : child
|
||||||
|
}
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
|
88
docs/dev/daemon-process-tree.svg
Normal file
88
docs/dev/daemon-process-tree.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 18 KiB |
@ -1,26 +1,21 @@
|
|||||||
# Design Principles
|
# Design Principles
|
||||||
|
|
||||||
The following points form the basis for all design decisions made within the
|
The following points form the basis for all design decisions made within the
|
||||||
Isle project.
|
cryptic-net project.
|
||||||
|
|
||||||
* "Sometimes, magic is just someone spending more time on something than anyone
|
* The UX is aggressively optimized to eliminate manual intervention by users.
|
||||||
else might reasonably expect." - Teller
|
|
||||||
|
|
||||||
Isle should feel magical, in that it's making the seemingly impossible feel
|
|
||||||
easy. Accomplishing this requires a lot of care, precision, and time on the
|
|
||||||
part of Isle developers.
|
|
||||||
|
|
||||||
* The UX is aggressively optimized to eliminate manual intervention by members.
|
|
||||||
All other concerns are secondary. The concept of "UX" extends beyond GUI
|
All other concerns are secondary. The concept of "UX" extends beyond GUI
|
||||||
interfaces, and encompasses all interactions of any sort with an isle
|
interfaces, and encompasses all interactions of any sort with a cryptic-net
|
||||||
process.
|
process.
|
||||||
|
|
||||||
* All resources within an isle network are expected to be hosted on hardware
|
* All resources within a cryptic-net are expected to be hosted on hardware owned
|
||||||
owned by community members, for example home media servers or gaming rigs.
|
by community members, for example home media servers or gaming rigs. Thus, a
|
||||||
Thus, an isle network is fully autonomous.
|
cryptic-net is fully autonomous.
|
||||||
|
|
||||||
* Hardware resources are expected to be heterogenous and geographically
|
* Hardware resources are expected to be heterogenous and geographically
|
||||||
dispersed.
|
dispersed.
|
||||||
|
|
||||||
* It is expected that a single host might be a part of multiple, independent
|
* It is expected that a single host might be a part of multiple, independent
|
||||||
isle networks. These should not conflict with each other, nor share resources.
|
cryptic-net networks. These should not conflict with each other, nor share
|
||||||
|
resources.
|
||||||
|
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
# Glossary
|
|
||||||
|
|
||||||
The purpose of this document is define the specific terms which should be used
|
|
||||||
for various concepts, with the goal of establishing consistency throughout
|
|
||||||
documentation and source code.
|
|
||||||
|
|
||||||
- "Isle" - The name of this project, which is a proper noun and so should always
|
|
||||||
be capitalized.
|
|
||||||
|
|
||||||
- "isle" - The name of the binary or program produced by the Isle project. isle
|
|
||||||
is the name of the file itself, as well as an instance of the process, and so
|
|
||||||
is always lower-case.
|
|
||||||
|
|
||||||
- "host" - A computer or device on which isle is run.
|
|
||||||
|
|
||||||
- "isle network", "network" - A collection of hosts which communicate and share
|
|
||||||
resources with each other via the Isle project.
|
|
||||||
|
|
||||||
- "garage cluster" - Garage is one of the sub-processes which isle is able to
|
|
||||||
run. These garage process connect together to form a cluster. We use the
|
|
||||||
term "cluster" in the context of garage to stay consistent with garage's
|
|
||||||
documentation and command-line.
|
|
||||||
|
|
||||||
- "member" - A person who takes part in the usage, operation, or administration
|
|
||||||
of an isle network.
|
|
@ -1,16 +0,0 @@
|
|||||||
# Developer Documentation
|
|
||||||
|
|
||||||
This section of the documentation is targeted at those who are contributing to
|
|
||||||
the Isle codebase. It is recommended to start with the following pages, in order
|
|
||||||
to better understand how to navigate and work on the codebase.
|
|
||||||
|
|
||||||
* [Glossary](./glossary.md)
|
|
||||||
* [Design Principles](./design-principles.md)
|
|
||||||
* [Architecture](./architecture.md)
|
|
||||||
|
|
||||||
These pages can be helpful in specific situations.
|
|
||||||
|
|
||||||
* [Building Isle](./building.md)
|
|
||||||
* [Testing Isle](./testing.md)
|
|
||||||
* [Rebuilding Documentation](./rebuilding-documentation.md)
|
|
||||||
* [Releases](./releases.md)
|
|
@ -1,6 +1,6 @@
|
|||||||
# Rebuilding Documentation
|
# Rebuilding Documentation
|
||||||
|
|
||||||
Most documentation for Isle takes the form of markdown (`.md`) files,
|
Most documentation for cryptic-net takes the form of markdown (`.md`) files,
|
||||||
which do not require any build step. There are a few other kinds of files, such
|
which do not require any build step. There are a few other kinds of files, such
|
||||||
as `.plantuml` files, which do require a build step. If these are changed then
|
as `.plantuml` files, which do require a build step. If these are changed then
|
||||||
their artifacts should be rebuilt by doing:
|
their artifacts should be rebuilt by doing:
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
# Releases
|
|
||||||
|
|
||||||
A release consists of:
|
|
||||||
|
|
||||||
- A full set of `isle` binaries for all supported platforms, compiled from the
|
|
||||||
same source.
|
|
||||||
- A text file containing hashes of each binary.
|
|
||||||
- A file containing a signature of the hash file, created by whoever is building
|
|
||||||
the release.
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
*NOTE: This has only been tested from an x86_64 linux machine*
|
|
||||||
|
|
||||||
To create a release only a functional nix installation is required. Simply run
|
|
||||||
the `./release.sh` script, and input a release name when prompted.
|
|
||||||
|
|
||||||
From here an `isle` binary will be cross-compiled for all supported
|
|
||||||
platforms. This will take a long time the first time you perform it on your
|
|
||||||
machine.
|
|
||||||
|
|
||||||
Once compilation is completed, the release will be signed using the default GPG
|
|
||||||
key on your machine, and you will be prompted for its password in order to
|
|
||||||
create the signature.
|
|
||||||
|
|
||||||
## Releasing
|
|
||||||
|
|
||||||
Releases are uploaded to the repository's Releases page, and release naming
|
|
||||||
follows the conventional semantic versioning system. Each release should be
|
|
||||||
accompanied by a set of changes which have occurred since the last release,
|
|
||||||
described both in the `CHANGELOG.md` file and in the description on the Release
|
|
||||||
itself.
|
|
@ -1,39 +0,0 @@
|
|||||||
# Testing Isle
|
|
||||||
|
|
||||||
All tests are currently written as go tests, and as such can be run from the
|
|
||||||
`go` directory using the normal go testing tool.
|
|
||||||
|
|
||||||
```
|
|
||||||
cd go
|
|
||||||
go test -run Foo ./daemon
|
|
||||||
go test ./... # Test everything
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration Tests
|
|
||||||
|
|
||||||
Integration tests are those which require processes or state external to the
|
|
||||||
test itself. Integration tests are marked using the
|
|
||||||
`toolkit.MarkIntegrationTest` function, which will cause them to be skipped
|
|
||||||
unless being run in the integration test environment.
|
|
||||||
|
|
||||||
Besides a normal nix installation (like all Isle development needs), integration
|
|
||||||
tests also require `sudo` and [capsh][capsh] to be installed on the system.
|
|
||||||
|
|
||||||
[capsh]: https://www.man7.org/linux/man-pages/man1/capsh.1.html
|
|
||||||
|
|
||||||
By running tests using the `go/integration_test.sh` script the tests will be
|
|
||||||
automatically run in the integration test environment. All arguments will be
|
|
||||||
passed directly to the go testing tool.
|
|
||||||
|
|
||||||
```
|
|
||||||
cd go
|
|
||||||
./integration_test.sh -run Foo ./daemon
|
|
||||||
```
|
|
||||||
|
|
||||||
`integration_test.sh` wraps a call to `go test` in a bash shell which has all
|
|
||||||
required binaries available to it, and which has acquired necessary
|
|
||||||
[capabilities][capabilities] to use the binaries as needed. Acquiring
|
|
||||||
capabilities is done by elevating the user to root using `sudo`, and then
|
|
||||||
dropping them back down to a shell of the original user with capabilities set.
|
|
||||||
|
|
||||||
[capabilities]: https://wiki.archlinux.org/title/Capabilities
|
|
@ -1,86 +0,0 @@
|
|||||||
# Installation
|
|
||||||
|
|
||||||
This document will guide you through the process of obtaining and installing
|
|
||||||
Isle on your machine.
|
|
||||||
|
|
||||||
NOTE currently only linux machines with the following architectures are
|
|
||||||
supported:
|
|
||||||
|
|
||||||
- `x86_64` (aka `amd64`)
|
|
||||||
- `aarch64` (aka `arm64`)
|
|
||||||
- `i686`
|
|
||||||
|
|
||||||
(`i686` has not been tested.)
|
|
||||||
|
|
||||||
More OSs and architectures coming soon!
|
|
||||||
|
|
||||||
## Install isle
|
|
||||||
|
|
||||||
How isle gets installed depends on which Linux distribution you are using.
|
|
||||||
|
|
||||||
### Archlinux (also Manjaro)
|
|
||||||
|
|
||||||
Download the latest `.pkg.tar.zst` package file for your platform from
|
|
||||||
[this link][latest].
|
|
||||||
|
|
||||||
Install the package using pacman:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo pacman -U /path/to/isle-*.pkg.tar.zst
|
|
||||||
```
|
|
||||||
|
|
||||||
### Other Distributions
|
|
||||||
|
|
||||||
If a package file is not available for your distribution you can still install
|
|
||||||
an AppImage directly. It is assumed that all commands below are run as root.
|
|
||||||
|
|
||||||
Download the latest `.AppImage` binary for your platform from
|
|
||||||
[this link][latest], and place it in your `/usr/bin` directory.
|
|
||||||
|
|
||||||
Create a `daemon.yml` file using default values by doing:
|
|
||||||
```
|
|
||||||
mkdir -p /etc/isle/
|
|
||||||
isle daemon --dump-config > /etc/isle/daemon.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
Create a system user for the isle daemon to run as:
|
|
||||||
```
|
|
||||||
useradd -r -s /bin/false -C "isle Daemon" isle
|
|
||||||
```
|
|
||||||
|
|
||||||
If your distro uses systemd, download [the latest systemd service
|
|
||||||
file][serviceFile] and place it in `/etc/systemd/system`. Run `systemctl
|
|
||||||
daemon-reload` to ensure systemd has seen the new service file.
|
|
||||||
|
|
||||||
If your distro uses an init system other than systemd then you will need to
|
|
||||||
configure that yourself. You can use the systemd service file linked above as a
|
|
||||||
reference.
|
|
||||||
|
|
||||||
[latest]: https://code.betamike.com/micropelago/isle/releases/latest
|
|
||||||
[serviceFile]: https://code.betamike.com/micropelago/isle/src/branch/main/dist/linux/isle.service
|
|
||||||
|
|
||||||
### From Source
|
|
||||||
|
|
||||||
If you'd like to build your own `isle` binary from scratch, see the [Building
|
|
||||||
Isle](./dev/building.md) document.
|
|
||||||
|
|
||||||
## Add Users to the `isle` Group (Optional)
|
|
||||||
|
|
||||||
If you wish to run isle commands as a user other than root, you can add that
|
|
||||||
user to the `isle` group:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo usermod -aG isle username
|
|
||||||
```
|
|
||||||
|
|
||||||
## Start the isle Service
|
|
||||||
|
|
||||||
Once installed and bootstrapped you can enable and start the isle service by
|
|
||||||
doing:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo systemctl enable --now isle
|
|
||||||
```
|
|
||||||
|
|
||||||
(NOTE If your distro uses an init system other than systemd then you will need
|
|
||||||
to instead start isle according to that system's requirements.)
|
|
@ -1,7 +1,7 @@
|
|||||||
# Contributing a Lighthouse
|
# Contributing a Lighthouse
|
||||||
|
|
||||||
The [nebula][nebula] project provides the VPN component which is used by
|
The [nebula][nebula] project provides the VPN component which is used by
|
||||||
Isle. Every nebula network requires at least one (but preferably more)
|
cryptic-net. Every nebula network requires at least one (but preferably more)
|
||||||
publicly accessible hosts. These hosts are called lighthouses.
|
publicly accessible hosts. These hosts are called lighthouses.
|
||||||
|
|
||||||
Lighthouses do _not_ route traffic between hosts on the VPN. Rather, they
|
Lighthouses do _not_ route traffic between hosts on the VPN. Rather, they
|
||||||
@ -10,7 +10,7 @@ NAT punching through any NATs that hosts might be behind. As such, they are very
|
|||||||
lightweight to run, and require no storage resources at all.
|
lightweight to run, and require no storage resources at all.
|
||||||
|
|
||||||
If your host machine has a public static IP, or a dynamic public IP with
|
If your host machine has a public static IP, or a dynamic public IP with
|
||||||
[dDNS][ddns] set up, then it can contribute a lighthouse.
|
[dDNS][ddns] set up, then it can contribute a lighthouse for cryptic-net.
|
||||||
|
|
||||||
[nebula]: https://github.com/slackhq/nebula
|
[nebula]: https://github.com/slackhq/nebula
|
||||||
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/
|
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/
|
||||||
@ -26,14 +26,21 @@ traffic on that port to your host.
|
|||||||
|
|
||||||
Configure your host's firewall to allow all UDP traffic on that port.
|
Configure your host's firewall to allow all UDP traffic on that port.
|
||||||
|
|
||||||
|
## Create daemon.yml
|
||||||
|
|
||||||
|
First, if you haven't already, [create a `daemon.yml`
|
||||||
|
file](../user/creating-a-daemonyml-file.md). This will be used to
|
||||||
|
configure your `cryptic-net daemon` process with the public address that other
|
||||||
|
hosts can find your daemon on.
|
||||||
|
|
||||||
## Edit daemon.yml
|
## Edit daemon.yml
|
||||||
|
|
||||||
Open your `/etc/isle/daemon.yml` file in a text editor, and find the
|
Open your `daemon.yml` file in a text editor, and find the `vpn.public_addr`
|
||||||
`vpn.public_addr` field. Update that field to reflect your host's IP/DNS name
|
field. Update that field to reflect your host's IP/DNS name and your chosen UDP
|
||||||
and your chosen UDP port.
|
port.
|
||||||
|
|
||||||
## Restart the Daemon
|
## Restart the Daemon
|
||||||
|
|
||||||
With the `daemon.yml` configured, you should restart your `isle daemon`
|
With the `daemon.yml` configured, you should restart your `cryptic-net daemon`
|
||||||
process. On startup the daemon will add its public address to the global
|
process. On startup the daemon will add its public address to the global
|
||||||
configuration, which other hosts will pick up on and begin using.
|
configuration, which other hosts will pick up on and begin using.
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
# Contributing Storage
|
# Contributing Storage
|
||||||
|
|
||||||
This document is for you if your host machine can be reliably be online at all
|
If your host machine can be reasonably sure of being online most, if not all, of
|
||||||
times and has 1GB or more of unused drive space you'd like to contribute to the
|
the time, and has 100GB or more of unused drive space you'd like to contribute
|
||||||
network.
|
to the network, then this document is for you.
|
||||||
|
|
||||||
## Edit `daemon.yml`
|
## Create daemon.yml
|
||||||
|
|
||||||
Open your `/etc/isle/daemon.yml` file in a text editor, and find the
|
First, if you haven't already, [create a `daemon.yml`
|
||||||
|
file](../user/creating-a-daemonyml-file.md). This will be used to
|
||||||
|
configure your `cryptic-net daemon` process with the storage locations and
|
||||||
|
capacities you want to contribute.
|
||||||
|
|
||||||
|
## Edit daemon.yml
|
||||||
|
|
||||||
|
Open your `daemon.yml` file in a text editor, and find the
|
||||||
`storage.allocations` section.
|
`storage.allocations` section.
|
||||||
|
|
||||||
Each allocation in the allocations list describes the space being contributed
|
Each allocation in the allocations list describes the space being contributed
|
||||||
@ -16,7 +23,7 @@ one allocation listed.
|
|||||||
The comments in the file should be self-explanatory, but ask your admin if you
|
The comments in the file should be self-explanatory, but ask your admin if you
|
||||||
need any clarification.
|
need any clarification.
|
||||||
|
|
||||||
Here is an example set of allocations for a host which is contributing space
|
Here are an example set of allocations for a host which is contributing space
|
||||||
from two separate drives:
|
from two separate drives:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -24,44 +31,37 @@ storage:
|
|||||||
allocations:
|
allocations:
|
||||||
|
|
||||||
# 1.2 TB are being shared from drive1
|
# 1.2 TB are being shared from drive1
|
||||||
- data_path: /mnt/drive1/isle/data
|
- data_path: /mnt/drive1/cryptic-net/data
|
||||||
meta_path: /mnt/drive1/isle/meta
|
meta_path: /mnt/drive1/cryptic-net/meta
|
||||||
capacity: 1200
|
capacity: 1200
|
||||||
|
api_port: 3900
|
||||||
|
rpc_port: 3901
|
||||||
|
web_port: 3902
|
||||||
|
|
||||||
# 100 GB are being shared from drive2
|
# 100 GB (the minimum) are being shared from drive2
|
||||||
- data_path: /mnt/drive2/isle/data
|
- data_path: /mnt/drive2/cryptic-net/data
|
||||||
meta_path: /mnt/drive2/isle/meta
|
meta_path: /mnt/drive2/cryptic-net/meta
|
||||||
capacity: 100
|
capacity: 100
|
||||||
|
api_port: 3910
|
||||||
|
rpc_port: 3911
|
||||||
|
web_port: 3912
|
||||||
```
|
```
|
||||||
|
|
||||||
## Set Up Your Firewall
|
## Setup Firewall
|
||||||
|
|
||||||
See the doc on [Firewalls](./firewalls.md), to be sure that your host's firewall
|
You will need to configure your hosts's firewall to allow traffic from
|
||||||
is properly set up for providing storage.
|
cryptic-net IPs on the ports you specified in your allocations.
|
||||||
|
|
||||||
## Restart the Daemon
|
## Restart the Daemon
|
||||||
|
|
||||||
With the `daemon.yml` configured, you should restart your `isle daemon`
|
With the `daemon.yml` configured, you should restart your `cryptic-net daemon`
|
||||||
process.
|
process.
|
||||||
|
|
||||||
The `isle daemon` will automatically allow the ports used for your
|
|
||||||
storage allocations in the vpn firewall.
|
|
||||||
|
|
||||||
## Removing Allocations
|
|
||||||
|
|
||||||
If you later decide to no longer provide storage simply remove the
|
|
||||||
`storage.allocations` item from your `/etc/isle/daemon.yml` file and restart the
|
|
||||||
`isle daemon` process.
|
|
||||||
|
|
||||||
Once removed, it is advisable to wait some time before removing storage
|
|
||||||
allocations from other hosts. This ensures that all data which was previously
|
|
||||||
on this host has had a chance to fully replicate to multiple other hosts.
|
|
||||||
|
|
||||||
## Further Reading
|
## Further Reading
|
||||||
|
|
||||||
Isle uses the [garage][garage] project for its storage system. See the
|
cryptic-net uses the [garage][garage] project for its storage system. See the
|
||||||
[Managing Garage](managing-garage.md) document for more
|
[Managing Garage](managing-garage.md) document for more
|
||||||
information on how to interact directly with the garage instance being run by
|
information on how to interact directly with the garage instance being run by
|
||||||
isle.
|
cryptic-net.
|
||||||
|
|
||||||
[garage]: https://garagehq.deuxfleurs.fr/documentation/quick-start/
|
[garage]: https://garagehq.deuxfleurs.fr/documentation/quick-start/
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
# Firewalls
|
|
||||||
|
|
||||||
When providing services on your host, whether
|
|
||||||
[network](./contributing-a-lighthouse.md) or
|
|
||||||
[storage](./contributing-storage.md), you will need to ensure that your host's
|
|
||||||
firewall is configured correctly to do so.
|
|
||||||
|
|
||||||
To make matters even more confusing, there are actually two firewalls at play:
|
|
||||||
the host's firewall, and the VPN firewall.
|
|
||||||
|
|
||||||
## VPN Firewall
|
|
||||||
|
|
||||||
Isle uses the [nebula](https://github.com/slackhq/nebula) project to
|
|
||||||
provide its VPN layer. Nebula ships with its own [builtin
|
|
||||||
firewall](https://nebula.defined.net/docs/config/firewall), which only applies
|
|
||||||
to connections coming in over the virtual network interface which it creates.
|
|
||||||
This firewall can be manually configured as part of the `/etc/isle/daemon.yml`
|
|
||||||
file.
|
|
||||||
|
|
||||||
Any storage instances which are defined as part of the `daemon.yml` file will
|
|
||||||
have their network ports automatically added to the VPN firewall by isle.
|
|
||||||
This means that you only need to configure the VPN firewall if you are hosting
|
|
||||||
services for your isle network besides storage.
|
|
||||||
|
|
||||||
## Host Firewall
|
|
||||||
|
|
||||||
The host you are running isle on will almost definitely have a firewall
|
|
||||||
running, separate from the VPN firewall. If you wish to provide services for
|
|
||||||
your isle network from your host, you will need to allow their ports in your
|
|
||||||
host's firewall.
|
|
||||||
|
|
||||||
**isle does _not_ automatically configure your host's firewall to any extent!**
|
|
||||||
|
|
||||||
One option is to open your host to all traffic from your isle network, and
|
|
||||||
allow the VPN firewall to be fully responsible for filtering traffic. To do this
|
|
||||||
on Linux using iptables, for example, you would add something like this to your
|
|
||||||
iptables configuration:
|
|
||||||
|
|
||||||
```
|
|
||||||
-A INPUT --source <network CIDR> --jump ACCEPT
|
|
||||||
```
|
|
||||||
|
|
||||||
being sure to replace the network CIDR with the one for you network.
|
|
||||||
|
|
||||||
If you don't feel comfortable allowing nebula to deal with all packet filtering,
|
|
||||||
you will need to manually determine and add the ports for each nebula service to
|
|
||||||
your host's firewall. It is recommended that you manually specify any storage
|
|
||||||
allocation ports defined in your `daemon.yml` if this is the approach you take.
|
|
@ -1,11 +1,11 @@
|
|||||||
# Managing Garage
|
# Managing Garage
|
||||||
|
|
||||||
The garage project provides the network storage component for
|
The garage project provides the network storage component for
|
||||||
Isle. If you're reading this document then you would likely benefit
|
cryptic-net. If you're reading this document then you would likely benefit
|
||||||
greatly from reading the [garage documentation][garage] on their website. It's
|
greatly from reading the [garage documentation][garage] on their website. It's
|
||||||
extremely well written and concise.
|
extremely well written and concise.
|
||||||
|
|
||||||
Note that the `isle daemon` process will handle all setup steps described
|
Note that the `cryptic-net daemon` process will handle all setup steps described
|
||||||
in that documentation, but it's still good to have an understanding of how
|
in that documentation, but it's still good to have an understanding of how
|
||||||
garage works and what it can do.
|
garage works and what it can do.
|
||||||
|
|
||||||
@ -13,12 +13,12 @@ garage works and what it can do.
|
|||||||
|
|
||||||
## Garage Runtime Note
|
## Garage Runtime Note
|
||||||
|
|
||||||
There is an important thing to note regarding how isle runs garage. As
|
There is an important thing to note regarding how cryptic-net runs garage. As
|
||||||
described in the [Contributing Storage](contributing-storage.md) document, a
|
described in the [Contributing Storage](contributing-storage.md) document, a
|
||||||
single `isle daemon` process can be configured to provide any number of
|
single `cryptic-net daemon` process can be configured to provide any number of
|
||||||
storage allocations.
|
storage allocations.
|
||||||
|
|
||||||
For each allocation which is configured, `isle daemon` will configure and
|
For each allocation which is configured, `cryptic-net daemon` will configure and
|
||||||
run a separate `garage server` instance as a sub-process. Each garage will use
|
run a separate `garage server` instance as a sub-process. Each garage will use
|
||||||
the host's name as its zone in the garage cluster layout, which means that the
|
the host's name as its zone in the garage cluster layout, which means that the
|
||||||
cluster will prefer to not replicate the same data within the same host, but may
|
cluster will prefer to not replicate the same data within the same host, but may
|
||||||
@ -26,14 +26,14 @@ do so if necessary.
|
|||||||
|
|
||||||
## Garage CLI
|
## Garage CLI
|
||||||
|
|
||||||
Every `isle` binary contains a full `garage` binary embedded into it.
|
Every `cryptic-net` binary contains a full `garage` binary embedded into it.
|
||||||
This binary can be accessed directly like so:
|
This binary can be accessed directly like so:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo isle garage cli <subcmd> <args>
|
sudo cryptic-net garage cli <subcmd> <args>
|
||||||
```
|
```
|
||||||
|
|
||||||
Before handing off execution to the `garage` binary, the `isle` process
|
Before handing off execution to the `garage` binary, the `cryptic-net` process
|
||||||
will automatically set up the RPC host and secret environment variables.
|
will automatically set up the RPC host and secret environment variables.
|
||||||
|
|
||||||
If the host which is running the command has more than one allocation
|
If the host which is running the command has more than one allocation
|
||||||
@ -47,7 +47,7 @@ connected to.
|
|||||||
To display the current layout of the garage cluster:
|
To display the current layout of the garage cluster:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo isle garage cli layout show
|
sudo cryptic-net garage cli layout show
|
||||||
```
|
```
|
||||||
|
|
||||||
**(DO NOT CHANGE THE CLUSTER LAYOUT UNLESS YOU KNOW WHAT YOU'RE DOING!)**
|
**(DO NOT CHANGE THE CLUSTER LAYOUT UNLESS YOU KNOW WHAT YOU'RE DOING!)**
|
||||||
@ -55,11 +55,11 @@ sudo isle garage cli layout show
|
|||||||
To create a new bucket:
|
To create a new bucket:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo isle garage cli bucket create new-bucket
|
sudo cryptic-net garage cli bucket create new-bucket
|
||||||
```
|
```
|
||||||
|
|
||||||
To list existing buckets:
|
To list existing buckets:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo isle garage cli bucket list
|
sudo cryptic-net garage cli bucket list
|
||||||
```
|
```
|
||||||
|
100
docs/roadmap.md
Normal file
100
docs/roadmap.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# Roadmap
|
||||||
|
|
||||||
|
The following are rough outlines of upcoming work on the roadmap, roughly in the
|
||||||
|
order they will be implemented.
|
||||||
|
|
||||||
|
## Main quest
|
||||||
|
|
||||||
|
These items are listed more or less in the order they need to be completed, as
|
||||||
|
they generally depend on the items previous to them.
|
||||||
|
|
||||||
|
### Cross Compilation
|
||||||
|
|
||||||
|
Currently the only supported OS/CPU is Linux/amd64. This can be expanded
|
||||||
|
_theoretically_ quite easily, using nix's cross compilation tools. First target
|
||||||
|
should be OSX/arm64, but windows would also be quite the get.
|
||||||
|
|
||||||
|
### Bootstrap
|
||||||
|
|
||||||
|
This will be difficult. There's currently no way to bootstrap a new cryptic-net
|
||||||
|
network from scratch, only the currently existing one is supported. Support for
|
||||||
|
IPv6 internal CIDR should also be a part of this effort.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Once bootstrap is generalized, we'll be able to write some automated tests.
|
||||||
|
|
||||||
|
## Side quests
|
||||||
|
|
||||||
|
These items aren't necessarily required by the main quest, and aren't dependent
|
||||||
|
on any other items being completed. They are nice-to-haves that we do want to
|
||||||
|
eventually complete, but aren't the main focus.
|
||||||
|
|
||||||
|
### DNS resolver settings
|
||||||
|
|
||||||
|
The daemon should update the resolver settings of the host, so that it
|
||||||
|
automatically serves DNS queries, unless set to not do so in `daemon.yml`.
|
||||||
|
|
||||||
|
### Install sub-command
|
||||||
|
|
||||||
|
It would be great to have a `cryptic-net install` sub-command which would
|
||||||
|
auto-detect the installed operating system and install the daemon automatically.
|
||||||
|
|
||||||
|
### Web server + interface
|
||||||
|
|
||||||
|
One idea is to have every `cryptic-net daemon` run a webserver as one of its
|
||||||
|
sub-processes. This server could serve multiple functions:
|
||||||
|
|
||||||
|
- Possible public gateway, if the host is configured to be public, into internal
|
||||||
|
cryptic-net services. This will require some sort of service discovery
|
||||||
|
mechanism that other hosts in the network can easily hook into via their
|
||||||
|
`daemon.yml`s. Ideally this mechanism would involve little beyond updating
|
||||||
|
entries in garage, which the public gateways then pick up on.
|
||||||
|
|
||||||
|
One wrinkle here will be TLS certificates. Ideally internal hosts could host
|
||||||
|
websites publicly via the gateways on their network, using arbitrary domains
|
||||||
|
that the users own. The gateways will need some way of obtaining certs for
|
||||||
|
these domains automatically (or as automatically as possible).
|
||||||
|
|
||||||
|
- Local interface for the `cryptic-net daemon` process. For example, status and
|
||||||
|
connectivity information for the local host could be provided via a simple web
|
||||||
|
interface, which the user can open in their browser. This saves us the effort
|
||||||
|
of needing to develop UIs for individual OSs. This could also make remotely
|
||||||
|
debugging hosts easier for admins.
|
||||||
|
|
||||||
|
### Mobile app
|
||||||
|
|
||||||
|
To start with a simple mobile app which provided connectivity to the network
|
||||||
|
would be great. Such a mobile app could be based on the existing
|
||||||
|
[mobile_nebula](https://github.com/DefinedNet/mobile_nebula). The main changes
|
||||||
|
needed would be:
|
||||||
|
|
||||||
|
- Allow importing a `bootstrap.tgz` file, rather than requiring manual setup by
|
||||||
|
users.
|
||||||
|
|
||||||
|
- Set device's DNS settings. There is an [open
|
||||||
|
PR](https://github.com/DefinedNet/mobile_nebula/pull/18) for android to do
|
||||||
|
this upstream.
|
||||||
|
|
||||||
|
- Rebranding and possibly submitting to Apple app store (bleh).
|
||||||
|
|
||||||
|
### Don't run as root
|
||||||
|
|
||||||
|
It's currently a pretty hard requirement for `cryptic-net daemon` to run as
|
||||||
|
root. This is due to:
|
||||||
|
|
||||||
|
- nebula's network interface root to be started.
|
||||||
|
|
||||||
|
- dnsmasq listening on port 53, generally a protected port.
|
||||||
|
|
||||||
|
If we can't figure out how to get these things running from the start as
|
||||||
|
non-privileged users, we at least need to get cryptic-net to drop priveleges
|
||||||
|
from root after initial startup.
|
||||||
|
|
||||||
|
### Plugins
|
||||||
|
|
||||||
|
It would not be difficult to spec out a plugin system using nix commands.
|
||||||
|
Existing components could be rigged to use this plugin system, and we could then
|
||||||
|
use the system to add future components which might prove useful. Once the
|
||||||
|
project is public such a system would be much appreciated I think, as it would
|
||||||
|
let other groups rig their binaries with all sorts of new functionality.
|
@ -1,20 +1,14 @@
|
|||||||
{
|
{
|
||||||
|
|
||||||
buildSystem ? builtins.currentSystem,
|
pkgs ? (import ../nix/pkgs.nix).stable,
|
||||||
hostSystem ? buildSystem,
|
|
||||||
pkgsNix ? (import ../nix/pkgs.nix),
|
|
||||||
|
|
||||||
}: let
|
}: pkgs.mkShell {
|
||||||
pkgs = pkgsNix.default {
|
name = "cryptic-net-build-docs";
|
||||||
inherit buildSystem hostSystem;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
pkgs.mkShell {
|
|
||||||
name = "isle-build-docs";
|
|
||||||
buildInputs = [ pkgs.plantuml ];
|
buildInputs = [ pkgs.plantuml ];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
set -e
|
set -e
|
||||||
plantuml -tsvg ./dev/*.plantuml
|
plantuml -tsvg ./dev/*.plantuml
|
||||||
exit 0
|
exit 0
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
32
docs/user/creating-a-daemonyml-file.md
Normal file
32
docs/user/creating-a-daemonyml-file.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Creating a daemon.yml File
|
||||||
|
|
||||||
|
The `cryptic-net daemon` process has generally sane defaults and does not need
|
||||||
|
to be configured for most users. However, in some cases it does, so this
|
||||||
|
document describes how to use the `daemon.yml` file to handle those cases.
|
||||||
|
|
||||||
|
## Create daemon.yml
|
||||||
|
|
||||||
|
First, create a `daemon.yml` file. You can create a new `daemon.yml` with
|
||||||
|
default values filled in by doing:
|
||||||
|
|
||||||
|
```
|
||||||
|
cryptic-net daemon --dump-config > /path/to/daemon.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
If you open that file in a text editor you can view all default values that
|
||||||
|
`cryptic-net daemon` ships with, as well as documentation for all configurable
|
||||||
|
parameters. Feel free to edit this file as needed.
|
||||||
|
|
||||||
|
## Using daemon.yml
|
||||||
|
|
||||||
|
With the `daemon.yml` created and configured, you can configure your daemon
|
||||||
|
process to use it by passing it as the `-c` argument:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo cryptic-net daemon -c /path/to/daemon.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are an operator then your host should be running its `cryptic-net daemon`
|
||||||
|
process in systemd (see [Getting Started](getting-started.md) if
|
||||||
|
not), and you will need to modify the `cryptic-net.service` accordingly.
|
||||||
|
|
117
docs/user/getting-started.md
Normal file
117
docs/user/getting-started.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# Getting Started
|
||||||
|
|
||||||
|
This document will guide you through the process of obtaining a cryptic-net
|
||||||
|
binary and joining the network.
|
||||||
|
|
||||||
|
NOTE currently only linux machines with amd64/x86_64 processors are supported.
|
||||||
|
More OSs and architectures coming soon!
|
||||||
|
|
||||||
|
## Obtaining a cryptic-net Binary
|
||||||
|
|
||||||
|
Every host can have a binary built for it which has all configuration for that
|
||||||
|
host embedded directly into it. Such binaries require no extra configuration by
|
||||||
|
the user to use, and have no dependencies on anything else in the user's system.
|
||||||
|
|
||||||
|
The process of obtaining a custom binary for your host is quite simple: ask an
|
||||||
|
admin of the network you'd like to join to give you one!
|
||||||
|
|
||||||
|
Note that if you'd like to join the network on multiple devices, each device
|
||||||
|
will needs its own binary, so be sure to tell your admin how many you want to
|
||||||
|
add and their names.
|
||||||
|
|
||||||
|
### Obtaining a cryptic-net Binary, the Hard Way
|
||||||
|
|
||||||
|
Alternatively, you can build your own binary by running the following from the
|
||||||
|
project's root:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-build -A appImage
|
||||||
|
```
|
||||||
|
|
||||||
|
(*NOTE* Dependencies of `cryptic-net` seemingly compile all of musl and rust
|
||||||
|
from scratch (it's not clear why, blame garage!). If you have not otherwise
|
||||||
|
configured it, nix might be using a tmpfs as its build directory, and the
|
||||||
|
capacity of this tmpfs will probably be exceeded by this build. You can change
|
||||||
|
your build directory to somewhere on-disk by setting the TMPDIR environment
|
||||||
|
variable for `nix-daemon` (see [this github issue][tmpdir-gh].))
|
||||||
|
|
||||||
|
The resulting binary can be found in the `result` directory which is created.
|
||||||
|
|
||||||
|
In this case you will need an admin to provide you with a `bootstrap.tgz` for
|
||||||
|
your host, rather than a custom binary. When running the daemon in the following
|
||||||
|
steps you will need to provide the `--bootstrap-path` CLI argument to the daemon
|
||||||
|
process.
|
||||||
|
|
||||||
|
[tmpdir-gh]: https://github.com/NixOS/nix/issues/2098#issuecomment-383243838
|
||||||
|
|
||||||
|
## Running the Daemon
|
||||||
|
|
||||||
|
Once you have a binary, you will need to run the `daemon` sub-command as the
|
||||||
|
root user. This can most easily be done using the `sudo` command, in a terminal:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo /path/to/cryptic-net daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start the daemon process, which will keep running until you kill it
|
||||||
|
with `ctrl-c`.
|
||||||
|
|
||||||
|
You can double check that the daemon is running properly by pinging a private IP
|
||||||
|
from the network in a separate terminal:
|
||||||
|
|
||||||
|
```
|
||||||
|
ping 10.10.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
If the pings are successful then your daemon is working!
|
||||||
|
|
||||||
|
## Installing the Daemon as a Systemd Service
|
||||||
|
|
||||||
|
NOTE in the future we will introduce an `install` sub-command which will
|
||||||
|
automate most of this section.
|
||||||
|
|
||||||
|
Rather than running the daemon manually, you can install it as a systemd
|
||||||
|
service. This way your daemon will automatically start in the background on
|
||||||
|
startup, and will be restarted if it has any issues.
|
||||||
|
|
||||||
|
To do so, create a file at `/etc/systemd/system/cryptic-net.service` with the
|
||||||
|
following contents:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description=cryptic-net
|
||||||
|
Requires=network.target
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=always
|
||||||
|
RestartSec=1s
|
||||||
|
User=root
|
||||||
|
ExecStart=/path/to/cryptic-net daemon
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember to change the `/path/to/cryptic-net` part to the actual absolute path
|
||||||
|
to your binary!
|
||||||
|
|
||||||
|
Once created, perform the following commands in a terminal to enable the
|
||||||
|
service:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now cryptic-net
|
||||||
|
```
|
||||||
|
|
||||||
|
You can check the service's status by doing:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo systemctl status cryptic-net
|
||||||
|
```
|
||||||
|
|
||||||
|
and you can view its full logs by doing:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo journalctl -lu cryptic-net
|
||||||
|
```
|
@ -1,20 +0,0 @@
|
|||||||
# Join a Network
|
|
||||||
|
|
||||||
This document guide you through the process of joining an existing network
|
|
||||||
of isle hosts. If instead you wish to create a new network for others to join
|
|
||||||
then see the [Creating a New Network][creating-a-new-network] page.
|
|
||||||
|
|
||||||
To join an existing network you will need to first obtain a `bootstrap.json`
|
|
||||||
file. The `bootstrap.json` file contains all information required for your
|
|
||||||
particular host to join the network, and must be generated and provided to you
|
|
||||||
by an admin for the network.
|
|
||||||
|
|
||||||
Once obtained, you can join the network by doing:
|
|
||||||
|
|
||||||
```
|
|
||||||
isle network join --bootstrap-path /path/to/bootstrap.json
|
|
||||||
```
|
|
||||||
|
|
||||||
After a few moments you will have successfully joined the network!
|
|
||||||
|
|
||||||
[creating-a-new-network]: ../admin/creating-a-new-network.md
|
|
@ -1,35 +1,36 @@
|
|||||||
# Using DNS
|
# Using DNS
|
||||||
|
|
||||||
Every `isle daemon` process ships with a DNS server which runs
|
Every `cryptic-net daemon` process ships with a DNS server which runs
|
||||||
automatically. This server will listen on port 53 on the VPN IP of that
|
automatically. This server will listen on port 53 on the VPN IP of that
|
||||||
particular host.
|
particular host.
|
||||||
|
|
||||||
The server will serve requests for `<hostname>.hosts.<domain>` hostnames,
|
The server will serve requests for `<hostname>.hosts.cryptic.io` hostnames,
|
||||||
where `<hostname>` is the name of any host in the network, and `<domain`> is the
|
where `<hostname>` is any host's name in the `bootstrap/nebula/hosts` directory.
|
||||||
network's domain name.
|
The returned IP will be the corresponding IP for the host, as listed in the
|
||||||
|
host's `bootstrap/nebula/hosts` file.
|
||||||
|
|
||||||
If a request for a hostname not within the network's domain is received then the
|
If a request for a non `.cryptic.io` hostname is received then the server will
|
||||||
server will forward the request to a pre-configured public resolver. The set of
|
forward the request to a pre-configured public resolver. The set of public
|
||||||
public resolvers used can be configured in the `/etc/isle/daemon.yml` file.
|
resolvers used can be configured using the
|
||||||
|
[daemon.yml](creating-a-daemonyml-file.md) file.
|
||||||
|
|
||||||
This DNS server is an optional feature of Isle, and not required in general for
|
This DNS server is an optional feature of cryptic-net, and not required in
|
||||||
making use of the network.
|
general for making use of the network.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
As an example of how to make use of this DNS server, let's say my host's IP on
|
As an example of how to make use of this DNS server, let's say my host's IP on
|
||||||
the network is `10.10.1.1`, and my network's domain is `cool.internal`.
|
the network is `10.10.1.1`. In order to configure the host to use the
|
||||||
In order to configure the host to use the isle DNS server for all DNS
|
cryptic-net DNS server for all DNS requests, I could do something like this:
|
||||||
requests, I could do something like this:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo su
|
sudo su
|
||||||
echo "nameserver 10.10.1.1" > /etc/resolv.conf
|
echo "nameserver 10.10.1.1" > /etc/resolv.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
From that point, all DNS requests on my host would hit the isle DNS
|
From that point, all DNS requests on my host would hit the cryptic-net DNS
|
||||||
server. If I request `my-host.hosts.cool.internal`, it would respond with the
|
server. If I request `my-host.cryptic.io`, it would respond with the appropriate
|
||||||
appropriate private IP.
|
private IP.
|
||||||
|
|
||||||
NOTE that configuration of dns resolvers is very OS-specific, even amongst Linux
|
NOTE that configuration of dns resolvers is very OS-specific, even amongst Linux
|
||||||
distributions, so ensure you know how your resolver configuration works before
|
distributions, so ensure you know how your resolver configuration works before
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"root": {}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
38
flake.nix
38
flake.nix
@ -1,38 +0,0 @@
|
|||||||
# TODO this is currently broken, garage is using builtins.currentSystem for some
|
|
||||||
# reason.
|
|
||||||
{
|
|
||||||
description = "isle provides the foundation for an autonomous community cloud infrastructure";
|
|
||||||
|
|
||||||
outputs = {
|
|
||||||
self,
|
|
||||||
}: let
|
|
||||||
|
|
||||||
supportedSystems = (import ./nix/pkgs.nix).supportedSystems;
|
|
||||||
|
|
||||||
mkPkg = (buildSystem: hostSystem: let
|
|
||||||
|
|
||||||
defaultAttrs = (import ./default.nix) {
|
|
||||||
inherit hostSystem buildSystem;
|
|
||||||
revision = if self ? rev then self.rev else "dirty";
|
|
||||||
releaseName = "flake";
|
|
||||||
};
|
|
||||||
|
|
||||||
in
|
|
||||||
defaultAttrs.build.appImage
|
|
||||||
);
|
|
||||||
|
|
||||||
pkgsForBuildSystem = (buildSystem: {
|
|
||||||
default = mkPkg buildSystem buildSystem;
|
|
||||||
});
|
|
||||||
|
|
||||||
in {
|
|
||||||
|
|
||||||
packages = (builtins.foldl'
|
|
||||||
(pkgs: buildSystem:
|
|
||||||
pkgs // { "${buildSystem}" = pkgsForBuildSystem buildSystem; })
|
|
||||||
{}
|
|
||||||
supportedSystems
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
34
garage/default.nix
Normal file
34
garage/default.nix
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
|
||||||
|
fetchgit,
|
||||||
|
buildEnv,
|
||||||
|
minio-client,
|
||||||
|
|
||||||
|
}: let
|
||||||
|
|
||||||
|
version = "0.8.0-rc1";
|
||||||
|
|
||||||
|
src = fetchgit {
|
||||||
|
name = "garage-v${version}";
|
||||||
|
url = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git";
|
||||||
|
rev = "2197753dfdb25944e55c25d911ae6d14b8506c4d";
|
||||||
|
sha256 = "sha256-Rzwx1/vl3xg5bj4Chxj8VLBZ25zlPawOc+uMl3AHhkw=";
|
||||||
|
};
|
||||||
|
|
||||||
|
in rec {
|
||||||
|
|
||||||
|
garage = (import "${src}/default.nix") { git_version = version; };
|
||||||
|
|
||||||
|
minioClient = minio-client;
|
||||||
|
|
||||||
|
env = buildEnv {
|
||||||
|
name = "cryptic-net-garage";
|
||||||
|
paths = [
|
||||||
|
garage
|
||||||
|
minioClient
|
||||||
|
./src
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
24
garage/src/bin/garage-apply-layout-diff
Normal file
24
garage/src/bin/garage-apply-layout-diff
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
tmp="$(mktemp -d -t cryptic-net-garage-apply-layout-diff-XXX)"
|
||||||
|
|
||||||
|
( trap "rm -rf '$tmp'" EXIT
|
||||||
|
|
||||||
|
tar xzf "$_BOOTSTRAP_PATH" -C "$tmp" ./hosts
|
||||||
|
|
||||||
|
thisHostName=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./hostname)
|
||||||
|
thisHostIP=$(cat "$tmp"/hosts/"$thisHostName".yml | yq '.nebula.ip')
|
||||||
|
|
||||||
|
firstRPCPort=$(cat "$_DAEMON_YML_PATH" | yq '.storage.allocations[0].rpc_port')
|
||||||
|
|
||||||
|
firstPeerID=$(cryptic-net-main garage-peer-keygen -danger -ip "$thisHostIP" -port "$firstRPCPort")
|
||||||
|
|
||||||
|
export GARAGE_RPC_HOST="$firstPeerID"@"$thisHostIP":"$firstRPCPort"
|
||||||
|
export GARAGE_RPC_SECRET=$(tar -xzf "$_BOOTSTRAP_PATH" --to-stdout "./garage/rpc-secret.txt")
|
||||||
|
|
||||||
|
garage layout show | cryptic-net-main garage-layout-diff | while read diffLine; do
|
||||||
|
echo "> $diffLine"
|
||||||
|
$diffLine
|
||||||
|
done
|
||||||
|
)
|
9
go-workspace/README.md
Normal file
9
go-workspace/README.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# go-workspace
|
||||||
|
|
||||||
|
This module is used for building all custom go binaries within the cryptic-net
|
||||||
|
project.
|
||||||
|
|
||||||
|
The reason binaries are contained here, and not under the sub-directory for the
|
||||||
|
sub-process the correspond to like most other code in this project, is that nix
|
||||||
|
makes it difficult to compose multiple modules defined locally. If nix ever
|
||||||
|
fixes this we should split this out.
|
18
go-workspace/default.nix
Normal file
18
go-workspace/default.nix
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
buildGoModule,
|
||||||
|
}: let
|
||||||
|
|
||||||
|
build = subPackage: buildGoModule {
|
||||||
|
|
||||||
|
pname = "cryptic-net-" + (builtins.baseNameOf subPackage);
|
||||||
|
version = "unstable";
|
||||||
|
src = ./src;
|
||||||
|
vendorSha256 = "sha256-UqMxbu/v/zDR4yJgUYiLs7HuHkvsZc/MJiWgw/8g+xk=";
|
||||||
|
subPackages = [
|
||||||
|
subPackage
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
in {
|
||||||
|
crypticNetMain = build "cmd/cryptic-net-main";
|
||||||
|
}
|
135
go-workspace/src/admin/admin.go
Normal file
135
go-workspace/src/admin/admin.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// Package admin deals with the parsing and creation of admin.tgz files.
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cryptic-net/garage"
|
||||||
|
"cryptic-net/nebula"
|
||||||
|
"cryptic-net/tarutil"
|
||||||
|
"cryptic-net/yamlutil"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
creationParamsPath = "admin/creation-params.yml"
|
||||||
|
|
||||||
|
nebulaCertsCACertPath = "nebula/certs/ca.crt"
|
||||||
|
nebulaCertsCAKeyPath = "nebula/certs/ca.key"
|
||||||
|
|
||||||
|
garageGlobalBucketKeyYmlPath = "garage/cryptic-net-global-bucket-key.yml"
|
||||||
|
garageAdminBucketKeyYmlPath = "garage/cryptic-net-admin-bucket-key.yml"
|
||||||
|
garageRPCSecretPath = "garage/rpc-secret.txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreationParams are general parameters used when creating a new network. These
|
||||||
|
// are available to all hosts within the network via their bootstrap files.
|
||||||
|
type CreationParams struct {
|
||||||
|
Domain string `yaml:"domain"`
|
||||||
|
CIDRs []string `yaml:"cidrs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin is used for accessing all information contained within an admin.tgz.
|
||||||
|
type Admin struct {
|
||||||
|
CreationParams CreationParams
|
||||||
|
|
||||||
|
NebulaCACert nebula.CACert
|
||||||
|
|
||||||
|
GarageRPCSecret string
|
||||||
|
GarageGlobalBucketS3APICredentials garage.S3APICredentials
|
||||||
|
GarageAdminBucketS3APICredentials garage.S3APICredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromFS loads an Admin instance from the given fs.FS, which presumably
|
||||||
|
// represents the file structure of an admin.tgz file.
|
||||||
|
func FromFS(adminFS fs.FS) (Admin, error) {
|
||||||
|
|
||||||
|
var a Admin
|
||||||
|
|
||||||
|
filesToLoadAsYAML := []struct {
|
||||||
|
into interface{}
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{&a.CreationParams, creationParamsPath},
|
||||||
|
{&a.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath},
|
||||||
|
{&a.GarageAdminBucketS3APICredentials, garageAdminBucketKeyYmlPath},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range filesToLoadAsYAML {
|
||||||
|
if err := yamlutil.LoadYamlFSFile(f.into, adminFS, f.path); err != nil {
|
||||||
|
return Admin{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filesToLoadAsString := []struct {
|
||||||
|
into *string
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{&a.NebulaCACert.CACert, nebulaCertsCACertPath},
|
||||||
|
{&a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
|
||||||
|
{&a.GarageRPCSecret, garageRPCSecretPath},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range filesToLoadAsString {
|
||||||
|
body, err := fs.ReadFile(adminFS, f.path)
|
||||||
|
if err != nil {
|
||||||
|
return Admin{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
|
||||||
|
}
|
||||||
|
*f.into = string(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromReader reads an admin.tgz file from the given io.Reader.
|
||||||
|
func FromReader(r io.Reader) (Admin, error) {
|
||||||
|
|
||||||
|
fs, err := tarutil.FSFromReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return Admin{}, fmt.Errorf("reading admin.tgz: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return FromFS(fs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTo writes the Admin as a new admin.tgz to the given io.Writer.
|
||||||
|
func (a Admin) WriteTo(into io.Writer) error {
|
||||||
|
|
||||||
|
w := tarutil.NewTGZWriter(into)
|
||||||
|
|
||||||
|
filesToWriteAsYAML := []struct {
|
||||||
|
value interface{}
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{a.CreationParams, creationParamsPath},
|
||||||
|
{a.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath},
|
||||||
|
{a.GarageAdminBucketS3APICredentials, garageAdminBucketKeyYmlPath},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range filesToWriteAsYAML {
|
||||||
|
|
||||||
|
b, err := yaml.Marshal(f.value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("yaml encoding data for %q: %w", f.path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteFileBytes(f.path, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
filesToWriteAsString := []struct {
|
||||||
|
value string
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{a.NebulaCACert.CACert, nebulaCertsCACertPath},
|
||||||
|
{a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
|
||||||
|
{a.GarageRPCSecret, garageRPCSecretPath},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range filesToWriteAsString {
|
||||||
|
w.WriteFileBytes(f.path, []byte(f.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.Close()
|
||||||
|
}
|
212
go-workspace/src/bootstrap/bootstrap.go
Normal file
212
go-workspace/src/bootstrap/bootstrap.go
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
// Package bootstrap deals with the parsing and creation of bootstrap.tgz files.
|
||||||
|
// It also contains some helpers which rely on bootstrap data.
|
||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cryptic-net/admin"
|
||||||
|
"cryptic-net/garage"
|
||||||
|
"cryptic-net/nebula"
|
||||||
|
"cryptic-net/tarutil"
|
||||||
|
"cryptic-net/yamlutil"
|
||||||
|
"crypto/sha512"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Paths within the bootstrap FS which for general data.
|
||||||
|
const (
|
||||||
|
adminCreationParamsPath = "admin/creation-params.yml"
|
||||||
|
hostNamePath = "hostname"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bootstrap is used for accessing all information contained within a
|
||||||
|
// bootstrap.tgz file.
|
||||||
|
type Bootstrap struct {
|
||||||
|
AdminCreationParams admin.CreationParams
|
||||||
|
|
||||||
|
Hosts map[string]Host
|
||||||
|
HostName string
|
||||||
|
|
||||||
|
NebulaHostCert nebula.HostCert
|
||||||
|
|
||||||
|
GarageRPCSecret string
|
||||||
|
GarageGlobalBucketS3APICredentials garage.S3APICredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromFS loads a Boostrap instance from the given fs.FS, which presumably
|
||||||
|
// represents the file structure of a bootstrap.tgz file.
|
||||||
|
func FromFS(bootstrapFS fs.FS) (Bootstrap, error) {
|
||||||
|
|
||||||
|
var (
|
||||||
|
b Bootstrap
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if b.Hosts, err = loadHosts(bootstrapFS); err != nil {
|
||||||
|
return Bootstrap{}, fmt.Errorf("loading hosts info from fs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filesToLoadAsYAML := []struct {
|
||||||
|
into interface{}
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{&b.AdminCreationParams, adminCreationParamsPath},
|
||||||
|
{&b.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range filesToLoadAsYAML {
|
||||||
|
if err := yamlutil.LoadYamlFSFile(f.into, bootstrapFS, f.path); err != nil {
|
||||||
|
return Bootstrap{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filesToLoadAsString := []struct {
|
||||||
|
into *string
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{&b.HostName, hostNamePath},
|
||||||
|
{&b.NebulaHostCert.CACert, nebulaCertsCACertPath},
|
||||||
|
{&b.NebulaHostCert.HostCert, nebulaCertsHostCertPath},
|
||||||
|
{&b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath},
|
||||||
|
{&b.GarageRPCSecret, garageRPCSecretPath},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range filesToLoadAsString {
|
||||||
|
body, err := fs.ReadFile(bootstrapFS, f.path)
|
||||||
|
if err != nil {
|
||||||
|
return Bootstrap{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
|
||||||
|
}
|
||||||
|
*f.into = string(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromReader reads a bootstrap.tgz file from the given io.Reader.
|
||||||
|
func FromReader(r io.Reader) (Bootstrap, error) {
|
||||||
|
|
||||||
|
fs, err := tarutil.FSFromReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return Bootstrap{}, fmt.Errorf("reading bootstrap.tgz: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return FromFS(fs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromFile reads a bootstrap.tgz from a file at the given path.
|
||||||
|
func FromFile(path string) (Bootstrap, error) {
|
||||||
|
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return Bootstrap{}, fmt.Errorf("opening file: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
return FromReader(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTo writes the Bootstrap as a new bootstrap.tgz to the given io.Writer.
|
||||||
|
func (b Bootstrap) WriteTo(into io.Writer) error {
|
||||||
|
|
||||||
|
w := tarutil.NewTGZWriter(into)
|
||||||
|
|
||||||
|
for _, host := range b.Hosts {
|
||||||
|
|
||||||
|
hostB, err := yaml.Marshal(host)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("yaml encoding host %#v: %w", host, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(hostsDirPath, host.Name+".yml")
|
||||||
|
|
||||||
|
w.WriteFileBytes(path, hostB)
|
||||||
|
}
|
||||||
|
|
||||||
|
filesToWriteAsYAML := []struct {
|
||||||
|
value interface{}
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{b.AdminCreationParams, adminCreationParamsPath},
|
||||||
|
{b.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range filesToWriteAsYAML {
|
||||||
|
|
||||||
|
b, err := yaml.Marshal(f.value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("yaml encoding data for %q: %w", f.path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteFileBytes(f.path, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
filesToWriteAsString := []struct {
|
||||||
|
value string
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{b.HostName, hostNamePath},
|
||||||
|
{b.NebulaHostCert.CACert, nebulaCertsCACertPath},
|
||||||
|
{b.NebulaHostCert.HostCert, nebulaCertsHostCertPath},
|
||||||
|
{b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath},
|
||||||
|
{b.GarageRPCSecret, garageRPCSecretPath},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range filesToWriteAsString {
|
||||||
|
w.WriteFileBytes(f.path, []byte(f.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThisHost is a shortcut for b.Hosts[b.HostName], but will panic if the
|
||||||
|
// HostName isn't found in the Hosts map.
|
||||||
|
func (b Bootstrap) ThisHost() Host {
|
||||||
|
|
||||||
|
host, ok := b.Hosts[b.HostName]
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("hostname %q not defined in bootstrap's hosts", b.HostName))
|
||||||
|
}
|
||||||
|
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash returns a deterministic hash of the given hosts map.
|
||||||
|
func HostsHash(hostsMap map[string]Host) ([]byte, error) {
|
||||||
|
|
||||||
|
hosts := make([]Host, 0, len(hostsMap))
|
||||||
|
for _, host := range hostsMap {
|
||||||
|
hosts = append(hosts, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(hosts, func(i, j int) bool { return hosts[i].Name < hosts[j].Name })
|
||||||
|
|
||||||
|
h := sha512.New()
|
||||||
|
|
||||||
|
if err := yaml.NewEncoder(h).Encode(hosts); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Sum(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHosts returns a copy of the Bootstrap with the given set of Hosts applied
|
||||||
|
// to it. It will _not_ overwrite the Host for _this_ host, however.
|
||||||
|
func (b Bootstrap) WithHosts(hosts map[string]Host) Bootstrap {
|
||||||
|
|
||||||
|
hostsCopy := make(map[string]Host, len(hosts))
|
||||||
|
|
||||||
|
for name, host := range hosts {
|
||||||
|
hostsCopy[name] = host
|
||||||
|
}
|
||||||
|
|
||||||
|
hostsCopy[b.HostName] = b.ThisHost()
|
||||||
|
|
||||||
|
b.Hosts = hostsCopy
|
||||||
|
return b
|
||||||
|
}
|
86
go-workspace/src/bootstrap/garage.go
Normal file
86
go-workspace/src/bootstrap/garage.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cryptic-net/garage"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Paths within the bootstrap FS related to garage.
|
||||||
|
const (
|
||||||
|
garageGlobalBucketKeyYmlPath = "garage/cryptic-net-global-bucket-key.yml"
|
||||||
|
garageRPCSecretPath = "garage/rpc-secret.txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GaragePeers returns a Peer for each known garage instance in the network.
|
||||||
|
func (b Bootstrap) GaragePeers() []garage.Peer {
|
||||||
|
|
||||||
|
var peers []garage.Peer
|
||||||
|
|
||||||
|
for _, host := range b.Hosts {
|
||||||
|
|
||||||
|
if host.Garage == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, instance := range host.Garage.Instances {
|
||||||
|
|
||||||
|
peer := garage.Peer{
|
||||||
|
IP: host.Nebula.IP,
|
||||||
|
RPCPort: instance.RPCPort,
|
||||||
|
S3APIPort: instance.S3APIPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
peers = append(peers, peer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return peers
|
||||||
|
}
|
||||||
|
|
||||||
|
// GarageRPCPeerAddrs returns the full RPC peer address for each known garage
|
||||||
|
// instance in the network.
|
||||||
|
func (b Bootstrap) GarageRPCPeerAddrs() []string {
|
||||||
|
var addrs []string
|
||||||
|
for _, peer := range b.GaragePeers() {
|
||||||
|
addrs = append(addrs, peer.RPCPeerAddr())
|
||||||
|
}
|
||||||
|
return addrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChooseGaragePeer returns a Peer for a garage instance from the network. It
|
||||||
|
// will prefer a garage instance on this particular host, if there is one, but
|
||||||
|
// will otherwise return a random endpoint.
|
||||||
|
func (b Bootstrap) ChooseGaragePeer() garage.Peer {
|
||||||
|
|
||||||
|
thisHost := b.ThisHost()
|
||||||
|
|
||||||
|
if thisHost.Garage != nil && len(thisHost.Garage.Instances) > 0 {
|
||||||
|
inst := thisHost.Garage.Instances[0]
|
||||||
|
return garage.Peer{
|
||||||
|
IP: thisHost.Nebula.IP,
|
||||||
|
RPCPort: inst.RPCPort,
|
||||||
|
S3APIPort: inst.S3APIPort,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range b.GaragePeers() {
|
||||||
|
return peer
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("no garage instances configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalBucketS3APIClient returns an S3 client pre-configured with access to
|
||||||
|
// the global bucket.
|
||||||
|
func (b Bootstrap) GlobalBucketS3APIClient() (garage.S3APIClient, error) {
|
||||||
|
|
||||||
|
addr := b.ChooseGaragePeer().S3APIAddr()
|
||||||
|
creds := b.GarageGlobalBucketS3APICredentials
|
||||||
|
|
||||||
|
client, err := garage.NewS3APIClient(addr, creds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connecting to garage S3 API At %q: %w", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, err
|
||||||
|
}
|
111
go-workspace/src/bootstrap/garage_global_bucket.go
Normal file
111
go-workspace/src/bootstrap/garage_global_bucket.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"cryptic-net/garage"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Paths within garage's global bucket
|
||||||
|
const (
|
||||||
|
garageGlobalBucketBootstrapHostsDirPath = "bootstrap/hosts"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PutGarageBoostrapHost places the <hostname>.yml file for the given host into
|
||||||
|
// garage so that other hosts are able to see relevant configuration for it.
|
||||||
|
//
|
||||||
|
// The given client should be for the global bucket.
|
||||||
|
func PutGarageBoostrapHost(
|
||||||
|
ctx context.Context, client garage.S3APIClient, host Host,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
if err := yaml.NewEncoder(buf).Encode(host); err != nil {
|
||||||
|
log.Fatalf("yaml encoding host data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(garageGlobalBucketBootstrapHostsDirPath, host.Name+".yml")
|
||||||
|
|
||||||
|
_, err := client.PutObject(
|
||||||
|
ctx, garage.GlobalBucket, filePath, buf, int64(buf.Len()),
|
||||||
|
minio.PutObjectOptions{},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing to %q in global bucket: %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveGarageBootstrapHost removes the <hostname>.yml for the given host from
|
||||||
|
// garage.
|
||||||
|
//
|
||||||
|
// The given client should be for the global bucket.
|
||||||
|
func RemoveGarageBootstrapHost(
|
||||||
|
ctx context.Context, client garage.S3APIClient, hostName string,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
filePath := filepath.Join(garageGlobalBucketBootstrapHostsDirPath, hostName+".yml")
|
||||||
|
|
||||||
|
return client.RemoveObject(
|
||||||
|
ctx, garage.GlobalBucket, filePath,
|
||||||
|
minio.RemoveObjectOptions{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGarageBootstrapHosts loads the <hostname>.yml file for all hosts stored in
|
||||||
|
// garage.
|
||||||
|
//
|
||||||
|
// The given client should be for the global bucket.
|
||||||
|
func GetGarageBootstrapHosts(
|
||||||
|
ctx context.Context, client garage.S3APIClient,
|
||||||
|
) (
|
||||||
|
map[string]Host, error,
|
||||||
|
) {
|
||||||
|
|
||||||
|
hosts := map[string]Host{}
|
||||||
|
|
||||||
|
objInfoCh := client.ListObjects(
|
||||||
|
ctx, garage.GlobalBucket,
|
||||||
|
minio.ListObjectsOptions{
|
||||||
|
Prefix: garageGlobalBucketBootstrapHostsDirPath,
|
||||||
|
Recursive: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
for objInfo := range objInfoCh {
|
||||||
|
|
||||||
|
if objInfo.Err != nil {
|
||||||
|
return nil, fmt.Errorf("listing objects: %w", objInfo.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, err := client.GetObject(
|
||||||
|
ctx, garage.GlobalBucket, objInfo.Key, minio.GetObjectOptions{},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("retrieving object %q: %w", objInfo.Key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var host Host
|
||||||
|
|
||||||
|
err = yaml.NewDecoder(obj).Decode(&host)
|
||||||
|
obj.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("yaml decoding object %q: %w", objInfo.Key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts[host.Name] = host
|
||||||
|
}
|
||||||
|
|
||||||
|
return hosts, nil
|
||||||
|
}
|
82
go-workspace/src/bootstrap/hosts.go
Normal file
82
go-workspace/src/bootstrap/hosts.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hostsDirPath = "hosts"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NebulaHost describes the nebula configuration of a Host which is relevant for
|
||||||
|
// other hosts to know.
|
||||||
|
type NebulaHost struct {
|
||||||
|
IP string `yaml:"ip"`
|
||||||
|
PublicAddr string `yaml:"public_addr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GarageHost describes a single garage instance in the GarageHost.
|
||||||
|
type GarageHostInstance struct {
|
||||||
|
RPCPort int `yaml:"rpc_port"`
|
||||||
|
S3APIPort int `yaml:"s3_api_port"`
|
||||||
|
WebPort int `yaml:"web_port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GarageHost describes the garage configuration of a Host which is relevant for
|
||||||
|
// other hosts to know.
|
||||||
|
type GarageHost struct {
|
||||||
|
Instances []GarageHostInstance `yaml:"instances"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host consolidates all information about a single host from the bootstrap
|
||||||
|
// file.
|
||||||
|
type Host struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Nebula NebulaHost `yaml:"nebula"`
|
||||||
|
Garage *GarageHost `yaml:"garage,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadHosts(bootstrapFS fs.FS) (map[string]Host, error) {
|
||||||
|
|
||||||
|
hosts := map[string]Host{}
|
||||||
|
|
||||||
|
readAsYaml := func(into interface{}, path string) error {
|
||||||
|
b, err := fs.ReadFile(bootstrapFS, path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading file from fs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return yaml.Unmarshal(b, into)
|
||||||
|
}
|
||||||
|
|
||||||
|
globPath := filepath.Join(hostsDirPath, "*.yml")
|
||||||
|
|
||||||
|
hostPaths, err := fs.Glob(bootstrapFS, globPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("listing host files at %q in fs: %w", globPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, hostPath := range hostPaths {
|
||||||
|
|
||||||
|
hostName := filepath.Base(hostPath)
|
||||||
|
hostName = strings.TrimSuffix(hostName, filepath.Ext(hostName))
|
||||||
|
|
||||||
|
var host Host
|
||||||
|
if err := readAsYaml(&host, hostPath); err != nil {
|
||||||
|
return nil, fmt.Errorf("reading %q as yaml: %w", hostPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts[hostName] = host
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hosts) == 0 {
|
||||||
|
return nil, fmt.Errorf("failed to load any hosts from fs")
|
||||||
|
}
|
||||||
|
|
||||||
|
return hosts, nil
|
||||||
|
}
|
8
go-workspace/src/bootstrap/nebula.go
Normal file
8
go-workspace/src/bootstrap/nebula.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package bootstrap
|
||||||
|
|
||||||
|
// Paths within the bootstrap FS related to nebula.
|
||||||
|
const (
|
||||||
|
nebulaCertsCACertPath = "nebula/certs/ca.crt"
|
||||||
|
nebulaCertsHostCertPath = "nebula/certs/host.crt"
|
||||||
|
nebulaCertsHostKeyPath = "nebula/certs/host.key"
|
||||||
|
)
|
81
go-workspace/src/cmd/cryptic-net-main/main.go
Normal file
81
go-workspace/src/cmd/cryptic-net-main/main.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
//
|
||||||
|
// This binary acts as a wrapper around other programs which would otherwise
|
||||||
|
// form their own binaries. We do this for two reasons:
|
||||||
|
//
|
||||||
|
// * Nix makes it difficult to determine which individuals binaries need to be
|
||||||
|
// rebuilt upon changes, so it rebuilds all of them no matter what changed. This
|
||||||
|
// makes development slow. By wrapping everything in a sinble binary we only
|
||||||
|
// ever have to build that binary.
|
||||||
|
//
|
||||||
|
// * If we have N binaries total, then we have N copies of the go runtime in our
|
||||||
|
// final AppImage. By bundling the binaries into a single one we can reduce the
|
||||||
|
// number go runtime copies to 1.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cryptic-net/cmd/entrypoint"
|
||||||
|
garage_entrypoint "cryptic-net/cmd/garage-entrypoint"
|
||||||
|
garage_layout_diff "cryptic-net/cmd/garage-layout-diff"
|
||||||
|
garage_peer_keygen "cryptic-net/cmd/garage-peer-keygen"
|
||||||
|
nebula_entrypoint "cryptic-net/cmd/nebula-entrypoint"
|
||||||
|
update_global_bucket "cryptic-net/cmd/update-global-bucket"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mainFn struct {
|
||||||
|
name string
|
||||||
|
fn func()
|
||||||
|
}
|
||||||
|
|
||||||
|
var mainFns = []mainFn{
|
||||||
|
{"entrypoint", entrypoint.Main},
|
||||||
|
{"garage-entrypoint", garage_entrypoint.Main},
|
||||||
|
{"garage-layout-diff", garage_layout_diff.Main},
|
||||||
|
{"garage-peer-keygen", garage_peer_keygen.Main},
|
||||||
|
{"nebula-entrypoint", nebula_entrypoint.Main},
|
||||||
|
{"update-global-bucket", update_global_bucket.Main},
|
||||||
|
}
|
||||||
|
|
||||||
|
var mainFnsMap = func() map[string]mainFn {
|
||||||
|
|
||||||
|
m := map[string]mainFn{}
|
||||||
|
|
||||||
|
for _, mainFn := range mainFns {
|
||||||
|
m[mainFn.name] = mainFn
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}()
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Fprintf(os.Stderr, "USAGE: %s <cmd>\n\n", os.Args[0])
|
||||||
|
fmt.Fprintf(os.Stderr, "Commands:\n\n")
|
||||||
|
|
||||||
|
for _, mainFn := range mainFns {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", mainFn.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Stderr.Sync()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
mainFn, ok := mainFnsMap[os.Args[1]]
|
||||||
|
if !ok {
|
||||||
|
usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove os.Args[1] from the arg list, so that other commands which consume
|
||||||
|
// args don't get confused
|
||||||
|
os.Args = append(os.Args[:1], os.Args[2:]...)
|
||||||
|
|
||||||
|
mainFn.fn()
|
||||||
|
}
|
208
go-workspace/src/cmd/entrypoint/admin.go
Normal file
208
go-workspace/src/cmd/entrypoint/admin.go
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cryptic-net/admin"
|
||||||
|
"cryptic-net/bootstrap"
|
||||||
|
"cryptic-net/nebula"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func readAdmin(path string) (admin.Admin, error) {
|
||||||
|
|
||||||
|
if path == "-" {
|
||||||
|
|
||||||
|
adm, err := admin.FromReader(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
return admin.Admin{}, fmt.Errorf("parsing admin.tgz from stdin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return adm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return admin.Admin{}, fmt.Errorf("opening file: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
return admin.FromReader(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
var subCmdAdminCreateNetwork = subCmd{
|
||||||
|
name: "create-network",
|
||||||
|
descr: "Creates a new cryptic-net network, outputting the resulting admin.tgz to stdout",
|
||||||
|
do: func(subCmdCtx subCmdCtx) error {
|
||||||
|
|
||||||
|
flags := subCmdCtx.flagSet(false)
|
||||||
|
|
||||||
|
daemonYmlPath := flags.StringP(
|
||||||
|
"config-path", "c", "",
|
||||||
|
"Optional path to a daemon.yml file to load configuration from.",
|
||||||
|
)
|
||||||
|
|
||||||
|
dumpConfig := flags.Bool(
|
||||||
|
"dump-config", false,
|
||||||
|
"Write the default configuration file to stdout and exit.",
|
||||||
|
)
|
||||||
|
|
||||||
|
domain := flags.StringP(
|
||||||
|
"domain", "d", "",
|
||||||
|
"Domain name that should be used as the root domain in the network.",
|
||||||
|
)
|
||||||
|
|
||||||
|
ipCIDRStrs := flags.StringP(
|
||||||
|
"ip-cidrs", "i", "",
|
||||||
|
"Comma-separated list of CIDRs which denote what IPs hosts on the network can be assigned.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||||
|
return fmt.Errorf("parsing flags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
env := subCmdCtx.env
|
||||||
|
|
||||||
|
if *dumpConfig {
|
||||||
|
return writeBuiltinDaemonYml(env, os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *domain == "" {
|
||||||
|
return errors.New("--domain is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
*domain = strings.TrimRight(strings.TrimLeft(*domain, "."), ".")
|
||||||
|
|
||||||
|
var (
|
||||||
|
ipCIDRs []*net.IPNet
|
||||||
|
thisIP net.IP
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, ipCIDRStr := range strings.Split(*ipCIDRStrs, ",") {
|
||||||
|
|
||||||
|
ipCIDRStr = strings.TrimSpace(ipCIDRStr)
|
||||||
|
if ipCIDRStr == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, ipCIDR, err := net.ParseCIDR(ipCIDRStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse CIDR %q: %w", ipCIDRStr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
thisIP = ip // we just need one IP from a CIDR, it doesn't matter which.
|
||||||
|
ipCIDRs = append(ipCIDRs, ipCIDR)
|
||||||
|
}
|
||||||
|
|
||||||
|
runtimeDirPath := env.RuntimeDirPath
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "will use runtime directory %q for temporary state\n", runtimeDirPath)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(runtimeDirPath, 0700); err != nil {
|
||||||
|
return fmt.Errorf("creating directory %q: %w", runtimeDirPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
fmt.Fprintf(os.Stderr, "cleaning up runtime directory %q\n", runtimeDirPath)
|
||||||
|
if err := os.RemoveAll(runtimeDirPath); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error removing temporary directory %q: %v", runtimeDirPath, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := writeMergedDaemonYml(env, *daemonYmlPath); err != nil {
|
||||||
|
return fmt.Errorf("merging and writing daemon.yml file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
daemon := env.ThisDaemon()
|
||||||
|
|
||||||
|
if len(daemon.Storage.Allocations) < 3 {
|
||||||
|
return fmt.Errorf("daemon.yml with at least 3 allocations was not provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var subCmdAdminMakeBootstrap = subCmd{
|
||||||
|
name: "make-bootstrap",
|
||||||
|
descr: "Creates a new bootstrap.tgz file for a particular host and writes it to stdout",
|
||||||
|
checkLock: true,
|
||||||
|
do: func(subCmdCtx subCmdCtx) error {
|
||||||
|
|
||||||
|
flags := subCmdCtx.flagSet(false)
|
||||||
|
|
||||||
|
name := flags.StringP(
|
||||||
|
"name", "n", "",
|
||||||
|
"Name of the host to generate bootstrap.tgz for",
|
||||||
|
)
|
||||||
|
|
||||||
|
adminPath := flags.StringP(
|
||||||
|
"admin-path", "a", "",
|
||||||
|
`Path to admin.tgz file. If the given path is "-" then stdin is used.`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||||
|
return fmt.Errorf("parsing flags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *name == "" || *adminPath == "" {
|
||||||
|
return errors.New("--name and --admin-path are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
env := subCmdCtx.env
|
||||||
|
|
||||||
|
adm, err := readAdmin(*adminPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := env.Bootstrap.GlobalBucketS3APIClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating client for global bucket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE this isn't _technically_ required, but if the `hosts add`
|
||||||
|
// command for this host has been run recently then it might not have
|
||||||
|
// made it into the bootstrap file yet, and so won't be in
|
||||||
|
// `env.Bootstrap`.
|
||||||
|
hosts, err := bootstrap.GetGarageBootstrapHosts(env.Context, client)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("retrieving host info from garage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
host, ok := hosts[*name]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("couldn't find host into for %q in garage, has `cryptic-net hosts add` been run yet?", *name)
|
||||||
|
}
|
||||||
|
|
||||||
|
nebulaHostCert, err := nebula.NewHostCert(adm.NebulaCACert, host.Name, host.Nebula.IP)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating new nebula host key/cert: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newBootstrap := bootstrap.Bootstrap{
|
||||||
|
AdminCreationParams: adm.CreationParams,
|
||||||
|
|
||||||
|
Hosts: hosts,
|
||||||
|
HostName: *name,
|
||||||
|
|
||||||
|
NebulaHostCert: nebulaHostCert,
|
||||||
|
|
||||||
|
GarageRPCSecret: adm.GarageRPCSecret,
|
||||||
|
GarageGlobalBucketS3APICredentials: adm.GarageGlobalBucketS3APICredentials,
|
||||||
|
}
|
||||||
|
|
||||||
|
return newBootstrap.WriteTo(os.Stdout)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var subCmdAdmin = subCmd{
|
||||||
|
name: "admin",
|
||||||
|
descr: "Sub-commands which only admins can run",
|
||||||
|
do: func(subCmdCtx subCmdCtx) error {
|
||||||
|
return subCmdCtx.doSubCmd(
|
||||||
|
subCmdAdminMakeBootstrap,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
371
go-workspace/src/cmd/entrypoint/daemon.go
Normal file
371
go-workspace/src/cmd/entrypoint/daemon.go
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
crypticnet "cryptic-net"
|
||||||
|
"cryptic-net/bootstrap"
|
||||||
|
"cryptic-net/garage"
|
||||||
|
|
||||||
|
"github.com/cryptic-io/pmux/pmuxlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The daemon sub-command deals with starting an actual cryptic-net daemon
|
||||||
|
// process, which is required to be running for most other cryptic-net
|
||||||
|
// functionality. The sub-command does the following:
|
||||||
|
//
|
||||||
|
// * Creates and locks the runtime directory.
|
||||||
|
//
|
||||||
|
// * Creates the data directory and copies the appdir bootstrap file into there,
|
||||||
|
// if it's not already there.
|
||||||
|
//
|
||||||
|
// * Merges the user-provided daemon.yml file with the default, and writes the
|
||||||
|
// result to the runtime dir.
|
||||||
|
//
|
||||||
|
// * Merges daemon.yml configuration into the bootstrap configuration, and
|
||||||
|
// rewrites the bootstrap file.
|
||||||
|
//
|
||||||
|
// * Sets up environment variables that all other sub-processes then use, based
|
||||||
|
// on the runtime dir.
|
||||||
|
//
|
||||||
|
// * Dynamically creates the root pmux config and runs pmux.
|
||||||
|
//
|
||||||
|
// * (On exit) cleans up the runtime directory.
|
||||||
|
|
||||||
|
func copyBootstrapToDataDir(env *crypticnet.Env, r io.Reader) error {
|
||||||
|
|
||||||
|
path := env.DataDirBootstrapPath()
|
||||||
|
dirPath := filepath.Dir(path)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(dirPath, 0700); err != nil {
|
||||||
|
return fmt.Errorf("creating directory %q: %w", dirPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating file %q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(f, r)
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("copying bootstrap file to %q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := env.LoadBootstrap(path); err != nil {
|
||||||
|
return fmt.Errorf("loading bootstrap from %q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates a new bootstrap file using available information from the network. If
|
||||||
|
// the new bootstrap file is different than the existing one, the existing one
|
||||||
|
// is overwritten, ReloadBootstrap is called on env, true is returned.
|
||||||
|
func reloadBootstrap(env *crypticnet.Env, s3Client garage.S3APIClient) (bool, error) {
|
||||||
|
|
||||||
|
newHosts, err := bootstrap.GetGarageBootstrapHosts(env.Context, s3Client)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("getting hosts from garage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newHostsHash, err := bootstrap.HostsHash(newHosts)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("calculating hash of new hosts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currHostsHash, err := bootstrap.HostsHash(env.Bootstrap.Hosts)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("calculating hash of current hosts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Equal(newHostsHash, currHostsHash) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if err := env.Bootstrap.WithHosts(newHosts).WriteTo(buf); err != nil {
|
||||||
|
return false, fmt.Errorf("writing new bootstrap file to buffer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := copyBootstrapToDataDir(env, buf); err != nil {
|
||||||
|
return false, fmt.Errorf("copying new bootstrap file to data dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runs a single pmux process ofor daemon, returning only once the env.Context
|
||||||
|
// has been canceled or bootstrap info has been changed. This will always block
|
||||||
|
// until the spawned pmux has returned.
|
||||||
|
func runDaemonPmuxOnce(env *crypticnet.Env, s3Client garage.S3APIClient) error {
|
||||||
|
|
||||||
|
thisHost := env.Bootstrap.ThisHost()
|
||||||
|
thisDaemon := env.ThisDaemon()
|
||||||
|
fmt.Fprintf(os.Stderr, "host name is %q, ip is %q\n", thisHost.Name, thisHost.Nebula.IP)
|
||||||
|
|
||||||
|
pmuxProcConfigs := []pmuxlib.ProcessConfig{
|
||||||
|
{
|
||||||
|
Name: "nebula",
|
||||||
|
Cmd: "cryptic-net-main",
|
||||||
|
Args: []string{
|
||||||
|
"nebula-entrypoint",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "dnsmasq",
|
||||||
|
Cmd: "bash",
|
||||||
|
Args: []string{
|
||||||
|
"wait-for-ip",
|
||||||
|
thisHost.Nebula.IP,
|
||||||
|
"dnsmasq-entrypoint",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var args []string
|
||||||
|
|
||||||
|
if allocs := thisDaemon.Storage.Allocations; len(allocs) > 0 {
|
||||||
|
for _, alloc := range allocs {
|
||||||
|
args = append(
|
||||||
|
args,
|
||||||
|
"wait-for",
|
||||||
|
net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
|
||||||
|
"--",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args = []string{
|
||||||
|
"wait-for-ip",
|
||||||
|
thisHost.Nebula.IP,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
|
||||||
|
Name: "update-global-bucket",
|
||||||
|
Cmd: "bash",
|
||||||
|
Args: append(args, "update-global-bucket"),
|
||||||
|
NoRestartOn: []int{0},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(thisDaemon.Storage.Allocations) > 0 {
|
||||||
|
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
|
||||||
|
Name: "garage",
|
||||||
|
Cmd: "bash",
|
||||||
|
Args: []string{
|
||||||
|
"wait-for-ip",
|
||||||
|
thisHost.Nebula.IP,
|
||||||
|
"cryptic-net-main", "garage-entrypoint",
|
||||||
|
},
|
||||||
|
|
||||||
|
// garage can take a while to clean up
|
||||||
|
SigKillWait: (1 * time.Minute) + (10 * time.Second),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pmuxConfig := pmuxlib.Config{Processes: pmuxProcConfigs}
|
||||||
|
|
||||||
|
doneCh := env.Context.Done()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(env.Context)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
pmuxlib.Run(ctx, pmuxConfig)
|
||||||
|
}()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(3 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
|
||||||
|
case <-doneCh:
|
||||||
|
return env.Context.Err()
|
||||||
|
|
||||||
|
case <-ticker.C:
|
||||||
|
|
||||||
|
fmt.Fprintln(os.Stderr, "checking for changes to bootstrap")
|
||||||
|
|
||||||
|
if changed, err := reloadBootstrap(env, s3Client); err != nil {
|
||||||
|
return fmt.Errorf("reloading bootstrap: %w", err)
|
||||||
|
|
||||||
|
} else if changed {
|
||||||
|
fmt.Fprintln(os.Stderr, "bootstrap info has changed, restarting all processes")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var subCmdDaemon = subCmd{
|
||||||
|
name: "daemon",
|
||||||
|
descr: "Runs the cryptic-net daemon (Default if no sub-command given)",
|
||||||
|
do: func(subCmdCtx subCmdCtx) error {
|
||||||
|
|
||||||
|
flags := subCmdCtx.flagSet(false)
|
||||||
|
|
||||||
|
daemonYmlPath := flags.StringP(
|
||||||
|
"config-path", "c", "",
|
||||||
|
"Optional path to a daemon.yml file to load configuration from.",
|
||||||
|
)
|
||||||
|
|
||||||
|
dumpConfig := flags.Bool(
|
||||||
|
"dump-config", false,
|
||||||
|
"Write the default configuration file to stdout and exit.",
|
||||||
|
)
|
||||||
|
|
||||||
|
bootstrapPath := flags.StringP(
|
||||||
|
"bootstrap-path", "b", "",
|
||||||
|
`Path to a bootstrap.tgz file. This only needs to be provided the first time the daemon is started, after that it is ignored. If the cryptic-net binary has a bootstrap built into it then this argument is always optional.`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||||
|
return fmt.Errorf("parsing flags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
env := subCmdCtx.env
|
||||||
|
|
||||||
|
if *dumpConfig {
|
||||||
|
return writeBuiltinDaemonYml(env, os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
runtimeDirPath := env.RuntimeDirPath
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "will use runtime directory %q for temporary state\n", runtimeDirPath)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(runtimeDirPath, 0700); err != nil {
|
||||||
|
return fmt.Errorf("creating directory %q: %w", runtimeDirPath, err)
|
||||||
|
|
||||||
|
} else if err := crypticnet.NewProcLock(runtimeDirPath).WriteLock(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not defer the cleaning of the runtime directory until the lock has
|
||||||
|
// been obtained, otherwise we might delete the directory out from under
|
||||||
|
// the feet of an already running daemon
|
||||||
|
defer func() {
|
||||||
|
fmt.Fprintf(os.Stderr, "cleaning up runtime directory %q\n", runtimeDirPath)
|
||||||
|
if err := os.RemoveAll(runtimeDirPath); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error removing temporary directory %q: %v", runtimeDirPath, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// If the bootstrap file is not being stored in the data dir, move it
|
||||||
|
// there and reload the bootstrap info
|
||||||
|
if env.BootstrapPath != env.DataDirBootstrapPath() {
|
||||||
|
|
||||||
|
path := env.BootstrapPath
|
||||||
|
|
||||||
|
// If there's no BootstrapPath then no bootstrap file could be
|
||||||
|
// found. In this case we require the user to provide one on the
|
||||||
|
// command-line.
|
||||||
|
if path == "" {
|
||||||
|
|
||||||
|
if *bootstrapPath == "" {
|
||||||
|
return errors.New("No bootstrap.tgz file could be found, and one is not provided with --bootstrap-path")
|
||||||
|
}
|
||||||
|
|
||||||
|
path = *bootstrapPath
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("opening file %q: %w", env.BootstrapPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = copyBootstrapToDataDir(env, f)
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("copying bootstrap file from %q: %w", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeMergedDaemonYml(env, *daemonYmlPath); err != nil {
|
||||||
|
return fmt.Errorf("merging and writing daemon.yml file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// we update this Host's data using whatever configuration has been
|
||||||
|
// provided by daemon.yml. This way the daemon has the most
|
||||||
|
// up-to-date possible bootstrap. This updated bootstrap will later
|
||||||
|
// get updated in garage using update-global-bucket, so other hosts
|
||||||
|
// will see it as well.
|
||||||
|
|
||||||
|
// ThisDaemon can only be called after writeDaemonYml.
|
||||||
|
daemon := env.ThisDaemon()
|
||||||
|
host := env.Bootstrap.ThisHost()
|
||||||
|
|
||||||
|
host.Nebula.PublicAddr = daemon.VPN.PublicAddr
|
||||||
|
|
||||||
|
host.Garage = nil
|
||||||
|
|
||||||
|
if allocs := daemon.Storage.Allocations; len(allocs) > 0 {
|
||||||
|
|
||||||
|
host.Garage = new(bootstrap.GarageHost)
|
||||||
|
|
||||||
|
for _, alloc := range allocs {
|
||||||
|
host.Garage.Instances = append(host.Garage.Instances, bootstrap.GarageHostInstance{
|
||||||
|
RPCPort: alloc.RPCPort,
|
||||||
|
S3APIPort: alloc.S3APIPort,
|
||||||
|
WebPort: alloc.WebPort,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env.Bootstrap.Hosts[host.Name] = host
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if err := env.Bootstrap.WithHosts(env.Bootstrap.Hosts).WriteTo(buf); err != nil {
|
||||||
|
return fmt.Errorf("writing new bootstrap file to buffer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := copyBootstrapToDataDir(env, buf); err != nil {
|
||||||
|
return fmt.Errorf("copying new bootstrap file to data dir: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, val := range env.ToMap() {
|
||||||
|
if err := os.Setenv(key, val); err != nil {
|
||||||
|
return fmt.Errorf("failed to set %q to %q: %w", key, val, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
|
||||||
|
// create s3Client anew on every loop, in case the topology has
|
||||||
|
// changed and we should be connecting to a different garage
|
||||||
|
// endpoint.
|
||||||
|
s3Client, err := env.Bootstrap.GlobalBucketS3APIClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating client for global bucket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runDaemonPmuxOnce(env, s3Client); errors.Is(err, context.Canceled) {
|
||||||
|
return nil
|
||||||
|
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("running pmux for daemon: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
72
go-workspace/src/cmd/entrypoint/daemon_yml.go
Normal file
72
go-workspace/src/cmd/entrypoint/daemon_yml.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
crypticnet "cryptic-net"
|
||||||
|
"cryptic-net/yamlutil"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/imdario/mergo"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func builtinDaemonYmlPath(env *crypticnet.Env) string {
|
||||||
|
return filepath.Join(env.AppDirPath, "etc", "daemon.yml")
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeBuiltinDaemonYml(env *crypticnet.Env, w io.Writer) error {
|
||||||
|
|
||||||
|
builtinDaemonYmlPath := builtinDaemonYmlPath(env)
|
||||||
|
|
||||||
|
builtinDaemonYml, err := os.ReadFile(builtinDaemonYmlPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading default daemon.yml at %q: %w", builtinDaemonYmlPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := w.Write(builtinDaemonYml); err != nil {
|
||||||
|
return fmt.Errorf("writing default daemon.yml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeMergedDaemonYml(env *crypticnet.Env, userDaemonYmlPath string) error {
|
||||||
|
|
||||||
|
builtinDaemonYmlPath := builtinDaemonYmlPath(env)
|
||||||
|
|
||||||
|
var fullDaemonYml map[string]interface{}
|
||||||
|
|
||||||
|
if err := yamlutil.LoadYamlFile(&fullDaemonYml, builtinDaemonYmlPath); err != nil {
|
||||||
|
return fmt.Errorf("parsing builtin daemon.yml file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if userDaemonYmlPath != "" {
|
||||||
|
|
||||||
|
var daemonYml map[string]interface{}
|
||||||
|
if err := yamlutil.LoadYamlFile(&daemonYml, userDaemonYmlPath); err != nil {
|
||||||
|
return fmt.Errorf("parsing %q: %w", userDaemonYmlPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mergo.Merge(&fullDaemonYml, daemonYml, mergo.WithOverride)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("merging contents of file %q: %w", userDaemonYmlPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fullDaemonYmlB, err := yaml.Marshal(fullDaemonYml)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("yaml marshaling daemon config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
daemonYmlPath := filepath.Join(env.RuntimeDirPath, "daemon.yml")
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(daemonYmlPath, fullDaemonYmlB, 0400); err != nil {
|
||||||
|
return fmt.Errorf("writing daemon.yml file to %q: %w", daemonYmlPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
118
go-workspace/src/cmd/entrypoint/garage.go
Normal file
118
go-workspace/src/cmd/entrypoint/garage.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
var subCmdGarageMC = subCmd{
|
||||||
|
name: "mc",
|
||||||
|
descr: "Runs the mc (minio-client) binary. The cryptic-net garage can be accessed under the `garage` alias",
|
||||||
|
checkLock: true,
|
||||||
|
do: func(subCmdCtx subCmdCtx) error {
|
||||||
|
|
||||||
|
flags := subCmdCtx.flagSet(true)
|
||||||
|
|
||||||
|
keyID := flags.StringP(
|
||||||
|
"key-id", "i", "",
|
||||||
|
"Optional key ID to use, defaults to that of the shared cryptic-net-global key",
|
||||||
|
)
|
||||||
|
|
||||||
|
keySecret := flags.StringP(
|
||||||
|
"key-secret", "s", "",
|
||||||
|
"Optional key secret to use, defaults to that of the shared cryptic-net-global key",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||||
|
return fmt.Errorf("parsing flags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
env := subCmdCtx.env
|
||||||
|
|
||||||
|
s3APIAddr := env.Bootstrap.ChooseGaragePeer().S3APIAddr()
|
||||||
|
|
||||||
|
if *keyID == "" || *keySecret == "" {
|
||||||
|
|
||||||
|
if *keyID == "" {
|
||||||
|
*keyID = env.Bootstrap.GarageGlobalBucketS3APICredentials.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
if *keySecret == "" {
|
||||||
|
*keyID = env.Bootstrap.GarageGlobalBucketS3APICredentials.Secret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := flags.Args()
|
||||||
|
|
||||||
|
if i := flags.ArgsLenAtDash(); i >= 0 {
|
||||||
|
args = args[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append([]string{"mc"}, args...)
|
||||||
|
|
||||||
|
var (
|
||||||
|
binPath = env.BinPath("mc")
|
||||||
|
cliEnv = append(
|
||||||
|
os.Environ(),
|
||||||
|
fmt.Sprintf(
|
||||||
|
"MC_HOST_garage=http://%s:%s@%s",
|
||||||
|
*keyID, *keySecret, s3APIAddr,
|
||||||
|
),
|
||||||
|
|
||||||
|
// The garage docs say this is necessary, though nothing bad
|
||||||
|
// seems to happen if we leave it out *shrug*
|
||||||
|
"MC_REGION=garage",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := syscall.Exec(binPath, args, cliEnv); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"calling exec(%q, %#v, %#v): %w",
|
||||||
|
binPath, args, cliEnv, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var subCmdGarageCLI = subCmd{
|
||||||
|
name: "cli",
|
||||||
|
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running cryptic-net daemon",
|
||||||
|
checkLock: true,
|
||||||
|
do: func(subCmdCtx subCmdCtx) error {
|
||||||
|
|
||||||
|
env := subCmdCtx.env
|
||||||
|
|
||||||
|
var (
|
||||||
|
binPath = env.BinPath("garage")
|
||||||
|
args = append([]string{"garage"}, subCmdCtx.args...)
|
||||||
|
cliEnv = append(
|
||||||
|
os.Environ(),
|
||||||
|
"GARAGE_RPC_HOST="+env.Bootstrap.ChooseGaragePeer().RPCAddr(),
|
||||||
|
"GARAGE_RPC_SECRET="+env.Bootstrap.GarageRPCSecret,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := syscall.Exec(binPath, args, cliEnv); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"calling exec(%q, %#v, %#v): %w",
|
||||||
|
binPath, args, cliEnv, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var subCmdGarage = subCmd{
|
||||||
|
name: "garage",
|
||||||
|
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running cryptic-net daemon",
|
||||||
|
do: func(subCmdCtx subCmdCtx) error {
|
||||||
|
return subCmdCtx.doSubCmd(
|
||||||
|
subCmdGarageCLI,
|
||||||
|
subCmdGarageMC,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
151
go-workspace/src/cmd/entrypoint/hosts.go
Normal file
151
go-workspace/src/cmd/entrypoint/hosts.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cryptic-net/bootstrap"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var hostNameRegexp = regexp.MustCompile(`^[a-z][a-z0-9\-]*$`)
|
||||||
|
|
||||||
|
func validateHostName(name string) error {
|
||||||
|
|
||||||
|
if !hostNameRegexp.MatchString(name) {
|
||||||
|
return errors.New("a host's name must start with a letter and only contain letters, numbers, and dashes")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var subCmdHostsAdd = subCmd{
|
||||||
|
name: "add",
|
||||||
|
descr: "Adds a host to the network",
|
||||||
|
checkLock: true,
|
||||||
|
do: func(subCmdCtx subCmdCtx) error {
|
||||||
|
|
||||||
|
flags := subCmdCtx.flagSet(false)
|
||||||
|
|
||||||
|
name := flags.StringP(
|
||||||
|
"name", "n", "",
|
||||||
|
"Name of the new host",
|
||||||
|
)
|
||||||
|
|
||||||
|
ip := flags.StringP(
|
||||||
|
"ip", "i", "",
|
||||||
|
"IP of the new host",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||||
|
return fmt.Errorf("parsing flags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *name == "" || *ip == "" {
|
||||||
|
return errors.New("--name and --ip are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateHostName(*name); err != nil {
|
||||||
|
return fmt.Errorf("invalid hostname %q: %w", *name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if net.ParseIP(*ip) == nil {
|
||||||
|
return fmt.Errorf("invalid ip %q", *ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO validate that the IP is in the correct CIDR
|
||||||
|
|
||||||
|
env := subCmdCtx.env
|
||||||
|
|
||||||
|
client, err := env.Bootstrap.GlobalBucketS3APIClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating client for global bucket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
host := bootstrap.Host{
|
||||||
|
Name: *name,
|
||||||
|
Nebula: bootstrap.NebulaHost{
|
||||||
|
IP: *ip,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return bootstrap.PutGarageBoostrapHost(env.Context, client, host)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var subCmdHostsList = subCmd{
|
||||||
|
name: "list",
|
||||||
|
descr: "Lists all hosts in the network, and their IPs",
|
||||||
|
checkLock: true,
|
||||||
|
do: func(subCmdCtx subCmdCtx) error {
|
||||||
|
|
||||||
|
env := subCmdCtx.env
|
||||||
|
|
||||||
|
client, err := env.Bootstrap.GlobalBucketS3APIClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating client for global bucket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hostsMap, err := bootstrap.GetGarageBootstrapHosts(env.Context, client)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("retrieving hosts from garage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts := make([]bootstrap.Host, 0, len(hostsMap))
|
||||||
|
for _, host := range hostsMap {
|
||||||
|
hosts = append(hosts, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(hosts, func(i, j int) bool { return hosts[i].Name < hosts[j].Name })
|
||||||
|
|
||||||
|
return yaml.NewEncoder(os.Stdout).Encode(hosts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var subCmdHostsDelete = subCmd{
|
||||||
|
name: "delete",
|
||||||
|
descr: "Deletes a host from the network",
|
||||||
|
checkLock: true,
|
||||||
|
do: func(subCmdCtx subCmdCtx) error {
|
||||||
|
|
||||||
|
flags := subCmdCtx.flagSet(false)
|
||||||
|
|
||||||
|
name := flags.StringP(
|
||||||
|
"name", "n", "",
|
||||||
|
"Name of the host to delete",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||||
|
return fmt.Errorf("parsing flags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *name == "" {
|
||||||
|
return errors.New("--name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
env := subCmdCtx.env
|
||||||
|
|
||||||
|
client, err := env.Bootstrap.GlobalBucketS3APIClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating client for global bucket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bootstrap.RemoveGarageBootstrapHost(env.Context, client, *name)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var subCmdHosts = subCmd{
|
||||||
|
name: "hosts",
|
||||||
|
descr: "Sub-commands having to do with configuration of hosts in the network",
|
||||||
|
do: func(subCmdCtx subCmdCtx) error {
|
||||||
|
return subCmdCtx.doSubCmd(
|
||||||
|
subCmdHostsAdd,
|
||||||
|
subCmdHostsDelete,
|
||||||
|
subCmdHostsList,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
37
go-workspace/src/cmd/entrypoint/main.go
Normal file
37
go-workspace/src/cmd/entrypoint/main.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
crypticnet "cryptic-net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The purpose of this binary is to act as the entrypoint of the cryptic-net
|
||||||
|
// process. It processes the command-line arguments which are passed in, and
|
||||||
|
// then passes execution along to an appropriate binary housed in AppDir/bin
|
||||||
|
// (usually a bash script, which is more versatile than a go program).
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
|
||||||
|
env, err := crypticnet.NewEnv(true)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("loading environment: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = subCmdCtx{
|
||||||
|
args: os.Args[1:],
|
||||||
|
env: env,
|
||||||
|
}.doSubCmd(
|
||||||
|
subCmdAdmin,
|
||||||
|
subCmdDaemon,
|
||||||
|
subCmdGarage,
|
||||||
|
subCmdHosts,
|
||||||
|
subCmdVersion,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
121
go-workspace/src/cmd/entrypoint/sub_cmd.go
Normal file
121
go-workspace/src/cmd/entrypoint/sub_cmd.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
crypticnet "cryptic-net"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// subCmdCtx contains all information available to a subCmd's do method.
|
||||||
|
type subCmdCtx struct {
|
||||||
|
subCmd subCmd // the subCmd itself
|
||||||
|
args []string // command-line arguments, excluding the subCmd itself.
|
||||||
|
subCmdNames []string // names of subCmds so far, including this one
|
||||||
|
env *crypticnet.Env
|
||||||
|
}
|
||||||
|
|
||||||
|
type subCmd struct {
|
||||||
|
name string
|
||||||
|
descr string
|
||||||
|
checkLock bool
|
||||||
|
do func(subCmdCtx) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx subCmdCtx) usagePrefix() string {
|
||||||
|
|
||||||
|
subCmdNamesStr := strings.Join(ctx.subCmdNames, " ")
|
||||||
|
if subCmdNamesStr != "" {
|
||||||
|
subCmdNamesStr += " "
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("\nUSAGE: %s %s", os.Args[0], subCmdNamesStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx subCmdCtx) flagSet(withPassthrough bool) *pflag.FlagSet {
|
||||||
|
flags := pflag.NewFlagSet(ctx.subCmd.name, pflag.ExitOnError)
|
||||||
|
flags.Usage = func() {
|
||||||
|
|
||||||
|
var passthroughStr string
|
||||||
|
if withPassthrough {
|
||||||
|
passthroughStr = " [--] [args...]"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(
|
||||||
|
os.Stderr, "%s[-h|--help] [%s flags...]%s\n\n",
|
||||||
|
ctx.usagePrefix(), ctx.subCmd.name, passthroughStr,
|
||||||
|
)
|
||||||
|
fmt.Fprintf(os.Stderr, "%s FLAGS:\n\n", strings.ToUpper(ctx.subCmd.name))
|
||||||
|
fmt.Fprintln(os.Stderr, flags.FlagUsages())
|
||||||
|
|
||||||
|
os.Stderr.Sync()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
|
||||||
|
|
||||||
|
printUsageExit := func(subCmdName string) {
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "unknown sub-command %q\n", subCmdName)
|
||||||
|
|
||||||
|
fmt.Fprintf(
|
||||||
|
os.Stderr,
|
||||||
|
"%s<subCmd> [-h|--help] [sub-command flags...]\n",
|
||||||
|
ctx.usagePrefix(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "\nSUB-COMMANDS:\n\n")
|
||||||
|
|
||||||
|
for _, subCmd := range subCmds {
|
||||||
|
fmt.Fprintf(os.Stderr, " %s\t%s\n", subCmd.name, subCmd.descr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "\n")
|
||||||
|
os.Stderr.Sync()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := ctx.args
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
printUsageExit("")
|
||||||
|
}
|
||||||
|
|
||||||
|
subCmdsMap := map[string]subCmd{}
|
||||||
|
for _, subCmd := range subCmds {
|
||||||
|
subCmdsMap[subCmd.name] = subCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
subCmdName, args := args[0], args[1:]
|
||||||
|
subCmd, ok := subCmdsMap[subCmdName]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
printUsageExit(subCmdName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if subCmd.checkLock {
|
||||||
|
|
||||||
|
err := crypticnet.NewProcLock(ctx.env.RuntimeDirPath).AssertLock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("checking lock file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := subCmd.do(subCmdCtx{
|
||||||
|
subCmd: subCmd,
|
||||||
|
args: args,
|
||||||
|
subCmdNames: append(ctx.subCmdNames, subCmdName),
|
||||||
|
env: ctx.env,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package entrypoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -9,10 +9,9 @@ import (
|
|||||||
var subCmdVersion = subCmd{
|
var subCmdVersion = subCmd{
|
||||||
name: "version",
|
name: "version",
|
||||||
descr: "Dumps version and build info to stdout",
|
descr: "Dumps version and build info to stdout",
|
||||||
noNetwork: true,
|
do: func(subCmdCtx subCmdCtx) error {
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
|
|
||||||
versionPath := filepath.Join(envAppDirPath, "share/version")
|
versionPath := filepath.Join(subCmdCtx.env.AppDirPath, "share/version")
|
||||||
|
|
||||||
version, err := os.ReadFile(versionPath)
|
version, err := os.ReadFile(versionPath)
|
||||||
|
|
129
go-workspace/src/cmd/garage-entrypoint/main.go
Normal file
129
go-workspace/src/cmd/garage-entrypoint/main.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
package garage_entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
crypticnet "cryptic-net"
|
||||||
|
"cryptic-net/garage"
|
||||||
|
|
||||||
|
"github.com/cryptic-io/pmux/pmuxlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeChildConf(
|
||||||
|
env *crypticnet.Env,
|
||||||
|
alloc crypticnet.DaemonYmlStorageAllocation,
|
||||||
|
) (string, error) {
|
||||||
|
|
||||||
|
if err := os.MkdirAll(alloc.MetaPath, 0750); err != nil {
|
||||||
|
return "", fmt.Errorf("making directory %q: %w", alloc.MetaPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
thisHost := env.Bootstrap.ThisHost()
|
||||||
|
|
||||||
|
peer := garage.Peer{
|
||||||
|
IP: thisHost.Nebula.IP,
|
||||||
|
RPCPort: alloc.RPCPort,
|
||||||
|
S3APIPort: alloc.S3APIPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey, privKey := peer.RPCPeerKey()
|
||||||
|
|
||||||
|
nodeKeyPath := filepath.Join(alloc.MetaPath, "node_key")
|
||||||
|
nodeKeyPubPath := filepath.Join(alloc.MetaPath, "node_keypub")
|
||||||
|
|
||||||
|
if err := os.WriteFile(nodeKeyPath, privKey, 0400); err != nil {
|
||||||
|
return "", fmt.Errorf("writing private key to %q: %w", nodeKeyPath, err)
|
||||||
|
|
||||||
|
} else if err := os.WriteFile(nodeKeyPubPath, pubKey, 0440); err != nil {
|
||||||
|
return "", fmt.Errorf("writing public key to %q: %w", nodeKeyPubPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
garageTomlPath := filepath.Join(
|
||||||
|
env.RuntimeDirPath, fmt.Sprintf("garage-%d.toml", alloc.RPCPort),
|
||||||
|
)
|
||||||
|
|
||||||
|
err := garage.WriteGarageTomlFile(garageTomlPath, garage.GarageTomlData{
|
||||||
|
MetaPath: alloc.MetaPath,
|
||||||
|
DataPath: alloc.DataPath,
|
||||||
|
|
||||||
|
RPCSecret: env.Bootstrap.GarageRPCSecret,
|
||||||
|
|
||||||
|
RPCAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
|
||||||
|
APIAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.S3APIPort)),
|
||||||
|
WebAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.WebPort)),
|
||||||
|
|
||||||
|
BootstrapPeers: env.Bootstrap.GarageRPCPeerAddrs(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("creating garage.toml file at %q: %w", garageTomlPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return garageTomlPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForArgs(env *crypticnet.Env, bin string, binArgs ...string) []string {
|
||||||
|
|
||||||
|
thisHost := env.Bootstrap.ThisHost()
|
||||||
|
|
||||||
|
var args []string
|
||||||
|
|
||||||
|
for _, alloc := range env.ThisDaemon().Storage.Allocations {
|
||||||
|
args = append(
|
||||||
|
args,
|
||||||
|
"wait-for",
|
||||||
|
net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
|
||||||
|
"--",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, bin)
|
||||||
|
args = append(args, binArgs...)
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
|
||||||
|
env, err := crypticnet.ReadEnv()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("reading envvars: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pmuxProcConfigs []pmuxlib.ProcessConfig
|
||||||
|
|
||||||
|
for _, alloc := range env.ThisDaemon().Storage.Allocations {
|
||||||
|
|
||||||
|
childConfPath, err := writeChildConf(env, alloc)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("writing child config file for alloc %+v: %v", alloc, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("wrote config file %q", childConfPath)
|
||||||
|
|
||||||
|
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
|
||||||
|
Name: fmt.Sprintf("garage-%d", alloc.RPCPort),
|
||||||
|
Cmd: "garage",
|
||||||
|
Args: []string{"-c", childConfPath, "server"},
|
||||||
|
SigKillWait: 1 * time.Minute,
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
|
||||||
|
Name: "garage-apply-layout-diff",
|
||||||
|
Cmd: "bash",
|
||||||
|
Args: waitForArgs(env, "bash", "garage-apply-layout-diff"),
|
||||||
|
NoRestartOn: []int{0},
|
||||||
|
})
|
||||||
|
|
||||||
|
pmuxlib.Run(env.Context, pmuxlib.Config{Processes: pmuxProcConfigs})
|
||||||
|
}
|
256
go-workspace/src/cmd/garage-layout-diff/main.go
Normal file
256
go-workspace/src/cmd/garage-layout-diff/main.go
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
package garage_layout_diff
|
||||||
|
|
||||||
|
// This binary accepts the output of `garage layout show` into its stdout, and
|
||||||
|
// it outputs a newline-delimited set of `garage layout $cmd` strings on
|
||||||
|
// stdout. The layout commands which are output will, if run, bring the current
|
||||||
|
// node's layout on the cluster up-to-date with what's in daemon.yml.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
crypticnet "cryptic-net"
|
||||||
|
"cryptic-net/garage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type clusterNode struct {
|
||||||
|
ID string
|
||||||
|
Zone string
|
||||||
|
Capacity int
|
||||||
|
}
|
||||||
|
|
||||||
|
type clusterNodes []clusterNode
|
||||||
|
|
||||||
|
func (n clusterNodes) get(id string) (clusterNode, bool) {
|
||||||
|
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
for _, node := range n {
|
||||||
|
|
||||||
|
if len(node.ID) > len(id) {
|
||||||
|
ok = strings.HasPrefix(node.ID, id)
|
||||||
|
} else {
|
||||||
|
ok = strings.HasPrefix(id, node.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return node, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clusterNode{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var currClusterLayoutVersionB = []byte("Current cluster layout version:")
|
||||||
|
|
||||||
|
func readCurrNodes(r io.Reader) (clusterNodes, int, error) {
|
||||||
|
|
||||||
|
input, err := io.ReadAll(r)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("reading stdin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE I'm not sure if this check should be turned on or not. It simplifies
|
||||||
|
// things to turn it off and just say that no one should ever be manually
|
||||||
|
// messing with the layout, but on the other hand maybe someone might?
|
||||||
|
//
|
||||||
|
//if i := bytes.Index(input, []byte("==== STAGED ROLE CHANGES ====")); i >= 0 {
|
||||||
|
// return nil, 0, errors.New("cluster layout has staged changes already, won't modify")
|
||||||
|
//}
|
||||||
|
|
||||||
|
/* The first section of input will always be something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
==== CURRENT CLUSTER LAYOUT ====
|
||||||
|
ID Tags Zone Capacity
|
||||||
|
AAA… ZZZ 1
|
||||||
|
BBB… ZZZ 1
|
||||||
|
CCC… ZZZ 1
|
||||||
|
|
||||||
|
Current cluster layout version: N
|
||||||
|
```
|
||||||
|
|
||||||
|
There may be more, depending on if the cluster already has changes staged,
|
||||||
|
but this will definitely be first. */
|
||||||
|
|
||||||
|
i := bytes.Index(input, currClusterLayoutVersionB)
|
||||||
|
|
||||||
|
if i < 0 {
|
||||||
|
return nil, 0, errors.New("no current cluster layout found in input")
|
||||||
|
}
|
||||||
|
|
||||||
|
input, tail := input[:i], input[i:]
|
||||||
|
|
||||||
|
var currNodes clusterNodes
|
||||||
|
|
||||||
|
for inputBuf := bufio.NewReader(bytes.NewBuffer(input)); ; {
|
||||||
|
|
||||||
|
line, err := inputBuf.ReadString('\n')
|
||||||
|
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("reading input line by line from buffer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
|
||||||
|
if len(fields) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
id := fields[0]
|
||||||
|
|
||||||
|
// The ID will always be given ending in this fucked up ellipses
|
||||||
|
if trimmedID := strings.TrimSuffix(id, "…"); id == trimmedID {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
id = trimmedID
|
||||||
|
}
|
||||||
|
|
||||||
|
zone := fields[1]
|
||||||
|
|
||||||
|
capacity, err := strconv.Atoi(fields[2])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("parsing capacity %q: %w", fields[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currNodes = append(currNodes, clusterNode{
|
||||||
|
ID: id,
|
||||||
|
Zone: zone,
|
||||||
|
Capacity: capacity,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse current cluster version from tail
|
||||||
|
tail = bytes.TrimPrefix(tail, currClusterLayoutVersionB)
|
||||||
|
|
||||||
|
if i := bytes.Index(tail, []byte("\n")); i > 0 {
|
||||||
|
tail = tail[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
tail = bytes.TrimSpace(tail)
|
||||||
|
|
||||||
|
version, err := strconv.Atoi(string(tail))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("parsing version string from %q: %w", tail, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return currNodes, version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readExpNodes(env *crypticnet.Env) clusterNodes {
|
||||||
|
|
||||||
|
thisHost := env.Bootstrap.ThisHost()
|
||||||
|
|
||||||
|
var expNodes clusterNodes
|
||||||
|
|
||||||
|
for _, alloc := range env.ThisDaemon().Storage.Allocations {
|
||||||
|
|
||||||
|
peer := garage.Peer{
|
||||||
|
IP: thisHost.Nebula.IP,
|
||||||
|
RPCPort: alloc.RPCPort,
|
||||||
|
S3APIPort: alloc.S3APIPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
id := peer.RPCPeerID()
|
||||||
|
|
||||||
|
expNodes = append(expNodes, clusterNode{
|
||||||
|
ID: id,
|
||||||
|
Zone: env.Bootstrap.HostName,
|
||||||
|
Capacity: alloc.Capacity / 100,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return expNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: The id formatting for currNodes and expNodes is different; expNodes has
|
||||||
|
// fully expanded ids, currNodes are abbreviated.
|
||||||
|
|
||||||
|
func diff(currNodes, expNodes clusterNodes) []string {
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
for _, node := range currNodes {
|
||||||
|
|
||||||
|
if _, ok := expNodes.get(node.ID); !ok {
|
||||||
|
lines = append(
|
||||||
|
lines,
|
||||||
|
fmt.Sprintf("garage layout remove %s", node.ID),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expNode := range expNodes {
|
||||||
|
|
||||||
|
currNode, ok := currNodes.get(expNode.ID)
|
||||||
|
|
||||||
|
currNode.ID = expNode.ID // so that equality checking works
|
||||||
|
|
||||||
|
if ok && currNode == expNode {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = append(
|
||||||
|
lines,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"garage layout assign %s -z %s -c %d",
|
||||||
|
expNode.ID,
|
||||||
|
expNode.Zone,
|
||||||
|
expNode.Capacity,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
|
||||||
|
env, err := crypticnet.ReadEnv()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("reading environment: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
currNodes, currVersion, err := readCurrNodes(os.Stdin)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("reading current layout from stdin: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
thisCurrNodes := make(clusterNodes, 0, len(currNodes))
|
||||||
|
|
||||||
|
for _, node := range currNodes {
|
||||||
|
|
||||||
|
if env.Bootstrap.HostName != node.Zone {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
thisCurrNodes = append(thisCurrNodes, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
expNodes := readExpNodes(env)
|
||||||
|
|
||||||
|
lines := diff(thisCurrNodes, expNodes)
|
||||||
|
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
fmt.Println(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("garage layout apply --version %d\n", currVersion+1)
|
||||||
|
}
|
137
go-workspace/src/cmd/garage-layout-diff/main_test.go
Normal file
137
go-workspace/src/cmd/garage-layout-diff/main_test.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package garage_layout_diff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadCurrNodes(t *testing.T) {
|
||||||
|
|
||||||
|
expNodes := clusterNodes{
|
||||||
|
{
|
||||||
|
ID: "AAA",
|
||||||
|
Zone: "XXX",
|
||||||
|
Capacity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "BBB",
|
||||||
|
Zone: "YYY",
|
||||||
|
Capacity: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "CCC",
|
||||||
|
Zone: "ZZZ",
|
||||||
|
Capacity: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expVersion := 666
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expNodes clusterNodes
|
||||||
|
expVersion int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: `
|
||||||
|
==== CURRENT CLUSTER LAYOUT ====
|
||||||
|
ID Tags Zone Capacity
|
||||||
|
AAA… XXX 1
|
||||||
|
BBB… YYY 2
|
||||||
|
CCC… ZZZ 3
|
||||||
|
|
||||||
|
Current cluster layout version: 666
|
||||||
|
`,
|
||||||
|
expNodes: expNodes,
|
||||||
|
expVersion: expVersion,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||||
|
|
||||||
|
gotNodes, gotVersion, err := readCurrNodes(
|
||||||
|
bytes.NewBufferString(test.input),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotVersion != test.expVersion {
|
||||||
|
t.Fatalf(
|
||||||
|
"expected version %d, got %d",
|
||||||
|
test.expVersion,
|
||||||
|
gotVersion,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(gotNodes, test.expNodes) {
|
||||||
|
t.Fatalf(
|
||||||
|
"expected nodes: %#v,\ngot nodes: %#v",
|
||||||
|
gotNodes,
|
||||||
|
test.expNodes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiff(t *testing.T) {
|
||||||
|
|
||||||
|
currNodes := clusterNodes{
|
||||||
|
{
|
||||||
|
ID: "1",
|
||||||
|
Zone: "zone",
|
||||||
|
Capacity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "2",
|
||||||
|
Zone: "zone",
|
||||||
|
Capacity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "3",
|
||||||
|
Zone: "zone",
|
||||||
|
Capacity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "4",
|
||||||
|
Zone: "zone",
|
||||||
|
Capacity: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expNodes := clusterNodes{
|
||||||
|
{
|
||||||
|
ID: "111",
|
||||||
|
Zone: "zone",
|
||||||
|
Capacity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "222",
|
||||||
|
Zone: "zone2",
|
||||||
|
Capacity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "333",
|
||||||
|
Zone: "zone",
|
||||||
|
Capacity: 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expLines := []string{
|
||||||
|
`garage layout remove 4`,
|
||||||
|
`garage layout assign 222 -z zone2 -c 1`,
|
||||||
|
`garage layout assign 333 -z zone -c 10`,
|
||||||
|
}
|
||||||
|
|
||||||
|
gotLines := diff(currNodes, expNodes)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(gotLines, expLines) {
|
||||||
|
t.Fatalf("expected lines: %#v,\ngot lines: %#v", expLines, gotLines)
|
||||||
|
}
|
||||||
|
}
|
65
go-workspace/src/cmd/garage-peer-keygen/main.go
Normal file
65
go-workspace/src/cmd/garage-peer-keygen/main.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package garage_peer_keygen
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
!! !!
|
||||||
|
!! DANGER !!
|
||||||
|
!! !!
|
||||||
|
!! This script will deterministically produce public/private keys given some !!
|
||||||
|
!! arbitrary input. This is NEVER what you want. It's only being used in !!
|
||||||
|
!! cryptic-net for a very specific purpose for which I think it's ok and is !!
|
||||||
|
!! very necessary, and people are probably _still_ going to yell at me. !!
|
||||||
|
!! !!
|
||||||
|
!! DONT USE THIS. !!
|
||||||
|
!! !!
|
||||||
|
!! - Brian !!
|
||||||
|
!! !!
|
||||||
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"cryptic-net/garage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
|
||||||
|
ip := flag.String("ip", "", "Internal IP address of the node to generate a key for")
|
||||||
|
port := flag.Int("port", 0, "RPC port number for the garage instance to generate a key for")
|
||||||
|
outPriv := flag.String("out-priv", "", "The path to the private key which should be created, if given.")
|
||||||
|
outPub := flag.String("out-pub", "", "The path to the public key which should be created, if given.")
|
||||||
|
danger := flag.Bool("danger", false, "Set this flag to indicate you understand WHY this binary should NEVER be used (see source code).")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if len(*ip) == 0 || *port == 0 || !*danger {
|
||||||
|
panic("The arguments -ip, -port, and -danger are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
peer := garage.Peer{
|
||||||
|
IP: *ip,
|
||||||
|
RPCPort: *port,
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey, privKey := peer.RPCPeerKey()
|
||||||
|
|
||||||
|
fmt.Fprintln(os.Stdout, hex.EncodeToString(pubKey))
|
||||||
|
|
||||||
|
if *outPub != "" {
|
||||||
|
if err := ioutil.WriteFile(*outPub, pubKey, 0444); err != nil {
|
||||||
|
panic(fmt.Errorf("writing public key to %q: %w", *outPub, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *outPriv != "" {
|
||||||
|
if err := ioutil.WriteFile(*outPriv, privKey, 0400); err != nil {
|
||||||
|
panic(fmt.Errorf("writing private key to %q: %w", *outPriv, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
134
go-workspace/src/cmd/nebula-entrypoint/main.go
Normal file
134
go-workspace/src/cmd/nebula-entrypoint/main.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
package nebula_entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cryptic-net/yamlutil"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
crypticnet "cryptic-net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
|
||||||
|
env, err := crypticnet.ReadEnv()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("reading envvars: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
lighthouseHostIPs []string
|
||||||
|
staticHostMap = map[string][]string{}
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, host := range env.Bootstrap.Hosts {
|
||||||
|
|
||||||
|
if host.Nebula.PublicAddr == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lighthouseHostIPs = append(lighthouseHostIPs, host.Nebula.IP)
|
||||||
|
staticHostMap[host.Nebula.IP] = []string{host.Nebula.PublicAddr}
|
||||||
|
}
|
||||||
|
|
||||||
|
config := map[string]interface{}{
|
||||||
|
"pki": map[string]string{
|
||||||
|
"ca": env.Bootstrap.NebulaHostCert.CACert,
|
||||||
|
"cert": env.Bootstrap.NebulaHostCert.HostCert,
|
||||||
|
"key": env.Bootstrap.NebulaHostCert.HostKey,
|
||||||
|
},
|
||||||
|
"static_host_map": staticHostMap,
|
||||||
|
"punchy": map[string]bool{
|
||||||
|
"punch": true,
|
||||||
|
"respond": true,
|
||||||
|
},
|
||||||
|
"tun": map[string]interface{}{
|
||||||
|
"dev": "cryptic-nebula1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if publicAddr := env.ThisDaemon().VPN.PublicAddr; publicAddr == "" {
|
||||||
|
|
||||||
|
config["listen"] = map[string]string{
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": "0",
|
||||||
|
}
|
||||||
|
|
||||||
|
config["lighthouse"] = map[string]interface{}{
|
||||||
|
"hosts": lighthouseHostIPs,
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
_, port, err := net.SplitHostPort(publicAddr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("parsing public address %q: %v", publicAddr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config["listen"] = map[string]string{
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": port,
|
||||||
|
}
|
||||||
|
|
||||||
|
config["lighthouse"] = map[string]interface{}{
|
||||||
|
"hosts": []string{},
|
||||||
|
"am_lighthouse": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thisDaemon := env.ThisDaemon()
|
||||||
|
|
||||||
|
var firewallInbound []crypticnet.ConfigFirewallRule
|
||||||
|
|
||||||
|
for _, alloc := range thisDaemon.Storage.Allocations {
|
||||||
|
firewallInbound = append(
|
||||||
|
firewallInbound,
|
||||||
|
crypticnet.ConfigFirewallRule{
|
||||||
|
Port: strconv.Itoa(alloc.S3APIPort),
|
||||||
|
Proto: "tcp",
|
||||||
|
Host: "any",
|
||||||
|
},
|
||||||
|
crypticnet.ConfigFirewallRule{
|
||||||
|
Port: strconv.Itoa(alloc.RPCPort),
|
||||||
|
Proto: "tcp",
|
||||||
|
Host: "any",
|
||||||
|
},
|
||||||
|
crypticnet.ConfigFirewallRule{
|
||||||
|
Port: strconv.Itoa(alloc.WebPort),
|
||||||
|
Proto: "tcp",
|
||||||
|
Host: "any",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
firewall := thisDaemon.VPN.Firewall
|
||||||
|
|
||||||
|
firewall.Inbound = append(firewallInbound, firewall.Inbound...)
|
||||||
|
|
||||||
|
config["firewall"] = firewall
|
||||||
|
|
||||||
|
nebulaYmlPath := filepath.Join(env.RuntimeDirPath, "nebula.yml")
|
||||||
|
|
||||||
|
if err := yamlutil.WriteYamlFile(config, nebulaYmlPath); err != nil {
|
||||||
|
log.Fatalf("writing nebula.yml to %q: %v", nebulaYmlPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
binPath = env.BinPath("nebula")
|
||||||
|
args = []string{"nebula", "-config", nebulaYmlPath}
|
||||||
|
cliEnv = os.Environ()
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := syscall.Exec(binPath, args, cliEnv); err != nil {
|
||||||
|
log.Fatalf("calling exec(%q, %#v, %#v)", binPath, args, cliEnv)
|
||||||
|
}
|
||||||
|
}
|
30
go-workspace/src/cmd/update-global-bucket/main.go
Normal file
30
go-workspace/src/cmd/update-global-bucket/main.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package update_global_bucket
|
||||||
|
|
||||||
|
import (
|
||||||
|
crypticnet "cryptic-net"
|
||||||
|
"cryptic-net/bootstrap"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
|
||||||
|
env, err := crypticnet.ReadEnv()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("reading envvars: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := env.Bootstrap.GlobalBucketS3APIClient()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("creating client for global bucket: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bootstrap.PutGarageBoostrapHost(
|
||||||
|
env.Context,
|
||||||
|
client,
|
||||||
|
env.Bootstrap.ThisHost(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
51
go-workspace/src/daemon_yml.go
Normal file
51
go-workspace/src/daemon_yml.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package crypticnet
|
||||||
|
|
||||||
|
type ConfigFirewall struct {
|
||||||
|
Conntrack ConfigConntrack `yaml:"conntrack"`
|
||||||
|
Outbound []ConfigFirewallRule `yaml:"outbound"`
|
||||||
|
Inbound []ConfigFirewallRule `yaml:"inbound"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigConntrack struct {
|
||||||
|
TCPTimeout string `yaml:"tcp_timeout"`
|
||||||
|
UDPTimeout string `yaml:"udp_timeout"`
|
||||||
|
DefaultTimeout string `yaml:"default_timeout"`
|
||||||
|
MaxConnections int `yaml:"max_connections"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigFirewallRule struct {
|
||||||
|
Port string `yaml:"port,omitempty"`
|
||||||
|
Code string `yaml:"code,omitempty"`
|
||||||
|
Proto string `yaml:"proto,omitempty"`
|
||||||
|
Host string `yaml:"host,omitempty"`
|
||||||
|
Group string `yaml:"group,omitempty"`
|
||||||
|
Groups []string `yaml:"groups,omitempty"`
|
||||||
|
CIDR string `yaml:"cidr,omitempty"`
|
||||||
|
CASha string `yaml:"ca_sha,omitempty"`
|
||||||
|
CAName string `yaml:"ca_name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DaemonYmlStorageAllocation describes the structure of each storage allocation
|
||||||
|
// within the daemon.yml file.
|
||||||
|
type DaemonYmlStorageAllocation struct {
|
||||||
|
DataPath string `yaml:"data_path"`
|
||||||
|
MetaPath string `yaml:"meta_path"`
|
||||||
|
Capacity int `yaml:"capacity"`
|
||||||
|
S3APIPort int `yaml:"api_port"` // TODO fix field name here
|
||||||
|
RPCPort int `yaml:"rpc_port"`
|
||||||
|
WebPort int `yaml:"web_port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DaemonYml describes the structure of the daemon.yml file.
|
||||||
|
type DaemonYml struct {
|
||||||
|
DNS struct {
|
||||||
|
Resolvers []string `yaml:"resolvers"`
|
||||||
|
} `yaml:"dns"`
|
||||||
|
VPN struct {
|
||||||
|
PublicAddr string `yaml:"public_addr"`
|
||||||
|
Firewall ConfigFirewall `yaml:"firewall"`
|
||||||
|
} `yaml:"vpn"`
|
||||||
|
Storage struct {
|
||||||
|
Allocations []DaemonYmlStorageAllocation
|
||||||
|
} `yaml:"storage"`
|
||||||
|
}
|
3
go-workspace/src/doc.go
Normal file
3
go-workspace/src/doc.go
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// Package globals defines global constants and variables which are valid
|
||||||
|
// across all cryptic-net processes and sub-processes.
|
||||||
|
package crypticnet
|
226
go-workspace/src/env.go
Normal file
226
go-workspace/src/env.go
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
package crypticnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"cryptic-net/bootstrap"
|
||||||
|
"cryptic-net/yamlutil"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Names of various environment variables which get set by the entrypoint.
|
||||||
|
const (
|
||||||
|
DaemonYmlPathEnvVar = "_DAEMON_YML_PATH"
|
||||||
|
BootstrapPathEnvVar = "_BOOTSTRAP_PATH"
|
||||||
|
RuntimeDirPathEnvVar = "_RUNTIME_DIR_PATH"
|
||||||
|
DataDirPathEnvVar = "_DATA_DIR_PATH"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Env contains the values of environment variables, as well as other entities
|
||||||
|
// which are useful across all processes.
|
||||||
|
type Env struct {
|
||||||
|
Context context.Context
|
||||||
|
|
||||||
|
AppDirPath string
|
||||||
|
DaemonYmlPath string
|
||||||
|
RuntimeDirPath string
|
||||||
|
DataDirPath string
|
||||||
|
|
||||||
|
// If NewEnv is called with bootstrapOptional, and a bootstrap file is not
|
||||||
|
// found, then these fields will not be set.
|
||||||
|
BootstrapPath string
|
||||||
|
Bootstrap bootstrap.Bootstrap
|
||||||
|
|
||||||
|
thisDaemon DaemonYml
|
||||||
|
thisDaemonOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAppDirPath() string {
|
||||||
|
appDirPath := os.Getenv("APPDIR")
|
||||||
|
if appDirPath == "" {
|
||||||
|
appDirPath = "."
|
||||||
|
}
|
||||||
|
return appDirPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEnv calculates an Env instance based on the APPDIR and XDG envvars.
|
||||||
|
//
|
||||||
|
// If bootstrapOptional is true then NewEnv will first check if a bootstrap file
|
||||||
|
// can be found in the expected places, and if not then it will not populate
|
||||||
|
// BootstrapFS or any other fields based on it.
|
||||||
|
func NewEnv(bootstrapOptional bool) (*Env, error) {
|
||||||
|
|
||||||
|
runtimeDirPath := filepath.Join(xdg.RuntimeDir, "cryptic-net")
|
||||||
|
appDirPath := getAppDirPath()
|
||||||
|
|
||||||
|
env := &Env{
|
||||||
|
AppDirPath: appDirPath,
|
||||||
|
DaemonYmlPath: filepath.Join(runtimeDirPath, "daemon.yml"),
|
||||||
|
RuntimeDirPath: runtimeDirPath,
|
||||||
|
DataDirPath: filepath.Join(xdg.DataHome, "cryptic-net"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return env, env.init(bootstrapOptional)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadEnv reads an Env from the process's environment variables, rather than
|
||||||
|
// calculating like NewEnv does.
|
||||||
|
func ReadEnv() (*Env, error) {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
readEnv := func(key string) string {
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val := os.Getenv(key)
|
||||||
|
|
||||||
|
if val == "" {
|
||||||
|
err = fmt.Errorf("envvar %q not set", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &Env{
|
||||||
|
AppDirPath: getAppDirPath(),
|
||||||
|
DaemonYmlPath: readEnv(DaemonYmlPathEnvVar),
|
||||||
|
RuntimeDirPath: readEnv(RuntimeDirPathEnvVar),
|
||||||
|
DataDirPath: readEnv(DataDirPathEnvVar),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return env, env.init(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataDirBootstrapPath returns the path to the bootstrap file within the user's
|
||||||
|
// data dir. If the file does not exist there it will be found in the AppDirPath
|
||||||
|
// by ReloadBootstrap.
|
||||||
|
func (e *Env) DataDirBootstrapPath() string {
|
||||||
|
return filepath.Join(e.DataDirPath, "bootstrap.tgz")
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadBootstrap sets BootstrapPath to the given value, and loads BootstrapFS
|
||||||
|
// and all derived fields based on that.
|
||||||
|
func (e *Env) LoadBootstrap(path string) error {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e.Bootstrap, err = bootstrap.FromFile(path); err != nil {
|
||||||
|
return fmt.Errorf("parsing bootstrap.tgz at %q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.BootstrapPath = path
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Env) initBootstrap(bootstrapOptional bool) error {
|
||||||
|
|
||||||
|
exists := func(path string) (bool, error) {
|
||||||
|
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return false, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return false, fmt.Errorf("stat'ing %q: %w", path, err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// start by checking if a bootstrap can be found in the user's data
|
||||||
|
// directory. This will only not be the case if daemon has never been
|
||||||
|
// successfully started.
|
||||||
|
{
|
||||||
|
bootstrapPath := e.DataDirBootstrapPath()
|
||||||
|
|
||||||
|
if exists, err := exists(bootstrapPath); err != nil {
|
||||||
|
return fmt.Errorf("determining if %q exists: %w", bootstrapPath, err)
|
||||||
|
|
||||||
|
} else if exists {
|
||||||
|
return e.LoadBootstrap(bootstrapPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to checking within the AppDir for a bootstrap which has been
|
||||||
|
// embedded into the binary.
|
||||||
|
{
|
||||||
|
bootstrapPath := filepath.Join(e.AppDirPath, "share/bootstrap.tgz")
|
||||||
|
|
||||||
|
if exists, err := exists(bootstrapPath); err != nil {
|
||||||
|
return fmt.Errorf("determining if %q exists: %w", bootstrapPath, err)
|
||||||
|
|
||||||
|
} else if !exists && !bootstrapOptional {
|
||||||
|
return fmt.Errorf("boostrap file not found at %q", bootstrapPath)
|
||||||
|
|
||||||
|
} else if exists {
|
||||||
|
return e.LoadBootstrap(bootstrapPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Env) init(bootstrapOptional bool) error {
|
||||||
|
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
e.Context, cancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
signalCh := make(chan os.Signal, 2)
|
||||||
|
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
sig := <-signalCh
|
||||||
|
cancel()
|
||||||
|
fmt.Fprintf(os.Stderr, "got signal %v, will exit gracefully\n", sig)
|
||||||
|
|
||||||
|
sig = <-signalCh
|
||||||
|
fmt.Fprintf(os.Stderr, "second interrupt signal %v received, force quitting, there may be zombie children left behind, good luck!\n", sig)
|
||||||
|
|
||||||
|
os.Stderr.Sync()
|
||||||
|
os.Exit(1)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := e.initBootstrap(bootstrapOptional); err != nil {
|
||||||
|
return fmt.Errorf("initializing bootstrap data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToMap returns the Env as a map of key/value strings. If this map is set into
|
||||||
|
// a process's environment, then that process can read it back using ReadEnv.
|
||||||
|
func (e *Env) ToMap() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
DaemonYmlPathEnvVar: e.DaemonYmlPath,
|
||||||
|
BootstrapPathEnvVar: e.BootstrapPath,
|
||||||
|
RuntimeDirPathEnvVar: e.RuntimeDirPath,
|
||||||
|
DataDirPathEnvVar: e.DataDirPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThisDaemon returns the DaemonYml (loaded from DaemonYmlPath) for the
|
||||||
|
// currently running process.
|
||||||
|
func (e *Env) ThisDaemon() DaemonYml {
|
||||||
|
e.thisDaemonOnce.Do(func() {
|
||||||
|
if err := yamlutil.LoadYamlFile(&e.thisDaemon, e.DaemonYmlPath); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return e.thisDaemon
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinPath returns the absolute path to a binary in the AppDir.
|
||||||
|
func (e *Env) BinPath(name string) string {
|
||||||
|
return filepath.Join(e.AppDirPath, "bin", name)
|
||||||
|
}
|
34
go-workspace/src/garage/client.go
Normal file
34
go-workspace/src/garage/client.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package garage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsKeyNotFound returns true if the given error is the result of a key not
|
||||||
|
// being found in a bucket.
|
||||||
|
func IsKeyNotFound(err error) bool {
|
||||||
|
var mErr minio.ErrorResponse
|
||||||
|
return errors.As(err, &mErr) && mErr.Code == "NoSuchKey"
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3APIClient is a client used to interact with garage's S3 API.
|
||||||
|
type S3APIClient = *minio.Client
|
||||||
|
|
||||||
|
// S3APICredentials describe data fields necessary for authenticating with a
|
||||||
|
// garage S3 API endpoint.
|
||||||
|
type S3APICredentials struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Secret string `yaml:"secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewS3APIClient returns a minio client configured to use the given garage S3 API
|
||||||
|
// endpoint.
|
||||||
|
func NewS3APIClient(addr string, creds S3APICredentials) (S3APIClient, error) {
|
||||||
|
return minio.New(addr, &minio.Options{
|
||||||
|
Creds: credentials.NewStaticV4(creds.ID, creds.Secret, ""),
|
||||||
|
Region: Region,
|
||||||
|
})
|
||||||
|
}
|
13
go-workspace/src/garage/garage.go
Normal file
13
go-workspace/src/garage/garage.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// Package garage contains helper functions and types which are useful for
|
||||||
|
// setting up garage configs, processes, and deployments.
|
||||||
|
package garage
|
||||||
|
|
||||||
|
const (
|
||||||
|
|
||||||
|
// Region is the region which garage is configured with.
|
||||||
|
Region = "garage"
|
||||||
|
|
||||||
|
// GlobalBucket is the name of the global garage bucket which is
|
||||||
|
// accessible to all hosts in the network.
|
||||||
|
GlobalBucket = "cryptic-net-global"
|
||||||
|
)
|
41
go-workspace/src/garage/infinite_reader.go
Normal file
41
go-workspace/src/garage/infinite_reader.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package garage
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type infiniteReader struct {
|
||||||
|
b []byte
|
||||||
|
i int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInfiniteReader returns a reader which will produce the given bytes in
|
||||||
|
// repetition. len(b) must be greater than 0.
|
||||||
|
func NewInfiniteReader(b []byte) io.Reader {
|
||||||
|
|
||||||
|
if len(b) == 0 {
|
||||||
|
panic("len(b) must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &infiniteReader{b: b}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *infiniteReader) Read(b []byte) (int, error) {
|
||||||
|
|
||||||
|
// here, have a puzzle
|
||||||
|
|
||||||
|
var n int
|
||||||
|
|
||||||
|
for {
|
||||||
|
|
||||||
|
n += copy(b[n:], r.b[r.i:])
|
||||||
|
|
||||||
|
if r.i > 0 {
|
||||||
|
n += copy(b[n:], r.b[:r.i])
|
||||||
|
}
|
||||||
|
|
||||||
|
r.i = (r.i + n) % len(r.b)
|
||||||
|
|
||||||
|
if n >= len(b) {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
101
go-workspace/src/garage/infinite_reader_test.go
Normal file
101
go-workspace/src/garage/infinite_reader_test.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package garage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInfiniteReader(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
in []byte
|
||||||
|
size int
|
||||||
|
exp []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
in: []byte("a"),
|
||||||
|
size: 1,
|
||||||
|
exp: []string{"a"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: []byte("ab"),
|
||||||
|
size: 1,
|
||||||
|
exp: []string{"a", "b"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: []byte("ab"),
|
||||||
|
size: 2,
|
||||||
|
exp: []string{"ab"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: []byte("ab"),
|
||||||
|
size: 3,
|
||||||
|
exp: []string{"aba", "bab"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: []byte("ab"),
|
||||||
|
size: 4,
|
||||||
|
exp: []string{"abab"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: []byte("ab"),
|
||||||
|
size: 5,
|
||||||
|
exp: []string{"ababa", "babab"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: []byte("abc"),
|
||||||
|
size: 1,
|
||||||
|
exp: []string{"a", "b", "c"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: []byte("abc"),
|
||||||
|
size: 2,
|
||||||
|
exp: []string{"ab", "ca", "bc"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: []byte("abc"),
|
||||||
|
size: 3,
|
||||||
|
exp: []string{"abc"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: []byte("abc"),
|
||||||
|
size: 4,
|
||||||
|
exp: []string{"abca", "bcab", "cabc"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: []byte("abc"),
|
||||||
|
size: 5,
|
||||||
|
exp: []string{"abcab", "cabca", "bcabc"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||||
|
|
||||||
|
r := NewInfiniteReader(test.in)
|
||||||
|
buf := make([]byte, test.size)
|
||||||
|
|
||||||
|
assertRead := func(expBuf []byte) {
|
||||||
|
|
||||||
|
n, err := r.Read(buf)
|
||||||
|
|
||||||
|
if !bytes.Equal(buf, expBuf) {
|
||||||
|
t.Fatalf("expected bytes %q, got %q", expBuf, buf)
|
||||||
|
|
||||||
|
} else if n != len(buf) {
|
||||||
|
t.Fatalf("expected n %d, got %d", len(buf), n)
|
||||||
|
|
||||||
|
} else if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
for _, expStr := range test.exp {
|
||||||
|
assertRead([]byte(expStr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
66
go-workspace/src/garage/peer.go
Normal file
66
go-workspace/src/garage/peer.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package garage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Peer describes all information necessary to connect to a given garage node.
|
||||||
|
type Peer struct {
|
||||||
|
IP string
|
||||||
|
RPCPort int
|
||||||
|
S3APIPort int
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCPeerKey deterministically generates a public/private keys which can
|
||||||
|
// be used as a garage node key.
|
||||||
|
//
|
||||||
|
// DANGER: This function will deterministically produce public/private keys
|
||||||
|
// given some arbitrary input. This is NEVER what you want. It's only being used
|
||||||
|
// in cryptic-net for a very specific purpose for which I think it's ok and is
|
||||||
|
// very necessary, and people are probably _still_ going to yell at me.
|
||||||
|
//
|
||||||
|
func (p Peer) RPCPeerKey() (pubKey, privKey []byte) {
|
||||||
|
input := []byte(net.JoinHostPort(p.IP, strconv.Itoa(p.RPCPort)))
|
||||||
|
|
||||||
|
// Append the length of the input to the input, so that the input "foo"
|
||||||
|
// doesn't generate the same key as the input "foofoo".
|
||||||
|
input = strconv.AppendInt(input, int64(len(input)), 10)
|
||||||
|
|
||||||
|
pubKey, privKey, err := ed25519.GenerateKey(NewInfiniteReader(input))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pubKey, privKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCPeerID returns the peer ID of the garage node for use in communicating
|
||||||
|
// over RPC.
|
||||||
|
//
|
||||||
|
// DANGER: See warning on RPCPeerKey.
|
||||||
|
func (p Peer) RPCPeerID() string {
|
||||||
|
pubKey, _ := p.RPCPeerKey()
|
||||||
|
return hex.EncodeToString(pubKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCAddr returns the address of the peer's RPC port.
|
||||||
|
func (p Peer) RPCAddr() string {
|
||||||
|
return net.JoinHostPort(p.IP, strconv.Itoa(p.RPCPort))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCPeerAddr returns the full peer address (e.g. "id@ip:port") of the garage
|
||||||
|
// node for use in communicating over RPC.
|
||||||
|
//
|
||||||
|
// DANGER: See warning on RPCPeerKey.
|
||||||
|
func (p Peer) RPCPeerAddr() string {
|
||||||
|
return fmt.Sprintf("%s@%s", p.RPCPeerID(), p.RPCAddr())
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3APIAddr returns the address of the peer's S3 API port.
|
||||||
|
func (p Peer) S3APIAddr() string {
|
||||||
|
return net.JoinHostPort(p.IP, strconv.Itoa(p.S3APIPort))
|
||||||
|
}
|
76
go-workspace/src/garage/tpl.go
Normal file
76
go-workspace/src/garage/tpl.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package garage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GarageTomlData describes all fields needed for rendering a garage.toml
|
||||||
|
// file via this package's template.
|
||||||
|
type GarageTomlData struct {
|
||||||
|
MetaPath string
|
||||||
|
DataPath string
|
||||||
|
|
||||||
|
RPCSecret string
|
||||||
|
|
||||||
|
RPCAddr string
|
||||||
|
APIAddr string
|
||||||
|
WebAddr string
|
||||||
|
|
||||||
|
BootstrapPeers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var garageTomlTpl = template.Must(template.New("").Parse(`
|
||||||
|
|
||||||
|
metadata_dir = "{{ .MetaPath }}"
|
||||||
|
data_dir = "{{ .DataPath }}"
|
||||||
|
|
||||||
|
replication_mode = "3"
|
||||||
|
|
||||||
|
rpc_secret = "{{ .RPCSecret }}"
|
||||||
|
rpc_bind_addr = "{{ .RPCAddr }}"
|
||||||
|
rpc_public_addr = "{{ .RPCAddr }}"
|
||||||
|
|
||||||
|
bootstrap_peers = [{{- range .BootstrapPeers }}
|
||||||
|
"{{ . }}",
|
||||||
|
{{ end -}}]
|
||||||
|
|
||||||
|
[s3_api]
|
||||||
|
api_bind_addr = "{{ .APIAddr }}"
|
||||||
|
s3_region = "garage"
|
||||||
|
|
||||||
|
[s3_web]
|
||||||
|
bind_addr = "{{ .WebAddr }}"
|
||||||
|
root_domain = ".example.com"
|
||||||
|
|
||||||
|
`))
|
||||||
|
|
||||||
|
// RenderGarageToml renders a garage.toml using the given data into the writer.
|
||||||
|
func RenderGarageToml(into io.Writer, data GarageTomlData) error {
|
||||||
|
return garageTomlTpl.Execute(into, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteGarageTomlFile renders a garage.toml using the given data to a new file
|
||||||
|
// at the given path.
|
||||||
|
func WriteGarageTomlFile(path string, data GarageTomlData) error {
|
||||||
|
|
||||||
|
file, err := os.OpenFile(
|
||||||
|
path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
err = RenderGarageToml(file, data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("rendering template to file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,25 +1,24 @@
|
|||||||
module isle
|
module cryptic-net
|
||||||
|
|
||||||
go 1.22
|
go 1.17
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.betamike.com/micropelago/pmux v0.0.0-20241029124408-55bc7097f7b8
|
|
||||||
dev.mediocregopher.com/mediocre-go-lib.git v0.0.0-20241023182613-55984cdf5233
|
|
||||||
github.com/adrg/xdg v0.4.0
|
github.com/adrg/xdg v0.4.0
|
||||||
github.com/jxskiss/base62 v1.1.0
|
github.com/cryptic-io/pmux v0.0.0-20220630194257-a451ee620c83
|
||||||
|
github.com/imdario/mergo v0.3.12
|
||||||
github.com/minio/minio-go/v7 v7.0.28
|
github.com/minio/minio-go/v7 v7.0.28
|
||||||
github.com/sergi/go-diff v1.3.1
|
github.com/nlepage/go-tarfs v1.1.0
|
||||||
|
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||||
github.com/slackhq/nebula v1.6.1
|
github.com/slackhq/nebula v1.6.1
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/stretchr/testify v1.9.0
|
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29
|
||||||
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.6 // indirect
|
||||||
github.com/google/uuid v1.1.1 // indirect
|
github.com/google/uuid v1.1.1 // indirect
|
||||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
@ -31,12 +30,12 @@ require (
|
|||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
github.com/rs/xid v1.2.1 // indirect
|
github.com/rs/xid v1.2.1 // indirect
|
||||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
github.com/smartystreets/assertions v1.13.0 // indirect
|
github.com/smartystreets/assertions v1.13.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect
|
github.com/tklauser/numcpus v0.4.0 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||||
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect
|
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect
|
||||||
golang.org/x/sys v0.0.0-20220406155245-289d7a0edf71 // indirect
|
golang.org/x/sys v0.0.0-20220406155245-289d7a0edf71 // indirect
|
||||||
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 // indirect
|
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 // indirect
|
@ -1,35 +1,34 @@
|
|||||||
code.betamike.com/micropelago/pmux v0.0.0-20241029124408-55bc7097f7b8 h1:DTAMr60/y9ZtpMORg50LYGpxyvA9+3UFKVW4mb4CwAE=
|
|
||||||
code.betamike.com/micropelago/pmux v0.0.0-20241029124408-55bc7097f7b8/go.mod h1:WlEWacLREVfIQl1IlBjKzuDgL56DFRvyl7YiL5gGP4w=
|
|
||||||
dev.mediocregopher.com/mediocre-go-lib.git v0.0.0-20241023182613-55984cdf5233 h1:Ea4HixNfDNDPh7zMngPpEeDf8gpociSPEROBFBedqIY=
|
|
||||||
dev.mediocregopher.com/mediocre-go-lib.git v0.0.0-20241023182613-55984cdf5233/go.mod h1:nP+AtQWrc3k5qq5y3ABiBLkOfUPlk/FO9fpTFpF+jgs=
|
|
||||||
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
||||||
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
||||||
|
github.com/cryptic-io/pmux v0.0.0-20220630194257-a451ee620c83 h1:0W4j4Rg0hMPyUpuvZuj1u9Gmgah9SSsfAUeNvTxo2BA=
|
||||||
|
github.com/cryptic-io/pmux v0.0.0-20220630194257-a451ee620c83/go.mod h1:fg/CCfMpcbqO7/iqLjoYskopsVO3JqdD3Oij83+YXSI=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||||
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||||
|
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||||
|
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
|
|
||||||
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
|
|
||||||
github.com/klauspost/compress v1.13.5 h1:9O69jUPDcsT9fEm74W92rZL9FQY7rCdaXVneq+yyzl4=
|
github.com/klauspost/compress v1.13.5 h1:9O69jUPDcsT9fEm74W92rZL9FQY7rCdaXVneq+yyzl4=
|
||||||
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
|
github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
|
||||||
github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
|
github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
@ -48,12 +47,14 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/nlepage/go-tarfs v1.1.0 h1:bsACOiZMB/zFjYG/sE01070i9Fl26MnRpw0L6WuyfVs=
|
||||||
|
github.com/nlepage/go-tarfs v1.1.0/go.mod h1:IhxRcLhLkawBetnwu/JNuoPkq/6cclAllhgEa6SmzS8=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
|
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
|
||||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
github.com/slackhq/nebula v1.6.1 h1:/OCTR3abj0Sbf2nGoLUrdDXImrCv0ZVFpVPP5qa0DsM=
|
github.com/slackhq/nebula v1.6.1 h1:/OCTR3abj0Sbf2nGoLUrdDXImrCv0ZVFpVPP5qa0DsM=
|
||||||
@ -61,44 +62,45 @@ github.com/slackhq/nebula v1.6.1/go.mod h1:UmkqnXe4O53QwToSl/gG7sM4BroQwAB7dd4hU
|
|||||||
github.com/smartystreets/assertions v1.13.0 h1:Dx1kYM01xsSqKPno3aqLnrwac2LetPvN23diwyr69Qs=
|
github.com/smartystreets/assertions v1.13.0 h1:Dx1kYM01xsSqKPno3aqLnrwac2LetPvN23diwyr69Qs=
|
||||||
github.com/smartystreets/assertions v1.13.0/go.mod h1:wDmR7qL282YbGsPy6H/yAsesrxfxaaSlJazyFLYVFx8=
|
github.com/smartystreets/assertions v1.13.0/go.mod h1:wDmR7qL282YbGsPy6H/yAsesrxfxaaSlJazyFLYVFx8=
|
||||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ=
|
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
|
||||||
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
|
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
|
||||||
|
github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o=
|
||||||
|
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o=
|
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o=
|
||||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
|
||||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
|
||||||
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0=
|
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0=
|
||||||
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220406155245-289d7a0edf71 h1:PRD0hj6tTuUnCFD08vkvjkYFbQg/9lV8KIxe1y4/cvU=
|
golang.org/x/sys v0.0.0-20220406155245-289d7a0edf71 h1:PRD0hj6tTuUnCFD08vkvjkYFbQg/9lV8KIxe1y4/cvU=
|
||||||
golang.org/x/sys v0.0.0-20220406155245-289d7a0edf71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220406155245-289d7a0edf71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 h1:GLw7MR8AfAG2GmGcmVgObFOHXYypgGjnGno25RDwn3Y=
|
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 h1:GLw7MR8AfAG2GmGcmVgObFOHXYypgGjnGno25RDwn3Y=
|
||||||
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0=
|
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
152
go-workspace/src/nebula/nebula.go
Normal file
152
go-workspace/src/nebula/nebula.go
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
// Package nebula contains helper functions and types which are useful for
|
||||||
|
// setting up nebula configs, processes, and deployments.
|
||||||
|
package nebula
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/cert"
|
||||||
|
"golang.org/x/crypto/curve25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HostCert contains the certificate and private key files which will need to
|
||||||
|
// be present on a particular host. Each file is PEM encoded.
|
||||||
|
type HostCert struct {
|
||||||
|
CACert string
|
||||||
|
HostKey string
|
||||||
|
HostCert string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CACert contains the certificate and private files which can be used to create
|
||||||
|
// HostCerts. Each file is PEM encoded.
|
||||||
|
type CACert struct {
|
||||||
|
CACert string
|
||||||
|
CAKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHostCert generates a new key/cert for a nebula host using the CA key
|
||||||
|
// which will be found in the adminFS.
|
||||||
|
func NewHostCert(
|
||||||
|
caCert CACert, hostName, hostIP string,
|
||||||
|
) (
|
||||||
|
HostCert, error,
|
||||||
|
) {
|
||||||
|
|
||||||
|
// The logic here is largely based on
|
||||||
|
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go
|
||||||
|
|
||||||
|
caKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCert.CAKey))
|
||||||
|
if err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("unmarshaling ca.key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caCrt, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCert.CACert))
|
||||||
|
if err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("unmarshaling ca.crt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
issuer, err := caCrt.Sha256Sum()
|
||||||
|
if err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("getting ca.crt issuer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expireAt := caCrt.Details.NotAfter.Add(-1 * time.Second)
|
||||||
|
|
||||||
|
ip := net.ParseIP(hostIP)
|
||||||
|
if ip == nil {
|
||||||
|
return HostCert{}, fmt.Errorf("invalid host ip %q", hostIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
ipNet := &net.IPNet{
|
||||||
|
IP: ip,
|
||||||
|
Mask: ipCIDRMask,
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostPub, hostKey []byte
|
||||||
|
{
|
||||||
|
var pubkey, privkey [32]byte
|
||||||
|
if _, err := io.ReadFull(rand.Reader, privkey[:]); err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("reading random bytes to form private key: %w", err)
|
||||||
|
}
|
||||||
|
curve25519.ScalarBaseMult(&pubkey, &privkey)
|
||||||
|
hostPub, hostKey = pubkey[:], privkey[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
hostCrt := cert.NebulaCertificate{
|
||||||
|
Details: cert.NebulaCertificateDetails{
|
||||||
|
Name: hostName,
|
||||||
|
Ips: []*net.IPNet{ipNet},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: expireAt,
|
||||||
|
PublicKey: hostPub,
|
||||||
|
IsCA: false,
|
||||||
|
Issuer: issuer,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hostCrt.CheckRootConstrains(caCrt); err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("validating certificate constraints: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hostCrt.Sign(caKey); err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("signing host cert with ca.key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
|
||||||
|
|
||||||
|
hostCrtPEM, err := hostCrt.MarshalToPEM()
|
||||||
|
if err != nil {
|
||||||
|
return HostCert{}, fmt.Errorf("marshalling host.crt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return HostCert{
|
||||||
|
CACert: caCert.CACert,
|
||||||
|
HostKey: string(hostKeyPEM),
|
||||||
|
HostCert: string(hostCrtPEM),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCACert generates a CACert. The domain should be the network's root domain,
|
||||||
|
// and is included in the signing certificate's Name field.
|
||||||
|
func NewCACert(domain string, ipCIDRS []*net.IPNet) (CACert, error) {
|
||||||
|
|
||||||
|
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("generating ed25519 key: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
expireAt := now.Add(2 * 365 * 24 * time.Hour)
|
||||||
|
|
||||||
|
caCrt := cert.NebulaCertificate{
|
||||||
|
Details: cert.NebulaCertificateDetails{
|
||||||
|
Name: fmt.Sprintf("%s cryptic-net root cert", domain),
|
||||||
|
Ips: ipCIDRS,
|
||||||
|
NotBefore: now,
|
||||||
|
NotAfter: expireAt,
|
||||||
|
PublicKey: pubKey,
|
||||||
|
IsCA: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := caCrt.Sign(privKey); err != nil {
|
||||||
|
return CACert{}, fmt.Errorf("signing caCrt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caKeyPEM := cert.MarshalEd25519PrivateKey(privKey)
|
||||||
|
|
||||||
|
caCrtPem, err := caCrt.MarshalToPEM()
|
||||||
|
if err != nil {
|
||||||
|
return CACert{}, fmt.Errorf("marshaling caCrt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return CACert{
|
||||||
|
CACert: string(caCrtPem),
|
||||||
|
CAKey: string(caKeyPEM),
|
||||||
|
}, nil
|
||||||
|
}
|
99
go-workspace/src/proc_lock.go
Normal file
99
go-workspace/src/proc_lock.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package crypticnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/shirou/gopsutil/process"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errDaemonNotRunning = errors.New("no cryptic-net daemon process running")
|
||||||
|
|
||||||
|
// ProcLock is used to lock a process.
|
||||||
|
type ProcLock interface {
|
||||||
|
|
||||||
|
// WriteLock creates a new lock, or errors if the lock is alread held.
|
||||||
|
WriteLock() error
|
||||||
|
|
||||||
|
// AssertLock returns an error if the lock already exists.
|
||||||
|
AssertLock() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type procLock struct {
|
||||||
|
dir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProcLock returns a ProcLock which will use a file in the given directory
|
||||||
|
// to lock the process.
|
||||||
|
func NewProcLock(dir string) ProcLock {
|
||||||
|
return &procLock{dir: dir}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pl *procLock) path() string {
|
||||||
|
return filepath.Join(pl.dir, "lock")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pl *procLock) WriteLock() error {
|
||||||
|
|
||||||
|
lockFilePath := pl.path()
|
||||||
|
|
||||||
|
lockFile, err := os.OpenFile(
|
||||||
|
lockFilePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0400,
|
||||||
|
)
|
||||||
|
|
||||||
|
if errors.Is(err, os.ErrExist) {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"lock file %q already exists, if the cryptic-net daemon is not already running you can safely delete this file",
|
||||||
|
lockFilePath,
|
||||||
|
)
|
||||||
|
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("opening lockfile %q: %w", lockFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer lockFile.Close()
|
||||||
|
|
||||||
|
if _, err := fmt.Fprintf(lockFile, "%d\n", os.Getpid()); err != nil {
|
||||||
|
return fmt.Errorf("writing pid to %q: %w", lockFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks that the lock file exists and that the process which created it also
|
||||||
|
// still exists.
|
||||||
|
func (pl *procLock) AssertLock() error {
|
||||||
|
|
||||||
|
lockFilePath := pl.path()
|
||||||
|
|
||||||
|
lockFile, err := os.Open(lockFilePath)
|
||||||
|
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return errDaemonNotRunning
|
||||||
|
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("checking lock file %q: %w", lockFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer lockFile.Close()
|
||||||
|
|
||||||
|
var pid int32
|
||||||
|
|
||||||
|
if _, err := fmt.Fscan(lockFile, &pid); err != nil {
|
||||||
|
return fmt.Errorf("scanning pid from lock file %q: %w", lockFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
procExists, err := process.PidExists(pid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("checking if process %d exists: %w", pid, err)
|
||||||
|
|
||||||
|
} else if !procExists {
|
||||||
|
return errDaemonNotRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
24
go-workspace/src/tarutil/tarutil.go
Normal file
24
go-workspace/src/tarutil/tarutil.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// Package tarutil implements utilities which are useful for interacting with
|
||||||
|
// tar and tgz files.
|
||||||
|
package tarutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
|
"github.com/nlepage/go-tarfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FSFromReader returns a FS instance which will read the contents of a tgz
|
||||||
|
// file from the given Reader.
|
||||||
|
func FSFromReader(r io.Reader) (fs.FS, error) {
|
||||||
|
gf, err := gzip.NewReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("un-gziping: %w", err)
|
||||||
|
}
|
||||||
|
defer gf.Close()
|
||||||
|
|
||||||
|
return tarfs.New(gf)
|
||||||
|
}
|
112
go-workspace/src/tarutil/tgz_writer.go
Normal file
112
go-workspace/src/tarutil/tgz_writer.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package tarutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TGZWriter is a utility for writing tgz files. If an internal error is
|
||||||
|
// encountered by any method then all subsequent methods will be no-ops, and
|
||||||
|
// Close() will return that error (after closing out resources).
|
||||||
|
type TGZWriter struct {
|
||||||
|
gzipW *gzip.Writer
|
||||||
|
tarW *tar.Writer
|
||||||
|
err error
|
||||||
|
|
||||||
|
dirsWritten map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTGZWriter initializes and returns a new instance of TGZWriter which will
|
||||||
|
// write all data to the given io.Writer.
|
||||||
|
func NewTGZWriter(w io.Writer) *TGZWriter {
|
||||||
|
gzipW := gzip.NewWriter(w)
|
||||||
|
tarW := tar.NewWriter(gzipW)
|
||||||
|
return &TGZWriter{
|
||||||
|
gzipW: gzipW,
|
||||||
|
tarW: tarW,
|
||||||
|
dirsWritten: map[string]bool{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close cleans up all open resources being held by TGZWriter, and returns the
|
||||||
|
// first internal error which was encountered during its operation (if any).
|
||||||
|
func (w *TGZWriter) Close() error {
|
||||||
|
w.tarW.Close()
|
||||||
|
w.gzipW.Close()
|
||||||
|
return w.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *TGZWriter) writeDir(path string) {
|
||||||
|
|
||||||
|
if w.err != nil {
|
||||||
|
return
|
||||||
|
|
||||||
|
} else if path != "." {
|
||||||
|
w.writeDir(filepath.Dir(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == "." {
|
||||||
|
path = "./"
|
||||||
|
} else {
|
||||||
|
path = "./" + strings.TrimPrefix(path, "./")
|
||||||
|
path = path + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.dirsWritten[path] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := w.tarW.WriteHeader(&tar.Header{
|
||||||
|
Name: path,
|
||||||
|
Mode: 0700,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
w.err = fmt.Errorf("writing header for directory %q: %w", path, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.dirsWritten[path] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFile writes a file to the tgz archive. The file will automatically be
|
||||||
|
// rooted to the "." directory, and any sub-directories the file exists in
|
||||||
|
// should have already been created.
|
||||||
|
func (w *TGZWriter) WriteFile(path string, size int64, body io.Reader) {
|
||||||
|
|
||||||
|
if w.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path = "./" + strings.TrimPrefix(path, "./")
|
||||||
|
|
||||||
|
w.writeDir(filepath.Dir(path))
|
||||||
|
|
||||||
|
err := w.tarW.WriteHeader(&tar.Header{
|
||||||
|
Name: path,
|
||||||
|
Size: size,
|
||||||
|
Mode: 0400,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
w.err = fmt.Errorf("writing header for file %q: %w", path, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(w.tarW, body); err != nil {
|
||||||
|
w.err = fmt.Errorf("writing file body of file %q: %w", path, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFileBytes is a shortcut for calling WriteFile with the given byte slice
|
||||||
|
// being used as the file body.
|
||||||
|
func (w *TGZWriter) WriteFileBytes(path string, body []byte) {
|
||||||
|
bodyR := bytes.NewReader(body)
|
||||||
|
w.WriteFile(path, bodyR.Size(), bodyR)
|
||||||
|
}
|
67
go-workspace/src/yamlutil/yamlutil.go
Normal file
67
go-workspace/src/yamlutil/yamlutil.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package yamlutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadYamlFile reads the file at the given path and unmarshals it into the
|
||||||
|
// given pointer.
|
||||||
|
func LoadYamlFile(into interface{}, path string) error {
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("opening file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if err = yaml.NewDecoder(file).Decode(into); err != nil {
|
||||||
|
return fmt.Errorf("decoding yaml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteYamlFile encodes the given data as a yaml document, and writes it to the
|
||||||
|
// given file path, overwriting any previous data.
|
||||||
|
func WriteYamlFile(data interface{}, path string) error {
|
||||||
|
|
||||||
|
file, err := os.OpenFile(
|
||||||
|
path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("opening file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = yaml.NewEncoder(file).Encode(data)
|
||||||
|
|
||||||
|
file.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing/encoding file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadYamlFSFile is like LoadYamlFile, but it will read the file from the given
|
||||||
|
// fs.FS instance.
|
||||||
|
func LoadYamlFSFile(into interface{}, f fs.FS, path string) error {
|
||||||
|
|
||||||
|
body, err := fs.ReadFile(f, path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading file from FS: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(body, into); err != nil {
|
||||||
|
return fmt.Errorf("yaml unmarshaling: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,4 +0,0 @@
|
|||||||
# https://github.com/golangci/golangci-lint/issues/4733
|
|
||||||
linters-settings:
|
|
||||||
errcheck:
|
|
||||||
ignore : ""
|
|
@ -1,195 +0,0 @@
|
|||||||
// Package bootstrap deals with the parsing and creation of bootstrap.json
|
|
||||||
// files. It also contains some helpers which rely on bootstrap data.
|
|
||||||
package bootstrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha512"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"isle/nebula"
|
|
||||||
"isle/toolkit"
|
|
||||||
"maps"
|
|
||||||
"net/netip"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StateDirPath returns the path within the user's state directory where the
|
|
||||||
// bootstrap file is stored.
|
|
||||||
func StateDirPath(dataDirPath string) string {
|
|
||||||
return filepath.Join(dataDirPath, "bootstrap.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreationParams are general parameters used when creating a new network. These
|
|
||||||
// are available to all hosts within the network via their bootstrap files.
|
|
||||||
type CreationParams struct {
|
|
||||||
ID string
|
|
||||||
Name string
|
|
||||||
Domain string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCreationParams instantiates and returns a CreationParams.
|
|
||||||
func NewCreationParams(name, domain string) CreationParams {
|
|
||||||
return CreationParams{
|
|
||||||
ID: toolkit.RandStr(32),
|
|
||||||
Name: name,
|
|
||||||
Domain: domain,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Annotate implements the mctx.Annotator interface.
|
|
||||||
func (p CreationParams) Annotate(aa mctx.Annotations) {
|
|
||||||
aa["networkID"] = p.ID
|
|
||||||
aa["networkName"] = p.Name
|
|
||||||
aa["networkDomain"] = p.Domain
|
|
||||||
}
|
|
||||||
|
|
||||||
// Matches returns true if the given string matches some aspect of the
|
|
||||||
// CreationParams.
|
|
||||||
func (p CreationParams) Matches(str string) bool {
|
|
||||||
if strings.HasPrefix(p.ID, str) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.EqualFold(p.Name, str) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.EqualFold(p.Domain, str) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conflicts returns true if either CreationParams has some parameter which
|
|
||||||
// overlaps with that of the other.
|
|
||||||
func (p CreationParams) Conflicts(p2 CreationParams) bool {
|
|
||||||
if p.ID == p2.ID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.EqualFold(p.Name, p2.Name) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.EqualFold(p.Domain, p2.Domain) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bootstrap contains all information which is needed by a host daemon to join a
|
|
||||||
// network on boot.
|
|
||||||
type Bootstrap struct {
|
|
||||||
NetworkCreationParams CreationParams
|
|
||||||
CAPublicCredentials nebula.CAPublicCredentials
|
|
||||||
|
|
||||||
PrivateCredentials nebula.HostPrivateCredentials
|
|
||||||
HostAssigned `json:"-"`
|
|
||||||
SignedHostAssigned nebula.Signed[HostAssigned] // signed by CA
|
|
||||||
|
|
||||||
Hosts map[nebula.HostName]Host
|
|
||||||
}
|
|
||||||
|
|
||||||
// New initializes and returns a new Bootstrap file for a new host.
|
|
||||||
//
|
|
||||||
// TODO in the resulting bootstrap only include this host and hosts which are
|
|
||||||
// necessary for connecting to nebula/garage. Remember to immediately re-poll
|
|
||||||
// garage for the full hosts list during network joining.
|
|
||||||
func New(
|
|
||||||
caCreds nebula.CACredentials,
|
|
||||||
adminCreationParams CreationParams,
|
|
||||||
existingHosts map[nebula.HostName]Host,
|
|
||||||
name nebula.HostName,
|
|
||||||
ip netip.Addr,
|
|
||||||
) (
|
|
||||||
Bootstrap, error,
|
|
||||||
) {
|
|
||||||
hostPubCreds, hostPrivCreds, err := nebula.NewHostCredentials(
|
|
||||||
caCreds, name, ip,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return Bootstrap{}, fmt.Errorf("generating host credentials: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assigned := HostAssigned{
|
|
||||||
Name: name,
|
|
||||||
PublicCredentials: hostPubCreds,
|
|
||||||
}
|
|
||||||
|
|
||||||
signedAssigned, err := nebula.Sign(assigned, caCreds.SigningPrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return Bootstrap{}, fmt.Errorf("signing assigned fields: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
existingHosts = maps.Clone(existingHosts)
|
|
||||||
existingHosts[name] = Host{
|
|
||||||
HostAssigned: assigned,
|
|
||||||
}
|
|
||||||
|
|
||||||
return Bootstrap{
|
|
||||||
NetworkCreationParams: adminCreationParams,
|
|
||||||
CAPublicCredentials: caCreds.Public,
|
|
||||||
PrivateCredentials: hostPrivCreds,
|
|
||||||
HostAssigned: assigned,
|
|
||||||
SignedHostAssigned: signedAssigned,
|
|
||||||
Hosts: existingHosts,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON implements the json.Unmarshaler interface. It will
|
|
||||||
// automatically populate the HostAssigned field by unwrapping the
|
|
||||||
// SignedHostAssigned field.
|
|
||||||
func (b *Bootstrap) UnmarshalJSON(data []byte) error {
|
|
||||||
type inner Bootstrap
|
|
||||||
|
|
||||||
err := json.Unmarshal(data, (*inner)(b))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
b.HostAssigned, err = b.SignedHostAssigned.Unwrap(
|
|
||||||
b.CAPublicCredentials.SigningKey,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unwrapping HostAssigned: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ThisHost is a shortcut for b.Hosts[b.HostName], but will panic if the
|
|
||||||
// HostName isn't found in the Hosts map.
|
|
||||||
func (b Bootstrap) ThisHost() Host {
|
|
||||||
host, ok := b.Hosts[b.Name]
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Sprintf("hostname %q not defined in bootstrap's hosts", b.Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
return host
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash returns a deterministic hash of the given hosts map.
|
|
||||||
func HostsHash(hostsMap map[nebula.HostName]Host) ([]byte, error) {
|
|
||||||
|
|
||||||
hosts := make([]Host, 0, len(hostsMap))
|
|
||||||
for _, host := range hostsMap {
|
|
||||||
hosts = append(hosts, host)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(hosts, func(i, j int) bool {
|
|
||||||
return hosts[i].Name < hosts[j].Name
|
|
||||||
})
|
|
||||||
|
|
||||||
h := sha512.New()
|
|
||||||
if err := json.NewEncoder(h).Encode(hosts); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.Sum(nil), nil
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
package bootstrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"isle/garage"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GarageNodes returns a Node for each known garage instance in the network.
|
|
||||||
func (b Bootstrap) GarageNodes() []garage.RemoteNode {
|
|
||||||
var nodes []garage.RemoteNode
|
|
||||||
for _, host := range b.Hosts {
|
|
||||||
nodes = append(nodes, host.GarageNodes()...)
|
|
||||||
}
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChooseGarageNode returns a RemoteNode for a garage instance from the network.
|
|
||||||
// It will prefer a garage instance on this particular host, if there is one,
|
|
||||||
// but will otherwise return a random endpoint.
|
|
||||||
func (b Bootstrap) ChooseGarageNode() garage.RemoteNode {
|
|
||||||
thisHost := b.ThisHost()
|
|
||||||
if len(thisHost.Garage.Instances) > 0 {
|
|
||||||
return thisHost.GarageNodes()[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, node := range b.GarageNodes() {
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
panic("no garage instances configured")
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
package bootstrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"isle/garage"
|
|
||||||
"isle/nebula"
|
|
||||||
"net/netip"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NebulaHost describes the nebula configuration of a Host which is relevant for
|
|
||||||
// other hosts to know.
|
|
||||||
type NebulaHost struct {
|
|
||||||
PublicAddr string
|
|
||||||
}
|
|
||||||
|
|
||||||
// GarageHost describes a single garage instance in the GarageHost.
|
|
||||||
type GarageHostInstance struct {
|
|
||||||
ID string
|
|
||||||
RPCPort int
|
|
||||||
S3APIPort int
|
|
||||||
}
|
|
||||||
|
|
||||||
// GarageHost describes the garage configuration of a Host which is relevant for
|
|
||||||
// other hosts to know.
|
|
||||||
type GarageHost struct {
|
|
||||||
Instances []GarageHostInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
// HostAssigned are all fields related to a host which were assigned to it by an
|
|
||||||
// admin.
|
|
||||||
type HostAssigned struct {
|
|
||||||
Name nebula.HostName
|
|
||||||
PublicCredentials nebula.HostPublicCredentials
|
|
||||||
}
|
|
||||||
|
|
||||||
// HostConfigured are all the fields a host can configure for itself.
|
|
||||||
type HostConfigured struct {
|
|
||||||
Nebula NebulaHost `json:",omitempty"`
|
|
||||||
Garage GarageHost `json:",omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthenticatedHost wraps all the data about a host which other hosts may know
|
|
||||||
// about it, such that those hosts can authenticate that the data is valid and
|
|
||||||
// approved by an admin.
|
|
||||||
type AuthenticatedHost struct {
|
|
||||||
Assigned nebula.Signed[HostAssigned] // signed by CA
|
|
||||||
Configured nebula.Signed[HostConfigured] // signed by host
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unwrap attempts to authenticate and unwrap the Host embedded in this
|
|
||||||
// instance. nebula.ErrInvalidSignature is returned if any signatures are
|
|
||||||
// invalid.
|
|
||||||
func (ah AuthenticatedHost) Unwrap(caCreds nebula.CAPublicCredentials) (Host, error) {
|
|
||||||
assigned, err := ah.Assigned.Unwrap(caCreds.SigningKey)
|
|
||||||
if err != nil {
|
|
||||||
return Host{}, fmt.Errorf("unwrapping assigned fields using CA public key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
configured, err := ah.Configured.Unwrap(assigned.PublicCredentials.SigningKey)
|
|
||||||
if err != nil {
|
|
||||||
return Host{}, fmt.Errorf("unwrapping configured fields using host public key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Host{assigned, configured}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Host contains all data bout a host which other hosts may know about it.
|
|
||||||
//
|
|
||||||
// A Host should only be obtained over the network as an AuthenticatedHost, and
|
|
||||||
// subsequently Unwrapped.
|
|
||||||
type Host struct {
|
|
||||||
HostAssigned
|
|
||||||
HostConfigured
|
|
||||||
}
|
|
||||||
|
|
||||||
// IP returns the IP address encoded in the Host's nebula certificate, or panics
|
|
||||||
// if there is an error.
|
|
||||||
//
|
|
||||||
// This assumes that the Host and its data has already been verified against the
|
|
||||||
// CA signing key.
|
|
||||||
func (h Host) IP() netip.Addr {
|
|
||||||
cert := h.PublicCredentials.Cert.Unwrap()
|
|
||||||
if len(cert.Details.Ips) == 0 {
|
|
||||||
panic(fmt.Sprintf("host %q not configured with any ips: %+v", h.Name, h))
|
|
||||||
}
|
|
||||||
|
|
||||||
ip := cert.Details.Ips[0].IP
|
|
||||||
addr, ok := netip.AddrFromSlice(ip)
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Sprintf("ip %q (%#v) is not valid, somehow", ip, ip))
|
|
||||||
}
|
|
||||||
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
|
|
||||||
// GarageNodes returns a RemoteNode for each garage instance advertised by this
|
|
||||||
// Host.
|
|
||||||
func (h Host) GarageNodes() []garage.RemoteNode {
|
|
||||||
var nodes []garage.RemoteNode
|
|
||||||
for _, instance := range h.Garage.Instances {
|
|
||||||
nodes = append(nodes, garage.RemoteNode{
|
|
||||||
ID: instance.ID,
|
|
||||||
IP: h.IP().String(),
|
|
||||||
RPCPort: instance.RPCPort,
|
|
||||||
S3APIPort: instance.S3APIPort,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nodes
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"isle/bootstrap"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (ctx subCmdCtx) getHosts() ([]bootstrap.Host, error) {
|
|
||||||
return ctx.getDaemonRPC().GetHosts(ctx)
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"isle/daemon"
|
|
||||||
"isle/daemon/daecommon"
|
|
||||||
"isle/daemon/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO it would be good to have an `isle daemon config-check` kind of command,
|
|
||||||
// which could be run prior to a systemd service restart to make sure we don't
|
|
||||||
// restart the service into a configuration that will definitely fail.
|
|
||||||
|
|
||||||
var subCmdDaemon = subCmd{
|
|
||||||
name: "daemon",
|
|
||||||
descr: "Runs the isle daemon (Default if no sub-command given)",
|
|
||||||
noNetwork: true,
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
daemonConfigPath := ctx.flags.StringP(
|
|
||||||
"config-path", "c", "",
|
|
||||||
"Optional path to a daemon.yml file to load configuration from.",
|
|
||||||
)
|
|
||||||
|
|
||||||
dumpConfig := ctx.flags.Bool(
|
|
||||||
"dump-config", false,
|
|
||||||
"Write the default configuration file to stdout and exit.",
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx, err := ctx.withParsedFlags()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *dumpConfig {
|
|
||||||
return daecommon.CopyDefaultConfig(os.Stdout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO check that daemon is either running as root, or that the
|
|
||||||
// required linux capabilities are set.
|
|
||||||
// TODO check that the tun module is loaded (for nebula).
|
|
||||||
|
|
||||||
daemonConfig, err := daecommon.LoadConfig(*daemonConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("loading daemon config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
networkLoader, err := network.NewLoader(
|
|
||||||
ctx,
|
|
||||||
ctx.logger.WithNamespace("loader"),
|
|
||||||
envBinDirPath,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("instantiating network loader: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
daemonInst, err := daemon.New(
|
|
||||||
ctx, ctx.logger, networkLoader, daemonConfig,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("starting daemon: %w", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
ctx.logger.Info(ctx, "Stopping child processes")
|
|
||||||
if err := daemonInst.Shutdown(); err != nil {
|
|
||||||
ctx.logger.Error(ctx, "Shutting down daemon cleanly failed, there may be orphaned child processes", err)
|
|
||||||
}
|
|
||||||
ctx.logger.Info(ctx, "Child processes successfully stopped")
|
|
||||||
}()
|
|
||||||
|
|
||||||
{
|
|
||||||
logger := ctx.logger.WithNamespace("http")
|
|
||||||
httpSrv, err := newHTTPServer(
|
|
||||||
ctx, logger, daemonInst,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("starting HTTP server: %w", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
// see comment in daemonInst shutdown logic regarding background
|
|
||||||
// context.
|
|
||||||
logger.Info(ctx, "Shutting down HTTP socket")
|
|
||||||
if err := httpSrv.Shutdown(context.Background()); err != nil {
|
|
||||||
logger.Error(ctx, "Failed to cleanly shutdown http server", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
<-ctx.Done()
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"isle/daemon"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
|
|
||||||
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
const daemonHTTPRPCPath = "/rpc/v0.json"
|
|
||||||
|
|
||||||
func newHTTPServer(
|
|
||||||
ctx context.Context, logger *mlog.Logger, daemonInst *daemon.Daemon,
|
|
||||||
) (
|
|
||||||
*http.Server, error,
|
|
||||||
) {
|
|
||||||
socketPath := daemon.HTTPSocketPath()
|
|
||||||
ctx = mctx.Annotate(ctx, "socketPath", socketPath)
|
|
||||||
|
|
||||||
if err := os.Remove(socketPath); errors.Is(err, fs.ErrNotExist) {
|
|
||||||
// No problem
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"removing %q prior to listening: %w", socketPath, err,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
logger.WarnString(
|
|
||||||
ctx, "Deleted existing socket file prior to listening, it's possible a previous daemon failed to shutdown gracefully",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
l, err := net.Listen("unix", socketPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"listening on socket %q: %w", socketPath, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Chmod(socketPath, 0660); err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"setting permissions of %q to 0660: %w", socketPath, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info(ctx, "HTTP server socket created")
|
|
||||||
httpMux := http.NewServeMux()
|
|
||||||
httpMux.Handle(daemonHTTPRPCPath, daemonInst.HTTPRPCHandler())
|
|
||||||
|
|
||||||
srv := &http.Server{Handler: httpMux}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err := srv.Serve(l); !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
logger.Fatal(ctx, "HTTP server unexpectedly shut down", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return srv, nil
|
|
||||||
}
|
|
@ -1,64 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"isle/nebula"
|
|
||||||
"isle/toolkit"
|
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type textUnmarshaler[T any] interface {
|
|
||||||
encoding.TextUnmarshaler
|
|
||||||
*T
|
|
||||||
}
|
|
||||||
|
|
||||||
type textUnmarshalerFlag[T encoding.TextMarshaler, P textUnmarshaler[T]] struct {
|
|
||||||
V T
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *textUnmarshalerFlag[T, P]) Set(v string) error {
|
|
||||||
return P(&(f.V)).UnmarshalText([]byte(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *textUnmarshalerFlag[T, P]) String() string {
|
|
||||||
b, err := f.V.MarshalText()
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("calling MarshalText on %#v: %v", f.V, err))
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *textUnmarshalerFlag[T, P]) Type() string { return "string" }
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
type (
|
|
||||||
hostNameFlag = textUnmarshalerFlag[nebula.HostName, *nebula.HostName]
|
|
||||||
ipNetFlag = textUnmarshalerFlag[nebula.IPNet, *nebula.IPNet]
|
|
||||||
ipFlag = textUnmarshalerFlag[netip.Addr, *netip.Addr]
|
|
||||||
)
|
|
||||||
|
|
||||||
type logLevelFlag struct {
|
|
||||||
mlog.Level
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *logLevelFlag) Set(v string) error {
|
|
||||||
f.Level = toolkit.LogLevelFromString(v)
|
|
||||||
if f.Level == nil {
|
|
||||||
return errors.New("not a valid log level")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *logLevelFlag) String() string {
|
|
||||||
if f.Level == nil {
|
|
||||||
return "UNKNOWN"
|
|
||||||
}
|
|
||||||
return f.Level.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *logLevelFlag) Type() string { return "string" }
|
|
@ -1,164 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"isle/daemon/daecommon"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
// minio-client keeps a configuration directory which contains various pieces of
|
|
||||||
// information which may or may not be useful. Unfortunately when it initializes
|
|
||||||
// this directory it likes to print some annoying logs, so we pre-initialize in
|
|
||||||
// order to prevent it from doing so.
|
|
||||||
func initMCConfigDir(envVars daecommon.EnvVars) (string, error) {
|
|
||||||
var (
|
|
||||||
path = filepath.Join(envVars.StateDir.Path, "mc")
|
|
||||||
sharePath = filepath.Join(path, "share")
|
|
||||||
configJSONPath = filepath.Join(path, "config.json")
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := os.MkdirAll(sharePath, 0700); err != nil {
|
|
||||||
return "", fmt.Errorf("creating %q: %w", sharePath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile(configJSONPath, []byte(`{}`), 0600); err != nil {
|
|
||||||
return "", fmt.Errorf("writing %q: %w", configJSONPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdGarageMC = subCmd{
|
|
||||||
name: "mc",
|
|
||||||
descr: "Runs the mc (minio-client) binary. The isle garage can be accessed under the `garage` alias",
|
|
||||||
passthroughArgs: true,
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
keyID := ctx.flags.StringP(
|
|
||||||
"key-id", "i", "",
|
|
||||||
"Optional key ID to use, defaults to that of the shared global key",
|
|
||||||
)
|
|
||||||
|
|
||||||
keySecret := ctx.flags.StringP(
|
|
||||||
"key-secret", "s", "",
|
|
||||||
"Optional key secret to use, defaults to that of the shared global key",
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx, err := ctx.withParsedFlags()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
clientParams, err := ctx.getDaemonRPC().GetGarageClientParams(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("calling GetGarageClientParams: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s3APIAddr := clientParams.Node.S3APIAddr()
|
|
||||||
|
|
||||||
if *keyID == "" {
|
|
||||||
*keyID = clientParams.GlobalBucketS3APICredentials.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
if *keySecret == "" {
|
|
||||||
*keySecret = clientParams.GlobalBucketS3APICredentials.Secret
|
|
||||||
}
|
|
||||||
|
|
||||||
args := ctx.flags.Args()
|
|
||||||
|
|
||||||
if i := ctx.flags.ArgsLenAtDash(); i >= 0 {
|
|
||||||
args = args[i:]
|
|
||||||
}
|
|
||||||
|
|
||||||
envVars := daecommon.GetEnvVars()
|
|
||||||
|
|
||||||
configDir, err := initMCConfigDir(envVars)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("initializing minio-client config directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
args = append([]string{
|
|
||||||
binPath("mc"),
|
|
||||||
"--config-dir", configDir,
|
|
||||||
}, args...)
|
|
||||||
|
|
||||||
var (
|
|
||||||
mcHostVar = fmt.Sprintf(
|
|
||||||
"MC_HOST_garage=http://%s:%s@%s",
|
|
||||||
*keyID, *keySecret, s3APIAddr,
|
|
||||||
)
|
|
||||||
|
|
||||||
binPath = binPath("mc")
|
|
||||||
cliEnv = append(
|
|
||||||
os.Environ(),
|
|
||||||
mcHostVar,
|
|
||||||
|
|
||||||
// The garage docs say this is necessary, though nothing bad
|
|
||||||
// seems to happen if we leave it out *shrug*
|
|
||||||
"MC_REGION=garage",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := syscall.Exec(binPath, args, cliEnv); err != nil {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"calling exec(%q, %#v, %#v): %w",
|
|
||||||
binPath, args, cliEnv, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdGarageCLI = subCmd{
|
|
||||||
name: "cli",
|
|
||||||
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon",
|
|
||||||
passthroughArgs: true,
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
ctx, err := ctx.withParsedFlags()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
clientParams, err := ctx.getDaemonRPC().GetGarageClientParams(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("calling GetGarageClientParams: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if clientParams.RPCSecret == "" {
|
|
||||||
return errors.New("this host does not have the garage RPC secret")
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
binPath = binPath("garage")
|
|
||||||
args = append([]string{"garage"}, ctx.opts.args...)
|
|
||||||
cliEnv = append(
|
|
||||||
os.Environ(),
|
|
||||||
"GARAGE_RPC_HOST="+clientParams.Node.RPCNodeAddr(),
|
|
||||||
"GARAGE_RPC_SECRET="+clientParams.RPCSecret,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := syscall.Exec(binPath, args, cliEnv); err != nil {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"calling exec(%q, %#v, %#v): %w",
|
|
||||||
binPath, args, cliEnv, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdGarage = subCmd{
|
|
||||||
name: "garage",
|
|
||||||
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon",
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
return ctx.doSubCmd(
|
|
||||||
subCmdGarageCLI,
|
|
||||||
subCmdGarageMC,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,142 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"isle/bootstrap"
|
|
||||||
"isle/daemon/network"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
)
|
|
||||||
|
|
||||||
var subCmdHostCreate = subCmd{
|
|
||||||
name: "create",
|
|
||||||
descr: "Creates a new host in the network, writing its new bootstrap.json to stdout",
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
var (
|
|
||||||
hostName hostNameFlag
|
|
||||||
ip ipFlag
|
|
||||||
)
|
|
||||||
|
|
||||||
hostNameF := ctx.flags.VarPF(
|
|
||||||
&hostName,
|
|
||||||
"hostname", "n",
|
|
||||||
"Name of the host to generate bootstrap.json for",
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx.flags.VarP(&ip, "ip", "i", "IP of the new host. An available IP will be chosen if none is given.")
|
|
||||||
|
|
||||||
canCreateHosts := ctx.flags.Bool(
|
|
||||||
"can-create-hosts",
|
|
||||||
false,
|
|
||||||
"The new host should have the ability to create hosts too",
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx, err := ctx.withParsedFlags()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hostNameF.Changed {
|
|
||||||
return errors.New("--hostname is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := ctx.getDaemonRPC().CreateHost(
|
|
||||||
ctx, hostName.V, network.CreateHostOpts{
|
|
||||||
IP: ip.V,
|
|
||||||
CanCreateHosts: *canCreateHosts,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("calling CreateHost: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.NewEncoder(os.Stdout).Encode(res)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdHostList = subCmd{
|
|
||||||
name: "list",
|
|
||||||
descr: "Lists all hosts in the network, and their IPs",
|
|
||||||
do: doWithOutput(func(ctx subCmdCtx) (any, error) {
|
|
||||||
ctx, err := ctx.withParsedFlags()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostsRes, err := ctx.getHosts()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("calling GetHosts: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
type host struct {
|
|
||||||
Name string
|
|
||||||
VPN struct {
|
|
||||||
IP string
|
|
||||||
}
|
|
||||||
Storage bootstrap.GarageHost `json:",omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
hosts := make([]host, 0, len(hostsRes))
|
|
||||||
for _, h := range hostsRes {
|
|
||||||
|
|
||||||
host := host{
|
|
||||||
Name: string(h.Name),
|
|
||||||
Storage: h.Garage,
|
|
||||||
}
|
|
||||||
|
|
||||||
host.VPN.IP = h.IP().String()
|
|
||||||
|
|
||||||
hosts = append(hosts, host)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(hosts, func(i, j int) bool { return hosts[i].Name < hosts[j].Name })
|
|
||||||
|
|
||||||
return hosts, nil
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdHostRemove = subCmd{
|
|
||||||
name: "remove",
|
|
||||||
descr: "Removes a host from the network",
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
var (
|
|
||||||
hostName hostNameFlag
|
|
||||||
)
|
|
||||||
|
|
||||||
hostNameF := ctx.flags.VarPF(
|
|
||||||
&hostName,
|
|
||||||
"hostname", "n",
|
|
||||||
"Name of the host to remove",
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx, err := ctx.withParsedFlags()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hostNameF.Changed {
|
|
||||||
return errors.New("--hostname is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ctx.getDaemonRPC().RemoveHost(ctx, hostName.V); err != nil {
|
|
||||||
return fmt.Errorf("calling RemoveHost: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdHost = subCmd{
|
|
||||||
name: "host",
|
|
||||||
plural: "s",
|
|
||||||
descr: "Sub-commands having to do with configuration of hosts in the network",
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
return ctx.doSubCmd(
|
|
||||||
subCmdHostCreate,
|
|
||||||
subCmdHostRemove,
|
|
||||||
subCmdHostList,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
|
|
||||||
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getAppDirPath() string {
|
|
||||||
appDirPath := os.Getenv("APPDIR")
|
|
||||||
if appDirPath == "" {
|
|
||||||
appDirPath = "."
|
|
||||||
}
|
|
||||||
return appDirPath
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
envAppDirPath = getAppDirPath()
|
|
||||||
envBinDirPath = filepath.Join(envAppDirPath, "bin")
|
|
||||||
)
|
|
||||||
|
|
||||||
func binPath(name string) string {
|
|
||||||
return filepath.Join(envBinDirPath, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
var rootCmd = subCmd{
|
|
||||||
name: "isle",
|
|
||||||
descr: "All Isle sub-commands",
|
|
||||||
noNetwork: true,
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
return ctx.doSubCmd(
|
|
||||||
subCmdDaemon,
|
|
||||||
subCmdGarage,
|
|
||||||
subCmdHost,
|
|
||||||
subCmdNebula,
|
|
||||||
subCmdNetwork,
|
|
||||||
subCmdStorage,
|
|
||||||
subCmdVersion,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func doRootCmd(
|
|
||||||
ctx context.Context,
|
|
||||||
logger *mlog.Logger,
|
|
||||||
opts *subCmdCtxOpts,
|
|
||||||
) error {
|
|
||||||
subCmdCtx := newSubCmdCtx(ctx, logger, rootCmd, opts)
|
|
||||||
return subCmdCtx.subCmd.do(subCmdCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
logger := mlog.NewLogger(nil)
|
|
||||||
defer logger.Close()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
signalCh := make(chan os.Signal, 2)
|
|
||||||
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
sig := <-signalCh
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
ctx := mctx.Annotate(ctx, "signal", sig.String())
|
|
||||||
logger.Info(ctx, "got signal, exiting gracefully")
|
|
||||||
|
|
||||||
sig = <-signalCh
|
|
||||||
|
|
||||||
ctx = mctx.Annotate(ctx, "signal", sig.String())
|
|
||||||
logger.FatalString(ctx, "second signal received, force quitting, there may be zombie children left behind, good luck!")
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := doRootCmd(ctx, logger, nil); err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"isle/daemon"
|
|
||||||
"isle/toolkit"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type runHarness struct {
|
|
||||||
ctx context.Context
|
|
||||||
logger *mlog.Logger
|
|
||||||
daemonRPC *daemon.MockRPC
|
|
||||||
stdout *bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRunHarness(t *testing.T) *runHarness {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
ctx = context.Background()
|
|
||||||
logger = toolkit.NewTestLogger(t)
|
|
||||||
daemonRPC = daemon.NewMockRPC(t)
|
|
||||||
stdout = new(bytes.Buffer)
|
|
||||||
)
|
|
||||||
|
|
||||||
return &runHarness{ctx, logger, daemonRPC, stdout}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *runHarness) run(_ *testing.T, args ...string) error {
|
|
||||||
return doRootCmd(h.ctx, h.logger, &subCmdCtxOpts{
|
|
||||||
args: args,
|
|
||||||
daemonRPC: h.daemonRPC,
|
|
||||||
stdout: h.stdout,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *runHarness) runAssertStdout(
|
|
||||||
t *testing.T,
|
|
||||||
want any,
|
|
||||||
args ...string,
|
|
||||||
) {
|
|
||||||
var (
|
|
||||||
gotType = reflect.ValueOf(want)
|
|
||||||
got = reflect.New(gotType.Type())
|
|
||||||
)
|
|
||||||
|
|
||||||
h.stdout.Reset()
|
|
||||||
assert.NoError(t, h.run(t, args...))
|
|
||||||
assert.NoError(t, yaml.Unmarshal(h.stdout.Bytes(), got.Interface()))
|
|
||||||
assert.Equal(t, want, got.Elem().Interface())
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"isle/nebula"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var subCmdNebulaCreateCert = subCmd{
|
|
||||||
name: "create-cert",
|
|
||||||
descr: "Creates a signed nebula certificate file for an existing host and writes it to stdout",
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
|
|
||||||
var hostName hostNameFlag
|
|
||||||
hostNameF := ctx.flags.VarPF(
|
|
||||||
&hostName,
|
|
||||||
"hostname", "n",
|
|
||||||
"Name of the host to generate a certificate for",
|
|
||||||
)
|
|
||||||
|
|
||||||
pubKeyPath := ctx.flags.StringP(
|
|
||||||
"public-key-path", "p", "",
|
|
||||||
`Path to PEM file containing public key which will be embedded in the cert.`,
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx, err := ctx.withParsedFlags()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hostNameF.Changed || *pubKeyPath == "" {
|
|
||||||
return errors.New("--hostname and --pub-key-path are required")
|
|
||||||
}
|
|
||||||
|
|
||||||
hostPubPEM, err := os.ReadFile(*pubKeyPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("reading public key from %q: %w", *pubKeyPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostPub nebula.EncryptingPublicKey
|
|
||||||
if err := hostPub.UnmarshalNebulaPEM(hostPubPEM); err != nil {
|
|
||||||
return fmt.Errorf("unmarshaling public key as PEM: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := ctx.getDaemonRPC().CreateNebulaCertificate(
|
|
||||||
ctx, hostName.V, hostPub,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("calling CreateNebulaCertificate: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nebulaHostCertPEM, err := res.Unwrap().MarshalToPEM()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("marshaling cert to PEM: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stdout.Write([]byte(nebulaHostCertPEM)); err != nil {
|
|
||||||
return fmt.Errorf("writing to stdout: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdNebulaShow = subCmd{
|
|
||||||
name: "show",
|
|
||||||
descr: "Writes nebula network information to stdout in JSON format",
|
|
||||||
do: doWithOutput(func(ctx subCmdCtx) (any, error) {
|
|
||||||
ctx, err := ctx.withParsedFlags()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hosts, err := ctx.getHosts()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("getting hosts: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
caPublicCreds, err := ctx.getDaemonRPC().GetNebulaCAPublicCredentials(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("calling GetNebulaCAPublicCredentials: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
caCert := caPublicCreds.Cert
|
|
||||||
caCertDetails := caCert.Unwrap().Details
|
|
||||||
|
|
||||||
if len(caCertDetails.Subnets) != 1 {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"malformed ca.crt, contains unexpected subnets %#v",
|
|
||||||
caCertDetails.Subnets,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
subnet := caCertDetails.Subnets[0]
|
|
||||||
|
|
||||||
type outLighthouse struct {
|
|
||||||
PublicAddr string
|
|
||||||
IP string
|
|
||||||
}
|
|
||||||
|
|
||||||
out := struct {
|
|
||||||
CACert nebula.Certificate
|
|
||||||
SubnetCIDR string
|
|
||||||
Lighthouses []outLighthouse
|
|
||||||
}{
|
|
||||||
CACert: caCert,
|
|
||||||
SubnetCIDR: subnet.String(),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, h := range hosts {
|
|
||||||
if h.Nebula.PublicAddr == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
out.Lighthouses = append(out.Lighthouses, outLighthouse{
|
|
||||||
PublicAddr: h.Nebula.PublicAddr,
|
|
||||||
IP: h.IP().String(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdNebula = subCmd{
|
|
||||||
name: "nebula",
|
|
||||||
descr: "Sub-commands related to the nebula VPN",
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
return ctx.doSubCmd(
|
|
||||||
subCmdNebulaCreateCert,
|
|
||||||
subCmdNebulaShow,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,134 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"isle/daemon/network"
|
|
||||||
"isle/jsonutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
var subCmdNetworkCreate = subCmd{
|
|
||||||
name: "create",
|
|
||||||
descr: "Create's a new network, with this host being the first host in that network.",
|
|
||||||
noNetwork: true,
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
var (
|
|
||||||
ipNet ipNetFlag
|
|
||||||
hostName hostNameFlag
|
|
||||||
)
|
|
||||||
|
|
||||||
name := ctx.flags.StringP(
|
|
||||||
"name", "N", "",
|
|
||||||
"Human-readable name to identify the network as.",
|
|
||||||
)
|
|
||||||
|
|
||||||
domain := ctx.flags.StringP(
|
|
||||||
"domain", "d", "",
|
|
||||||
"Domain name that should be used as the root domain in the network.",
|
|
||||||
)
|
|
||||||
|
|
||||||
ipNetF := ctx.flags.VarPF(
|
|
||||||
&ipNet, "ip-net", "i",
|
|
||||||
`An IP subnet, in CIDR form, which will be the overall range of`+
|
|
||||||
` possible IPs in the network. The first IP in this network`+
|
|
||||||
` range will become this first host's IP.`,
|
|
||||||
)
|
|
||||||
|
|
||||||
hostNameF := ctx.flags.VarPF(
|
|
||||||
&hostName,
|
|
||||||
"hostname", "n",
|
|
||||||
"Name of this host, which will be the first host in the network",
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx, err := ctx.withParsedFlags()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *name == "" ||
|
|
||||||
*domain == "" ||
|
|
||||||
!ipNetF.Changed ||
|
|
||||||
!hostNameF.Changed {
|
|
||||||
return errors.New("--name, --domain, --ip-net, and --hostname are required")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ctx.getDaemonRPC().CreateNetwork(
|
|
||||||
ctx, *name, *domain, ipNet.V, hostName.V,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating network: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdNetworkJoin = subCmd{
|
|
||||||
name: "join",
|
|
||||||
descr: "Joins this host to an existing network",
|
|
||||||
noNetwork: true,
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
bootstrapPath := ctx.flags.StringP(
|
|
||||||
"bootstrap-path", "b", "", "Path to a bootstrap.json file.",
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx, err := ctx.withParsedFlags()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *bootstrapPath == "" {
|
|
||||||
return errors.New("--bootstrap-path is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
var newBootstrap network.JoiningBootstrap
|
|
||||||
if err := jsonutil.LoadFile(&newBootstrap, *bootstrapPath); err != nil {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"loading bootstrap from %q: %w", *bootstrapPath, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.getDaemonRPC().JoinNetwork(ctx, newBootstrap)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdNetworkList = subCmd{
|
|
||||||
name: "list",
|
|
||||||
descr: "Lists all networks which have been joined",
|
|
||||||
noNetwork: true,
|
|
||||||
do: doWithOutput(func(ctx subCmdCtx) (any, error) {
|
|
||||||
ctx, err := ctx.withParsedFlags()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.getDaemonRPC().GetNetworks(ctx)
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdNetworkGetConfig = subCmd{
|
|
||||||
name: "get-config",
|
|
||||||
descr: "Displays the currently active configuration for a joined network",
|
|
||||||
do: doWithOutput(func(ctx subCmdCtx) (any, error) {
|
|
||||||
ctx, err := ctx.withParsedFlags()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.getDaemonRPC().GetConfig(ctx)
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdNetwork = subCmd{
|
|
||||||
name: "network",
|
|
||||||
descr: "Sub-commands related to network membership",
|
|
||||||
plural: "s",
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
return ctx.doSubCmd(
|
|
||||||
subCmdNetworkCreate,
|
|
||||||
subCmdNetworkJoin,
|
|
||||||
subCmdNetworkList,
|
|
||||||
subCmdNetworkGetConfig,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,208 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"isle/daemon/daecommon"
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
)
|
|
||||||
|
|
||||||
type storageAllocation struct {
|
|
||||||
Index int `yaml:"index"`
|
|
||||||
daecommon.ConfigStorageAllocation `yaml:",inline"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func indexStorageAllocations(
|
|
||||||
config daecommon.NetworkConfig,
|
|
||||||
) []storageAllocation {
|
|
||||||
slices.SortFunc(
|
|
||||||
config.Storage.Allocations,
|
|
||||||
func(i, j daecommon.ConfigStorageAllocation) int {
|
|
||||||
return cmp.Compare(i.RPCPort, j.RPCPort)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
allocs := make([]storageAllocation, len(config.Storage.Allocations))
|
|
||||||
for i := range config.Storage.Allocations {
|
|
||||||
allocs[i] = storageAllocation{i, config.Storage.Allocations[i]}
|
|
||||||
}
|
|
||||||
|
|
||||||
return allocs
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdStorageAllocationAdd = subCmd{
|
|
||||||
name: "add-allocation",
|
|
||||||
descr: "Adds a new storage allocation to the host",
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
var alloc daecommon.ConfigStorageAllocation
|
|
||||||
|
|
||||||
ctx.flags.StringVar(
|
|
||||||
&alloc.DataPath,
|
|
||||||
"data-path",
|
|
||||||
"",
|
|
||||||
"Path to the directory data should be stored in",
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx.flags.StringVar(
|
|
||||||
&alloc.MetaPath,
|
|
||||||
"meta-path",
|
|
||||||
"",
|
|
||||||
"Path to the directory metadata should be stored in. This is a"+
|
|
||||||
" smaller dataset which benefits from a faster drive, if"+
|
|
||||||
" possible.",
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx.flags.IntVar(
|
|
||||||
&alloc.Capacity,
|
|
||||||
"capacity",
|
|
||||||
0,
|
|
||||||
"How many gigabytes to allocate.",
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx.flags.IntVar(
|
|
||||||
&alloc.S3APIPort,
|
|
||||||
"s3-api-port",
|
|
||||||
0,
|
|
||||||
"Which port of the VPN network interface to serve the S3 API on."+
|
|
||||||
" Will be automatically assigned if not given.",
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx.flags.IntVar(
|
|
||||||
&alloc.RPCPort,
|
|
||||||
"rpc-port",
|
|
||||||
0,
|
|
||||||
"Which port of the VPN network interface to serve RPC requests on."+
|
|
||||||
" Will be automatically assigned if not given. Once this port"+
|
|
||||||
" is defined for an allocation it cannot be changed.",
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx.flags.IntVar(
|
|
||||||
&alloc.AdminPort,
|
|
||||||
"admin-port",
|
|
||||||
0,
|
|
||||||
"Which port of the VPN network interface to serve admin requests"+
|
|
||||||
" on. Will be automatically assigned if not given.",
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx, err := ctx.withParsedFlags()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if alloc.DataPath == "" || alloc.MetaPath == "" || alloc.Capacity == 0 {
|
|
||||||
return errors.New(
|
|
||||||
"--data-path, --meta-path, and --capacity are required",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
config, err := ctx.getDaemonRPC().GetConfig(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting network config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Storage.Allocations = append(config.Storage.Allocations, alloc)
|
|
||||||
|
|
||||||
if err := ctx.getDaemonRPC().SetConfig(ctx, config); err != nil {
|
|
||||||
return fmt.Errorf("updating the network config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdStorageAllocationList = subCmd{
|
|
||||||
name: "list-allocation",
|
|
||||||
plural: "s",
|
|
||||||
descr: "Lists all storage which is currently allocated on this host",
|
|
||||||
do: doWithOutput(func(ctx subCmdCtx) (any, error) {
|
|
||||||
ctx, err := ctx.withParsedFlags()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
config, err := ctx.getDaemonRPC().GetConfig(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("getting network config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return indexStorageAllocations(config), nil
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdStorageAllocationRemove = subCmd{
|
|
||||||
name: "remove-allocation",
|
|
||||||
descr: "Removes an allocation which has been previously added. " +
|
|
||||||
"Allocations are identified by their index field from the output of " +
|
|
||||||
"`storage list-allocation(s)`.",
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
indexes := ctx.flags.IntSlice(
|
|
||||||
"index", nil,
|
|
||||||
"Index of the storage allocation which should be removed. Can be "+
|
|
||||||
"specified more than once",
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx, err := ctx.withParsedFlags()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(*indexes) == 0 {
|
|
||||||
return errors.New("At least one --index must be specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
config, err := ctx.getDaemonRPC().GetConfig(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting network config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
allocs = indexStorageAllocations(config)
|
|
||||||
allocsByIndex = map[int]daecommon.ConfigStorageAllocation{}
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, alloc := range allocs {
|
|
||||||
allocsByIndex[alloc.Index] = alloc.ConfigStorageAllocation
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, index := range *indexes {
|
|
||||||
if _, ok := allocsByIndex[index]; !ok {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"Index %d not found in configured storage allocations: %w",
|
|
||||||
index, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
delete(allocsByIndex, index)
|
|
||||||
}
|
|
||||||
|
|
||||||
// we sort the new allocation set so that tests are deterministic
|
|
||||||
newAllocs := maps.Values(allocsByIndex)
|
|
||||||
slices.SortFunc(
|
|
||||||
newAllocs,
|
|
||||||
func(i, j daecommon.ConfigStorageAllocation) int {
|
|
||||||
return cmp.Compare(i.RPCPort, j.RPCPort)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
config.Storage.Allocations = newAllocs
|
|
||||||
if err := ctx.getDaemonRPC().SetConfig(ctx, config); err != nil {
|
|
||||||
return fmt.Errorf("updating the network config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmdStorage = subCmd{
|
|
||||||
name: "storage",
|
|
||||||
descr: "Sub-commands having to do with configuration of storage on this host",
|
|
||||||
do: func(ctx subCmdCtx) error {
|
|
||||||
return ctx.doSubCmd(
|
|
||||||
subCmdStorageAllocationAdd,
|
|
||||||
subCmdStorageAllocationList,
|
|
||||||
subCmdStorageAllocationRemove,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,289 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"isle/daemon"
|
|
||||||
"isle/daemon/daecommon"
|
|
||||||
"isle/toolkit"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStorageAllocationAdd(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
setExpectations func(*daemon.MockRPC)
|
|
||||||
wantAlloc daecommon.ConfigStorageAllocation
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "success/no ports",
|
|
||||||
args: []string{
|
|
||||||
"--data-path", "foo", "--meta-path=bar", "--capacity", "1",
|
|
||||||
},
|
|
||||||
wantAlloc: daecommon.ConfigStorageAllocation{
|
|
||||||
DataPath: "foo",
|
|
||||||
MetaPath: "bar",
|
|
||||||
Capacity: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success/all ports",
|
|
||||||
args: []string{
|
|
||||||
"--data-path", "foo",
|
|
||||||
"--meta-path=bar",
|
|
||||||
"--capacity", "1",
|
|
||||||
"--s3-api-port", "1000",
|
|
||||||
"--rpc-port=2000",
|
|
||||||
"--admin-port", "3000",
|
|
||||||
},
|
|
||||||
wantAlloc: daecommon.ConfigStorageAllocation{
|
|
||||||
DataPath: "foo",
|
|
||||||
MetaPath: "bar",
|
|
||||||
Capacity: 1,
|
|
||||||
S3APIPort: 1000,
|
|
||||||
RPCPort: 2000,
|
|
||||||
AdminPort: 3000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
var (
|
|
||||||
h = newRunHarness(t)
|
|
||||||
config = daecommon.NewNetworkConfig(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
h.daemonRPC.
|
|
||||||
On("GetConfig", toolkit.MockArg[context.Context]()).
|
|
||||||
Return(config, nil).
|
|
||||||
Once()
|
|
||||||
|
|
||||||
config.Storage.Allocations = append(
|
|
||||||
config.Storage.Allocations, test.wantAlloc,
|
|
||||||
)
|
|
||||||
|
|
||||||
h.daemonRPC.
|
|
||||||
On(
|
|
||||||
"SetConfig",
|
|
||||||
toolkit.MockArg[context.Context](),
|
|
||||||
config,
|
|
||||||
).
|
|
||||||
Return(nil).
|
|
||||||
Once()
|
|
||||||
|
|
||||||
args := []string{"storage", "add-allocation"}
|
|
||||||
args = append(args, test.args...)
|
|
||||||
assert.NoError(t, h.run(t, args...))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStorageAllocationList(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
allocs []daecommon.ConfigStorageAllocation
|
|
||||||
want any
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
allocs: nil,
|
|
||||||
want: []any{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// results should get sorted according to RPCPort, with index
|
|
||||||
// reflecting that order.
|
|
||||||
name: "success",
|
|
||||||
allocs: []daecommon.ConfigStorageAllocation{
|
|
||||||
{
|
|
||||||
DataPath: "b",
|
|
||||||
MetaPath: "B",
|
|
||||||
Capacity: 2,
|
|
||||||
S3APIPort: 2000,
|
|
||||||
RPCPort: 2001,
|
|
||||||
AdminPort: 2002,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
DataPath: "a",
|
|
||||||
MetaPath: "A",
|
|
||||||
Capacity: 1,
|
|
||||||
S3APIPort: 1000,
|
|
||||||
RPCPort: 1001,
|
|
||||||
AdminPort: 1002,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: []map[string]any{
|
|
||||||
{
|
|
||||||
"index": 0,
|
|
||||||
"data_path": "a",
|
|
||||||
"meta_path": "A",
|
|
||||||
"capacity": 1,
|
|
||||||
"s3_api_port": 1000,
|
|
||||||
"rpc_port": 1001,
|
|
||||||
"admin_port": 1002,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"index": 1,
|
|
||||||
"data_path": "b",
|
|
||||||
"meta_path": "B",
|
|
||||||
"capacity": 2,
|
|
||||||
"s3_api_port": 2000,
|
|
||||||
"rpc_port": 2001,
|
|
||||||
"admin_port": 2002,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
var (
|
|
||||||
h = newRunHarness(t)
|
|
||||||
config daecommon.NetworkConfig
|
|
||||||
)
|
|
||||||
|
|
||||||
config.Storage.Allocations = test.allocs
|
|
||||||
|
|
||||||
h.daemonRPC.
|
|
||||||
On("GetConfig", toolkit.MockArg[context.Context]()).
|
|
||||||
Return(config, nil).
|
|
||||||
Once()
|
|
||||||
|
|
||||||
h.runAssertStdout(t, test.want, "storage", "list-allocations")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStorageAllocationRemove(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
config := func(rpcPorts ...int) daecommon.NetworkConfig {
|
|
||||||
return daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
|
|
||||||
for _, rpcPort := range rpcPorts {
|
|
||||||
c.Storage.Allocations = append(
|
|
||||||
c.Storage.Allocations,
|
|
||||||
daecommon.ConfigStorageAllocation{RPCPort: rpcPort},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
setExpectations func(*daemon.MockRPC)
|
|
||||||
wantErr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "error/no indexes",
|
|
||||||
args: nil,
|
|
||||||
wantErr: "At least one --index must be specified",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "error/unknown index",
|
|
||||||
args: []string{"--index", "1"},
|
|
||||||
setExpectations: func(daemonRPC *daemon.MockRPC) {
|
|
||||||
daemonRPC.
|
|
||||||
On("GetConfig", toolkit.MockArg[context.Context]()).
|
|
||||||
Return(config(1000), nil).
|
|
||||||
Once()
|
|
||||||
},
|
|
||||||
wantErr: "Index 1 not found",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success/remove single",
|
|
||||||
args: []string{"--index", "0"},
|
|
||||||
setExpectations: func(daemonRPC *daemon.MockRPC) {
|
|
||||||
config := config(1000, 2000)
|
|
||||||
|
|
||||||
daemonRPC.
|
|
||||||
On("GetConfig", toolkit.MockArg[context.Context]()).
|
|
||||||
Return(config, nil).
|
|
||||||
Once()
|
|
||||||
|
|
||||||
config.Storage.Allocations = config.Storage.Allocations[1:]
|
|
||||||
|
|
||||||
daemonRPC.
|
|
||||||
On(
|
|
||||||
"SetConfig",
|
|
||||||
toolkit.MockArg[context.Context](),
|
|
||||||
config,
|
|
||||||
).
|
|
||||||
Return(nil).
|
|
||||||
Once()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success/remove multiple",
|
|
||||||
args: []string{"--index", "0", "--index", "2"},
|
|
||||||
setExpectations: func(daemonRPC *daemon.MockRPC) {
|
|
||||||
config := config(1000, 2000, 3000)
|
|
||||||
|
|
||||||
daemonRPC.
|
|
||||||
On("GetConfig", toolkit.MockArg[context.Context]()).
|
|
||||||
Return(config, nil).
|
|
||||||
Once()
|
|
||||||
|
|
||||||
config.Storage.Allocations = config.Storage.Allocations[1:2]
|
|
||||||
|
|
||||||
daemonRPC.
|
|
||||||
On(
|
|
||||||
"SetConfig",
|
|
||||||
toolkit.MockArg[context.Context](),
|
|
||||||
config,
|
|
||||||
).
|
|
||||||
Return(nil).
|
|
||||||
Once()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success/remove all",
|
|
||||||
args: []string{"--index", "0", "--index", "1"},
|
|
||||||
setExpectations: func(daemonRPC *daemon.MockRPC) {
|
|
||||||
config := config(1000, 2000)
|
|
||||||
|
|
||||||
daemonRPC.
|
|
||||||
On("GetConfig", toolkit.MockArg[context.Context]()).
|
|
||||||
Return(config, nil).
|
|
||||||
Once()
|
|
||||||
|
|
||||||
config.Storage.Allocations = config.Storage.Allocations[:0]
|
|
||||||
|
|
||||||
daemonRPC.
|
|
||||||
On(
|
|
||||||
"SetConfig",
|
|
||||||
toolkit.MockArg[context.Context](),
|
|
||||||
config,
|
|
||||||
).
|
|
||||||
Return(nil).
|
|
||||||
Once()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
h := newRunHarness(t)
|
|
||||||
|
|
||||||
if test.setExpectations != nil {
|
|
||||||
test.setExpectations(h.daemonRPC)
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{"storage", "remove-allocation"}
|
|
||||||
args = append(args, test.args...)
|
|
||||||
err := h.run(t, args...)
|
|
||||||
|
|
||||||
if test.wantErr == "" {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
} else {
|
|
||||||
assert.Contains(t, err.Error(), test.wantErr)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,273 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"isle/daemon"
|
|
||||||
"isle/daemon/jsonrpc2"
|
|
||||||
"isle/jsonutil"
|
|
||||||
"isle/toolkit"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type subCmd struct {
|
|
||||||
name string
|
|
||||||
descr string
|
|
||||||
do func(subCmdCtx) error
|
|
||||||
|
|
||||||
// If set then the name will be allowed to be suffixed with this string.
|
|
||||||
plural string
|
|
||||||
|
|
||||||
// noNetwork, if true, means the call doesn't require a network to be
|
|
||||||
// specified on the command-line if there are more than one networks
|
|
||||||
// configured.
|
|
||||||
noNetwork bool
|
|
||||||
|
|
||||||
// Extra arguments on the command-line will be passed through to some
|
|
||||||
// underlying command.
|
|
||||||
passthroughArgs bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type subCmdCtxOpts struct {
|
|
||||||
args []string // command-line arguments, excluding the subCmd itself.
|
|
||||||
subCmdNames []string // names of subCmds so far, including this one
|
|
||||||
daemonRPC daemon.RPC
|
|
||||||
stdout io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *subCmdCtxOpts) withDefaults() *subCmdCtxOpts {
|
|
||||||
if o == nil {
|
|
||||||
o = new(subCmdCtxOpts)
|
|
||||||
}
|
|
||||||
|
|
||||||
if o.args == nil {
|
|
||||||
o.args = os.Args[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if o.stdout == nil {
|
|
||||||
o.stdout = os.Stdout
|
|
||||||
}
|
|
||||||
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
// subCmdCtx contains all information available to a subCmd's do method.
|
|
||||||
type subCmdCtx struct {
|
|
||||||
context.Context
|
|
||||||
logger *mlog.Logger
|
|
||||||
subCmd subCmd // the subCmd itself
|
|
||||||
opts *subCmdCtxOpts
|
|
||||||
|
|
||||||
flags *pflag.FlagSet
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSubCmdCtx(
|
|
||||||
ctx context.Context,
|
|
||||||
logger *mlog.Logger,
|
|
||||||
subCmd subCmd,
|
|
||||||
opts *subCmdCtxOpts,
|
|
||||||
) subCmdCtx {
|
|
||||||
opts = opts.withDefaults()
|
|
||||||
|
|
||||||
return subCmdCtx{
|
|
||||||
Context: ctx,
|
|
||||||
logger: logger,
|
|
||||||
subCmd: subCmd,
|
|
||||||
opts: opts,
|
|
||||||
flags: pflag.NewFlagSet(subCmd.name, pflag.ExitOnError),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func usagePrefix(subCmdNames []string) string {
|
|
||||||
subCmdNamesStr := strings.Join(subCmdNames, " ")
|
|
||||||
if subCmdNamesStr != "" {
|
|
||||||
subCmdNamesStr += " "
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("\nUSAGE: %s %s", os.Args[0], subCmdNamesStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx subCmdCtx) getDaemonRPC() daemon.RPC {
|
|
||||||
if ctx.opts.daemonRPC == nil {
|
|
||||||
// TODO Close is not being called on the HTTPClient
|
|
||||||
httpClient, baseURL := toolkit.NewUnixHTTPClient(
|
|
||||||
ctx.logger.WithNamespace("http-client"),
|
|
||||||
daemon.HTTPSocketPath(),
|
|
||||||
)
|
|
||||||
|
|
||||||
baseURL.Path = daemonHTTPRPCPath
|
|
||||||
|
|
||||||
ctx.opts.daemonRPC = daemon.RPCFromClient(
|
|
||||||
jsonrpc2.NewHTTPClient(httpClient, baseURL.String()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return ctx.opts.daemonRPC
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx subCmdCtx) withParsedFlags() (subCmdCtx, error) {
|
|
||||||
logLevel := logLevelFlag{mlog.LevelInfo}
|
|
||||||
ctx.flags.VarP(
|
|
||||||
&logLevel,
|
|
||||||
"log-level", "l",
|
|
||||||
"Maximum log level to output. Can be DEBUG, CHILD, INFO, WARN, ERROR, or FATAL.",
|
|
||||||
)
|
|
||||||
|
|
||||||
var network string
|
|
||||||
if !ctx.subCmd.noNetwork {
|
|
||||||
ctx.flags.StringVar(
|
|
||||||
&network,
|
|
||||||
"network", "",
|
|
||||||
"Which network to perform the command against, if more than one is joined. Can be an ID, name, or domain.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.flags.VisitAll(func(f *pflag.Flag) {
|
|
||||||
if f.Shorthand == "h" {
|
|
||||||
panic(fmt.Sprintf("flag %+v has reserved shorthand `-h`", f))
|
|
||||||
}
|
|
||||||
if f.Name == "help" {
|
|
||||||
panic(fmt.Sprintf("flag %+v has reserved name `--help`", f))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx.flags.Usage = func() {
|
|
||||||
var passthroughStr string
|
|
||||||
if ctx.subCmd.passthroughArgs {
|
|
||||||
passthroughStr = " [--] [args...]"
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(
|
|
||||||
os.Stderr, "%s[-h|--help] [%s flags...]%s\n\n",
|
|
||||||
usagePrefix(ctx.opts.subCmdNames), ctx.subCmd.name, passthroughStr,
|
|
||||||
)
|
|
||||||
fmt.Fprintf(
|
|
||||||
os.Stderr, "%s FLAGS:\n\n", strings.ToUpper(ctx.subCmd.name),
|
|
||||||
)
|
|
||||||
fmt.Fprintln(os.Stderr, ctx.flags.FlagUsages())
|
|
||||||
|
|
||||||
os.Stderr.Sync()
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ctx.flags.Parse(ctx.opts.args); err != nil {
|
|
||||||
return ctx, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Context = daemon.WithNetwork(ctx.Context, network)
|
|
||||||
ctx.logger = ctx.logger.WithMaxLevel(logLevel.Int())
|
|
||||||
|
|
||||||
return ctx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
|
|
||||||
|
|
||||||
printUsageExit := func(subCmdName string) {
|
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "unknown sub-command %q\n", subCmdName)
|
|
||||||
|
|
||||||
fmt.Fprintf(
|
|
||||||
os.Stderr,
|
|
||||||
"%s<subCmd> [-h|--help] [sub-command flags...]\n",
|
|
||||||
usagePrefix(ctx.opts.subCmdNames),
|
|
||||||
)
|
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "\nSUB-COMMANDS:\n\n")
|
|
||||||
|
|
||||||
for _, subCmd := range subCmds {
|
|
||||||
name := subCmd.name
|
|
||||||
if subCmd.plural != "" {
|
|
||||||
name += "(" + subCmd.plural + ")"
|
|
||||||
}
|
|
||||||
fmt.Fprintf(os.Stderr, " %s\t%s\n", name, subCmd.descr)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "\n")
|
|
||||||
os.Stderr.Sync()
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
args := ctx.opts.args
|
|
||||||
|
|
||||||
if len(args) == 0 {
|
|
||||||
printUsageExit("")
|
|
||||||
}
|
|
||||||
|
|
||||||
subCmdsMap := map[string]subCmd{}
|
|
||||||
for _, subCmd := range subCmds {
|
|
||||||
subCmdsMap[subCmd.name] = subCmd
|
|
||||||
if subCmd.plural != "" {
|
|
||||||
subCmdsMap[subCmd.name+subCmd.plural] = subCmd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subCmdName, args := args[0], args[1:]
|
|
||||||
|
|
||||||
subCmd, ok := subCmdsMap[subCmdName]
|
|
||||||
if !ok {
|
|
||||||
printUsageExit(subCmdName)
|
|
||||||
}
|
|
||||||
|
|
||||||
nextSubCmdCtxOpts := *ctx.opts
|
|
||||||
nextSubCmdCtxOpts.args = args
|
|
||||||
nextSubCmdCtxOpts.subCmdNames = append(ctx.opts.subCmdNames, subCmdName)
|
|
||||||
|
|
||||||
nextSubCmdCtx := newSubCmdCtx(
|
|
||||||
ctx.Context, ctx.logger, subCmd, &nextSubCmdCtxOpts,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := subCmd.do(nextSubCmdCtx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type outputFormat string
|
|
||||||
|
|
||||||
func (f outputFormat) MarshalText() ([]byte, error) { return []byte(f), nil }
|
|
||||||
|
|
||||||
func (f *outputFormat) UnmarshalText(b []byte) error {
|
|
||||||
*f = outputFormat(strings.ToLower(string(b)))
|
|
||||||
switch *f {
|
|
||||||
case "json", "yaml":
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return errors.New("invalid output format")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// doWithOutput wraps a subCmd's do function so that it will output some value
|
|
||||||
// to stdout. The value will be formatted according to a command-line argument.
|
|
||||||
func doWithOutput(fn func(subCmdCtx) (any, error)) func(subCmdCtx) error {
|
|
||||||
return func(ctx subCmdCtx) error {
|
|
||||||
type outputFormatFlag = textUnmarshalerFlag[outputFormat, *outputFormat]
|
|
||||||
|
|
||||||
outputFormat := outputFormatFlag{"yaml"}
|
|
||||||
ctx.flags.Var(
|
|
||||||
&outputFormat,
|
|
||||||
"format",
|
|
||||||
"How to format the output value. Can be 'json' or 'yaml'.",
|
|
||||||
)
|
|
||||||
|
|
||||||
res, err := fn(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch outputFormat.V {
|
|
||||||
case "json":
|
|
||||||
return jsonutil.WriteIndented(ctx.opts.stdout, res)
|
|
||||||
case "yaml":
|
|
||||||
return yaml.NewEncoder(ctx.opts.stdout).Encode(res)
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unexpected outputFormat %q", outputFormat))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user