diff --git a/api/connect.go b/api/connect.go index 0f75a45fad..4b4e06900d 100644 --- a/api/connect.go +++ b/api/connect.go @@ -1,37 +1,5 @@ package api -import ( - "time" -) - -// CARootList is the structure for the results of listing roots. -type CARootList struct { - ActiveRootID string - Roots []*CARoot -} - -// CARoot is a single CA within Connect. -type CARoot struct { - ID string - Name string - RootCert string - Active bool - CreateIndex uint64 - ModifyIndex uint64 -} - -type IssuedCert struct { - SerialNumber string - CertPEM string - PrivateKeyPEM string - Service string - ServiceURI string - ValidAfter time.Time - ValidBefore time.Time - CreateIndex uint64 - ModifyIndex uint64 -} - // Connect can be used to work with endpoints related to Connect, the // feature for securely connecting services within Consul. type Connect struct { @@ -42,24 +10,3 @@ type Connect struct { func (c *Client) Connect() *Connect { return &Connect{c} } - -// CARoots queries the list of available roots. -func (h *Connect) CARoots(q *QueryOptions) (*CARootList, *QueryMeta, error) { - r := h.c.newRequest("GET", "/v1/connect/ca/roots") - r.setQueryOptions(q) - rtt, resp, err := requireOK(h.c.doRequest(r)) - if err != nil { - return nil, nil, err - } - defer resp.Body.Close() - - qm := &QueryMeta{} - parseQueryMeta(resp, qm) - qm.RequestTime = rtt - - var out CARootList - if err := decodeBody(resp, &out); err != nil { - return nil, nil, err - } - return &out, qm, nil -} diff --git a/api/connect_ca.go b/api/connect_ca.go new file mode 100644 index 0000000000..19046c2ab9 --- /dev/null +++ b/api/connect_ca.go @@ -0,0 +1,80 @@ +package api + +import ( + "time" +) + +// CARootList is the structure for the results of listing roots. +type CARootList struct { + ActiveRootID string + Roots []*CARoot +} + +// CARoot represents a root CA certificate that is trusted. +type CARoot struct { + // ID is a globally unique ID (UUID) representing this CA root. + ID string + + // Name is a human-friendly name for this CA root. This value is + // opaque to Consul and is not used for anything internally. + Name string + + // RootCert is the PEM-encoded public certificate. + RootCert string + + // Active is true if this is the current active CA. This must only + // be true for exactly one CA. For any method that modifies roots in the + // state store, tests should be written to verify that multiple roots + // cannot be active. + Active bool + + CreateIndex uint64 + ModifyIndex uint64 +} + +// IssuedCert is a certificate that has been issued by a Connect CA. +type IssuedCert struct { + // SerialNumber is the unique serial number for this certificate. + // This is encoded in standard hex separated by :. + SerialNumber string + + // CertPEM and PrivateKeyPEM are the PEM-encoded certificate and private + // key for that cert, respectively. This should not be stored in the + // state store, but is present in the sign API response. + CertPEM string `json:",omitempty"` + PrivateKeyPEM string `json:",omitempty"` + + // Service is the name of the service for which the cert was issued. + // ServiceURI is the cert URI value. + Service string + ServiceURI string + + // ValidAfter and ValidBefore are the validity periods for the + // certificate. + ValidAfter time.Time + ValidBefore time.Time + + CreateIndex uint64 + ModifyIndex uint64 +} + +// CARoots queries the list of available roots. +func (h *Connect) CARoots(q *QueryOptions) (*CARootList, *QueryMeta, error) { + r := h.c.newRequest("GET", "/v1/connect/ca/roots") + r.setQueryOptions(q) + rtt, resp, err := requireOK(h.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out CARootList + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return &out, qm, nil +} diff --git a/api/connect_test.go b/api/connect_ca_test.go similarity index 100% rename from api/connect_test.go rename to api/connect_ca_test.go diff --git a/api/connect_intention.go b/api/connect_intention.go new file mode 100644 index 0000000000..b138dd4aef --- /dev/null +++ b/api/connect_intention.go @@ -0,0 +1,112 @@ +package api + +import ( + "time" +) + +// Intention defines an intention for the Connect Service Graph. This defines +// the allowed or denied behavior of a connection between two services using +// Connect. +type Intention struct { + // ID is the UUID-based ID for the intention, always generated by Consul. + ID string + + // Description is a human-friendly description of this intention. + // It is opaque to Consul and is only stored and transferred in API + // requests. + Description string + + // SourceNS, SourceName are the namespace and name, respectively, of + // the source service. Either of these may be the wildcard "*", but only + // the full value can be a wildcard. Partial wildcards are not allowed. + // The source may also be a non-Consul service, as specified by SourceType. + // + // DestinationNS, DestinationName is the same, but for the destination + // service. The same rules apply. The destination is always a Consul + // service. + SourceNS, SourceName string + DestinationNS, DestinationName string + + // SourceType is the type of the value for the source. + SourceType IntentionSourceType + + // Action is whether this is a whitelist or blacklist intention. + Action IntentionAction + + // DefaultAddr, DefaultPort of the local listening proxy (if any) to + // make this connection. + DefaultAddr string + DefaultPort int + + // Meta is arbitrary metadata associated with the intention. This is + // opaque to Consul but is served in API responses. + Meta map[string]string + + // CreatedAt and UpdatedAt keep track of when this record was created + // or modified. + CreatedAt, UpdatedAt time.Time + + CreateIndex uint64 + ModifyIndex uint64 +} + +// IntentionAction is the action that the intention represents. This +// can be "allow" or "deny" to whitelist or blacklist intentions. +type IntentionAction string + +const ( + IntentionActionAllow IntentionAction = "allow" + IntentionActionDeny IntentionAction = "deny" +) + +// IntentionSourceType is the type of the source within an intention. +type IntentionSourceType string + +const ( + // IntentionSourceConsul is a service within the Consul catalog. + IntentionSourceConsul IntentionSourceType = "consul" +) + +// Intentions returns the list of intentions. +func (h *Connect) Intentions(q *QueryOptions) ([]*Intention, *QueryMeta, error) { + r := h.c.newRequest("GET", "/v1/connect/intentions") + r.setQueryOptions(q) + rtt, resp, err := requireOK(h.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out []*Intention + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return out, qm, nil +} + +// IntentionCreate will create a new intention. The ID in the given +// structure must be empty and a generate ID will be returned on +// success. +func (c *Connect) IntentionCreate(ixn *Intention, q *WriteOptions) (string, *WriteMeta, error) { + r := c.c.newRequest("POST", "/v1/connect/intentions") + r.setWriteOptions(q) + r.obj = ixn + rtt, resp, err := requireOK(c.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 +} diff --git a/api/connect_intention_test.go b/api/connect_intention_test.go new file mode 100644 index 0000000000..2fc7426021 --- /dev/null +++ b/api/connect_intention_test.go @@ -0,0 +1,48 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAPI_ConnectIntentionCreate(t *testing.T) { + t.Parallel() + + require := require.New(t) + c, s := makeClient(t) + defer s.Stop() + + connect := c.Connect() + + // Create + ixn := testIntention() + id, _, err := connect.IntentionCreate(ixn, nil) + require.Nil(err) + require.NotEmpty(id) + + // List it + list, _, err := connect.Intentions(nil) + require.Nil(err) + require.Len(list, 1) + + actual := list[0] + ixn.ID = id + ixn.CreatedAt = actual.CreatedAt + ixn.UpdatedAt = actual.UpdatedAt + ixn.CreateIndex = actual.CreateIndex + ixn.ModifyIndex = actual.ModifyIndex + require.Equal(ixn, actual) +} + +func testIntention() *Intention { + return &Intention{ + SourceNS: "eng", + SourceName: "api", + DestinationNS: "eng", + DestinationName: "db", + Action: IntentionActionAllow, + SourceType: IntentionSourceConsul, + Meta: map[string]string{}, + } +}