Compare commits
3 Commits
main
...
3f3ad43cb2
Author | SHA1 | Date | |
---|---|---|---|
|
3f3ad43cb2 | ||
|
15c5c904a2 | ||
|
81d4a35b24 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
*-bin
|
||||
*admin.yml*
|
||||
*bootstrap.yml*
|
||||
*-admin.tgz*
|
||||
*-bootstrap.tgz
|
||||
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]
|
||||
Name=Isle
|
||||
Name[en]=Isle
|
||||
Name=Cryptic Net
|
||||
Name[en]=Cryptic Net
|
||||
Exec=AppRun
|
||||
|
||||
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
|
||||
entrypoint to Isle on [the Micropelago website][isle].
|
||||
The cryptic-net project provides the foundation for an **autonomous community
|
||||
cloud infrastructure**.
|
||||
|
||||
Isle runs on a host as a server daemon, and connects to other isle instances to
|
||||
form a peer-to-peer network. Isle networks are completely self-hosted; no
|
||||
third-parties are required for a network to function.
|
||||
This project targets communities of individuals, where certain members of the
|
||||
community would like to host services and applications from servers running in
|
||||
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
|
||||
host services for themselves and others. These capabilities include:
|
||||
The core components of cryptic-net, currently, are:
|
||||
|
||||
* A VPN which enables direct peer-to-peer communication between network members.
|
||||
Even if most hosts in the network are on a private LAN (e.g. their home WiFi
|
||||
network) or have a dynamic IP, they can still communicate directly with each
|
||||
other.
|
||||
* A VPN which enables direct peer-to-peer communication. Even if most hosts in
|
||||
the network are on a private LAN (e.g. their home WiFi network) or have a
|
||||
dynamic IP, they can still communicate directly with each other.
|
||||
|
||||
* An S3-compatible network filesystem. Each member can provide as much storage
|
||||
as they care to, if any. Stored data is sharded and replicated across all
|
||||
hosts that choose to provide storage.
|
||||
* An S3-compatible network filesystem. Each participant can provide as much
|
||||
storage as they care to, if any. Stored data is sharded and replicated across
|
||||
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
|
||||
this case the networks remain siloed from each other, such that members of one
|
||||
network are unable to access resources or communicate with members of the other.
|
||||
Participants are able to build upon these foundations to host services for
|
||||
themselves and others. They can be assured that their communications are private
|
||||
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
|
||||
network, and all other functionality available via the command-line.
|
||||
_NOTE: There is currently only a single live cryptic-net which can be joined,
|
||||
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)
|
||||
* [Command-line Usage](./docs/command-line.md)
|
||||
* [Join a Network](./docs/user/join-a-network.md)
|
||||
cryptic-net users fall into different roles, depending on their level of
|
||||
involvement and expertise within their particular network. The documentation for
|
||||
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
|
||||
the [Developer Documentation](./docs/dev/index.md).
|
||||
### User Docs
|
||||
|
||||
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",
|
||||
releaseName ? "dev",
|
||||
}: let
|
||||
pkgs ? (import ./nix/pkgs.nix).stable,
|
||||
bootstrap ? null,
|
||||
|
||||
pkgs = pkgsNix.default {
|
||||
inherit buildSystem hostSystem;
|
||||
}: rec {
|
||||
|
||||
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 {
|
||||
name = "isle-version";
|
||||
name = "cryptic-net-version";
|
||||
|
||||
inherit buildSystem hostSystem revision releaseName;
|
||||
|
||||
nativeBuildInputs = [ pkgsNative.git ];
|
||||
|
||||
goVersion = pkgs.go.version;
|
||||
garageVersion = garageNix.version;
|
||||
nixpkgsVersion = pkgsNix.version;
|
||||
buildInputs = [ pkgs.git pkgs.go ];
|
||||
src = ./.;
|
||||
inherit bootstrap;
|
||||
|
||||
builder = builtins.toFile "builder.sh" ''
|
||||
source $stdenv/setup
|
||||
|
||||
versionFile=version
|
||||
|
||||
echo "Release: $releaseName" >> "$versionFile"
|
||||
echo "Platform: $hostSystem" >> "$versionFile"
|
||||
echo "Git Revision: $revision" >> "$versionFile"
|
||||
echo "Go Version: $goVersion" >> "$versionFile"
|
||||
echo "Garage Version: $garageVersion" >> "$versionFile"
|
||||
echo "NixPkgs Version: $nixpkgsVersion" >> "$versionFile"
|
||||
echo "Build Platform: $buildSystem" >> "$versionFile"
|
||||
if [ "$bootstrap" != "" ]; then
|
||||
hostName=$(tar -xzf "$bootstrap" --to-stdout ./hostname)
|
||||
echo "Built for host: $hostName" >> "$versionFile"
|
||||
fi
|
||||
|
||||
echo "Build date: $(date)" >> "$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
|
||||
cp "$versionFile" "$out"/share
|
||||
'';
|
||||
};
|
||||
|
||||
goBinaries = pkgs.buildGoModule {
|
||||
pname = "isle-go-binaries";
|
||||
version = "unstable";
|
||||
goWorkspace = pkgs.callPackage ./go-workspace {};
|
||||
|
||||
# If this seems pointless, that's because it is! buildGoModule doesn't like
|
||||
# it if the src derivation's name ends in "-go". So this mkDerivation here
|
||||
# only serves to give buildGoModule a src derivation with a name it likes.
|
||||
src = pkgs.stdenv.mkDerivation {
|
||||
name = "isle-go-src";
|
||||
src = ./go;
|
||||
builder = builtins.toFile "builder.sh" ''
|
||||
source $stdenv/setup
|
||||
cp -r "$src" "$out"
|
||||
'';
|
||||
};
|
||||
dnsmasq = (pkgs.callPackage ./dnsmasq {
|
||||
glibcStatic = pkgs.glibc.static;
|
||||
}).env;
|
||||
|
||||
vendorHash = "sha256-CYnZNk1wTw/88L6SxNJTUojWartbGdL44c4GKFc8s2k=";
|
||||
garage = (pkgs.callPackage ./garage {}).env;
|
||||
|
||||
subPackages = [
|
||||
"./cmd/entrypoint"
|
||||
];
|
||||
};
|
||||
waitFor = pkgs.callPackage ./nix/wait-for.nix {};
|
||||
|
||||
dnsmasq = (pkgs.callPackage ./nix/dnsmasq.nix {
|
||||
stdenv = pkgs.pkgsStatic.stdenv;
|
||||
});
|
||||
|
||||
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";
|
||||
appDir = pkgs.buildEnv {
|
||||
name = "cryptic-net-AppDir";
|
||||
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
|
||||
version
|
||||
dnsmasq
|
||||
nebula
|
||||
garage
|
||||
pkgs.minio-client
|
||||
];
|
||||
waitFor
|
||||
goWorkspace.crypticNetMain
|
||||
|
||||
] ++ (if bootstrap != null then [ rootedBootstrap ] else []);
|
||||
};
|
||||
|
||||
appDir = pkgs.stdenv.mkDerivation {
|
||||
name = "isle-AppDir";
|
||||
appimagetool = pkgs.callPackage ./nix/appimagetool.nix {};
|
||||
|
||||
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 {
|
||||
name = "isle-AppImage";
|
||||
name = "cryptic-net-AppImage";
|
||||
src = appDir;
|
||||
|
||||
nativeBuildInputs = [
|
||||
(pkgsNative.callPackage ./nix/appimagetool.nix {})
|
||||
];
|
||||
buildInputs = [ appimagetool ];
|
||||
|
||||
ARCH = pkgs.stdenv.hostPlatform.parsed.cpu.name;
|
||||
ARCH = "x86_64";
|
||||
|
||||
builder = builtins.toFile "build.sh" ''
|
||||
source $stdenv/setup
|
||||
cp -rL "$src" isle.AppDir
|
||||
chmod +w isle.AppDir -R
|
||||
|
||||
export VERSION=debug
|
||||
|
||||
# https://github.com/probonopd/go-appimage/issues/155
|
||||
unset SOURCE_DATE_EPOCH
|
||||
|
||||
appimagetool ./isle.AppDir
|
||||
mkdir -p "$out"/bin
|
||||
mv Isle-* "$out"/bin/isle
|
||||
cp -rL "$src" cryptic-net
|
||||
chmod +w cryptic-net -R
|
||||
mkdir $out
|
||||
appimagetool cryptic-net "$out/cryptic-net"
|
||||
'';
|
||||
};
|
||||
|
||||
archPkg = ((import ./dist/linux/arch) {
|
||||
inherit hostSystem releaseName appImage;
|
||||
pkgs = pkgsNative;
|
||||
});
|
||||
};
|
||||
service = pkgs.writeText "cryptic-service" ''
|
||||
[Unit]
|
||||
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
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
## 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:
|
||||
|
||||
```
|
||||
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
|
||||
shared only with the user it was generated for. The `bootstrap.json` file should
|
||||
not be re-used between hosts.
|
||||
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.tgz` file should
|
||||
not be re-used between hosts either.
|
||||
|
||||
The user can now proceed with calling `isle network join`, as described in the
|
||||
[Getting Started][getting-started] document.
|
||||
If the user already has access to a `cryptic-net` binary then the new
|
||||
`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
|
||||
|
||||
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
|
||||
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.
|
||||
* The UX is aggressively optimized to eliminate manual intervention by users.
|
||||
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.
|
||||
|
||||
* All resources within an isle network are expected to be hosted on hardware
|
||||
owned by community members, for example home media servers or gaming rigs.
|
||||
Thus, an isle network is fully autonomous.
|
||||
* All resources within a cryptic-net are expected to be hosted on hardware owned
|
||||
by community members, for example home media servers or gaming rigs. Thus, a
|
||||
cryptic-net is fully autonomous.
|
||||
|
||||
* Hardware resources are expected to be heterogenous and geographically
|
||||
dispersed.
|
||||
|
||||
* 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
|
||||
|
||||
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
|
||||
as `.plantuml` files, which do require a build step. If these are changed then
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
[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.
|
||||
|
||||
## 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
|
||||
|
||||
Open your `/etc/isle/daemon.yml` file in a text editor, and find the
|
||||
`vpn.public_addr` field. Update that field to reflect your host's IP/DNS name
|
||||
and your chosen UDP port.
|
||||
Open your `daemon.yml` file in a text editor, and find the `vpn.public_addr`
|
||||
field. Update that field to reflect your host's IP/DNS name and your chosen UDP
|
||||
port.
|
||||
|
||||
## 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
|
||||
configuration, which other hosts will pick up on and begin using.
|
||||
|
@ -1,12 +1,19 @@
|
||||
# Contributing Storage
|
||||
|
||||
This document is for you if your host machine can be reliably be online at all
|
||||
times and has 1GB or more of unused drive space you'd like to contribute to the
|
||||
network.
|
||||
If your host machine can be reasonably sure of being online most, if not all, of
|
||||
the time, and has 100GB or more of unused drive space you'd like to contribute
|
||||
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.
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
```
|
||||
@ -24,44 +31,37 @@ storage:
|
||||
allocations:
|
||||
|
||||
# 1.2 TB are being shared from drive1
|
||||
- data_path: /mnt/drive1/isle/data
|
||||
meta_path: /mnt/drive1/isle/meta
|
||||
- data_path: /mnt/drive1/cryptic-net/data
|
||||
meta_path: /mnt/drive1/cryptic-net/meta
|
||||
capacity: 1200
|
||||
api_port: 3900
|
||||
rpc_port: 3901
|
||||
web_port: 3902
|
||||
|
||||
# 100 GB are being shared from drive2
|
||||
- data_path: /mnt/drive2/isle/data
|
||||
meta_path: /mnt/drive2/isle/meta
|
||||
# 100 GB (the minimum) are being shared from drive2
|
||||
- data_path: /mnt/drive2/cryptic-net/data
|
||||
meta_path: /mnt/drive2/cryptic-net/meta
|
||||
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
|
||||
is properly set up for providing storage.
|
||||
You will need to configure your hosts's firewall to allow traffic from
|
||||
cryptic-net IPs on the ports you specified in your allocations.
|
||||
|
||||
## 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.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
information on how to interact directly with the garage instance being run by
|
||||
isle.
|
||||
cryptic-net.
|
||||
|
||||
[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
|
||||
|
||||
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
|
||||
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
|
||||
garage works and what it can do.
|
||||
|
||||
@ -13,12 +13,12 @@ garage works and what it can do.
|
||||
|
||||
## 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
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
@ -26,14 +26,14 @@ do so if necessary.
|
||||
|
||||
## 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:
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
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!)**
|
||||
@ -55,11 +55,11 @@ sudo isle garage cli layout show
|
||||
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:
|
||||
|
||||
```
|
||||
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,17 +1,11 @@
|
||||
{
|
||||
|
||||
buildSystem ? builtins.currentSystem,
|
||||
hostSystem ? buildSystem,
|
||||
pkgsNix ? (import ../nix/pkgs.nix),
|
||||
pkgs ? (import ../nix/pkgs.nix).stable,
|
||||
|
||||
}: let
|
||||
pkgs = pkgsNix.default {
|
||||
inherit buildSystem hostSystem;
|
||||
};
|
||||
in
|
||||
pkgs.mkShell {
|
||||
name = "isle-build-docs";
|
||||
}: pkgs.mkShell {
|
||||
name = "cryptic-net-build-docs";
|
||||
buildInputs = [ pkgs.plantuml ];
|
||||
|
||||
shellHook = ''
|
||||
set -e
|
||||
plantuml -tsvg ./dev/*.plantuml
|
||||
|
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
|
||||
|
||||
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
|
||||
particular host.
|
||||
|
||||
The server will serve requests for `<hostname>.hosts.<domain>` hostnames,
|
||||
where `<hostname>` is the name of any host in the network, and `<domain`> is the
|
||||
network's domain name.
|
||||
The server will serve requests for `<hostname>.hosts.cryptic.io` hostnames,
|
||||
where `<hostname>` is any host's name in the `bootstrap/nebula/hosts` directory.
|
||||
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
|
||||
server will forward the request to a pre-configured public resolver. The set of
|
||||
public resolvers used can be configured in the `/etc/isle/daemon.yml` file.
|
||||
If a request for a non `.cryptic.io` hostname is received then the server will
|
||||
forward the request to a pre-configured public resolver. The set of public
|
||||
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
|
||||
making use of the network.
|
||||
This DNS server is an optional feature of cryptic-net, and not required in
|
||||
general for making use of the network.
|
||||
|
||||
## Example
|
||||
|
||||
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`.
|
||||
In order to configure the host to use the isle DNS server for all DNS
|
||||
requests, I could do something like this:
|
||||
the network is `10.10.1.1`. In order to configure the host to use the
|
||||
cryptic-net DNS server for all DNS requests, I could do something like this:
|
||||
|
||||
```
|
||||
sudo su
|
||||
echo "nameserver 10.10.1.1" > /etc/resolv.conf
|
||||
```
|
||||
|
||||
From that point, all DNS requests on my host would hit the isle DNS
|
||||
server. If I request `my-host.hosts.cool.internal`, it would respond with the
|
||||
appropriate private IP.
|
||||
From that point, all DNS requests on my host would hit the cryptic-net DNS
|
||||
server. If I request `my-host.cryptic.io`, it would respond with the appropriate
|
||||
private IP.
|
||||
|
||||
NOTE that configuration of dns resolvers is very OS-specific, even amongst Linux
|
||||
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 (
|
||||
"fmt"
|
||||
@ -9,10 +9,9 @@ import (
|
||||
var subCmdVersion = subCmd{
|
||||
name: "version",
|
||||
descr: "Dumps version and build info to stdout",
|
||||
noNetwork: true,
|
||||
do: func(ctx subCmdCtx) error {
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
|
||||
versionPath := filepath.Join(envAppDirPath, "share/version")
|
||||
versionPath := filepath.Join(subCmdCtx.env.AppDirPath, "share/version")
|
||||
|
||||
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 (
|
||||
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/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/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/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // 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/gopherjs/gopherjs v1.17.2 // 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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // 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/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/smartystreets/assertions v1.13.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.10 // 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/sys v0.0.0-20220406155245-289d7a0edf71 // 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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/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/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.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
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/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
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/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/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
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/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/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
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/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/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
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/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
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/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
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/go.mod h1:wDmR7qL282YbGsPy6H/yAsesrxfxaaSlJazyFLYVFx8=
|
||||
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/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
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.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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ=
|
||||
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
|
||||
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/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/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-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/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/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-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.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
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 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/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
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.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.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
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