Discovery consul service meta (#4280)
* Upgrade Consul client * Add ServiceMeta to the labels in ConsulSD Signed-off-by: Romain Baugue <romain.baugue@elwinar.com>pull/4347/merge
parent
d0f11a3cc6
commit
b41be4ef52
@ -0,0 +1,193 @@ |
||||
// The /v1/operator/area endpoints are available only in Consul Enterprise and
|
||||
// interact with its network area subsystem. Network areas are used to link
|
||||
// together Consul servers in different Consul datacenters. With network areas,
|
||||
// Consul datacenters can be linked together in ways other than a fully-connected
|
||||
// mesh, as is required for Consul's WAN.
|
||||
package api |
||||
|
||||
import ( |
||||
"net" |
||||
"time" |
||||
) |
||||
|
||||
// Area defines a network area.
|
||||
type Area struct { |
||||
// ID is this identifier for an area (a UUID). This must be left empty
|
||||
// when creating a new area.
|
||||
ID string |
||||
|
||||
// PeerDatacenter is the peer Consul datacenter that will make up the
|
||||
// other side of this network area. Network areas always involve a pair
|
||||
// of datacenters: the datacenter where the area was created, and the
|
||||
// peer datacenter. This is required.
|
||||
PeerDatacenter string |
||||
|
||||
// RetryJoin specifies the address of Consul servers to join to, such as
|
||||
// an IPs or hostnames with an optional port number. This is optional.
|
||||
RetryJoin []string |
||||
|
||||
// UseTLS specifies whether gossip over this area should be encrypted with TLS
|
||||
// if possible.
|
||||
UseTLS bool |
||||
} |
||||
|
||||
// AreaJoinResponse is returned when a join occurs and gives the result for each
|
||||
// address.
|
||||
type AreaJoinResponse struct { |
||||
// The address that was joined.
|
||||
Address string |
||||
|
||||
// Whether or not the join was a success.
|
||||
Joined bool |
||||
|
||||
// If we couldn't join, this is the message with information.
|
||||
Error string |
||||
} |
||||
|
||||
// SerfMember is a generic structure for reporting information about members in
|
||||
// a Serf cluster. This is only used by the area endpoints right now, but this
|
||||
// could be expanded to other endpoints in the future.
|
||||
type SerfMember struct { |
||||
// ID is the node identifier (a UUID).
|
||||
ID string |
||||
|
||||
// Name is the node name.
|
||||
Name string |
||||
|
||||
// Addr has the IP address.
|
||||
Addr net.IP |
||||
|
||||
// Port is the RPC port.
|
||||
Port uint16 |
||||
|
||||
// Datacenter is the DC name.
|
||||
Datacenter string |
||||
|
||||
// Role is "client", "server", or "unknown".
|
||||
Role string |
||||
|
||||
// Build has the version of the Consul agent.
|
||||
Build string |
||||
|
||||
// Protocol is the protocol of the Consul agent.
|
||||
Protocol int |
||||
|
||||
// Status is the Serf health status "none", "alive", "leaving", "left",
|
||||
// or "failed".
|
||||
Status string |
||||
|
||||
// RTT is the estimated round trip time from the server handling the
|
||||
// request to the this member. This will be negative if no RTT estimate
|
||||
// is available.
|
||||
RTT time.Duration |
||||
} |
||||
|
||||
// AreaCreate will create a new network area. The ID in the given structure must
|
||||
// be empty and a generated ID will be returned on success.
|
||||
func (op *Operator) AreaCreate(area *Area, q *WriteOptions) (string, *WriteMeta, error) { |
||||
r := op.c.newRequest("POST", "/v1/operator/area") |
||||
r.setWriteOptions(q) |
||||
r.obj = area |
||||
rtt, resp, err := requireOK(op.c.doRequest(r)) |
||||
if err != nil { |
||||
return "", nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
wm := &WriteMeta{} |
||||
wm.RequestTime = rtt |
||||
|
||||
var out struct{ ID string } |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return "", nil, err |
||||
} |
||||
return out.ID, wm, nil |
||||
} |
||||
|
||||
// AreaUpdate will update the configuration of the network area with the given ID.
|
||||
func (op *Operator) AreaUpdate(areaID string, area *Area, q *WriteOptions) (string, *WriteMeta, error) { |
||||
r := op.c.newRequest("PUT", "/v1/operator/area/"+areaID) |
||||
r.setWriteOptions(q) |
||||
r.obj = area |
||||
rtt, resp, err := requireOK(op.c.doRequest(r)) |
||||
if err != nil { |
||||
return "", nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
wm := &WriteMeta{} |
||||
wm.RequestTime = rtt |
||||
|
||||
var out struct{ ID string } |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return "", nil, err |
||||
} |
||||
return out.ID, wm, nil |
||||
} |
||||
|
||||
// AreaGet returns a single network area.
|
||||
func (op *Operator) AreaGet(areaID string, q *QueryOptions) ([]*Area, *QueryMeta, error) { |
||||
var out []*Area |
||||
qm, err := op.c.query("/v1/operator/area/"+areaID, &out, q) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return out, qm, nil |
||||
} |
||||
|
||||
// AreaList returns all the available network areas.
|
||||
func (op *Operator) AreaList(q *QueryOptions) ([]*Area, *QueryMeta, error) { |
||||
var out []*Area |
||||
qm, err := op.c.query("/v1/operator/area", &out, q) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return out, qm, nil |
||||
} |
||||
|
||||
// AreaDelete deletes the given network area.
|
||||
func (op *Operator) AreaDelete(areaID string, q *WriteOptions) (*WriteMeta, error) { |
||||
r := op.c.newRequest("DELETE", "/v1/operator/area/"+areaID) |
||||
r.setWriteOptions(q) |
||||
rtt, resp, err := requireOK(op.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
wm := &WriteMeta{} |
||||
wm.RequestTime = rtt |
||||
return wm, nil |
||||
} |
||||
|
||||
// AreaJoin attempts to join the given set of join addresses to the given
|
||||
// network area. See the Area structure for details about join addresses.
|
||||
func (op *Operator) AreaJoin(areaID string, addresses []string, q *WriteOptions) ([]*AreaJoinResponse, *WriteMeta, error) { |
||||
r := op.c.newRequest("PUT", "/v1/operator/area/"+areaID+"/join") |
||||
r.setWriteOptions(q) |
||||
r.obj = addresses |
||||
rtt, resp, err := requireOK(op.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
wm := &WriteMeta{} |
||||
wm.RequestTime = rtt |
||||
|
||||
var out []*AreaJoinResponse |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return out, wm, nil |
||||
} |
||||
|
||||
// AreaMembers lists the Serf information about the members in the given area.
|
||||
func (op *Operator) AreaMembers(areaID string, q *QueryOptions) ([]*SerfMember, *QueryMeta, error) { |
||||
var out []*SerfMember |
||||
qm, err := op.c.query("/v1/operator/area/"+areaID+"/members", &out, q) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return out, qm, nil |
||||
} |
||||
@ -0,0 +1,219 @@ |
||||
package api |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"io" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
// AutopilotConfiguration is used for querying/setting the Autopilot configuration.
|
||||
// Autopilot helps manage operator tasks related to Consul servers like removing
|
||||
// failed servers from the Raft quorum.
|
||||
type AutopilotConfiguration struct { |
||||
// CleanupDeadServers controls whether to remove dead servers from the Raft
|
||||
// peer list when a new server joins
|
||||
CleanupDeadServers bool |
||||
|
||||
// LastContactThreshold is the limit on the amount of time a server can go
|
||||
// without leader contact before being considered unhealthy.
|
||||
LastContactThreshold *ReadableDuration |
||||
|
||||
// MaxTrailingLogs is the amount of entries in the Raft Log that a server can
|
||||
// be behind before being considered unhealthy.
|
||||
MaxTrailingLogs uint64 |
||||
|
||||
// ServerStabilizationTime is the minimum amount of time a server must be
|
||||
// in a stable, healthy state before it can be added to the cluster. Only
|
||||
// applicable with Raft protocol version 3 or higher.
|
||||
ServerStabilizationTime *ReadableDuration |
||||
|
||||
// (Enterprise-only) RedundancyZoneTag is the node tag to use for separating
|
||||
// servers into zones for redundancy. If left blank, this feature will be disabled.
|
||||
RedundancyZoneTag string |
||||
|
||||
// (Enterprise-only) DisableUpgradeMigration will disable Autopilot's upgrade migration
|
||||
// strategy of waiting until enough newer-versioned servers have been added to the
|
||||
// cluster before promoting them to voters.
|
||||
DisableUpgradeMigration bool |
||||
|
||||
// (Enterprise-only) UpgradeVersionTag is the node tag to use for version info when
|
||||
// performing upgrade migrations. If left blank, the Consul version will be used.
|
||||
UpgradeVersionTag string |
||||
|
||||
// CreateIndex holds the index corresponding the creation of this configuration.
|
||||
// This is a read-only field.
|
||||
CreateIndex uint64 |
||||
|
||||
// ModifyIndex will be set to the index of the last update when retrieving the
|
||||
// Autopilot configuration. Resubmitting a configuration with
|
||||
// AutopilotCASConfiguration will perform a check-and-set operation which ensures
|
||||
// there hasn't been a subsequent update since the configuration was retrieved.
|
||||
ModifyIndex uint64 |
||||
} |
||||
|
||||
// ServerHealth is the health (from the leader's point of view) of a server.
|
||||
type ServerHealth struct { |
||||
// ID is the raft ID of the server.
|
||||
ID string |
||||
|
||||
// Name is the node name of the server.
|
||||
Name string |
||||
|
||||
// Address is the address of the server.
|
||||
Address string |
||||
|
||||
// The status of the SerfHealth check for the server.
|
||||
SerfStatus string |
||||
|
||||
// Version is the Consul version of the server.
|
||||
Version string |
||||
|
||||
// Leader is whether this server is currently the leader.
|
||||
Leader bool |
||||
|
||||
// LastContact is the time since this node's last contact with the leader.
|
||||
LastContact *ReadableDuration |
||||
|
||||
// LastTerm is the highest leader term this server has a record of in its Raft log.
|
||||
LastTerm uint64 |
||||
|
||||
// LastIndex is the last log index this server has a record of in its Raft log.
|
||||
LastIndex uint64 |
||||
|
||||
// Healthy is whether or not the server is healthy according to the current
|
||||
// Autopilot config.
|
||||
Healthy bool |
||||
|
||||
// Voter is whether this is a voting server.
|
||||
Voter bool |
||||
|
||||
// StableSince is the last time this server's Healthy value changed.
|
||||
StableSince time.Time |
||||
} |
||||
|
||||
// OperatorHealthReply is a representation of the overall health of the cluster
|
||||
type OperatorHealthReply struct { |
||||
// Healthy is true if all the servers in the cluster are healthy.
|
||||
Healthy bool |
||||
|
||||
// FailureTolerance is the number of healthy servers that could be lost without
|
||||
// an outage occurring.
|
||||
FailureTolerance int |
||||
|
||||
// Servers holds the health of each server.
|
||||
Servers []ServerHealth |
||||
} |
||||
|
||||
// ReadableDuration is a duration type that is serialized to JSON in human readable format.
|
||||
type ReadableDuration time.Duration |
||||
|
||||
func NewReadableDuration(dur time.Duration) *ReadableDuration { |
||||
d := ReadableDuration(dur) |
||||
return &d |
||||
} |
||||
|
||||
func (d *ReadableDuration) String() string { |
||||
return d.Duration().String() |
||||
} |
||||
|
||||
func (d *ReadableDuration) Duration() time.Duration { |
||||
if d == nil { |
||||
return time.Duration(0) |
||||
} |
||||
return time.Duration(*d) |
||||
} |
||||
|
||||
func (d *ReadableDuration) MarshalJSON() ([]byte, error) { |
||||
return []byte(fmt.Sprintf(`"%s"`, d.Duration().String())), nil |
||||
} |
||||
|
||||
func (d *ReadableDuration) UnmarshalJSON(raw []byte) error { |
||||
if d == nil { |
||||
return fmt.Errorf("cannot unmarshal to nil pointer") |
||||
} |
||||
|
||||
str := string(raw) |
||||
if len(str) < 2 || str[0] != '"' || str[len(str)-1] != '"' { |
||||
return fmt.Errorf("must be enclosed with quotes: %s", str) |
||||
} |
||||
dur, err := time.ParseDuration(str[1 : len(str)-1]) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
*d = ReadableDuration(dur) |
||||
return nil |
||||
} |
||||
|
||||
// AutopilotGetConfiguration is used to query the current Autopilot configuration.
|
||||
func (op *Operator) AutopilotGetConfiguration(q *QueryOptions) (*AutopilotConfiguration, error) { |
||||
r := op.c.newRequest("GET", "/v1/operator/autopilot/configuration") |
||||
r.setQueryOptions(q) |
||||
_, resp, err := requireOK(op.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
var out AutopilotConfiguration |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &out, nil |
||||
} |
||||
|
||||
// AutopilotSetConfiguration is used to set the current Autopilot configuration.
|
||||
func (op *Operator) AutopilotSetConfiguration(conf *AutopilotConfiguration, q *WriteOptions) error { |
||||
r := op.c.newRequest("PUT", "/v1/operator/autopilot/configuration") |
||||
r.setWriteOptions(q) |
||||
r.obj = conf |
||||
_, resp, err := requireOK(op.c.doRequest(r)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
resp.Body.Close() |
||||
return nil |
||||
} |
||||
|
||||
// AutopilotCASConfiguration is used to perform a Check-And-Set update on the
|
||||
// Autopilot configuration. The ModifyIndex value will be respected. Returns
|
||||
// true on success or false on failures.
|
||||
func (op *Operator) AutopilotCASConfiguration(conf *AutopilotConfiguration, q *WriteOptions) (bool, error) { |
||||
r := op.c.newRequest("PUT", "/v1/operator/autopilot/configuration") |
||||
r.setWriteOptions(q) |
||||
r.params.Set("cas", strconv.FormatUint(conf.ModifyIndex, 10)) |
||||
r.obj = conf |
||||
_, resp, err := requireOK(op.c.doRequest(r)) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
var buf bytes.Buffer |
||||
if _, err := io.Copy(&buf, resp.Body); err != nil { |
||||
return false, fmt.Errorf("Failed to read response: %v", err) |
||||
} |
||||
res := strings.Contains(buf.String(), "true") |
||||
|
||||
return res, nil |
||||
} |
||||
|
||||
// AutopilotServerHealth
|
||||
func (op *Operator) AutopilotServerHealth(q *QueryOptions) (*OperatorHealthReply, error) { |
||||
r := op.c.newRequest("GET", "/v1/operator/autopilot/health") |
||||
r.setQueryOptions(q) |
||||
_, resp, err := requireOK(op.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
var out OperatorHealthReply |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, err |
||||
} |
||||
return &out, nil |
||||
} |
||||
@ -0,0 +1,86 @@ |
||||
package api |
||||
|
||||
// keyringRequest is used for performing Keyring operations
|
||||
type keyringRequest struct { |
||||
Key string |
||||
} |
||||
|
||||
// KeyringResponse is returned when listing the gossip encryption keys
|
||||
type KeyringResponse struct { |
||||
// Whether this response is for a WAN ring
|
||||
WAN bool |
||||
|
||||
// The datacenter name this request corresponds to
|
||||
Datacenter string |
||||
|
||||
// Segment has the network segment this request corresponds to.
|
||||
Segment string |
||||
|
||||
// A map of the encryption keys to the number of nodes they're installed on
|
||||
Keys map[string]int |
||||
|
||||
// The total number of nodes in this ring
|
||||
NumNodes int |
||||
} |
||||
|
||||
// KeyringInstall is used to install a new gossip encryption key into the cluster
|
||||
func (op *Operator) KeyringInstall(key string, q *WriteOptions) error { |
||||
r := op.c.newRequest("POST", "/v1/operator/keyring") |
||||
r.setWriteOptions(q) |
||||
r.obj = keyringRequest{ |
||||
Key: key, |
||||
} |
||||
_, resp, err := requireOK(op.c.doRequest(r)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
resp.Body.Close() |
||||
return nil |
||||
} |
||||
|
||||
// KeyringList is used to list the gossip keys installed in the cluster
|
||||
func (op *Operator) KeyringList(q *QueryOptions) ([]*KeyringResponse, error) { |
||||
r := op.c.newRequest("GET", "/v1/operator/keyring") |
||||
r.setQueryOptions(q) |
||||
_, resp, err := requireOK(op.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
var out []*KeyringResponse |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, err |
||||
} |
||||
return out, nil |
||||
} |
||||
|
||||
// KeyringRemove is used to remove a gossip encryption key from the cluster
|
||||
func (op *Operator) KeyringRemove(key string, q *WriteOptions) error { |
||||
r := op.c.newRequest("DELETE", "/v1/operator/keyring") |
||||
r.setWriteOptions(q) |
||||
r.obj = keyringRequest{ |
||||
Key: key, |
||||
} |
||||
_, resp, err := requireOK(op.c.doRequest(r)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
resp.Body.Close() |
||||
return nil |
||||
} |
||||
|
||||
// KeyringUse is used to change the active gossip encryption key
|
||||
func (op *Operator) KeyringUse(key string, q *WriteOptions) error { |
||||
r := op.c.newRequest("PUT", "/v1/operator/keyring") |
||||
r.setWriteOptions(q) |
||||
r.obj = keyringRequest{ |
||||
Key: key, |
||||
} |
||||
_, resp, err := requireOK(op.c.doRequest(r)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
resp.Body.Close() |
||||
return nil |
||||
} |
||||
@ -0,0 +1,89 @@ |
||||
package api |
||||
|
||||
// RaftServer has information about a server in the Raft configuration.
|
||||
type RaftServer struct { |
||||
// ID is the unique ID for the server. These are currently the same
|
||||
// as the address, but they will be changed to a real GUID in a future
|
||||
// release of Consul.
|
||||
ID string |
||||
|
||||
// Node is the node name of the server, as known by Consul, or this
|
||||
// will be set to "(unknown)" otherwise.
|
||||
Node string |
||||
|
||||
// Address is the IP:port of the server, used for Raft communications.
|
||||
Address string |
||||
|
||||
// Leader is true if this server is the current cluster leader.
|
||||
Leader bool |
||||
|
||||
// Protocol version is the raft protocol version used by the server
|
||||
ProtocolVersion string |
||||
|
||||
// Voter is true if this server has a vote in the cluster. This might
|
||||
// be false if the server is staging and still coming online, or if
|
||||
// it's a non-voting server, which will be added in a future release of
|
||||
// Consul.
|
||||
Voter bool |
||||
} |
||||
|
||||
// RaftConfiguration is returned when querying for the current Raft configuration.
|
||||
type RaftConfiguration struct { |
||||
// Servers has the list of servers in the Raft configuration.
|
||||
Servers []*RaftServer |
||||
|
||||
// Index has the Raft index of this configuration.
|
||||
Index uint64 |
||||
} |
||||
|
||||
// RaftGetConfiguration is used to query the current Raft peer set.
|
||||
func (op *Operator) RaftGetConfiguration(q *QueryOptions) (*RaftConfiguration, error) { |
||||
r := op.c.newRequest("GET", "/v1/operator/raft/configuration") |
||||
r.setQueryOptions(q) |
||||
_, resp, err := requireOK(op.c.doRequest(r)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
var out RaftConfiguration |
||||
if err := decodeBody(resp, &out); err != nil { |
||||
return nil, err |
||||
} |
||||
return &out, nil |
||||
} |
||||
|
||||
// RaftRemovePeerByAddress is used to kick a stale peer (one that it in the Raft
|
||||
// quorum but no longer known to Serf or the catalog) by address in the form of
|
||||
// "IP:port".
|
||||
func (op *Operator) RaftRemovePeerByAddress(address string, q *WriteOptions) error { |
||||
r := op.c.newRequest("DELETE", "/v1/operator/raft/peer") |
||||
r.setWriteOptions(q) |
||||
|
||||
r.params.Set("address", string(address)) |
||||
|
||||
_, resp, err := requireOK(op.c.doRequest(r)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
resp.Body.Close() |
||||
return nil |
||||
} |
||||
|
||||
// RaftRemovePeerByID is used to kick a stale peer (one that it in the Raft
|
||||
// quorum but no longer known to Serf or the catalog) by ID.
|
||||
func (op *Operator) RaftRemovePeerByID(id string, q *WriteOptions) error { |
||||
r := op.c.newRequest("DELETE", "/v1/operator/raft/peer") |
||||
r.setWriteOptions(q) |
||||
|
||||
r.params.Set("id", string(id)) |
||||
|
||||
_, resp, err := requireOK(op.c.doRequest(r)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
resp.Body.Close() |
||||
return nil |
||||
} |
||||
@ -0,0 +1,11 @@ |
||||
package api |
||||
|
||||
// SegmentList returns all the available LAN segments.
|
||||
func (op *Operator) SegmentList(q *QueryOptions) ([]string, *QueryMeta, error) { |
||||
var out []string |
||||
qm, err := op.c.query("/v1/operator/segment", &out, q) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return out, qm, nil |
||||
} |
||||
@ -0,0 +1,363 @@ |
||||
Mozilla Public License, version 2.0 |
||||
|
||||
1. Definitions |
||||
|
||||
1.1. "Contributor" |
||||
|
||||
means each individual or legal entity that creates, contributes to the |
||||
creation of, or owns Covered Software. |
||||
|
||||
1.2. "Contributor Version" |
||||
|
||||
means the combination of the Contributions of others (if any) used by a |
||||
Contributor and that particular Contributor's Contribution. |
||||
|
||||
1.3. "Contribution" |
||||
|
||||
means Covered Software of a particular Contributor. |
||||
|
||||
1.4. "Covered Software" |
||||
|
||||
means Source Code Form to which the initial Contributor has attached the |
||||
notice in Exhibit A, the Executable Form of such Source Code Form, and |
||||
Modifications of such Source Code Form, in each case including portions |
||||
thereof. |
||||
|
||||
1.5. "Incompatible With Secondary Licenses" |
||||
means |
||||
|
||||
a. that the initial Contributor has attached the notice described in |
||||
Exhibit B to the Covered Software; or |
||||
|
||||
b. that the Covered Software was made available under the terms of |
||||
version 1.1 or earlier of the License, but not also under the terms of |
||||
a Secondary License. |
||||
|
||||
1.6. "Executable Form" |
||||
|
||||
means any form of the work other than Source Code Form. |
||||
|
||||
1.7. "Larger Work" |
||||
|
||||
means a work that combines Covered Software with other material, in a |
||||
separate file or files, that is not Covered Software. |
||||
|
||||
1.8. "License" |
||||
|
||||
means this document. |
||||
|
||||
1.9. "Licensable" |
||||
|
||||
means having the right to grant, to the maximum extent possible, whether |
||||
at the time of the initial grant or subsequently, any and all of the |
||||
rights conveyed by this License. |
||||
|
||||
1.10. "Modifications" |
||||
|
||||
means any of the following: |
||||
|
||||
a. any file in Source Code Form that results from an addition to, |
||||
deletion from, or modification of the contents of Covered Software; or |
||||
|
||||
b. any new file in Source Code Form that contains any Covered Software. |
||||
|
||||
1.11. "Patent Claims" of a Contributor |
||||
|
||||
means any patent claim(s), including without limitation, method, |
||||
process, and apparatus claims, in any patent Licensable by such |
||||
Contributor that would be infringed, but for the grant of the License, |
||||
by the making, using, selling, offering for sale, having made, import, |
||||
or transfer of either its Contributions or its Contributor Version. |
||||
|
||||
1.12. "Secondary License" |
||||
|
||||
means either the GNU General Public License, Version 2.0, the GNU Lesser |
||||
General Public License, Version 2.1, the GNU Affero General Public |
||||
License, Version 3.0, or any later versions of those licenses. |
||||
|
||||
1.13. "Source Code Form" |
||||
|
||||
means the form of the work preferred for making modifications. |
||||
|
||||
1.14. "You" (or "Your") |
||||
|
||||
means an individual or a legal entity exercising rights under this |
||||
License. For legal entities, "You" includes any entity that controls, is |
||||
controlled by, or is under common control with You. For purposes of this |
||||
definition, "control" means (a) the power, direct or indirect, to cause |
||||
the direction or management of such entity, whether by contract or |
||||
otherwise, or (b) ownership of more than fifty percent (50%) of the |
||||
outstanding shares or beneficial ownership of such entity. |
||||
|
||||
|
||||
2. License Grants and Conditions |
||||
|
||||
2.1. Grants |
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free, |
||||
non-exclusive license: |
||||
|
||||
a. under intellectual property rights (other than patent or trademark) |
||||
Licensable by such Contributor to use, reproduce, make available, |
||||
modify, display, perform, distribute, and otherwise exploit its |
||||
Contributions, either on an unmodified basis, with Modifications, or |
||||
as part of a Larger Work; and |
||||
|
||||
b. under Patent Claims of such Contributor to make, use, sell, offer for |
||||
sale, have made, import, and otherwise transfer either its |
||||
Contributions or its Contributor Version. |
||||
|
||||
2.2. Effective Date |
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution |
||||
become effective for each Contribution on the date the Contributor first |
||||
distributes such Contribution. |
||||
|
||||
2.3. Limitations on Grant Scope |
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under |
||||
this License. No additional rights or licenses will be implied from the |
||||
distribution or licensing of Covered Software under this License. |
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a |
||||
Contributor: |
||||
|
||||
a. for any code that a Contributor has removed from Covered Software; or |
||||
|
||||
b. for infringements caused by: (i) Your and any other third party's |
||||
modifications of Covered Software, or (ii) the combination of its |
||||
Contributions with other software (except as part of its Contributor |
||||
Version); or |
||||
|
||||
c. under Patent Claims infringed by Covered Software in the absence of |
||||
its Contributions. |
||||
|
||||
This License does not grant any rights in the trademarks, service marks, |
||||
or logos of any Contributor (except as may be necessary to comply with |
||||
the notice requirements in Section 3.4). |
||||
|
||||
2.4. Subsequent Licenses |
||||
|
||||
No Contributor makes additional grants as a result of Your choice to |
||||
distribute the Covered Software under a subsequent version of this |
||||
License (see Section 10.2) or under the terms of a Secondary License (if |
||||
permitted under the terms of Section 3.3). |
||||
|
||||
2.5. Representation |
||||
|
||||
Each Contributor represents that the Contributor believes its |
||||
Contributions are its original creation(s) or it has sufficient rights to |
||||
grant the rights to its Contributions conveyed by this License. |
||||
|
||||
2.6. Fair Use |
||||
|
||||
This License is not intended to limit any rights You have under |
||||
applicable copyright doctrines of fair use, fair dealing, or other |
||||
equivalents. |
||||
|
||||
2.7. Conditions |
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in |
||||
Section 2.1. |
||||
|
||||
|
||||
3. Responsibilities |
||||
|
||||
3.1. Distribution of Source Form |
||||
|
||||
All distribution of Covered Software in Source Code Form, including any |
||||
Modifications that You create or to which You contribute, must be under |
||||
the terms of this License. You must inform recipients that the Source |
||||
Code Form of the Covered Software is governed by the terms of this |
||||
License, and how they can obtain a copy of this License. You may not |
||||
attempt to alter or restrict the recipients' rights in the Source Code |
||||
Form. |
||||
|
||||
3.2. Distribution of Executable Form |
||||
|
||||
If You distribute Covered Software in Executable Form then: |
||||
|
||||
a. such Covered Software must also be made available in Source Code Form, |
||||
as described in Section 3.1, and You must inform recipients of the |
||||
Executable Form how they can obtain a copy of such Source Code Form by |
||||
reasonable means in a timely manner, at a charge no more than the cost |
||||
of distribution to the recipient; and |
||||
|
||||
b. You may distribute such Executable Form under the terms of this |
||||
License, or sublicense it under different terms, provided that the |
||||
license for the Executable Form does not attempt to limit or alter the |
||||
recipients' rights in the Source Code Form under this License. |
||||
|
||||
3.3. Distribution of a Larger Work |
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice, |
||||
provided that You also comply with the requirements of this License for |
||||
the Covered Software. If the Larger Work is a combination of Covered |
||||
Software with a work governed by one or more Secondary Licenses, and the |
||||
Covered Software is not Incompatible With Secondary Licenses, this |
||||
License permits You to additionally distribute such Covered Software |
||||
under the terms of such Secondary License(s), so that the recipient of |
||||
the Larger Work may, at their option, further distribute the Covered |
||||
Software under the terms of either this License or such Secondary |
||||
License(s). |
||||
|
||||
3.4. Notices |
||||
|
||||
You may not remove or alter the substance of any license notices |
||||
(including copyright notices, patent notices, disclaimers of warranty, or |
||||
limitations of liability) contained within the Source Code Form of the |
||||
Covered Software, except that You may alter any license notices to the |
||||
extent required to remedy known factual inaccuracies. |
||||
|
||||
3.5. Application of Additional Terms |
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support, |
||||
indemnity or liability obligations to one or more recipients of Covered |
||||
Software. However, You may do so only on Your own behalf, and not on |
||||
behalf of any Contributor. You must make it absolutely clear that any |
||||
such warranty, support, indemnity, or liability obligation is offered by |
||||
You alone, and You hereby agree to indemnify every Contributor for any |
||||
liability incurred by such Contributor as a result of warranty, support, |
||||
indemnity or liability terms You offer. You may include additional |
||||
disclaimers of warranty and limitations of liability specific to any |
||||
jurisdiction. |
||||
|
||||
4. Inability to Comply Due to Statute or Regulation |
||||
|
||||
If it is impossible for You to comply with any of the terms of this License |
||||
with respect to some or all of the Covered Software due to statute, |
||||
judicial order, or regulation then You must: (a) comply with the terms of |
||||
this License to the maximum extent possible; and (b) describe the |
||||
limitations and the code they affect. Such description must be placed in a |
||||
text file included with all distributions of the Covered Software under |
||||
this License. Except to the extent prohibited by statute or regulation, |
||||
such description must be sufficiently detailed for a recipient of ordinary |
||||
skill to be able to understand it. |
||||
|
||||
5. Termination |
||||
|
||||
5.1. The rights granted under this License will terminate automatically if You |
||||
fail to comply with any of its terms. However, if You become compliant, |
||||
then the rights granted under this License from a particular Contributor |
||||
are reinstated (a) provisionally, unless and until such Contributor |
||||
explicitly and finally terminates Your grants, and (b) on an ongoing |
||||
basis, if such Contributor fails to notify You of the non-compliance by |
||||
some reasonable means prior to 60 days after You have come back into |
||||
compliance. Moreover, Your grants from a particular Contributor are |
||||
reinstated on an ongoing basis if such Contributor notifies You of the |
||||
non-compliance by some reasonable means, this is the first time You have |
||||
received notice of non-compliance with this License from such |
||||
Contributor, and You become compliant prior to 30 days after Your receipt |
||||
of the notice. |
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent |
||||
infringement claim (excluding declaratory judgment actions, |
||||
counter-claims, and cross-claims) alleging that a Contributor Version |
||||
directly or indirectly infringes any patent, then the rights granted to |
||||
You by any and all Contributors for the Covered Software under Section |
||||
2.1 of this License shall terminate. |
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user |
||||
license agreements (excluding distributors and resellers) which have been |
||||
validly granted by You or Your distributors under this License prior to |
||||
termination shall survive termination. |
||||
|
||||
6. Disclaimer of Warranty |
||||
|
||||
Covered Software is provided under this License on an "as is" basis, |
||||
without warranty of any kind, either expressed, implied, or statutory, |
||||
including, without limitation, warranties that the Covered Software is free |
||||
of defects, merchantable, fit for a particular purpose or non-infringing. |
||||
The entire risk as to the quality and performance of the Covered Software |
||||
is with You. Should any Covered Software prove defective in any respect, |
||||
You (not any Contributor) assume the cost of any necessary servicing, |
||||
repair, or correction. This disclaimer of warranty constitutes an essential |
||||
part of this License. No use of any Covered Software is authorized under |
||||
this License except under this disclaimer. |
||||
|
||||
7. Limitation of Liability |
||||
|
||||
Under no circumstances and under no legal theory, whether tort (including |
||||
negligence), contract, or otherwise, shall any Contributor, or anyone who |
||||
distributes Covered Software as permitted above, be liable to You for any |
||||
direct, indirect, special, incidental, or consequential damages of any |
||||
character including, without limitation, damages for lost profits, loss of |
||||
goodwill, work stoppage, computer failure or malfunction, or any and all |
||||
other commercial damages or losses, even if such party shall have been |
||||
informed of the possibility of such damages. This limitation of liability |
||||
shall not apply to liability for death or personal injury resulting from |
||||
such party's negligence to the extent applicable law prohibits such |
||||
limitation. Some jurisdictions do not allow the exclusion or limitation of |
||||
incidental or consequential damages, so this exclusion and limitation may |
||||
not apply to You. |
||||
|
||||
8. Litigation |
||||
|
||||
Any litigation relating to this License may be brought only in the courts |
||||
of a jurisdiction where the defendant maintains its principal place of |
||||
business and such litigation shall be governed by laws of that |
||||
jurisdiction, without reference to its conflict-of-law provisions. Nothing |
||||
in this Section shall prevent a party's ability to bring cross-claims or |
||||
counter-claims. |
||||
|
||||
9. Miscellaneous |
||||
|
||||
This License represents the complete agreement concerning the subject |
||||
matter hereof. If any provision of this License is held to be |
||||
unenforceable, such provision shall be reformed only to the extent |
||||
necessary to make it enforceable. Any law or regulation which provides that |
||||
the language of a contract shall be construed against the drafter shall not |
||||
be used to construe this License against a Contributor. |
||||
|
||||
|
||||
10. Versions of the License |
||||
|
||||
10.1. New Versions |
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section |
||||
10.3, no one other than the license steward has the right to modify or |
||||
publish new versions of this License. Each version will be given a |
||||
distinguishing version number. |
||||
|
||||
10.2. Effect of New Versions |
||||
|
||||
You may distribute the Covered Software under the terms of the version |
||||
of the License under which You originally received the Covered Software, |
||||
or under the terms of any subsequent version published by the license |
||||
steward. |
||||
|
||||
10.3. Modified Versions |
||||
|
||||
If you create software not governed by this License, and you want to |
||||
create a new license for such software, you may create and use a |
||||
modified version of this License if you rename the license and remove |
||||
any references to the name of the license steward (except to note that |
||||
such modified license differs from this License). |
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary |
||||
Licenses If You choose to distribute Source Code Form that is |
||||
Incompatible With Secondary Licenses under the terms of this version of |
||||
the License, the notice described in Exhibit B of this License must be |
||||
attached. |
||||
|
||||
Exhibit A - Source Code Form License Notice |
||||
|
||||
This Source Code Form is subject to the |
||||
terms of the Mozilla Public License, v. |
||||
2.0. If a copy of the MPL was not |
||||
distributed with this file, You can |
||||
obtain one at |
||||
http://mozilla.org/MPL/2.0/. |
||||
|
||||
If it is not possible or desirable to put the notice in a particular file, |
||||
then You may include the notice in a location (such as a LICENSE file in a |
||||
relevant directory) where a recipient would be likely to look for such a |
||||
notice. |
||||
|
||||
You may add additional accurate notices of copyright ownership. |
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice |
||||
|
||||
This Source Code Form is "Incompatible |
||||
With Secondary Licenses", as defined by |
||||
the Mozilla Public License, v. 2.0. |
||||
|
||||
@ -0,0 +1,8 @@ |
||||
TEST?=./...
|
||||
|
||||
test: |
||||
go test $(TEST) $(TESTARGS) -timeout=3s -parallel=4
|
||||
go vet $(TEST)
|
||||
go test $(TEST) -race
|
||||
|
||||
.PHONY: test |
||||
@ -0,0 +1,43 @@ |
||||
# rootcerts |
||||
|
||||
Functions for loading root certificates for TLS connections. |
||||
|
||||
----- |
||||
|
||||
Go's standard library `crypto/tls` provides a common mechanism for configuring |
||||
TLS connections in `tls.Config`. The `RootCAs` field on this struct is a pool |
||||
of certificates for the client to use as a trust store when verifying server |
||||
certificates. |
||||
|
||||
This library contains utility functions for loading certificates destined for |
||||
that field, as well as one other important thing: |
||||
|
||||
When the `RootCAs` field is `nil`, the standard library attempts to load the |
||||
host's root CA set. This behavior is OS-specific, and the Darwin |
||||
implementation contains [a bug that prevents trusted certificates from the |
||||
System and Login keychains from being loaded][1]. This library contains |
||||
Darwin-specific behavior that works around that bug. |
||||
|
||||
[1]: https://github.com/golang/go/issues/14514 |
||||
|
||||
## Example Usage |
||||
|
||||
Here's a snippet demonstrating how this library is meant to be used: |
||||
|
||||
```go |
||||
func httpClient() (*http.Client, error) |
||||
tlsConfig := &tls.Config{} |
||||
err := rootcerts.ConfigureTLS(tlsConfig, &rootcerts.Config{ |
||||
CAFile: os.Getenv("MYAPP_CAFILE"), |
||||
CAPath: os.Getenv("MYAPP_CAPATH"), |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
c := cleanhttp.DefaultClient() |
||||
t := cleanhttp.DefaultTransport() |
||||
t.TLSClientConfig = tlsConfig |
||||
c.Transport = t |
||||
return c, nil |
||||
} |
||||
``` |
||||
@ -0,0 +1,9 @@ |
||||
// Package rootcerts contains functions to aid in loading CA certificates for
|
||||
// TLS connections.
|
||||
//
|
||||
// In addition, its default behavior on Darwin works around an open issue [1]
|
||||
// in Go's crypto/x509 that prevents certicates from being loaded from the
|
||||
// System or Login keychains.
|
||||
//
|
||||
// [1] https://github.com/golang/go/issues/14514
|
||||
package rootcerts |
||||
@ -0,0 +1,103 @@ |
||||
package rootcerts |
||||
|
||||
import ( |
||||
"crypto/tls" |
||||
"crypto/x509" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"os" |
||||
"path/filepath" |
||||
) |
||||
|
||||
// Config determines where LoadCACerts will load certificates from. When both
|
||||
// CAFile and CAPath are blank, this library's functions will either load
|
||||
// system roots explicitly and return them, or set the CertPool to nil to allow
|
||||
// Go's standard library to load system certs.
|
||||
type Config struct { |
||||
// CAFile is a path to a PEM-encoded certificate file or bundle. Takes
|
||||
// precedence over CAPath.
|
||||
CAFile string |
||||
|
||||
// CAPath is a path to a directory populated with PEM-encoded certificates.
|
||||
CAPath string |
||||
} |
||||
|
||||
// ConfigureTLS sets up the RootCAs on the provided tls.Config based on the
|
||||
// Config specified.
|
||||
func ConfigureTLS(t *tls.Config, c *Config) error { |
||||
if t == nil { |
||||
return nil |
||||
} |
||||
pool, err := LoadCACerts(c) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
t.RootCAs = pool |
||||
return nil |
||||
} |
||||
|
||||
// LoadCACerts loads a CertPool based on the Config specified.
|
||||
func LoadCACerts(c *Config) (*x509.CertPool, error) { |
||||
if c == nil { |
||||
c = &Config{} |
||||
} |
||||
if c.CAFile != "" { |
||||
return LoadCAFile(c.CAFile) |
||||
} |
||||
if c.CAPath != "" { |
||||
return LoadCAPath(c.CAPath) |
||||
} |
||||
|
||||
return LoadSystemCAs() |
||||
} |
||||
|
||||
// LoadCAFile loads a single PEM-encoded file from the path specified.
|
||||
func LoadCAFile(caFile string) (*x509.CertPool, error) { |
||||
pool := x509.NewCertPool() |
||||
|
||||
pem, err := ioutil.ReadFile(caFile) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("Error loading CA File: %s", err) |
||||
} |
||||
|
||||
ok := pool.AppendCertsFromPEM(pem) |
||||
if !ok { |
||||
return nil, fmt.Errorf("Error loading CA File: Couldn't parse PEM in: %s", caFile) |
||||
} |
||||
|
||||
return pool, nil |
||||
} |
||||
|
||||
// LoadCAPath walks the provided path and loads all certificates encounted into
|
||||
// a pool.
|
||||
func LoadCAPath(caPath string) (*x509.CertPool, error) { |
||||
pool := x509.NewCertPool() |
||||
walkFn := func(path string, info os.FileInfo, err error) error { |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if info.IsDir() { |
||||
return nil |
||||
} |
||||
|
||||
pem, err := ioutil.ReadFile(path) |
||||
if err != nil { |
||||
return fmt.Errorf("Error loading file from CAPath: %s", err) |
||||
} |
||||
|
||||
ok := pool.AppendCertsFromPEM(pem) |
||||
if !ok { |
||||
return fmt.Errorf("Error loading CA Path: Couldn't parse PEM in: %s", path) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
err := filepath.Walk(caPath, walkFn) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return pool, nil |
||||
} |
||||
@ -0,0 +1,12 @@ |
||||
// +build !darwin
|
||||
|
||||
package rootcerts |
||||
|
||||
import "crypto/x509" |
||||
|
||||
// LoadSystemCAs does nothing on non-Darwin systems. We return nil so that
|
||||
// default behavior of standard TLS config libraries is triggered, which is to
|
||||
// load system certs.
|
||||
func LoadSystemCAs() (*x509.CertPool, error) { |
||||
return nil, nil |
||||
} |
||||
@ -0,0 +1,48 @@ |
||||
package rootcerts |
||||
|
||||
import ( |
||||
"crypto/x509" |
||||
"os/exec" |
||||
"path" |
||||
|
||||
"github.com/mitchellh/go-homedir" |
||||
) |
||||
|
||||
// LoadSystemCAs has special behavior on Darwin systems to work around
|
||||
func LoadSystemCAs() (*x509.CertPool, error) { |
||||
pool := x509.NewCertPool() |
||||
|
||||
for _, keychain := range certKeychains() { |
||||
err := addCertsFromKeychain(pool, keychain) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
return pool, nil |
||||
} |
||||
|
||||
func addCertsFromKeychain(pool *x509.CertPool, keychain string) error { |
||||
cmd := exec.Command("/usr/bin/security", "find-certificate", "-a", "-p", keychain) |
||||
data, err := cmd.Output() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
pool.AppendCertsFromPEM(data) |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func certKeychains() []string { |
||||
keychains := []string{ |
||||
"/System/Library/Keychains/SystemRootCertificates.keychain", |
||||
"/Library/Keychains/System.keychain", |
||||
} |
||||
home, err := homedir.Dir() |
||||
if err == nil { |
||||
loginKeychain := path.Join(home, "Library", "Keychains", "login.keychain") |
||||
keychains = append(keychains, loginKeychain) |
||||
} |
||||
return keychains |
||||
} |
||||
@ -0,0 +1,21 @@ |
||||
The MIT License (MIT) |
||||
|
||||
Copyright (c) 2013 Mitchell Hashimoto |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in |
||||
all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||
THE SOFTWARE. |
||||
@ -0,0 +1,14 @@ |
||||
# go-homedir |
||||
|
||||
This is a Go library for detecting the user's home directory without |
||||
the use of cgo, so the library can be used in cross-compilation environments. |
||||
|
||||
Usage is incredibly simple, just call `homedir.Dir()` to get the home directory |
||||
for a user, and `homedir.Expand()` to expand the `~` in a path to the home |
||||
directory. |
||||
|
||||
**Why not just use `os/user`?** The built-in `os/user` package requires |
||||
cgo on Darwin systems. This means that any Go code that uses that package |
||||
cannot cross compile. But 99% of the time the use for `os/user` is just to |
||||
retrieve the home directory, which we can do for the current user without |
||||
cgo. This library does that, enabling cross-compilation. |
||||
@ -0,0 +1,155 @@ |
||||
package homedir |
||||
|
||||
import ( |
||||
"bytes" |
||||
"errors" |
||||
"os" |
||||
"os/exec" |
||||
"path/filepath" |
||||
"runtime" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
) |
||||
|
||||
// DisableCache will disable caching of the home directory. Caching is enabled
|
||||
// by default.
|
||||
var DisableCache bool |
||||
|
||||
var homedirCache string |
||||
var cacheLock sync.RWMutex |
||||
|
||||
// Dir returns the home directory for the executing user.
|
||||
//
|
||||
// This uses an OS-specific method for discovering the home directory.
|
||||
// An error is returned if a home directory cannot be detected.
|
||||
func Dir() (string, error) { |
||||
if !DisableCache { |
||||
cacheLock.RLock() |
||||
cached := homedirCache |
||||
cacheLock.RUnlock() |
||||
if cached != "" { |
||||
return cached, nil |
||||
} |
||||
} |
||||
|
||||
cacheLock.Lock() |
||||
defer cacheLock.Unlock() |
||||
|
||||
var result string |
||||
var err error |
||||
if runtime.GOOS == "windows" { |
||||
result, err = dirWindows() |
||||
} else { |
||||
// Unix-like system, so just assume Unix
|
||||
result, err = dirUnix() |
||||
} |
||||
|
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
homedirCache = result |
||||
return result, nil |
||||
} |
||||
|
||||
// Expand expands the path to include the home directory if the path
|
||||
// is prefixed with `~`. If it isn't prefixed with `~`, the path is
|
||||
// returned as-is.
|
||||
func Expand(path string) (string, error) { |
||||
if len(path) == 0 { |
||||
return path, nil |
||||
} |
||||
|
||||
if path[0] != '~' { |
||||
return path, nil |
||||
} |
||||
|
||||
if len(path) > 1 && path[1] != '/' && path[1] != '\\' { |
||||
return "", errors.New("cannot expand user-specific home dir") |
||||
} |
||||
|
||||
dir, err := Dir() |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
return filepath.Join(dir, path[1:]), nil |
||||
} |
||||
|
||||
func dirUnix() (string, error) { |
||||
homeEnv := "HOME" |
||||
if runtime.GOOS == "plan9" { |
||||
// On plan9, env vars are lowercase.
|
||||
homeEnv = "home" |
||||
} |
||||
|
||||
// First prefer the HOME environmental variable
|
||||
if home := os.Getenv(homeEnv); home != "" { |
||||
return home, nil |
||||
} |
||||
|
||||
var stdout bytes.Buffer |
||||
|
||||
// If that fails, try OS specific commands
|
||||
if runtime.GOOS == "darwin" { |
||||
cmd := exec.Command("sh", "-c", `dscl -q . -read /Users/"$(whoami)" NFSHomeDirectory | sed 's/^[^ ]*: //'`) |
||||
cmd.Stdout = &stdout |
||||
if err := cmd.Run(); err == nil { |
||||
result := strings.TrimSpace(stdout.String()) |
||||
if result != "" { |
||||
return result, nil |
||||
} |
||||
} |
||||
} else { |
||||
cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid())) |
||||
cmd.Stdout = &stdout |
||||
if err := cmd.Run(); err != nil { |
||||
// If the error is ErrNotFound, we ignore it. Otherwise, return it.
|
||||
if err != exec.ErrNotFound { |
||||
return "", err |
||||
} |
||||
} else { |
||||
if passwd := strings.TrimSpace(stdout.String()); passwd != "" { |
||||
// username:password:uid:gid:gecos:home:shell
|
||||
passwdParts := strings.SplitN(passwd, ":", 7) |
||||
if len(passwdParts) > 5 { |
||||
return passwdParts[5], nil |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// If all else fails, try the shell
|
||||
stdout.Reset() |
||||
cmd := exec.Command("sh", "-c", "cd && pwd") |
||||
cmd.Stdout = &stdout |
||||
if err := cmd.Run(); err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
result := strings.TrimSpace(stdout.String()) |
||||
if result == "" { |
||||
return "", errors.New("blank output when reading home directory") |
||||
} |
||||
|
||||
return result, nil |
||||
} |
||||
|
||||
func dirWindows() (string, error) { |
||||
// First prefer the HOME environmental variable
|
||||
if home := os.Getenv("HOME"); home != "" { |
||||
return home, nil |
||||
} |
||||
|
||||
drive := os.Getenv("HOMEDRIVE") |
||||
path := os.Getenv("HOMEPATH") |
||||
home := drive + path |
||||
if drive == "" || path == "" { |
||||
home = os.Getenv("USERPROFILE") |
||||
} |
||||
if home == "" { |
||||
return "", errors.New("HOMEDRIVE, HOMEPATH, and USERPROFILE are blank") |
||||
} |
||||
|
||||
return home, nil |
||||
} |
||||
Loading…
Reference in new issue