Initial commit
This commit is contained in:
commit
482b4d56dd
36 files changed
+6121
No files matched your search
+120
@@ -0,0 +1,120 @@
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
.cover
|
||||
|
||||
# IntelliJ IDEA project files
|
||||
.idea
|
||||
*.ipr
|
||||
*.iml
|
||||
*.iws
|
||||
|
||||
### Logs ###
|
||||
*.log
|
||||
logs/
|
||||
|
||||
### direnv ###
|
||||
.envrc
|
||||
.direnv/
|
||||
|
||||
### Temp directories ###
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
### Visual Studio ###
|
||||
.vscode/
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
### Git ###
|
||||
# Created by git for backups. To disable backups in Git:
|
||||
# $ git config --global mergetool.keepBackup false
|
||||
*.orig
|
||||
|
||||
# Created by git when using merge tools for conflicts
|
||||
*.BACKUP.*
|
||||
*.BASE.*
|
||||
*.LOCAL.*
|
||||
*.REMOTE.*
|
||||
*_BACKUP_*.txt
|
||||
*_BASE_*.txt
|
||||
*_LOCAL_*.txt
|
||||
*_REMOTE_*.txt
|
||||
|
||||
### Go ###
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
### Tags ###
|
||||
# Ignore tags created by etags, ctags, gtags (GNU global) and cscope
|
||||
TAGS
|
||||
.TAGS
|
||||
!TAGS/
|
||||
tags
|
||||
.tags
|
||||
!tags/
|
||||
gtags.files
|
||||
GTAGS
|
||||
GRTAGS
|
||||
GPATH
|
||||
GSYMS
|
||||
cscope.files
|
||||
cscope.out
|
||||
cscope.in.out
|
||||
cscope.po.out
|
||||
|
||||
### Vagrant ###
|
||||
# General
|
||||
.vagrant/
|
||||
|
||||
# Log files (if you are creating logs in debug mode, uncomment this)
|
||||
# *.log
|
||||
|
||||
### Vagrant Patch ###
|
||||
*.box
|
||||
|
||||
### Vim ###
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
Sessionx.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
*~
|
||||
|
||||
# Auto-generated tag files
|
||||
# Persistent undo
|
||||
[._]*.un~
|
||||
|
||||
# Default output of command compilation
|
||||
main
|
||||
|
||||
# vim: set filetype=conf :
|
||||
@@ -0,0 +1,363 @@
|
||||
Mozilla Public License, version 2.0
|
||||
|
||||
1. Definitions
|
||||
|
||||
1.1. "Contributor"
|
||||
|
||||
means each individual or legal entity that creates, contributes to the
|
||||
creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
|
||||
means the combination of the Contributions of others (if any) used by a
|
||||
Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
|
||||
means Source Code Form to which the initial Contributor has attached the
|
||||
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
||||
Modifications of such Source Code Form, in each case including portions
|
||||
thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
a. that the initial Contributor has attached the notice described in
|
||||
Exhibit B to the Covered Software; or
|
||||
|
||||
b. that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the terms of
|
||||
a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
|
||||
means a work that combines Covered Software with other material, in a
|
||||
separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
|
||||
means having the right to grant, to the maximum extent possible, whether
|
||||
at the time of the initial grant or subsequently, any and all of the
|
||||
rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
|
||||
means any of the following:
|
||||
|
||||
a. any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered Software; or
|
||||
|
||||
b. any new file in Source Code Form that contains any Covered Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the License,
|
||||
by the making, using, selling, offering for sale, having made, import,
|
||||
or transfer of either its Contributions or its Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
|
||||
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
||||
General Public License, Version 2.1, the GNU Affero General Public
|
||||
License, Version 3.0, or any later versions of those licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that controls, is
|
||||
controlled by, or is under common control with You. For purposes of this
|
||||
definition, "control" means (a) the power, direct or indirect, to cause
|
||||
the direction or management of such entity, whether by contract or
|
||||
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
||||
outstanding shares or beneficial ownership of such entity.
|
||||
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
a. under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
||||
sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
a. for any code that a Contributor has removed from Covered Software; or
|
||||
|
||||
b. for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
c. under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights to
|
||||
grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
||||
Section 2.1.
|
||||
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
a. such Covered Software must also be made available in Source Code Form,
|
||||
as described in Section 3.1, and You must inform recipients of the
|
||||
Executable Form how they can obtain a copy of such Source Code Form by
|
||||
reasonable means in a timely manner, at a charge no more than the cost
|
||||
of distribution to the recipient; and
|
||||
|
||||
b. You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter the
|
||||
recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty, or
|
||||
limitations of liability) contained within the Source Code Form of the
|
||||
Covered Software, except that You may alter any license notices to the
|
||||
extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this License
|
||||
with respect to some or all of the Covered Software due to statute,
|
||||
judicial order, or regulation then You must: (a) comply with the terms of
|
||||
this License to the maximum extent possible; and (b) describe the
|
||||
limitations and the code they affect. Such description must be placed in a
|
||||
text file included with all distributions of the Covered Software under
|
||||
this License. Except to the extent prohibited by statute or regulation,
|
||||
such description must be sufficiently detailed for a recipient of ordinary
|
||||
skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically if You
|
||||
fail to comply with any of its terms. However, if You become compliant,
|
||||
then the rights granted under this License from a particular Contributor
|
||||
are reinstated (a) provisionally, unless and until such Contributor
|
||||
explicitly and finally terminates Your grants, and (b) on an ongoing
|
||||
basis, if such Contributor fails to notify You of the non-compliance by
|
||||
some reasonable means prior to 60 days after You have come back into
|
||||
compliance. Moreover, Your grants from a particular Contributor are
|
||||
reinstated on an ongoing basis if such Contributor notifies You of the
|
||||
non-compliance by some reasonable means, this is the first time You have
|
||||
received notice of non-compliance with this License from such
|
||||
Contributor, and You become compliant prior to 30 days after Your receipt
|
||||
of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
||||
license agreements (excluding distributors and resellers) which have been
|
||||
validly granted by You or Your distributors under this License prior to
|
||||
termination shall survive termination.
|
||||
|
||||
6. Disclaimer of Warranty
|
||||
|
||||
Covered Software is provided under this License on an "as is" basis,
|
||||
without warranty of any kind, either expressed, implied, or statutory,
|
||||
including, without limitation, warranties that the Covered Software is free
|
||||
of defects, merchantable, fit for a particular purpose or non-infringing.
|
||||
The entire risk as to the quality and performance of the Covered Software
|
||||
is with You. Should any Covered Software prove defective in any respect,
|
||||
You (not any Contributor) assume the cost of any necessary servicing,
|
||||
repair, or correction. This disclaimer of warranty constitutes an essential
|
||||
part of this License. No use of any Covered Software is authorized under
|
||||
this License except under this disclaimer.
|
||||
|
||||
7. Limitation of Liability
|
||||
|
||||
Under no circumstances and under no legal theory, whether tort (including
|
||||
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
||||
distributes Covered Software as permitted above, be liable to You for any
|
||||
direct, indirect, special, incidental, or consequential damages of any
|
||||
character including, without limitation, damages for lost profits, loss of
|
||||
goodwill, work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses, even if such party shall have been
|
||||
informed of the possibility of such damages. This limitation of liability
|
||||
shall not apply to liability for death or personal injury resulting from
|
||||
such party's negligence to the extent applicable law prohibits such
|
||||
limitation. Some jurisdictions do not allow the exclusion or limitation of
|
||||
incidental or consequential damages, so this exclusion and limitation may
|
||||
not apply to You.
|
||||
|
||||
8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the courts
|
||||
of a jurisdiction where the defendant maintains its principal place of
|
||||
business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions. Nothing
|
||||
in this Section shall prevent a party's ability to bring cross-claims or
|
||||
counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides that
|
||||
the language of a contract shall be construed against the drafter shall not
|
||||
be used to construe this License against a Contributor.
|
||||
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses If You choose to distribute Source Code Form that is
|
||||
Incompatible With Secondary Licenses under the terms of this version of
|
||||
the License, the notice described in Exhibit B of this License must be
|
||||
attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular file,
|
||||
then You may include the notice in a location (such as a LICENSE file in a
|
||||
relevant directory) where a recipient would be likely to look for such a
|
||||
notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
|
||||
This Source Code Form is "Incompatible
|
||||
With Secondary Licenses", as defined by
|
||||
the Mozilla Public License, v. 2.0.
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# Watchtower
|
||||
|
||||
----
|
||||
|
||||
**Please note**: We take Watchtower's security and our users' trust very
|
||||
seriously. If you believe you have found a security issue in Watchtower,
|
||||
_please responsibly disclose_ by contacting us at
|
||||
[security@hashicorp.com](mailto:security@hashicorp.com).
|
||||
|
||||
----
|
||||
|
||||
Watchtower is a tool for controlling access to networked resources in the
|
||||
[SDP](https://en.wikipedia.org/wiki/Software_Defined_Perimeter) model. It is
|
||||
designed to be straightforward to understand, highly scalable, and resilient.
|
||||
It can run in clouds, on-prem, secure enclaves and more, and does not require
|
||||
an agent to be installed on every end host.
|
||||
|
||||
Unlike firewalls, Watchtower performs per-access authentication and
|
||||
authorization checks, allowing for much higher level mappings of users to
|
||||
services or hosts than at network layers. Although complementary to secrets
|
||||
managers (like HashiCorp's own [Vault](https://www.vaultproject.io/)),
|
||||
Watchtower fills a different niche, allowing the credential that is eventually
|
||||
used to be hidden entirely from the user.
|
||||
|
||||
Getting Started
|
||||
-------------------------------
|
||||
|
||||
Watchtower consists of two server components: Controllers, which serve the API
|
||||
and coordinate session requests; and Workers, which perform the actual session
|
||||
handling. A normal Watchtower installation will consist of one or more
|
||||
Controllers paired with one or more Workers. A single Watchtower binary can act
|
||||
in either of these two modes.
|
||||
|
||||
Additionally, Watchtower desktop clients are provided that simplify providing
|
||||
access to request and connect to authorized sessions from desktop machines.
|
||||
|
||||
Watchtower does _not_ require software to be installed on the endpoint hosts
|
||||
and services.
|
||||
|
||||
## Requirements
|
||||
|
||||
Watchtower has two external dependencies: a SQL database, and one or more
|
||||
KMSes. Both are readily available from cloud vendors, but can be satisfied by
|
||||
on-premise technologies as well.
|
||||
|
||||
* The database contains Watchtower's configuration and session information and
|
||||
must be accessible by Controller nodes. Values that are secrets (such as
|
||||
credentials) are encrypted in the database. Currently, PostgreSQL is
|
||||
supported as a database. This need can be satisfied by both hosted and
|
||||
self-run instances. For more information on specific requirements and setup,
|
||||
see our [Database Guide], however, in most instances all that is needed is a
|
||||
database endpoint and appropriate credentials.
|
||||
|
||||
* Any cloud KMS or Vault's Transit Secrets Engine can be used to satisfy the
|
||||
KMS requirement. Currently, two keys within the KMS are needed: one for
|
||||
authenticating other cluster components, which must be accessible by both
|
||||
Controllers and Workers; and one for encrypting secret values in the
|
||||
database, which need only be accessible to Controllers. These keys can be
|
||||
changed over time (so long as the original key remains available for any
|
||||
decryption needs), and key derivation is used extensively to avoid key sprawl
|
||||
of these high-value keys.
|
||||
|
||||
Watchtower has a `dev` mode that can be used for testing. In this mode both a
|
||||
Controller and Worker are started with a single command, and they have the
|
||||
following properties:
|
||||
|
||||
* The Controller will start a PostgreSQL Docker container to use as storage.
|
||||
This container will be shut down and removed (if possible) when the
|
||||
Controller is (gracefully) shut down.
|
||||
* The Controller will use an internal KMS with an ephemeral key
|
||||
|
||||
## Trying out Watchtower
|
||||
|
||||
### Starting Watchtower in Dev Mode
|
||||
|
||||
The following steps assume that you are running Watchtower in `dev` mode.
|
||||
Running Watchtower in a more permanent context requires a few more steps, such
|
||||
as writing some simple configuration files to tell the nodes how to reach their
|
||||
database and KMS. The steps below, along with the extra information for needed
|
||||
for permanent installations, are detailed in our [Installation Guide].
|
||||
|
||||
1. Download the latest release of the server binary and appropriate desktop
|
||||
client(s) from our [releases page]
|
||||
2. Start the server binary with `watchtower dev`. This will start a Controller
|
||||
service listening on `http://127.0.0.1:9200` for incoming API requests and a
|
||||
Worker service listening on the same address/port for incoming session
|
||||
requests. It will also create various default Catalogs and Sets, and display
|
||||
various bits of information, such as a username and password that can be
|
||||
used to log in.
|
||||
|
||||
### Configuring Resources
|
||||
|
||||
For a simple test of Watchtower in `dev` mode you don't generally need to
|
||||
configure any resources at all! But it's useful to understand what `dev` mode
|
||||
did for you so you can then take further steps:
|
||||
|
||||
In Watchtower, a Set is a grouping of resources that share a similarity
|
||||
suitable for using in configuration; for instance, a User Set combines groups
|
||||
of users that should have equivalent permissions. Most Sets are automatically
|
||||
created and managed by Catalogs; for instance, an AWS Catalog can create Host
|
||||
Sets from EC2 instance, or a Consul Catalog can create Host Sets from Consul
|
||||
service catalogs. Although you can test out the full functionality of Catalogs
|
||||
and Sets in `dev` mode, this mode will automatically create some resources for
|
||||
you suitable for local testing:
|
||||
|
||||
* A Host Catalog with a default Host Set containing the local machine
|
||||
(`127.0.0.1`) and defining an SSH service on port 22
|
||||
* A User Catalog with a default User Set containing a user with the given
|
||||
username and associated password. This can be used to log in to the web UI
|
||||
and the desktop client.
|
||||
* A Permissions Set mapping the given User Set to a set of permissions, in this
|
||||
case granting access to make connections to resources
|
||||
* A Credentials Catalog with a default Credential Set that is empty (meaning
|
||||
Watchtower will require and use the user's credentials when connecting to the
|
||||
networked resource; in other words, in this situation it will run in a
|
||||
TCP-only mode)
|
||||
|
||||
To actually make the connection to the networked resource, a Target must be
|
||||
defined. Targets group a Host Set, a Credential Set, and a User Set, which,
|
||||
along with validation against the permissions defined by relevant Permissions
|
||||
Sets, together allow Watchtower to understand what user principals are allowed
|
||||
to connect where and using what authentication information. A `dev` Watchtower
|
||||
will create a default Target that allows the generated user to make SSH
|
||||
connections to the local machine on port 22.
|
||||
|
||||
You can of course go into Watchtower's web UI or use its API to change these
|
||||
default values, for instance if you instead want to connect to a different host
|
||||
or need to modify the port to connect to, or want to test out the capabilities
|
||||
of Watchtower to supply the credentials for the session without user
|
||||
input/knowledge.
|
||||
|
||||
### Making the Connection
|
||||
|
||||
Next, let's actually make a connection to your local SSH daemon via Watchtower:
|
||||
|
||||
3. Start the desktop client and point it at the local Watchtower `dev` session.
|
||||
Log in with the generated username and password.
|
||||
4. You can now select the Target that grants access to the local machine and
|
||||
hit "Request Session". Doing so will cause Watchtower to check whether this
|
||||
action is allowed, and if so, send session information back to the client,
|
||||
allowing it to start a local authenticated proxy to the Worker. You will be
|
||||
shown a CLI command that can be copied and pasted into the CLI to connect to
|
||||
the local machine via Watchtower.
|
||||
5. Paste the CLI command into your terminal, or use the given address and port
|
||||
in the SSH client of your choice, and connect!
|
||||
6. When the session has ended, look in the given temporary directory to see
|
||||
session information; or navigate to the Sessions page in the web UI to see
|
||||
the same information
|
||||
|
||||
### Going Further
|
||||
|
||||
This example is a simple way to get started but omits several key steps that
|
||||
could be taken in a production context:
|
||||
|
||||
* Using a firewall or other means to restrict the set of hosts allowed to
|
||||
connect to a local SSH daemon to only Watchtower Worker nodes, thereby making
|
||||
Watchtower the _only_ means of ingress to a host
|
||||
* Using credentials specified within Watchtower rather than the client's own
|
||||
credentials. Because these credentials are not divulged to users, even if a
|
||||
user gets around some network-level firewall or other restriction to gain
|
||||
direct network access to a host, they will be unable to authenticate.
|
||||
* Recording the session for later auditing or incident management purposes
|
||||
|
||||
`dev` mode by default is operating in a direct proxying mode, where it is
|
||||
simply passing the network streams along once the session has been authorized.
|
||||
However, Watchtower is built to be flexible and eventually able to understand a
|
||||
variety of networked protocols, with more capabilities to come in the future.
|
||||
We have a long roadmap planned out so stay tuned for information about new
|
||||
features and capabilities!
|
||||
@@ -0,0 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(cmd.Run(os.Args[1:]))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package globals
|
||||
|
||||
import "time"
|
||||
|
||||
// NOTE:
|
||||
// Globals are bad. But there are some situations where we want a value that is
|
||||
// constant per invocation but invocation may be in various places, such as a
|
||||
// test or via CLI, and placing such a value in any given package leads to
|
||||
// import issues. These values should only ever be set at startup, but simply
|
||||
// available to reference from anywhere.
|
||||
|
||||
var (
|
||||
// DefaultMaxRequestDuration is the amount of time we'll wait for a request
|
||||
DefaultMaxRequestDuration = 90 * time.Second
|
||||
|
||||
// DefaultMaxRequestSize is the maximum size of a request we allow by default
|
||||
DefaultMaxRequestSize = int64(1024 * 1024)
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
module github.com/hashicorp/watchtower
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/armon/go-metrics v0.3.3
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
|
||||
github.com/hashicorp/errwrap v1.0.0
|
||||
github.com/hashicorp/go-alpnmux v0.0.0-20200304172639-978773dcd35e
|
||||
github.com/hashicorp/go-hclog v0.12.1
|
||||
github.com/hashicorp/go-kms-wrapping v0.5.3
|
||||
github.com/hashicorp/go-multierror v1.0.0
|
||||
github.com/hashicorp/hcl v1.0.0
|
||||
github.com/hashicorp/vault v1.2.1-0.20200225214617-b7caef46c3c8
|
||||
github.com/hashicorp/vault/api v1.0.5-0.20200225214617-b7caef46c3c8
|
||||
github.com/hashicorp/vault/sdk v0.1.14-0.20200225214617-b7caef46c3c8
|
||||
github.com/kr/text v0.2.0
|
||||
github.com/lib/pq v1.3.0
|
||||
github.com/mattn/go-colorable v0.1.6
|
||||
github.com/mitchellh/cli v1.0.0
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/mapstructure v1.1.2
|
||||
github.com/ory/dockertest/v3 v3.5.4
|
||||
github.com/pires/go-proxyproto v0.0.0-20200213100827-833e5d06d8f0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/posener/complete v1.2.3
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
|
||||
google.golang.org/grpc v1.27.1
|
||||
)
|
||||
@@ -0,0 +1,332 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxLineLength is the maximum width of any line.
|
||||
maxLineLength int = 78
|
||||
|
||||
// NotSetValue is a flag value for a not-set value
|
||||
NotSetValue = "(not set)"
|
||||
)
|
||||
|
||||
// reRemoveWhitespace is a regular expression for stripping whitespace from
|
||||
// a string.
|
||||
var reRemoveWhitespace = regexp.MustCompile(`[\s]+`)
|
||||
|
||||
type BaseCommand struct {
|
||||
UI cli.Ui
|
||||
Address string
|
||||
Context context.Context
|
||||
|
||||
flags *FlagSets
|
||||
flagsOnce sync.Once
|
||||
|
||||
flagCACert string
|
||||
flagCAPath string
|
||||
flagClientCert string
|
||||
flagClientKey string
|
||||
flagTLSServerName string
|
||||
flagTLSInsecure bool
|
||||
|
||||
flagFormat string
|
||||
flagField string
|
||||
flagOutputCurlString bool
|
||||
}
|
||||
|
||||
func (c *BaseCommand) SetAddress(addr string) {
|
||||
c.Address = addr
|
||||
}
|
||||
|
||||
type FlagSetBit uint
|
||||
|
||||
const (
|
||||
FlagSetNone FlagSetBit = 1 << iota
|
||||
FlagSetHTTP
|
||||
FlagSetOutputField
|
||||
FlagSetOutputFormat
|
||||
)
|
||||
|
||||
// FlagSet creates the flags for this command. The result is cached on the
|
||||
// command to save performance on future calls.
|
||||
func (c *BaseCommand) FlagSet(bit FlagSetBit) *FlagSets {
|
||||
c.flagsOnce.Do(func() {
|
||||
set := NewFlagSets(c.UI)
|
||||
|
||||
// These flag sets will apply to all leaf subcommands.
|
||||
// TODO: Optional, but FlagSetHTTP can be safely removed from the individual
|
||||
// Flags() subcommands.
|
||||
bit = bit | FlagSetHTTP
|
||||
|
||||
if bit&FlagSetHTTP != 0 {
|
||||
f := set.NewFlagSet("HTTP Options")
|
||||
|
||||
addrStringVar := &StringVar{
|
||||
Name: FlagNameAddress,
|
||||
Target: &c.Address,
|
||||
EnvVar: EnvWatchtowerAddress,
|
||||
Completion: complete.PredictAnything,
|
||||
Usage: "Address of the Watchtower controller.",
|
||||
}
|
||||
if c.Address != "" {
|
||||
addrStringVar.Default = c.Address
|
||||
} else {
|
||||
addrStringVar.Default = "https://127.0.0.1:9200"
|
||||
}
|
||||
f.StringVar(addrStringVar)
|
||||
|
||||
f.StringVar(&StringVar{
|
||||
Name: FlagNameCACert,
|
||||
Target: &c.flagCACert,
|
||||
Default: "",
|
||||
EnvVar: EnvWatchtowerCACert,
|
||||
Completion: complete.PredictFiles("*"),
|
||||
Usage: "Path on the local disk to a single PEM-encoded CA " +
|
||||
"certificate to verify the Controller or Worker's server's SSL certificate. This " +
|
||||
"takes precedence over -ca-path.",
|
||||
})
|
||||
|
||||
f.StringVar(&StringVar{
|
||||
Name: FlagNameCAPath,
|
||||
Target: &c.flagCAPath,
|
||||
Default: "",
|
||||
EnvVar: EnvWatchtowerCAPath,
|
||||
Completion: complete.PredictDirs("*"),
|
||||
Usage: "Path on the local disk to a directory of PEM-encoded CA " +
|
||||
"certificates to verify the SSL certificate of the Controller.",
|
||||
})
|
||||
|
||||
f.StringVar(&StringVar{
|
||||
Name: FlagNameClientCert,
|
||||
Target: &c.flagClientCert,
|
||||
Default: "",
|
||||
EnvVar: EnvWatchtowerClientCert,
|
||||
Completion: complete.PredictFiles("*"),
|
||||
Usage: "Path on the local disk to a single PEM-encoded CA " +
|
||||
"certificate to use for TLS authentication to the Watchtower Controller. If " +
|
||||
"this flag is specified, -client-key is also required.",
|
||||
})
|
||||
|
||||
f.StringVar(&StringVar{
|
||||
Name: FlagNameClientKey,
|
||||
Target: &c.flagClientKey,
|
||||
Default: "",
|
||||
EnvVar: EnvWatchtowerClientKey,
|
||||
Completion: complete.PredictFiles("*"),
|
||||
Usage: "Path on the local disk to a single PEM-encoded private key " +
|
||||
"matching the client certificate from -client-cert.",
|
||||
})
|
||||
|
||||
f.StringVar(&StringVar{
|
||||
Name: FlagTLSServerName,
|
||||
Target: &c.flagTLSServerName,
|
||||
Default: "",
|
||||
EnvVar: EnvWatchtowerTLSServerName,
|
||||
Completion: complete.PredictAnything,
|
||||
Usage: "Name to use as the SNI host when connecting to the Watchtower " +
|
||||
"server via TLS.",
|
||||
})
|
||||
|
||||
f.BoolVar(&BoolVar{
|
||||
Name: FlagNameTLSInsecure,
|
||||
Target: &c.flagTLSInsecure,
|
||||
Default: false,
|
||||
EnvVar: EnvWatchtowerTLSInsecure,
|
||||
Usage: "Disable verification of TLS certificates. Using this option " +
|
||||
"is highly discouraged as it decreases the security of data " +
|
||||
"transmissions to and from the Watchtower server.",
|
||||
})
|
||||
|
||||
f.BoolVar(&BoolVar{
|
||||
Name: "output-curl-string",
|
||||
Target: &c.flagOutputCurlString,
|
||||
Default: false,
|
||||
Usage: "Instead of executing the request, print an equivalent cURL " +
|
||||
"command string and exit.",
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if bit&(FlagSetOutputField|FlagSetOutputFormat) != 0 {
|
||||
f := set.NewFlagSet("Output Options")
|
||||
|
||||
if bit&FlagSetOutputField != 0 {
|
||||
f.StringVar(&StringVar{
|
||||
Name: "field",
|
||||
Target: &c.flagField,
|
||||
Default: "",
|
||||
Completion: complete.PredictAnything,
|
||||
Usage: "Print only the field with the given name. Specifying " +
|
||||
"this option will take precedence over other formatting " +
|
||||
"directives. The result will not have a trailing newline " +
|
||||
"making it ideal for piping to other processes.",
|
||||
})
|
||||
}
|
||||
|
||||
if bit&FlagSetOutputFormat != 0 {
|
||||
f.StringVar(&StringVar{
|
||||
Name: "format",
|
||||
Target: &c.flagFormat,
|
||||
Default: "table",
|
||||
EnvVar: EnvWatchtowerCLIFormat,
|
||||
Completion: complete.PredictSet("table", "json", "yaml"),
|
||||
Usage: "Print the output in the given format. Valid formats " +
|
||||
"are \"table\", \"json\", or \"yaml\".",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
c.flags = set
|
||||
})
|
||||
|
||||
return c.flags
|
||||
}
|
||||
|
||||
// FlagSets is a group of flag sets.
|
||||
type FlagSets struct {
|
||||
flagSets []*FlagSet
|
||||
mainSet *flag.FlagSet
|
||||
hiddens map[string]struct{}
|
||||
completions complete.Flags
|
||||
}
|
||||
|
||||
// NewFlagSets creates a new flag sets.
|
||||
func NewFlagSets(ui cli.Ui) *FlagSets {
|
||||
mainSet := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
|
||||
// Errors and usage are controlled by the CLI.
|
||||
mainSet.Usage = func() {}
|
||||
mainSet.SetOutput(ioutil.Discard)
|
||||
|
||||
return &FlagSets{
|
||||
flagSets: make([]*FlagSet, 0, 6),
|
||||
mainSet: mainSet,
|
||||
hiddens: make(map[string]struct{}),
|
||||
completions: complete.Flags{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewFlagSet creates a new flag set from the given flag sets.
|
||||
func (f *FlagSets) NewFlagSet(name string) *FlagSet {
|
||||
flagSet := NewFlagSet(name)
|
||||
flagSet.mainSet = f.mainSet
|
||||
flagSet.completions = f.completions
|
||||
f.flagSets = append(f.flagSets, flagSet)
|
||||
return flagSet
|
||||
}
|
||||
|
||||
// Completions returns the completions for this flag set.
|
||||
func (f *FlagSets) Completions() complete.Flags {
|
||||
return f.completions
|
||||
}
|
||||
|
||||
// Parse parses the given flags, returning any errors.
|
||||
func (f *FlagSets) Parse(args []string) error {
|
||||
return f.mainSet.Parse(args)
|
||||
}
|
||||
|
||||
// Parsed reports whether the command-line flags have been parsed.
|
||||
func (f *FlagSets) Parsed() bool {
|
||||
return f.mainSet.Parsed()
|
||||
}
|
||||
|
||||
// Args returns the remaining args after parsing.
|
||||
func (f *FlagSets) Args() []string {
|
||||
return f.mainSet.Args()
|
||||
}
|
||||
|
||||
// Visit visits the flags in lexicographical order, calling fn for each. It
|
||||
// visits only those flags that have been set.
|
||||
func (f *FlagSets) Visit(fn func(*flag.Flag)) {
|
||||
f.mainSet.Visit(fn)
|
||||
}
|
||||
|
||||
// Help builds custom help for this command, grouping by flag set.
|
||||
func (fs *FlagSets) Help() string {
|
||||
var out bytes.Buffer
|
||||
|
||||
for _, set := range fs.flagSets {
|
||||
printFlagTitle(&out, set.name+":")
|
||||
set.VisitAll(func(f *flag.Flag) {
|
||||
// Skip any hidden flags
|
||||
if v, ok := f.Value.(FlagVisibility); ok && v.Hidden() {
|
||||
return
|
||||
}
|
||||
printFlagDetail(&out, f)
|
||||
})
|
||||
}
|
||||
|
||||
return strings.TrimRight(out.String(), "\n")
|
||||
}
|
||||
|
||||
// FlagSet is a grouped wrapper around a real flag set and a grouped flag set.
|
||||
type FlagSet struct {
|
||||
name string
|
||||
flagSet *flag.FlagSet
|
||||
mainSet *flag.FlagSet
|
||||
completions complete.Flags
|
||||
}
|
||||
|
||||
// NewFlagSet creates a new flag set.
|
||||
func NewFlagSet(name string) *FlagSet {
|
||||
return &FlagSet{
|
||||
name: name,
|
||||
flagSet: flag.NewFlagSet(name, flag.ContinueOnError),
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the name of this flag set.
|
||||
func (f *FlagSet) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f *FlagSet) Visit(fn func(*flag.Flag)) {
|
||||
f.flagSet.Visit(fn)
|
||||
}
|
||||
|
||||
func (f *FlagSet) VisitAll(fn func(*flag.Flag)) {
|
||||
f.flagSet.VisitAll(fn)
|
||||
}
|
||||
|
||||
// printFlagTitle prints a consistently-formatted title to the given writer.
|
||||
func printFlagTitle(w io.Writer, s string) {
|
||||
fmt.Fprintf(w, "%s\n\n", s)
|
||||
}
|
||||
|
||||
// printFlagDetail prints a single flag to the given writer.
|
||||
func printFlagDetail(w io.Writer, f *flag.Flag) {
|
||||
// Check if the flag is hidden - do not print any flag detail or help output
|
||||
// if it is hidden.
|
||||
if h, ok := f.Value.(FlagVisibility); ok && h.Hidden() {
|
||||
return
|
||||
}
|
||||
|
||||
// Check for a detailed example
|
||||
example := ""
|
||||
if t, ok := f.Value.(FlagExample); ok {
|
||||
example = t.Example()
|
||||
}
|
||||
|
||||
if example != "" {
|
||||
fmt.Fprintf(w, " -%s=<%s>\n", f.Name, example)
|
||||
} else {
|
||||
fmt.Fprintf(w, " -%s\n", f.Name)
|
||||
}
|
||||
|
||||
usage := reRemoveWhitespace.ReplaceAllString(f.Usage, " ")
|
||||
indented := WrapAtLengthWithPadding(usage, 6)
|
||||
fmt.Fprintf(w, "%s\n\n", indented)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package base
|
||||
|
||||
const (
|
||||
// flagNameAddress is the flag used in the base command to read in the
|
||||
// address of the Watchtower server.
|
||||
FlagNameAddress = "address"
|
||||
// flagnameCACert is the flag used in the base command to read in the CA
|
||||
// cert.
|
||||
FlagNameCACert = "ca-cert"
|
||||
// flagnameCAPath is the flag used in the base command to read in the CA
|
||||
// cert path.
|
||||
FlagNameCAPath = "ca-path"
|
||||
//flagNameClientCert is the flag used in the base command to read in the
|
||||
//client key
|
||||
FlagNameClientKey = "client-key"
|
||||
//flagNameClientCert is the flag used in the base command to read in the
|
||||
//client cert
|
||||
FlagNameClientCert = "client-cert"
|
||||
// flagNameTLSInsecure is the flag used in the base command to read in
|
||||
// the option to ignore TLS certificate verification.
|
||||
FlagNameTLSInsecure = "tls-insecure"
|
||||
// flagTLSServerName is the flag used in the base command to read in
|
||||
// the TLS server name.
|
||||
FlagTLSServerName = "tls-server-name"
|
||||
)
|
||||
|
||||
const EnvWatchtowerCLINoColor = `WATCHTOWER_CLI_NO_COLOR`
|
||||
const EnvWatchtowerCLIFormat = `WATCHTOWER_CLI_FORMAT`
|
||||
const EnvWatchtowerAddress = "WATCHTOWER_ADDR"
|
||||
const EnvWatchtowerCACert = "WATCHTOWER_CACERT"
|
||||
const EnvWatchtowerCAPath = "WATCHTOWER_CAPATH"
|
||||
const EnvWatchtowerClientCert = "WATCHTOWER_CLIENT_CERT"
|
||||
const EnvWatchtowerClientKey = "WATCHTOWER_CLIENT_KEY"
|
||||
const EnvWatchtowerClientTimeout = "WATCHTOWER_CLIENT_TIMEOUT"
|
||||
const EnvWatchtowerTLSInsecure = "WATCHTOWER_TLS_INSECURE"
|
||||
const EnvWatchtowerTLSServerName = "WATCHTOWER_TLS_SERVER_NAME"
|
||||
const EnvWatchtowerMaxRetries = "WATCHTOWER_MAX_RETRIES"
|
||||
const EnvWatchtowerToken = "WATCHTOWER_TOKEN"
|
||||
const EnvRateLimit = "WATCHTOWER_RATE_LIMIT"
|
||||
@@ -0,0 +1,780 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
// FlagExample is an interface which declares an example value.
|
||||
type FlagExample interface {
|
||||
Example() string
|
||||
}
|
||||
|
||||
// FlagVisibility is an interface which declares whether a flag should be
|
||||
// hidden from help and completions. This is usually used for deprecations
|
||||
// on "internal-only" flags.
|
||||
type FlagVisibility interface {
|
||||
Hidden() bool
|
||||
}
|
||||
|
||||
// FlagBool is an interface which boolean flags implement.
|
||||
type FlagBool interface {
|
||||
IsBoolFlag() bool
|
||||
}
|
||||
|
||||
// -- BoolVar and boolValue
|
||||
type BoolVar struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
Usage string
|
||||
Default bool
|
||||
Hidden bool
|
||||
EnvVar string
|
||||
Target *bool
|
||||
Completion complete.Predictor
|
||||
}
|
||||
|
||||
func (f *FlagSet) BoolVar(i *BoolVar) {
|
||||
def := i.Default
|
||||
if v, exist := os.LookupEnv(i.EnvVar); exist {
|
||||
if b, err := strconv.ParseBool(v); err == nil {
|
||||
def = b
|
||||
}
|
||||
}
|
||||
|
||||
f.VarFlag(&VarFlag{
|
||||
Name: i.Name,
|
||||
Aliases: i.Aliases,
|
||||
Usage: i.Usage,
|
||||
Default: strconv.FormatBool(i.Default),
|
||||
EnvVar: i.EnvVar,
|
||||
Value: newBoolValue(def, i.Target, i.Hidden),
|
||||
Completion: i.Completion,
|
||||
})
|
||||
}
|
||||
|
||||
type boolValue struct {
|
||||
hidden bool
|
||||
target *bool
|
||||
}
|
||||
|
||||
func newBoolValue(def bool, target *bool, hidden bool) *boolValue {
|
||||
*target = def
|
||||
|
||||
return &boolValue{
|
||||
hidden: hidden,
|
||||
target: target,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *boolValue) Set(s string) error {
|
||||
v, err := strconv.ParseBool(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*b.target = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *boolValue) Get() interface{} { return *b.target }
|
||||
func (b *boolValue) String() string { return strconv.FormatBool(*b.target) }
|
||||
func (b *boolValue) Example() string { return "" }
|
||||
func (b *boolValue) Hidden() bool { return b.hidden }
|
||||
func (b *boolValue) IsBoolFlag() bool { return true }
|
||||
|
||||
// -- IntVar and intValue
|
||||
type IntVar struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
Usage string
|
||||
Default int
|
||||
Hidden bool
|
||||
EnvVar string
|
||||
Target *int
|
||||
Completion complete.Predictor
|
||||
}
|
||||
|
||||
func (f *FlagSet) IntVar(i *IntVar) {
|
||||
initial := i.Default
|
||||
if v, exist := os.LookupEnv(i.EnvVar); exist {
|
||||
if i, err := strconv.ParseInt(v, 0, 64); err == nil {
|
||||
initial = int(i)
|
||||
}
|
||||
}
|
||||
|
||||
def := ""
|
||||
if i.Default != 0 {
|
||||
def = strconv.FormatInt(int64(i.Default), 10)
|
||||
}
|
||||
|
||||
f.VarFlag(&VarFlag{
|
||||
Name: i.Name,
|
||||
Aliases: i.Aliases,
|
||||
Usage: i.Usage,
|
||||
Default: def,
|
||||
EnvVar: i.EnvVar,
|
||||
Value: newIntValue(initial, i.Target, i.Hidden),
|
||||
Completion: i.Completion,
|
||||
})
|
||||
}
|
||||
|
||||
type intValue struct {
|
||||
hidden bool
|
||||
target *int
|
||||
}
|
||||
|
||||
func newIntValue(def int, target *int, hidden bool) *intValue {
|
||||
*target = def
|
||||
return &intValue{
|
||||
hidden: hidden,
|
||||
target: target,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *intValue) Set(s string) error {
|
||||
v, err := strconv.ParseInt(s, 0, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*i.target = int(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *intValue) Get() interface{} { return int(*i.target) }
|
||||
func (i *intValue) String() string { return strconv.Itoa(int(*i.target)) }
|
||||
func (i *intValue) Example() string { return "int" }
|
||||
func (i *intValue) Hidden() bool { return i.hidden }
|
||||
|
||||
// -- Int64Var and int64Value
|
||||
type Int64Var struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
Usage string
|
||||
Default int64
|
||||
Hidden bool
|
||||
EnvVar string
|
||||
Target *int64
|
||||
Completion complete.Predictor
|
||||
}
|
||||
|
||||
func (f *FlagSet) Int64Var(i *Int64Var) {
|
||||
initial := i.Default
|
||||
if v, exist := os.LookupEnv(i.EnvVar); exist {
|
||||
if i, err := strconv.ParseInt(v, 0, 64); err == nil {
|
||||
initial = i
|
||||
}
|
||||
}
|
||||
|
||||
def := ""
|
||||
if i.Default != 0 {
|
||||
def = strconv.FormatInt(int64(i.Default), 10)
|
||||
}
|
||||
|
||||
f.VarFlag(&VarFlag{
|
||||
Name: i.Name,
|
||||
Aliases: i.Aliases,
|
||||
Usage: i.Usage,
|
||||
Default: def,
|
||||
EnvVar: i.EnvVar,
|
||||
Value: newInt64Value(initial, i.Target, i.Hidden),
|
||||
Completion: i.Completion,
|
||||
})
|
||||
}
|
||||
|
||||
type int64Value struct {
|
||||
hidden bool
|
||||
target *int64
|
||||
}
|
||||
|
||||
func newInt64Value(def int64, target *int64, hidden bool) *int64Value {
|
||||
*target = def
|
||||
return &int64Value{
|
||||
hidden: hidden,
|
||||
target: target,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *int64Value) Set(s string) error {
|
||||
v, err := strconv.ParseInt(s, 0, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*i.target = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *int64Value) Get() interface{} { return int64(*i.target) }
|
||||
func (i *int64Value) String() string { return strconv.FormatInt(int64(*i.target), 10) }
|
||||
func (i *int64Value) Example() string { return "int" }
|
||||
func (i *int64Value) Hidden() bool { return i.hidden }
|
||||
|
||||
// -- UintVar && uintValue
|
||||
type UintVar struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
Usage string
|
||||
Default uint
|
||||
Hidden bool
|
||||
EnvVar string
|
||||
Target *uint
|
||||
Completion complete.Predictor
|
||||
}
|
||||
|
||||
func (f *FlagSet) UintVar(i *UintVar) {
|
||||
initial := i.Default
|
||||
if v, exist := os.LookupEnv(i.EnvVar); exist {
|
||||
if i, err := strconv.ParseUint(v, 0, 64); err == nil {
|
||||
initial = uint(i)
|
||||
}
|
||||
}
|
||||
|
||||
def := ""
|
||||
if i.Default != 0 {
|
||||
def = strconv.FormatUint(uint64(i.Default), 10)
|
||||
}
|
||||
|
||||
f.VarFlag(&VarFlag{
|
||||
Name: i.Name,
|
||||
Aliases: i.Aliases,
|
||||
Usage: i.Usage,
|
||||
Default: def,
|
||||
EnvVar: i.EnvVar,
|
||||
Value: newUintValue(initial, i.Target, i.Hidden),
|
||||
Completion: i.Completion,
|
||||
})
|
||||
}
|
||||
|
||||
type uintValue struct {
|
||||
hidden bool
|
||||
target *uint
|
||||
}
|
||||
|
||||
func newUintValue(def uint, target *uint, hidden bool) *uintValue {
|
||||
*target = def
|
||||
return &uintValue{
|
||||
hidden: hidden,
|
||||
target: target,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *uintValue) Set(s string) error {
|
||||
v, err := strconv.ParseUint(s, 0, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*i.target = uint(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *uintValue) Get() interface{} { return uint(*i.target) }
|
||||
func (i *uintValue) String() string { return strconv.FormatUint(uint64(*i.target), 10) }
|
||||
func (i *uintValue) Example() string { return "uint" }
|
||||
func (i *uintValue) Hidden() bool { return i.hidden }
|
||||
|
||||
// -- Uint64Var and uint64Value
|
||||
type Uint64Var struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
Usage string
|
||||
Default uint64
|
||||
Hidden bool
|
||||
EnvVar string
|
||||
Target *uint64
|
||||
Completion complete.Predictor
|
||||
}
|
||||
|
||||
func (f *FlagSet) Uint64Var(i *Uint64Var) {
|
||||
initial := i.Default
|
||||
if v, exist := os.LookupEnv(i.EnvVar); exist {
|
||||
if i, err := strconv.ParseUint(v, 0, 64); err == nil {
|
||||
initial = i
|
||||
}
|
||||
}
|
||||
|
||||
def := ""
|
||||
if i.Default != 0 {
|
||||
strconv.FormatUint(i.Default, 10)
|
||||
}
|
||||
|
||||
f.VarFlag(&VarFlag{
|
||||
Name: i.Name,
|
||||
Aliases: i.Aliases,
|
||||
Usage: i.Usage,
|
||||
Default: def,
|
||||
EnvVar: i.EnvVar,
|
||||
Value: newUint64Value(initial, i.Target, i.Hidden),
|
||||
Completion: i.Completion,
|
||||
})
|
||||
}
|
||||
|
||||
type uint64Value struct {
|
||||
hidden bool
|
||||
target *uint64
|
||||
}
|
||||
|
||||
func newUint64Value(def uint64, target *uint64, hidden bool) *uint64Value {
|
||||
*target = def
|
||||
return &uint64Value{
|
||||
hidden: hidden,
|
||||
target: target,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *uint64Value) Set(s string) error {
|
||||
v, err := strconv.ParseUint(s, 0, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*i.target = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *uint64Value) Get() interface{} { return uint64(*i.target) }
|
||||
func (i *uint64Value) String() string { return strconv.FormatUint(uint64(*i.target), 10) }
|
||||
func (i *uint64Value) Example() string { return "uint" }
|
||||
func (i *uint64Value) Hidden() bool { return i.hidden }
|
||||
|
||||
// -- StringVar and stringValue
|
||||
type StringVar struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
Usage string
|
||||
Default string
|
||||
Hidden bool
|
||||
EnvVar string
|
||||
Target *string
|
||||
Completion complete.Predictor
|
||||
}
|
||||
|
||||
func (f *FlagSet) StringVar(i *StringVar) {
|
||||
initial := i.Default
|
||||
if v, exist := os.LookupEnv(i.EnvVar); exist {
|
||||
initial = v
|
||||
}
|
||||
|
||||
def := ""
|
||||
if i.Default != "" {
|
||||
def = i.Default
|
||||
}
|
||||
|
||||
f.VarFlag(&VarFlag{
|
||||
Name: i.Name,
|
||||
Aliases: i.Aliases,
|
||||
Usage: i.Usage,
|
||||
Default: def,
|
||||
EnvVar: i.EnvVar,
|
||||
Value: newStringValue(initial, i.Target, i.Hidden),
|
||||
Completion: i.Completion,
|
||||
})
|
||||
}
|
||||
|
||||
type stringValue struct {
|
||||
hidden bool
|
||||
target *string
|
||||
}
|
||||
|
||||
func newStringValue(def string, target *string, hidden bool) *stringValue {
|
||||
*target = def
|
||||
return &stringValue{
|
||||
hidden: hidden,
|
||||
target: target,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stringValue) Set(val string) error {
|
||||
*s.target = val
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stringValue) Get() interface{} { return *s.target }
|
||||
func (s *stringValue) String() string { return *s.target }
|
||||
func (s *stringValue) Example() string { return "string" }
|
||||
func (s *stringValue) Hidden() bool { return s.hidden }
|
||||
|
||||
// -- Float64Var and float64Value
|
||||
type Float64Var struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
Usage string
|
||||
Default float64
|
||||
Hidden bool
|
||||
EnvVar string
|
||||
Target *float64
|
||||
Completion complete.Predictor
|
||||
}
|
||||
|
||||
func (f *FlagSet) Float64Var(i *Float64Var) {
|
||||
initial := i.Default
|
||||
if v, exist := os.LookupEnv(i.EnvVar); exist {
|
||||
if i, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
initial = i
|
||||
}
|
||||
}
|
||||
|
||||
def := ""
|
||||
if i.Default != 0 {
|
||||
def = strconv.FormatFloat(i.Default, 'e', -1, 64)
|
||||
}
|
||||
|
||||
f.VarFlag(&VarFlag{
|
||||
Name: i.Name,
|
||||
Aliases: i.Aliases,
|
||||
Usage: i.Usage,
|
||||
Default: def,
|
||||
EnvVar: i.EnvVar,
|
||||
Value: newFloat64Value(initial, i.Target, i.Hidden),
|
||||
Completion: i.Completion,
|
||||
})
|
||||
}
|
||||
|
||||
type float64Value struct {
|
||||
hidden bool
|
||||
target *float64
|
||||
}
|
||||
|
||||
func newFloat64Value(def float64, target *float64, hidden bool) *float64Value {
|
||||
*target = def
|
||||
return &float64Value{
|
||||
hidden: hidden,
|
||||
target: target,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *float64Value) Set(s string) error {
|
||||
v, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*f.target = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *float64Value) Get() interface{} { return float64(*f.target) }
|
||||
func (f *float64Value) String() string { return strconv.FormatFloat(float64(*f.target), 'g', -1, 64) }
|
||||
func (f *float64Value) Example() string { return "float" }
|
||||
func (f *float64Value) Hidden() bool { return f.hidden }
|
||||
|
||||
// -- DurationVar and durationValue
|
||||
type DurationVar struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
Usage string
|
||||
Default time.Duration
|
||||
Hidden bool
|
||||
EnvVar string
|
||||
Target *time.Duration
|
||||
Completion complete.Predictor
|
||||
}
|
||||
|
||||
func (f *FlagSet) DurationVar(i *DurationVar) {
|
||||
initial := i.Default
|
||||
if v, exist := os.LookupEnv(i.EnvVar); exist {
|
||||
if d, err := time.ParseDuration(appendDurationSuffix(v)); err == nil {
|
||||
initial = d
|
||||
}
|
||||
}
|
||||
|
||||
def := ""
|
||||
if i.Default != 0 {
|
||||
def = i.Default.String()
|
||||
}
|
||||
|
||||
f.VarFlag(&VarFlag{
|
||||
Name: i.Name,
|
||||
Aliases: i.Aliases,
|
||||
Usage: i.Usage,
|
||||
Default: def,
|
||||
EnvVar: i.EnvVar,
|
||||
Value: newDurationValue(initial, i.Target, i.Hidden),
|
||||
Completion: i.Completion,
|
||||
})
|
||||
}
|
||||
|
||||
type durationValue struct {
|
||||
hidden bool
|
||||
target *time.Duration
|
||||
}
|
||||
|
||||
func newDurationValue(def time.Duration, target *time.Duration, hidden bool) *durationValue {
|
||||
*target = def
|
||||
return &durationValue{
|
||||
hidden: hidden,
|
||||
target: target,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *durationValue) Set(s string) error {
|
||||
// Maintain bc for people specifying "system" as the value.
|
||||
if s == "system" {
|
||||
s = "-1"
|
||||
}
|
||||
|
||||
v, err := time.ParseDuration(appendDurationSuffix(s))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*d.target = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *durationValue) Get() interface{} { return time.Duration(*d.target) }
|
||||
func (d *durationValue) String() string { return (*d.target).String() }
|
||||
func (d *durationValue) Example() string { return "duration" }
|
||||
func (d *durationValue) Hidden() bool { return d.hidden }
|
||||
|
||||
// appendDurationSuffix is used as a backwards-compat tool for assuming users
|
||||
// meant "seconds" when they do not provide a suffixed duration value.
|
||||
func appendDurationSuffix(s string) string {
|
||||
if strings.HasSuffix(s, "s") || strings.HasSuffix(s, "m") || strings.HasSuffix(s, "h") {
|
||||
return s
|
||||
}
|
||||
return s + "s"
|
||||
}
|
||||
|
||||
// -- StringSliceVar and stringSliceValue
|
||||
type StringSliceVar struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
Usage string
|
||||
Default []string
|
||||
Hidden bool
|
||||
EnvVar string
|
||||
Target *[]string
|
||||
Completion complete.Predictor
|
||||
}
|
||||
|
||||
func (f *FlagSet) StringSliceVar(i *StringSliceVar) {
|
||||
initial := i.Default
|
||||
if v, exist := os.LookupEnv(i.EnvVar); exist {
|
||||
parts := strings.Split(v, ",")
|
||||
for i := range parts {
|
||||
parts[i] = strings.TrimSpace(parts[i])
|
||||
}
|
||||
initial = parts
|
||||
}
|
||||
|
||||
def := ""
|
||||
if i.Default != nil {
|
||||
def = strings.Join(i.Default, ",")
|
||||
}
|
||||
|
||||
f.VarFlag(&VarFlag{
|
||||
Name: i.Name,
|
||||
Aliases: i.Aliases,
|
||||
Usage: i.Usage,
|
||||
Default: def,
|
||||
EnvVar: i.EnvVar,
|
||||
Value: newStringSliceValue(initial, i.Target, i.Hidden),
|
||||
Completion: i.Completion,
|
||||
})
|
||||
}
|
||||
|
||||
type stringSliceValue struct {
|
||||
hidden bool
|
||||
target *[]string
|
||||
}
|
||||
|
||||
func newStringSliceValue(def []string, target *[]string, hidden bool) *stringSliceValue {
|
||||
*target = def
|
||||
return &stringSliceValue{
|
||||
hidden: hidden,
|
||||
target: target,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stringSliceValue) Set(val string) error {
|
||||
*s.target = append(*s.target, strings.TrimSpace(val))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stringSliceValue) Get() interface{} { return *s.target }
|
||||
func (s *stringSliceValue) String() string { return strings.Join(*s.target, ",") }
|
||||
func (s *stringSliceValue) Example() string { return "string" }
|
||||
func (s *stringSliceValue) Hidden() bool { return s.hidden }
|
||||
|
||||
// -- StringMapVar and stringMapValue
|
||||
type StringMapVar struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
Usage string
|
||||
Default map[string]string
|
||||
Hidden bool
|
||||
Target *map[string]string
|
||||
Completion complete.Predictor
|
||||
}
|
||||
|
||||
func (f *FlagSet) StringMapVar(i *StringMapVar) {
|
||||
def := ""
|
||||
if i.Default != nil {
|
||||
def = mapToKV(i.Default)
|
||||
}
|
||||
|
||||
f.VarFlag(&VarFlag{
|
||||
Name: i.Name,
|
||||
Aliases: i.Aliases,
|
||||
Usage: i.Usage,
|
||||
Default: def,
|
||||
Value: newStringMapValue(i.Default, i.Target, i.Hidden),
|
||||
Completion: i.Completion,
|
||||
})
|
||||
}
|
||||
|
||||
type stringMapValue struct {
|
||||
hidden bool
|
||||
target *map[string]string
|
||||
}
|
||||
|
||||
func newStringMapValue(def map[string]string, target *map[string]string, hidden bool) *stringMapValue {
|
||||
*target = def
|
||||
return &stringMapValue{
|
||||
hidden: hidden,
|
||||
target: target,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stringMapValue) Set(val string) error {
|
||||
idx := strings.Index(val, "=")
|
||||
if idx == -1 {
|
||||
return fmt.Errorf("missing = in KV pair: %q", val)
|
||||
}
|
||||
|
||||
if *s.target == nil {
|
||||
*s.target = make(map[string]string)
|
||||
}
|
||||
|
||||
k, v := val[0:idx], val[idx+1:]
|
||||
(*s.target)[k] = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stringMapValue) Get() interface{} { return *s.target }
|
||||
func (s *stringMapValue) String() string { return mapToKV(*s.target) }
|
||||
func (s *stringMapValue) Example() string { return "key=value" }
|
||||
func (s *stringMapValue) Hidden() bool { return s.hidden }
|
||||
|
||||
func mapToKV(m map[string]string) string {
|
||||
list := make([]string, 0, len(m))
|
||||
for k, _ := range m {
|
||||
list = append(list, k)
|
||||
}
|
||||
sort.Strings(list)
|
||||
|
||||
for i, k := range list {
|
||||
list[i] = k + "=" + m[k]
|
||||
}
|
||||
|
||||
return strings.Join(list, ",")
|
||||
}
|
||||
|
||||
// -- VarFlag
|
||||
type VarFlag struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
Usage string
|
||||
Default string
|
||||
EnvVar string
|
||||
Value flag.Value
|
||||
Completion complete.Predictor
|
||||
}
|
||||
|
||||
func (f *FlagSet) VarFlag(i *VarFlag) {
|
||||
// If the flag is marked as hidden, just add it to the set and return to
|
||||
// avoid unnecessary computations here. We do not want to add completions or
|
||||
// generate help output for hidden flags.
|
||||
if v, ok := i.Value.(FlagVisibility); ok && v.Hidden() {
|
||||
f.Var(i.Value, i.Name, "")
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate the full usage
|
||||
usage := i.Usage
|
||||
|
||||
if len(i.Aliases) > 0 {
|
||||
sentence := make([]string, len(i.Aliases))
|
||||
for i, a := range i.Aliases {
|
||||
sentence[i] = fmt.Sprintf(`"-%s"`, a)
|
||||
}
|
||||
|
||||
aliases := ""
|
||||
switch len(sentence) {
|
||||
case 0:
|
||||
// impossible...
|
||||
case 1:
|
||||
aliases = sentence[0]
|
||||
case 2:
|
||||
aliases = sentence[0] + " and " + sentence[1]
|
||||
default:
|
||||
sentence[len(sentence)-1] = "and " + sentence[len(sentence)-1]
|
||||
aliases = strings.Join(sentence, ", ")
|
||||
}
|
||||
|
||||
usage += fmt.Sprintf(" This is aliased as %s.", aliases)
|
||||
}
|
||||
|
||||
if i.Default != "" {
|
||||
usage += fmt.Sprintf(" The default is %s.", i.Default)
|
||||
}
|
||||
|
||||
if i.EnvVar != "" {
|
||||
usage += fmt.Sprintf(" This can also be specified via the %s "+
|
||||
"environment variable.", i.EnvVar)
|
||||
}
|
||||
|
||||
// Add aliases to the main set
|
||||
for _, a := range i.Aliases {
|
||||
f.mainSet.Var(i.Value, a, "")
|
||||
}
|
||||
|
||||
f.Var(i.Value, i.Name, usage)
|
||||
f.completions["-"+i.Name] = i.Completion
|
||||
}
|
||||
|
||||
// Var is a lower-level API for adding something to the flags. It should be used
|
||||
// with caution, since it bypasses all validation. Consider VarFlag instead.
|
||||
func (f *FlagSet) Var(value flag.Value, name, usage string) {
|
||||
f.mainSet.Var(value, name, usage)
|
||||
f.flagSet.Var(value, name, usage)
|
||||
}
|
||||
|
||||
// -- helpers
|
||||
func envDefault(key, def string) string {
|
||||
if v, exist := os.LookupEnv(key); exist {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func envBoolDefault(key string, def bool) bool {
|
||||
if v, exist := os.LookupEnv(key); exist {
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func envDurationDefault(key string, def time.Duration) time.Duration {
|
||||
if v, exist := os.LookupEnv(key); exist {
|
||||
d, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return d
|
||||
}
|
||||
return def
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/ryanuber/columnize"
|
||||
)
|
||||
|
||||
const (
|
||||
// hopeDelim is the delimiter to use when splitting columns. We call it a
|
||||
// hopeDelim because we hope that it's never contained in a secret.
|
||||
hopeDelim = "♨"
|
||||
)
|
||||
|
||||
type FormatOptions struct {
|
||||
Format string
|
||||
}
|
||||
|
||||
// An output formatter for json output of an object
|
||||
type JsonFormatter struct{}
|
||||
|
||||
func (j JsonFormatter) Format(data interface{}) ([]byte, error) {
|
||||
return json.MarshalIndent(data, "", " ")
|
||||
}
|
||||
|
||||
/*
|
||||
func (j JsonFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) error {
|
||||
b, err := j.Format(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ui.Output(string(b))
|
||||
return nil
|
||||
}
|
||||
*/
|
||||
|
||||
// An output formatter for yaml output format of an object
|
||||
type YamlFormatter struct{}
|
||||
|
||||
func (y YamlFormatter) Format(data interface{}) ([]byte, error) {
|
||||
return yaml.Marshal(data)
|
||||
}
|
||||
|
||||
/*
|
||||
func (y YamlFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) error {
|
||||
b, err := y.Format(data)
|
||||
if err == nil {
|
||||
ui.Output(strings.TrimSpace(string(b)))
|
||||
}
|
||||
return err
|
||||
}
|
||||
*/
|
||||
|
||||
// An output formatter for table output of an object
|
||||
type TableFormatter struct{}
|
||||
|
||||
// We don't use this
|
||||
func (t TableFormatter) Format(data interface{}) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
/*
|
||||
func (t TableFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) error {
|
||||
switch data.(type) {
|
||||
case *api.Secret:
|
||||
return t.OutputSecret(ui, secret)
|
||||
case []interface{}:
|
||||
return t.OutputList(ui, secret, data)
|
||||
case []string:
|
||||
return t.OutputList(ui, nil, data)
|
||||
case map[string]interface{}:
|
||||
return t.OutputMap(ui, data.(map[string]interface{}))
|
||||
default:
|
||||
return errors.New("cannot use the table formatter for this type")
|
||||
}
|
||||
}
|
||||
|
||||
func (t TableFormatter) OutputList(ui cli.Ui, secret *api.Secret, data interface{}) error {
|
||||
t.printWarnings(ui, secret)
|
||||
|
||||
switch data.(type) {
|
||||
case []interface{}:
|
||||
case []string:
|
||||
ui.Output(tableOutput(data.([]string), nil))
|
||||
return nil
|
||||
default:
|
||||
return errors.New("error: table formatter cannot output list for this data type")
|
||||
}
|
||||
|
||||
list := data.([]interface{})
|
||||
|
||||
if len(list) > 0 {
|
||||
keys := make([]string, len(list))
|
||||
for i, v := range list {
|
||||
typed, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("%v is not a string", v)
|
||||
}
|
||||
keys[i] = typed
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// Prepend the header
|
||||
keys = append([]string{"Keys"}, keys...)
|
||||
|
||||
ui.Output(tableOutput(keys, &columnize.Config{
|
||||
Delim: hopeDelim,
|
||||
}))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// printWarnings prints any warnings in the secret.
|
||||
func (t TableFormatter) printWarnings(ui cli.Ui, secret *api.Secret) {
|
||||
if secret != nil && len(secret.Warnings) > 0 {
|
||||
ui.Warn("WARNING! The following warnings were returned from Vault:\n")
|
||||
for _, warning := range secret.Warnings {
|
||||
ui.Warn(WrapAtLengthWithPadding(fmt.Sprintf("* %s", warning), 2))
|
||||
ui.Warn("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t TableFormatter) OutputSecret(ui cli.Ui, secret *api.Secret) error {
|
||||
if secret == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
t.printWarnings(ui, secret)
|
||||
|
||||
out := make([]string, 0, 8)
|
||||
if secret.LeaseDuration > 0 {
|
||||
if secret.LeaseID != "" {
|
||||
out = append(out, fmt.Sprintf("lease_id %s %s", hopeDelim, secret.LeaseID))
|
||||
out = append(out, fmt.Sprintf("lease_duration %s %v", hopeDelim, humanDurationInt(secret.LeaseDuration)))
|
||||
out = append(out, fmt.Sprintf("lease_renewable %s %t", hopeDelim, secret.Renewable))
|
||||
} else {
|
||||
// This is probably the generic secret backend which has leases, but we
|
||||
// print them as refresh_interval to reduce confusion.
|
||||
out = append(out, fmt.Sprintf("refresh_interval %s %v", hopeDelim, humanDurationInt(secret.LeaseDuration)))
|
||||
}
|
||||
}
|
||||
|
||||
if secret.Auth != nil {
|
||||
out = append(out, fmt.Sprintf("token %s %s", hopeDelim, secret.Auth.ClientToken))
|
||||
out = append(out, fmt.Sprintf("token_accessor %s %s", hopeDelim, secret.Auth.Accessor))
|
||||
// If the lease duration is 0, it's likely a root token, so output the
|
||||
// duration as "infinity" to clear things up.
|
||||
if secret.Auth.LeaseDuration == 0 {
|
||||
out = append(out, fmt.Sprintf("token_duration %s %s", hopeDelim, "∞"))
|
||||
} else {
|
||||
out = append(out, fmt.Sprintf("token_duration %s %v", hopeDelim, humanDurationInt(secret.Auth.LeaseDuration)))
|
||||
}
|
||||
out = append(out, fmt.Sprintf("token_renewable %s %t", hopeDelim, secret.Auth.Renewable))
|
||||
out = append(out, fmt.Sprintf("token_policies %s %q", hopeDelim, secret.Auth.TokenPolicies))
|
||||
out = append(out, fmt.Sprintf("identity_policies %s %q", hopeDelim, secret.Auth.IdentityPolicies))
|
||||
out = append(out, fmt.Sprintf("policies %s %q", hopeDelim, secret.Auth.Policies))
|
||||
for k, v := range secret.Auth.Metadata {
|
||||
out = append(out, fmt.Sprintf("token_meta_%s %s %v", k, hopeDelim, v))
|
||||
}
|
||||
}
|
||||
|
||||
if secret.WrapInfo != nil {
|
||||
out = append(out, fmt.Sprintf("wrapping_token: %s %s", hopeDelim, secret.WrapInfo.Token))
|
||||
out = append(out, fmt.Sprintf("wrapping_accessor: %s %s", hopeDelim, secret.WrapInfo.Accessor))
|
||||
out = append(out, fmt.Sprintf("wrapping_token_ttl: %s %v", hopeDelim, humanDurationInt(secret.WrapInfo.TTL)))
|
||||
out = append(out, fmt.Sprintf("wrapping_token_creation_time: %s %s", hopeDelim, secret.WrapInfo.CreationTime.String()))
|
||||
out = append(out, fmt.Sprintf("wrapping_token_creation_path: %s %s", hopeDelim, secret.WrapInfo.CreationPath))
|
||||
if secret.WrapInfo.WrappedAccessor != "" {
|
||||
out = append(out, fmt.Sprintf("wrapped_accessor: %s %s", hopeDelim, secret.WrapInfo.WrappedAccessor))
|
||||
}
|
||||
}
|
||||
|
||||
if len(secret.Data) > 0 {
|
||||
keys := make([]string, 0, len(secret.Data))
|
||||
for k := range secret.Data {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
v := secret.Data[k]
|
||||
|
||||
// If the field "looks" like a TTL, print it as a time duration instead.
|
||||
if looksLikeDuration(k) {
|
||||
v = humanDurationInt(v)
|
||||
}
|
||||
|
||||
out = append(out, fmt.Sprintf("%s %s %v", k, hopeDelim, v))
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far and still don't have any data, there's nothing to print,
|
||||
// sorry.
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prepend the header
|
||||
out = append([]string{"Key" + hopeDelim + "Value"}, out...)
|
||||
|
||||
ui.Output(tableOutput(out, &columnize.Config{
|
||||
Delim: hopeDelim,
|
||||
}))
|
||||
return nil
|
||||
}
|
||||
*/
|
||||
|
||||
func (t TableFormatter) OutputMap(ui cli.Ui, data map[string]interface{}) error {
|
||||
out := make([]string, 0, len(data)+1)
|
||||
if len(data) > 0 {
|
||||
keys := make([]string, 0, len(data))
|
||||
for k := range data {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
v := data[k]
|
||||
|
||||
// If the field "looks" like a TTL, print it as a time duration instead.
|
||||
if looksLikeDuration(k) {
|
||||
v = humanDurationInt(v)
|
||||
}
|
||||
|
||||
out = append(out, fmt.Sprintf("%s %s %v", k, hopeDelim, v))
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far and still don't have any data, there's nothing to print,
|
||||
// sorry.
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prepend the header
|
||||
out = append([]string{"Key" + hopeDelim + "Value"}, out...)
|
||||
|
||||
ui.Output(tableOutput(out, &columnize.Config{
|
||||
Delim: hopeDelim,
|
||||
}))
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
// OutputSealStatus will print *api.SealStatusResponse in the CLI according to the format provided
|
||||
func OutputSealStatus(ui cli.Ui, client *api.Client, status *api.SealStatusResponse) int {
|
||||
switch Format(ui) {
|
||||
case "table":
|
||||
default:
|
||||
return OutputData(ui, status)
|
||||
}
|
||||
|
||||
var sealPrefix string
|
||||
if status.RecoverySeal {
|
||||
sealPrefix = "Recovery "
|
||||
}
|
||||
|
||||
out := []string{}
|
||||
out = append(out, "Key | Value")
|
||||
out = append(out, fmt.Sprintf("%sSeal Type | %s", sealPrefix, status.Type))
|
||||
out = append(out, fmt.Sprintf("Initialized | %t", status.Initialized))
|
||||
out = append(out, fmt.Sprintf("Sealed | %t", status.Sealed))
|
||||
out = append(out, fmt.Sprintf("Total %sShares | %d", sealPrefix, status.N))
|
||||
out = append(out, fmt.Sprintf("Threshold | %d", status.T))
|
||||
|
||||
if status.Sealed {
|
||||
out = append(out, fmt.Sprintf("Unseal Progress | %d/%d", status.Progress, status.T))
|
||||
out = append(out, fmt.Sprintf("Unseal Nonce | %s", status.Nonce))
|
||||
}
|
||||
|
||||
if status.Migration {
|
||||
out = append(out, fmt.Sprintf("Seal Migration in Progress | %t", status.Migration))
|
||||
}
|
||||
|
||||
out = append(out, fmt.Sprintf("Version | %s", status.Version))
|
||||
|
||||
if status.ClusterName != "" && status.ClusterID != "" {
|
||||
out = append(out, fmt.Sprintf("Cluster Name | %s", status.ClusterName))
|
||||
out = append(out, fmt.Sprintf("Cluster ID | %s", status.ClusterID))
|
||||
}
|
||||
|
||||
// Mask the 'Vault is sealed' error, since this means HA is enabled, but that
|
||||
// we cannot query for the leader since we are sealed.
|
||||
leaderStatus, err := client.Sys().Leader()
|
||||
if err != nil && strings.Contains(err.Error(), "Vault is sealed") {
|
||||
leaderStatus = &api.LeaderResponse{HAEnabled: true}
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error checking leader status: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Output if HA is enabled
|
||||
out = append(out, fmt.Sprintf("HA Enabled | %t", leaderStatus.HAEnabled))
|
||||
if leaderStatus.HAEnabled {
|
||||
mode := "sealed"
|
||||
if !status.Sealed {
|
||||
out = append(out, fmt.Sprintf("HA Cluster | %s", leaderStatus.LeaderClusterAddress))
|
||||
mode = "standby"
|
||||
showLeaderAddr := false
|
||||
if leaderStatus.IsSelf {
|
||||
mode = "active"
|
||||
} else {
|
||||
if leaderStatus.LeaderAddress == "" {
|
||||
leaderStatus.LeaderAddress = "<none>"
|
||||
}
|
||||
showLeaderAddr = true
|
||||
}
|
||||
out = append(out, fmt.Sprintf("HA Mode | %s", mode))
|
||||
|
||||
// This is down here just to keep ordering consistent
|
||||
if showLeaderAddr {
|
||||
out = append(out, fmt.Sprintf("Active Node Address | %s", leaderStatus.LeaderAddress))
|
||||
}
|
||||
|
||||
if leaderStatus.PerfStandby {
|
||||
out = append(out, fmt.Sprintf("Performance Standby Node | %t", leaderStatus.PerfStandby))
|
||||
out = append(out, fmt.Sprintf("Performance Standby Last Remote WAL | %d", leaderStatus.PerfStandbyLastRemoteWAL))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if leaderStatus.LastWAL != 0 {
|
||||
out = append(out, fmt.Sprintf("Last WAL | %d", leaderStatus.LastWAL))
|
||||
}
|
||||
|
||||
ui.Output(tableOutput(out, nil))
|
||||
return 0
|
||||
}
|
||||
*/
|
||||
|
||||
// looksLikeDuration checks if the given key "k" looks like a duration value.
|
||||
// This is used to pretty-format duration values in responses, especially from
|
||||
// plugins.
|
||||
func looksLikeDuration(k string) bool {
|
||||
return k == "period" || strings.HasSuffix(k, "_period") ||
|
||||
k == "ttl" || strings.HasSuffix(k, "_ttl") ||
|
||||
k == "duration" || strings.HasSuffix(k, "_duration") ||
|
||||
k == "lease_max" || k == "ttl_max"
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
kvbuilder "github.com/hashicorp/vault/internalshared/kv-builder"
|
||||
"github.com/kr/text"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/ryanuber/columnize"
|
||||
)
|
||||
|
||||
// sanitizePath removes any leading or trailing things from a "path".
|
||||
func sanitizePath(s string) string {
|
||||
return ensureNoTrailingSlash(ensureNoLeadingSlash(strings.TrimSpace(s)))
|
||||
}
|
||||
|
||||
// ensureTrailingSlash ensures the given string has a trailing slash.
|
||||
func ensureTrailingSlash(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
for len(s) > 0 && s[len(s)-1] != '/' {
|
||||
s = s + "/"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ensureNoTrailingSlash ensures the given string has a trailing slash.
|
||||
func ensureNoTrailingSlash(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
for len(s) > 0 && s[len(s)-1] == '/' {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ensureNoLeadingSlash ensures the given string has a trailing slash.
|
||||
func ensureNoLeadingSlash(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
for len(s) > 0 && s[0] == '/' {
|
||||
s = s[1:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// columnOuput prints the list of items as a table with no headers.
|
||||
func columnOutput(list []string, c *columnize.Config) string {
|
||||
if len(list) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if c == nil {
|
||||
c = &columnize.Config{}
|
||||
}
|
||||
if c.Glue == "" {
|
||||
c.Glue = " "
|
||||
}
|
||||
if c.Empty == "" {
|
||||
c.Empty = "n/a"
|
||||
}
|
||||
|
||||
return columnize.Format(list, c)
|
||||
}
|
||||
|
||||
// tableOutput prints the list of items as columns, where the first row is
|
||||
// the list of headers.
|
||||
func tableOutput(list []string, c *columnize.Config) string {
|
||||
if len(list) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
delim := "|"
|
||||
if c != nil && c.Delim != "" {
|
||||
delim = c.Delim
|
||||
}
|
||||
|
||||
underline := ""
|
||||
headers := strings.Split(list[0], delim)
|
||||
for i, h := range headers {
|
||||
h = strings.TrimSpace(h)
|
||||
u := strings.Repeat("-", len(h))
|
||||
|
||||
underline = underline + u
|
||||
if i != len(headers)-1 {
|
||||
underline = underline + delim
|
||||
}
|
||||
}
|
||||
|
||||
list = append(list, "")
|
||||
copy(list[2:], list[1:])
|
||||
list[1] = underline
|
||||
|
||||
return columnOutput(list, c)
|
||||
}
|
||||
|
||||
// parseArgsData parses the given args in the format key=value into a map of
|
||||
// the provided arguments. The given reader can also supply key=value pairs.
|
||||
func parseArgsData(stdin io.Reader, args []string) (map[string]interface{}, error) {
|
||||
builder := &kvbuilder.Builder{Stdin: stdin}
|
||||
if err := builder.Add(args...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return builder.Map(), nil
|
||||
}
|
||||
|
||||
// parseArgsDataString parses the args data and returns the values as strings.
|
||||
// If the values cannot be represented as strings, an error is returned.
|
||||
func parseArgsDataString(stdin io.Reader, args []string) (map[string]string, error) {
|
||||
raw, err := parseArgsData(stdin, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result map[string]string
|
||||
if err := mapstructure.WeakDecode(raw, &result); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to convert values to strings")
|
||||
}
|
||||
if result == nil {
|
||||
result = make(map[string]string)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// parseArgsDataStringLists parses the args data and returns the values as
|
||||
// string lists. If the values cannot be represented as strings, an error is
|
||||
// returned.
|
||||
func parseArgsDataStringLists(stdin io.Reader, args []string) (map[string][]string, error) {
|
||||
raw, err := parseArgsData(stdin, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result map[string][]string
|
||||
if err := mapstructure.WeakDecode(raw, &result); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to convert values to strings")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// truncateToSeconds truncates the given duration to the number of seconds. If
|
||||
// the duration is less than 1s, it is returned as 0. The integer represents
|
||||
// the whole number unit of seconds for the duration.
|
||||
func truncateToSeconds(d time.Duration) int {
|
||||
d = d.Truncate(1 * time.Second)
|
||||
|
||||
// Handle the case where someone requested a ridiculously short increment -
|
||||
// increments must be larger than a second.
|
||||
if d < 1*time.Second {
|
||||
return 0
|
||||
}
|
||||
|
||||
return int(d.Seconds())
|
||||
}
|
||||
|
||||
// expandPath takes a filepath and returns the full expanded path, accounting
|
||||
// for user-relative things like ~/.
|
||||
func expandPath(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
e, err := homedir.Expand(s)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// WrapAtLengthWithPadding wraps the given text at the maxLineLength, taking
|
||||
// into account any provided left padding.
|
||||
func WrapAtLengthWithPadding(s string, pad int) string {
|
||||
wrapped := text.Wrap(s, maxLineLength-pad)
|
||||
lines := strings.Split(wrapped, "\n")
|
||||
for i, line := range lines {
|
||||
lines[i] = strings.Repeat(" ", pad) + line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// WrapAtLength wraps the given text to maxLineLength.
|
||||
func WrapAtLength(s string) string {
|
||||
return WrapAtLengthWithPadding(s, 0)
|
||||
}
|
||||
|
||||
// ttlToAPI converts a user-supplied ttl into an API-compatible string. If
|
||||
// the TTL is 0, this returns the empty string. If the TTL is negative, this
|
||||
// returns "system" to indicate to use the system values. Otherwise, the
|
||||
// time.Duration ttl is used.
|
||||
func ttlToAPI(d time.Duration) string {
|
||||
if d == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if d < 0 {
|
||||
return "system"
|
||||
}
|
||||
|
||||
return d.String()
|
||||
}
|
||||
|
||||
// humanDuration prints the time duration without those pesky zeros.
|
||||
func humanDuration(d time.Duration) string {
|
||||
if d == 0 {
|
||||
return "0s"
|
||||
}
|
||||
|
||||
s := d.String()
|
||||
if strings.HasSuffix(s, "m0s") {
|
||||
s = s[:len(s)-2]
|
||||
}
|
||||
if idx := strings.Index(s, "h0m"); idx > 0 {
|
||||
s = s[:idx+1] + s[idx+3:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// humanDurationInt prints the given int as if it were a time.Duration number
|
||||
// of seconds.
|
||||
func humanDurationInt(i interface{}) interface{} {
|
||||
switch i.(type) {
|
||||
case int:
|
||||
return humanDuration(time.Duration(i.(int)) * time.Second)
|
||||
case int64:
|
||||
return humanDuration(time.Duration(i.(int64)) * time.Second)
|
||||
case json.Number:
|
||||
if i, err := i.(json.Number).Int64(); err == nil {
|
||||
return humanDuration(time.Duration(i) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't know what type it is, just return the original value
|
||||
return i
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/sdk/helper/logging"
|
||||
)
|
||||
|
||||
func ProcessLogLevelAndFormat(flagLogLevel, flagLogFormat, configLogLevel, configLogFormat string) (hclog.Level, logging.LogFormat, error) {
|
||||
logFormat := logging.UnspecifiedFormat
|
||||
|
||||
// If the flag wasn't set, check config; if not set use info
|
||||
logLevel := strings.ToLower(strings.TrimSpace(flagLogLevel))
|
||||
if logLevel == NotSetValue {
|
||||
logLevel = strings.ToLower(strings.TrimSpace(configLogLevel))
|
||||
if logLevel == "" {
|
||||
logLevel = "info"
|
||||
}
|
||||
}
|
||||
|
||||
// Set level based off text value
|
||||
var level hclog.Level
|
||||
switch logLevel {
|
||||
case "trace":
|
||||
level = hclog.Trace
|
||||
case "debug":
|
||||
level = hclog.Debug
|
||||
case "notice", "info":
|
||||
level = hclog.Info
|
||||
case "warn", "warning":
|
||||
level = hclog.Warn
|
||||
case "err", "error":
|
||||
level = hclog.Error
|
||||
default:
|
||||
return level, logFormat, fmt.Errorf("unknown log level: %s", logLevel)
|
||||
}
|
||||
|
||||
if flagLogFormat != NotSetValue {
|
||||
var err error
|
||||
logFormat, err = logging.ParseLogFormat(flagLogFormat)
|
||||
if err != nil {
|
||||
return level, logFormat, err
|
||||
}
|
||||
}
|
||||
if logFormat == logging.UnspecifiedFormat {
|
||||
var err error
|
||||
logFormat, err = logging.ParseLogFormat(configLogFormat)
|
||||
if err != nil {
|
||||
return level, logFormat, err
|
||||
}
|
||||
}
|
||||
|
||||
return level, logFormat, nil
|
||||
}
|
||||
|
||||
type GRPCLogFaker struct {
|
||||
Logger hclog.Logger
|
||||
Log bool
|
||||
}
|
||||
|
||||
func (g *GRPCLogFaker) Fatal(args ...interface{}) {
|
||||
g.Logger.Error(fmt.Sprint(args...))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func (g *GRPCLogFaker) Fatalf(format string, args ...interface{}) {
|
||||
g.Logger.Error(fmt.Sprintf(format, args...))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func (g *GRPCLogFaker) Fatalln(args ...interface{}) {
|
||||
g.Logger.Error(fmt.Sprintln(args...))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func (g *GRPCLogFaker) Print(args ...interface{}) {
|
||||
if g.Log && g.Logger.IsDebug() {
|
||||
g.Logger.Debug(fmt.Sprint(args...))
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GRPCLogFaker) Printf(format string, args ...interface{}) {
|
||||
if g.Log && g.Logger.IsDebug() {
|
||||
g.Logger.Debug(fmt.Sprintf(format, args...))
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GRPCLogFaker) Println(args ...interface{}) {
|
||||
if g.Log && g.Logger.IsDebug() {
|
||||
g.Logger.Debug(fmt.Sprintln(args...))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/go-alpnmux"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/vault/internalshared/configutil"
|
||||
"github.com/hashicorp/vault/internalshared/gatedwriter"
|
||||
"github.com/hashicorp/vault/internalshared/reloadutil"
|
||||
"github.com/hashicorp/vault/sdk/helper/logging"
|
||||
"github.com/hashicorp/vault/sdk/helper/mlock"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
"github.com/hashicorp/watchtower/globals"
|
||||
"github.com/hashicorp/watchtower/internal/cmd/commands/controller/listener"
|
||||
"github.com/hashicorp/watchtower/version"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"golang.org/x/net/http/httpproxy"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type ServerListener struct {
|
||||
Mux *alpnmux.ALPNMux
|
||||
Config *configutil.Listener
|
||||
HTTPServer *http.Server
|
||||
}
|
||||
|
||||
type BaseServer struct {
|
||||
InfoKeys []string
|
||||
Info map[string]string
|
||||
|
||||
logOutput io.Writer
|
||||
GatedWriter *gatedwriter.Writer
|
||||
Logger hclog.Logger
|
||||
CombineLogs bool
|
||||
AllLoggers []hclog.Logger
|
||||
LogLevel hclog.Level
|
||||
|
||||
ControllerKMS wrapping.Wrapper
|
||||
WorkerAuthKMS wrapping.Wrapper
|
||||
SecureRandomReader io.Reader
|
||||
|
||||
InmemSink *metrics.InmemSink
|
||||
PrometheusEnabled bool
|
||||
|
||||
ReloadFuncsLock *sync.RWMutex
|
||||
ReloadFuncs map[string][]reloadutil.ReloadFunc
|
||||
|
||||
ShutdownFuncs []func() error
|
||||
|
||||
Listeners []*ServerListener
|
||||
|
||||
DevDatabasePassword string
|
||||
DevDatabaseName string
|
||||
DevDatabasePort string
|
||||
|
||||
dockertestPool *dockertest.Pool
|
||||
dockertestResource *dockertest.Resource
|
||||
}
|
||||
|
||||
func NewBaseServer() *BaseServer {
|
||||
return &BaseServer{
|
||||
InfoKeys: make([]string, 0, 20),
|
||||
Info: make(map[string]string),
|
||||
AllLoggers: make([]hclog.Logger, 0),
|
||||
SecureRandomReader: rand.Reader,
|
||||
ReloadFuncsLock: new(sync.RWMutex),
|
||||
ReloadFuncs: make(map[string][]reloadutil.ReloadFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseServer) SetupLogging(flagLogLevel, flagLogFormat, configLogLevel, configLogFormat string) error {
|
||||
b.logOutput = os.Stderr
|
||||
if b.CombineLogs {
|
||||
b.logOutput = os.Stdout
|
||||
}
|
||||
b.GatedWriter = gatedwriter.NewWriter(b.logOutput)
|
||||
|
||||
// Set up logging
|
||||
logLevel, logFormat, err := ProcessLogLevelAndFormat(flagLogLevel, flagLogFormat, configLogLevel, configLogFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Logger = hclog.New(&hclog.LoggerOptions{
|
||||
Output: b.GatedWriter,
|
||||
Level: logLevel,
|
||||
// Note that if logFormat is either unspecified or standard, then
|
||||
// the resulting logger's format will be standard.
|
||||
JSONFormat: logFormat == logging.JSONFormat,
|
||||
})
|
||||
// Create allLoggers which can be used to HUP the various derived loggers
|
||||
b.AllLoggers = []hclog.Logger{b.Logger}
|
||||
|
||||
// create GRPC logger
|
||||
namedGRPCLogFaker := b.Logger.Named("grpclogfaker")
|
||||
b.AllLoggers = append(b.AllLoggers, namedGRPCLogFaker)
|
||||
grpclog.SetLogger(&GRPCLogFaker{
|
||||
Logger: namedGRPCLogFaker,
|
||||
Log: os.Getenv("WATCHTOWER_GRPC_LOGGING") != "",
|
||||
})
|
||||
|
||||
b.Info["log level"] = logLevel.String()
|
||||
b.InfoKeys = append(b.InfoKeys, "log level")
|
||||
|
||||
b.LogLevel = logLevel
|
||||
|
||||
// log proxy settings
|
||||
proxyCfg := httpproxy.FromEnvironment()
|
||||
b.Logger.Info("proxy environment", "http_proxy", proxyCfg.HTTPProxy,
|
||||
"https_proxy", proxyCfg.HTTPSProxy, "no_proxy", proxyCfg.NoProxy)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseServer) ReleaseLogGate() {
|
||||
// Release the log gate.
|
||||
b.Logger.(hclog.OutputResettable).ResetOutputWithFlush(&hclog.LoggerOptions{
|
||||
Output: b.logOutput,
|
||||
}, b.GatedWriter)
|
||||
}
|
||||
|
||||
func (b *BaseServer) StorePidFile(pidPath string) error {
|
||||
// Quit fast if no pidfile
|
||||
if pidPath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open the PID file
|
||||
pidFile, err := os.OpenFile(pidPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open pid file: %w", err)
|
||||
}
|
||||
defer pidFile.Close()
|
||||
|
||||
// Write out the PID
|
||||
pid := os.Getpid()
|
||||
_, err = pidFile.WriteString(fmt.Sprintf("%d", pid))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not write to pid file: %w", err)
|
||||
}
|
||||
|
||||
b.ShutdownFuncs = append(b.ShutdownFuncs, func() error {
|
||||
if err := b.RemovePidFile(pidPath); err != nil {
|
||||
return fmt.Errorf("Error deleting the PID file: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseServer) RemovePidFile(pidPath string) error {
|
||||
if pidPath == "" {
|
||||
return nil
|
||||
}
|
||||
return os.Remove(pidPath)
|
||||
}
|
||||
|
||||
func (b *BaseServer) SetupMetrics(ui cli.Ui, telemetry *configutil.Telemetry) error {
|
||||
// TODO: Figure out a user-agent we want to use for the last param
|
||||
// TODO: Do we want different names for different components?
|
||||
var err error
|
||||
b.InmemSink, b.PrometheusEnabled, err = configutil.SetupTelemetry(telemetry, ui, "watchtower", "Watchtower", "watchtower")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error initializing telemetry: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseServer) PrintInfo(ui cli.Ui, mode string) {
|
||||
b.InfoKeys = append(b.InfoKeys, "version")
|
||||
verInfo := version.GetVersion()
|
||||
b.Info["version"] = verInfo.FullVersionNumber(false)
|
||||
if verInfo.Revision != "" {
|
||||
b.Info["version sha"] = strings.Trim(verInfo.Revision, "'")
|
||||
b.InfoKeys = append(b.InfoKeys, "version sha")
|
||||
}
|
||||
b.InfoKeys = append(b.InfoKeys, "cgo")
|
||||
b.Info["cgo"] = "disabled"
|
||||
if version.CgoEnabled {
|
||||
b.Info["cgo"] = "enabled"
|
||||
}
|
||||
|
||||
// Server configuration output
|
||||
padding := 24
|
||||
sort.Strings(b.InfoKeys)
|
||||
ui.Output(fmt.Sprintf("==> Watchtower %s configuration:\n", mode))
|
||||
for _, k := range b.InfoKeys {
|
||||
ui.Output(fmt.Sprintf(
|
||||
"%s%s: %s",
|
||||
strings.Repeat(" ", padding-len(k)),
|
||||
strings.Title(k),
|
||||
b.Info[k]))
|
||||
}
|
||||
ui.Output("")
|
||||
|
||||
// Output the header that the server has started
|
||||
if !b.CombineLogs {
|
||||
ui.Output(fmt.Sprintf("==> Watchtower %s started! Log data will stream in below:\n", mode))
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseServer) SetupListeners(ui cli.Ui, config *configutil.SharedConfig) error {
|
||||
// Initialize the listeners
|
||||
b.Listeners = make([]*ServerListener, 0, len(config.Listeners))
|
||||
// Make sure we close everything before we exit
|
||||
// If we successfully started a controller we'll have done this anyways so
|
||||
// we ignore errors
|
||||
b.ShutdownFuncs = append(b.ShutdownFuncs, func() error {
|
||||
for _, ln := range b.Listeners {
|
||||
ln.Mux.Close()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
b.ReloadFuncsLock.Lock()
|
||||
defer b.ReloadFuncsLock.Unlock()
|
||||
|
||||
for i, lnConfig := range config.Listeners {
|
||||
// Override for now
|
||||
// TODO: Way to configure
|
||||
lnConfig.TLSCipherSuites = []uint16{
|
||||
// 1.3
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
// 1.2
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
}
|
||||
|
||||
lnMux, props, reloadFunc, err := listener.NewListener(lnConfig, b.GatedWriter, ui)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error initializing listener of type %s: %w", lnConfig.Type, err)
|
||||
}
|
||||
|
||||
// X-Forwarded-For props
|
||||
{
|
||||
if len(lnConfig.XForwardedForAuthorizedAddrs) > 0 {
|
||||
props["x_forwarded_for_authorized_addrs"] = fmt.Sprintf("%v", lnConfig.XForwardedForAuthorizedAddrs)
|
||||
props["x_forwarded_for_reject_not_present"] = strconv.FormatBool(lnConfig.XForwardedForRejectNotPresent)
|
||||
props["x_forwarded_for_hop_skips"] = "0"
|
||||
}
|
||||
|
||||
if lnConfig.XForwardedForHopSkips > 0 {
|
||||
props["x_forwarded_for_hop_skips"] = fmt.Sprintf("%d", lnConfig.XForwardedForHopSkips)
|
||||
}
|
||||
}
|
||||
|
||||
if reloadFunc != nil {
|
||||
relSlice := b.ReloadFuncs["listener|"+lnConfig.Type]
|
||||
relSlice = append(relSlice, reloadFunc)
|
||||
b.ReloadFuncs["listener|"+lnConfig.Type] = relSlice
|
||||
}
|
||||
|
||||
if lnConfig.MaxRequestSize == 0 {
|
||||
lnConfig.MaxRequestSize = globals.DefaultMaxRequestSize
|
||||
}
|
||||
props["max_request_size"] = fmt.Sprintf("%d", lnConfig.MaxRequestSize)
|
||||
|
||||
if lnConfig.MaxRequestDuration == 0 {
|
||||
lnConfig.MaxRequestDuration = globals.DefaultMaxRequestDuration
|
||||
}
|
||||
props["max_request_duration"] = fmt.Sprintf("%s", lnConfig.MaxRequestDuration.String())
|
||||
|
||||
b.Listeners = append(b.Listeners, &ServerListener{
|
||||
Mux: lnMux,
|
||||
Config: lnConfig,
|
||||
})
|
||||
|
||||
// Store the listener props for output later
|
||||
key := fmt.Sprintf("listener %d", i+1)
|
||||
propsList := make([]string, 0, len(props))
|
||||
for k, v := range props {
|
||||
propsList = append(propsList, fmt.Sprintf(
|
||||
"%s: %q", k, v))
|
||||
}
|
||||
sort.Strings(propsList)
|
||||
b.InfoKeys = append(b.InfoKeys, key)
|
||||
b.Info[key] = fmt.Sprintf(
|
||||
"%s (%s)", lnConfig.Type, strings.Join(propsList, ", "))
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseServer) SetupKMSes(ui cli.Ui, config *configutil.SharedConfig, size int) error {
|
||||
switch len(config.Seals) {
|
||||
case size:
|
||||
for _, kms := range config.Seals {
|
||||
purpose := strings.ToLower(kms.Purpose)
|
||||
switch purpose {
|
||||
case "":
|
||||
return errors.New("KMS block missing 'purpose'")
|
||||
case "controller", "worker-auth":
|
||||
default:
|
||||
return fmt.Errorf("Unknown KMS purpose %q", kms.Purpose)
|
||||
}
|
||||
|
||||
kmsLogger := b.Logger.ResetNamed(fmt.Sprintf("kms-%s-%s", purpose, kms.Type))
|
||||
|
||||
b.AllLoggers = append(b.AllLoggers, kmsLogger)
|
||||
wrapper, wrapperConfigError := configutil.ConfigureWrapper(kms, &b.InfoKeys, &b.Info, kmsLogger)
|
||||
if wrapperConfigError != nil {
|
||||
if !errwrap.ContainsType(wrapperConfigError, new(logical.KeyNotFoundError)) {
|
||||
return fmt.Errorf(
|
||||
"Error parsing KMS configuration: %s", wrapperConfigError)
|
||||
}
|
||||
}
|
||||
if wrapper == nil {
|
||||
return fmt.Errorf(
|
||||
"After configuration nil KMS returned, KMS type was %s", kms.Type)
|
||||
}
|
||||
|
||||
if purpose == "controller" {
|
||||
b.ControllerKMS = wrapper
|
||||
} else {
|
||||
b.WorkerAuthKMS = wrapper
|
||||
}
|
||||
|
||||
// Ensure that the seal finalizer is called, even if using verify-only
|
||||
b.ShutdownFuncs = append(b.ShutdownFuncs, func() error {
|
||||
if err := wrapper.Finalize(context.Background()); err != nil {
|
||||
return fmt.Errorf("Error finalizing kms of type %s and purpose %s: %v", kms.Type, kms.Purpose, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("Wrong number of KMS blocks provided; expected %d, got %d", size, len(config.Seals))
|
||||
}
|
||||
|
||||
// prepare a secure random reader
|
||||
var err error
|
||||
b.SecureRandomReader, err = configutil.CreateSecureRandomReaderFunc(config, b.ControllerKMS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This might not be the _best_ place for this but we have access to config
|
||||
// here
|
||||
b.Info["mlock"] = fmt.Sprintf(
|
||||
"supported: %v, enabled: %v",
|
||||
mlock.Supported(), !config.DisableMlock && mlock.Supported())
|
||||
b.InfoKeys = append(b.InfoKeys, "mlock")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseServer) RunShutdownFuncs(ui cli.Ui) {
|
||||
for _, f := range b.ShutdownFuncs {
|
||||
if err := f(); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error running a shutdown task: %s", err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseServer) CreateDevDatabase() error {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not connect to docker: %w", err)
|
||||
}
|
||||
|
||||
resource, err := pool.Run("postgres", "latest", []string{"POSTGRES_PASSWORD=secret", "POSTGRES_DB=watchtower"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not start resource: %w", err)
|
||||
}
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
db, err := sql.Open("postgres", fmt.Sprintf("postgres://postgres:secret@localhost:%s/watchtower?sslmode=disable", resource.GetPort("5432/tcp")))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening postgres dev container: %w", err)
|
||||
}
|
||||
var mErr *multierror.Error
|
||||
if err := db.Ping(); err != nil {
|
||||
mErr = multierror.Append(fmt.Errorf("error pinging dev database container: %w", err))
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
mErr = multierror.Append(fmt.Errorf("error closing dev database container: %w", err))
|
||||
}
|
||||
return mErr.ErrorOrNil()
|
||||
}); err != nil {
|
||||
return fmt.Errorf("could not connect to docker: %w", err)
|
||||
}
|
||||
|
||||
b.dockertestPool = pool
|
||||
b.dockertestResource = resource
|
||||
b.DevDatabaseName = "watchtower"
|
||||
b.DevDatabasePassword = "secret"
|
||||
b.DevDatabasePort = resource.GetPort("5432/tcp")
|
||||
|
||||
b.InfoKeys = append(b.InfoKeys, "dev database name", "dev database password", "dev database port")
|
||||
b.Info["dev database name"] = "watchtower"
|
||||
b.Info["dev database password"] = "secret"
|
||||
b.Info["dev database port"] = b.DevDatabasePort
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseServer) DestroyDevDatabase() error {
|
||||
if b.dockertestPool == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if b.dockertestResource == nil {
|
||||
return errors.New("found a pool for dev database container but no resource")
|
||||
}
|
||||
|
||||
return b.dockertestPool.Purge(b.dockertestResource)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/cmd/base"
|
||||
"github.com/hashicorp/watchtower/internal/cmd/commands/controller"
|
||||
"github.com/hashicorp/watchtower/internal/cmd/commands/dev"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
// Commands is the mapping of all the available commands.
|
||||
var Commands map[string]cli.CommandFactory
|
||||
|
||||
func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
/*
|
||||
getBaseCommand := func() *base.BaseCommand {
|
||||
return &base.BaseCommand{
|
||||
UI: ui,
|
||||
Address: runOpts.Address,
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
Commands = map[string]cli.CommandFactory{
|
||||
"controller": func() (cli.Command, error) {
|
||||
return &controller.ControllerCommand{
|
||||
BaseCommand: &base.BaseCommand{
|
||||
UI: serverCmdUi,
|
||||
Address: runOpts.Address,
|
||||
},
|
||||
ShutdownCh: MakeShutdownCh(),
|
||||
SighupCh: MakeSighupCh(),
|
||||
SigUSR2Ch: MakeSigUSR2Ch(),
|
||||
}, nil
|
||||
},
|
||||
"dev": func() (cli.Command, error) {
|
||||
return &dev.DevCommand{
|
||||
BaseCommand: &base.BaseCommand{
|
||||
UI: serverCmdUi,
|
||||
Address: runOpts.Address,
|
||||
},
|
||||
ShutdownCh: MakeShutdownCh(),
|
||||
SighupCh: MakeSighupCh(),
|
||||
SigUSR2Ch: MakeSigUSR2Ch(),
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MakeShutdownCh returns a channel that can be used for shutdown
|
||||
// notifications for commands. This channel will send a message for every
|
||||
// SIGINT or SIGTERM received.
|
||||
func MakeShutdownCh() chan struct{} {
|
||||
resultCh := make(chan struct{})
|
||||
|
||||
shutdownCh := make(chan os.Signal, 4)
|
||||
signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-shutdownCh
|
||||
close(resultCh)
|
||||
}()
|
||||
return resultCh
|
||||
}
|
||||
|
||||
// MakeSighupCh returns a channel that can be used for SIGHUP
|
||||
// reloading. This channel will send a message for every
|
||||
// SIGHUP received.
|
||||
func MakeSighupCh() chan struct{} {
|
||||
resultCh := make(chan struct{})
|
||||
|
||||
signalCh := make(chan os.Signal, 4)
|
||||
signal.Notify(signalCh, syscall.SIGHUP)
|
||||
go func() {
|
||||
for {
|
||||
<-signalCh
|
||||
resultCh <- struct{}{}
|
||||
}
|
||||
}()
|
||||
return resultCh
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/hashicorp/hcl"
|
||||
"github.com/hashicorp/vault/internalshared/configutil"
|
||||
)
|
||||
|
||||
// Config is the configuration for the watchtower controller
|
||||
type Config struct {
|
||||
*configutil.SharedConfig `hcl:"-"`
|
||||
}
|
||||
|
||||
// DevConfig is a Config that is used for dev mode of Watchtower
|
||||
func DevConfig() (*Config, error) {
|
||||
randBuf := new(bytes.Buffer)
|
||||
n, err := randBuf.ReadFrom(&io.LimitedReader{
|
||||
R: rand.Reader,
|
||||
N: 64,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n != 64 {
|
||||
return nil, fmt.Errorf("expected to read 64 bytes, read %d", n)
|
||||
}
|
||||
controllerKey := base64.StdEncoding.EncodeToString(randBuf.Bytes()[0:32])
|
||||
workerAuthKey := base64.StdEncoding.EncodeToString(randBuf.Bytes()[32:64])
|
||||
|
||||
hclStr := `
|
||||
disable_mlock = true
|
||||
|
||||
listener "tcp" {
|
||||
tls_disable = true
|
||||
proxy_protocol_behavior = "allow_authorized"
|
||||
proxy_protocol_authorized_addrs = "127.0.0.1"
|
||||
}
|
||||
|
||||
telemetry {
|
||||
prometheus_retention_time = "24h"
|
||||
disable_hostname = true
|
||||
}
|
||||
|
||||
kms "aead" {
|
||||
purpose = "controller"
|
||||
aead_type = "aes-gcm"
|
||||
key = "%s"
|
||||
}
|
||||
|
||||
kms "aead" {
|
||||
purpose = "worker-auth"
|
||||
aead_type = "aes-gcm"
|
||||
key = "%s"
|
||||
}
|
||||
`
|
||||
|
||||
hclStr = fmt.Sprintf(hclStr, controllerKey, workerAuthKey)
|
||||
parsed, err := ParseConfig(hclStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing dev config: %w", err)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
SharedConfig: new(configutil.SharedConfig),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadConfigFile loads the configuration from the given file.
|
||||
func LoadConfigFile(path string) (*Config, error) {
|
||||
// Read the file
|
||||
d, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf, err := ParseConfig(string(d))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func ParseConfig(d string) (*Config, error) {
|
||||
obj, err := hcl.Parse(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Nothing to do here right now
|
||||
result := NewConfig()
|
||||
if err := hcl.DecodeObject(result, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sharedConfig, err := configutil.ParseConfig(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.SharedConfig = sharedConfig
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Sanitized returns a copy of the config with all values that are considered
|
||||
// sensitive stripped. It also strips all `*Raw` values that are mainly
|
||||
// used for parsing.
|
||||
//
|
||||
// Specifically, the fields that this method strips are:
|
||||
// - KMS.Config
|
||||
// - Telemetry.CirconusAPIToken
|
||||
func (c *Config) Sanitized() map[string]interface{} {
|
||||
// Create shared config if it doesn't exist (e.g. in tests) so that map
|
||||
// keys are actually populated
|
||||
if c.SharedConfig == nil {
|
||||
c.SharedConfig = new(configutil.SharedConfig)
|
||||
}
|
||||
sharedResult := c.SharedConfig.Sanitized()
|
||||
result := map[string]interface{}{}
|
||||
for k, v := range sharedResult {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/vault/sdk/helper/mlock"
|
||||
"github.com/hashicorp/watchtower/globals"
|
||||
"github.com/hashicorp/watchtower/internal/cmd/base"
|
||||
"github.com/hashicorp/watchtower/internal/cmd/commands/controller/config"
|
||||
"github.com/hashicorp/watchtower/internal/servers/controller"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
var _ cli.Command = (*ControllerCommand)(nil)
|
||||
var _ cli.CommandAutocomplete = (*ControllerCommand)(nil)
|
||||
|
||||
var memProfilerEnabled = false
|
||||
|
||||
type ControllerCommand struct {
|
||||
*base.BaseCommand
|
||||
*base.BaseServer
|
||||
|
||||
ShutdownCh chan struct{}
|
||||
SighupCh chan struct{}
|
||||
ReloadedCh chan struct{}
|
||||
SigUSR2Ch chan struct{}
|
||||
|
||||
cleanupGuard sync.Once
|
||||
|
||||
Config *config.Config
|
||||
|
||||
flagConfig string
|
||||
flagLogLevel string
|
||||
flagLogFormat string
|
||||
flagDev bool
|
||||
flagDevAdminToken string
|
||||
flagDevControllerListenAddr string
|
||||
flagCombineLogs bool
|
||||
}
|
||||
|
||||
func (c *ControllerCommand) Synopsis() string {
|
||||
return "Start a Watchtower controller"
|
||||
}
|
||||
|
||||
func (c *ControllerCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: watchtower controller [options]
|
||||
|
||||
Start a controller with a configuration file:
|
||||
|
||||
$ watchtower controller -config=/etc/controller/config.hcl
|
||||
|
||||
For a full list of examples, please see the documentation.
|
||||
|
||||
` + c.Flags().Help()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *ControllerCommand) Flags() *base.FlagSets {
|
||||
set := c.FlagSet(base.FlagSetHTTP)
|
||||
|
||||
f := set.NewFlagSet("Command Options")
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "config",
|
||||
Target: &c.flagConfig,
|
||||
Completion: complete.PredictOr(
|
||||
complete.PredictFiles("*.hcl"),
|
||||
complete.PredictFiles("*.json"),
|
||||
),
|
||||
Usage: "Path to a configuration file.",
|
||||
})
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "log-level",
|
||||
Target: &c.flagLogLevel,
|
||||
Default: base.NotSetValue,
|
||||
EnvVar: "WATCHTOWER_LOG_LEVEL",
|
||||
Completion: complete.PredictSet("trace", "debug", "info", "warn", "err"),
|
||||
Usage: "Log verbosity level. Supported values (in order of more detail to less) are " +
|
||||
"\"trace\", \"debug\", \"info\", \"warn\", and \"err\".",
|
||||
})
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "log-format",
|
||||
Target: &c.flagLogFormat,
|
||||
Default: base.NotSetValue,
|
||||
Completion: complete.PredictSet("standard", "json"),
|
||||
Usage: `Log format. Supported values are "standard" and "json".`,
|
||||
})
|
||||
|
||||
f = set.NewFlagSet("Dev Options")
|
||||
|
||||
f.BoolVar(&base.BoolVar{
|
||||
Name: "dev",
|
||||
Target: &c.flagDev,
|
||||
Usage: "Enable development mode. As the name implies, do not run \"dev\" mode in " +
|
||||
"production.",
|
||||
})
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "dev-admin-token",
|
||||
Target: &c.flagDevAdminToken,
|
||||
Default: "",
|
||||
EnvVar: "WATCHTWER_DEV_ADMIN_TOKEN",
|
||||
Usage: "Initial admin token. This only applies when running in \"dev\" " +
|
||||
"mode.",
|
||||
})
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "dev-listen-address",
|
||||
Target: &c.flagDevControllerListenAddr,
|
||||
Default: "127.0.0.1:9200",
|
||||
EnvVar: "WATCHTOWER_DEV_CONTROLLER_LISTEN_ADDRESS",
|
||||
Usage: "Address to bind the controller to in \"dev\" mode.",
|
||||
})
|
||||
|
||||
f.BoolVar(&base.BoolVar{
|
||||
Name: "combine-logs",
|
||||
Target: &c.flagCombineLogs,
|
||||
Default: false,
|
||||
Usage: "If set, both startup information and logs will be sent to stdout. If not set (the default), startup information will go to stdout and logs will be sent to stderr.",
|
||||
})
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *ControllerCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *ControllerCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *ControllerCommand) Run(args []string) int {
|
||||
c.BaseServer = base.NewBaseServer()
|
||||
c.CombineLogs = c.flagCombineLogs
|
||||
|
||||
if result := c.ParseFlagsAndConfig(args); result > 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
if memProfilerEnabled {
|
||||
c.startMemProfiler()
|
||||
}
|
||||
|
||||
if err := c.SetupLogging(c.flagLogLevel, c.flagLogFormat, c.Config.LogLevel, c.Config.LogFormat); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := c.SetupMetrics(c.UI, c.Config.Telemetry); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := c.SetupKMSes(c.UI, c.Config.SharedConfig, 2); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
if c.Config.DefaultMaxRequestDuration != 0 {
|
||||
globals.DefaultMaxRequestDuration = c.Config.DefaultMaxRequestDuration
|
||||
}
|
||||
|
||||
// If mlockall(2) isn't supported, show a warning. We disable this in dev
|
||||
// because it is quite scary to see when first using Vault. We also disable
|
||||
// this if the user has explicitly disabled mlock in configuration.
|
||||
if !c.flagDev && !c.Config.DisableMlock && !mlock.Supported() {
|
||||
c.UI.Warn(base.WrapAtLength(
|
||||
"WARNING! mlock is not supported on this system! An mlockall(2)-like " +
|
||||
"syscall to prevent memory from being swapped to disk is not " +
|
||||
"supported on this system. For better security, only run Vault on " +
|
||||
"systems where this call is supported. If you are running Vault " +
|
||||
"in a Docker container, provide the IPC_LOCK cap to the container."))
|
||||
}
|
||||
|
||||
if err := c.SetupListeners(c.UI, c.Config.SharedConfig); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Write out the PID to the file now that server has successfully started
|
||||
if err := c.StorePidFile(c.Config.PidFile); err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error storing PID: %w", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if c.flagDev {
|
||||
if err := c.CreateDevDatabase(); err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error creating dev database container: %s", err.Error()))
|
||||
return 1
|
||||
}
|
||||
c.ShutdownFuncs = append(c.ShutdownFuncs, c.DestroyDevDatabase)
|
||||
}
|
||||
|
||||
defer c.RunShutdownFuncs(c.UI)
|
||||
|
||||
c.PrintInfo(c.UI, "controller")
|
||||
c.ReleaseLogGate()
|
||||
|
||||
return c.Start()
|
||||
}
|
||||
|
||||
func (c *ControllerCommand) ParseFlagsAndConfig(args []string) int {
|
||||
var err error
|
||||
|
||||
f := c.Flags()
|
||||
|
||||
if err = f.Parse(args); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Validation
|
||||
if !c.flagDev {
|
||||
switch {
|
||||
case len(c.flagConfig) == 0:
|
||||
c.UI.Error("Must specify a config file using -config")
|
||||
return 1
|
||||
case c.flagDevAdminToken != "":
|
||||
c.UI.Warn(base.WrapAtLength(
|
||||
"You cannot specify a custom admin token ID outside of \"dev\" mode. " +
|
||||
"Your request has been ignored."))
|
||||
c.flagDevAdminToken = ""
|
||||
}
|
||||
|
||||
if len(c.flagConfig) == 0 {
|
||||
c.UI.Error("Must supply a config file with -config")
|
||||
return 1
|
||||
}
|
||||
c.Config, err = config.LoadConfigFile(c.flagConfig)
|
||||
if err != nil {
|
||||
c.UI.Error("Error parsing config: " + err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
} else {
|
||||
c.Config, err = config.DevConfig()
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error creating dev config: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if c.flagDevControllerListenAddr != "" {
|
||||
c.Config.Listeners[0].Address = c.flagDevControllerListenAddr
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *ControllerCommand) Start() int {
|
||||
// Instantiate the wait group
|
||||
controllerConfig := &controller.Config{
|
||||
RawConfig: c.Config,
|
||||
BaseServer: c.BaseServer,
|
||||
}
|
||||
|
||||
// Initialize the core
|
||||
controller, err := controller.NewController(controllerConfig)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error initializing controller: %w", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := controller.Start(); err != nil {
|
||||
c.UI.Error(fmt.Sprint("Error starting controller: %w", err))
|
||||
if err := controller.Shutdown(); err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error with controller shutdown: %w", err))
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// Wait for shutdown
|
||||
shutdownTriggered := false
|
||||
|
||||
for !shutdownTriggered {
|
||||
select {
|
||||
case <-c.ShutdownCh:
|
||||
c.UI.Output("==> Watchtower controller shutdown triggered")
|
||||
|
||||
if err := controller.Shutdown(); err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error with controller shutdown: %w", err))
|
||||
}
|
||||
|
||||
shutdownTriggered = true
|
||||
|
||||
case <-c.SighupCh:
|
||||
c.UI.Output("==> Watchtower controller reload triggered")
|
||||
|
||||
// Check for new log level
|
||||
var level hclog.Level
|
||||
var err error
|
||||
var newConf *config.Config
|
||||
|
||||
if c.flagConfig == "" {
|
||||
goto RUNRELOADFUNCS
|
||||
}
|
||||
|
||||
newConf, err = config.LoadConfigFile(c.flagConfig)
|
||||
if err != nil {
|
||||
c.Logger.Error("could not reload config", "path", c.flagConfig, "error", err)
|
||||
goto RUNRELOADFUNCS
|
||||
}
|
||||
|
||||
// Ensure at least one config was found.
|
||||
if newConf == nil {
|
||||
c.Logger.Error("no config found at reload time")
|
||||
goto RUNRELOADFUNCS
|
||||
}
|
||||
|
||||
// Commented out until we need this
|
||||
//controller.SetConfig(config)
|
||||
|
||||
if newConf.LogLevel != "" {
|
||||
configLogLevel := strings.ToLower(strings.TrimSpace(newConf.LogLevel))
|
||||
switch configLogLevel {
|
||||
case "trace":
|
||||
level = hclog.Trace
|
||||
case "debug":
|
||||
level = hclog.Debug
|
||||
case "notice", "info", "":
|
||||
level = hclog.Info
|
||||
case "warn", "warning":
|
||||
level = hclog.Warn
|
||||
case "err", "error":
|
||||
level = hclog.Error
|
||||
default:
|
||||
c.Logger.Error("unknown log level found on reload", "level", newConf.LogLevel)
|
||||
goto RUNRELOADFUNCS
|
||||
}
|
||||
controller.SetLogLevel(level)
|
||||
}
|
||||
|
||||
RUNRELOADFUNCS:
|
||||
if err := c.Reload(); err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error(s) were encountered during controller reload: %w", err))
|
||||
}
|
||||
|
||||
case <-c.SigUSR2Ch:
|
||||
buf := make([]byte, 32*1024*1024)
|
||||
n := runtime.Stack(buf[:], true)
|
||||
c.Logger.Info("goroutine trace", "stack", string(buf[:n]))
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *ControllerCommand) Reload() error {
|
||||
c.ReloadFuncsLock.RLock()
|
||||
defer c.ReloadFuncsLock.RUnlock()
|
||||
|
||||
var reloadErrors *multierror.Error
|
||||
|
||||
for k, relFuncs := range c.ReloadFuncs {
|
||||
switch {
|
||||
case strings.HasPrefix(k, "listener|"):
|
||||
for _, relFunc := range relFuncs {
|
||||
if relFunc != nil {
|
||||
if err := relFunc(); err != nil {
|
||||
reloadErrors = multierror.Append(reloadErrors, fmt.Errorf("error encountered reloading listener: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send a message that we reloaded. This prevents "guessing" sleep times
|
||||
// in tests.
|
||||
select {
|
||||
case c.ReloadedCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
|
||||
return reloadErrors.ErrorOrNil()
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// +build !memprofiler
|
||||
|
||||
package controller
|
||||
|
||||
func (c *ControllerCommand) startMemProfiler() {
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// +build memprofiler
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
memProfilerEnabled = true
|
||||
}
|
||||
|
||||
func (c *ControllerCommand) startMemProfiler() {
|
||||
profileDir := filepath.Join(os.TempDir(), "watchtowerprof")
|
||||
if err := os.MkdirAll(profileDir, 0700); err != nil {
|
||||
c.logger.Debug("could not create profile directory", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
filename := filepath.Join(profileDir, time.Now().UTC().Format("20060102_150405")) + ".pprof"
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
c.logger.Debug("could not create memory profile", "error", err)
|
||||
}
|
||||
runtime.GC()
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
c.logger.Debug("could not write memory profile", "error", err)
|
||||
}
|
||||
f.Close()
|
||||
c.logger.Debug("wrote memory profile", "filename", filename)
|
||||
time.Sleep(5 * time.Minute)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/pires/go-proxyproto"
|
||||
|
||||
// We must import sha512 so that it registers with the runtime so that
|
||||
// certificates that use it can be parsed.
|
||||
_ "crypto/sha512"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/hashicorp/go-alpnmux"
|
||||
"github.com/hashicorp/vault/internalshared/configutil"
|
||||
"github.com/hashicorp/vault/internalshared/reloadutil"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
// ListenerFactory is the factory function to create a listener.
|
||||
type ListenerFactory func(*configutil.Listener, io.Writer, cli.Ui) (*alpnmux.ALPNMux, map[string]string, reloadutil.ReloadFunc, error)
|
||||
|
||||
// BuiltinListeners is the list of built-in listener types.
|
||||
var BuiltinListeners = map[string]ListenerFactory{
|
||||
"tcp": tcpListenerFactory,
|
||||
}
|
||||
|
||||
// NewListener creates a new listener of the given type with the given
|
||||
// configuration. The type is looked up in the BuiltinListeners map.
|
||||
func NewListener(l *configutil.Listener, w io.Writer, ui cli.Ui) (*alpnmux.ALPNMux, map[string]string, reloadutil.ReloadFunc, error) {
|
||||
f, ok := BuiltinListeners[l.Type]
|
||||
if !ok {
|
||||
return nil, nil, nil, fmt.Errorf("unknown listener type: %q", l.Type)
|
||||
}
|
||||
|
||||
return f(l, w, ui)
|
||||
}
|
||||
|
||||
func listenerWrapProxy(ln net.Listener, l *configutil.Listener) (net.Listener, error) {
|
||||
behavior := l.ProxyProtocolBehavior
|
||||
if behavior == "" {
|
||||
return ln, nil
|
||||
}
|
||||
|
||||
authorizedAddrs := make([]string, 0, len(l.ProxyProtocolAuthorizedAddrs))
|
||||
for _, v := range l.ProxyProtocolAuthorizedAddrs {
|
||||
authorizedAddrs = append(authorizedAddrs, v.String())
|
||||
}
|
||||
|
||||
var policyFunc proxyproto.PolicyFunc
|
||||
|
||||
switch behavior {
|
||||
case "use_always":
|
||||
policyFunc = func(upstream net.Addr) (proxyproto.Policy, error) {
|
||||
return proxyproto.USE, nil
|
||||
}
|
||||
|
||||
case "allow_authorized":
|
||||
if len(authorizedAddrs) == 0 {
|
||||
return nil, errors.New("proxy_protocol_behavior set but no proxy_protocol_authorized_addrs value")
|
||||
}
|
||||
policyFunc = proxyproto.MustLaxWhiteListPolicy(authorizedAddrs)
|
||||
|
||||
case "deny_unauthorized":
|
||||
if len(authorizedAddrs) == 0 {
|
||||
return nil, errors.New("proxy_protocol_behavior set but no proxy_protocol_authorized_addrs value")
|
||||
}
|
||||
policyFunc = proxyproto.MustStrictWhiteListPolicy(authorizedAddrs)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown %q value: %q", "proxy_protocol_behavior", behavior)
|
||||
}
|
||||
|
||||
proxyListener := &proxyproto.Listener{
|
||||
Listener: ln,
|
||||
Policy: policyFunc,
|
||||
}
|
||||
|
||||
return proxyListener, nil
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-alpnmux"
|
||||
"github.com/hashicorp/vault/internalshared/configutil"
|
||||
"github.com/hashicorp/vault/internalshared/listenerutil"
|
||||
"github.com/hashicorp/vault/internalshared/reloadutil"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func tcpListenerFactory(l *configutil.Listener, _ io.Writer, ui cli.Ui) (*alpnmux.ALPNMux, map[string]string, reloadutil.ReloadFunc, error) {
|
||||
if l.Address == "" {
|
||||
l.Address = "127.0.0.1:9200"
|
||||
}
|
||||
|
||||
bindProto := "tcp"
|
||||
|
||||
// If they've passed 0.0.0.0, we only want to bind on IPv4
|
||||
// rather than golang's dual stack default
|
||||
if strings.HasPrefix(l.Address, "0.0.0.0:") {
|
||||
bindProto = "tcp4"
|
||||
}
|
||||
|
||||
ln, err := net.Listen(bindProto, l.Address)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
ln = TCPKeepAliveListener{ln.(*net.TCPListener)}
|
||||
|
||||
ln, err = listenerWrapProxy(ln, l)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
props := map[string]string{
|
||||
"addr": l.Address,
|
||||
}
|
||||
|
||||
alpnMux := alpnmux.New(ln, nil)
|
||||
|
||||
if l.TLSDisable {
|
||||
if _, err = alpnMux.RegisterProto(alpnmux.NoProto, nil); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return alpnMux, props, nil, nil
|
||||
}
|
||||
|
||||
tlsConfig, reloadFunc, err := listenerutil.GetTLSConfig(ln, l, props, ui)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
// Register no proto, "http/1.1", and "h2", with same TLS config
|
||||
if _, err = alpnMux.RegisterProto("", tlsConfig); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if _, err = alpnMux.RegisterProto("http/1.1", tlsConfig); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if _, err = alpnMux.RegisterProto("h2", tlsConfig); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return alpnMux, props, reloadFunc, nil
|
||||
}
|
||||
|
||||
// TCPKeepAliveListener sets TCP keep-alive timeouts on accepted
|
||||
// connections. It's used by ListenAndServe and ListenAndServeTLS so
|
||||
// dead TCP connections (e.g. closing laptop mid-download) eventually
|
||||
// go away.
|
||||
//
|
||||
// This is copied directly from the Go source code.
|
||||
type TCPKeepAliveListener struct {
|
||||
*net.TCPListener
|
||||
}
|
||||
|
||||
func (ln TCPKeepAliveListener) Accept() (c net.Conn, err error) {
|
||||
tc, err := ln.AcceptTCP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tc.SetKeepAlive(true)
|
||||
tc.SetKeepAlivePeriod(3 * time.Minute)
|
||||
return tc, nil
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package dev
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/cmd/base"
|
||||
controllercmd "github.com/hashicorp/watchtower/internal/cmd/commands/controller"
|
||||
controllerconfig "github.com/hashicorp/watchtower/internal/cmd/commands/controller/config"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
var _ cli.Command = (*DevCommand)(nil)
|
||||
var _ cli.CommandAutocomplete = (*DevCommand)(nil)
|
||||
|
||||
var memProfilerEnabled = false
|
||||
|
||||
type DevCommand struct {
|
||||
*base.BaseCommand
|
||||
*base.BaseServer
|
||||
|
||||
ShutdownCh chan struct{}
|
||||
SighupCh chan struct{}
|
||||
childSighupCh []chan struct{}
|
||||
ReloadedCh chan struct{}
|
||||
SigUSR2Ch chan struct{}
|
||||
|
||||
cleanupGuard sync.Once
|
||||
|
||||
flagConfig string
|
||||
flagLogLevel string
|
||||
flagLogFormat string
|
||||
flagDev bool
|
||||
flagDevAdminToken string
|
||||
flagDevControllerListenAddr string
|
||||
flagCombineLogs bool
|
||||
}
|
||||
|
||||
func (c *DevCommand) Synopsis() string {
|
||||
return "Start a Watchtower dev environment"
|
||||
}
|
||||
|
||||
func (c *DevCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: watchtower dev [options]
|
||||
|
||||
Start a dev environment:
|
||||
|
||||
$ watchtower dev
|
||||
|
||||
For a full list of examples, please see the documentation.
|
||||
|
||||
` + c.Flags().Help()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *DevCommand) Flags() *base.FlagSets {
|
||||
set := c.FlagSet(base.FlagSetHTTP)
|
||||
|
||||
f := set.NewFlagSet("Command Options")
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "log-level",
|
||||
Target: &c.flagLogLevel,
|
||||
Default: base.NotSetValue,
|
||||
EnvVar: "WATCHTOWER_LOG_LEVEL",
|
||||
Completion: complete.PredictSet("trace", "debug", "info", "warn", "err"),
|
||||
Usage: "Log verbosity level. Supported values (in order of more detail to less) are " +
|
||||
"\"trace\", \"debug\", \"info\", \"warn\", and \"err\".",
|
||||
})
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "log-format",
|
||||
Target: &c.flagLogFormat,
|
||||
Default: base.NotSetValue,
|
||||
Completion: complete.PredictSet("standard", "json"),
|
||||
Usage: `Log format. Supported values are "standard" and "json".`,
|
||||
})
|
||||
|
||||
f = set.NewFlagSet("Dev Options")
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "dev-admin-token",
|
||||
Target: &c.flagDevAdminToken,
|
||||
Default: "",
|
||||
EnvVar: "WATCHTWER_DEV_ADMIN_TOKEN",
|
||||
Usage: "Initial admin token. This only applies when running in \"dev\" " +
|
||||
"mode.",
|
||||
})
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "dev-listen-address",
|
||||
Target: &c.flagDevControllerListenAddr,
|
||||
Default: "127.0.0.1:9200",
|
||||
EnvVar: "WATCHTOWER_DEV_LISTEN_ADDRESS",
|
||||
Usage: "Address to bind against.",
|
||||
})
|
||||
|
||||
f.BoolVar(&base.BoolVar{
|
||||
Name: "combine-logs",
|
||||
Target: &c.flagCombineLogs,
|
||||
Default: false,
|
||||
Usage: "If set, both startup information and logs will be sent to stdout. If not set (the default), startup information will go to stdout and logs will be sent to stderr.",
|
||||
})
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *DevCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *DevCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *DevCommand) Run(args []string) int {
|
||||
c.BaseServer = base.NewBaseServer()
|
||||
c.CombineLogs = c.flagCombineLogs
|
||||
|
||||
var err error
|
||||
|
||||
f := c.Flags()
|
||||
|
||||
if err = f.Parse(args); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
childShutdownCh := make(chan struct{})
|
||||
|
||||
devControllerConfig, err := controllerconfig.DevConfig()
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error creating controller dev config: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if memProfilerEnabled {
|
||||
c.startMemProfiler()
|
||||
}
|
||||
|
||||
if err := c.SetupLogging(c.flagLogLevel, c.flagLogFormat, "", ""); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := c.SetupMetrics(c.UI, devControllerConfig.Telemetry); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := c.SetupKMSes(c.UI, devControllerConfig.SharedConfig, 2); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Initialize the listeners
|
||||
if err := c.SetupListeners(c.UI, devControllerConfig.SharedConfig); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Write out the PID to the file now that server has successfully started
|
||||
if err := c.StorePidFile(devControllerConfig.PidFile); err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error storing PID: %w", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
/*
|
||||
// If we're in Dev mode, then initialize the core
|
||||
if c.flagDev && !c.flagDevSkipInit {
|
||||
init, err := c.enableDev(core, coreConfig)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error initializing Dev mode: %w", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Print the big dev mode warning!
|
||||
c.UI.Warn(base.WrapAtLength(
|
||||
"WARNING! dev mode is enabled! In this mode, Watchtower runs entirely " +
|
||||
"in-memory and all state is lost upon shutdown."))
|
||||
c.UI.Warn("")
|
||||
c.UI.Warn("You may need to set the following environment variable:")
|
||||
c.UI.Warn("")
|
||||
|
||||
endpointURL := "http://" + config.Listeners[0].Config["address"].(string)
|
||||
if runtime.GOOS == "windows" {
|
||||
c.UI.Warn("PowerShell:")
|
||||
c.UI.Warn(fmt.Sprintf(" $env:WATCHTOWER_ADDR=\"%s\"", endpointURL))
|
||||
c.UI.Warn("cmd.exe:")
|
||||
c.UI.Warn(fmt.Sprintf(" set WATCHTOWER_ADDR=%s", endpointURL))
|
||||
} else {
|
||||
c.UI.Warn(fmt.Sprintf(" $ export VAULT_ADDR='%s'", endpointURL))
|
||||
}
|
||||
|
||||
c.UI.Warn(fmt.Sprintf("Root Token: %s", init.RootToken))
|
||||
|
||||
c.UI.Warn("")
|
||||
c.UI.Warn(base.WrapAtLength(
|
||||
"Development mode should NOT be used in production installations!"))
|
||||
c.UI.Warn("")
|
||||
}
|
||||
*/
|
||||
|
||||
defer c.RunShutdownFuncs(c.UI)
|
||||
|
||||
if err := c.CreateDevDatabase(); err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error creating dev database container: %s", err.Error()))
|
||||
return 1
|
||||
}
|
||||
c.ShutdownFuncs = append(c.ShutdownFuncs, c.DestroyDevDatabase)
|
||||
|
||||
c.PrintInfo(c.UI, "dev mode")
|
||||
c.ReleaseLogGate()
|
||||
|
||||
// Instantiate the wait group
|
||||
shutdownWg := &sync.WaitGroup{}
|
||||
shutdownWg.Add(1)
|
||||
controllerSighupCh := make(chan struct{})
|
||||
c.childSighupCh = append(c.childSighupCh, controllerSighupCh)
|
||||
go func() {
|
||||
defer shutdownWg.Done()
|
||||
devController := &controllercmd.ControllerCommand{
|
||||
BaseCommand: c.BaseCommand,
|
||||
BaseServer: c.BaseServer,
|
||||
ShutdownCh: childShutdownCh,
|
||||
SighupCh: controllerSighupCh,
|
||||
Config: devControllerConfig,
|
||||
}
|
||||
devController.Start()
|
||||
}()
|
||||
|
||||
// Wait for shutdown
|
||||
shutdownTriggered := false
|
||||
|
||||
for !shutdownTriggered {
|
||||
select {
|
||||
case <-c.ShutdownCh:
|
||||
c.UI.Output("==> Watchtower dev environment shutdown triggered")
|
||||
|
||||
close(childShutdownCh)
|
||||
|
||||
shutdownTriggered = true
|
||||
|
||||
case <-c.SighupCh:
|
||||
c.UI.Output("==> Watchtower dev environment reload triggered")
|
||||
for _, v := range c.childSighupCh {
|
||||
v <- struct{}{}
|
||||
}
|
||||
|
||||
case <-c.SigUSR2Ch:
|
||||
buf := make([]byte, 32*1024*1024)
|
||||
n := runtime.Stack(buf[:], true)
|
||||
c.Logger.Info("goroutine trace", "stack", string(buf[:n]))
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for dependent goroutines to complete
|
||||
shutdownWg.Wait()
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// +build !memprofiler
|
||||
|
||||
package dev
|
||||
|
||||
func (d *DevCommand) startMemProfiler() {
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// +build memprofiler
|
||||
|
||||
package dev
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
memProfilerEnabled = true
|
||||
}
|
||||
|
||||
func (d *DevCommand) startMemProfiler() {
|
||||
profileDir := filepath.Join(os.TempDir(), "watchtowerprof")
|
||||
if err := os.MkdirAll(profileDir, 0700); err != nil {
|
||||
d.logger.Debug("could not create profile directory", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
filename := filepath.Join(profileDir, time.Now().UTC().Format("20060102_150405")) + ".pprof"
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
d.logger.Debug("could not create memory profile", "error", err)
|
||||
}
|
||||
runtime.GC()
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
d.logger.Debug("could not write memory profile", "error", err)
|
||||
}
|
||||
f.Close()
|
||||
d.logger.Debug("wrote memory profile", "filename", filename)
|
||||
time.Sleep(5 * time.Minute)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// +build !windows
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// MakeSigUSR2Ch returns a channel that can be used for SIGUSR2
|
||||
// goroutine logging. This channel will send a message for every
|
||||
// SIGUSR2 received.
|
||||
func MakeSigUSR2Ch() chan struct{} {
|
||||
resultCh := make(chan struct{})
|
||||
|
||||
signalCh := make(chan os.Signal, 4)
|
||||
signal.Notify(signalCh, syscall.SIGUSR2)
|
||||
go func() {
|
||||
for {
|
||||
<-signalCh
|
||||
resultCh <- struct{}{}
|
||||
}
|
||||
}()
|
||||
return resultCh
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// +build windows
|
||||
|
||||
package cmd
|
||||
|
||||
// MakeSigUSR2Ch does nothing useful on Windows.
|
||||
func MakeSigUSR2Ch() chan struct{} {
|
||||
return make(chan struct{})
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/cmd/base"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
type Formatter interface {
|
||||
//Output(ui cli.Ui, secret *api.Secret, data interface{}) error
|
||||
Format(data interface{}) ([]byte, error)
|
||||
}
|
||||
|
||||
var Formatters = map[string]Formatter{
|
||||
"json": base.JsonFormatter{},
|
||||
"table": base.TableFormatter{},
|
||||
"yaml": base.YamlFormatter{},
|
||||
"yml": base.YamlFormatter{},
|
||||
}
|
||||
|
||||
func Format(ui cli.Ui) string {
|
||||
switch ui.(type) {
|
||||
case *WatchtowerUI:
|
||||
return ui.(*WatchtowerUI).format
|
||||
}
|
||||
|
||||
format := os.Getenv(base.EnvWatchtowerCLIFormat)
|
||||
if format == "" {
|
||||
format = "table"
|
||||
}
|
||||
|
||||
return format
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/watchtower/internal/cmd/base"
|
||||
colorable "github.com/mattn/go-colorable"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
type WatchtowerUI struct {
|
||||
cli.Ui
|
||||
format string
|
||||
}
|
||||
|
||||
// setupEnv parses args and may replace them and sets some env vars to known
|
||||
// values based on format options
|
||||
func setupEnv(args []string) (retArgs []string, format string, outputCurlString bool) {
|
||||
var nextArgFormat bool
|
||||
|
||||
for _, arg := range args {
|
||||
if nextArgFormat {
|
||||
nextArgFormat = false
|
||||
format = arg
|
||||
continue
|
||||
}
|
||||
|
||||
if arg == "--" {
|
||||
break
|
||||
}
|
||||
|
||||
if len(args) == 1 && (arg == "-v" || arg == "-version" || arg == "--version") {
|
||||
args = []string{"version"}
|
||||
break
|
||||
}
|
||||
|
||||
if arg == "-output-curl-string" {
|
||||
outputCurlString = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse a given flag here, which overrides the env var
|
||||
if strings.HasPrefix(arg, "--format=") {
|
||||
format = strings.TrimPrefix(arg, "--format=")
|
||||
}
|
||||
if strings.HasPrefix(arg, "-format=") {
|
||||
format = strings.TrimPrefix(arg, "-format=")
|
||||
}
|
||||
// For backwards compat, it could be specified without an equal sign
|
||||
if arg == "-format" || arg == "--format" {
|
||||
nextArgFormat = true
|
||||
}
|
||||
}
|
||||
|
||||
envWatchtowerCLIFormat := os.Getenv(base.EnvWatchtowerCLIFormat)
|
||||
// If we did not parse a value, fetch the env var
|
||||
if format == "" && envWatchtowerCLIFormat != "" {
|
||||
format = envWatchtowerCLIFormat
|
||||
}
|
||||
// Lowercase for consistency
|
||||
format = strings.ToLower(format)
|
||||
if format == "" {
|
||||
format = "table"
|
||||
}
|
||||
|
||||
return args, format, outputCurlString
|
||||
}
|
||||
|
||||
type RunOptions struct {
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
Address string
|
||||
}
|
||||
|
||||
func Run(args []string) int {
|
||||
return RunCustom(args, nil)
|
||||
}
|
||||
|
||||
// RunCustom allows passing in a base command template to pass to other
|
||||
// commands. Currently, this is only used for setting a custom token helper.
|
||||
func RunCustom(args []string, runOpts *RunOptions) int {
|
||||
if runOpts == nil {
|
||||
runOpts = &RunOptions{}
|
||||
}
|
||||
|
||||
var format string
|
||||
var outputCurlString bool
|
||||
args, format, outputCurlString = setupEnv(args)
|
||||
|
||||
// Don't use color if disabled
|
||||
useColor := true
|
||||
if os.Getenv(base.EnvWatchtowerCLINoColor) != "" || color.NoColor {
|
||||
useColor = false
|
||||
}
|
||||
|
||||
if runOpts.Stdout == nil {
|
||||
runOpts.Stdout = os.Stdout
|
||||
}
|
||||
if runOpts.Stderr == nil {
|
||||
runOpts.Stderr = os.Stderr
|
||||
}
|
||||
|
||||
// Only use colored UI if stdout is a tty, and not disabled
|
||||
if useColor && format == "table" {
|
||||
if f, ok := runOpts.Stdout.(*os.File); ok {
|
||||
runOpts.Stdout = colorable.NewColorable(f)
|
||||
}
|
||||
if f, ok := runOpts.Stderr.(*os.File); ok {
|
||||
runOpts.Stderr = colorable.NewColorable(f)
|
||||
}
|
||||
} else {
|
||||
runOpts.Stdout = colorable.NewNonColorable(runOpts.Stdout)
|
||||
runOpts.Stderr = colorable.NewNonColorable(runOpts.Stderr)
|
||||
}
|
||||
|
||||
uiErrWriter := runOpts.Stderr
|
||||
if outputCurlString {
|
||||
uiErrWriter = ioutil.Discard
|
||||
}
|
||||
|
||||
ui := &WatchtowerUI{
|
||||
Ui: &cli.ColoredUi{
|
||||
ErrorColor: cli.UiColorRed,
|
||||
WarnColor: cli.UiColorYellow,
|
||||
Ui: &cli.BasicUi{
|
||||
Reader: bufio.NewReader(os.Stdin),
|
||||
Writer: runOpts.Stdout,
|
||||
ErrorWriter: uiErrWriter,
|
||||
},
|
||||
},
|
||||
format: format,
|
||||
}
|
||||
|
||||
serverCmdUi := &WatchtowerUI{
|
||||
Ui: &cli.ColoredUi{
|
||||
ErrorColor: cli.UiColorRed,
|
||||
WarnColor: cli.UiColorYellow,
|
||||
Ui: &cli.BasicUi{
|
||||
Reader: bufio.NewReader(os.Stdin),
|
||||
Writer: runOpts.Stdout,
|
||||
},
|
||||
},
|
||||
format: format,
|
||||
}
|
||||
|
||||
if _, ok := Formatters[format]; !ok {
|
||||
ui.Error(fmt.Sprintf("Invalid output format: %s", format))
|
||||
return 1
|
||||
}
|
||||
|
||||
initCommands(ui, serverCmdUi, runOpts)
|
||||
|
||||
hiddenCommands := []string{"version"}
|
||||
|
||||
cli := &cli.CLI{
|
||||
Name: "watchtower",
|
||||
Args: args,
|
||||
Commands: Commands,
|
||||
HelpFunc: groupedHelpFunc(
|
||||
cli.BasicHelpFunc("watchtower"),
|
||||
),
|
||||
HelpWriter: runOpts.Stderr,
|
||||
HiddenCommands: hiddenCommands,
|
||||
Autocomplete: true,
|
||||
AutocompleteNoDefaultFlags: true,
|
||||
}
|
||||
|
||||
exitCode, err := cli.Run()
|
||||
if outputCurlString {
|
||||
if exitCode == 0 {
|
||||
fmt.Fprint(runOpts.Stderr, "Could not generate cURL command")
|
||||
return 1
|
||||
} else {
|
||||
if api.LastOutputStringError == nil {
|
||||
if exitCode == 127 {
|
||||
// Usage, just pass it through
|
||||
return exitCode
|
||||
}
|
||||
fmt.Fprint(runOpts.Stderr, "cURL command not set by API operation; run without -output-curl-string to see the generated error\n")
|
||||
return exitCode
|
||||
}
|
||||
if api.LastOutputStringError.Error() != api.ErrOutputStringRequest {
|
||||
runOpts.Stdout.Write([]byte(fmt.Sprintf("Error creating request string: %s\n", api.LastOutputStringError.Error())))
|
||||
return 1
|
||||
}
|
||||
runOpts.Stdout.Write([]byte(fmt.Sprintf("%s\n", api.LastOutputStringError.CurlString())))
|
||||
return 0
|
||||
}
|
||||
} else if err != nil {
|
||||
fmt.Fprintf(runOpts.Stderr, "Error executing CLI: %s\n", err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
return exitCode
|
||||
}
|
||||
|
||||
var commonCommands = []string{}
|
||||
|
||||
func groupedHelpFunc(f cli.HelpFunc) cli.HelpFunc {
|
||||
return func(commands map[string]cli.CommandFactory) string {
|
||||
var b bytes.Buffer
|
||||
tw := tabwriter.NewWriter(&b, 0, 2, 6, ' ', 0)
|
||||
|
||||
fmt.Fprintf(tw, "Usage: watchtower <command> [args]\n\n")
|
||||
fmt.Fprintf(tw, "Common commands:\n")
|
||||
for _, v := range commonCommands {
|
||||
printCommand(tw, v, commands[v])
|
||||
}
|
||||
|
||||
otherCommands := make([]string, 0, len(commands))
|
||||
for k := range commands {
|
||||
found := false
|
||||
for _, v := range commonCommands {
|
||||
if k == v {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
otherCommands = append(otherCommands, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(otherCommands)
|
||||
|
||||
fmt.Fprintf(tw, "\n")
|
||||
fmt.Fprintf(tw, "Other commands:\n")
|
||||
for _, v := range otherCommands {
|
||||
printCommand(tw, v, commands[v])
|
||||
}
|
||||
|
||||
tw.Flush()
|
||||
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
}
|
||||
|
||||
func printCommand(w io.Writer, name string, cmdFn cli.CommandFactory) {
|
||||
cmd, err := cmdFn()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to load %q command: %s", name, err))
|
||||
}
|
||||
fmt.Fprintf(w, " %s\t%s\n", name, cmd.Synopsis())
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
mathrand "math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/helper/base62"
|
||||
)
|
||||
|
||||
type workerTLSOpts struct {
|
||||
Address string
|
||||
Protos []string
|
||||
DumpDir string
|
||||
}
|
||||
|
||||
type certInfo struct {
|
||||
CACert []byte `json:"ca_cert"`
|
||||
CAKey []byte `json:"ca_key"`
|
||||
}
|
||||
|
||||
func (c Controller) workerTLS(opts workerTLSOpts) (*tls.Config, *certInfo, error) {
|
||||
info := new(certInfo)
|
||||
|
||||
certIPs := []net.IP{
|
||||
net.IPv6loopback,
|
||||
net.ParseIP("127.0.0.1"),
|
||||
}
|
||||
|
||||
if opts.Address != "" {
|
||||
baseAddr, err := net.ResolveTCPAddr("tcp", opts.Address)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
certIPs = append(certIPs, baseAddr.IP)
|
||||
}
|
||||
|
||||
_, caKey, err := ed25519.GenerateKey(c.conf.SecureRandomReader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
info.CAKey = caKey
|
||||
caHost, err := base62.Random(20)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
caCertTemplate := &x509.Certificate{
|
||||
Subject: pkix.Name{
|
||||
CommonName: caHost,
|
||||
},
|
||||
DNSNames: []string{caHost},
|
||||
IPAddresses: certIPs,
|
||||
KeyUsage: x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign),
|
||||
SerialNumber: big.NewInt(mathrand.Int63()),
|
||||
NotBefore: time.Now().Add(-30 * time.Second),
|
||||
NotAfter: time.Now().Add(262980 * time.Hour),
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
caBytes, err := x509.CreateCertificate(c.conf.SecureRandomReader, caCertTemplate, caCertTemplate, caKey.Public(), caKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
info.CACert = caBytes
|
||||
caCert, err := x509.ParseCertificate(caBytes)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rootCAs := x509.NewCertPool()
|
||||
rootCAs.AddCert(caCert)
|
||||
caCertPEMBlock := &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: caBytes,
|
||||
}
|
||||
caCertPEM := pem.EncodeToMemory(caCertPEMBlock)
|
||||
caCertPEMFile := filepath.Join(opts.DumpDir, "ca_cert.pem")
|
||||
|
||||
marshaledCAKey, err := x509.MarshalPKCS8PrivateKey(caKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
caKeyPEMBlock := &pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: marshaledCAKey,
|
||||
}
|
||||
caKeyPEM := pem.EncodeToMemory(caKeyPEMBlock)
|
||||
|
||||
//
|
||||
// Certs generation
|
||||
//
|
||||
_, key, err := ed25519.GenerateKey(c.conf.SecureRandomReader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
host, err := base62.Random(20)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
certTemplate := &x509.Certificate{
|
||||
Subject: pkix.Name{
|
||||
CommonName: host,
|
||||
},
|
||||
DNSNames: []string{host},
|
||||
IPAddresses: certIPs,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageServerAuth,
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
|
||||
SerialNumber: big.NewInt(mathrand.Int63()),
|
||||
NotBefore: time.Now().Add(-30 * time.Second),
|
||||
NotAfter: time.Now().Add(262980 * time.Hour),
|
||||
}
|
||||
certBytes, err := x509.CreateCertificate(c.conf.SecureRandomReader, certTemplate, caCert, key.Public(), caKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
certPEMBlock := &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certBytes,
|
||||
}
|
||||
certPEM := pem.EncodeToMemory(certPEMBlock)
|
||||
marshaledKey, err := x509.MarshalPKCS8PrivateKey(key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
keyPEMBlock := &pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: marshaledKey,
|
||||
}
|
||||
keyPEM := pem.EncodeToMemory(keyPEMBlock)
|
||||
|
||||
certFile := filepath.Join(opts.DumpDir, "cert.pem")
|
||||
keyFile := filepath.Join(opts.DumpDir, "key.pem")
|
||||
|
||||
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{tlsCert},
|
||||
RootCAs: rootCAs,
|
||||
ClientCAs: rootCAs,
|
||||
ClientAuth: tls.RequestClientCert,
|
||||
NextProtos: opts.Protos,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
tlsConfig.BuildNameToCertificate()
|
||||
|
||||
if opts.DumpDir != "" {
|
||||
if _, err := os.Stat(opts.DumpDir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(opts.DumpDir, 0700); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
if err := ioutil.WriteFile(filepath.Join(opts.DumpDir, "ca_key.pem"), caKeyPEM, 0755); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := ioutil.WriteFile(caCertPEMFile, caCertPEM, 0755); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := ioutil.WriteFile(certFile, certPEM, 0755); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := ioutil.WriteFile(keyFile, keyPEM, 0755); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return tlsConfig, info, nil
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/cmd/base"
|
||||
"github.com/hashicorp/watchtower/internal/cmd/commands/controller/config"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
*base.BaseServer
|
||||
RawConfig *config.Config
|
||||
BaseContext context.Context
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/sdk/helper/mlock"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
conf *Config
|
||||
|
||||
baseContext context.Context
|
||||
baseCancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewController(conf *Config) (*Controller, error) {
|
||||
if conf.Logger == nil {
|
||||
conf.Logger = hclog.New(&hclog.LoggerOptions{
|
||||
Level: hclog.Trace,
|
||||
})
|
||||
conf.AllLoggers = append(conf.AllLoggers, conf.Logger)
|
||||
}
|
||||
|
||||
if conf.SecureRandomReader == nil {
|
||||
conf.SecureRandomReader = rand.Reader
|
||||
}
|
||||
|
||||
if !conf.RawConfig.DisableMlock {
|
||||
// Ensure our memory usage is locked into physical RAM
|
||||
if err := mlock.LockMemory(); err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"Failed to lock memory: %v\n\n"+
|
||||
"This usually means that the mlock syscall is not available.\n"+
|
||||
"Watchtower uses mlock to prevent memory from being swapped to\n"+
|
||||
"disk. This requires root privileges as well as a machine\n"+
|
||||
"that supports mlock. Please enable mlock on your system or\n"+
|
||||
"disable Watchtower from using it. To disable Watchtower from using it,\n"+
|
||||
"set the `disable_mlock` configuration option in your configuration\n"+
|
||||
"file.",
|
||||
err)
|
||||
}
|
||||
}
|
||||
|
||||
conf.Logger = conf.Logger.Named("controller")
|
||||
|
||||
c := &Controller{
|
||||
conf: conf,
|
||||
}
|
||||
|
||||
c.baseContext, c.baseCancel = context.WithCancel(context.Background())
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Controller) Start() error {
|
||||
if err := c.startListeners(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) Shutdown() error {
|
||||
if err := c.stopListeners(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) SetLogLevel(level hclog.Level) {
|
||||
for _, logger := range c.conf.AllLoggers {
|
||||
logger.SetLevel(level)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/internalshared/configutil"
|
||||
"github.com/hashicorp/watchtower/globals"
|
||||
)
|
||||
|
||||
type HandlerProperties struct {
|
||||
ListenerConfig *configutil.Listener
|
||||
}
|
||||
|
||||
// Handler returns an http.Handler for the API. This can be used on
|
||||
// its own to mount the Vault API within another web server.
|
||||
func (c *Controller) Handler(props HandlerProperties) http.Handler {
|
||||
// Create the muxer to handle the actual endpoints
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.Handle("/v1/", handleDummy())
|
||||
|
||||
genericWrappedHandler := c.wrapGenericHandler(mux, props)
|
||||
|
||||
return genericWrappedHandler
|
||||
}
|
||||
|
||||
func handleDummy() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"foo": "bar"}`))
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Controller) wrapGenericHandler(h http.Handler, props HandlerProperties) http.Handler {
|
||||
var maxRequestDuration time.Duration
|
||||
var maxRequestSize int64
|
||||
if props.ListenerConfig != nil {
|
||||
maxRequestDuration = props.ListenerConfig.MaxRequestDuration
|
||||
maxRequestSize = props.ListenerConfig.MaxRequestSize
|
||||
}
|
||||
if maxRequestDuration == 0 {
|
||||
maxRequestDuration = globals.DefaultMaxRequestDuration
|
||||
}
|
||||
if maxRequestSize == 0 {
|
||||
maxRequestSize = globals.DefaultMaxRequestSize
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Set the Cache-Control header for all responses returned
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
|
||||
// Start with the request context
|
||||
ctx := r.Context()
|
||||
var cancelFunc context.CancelFunc
|
||||
// Add our timeout
|
||||
ctx, cancelFunc = context.WithTimeout(ctx, maxRequestDuration)
|
||||
// Add a size limiter if desired
|
||||
if maxRequestSize > 0 {
|
||||
ctx = context.WithValue(ctx, "max_request_size", maxRequestSize)
|
||||
}
|
||||
ctx = context.WithValue(ctx, "original_request_path", r.URL.Path)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
cancelFunc()
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
func WrapForwardedForHandler(h http.Handler, authorizedAddrs []*sockaddr.SockAddrMarshaler, rejectNotPresent, rejectNonAuthz bool, hopSkips int) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
headers, headersOK := r.Header[textproto.CanonicalMIMEHeaderKey("X-Forwarded-For")]
|
||||
if !headersOK || len(headers) == 0 {
|
||||
if !rejectNotPresent {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusBadRequest, fmt.Errorf("missing x-forwarded-for header and configured to reject when not present"))
|
||||
return
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
// If not rejecting treat it like we just don't have a valid
|
||||
// header because we can't do a comparison against an address we
|
||||
// can't understand
|
||||
if !rejectNotPresent {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusBadRequest, errwrap.Wrapf("error parsing client hostport: {{err}}", err))
|
||||
return
|
||||
}
|
||||
|
||||
addr, err := sockaddr.NewIPAddr(host)
|
||||
if err != nil {
|
||||
// We treat this the same as the case above
|
||||
if !rejectNotPresent {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusBadRequest, errwrap.Wrapf("error parsing client address: {{err}}", err))
|
||||
return
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, authz := range authorizedAddrs {
|
||||
if authz.Contains(addr) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
// If we didn't find it and aren't configured to reject, simply
|
||||
// don't trust it
|
||||
if !rejectNonAuthz {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusBadRequest, fmt.Errorf("client address not authorized for x-forwarded-for and configured to reject connection"))
|
||||
return
|
||||
}
|
||||
|
||||
// At this point we have at least one value and it's authorized
|
||||
|
||||
// Split comma separated ones, which are common. This brings it in line
|
||||
// to the multiple-header case.
|
||||
var acc []string
|
||||
for _, header := range headers {
|
||||
vals := strings.Split(header, ",")
|
||||
for _, v := range vals {
|
||||
acc = append(acc, strings.TrimSpace(v))
|
||||
}
|
||||
}
|
||||
|
||||
indexToUse := len(acc) - 1 - hopSkips
|
||||
if indexToUse < 0 {
|
||||
// This is likely an error in either configuration or other
|
||||
// infrastructure. We could either deny the request, or we
|
||||
// could simply not trust the value. Denying the request is
|
||||
// "safer" since if this logic is configured at all there may
|
||||
// be an assumption it can always be trusted. Given that we can
|
||||
// deny accepting the request at all if it's not from an
|
||||
// authorized address, if we're at this point the address is
|
||||
// authorized (or we've turned off explicit rejection) and we
|
||||
// should assume that what comes in should be properly
|
||||
// formatted.
|
||||
respondError(w, http.StatusBadRequest, fmt.Errorf("malformed x-forwarded-for configuration or request, hops to skip (%d) would skip before earliest chain link (chain length %d)", hopSkips, len(headers)))
|
||||
return
|
||||
}
|
||||
|
||||
r.RemoteAddr = net.JoinHostPort(acc[indexToUse], port)
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
})
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,148 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-alpnmux"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
func (c *Controller) startListeners() error {
|
||||
var retErr *multierror.Error
|
||||
servers := make([]func(), 0, len(c.conf.Listeners))
|
||||
for _, ln := range c.conf.Listeners {
|
||||
handler := c.Handler(HandlerProperties{
|
||||
ListenerConfig: ln.Config,
|
||||
})
|
||||
|
||||
/*
|
||||
// TODO: As I write this Vault's having this code audited, make sure to
|
||||
// port over any recommendations
|
||||
//
|
||||
// We perform validation on the config earlier, we can just cast here
|
||||
if _, ok := ln.config["x_forwarded_for_authorized_addrs"]; ok {
|
||||
hopSkips := ln.config["x_forwarded_for_hop_skips"].(int)
|
||||
authzdAddrs := ln.config["x_forwarded_for_authorized_addrs"].([]*sockaddr.SockAddrMarshaler)
|
||||
rejectNotPresent := ln.config["x_forwarded_for_reject_not_present"].(bool)
|
||||
rejectNonAuthz := ln.config["x_forwarded_for_reject_not_authorized"].(bool)
|
||||
if len(authzdAddrs) > 0 {
|
||||
handler = vaulthttp.WrapForwardedForHandler(handler, authzdAddrs, rejectNotPresent, rejectNonAuthz, hopSkips)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
server := &http.Server{
|
||||
Handler: handler,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
IdleTimeout: 5 * time.Minute,
|
||||
ErrorLog: c.conf.Logger.StandardLogger(nil),
|
||||
BaseContext: func(net.Listener) context.Context {
|
||||
return c.baseContext
|
||||
},
|
||||
}
|
||||
ln.HTTPServer = server
|
||||
|
||||
if ln.Config.HTTPReadHeaderTimeout > 0 {
|
||||
server.ReadHeaderTimeout = ln.Config.HTTPReadHeaderTimeout
|
||||
}
|
||||
if ln.Config.HTTPReadTimeout > 0 {
|
||||
server.ReadTimeout = ln.Config.HTTPReadTimeout
|
||||
}
|
||||
if ln.Config.HTTPWriteTimeout > 0 {
|
||||
server.WriteTimeout = ln.Config.HTTPWriteTimeout
|
||||
}
|
||||
if ln.Config.HTTPIdleTimeout > 0 {
|
||||
server.IdleTimeout = ln.Config.HTTPIdleTimeout
|
||||
}
|
||||
|
||||
switch ln.Config.TLSDisable {
|
||||
case true:
|
||||
l := ln.Mux.GetListener(alpnmux.NoProto)
|
||||
if l == nil {
|
||||
retErr = multierror.Append(retErr, errors.New("could not get non-tls listener"))
|
||||
continue
|
||||
}
|
||||
servers = append(servers, func() {
|
||||
go server.Serve(l)
|
||||
})
|
||||
|
||||
default:
|
||||
protos := []string{"", "http/1.1", "h2"}
|
||||
for _, v := range protos {
|
||||
l := ln.Mux.GetListener(v)
|
||||
if l == nil {
|
||||
retErr = multierror.Append(retErr, fmt.Errorf("could not get tls proto %q listener", v))
|
||||
continue
|
||||
}
|
||||
servers = append(servers, func() {
|
||||
go server.Serve(l)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
workerTLSConfig, peeringInfo, err := c.workerTLS(workerTLSOpts{
|
||||
Address: ln.Config.Address,
|
||||
Protos: []string{"watchtower-worker-v1"},
|
||||
})
|
||||
if err != nil {
|
||||
retErr = multierror.Append(retErr, fmt.Errorf("error getting TLS configuration: %w", err))
|
||||
continue
|
||||
}
|
||||
l, err := ln.Mux.RegisterProto("watchtower-worker-v1", workerTLSConfig)
|
||||
if err != nil {
|
||||
retErr = multierror.Append(retErr, fmt.Errorf("error getting sub-listener for worker proto: %w", err))
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: Start listner for real; for now send it to the http server just for testing
|
||||
servers = append(servers, func() {
|
||||
go server.Serve(l)
|
||||
})
|
||||
|
||||
// TODO: Add peering info into database
|
||||
_ = peeringInfo
|
||||
}
|
||||
|
||||
err := retErr.ErrorOrNil()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range servers {
|
||||
s()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) stopListeners() error {
|
||||
serverWg := new(sync.WaitGroup)
|
||||
for _, ln := range c.conf.Listeners {
|
||||
if ln.HTTPServer == nil {
|
||||
continue
|
||||
}
|
||||
serverWg.Add(1)
|
||||
go func() {
|
||||
shutdownKill, shutdownKillCancel := context.WithTimeout(c.baseContext, ln.Config.MaxRequestDuration)
|
||||
defer shutdownKillCancel()
|
||||
defer serverWg.Done()
|
||||
ln.HTTPServer.Shutdown(shutdownKill)
|
||||
}()
|
||||
}
|
||||
serverWg.Wait()
|
||||
|
||||
var retErr *multierror.Error
|
||||
for _, ln := range c.conf.Listeners {
|
||||
if err := ln.Mux.Close(); err != nil {
|
||||
retErr = multierror.Append(retErr, err)
|
||||
}
|
||||
}
|
||||
return retErr.ErrorOrNil()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build cgo
|
||||
|
||||
package version
|
||||
|
||||
func init() {
|
||||
CgoEnabled = true
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// VersionInfo
|
||||
type VersionInfo struct {
|
||||
Revision string
|
||||
Version string
|
||||
VersionPrerelease string
|
||||
VersionMetadata string
|
||||
}
|
||||
|
||||
func GetVersion() *VersionInfo {
|
||||
ver := Version
|
||||
rel := VersionPrerelease
|
||||
md := VersionMetadata
|
||||
if GitDescribe != "" {
|
||||
ver = GitDescribe
|
||||
}
|
||||
if GitDescribe == "" && rel == "" && VersionPrerelease != "" {
|
||||
rel = "dev"
|
||||
}
|
||||
|
||||
return &VersionInfo{
|
||||
Revision: GitCommit,
|
||||
Version: ver,
|
||||
VersionPrerelease: rel,
|
||||
VersionMetadata: md,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *VersionInfo) VersionNumber() string {
|
||||
if Version == "unknown" && VersionPrerelease == "unknown" {
|
||||
return "(version unknown)"
|
||||
}
|
||||
|
||||
version := fmt.Sprintf("%s", c.Version)
|
||||
|
||||
if c.VersionPrerelease != "" {
|
||||
version = fmt.Sprintf("%s-%s", version, c.VersionPrerelease)
|
||||
}
|
||||
|
||||
if c.VersionMetadata != "" {
|
||||
version = fmt.Sprintf("%s+%s", version, c.VersionMetadata)
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
func (c *VersionInfo) FullVersionNumber(rev bool) string {
|
||||
var versionString bytes.Buffer
|
||||
|
||||
if Version == "unknown" && VersionPrerelease == "unknown" {
|
||||
return "Watchtower (version unknown)"
|
||||
}
|
||||
|
||||
fmt.Fprintf(&versionString, "Watchtower v%s", c.Version)
|
||||
if c.VersionPrerelease != "" {
|
||||
fmt.Fprintf(&versionString, "-%s", c.VersionPrerelease)
|
||||
}
|
||||
|
||||
if c.VersionMetadata != "" {
|
||||
fmt.Fprintf(&versionString, "+%s", c.VersionMetadata)
|
||||
}
|
||||
|
||||
if rev && c.Revision != "" {
|
||||
fmt.Fprintf(&versionString, " (%s)", c.Revision)
|
||||
}
|
||||
|
||||
return versionString.String()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package version
|
||||
|
||||
var (
|
||||
// The git commit that was compiled. This will be filled in by the compiler.
|
||||
GitCommit string
|
||||
GitDescribe string
|
||||
|
||||
// Whether cgo is enabled or not; set at build time
|
||||
CgoEnabled bool
|
||||
|
||||
Version = "0.0.1"
|
||||
VersionPrerelease = ""
|
||||
VersionMetadata = ""
|
||||
)
|
||||
Reference in new issue
Block a user