Initial commit

This commit is contained in:
Jeff Mitchell committed 2020-03-04 12:51:46 -05:00
commit 482b4d56dd
36 files changed
+6121

No files matched your search

+120
View File
@@ -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 :
+363
View File
@@ -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.
+169
View File
@@ -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!
+11
View File
@@ -0,0 +1,11 @@
package main
import (
"os"
"github.com/hashicorp/watchtower/internal/cmd"
)
func main() {
os.Exit(cmd.Run(os.Args[1:]))
}
+18
View File
@@ -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)
)
+31
View File
@@ -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
)
+1007
View File
File diff suppressed because it is too large. Load diff
+332
View File
@@ -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)
}
+39
View File
@@ -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"
+780
View File
@@ -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
}
+348
View File
@@ -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"
}
+249
View File
@@ -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
}
+95
View File
@@ -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...))
}
}
+433
View File
@@ -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)
}
+83
View File
@@ -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
}
+265
View File
@@ -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() {
}
+40
View File
@@ -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)
}
}()
}
+26
View File
@@ -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
}
+8
View File
@@ -0,0 +1,8 @@
// +build windows
package cmd
// MakeSigUSR2Ch does nothing useful on Windows.
func MakeSigUSR2Ch() chan struct{} {
return make(chan struct{})
}
+34
View File
@@ -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
}
+254
View File
@@ -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())
}
+182
View File
@@ -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
}
+14
View File
@@ -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
}
+76
View File
@@ -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)
}
}
+159
View File
@@ -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
})
}
*/
+148
View File
@@ -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()
}
+7
View File
@@ -0,0 +1,7 @@
// +build cgo
package version
func init() {
CgoEnabled = true
}
+74
View File
@@ -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()
}
+14
View File
@@ -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 = ""
)