Skip to content

Commit 0937a58

Browse files
Add Kubernetes service registration (hashicorp#8249)
1 parent 942dd1e commit 0937a58

File tree

21 files changed

+2123
-2
lines changed

21 files changed

+2123
-2
lines changed

command/commands.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import (
6666

6767
sr "github.com/hashicorp/vault/serviceregistration"
6868
csr "github.com/hashicorp/vault/serviceregistration/consul"
69+
ksr "github.com/hashicorp/vault/serviceregistration/kubernetes"
6970
)
7071

7172
const (
@@ -161,7 +162,8 @@ var (
161162
}
162163

163164
serviceRegistrations = map[string]sr.Factory{
164-
"consul": csr.NewServiceRegistration,
165+
"consul": csr.NewServiceRegistration,
166+
"kubernetes": ksr.NewServiceRegistration,
165167
}
166168
)
167169

command/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1279,7 +1279,7 @@ CLUSTER_SYNTHESIS_COMPLETE:
12791279

12801280
// If ServiceRegistration is configured, then the backend must support HA
12811281
isBackendHA := coreConfig.HAPhysical != nil && coreConfig.HAPhysical.HAEnabled()
1282-
if (coreConfig.ServiceRegistration != nil) && !isBackendHA {
1282+
if !c.flagDev && (coreConfig.ServiceRegistration != nil) && !isBackendHA {
12831283
c.UI.Output("service_registration is configured, but storage does not support HA")
12841284
return 1
12851285
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ require (
2525
github.com/aws/aws-sdk-go v1.25.41
2626
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect
2727
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
28+
github.com/cenkalti/backoff v2.2.1+incompatible
2829
github.com/chrismalek/oktasdk-go v0.0.0-20181212195951-3430665dfaa0
2930
github.com/cockroachdb/apd v1.1.0 // indirect
3031
github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c

sdk/helper/certutil/certutil_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,31 @@ func TestTLSConfig(t *testing.T) {
403403
}
404404
}
405405

406+
func TestNewCertPool(t *testing.T) {
407+
caExample := `-----BEGIN CERTIFICATE-----
408+
MIIC5zCCAc+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p
409+
a3ViZUNBMB4XDTE5MTIxMDIzMDUxOVoXDTI5MTIwODIzMDUxOVowFTETMBEGA1UE
410+
AxMKbWluaWt1YmVDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANFi
411+
/RIdMHd865X6JygTb9riX01DA3QnR+RoXDXNnj8D3LziLG2n8ItXMJvWbU3sxxyy
412+
nX9HxJ0SIeexj1cYzdQBtJDjO1/PeuKc4CZ7zCukCAtHz8mC7BDPOU7F7pggpcQ0
413+
/t/pa2m22hmCu8aDF9WlUYHtJpYATnI/A5vz/VFLR9daxmkl59Qo3oHITj7vAzSx
414+
/75r9cibpQyJ+FhiHOZHQWYY2JYw2g4v5hm5hg5SFM9yFcZ75ISI9ebyFFIl9iBY
415+
zAk9jqv1mXvLr0Q39AVwMTamvGuap1oocjM9NIhQvaFL/DNqF1ouDQjCf5u2imLc
416+
TraO1/2KO8fqwOZCOrMCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgKkMB0GA1UdJQQW
417+
MBQGCCsGAQUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
418+
DQEBCwUAA4IBAQBtVZCwCPqUUUpIClAlE9nc2fo2bTs9gsjXRmqdQ5oaSomSLE93
419+
aJWYFuAhxPXtlApbLYZfW2m1sM3mTVQN60y0uE4e1jdSN1ErYQ9slJdYDAMaEmOh
420+
iSexj+Nd1scUiMHV9lf3ps5J8sYeCpwZX3sPmw7lqZojTS12pANBDcigsaj5RRyN
421+
9GyP3WkSQUsTpWlDb9Fd+KNdkCVw7nClIpBPA2KW4BQKw/rNSvOFD61mbzc89lo0
422+
Q9IFGQFFF8jO18lbyWqnRBGXcS4/G7jQ3S7C121d14YLUeAYOM7pJykI1g4CLx9y
423+
vitin0L6nprauWkKO38XgM4T75qKZpqtiOcT
424+
-----END CERTIFICATE-----
425+
`
426+
if _, err := NewCertPool(bytes.NewReader([]byte(caExample))); err != nil {
427+
t.Fatal(err)
428+
}
429+
}
430+
406431
func refreshRSA8CertBundle() *CertBundle {
407432
initTest.Do(setCerts)
408433
return &CertBundle{

sdk/helper/certutil/helpers.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"encoding/pem"
1515
"errors"
1616
"fmt"
17+
"io"
18+
"io/ioutil"
1719
"math/big"
1820
"net"
1921
"net/url"
@@ -804,3 +806,50 @@ func SignCertificate(data *CreationBundle) (*ParsedCertBundle, error) {
804806

805807
return result, nil
806808
}
809+
810+
func NewCertPool(reader io.Reader) (*x509.CertPool, error) {
811+
pemBlock, err := ioutil.ReadAll(reader)
812+
if err != nil {
813+
return nil, err
814+
}
815+
certs, err := parseCertsPEM(pemBlock)
816+
if err != nil {
817+
return nil, fmt.Errorf("error reading certs: %s", err)
818+
}
819+
pool := x509.NewCertPool()
820+
for _, cert := range certs {
821+
pool.AddCert(cert)
822+
}
823+
return pool, nil
824+
}
825+
826+
// parseCertsPEM returns the x509.Certificates contained in the given PEM-encoded byte array
827+
// Returns an error if a certificate could not be parsed, or if the data does not contain any certificates
828+
func parseCertsPEM(pemCerts []byte) ([]*x509.Certificate, error) {
829+
ok := false
830+
certs := []*x509.Certificate{}
831+
for len(pemCerts) > 0 {
832+
var block *pem.Block
833+
block, pemCerts = pem.Decode(pemCerts)
834+
if block == nil {
835+
break
836+
}
837+
// Only use PEM "CERTIFICATE" blocks without extra headers
838+
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
839+
continue
840+
}
841+
842+
cert, err := x509.ParseCertificate(block.Bytes)
843+
if err != nil {
844+
return certs, err
845+
}
846+
847+
certs = append(certs, cert)
848+
ok = true
849+
}
850+
851+
if !ok {
852+
return certs, errors.New("data does not contain any valid RSA or ECDSA certificates")
853+
}
854+
return certs, nil
855+
}
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
package client
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto/tls"
7+
"encoding/json"
8+
"errors"
9+
"fmt"
10+
"net/http"
11+
"time"
12+
13+
"github.com/hashicorp/go-cleanhttp"
14+
"github.com/hashicorp/go-hclog"
15+
"github.com/hashicorp/go-retryablehttp"
16+
)
17+
18+
var (
19+
// Retry configuration
20+
RetryWaitMin = 500 * time.Millisecond
21+
RetryWaitMax = 30 * time.Second
22+
RetryMax = 10
23+
24+
// Standard errs
25+
ErrNamespaceUnset = errors.New(`"namespace" is unset`)
26+
ErrPodNameUnset = errors.New(`"podName" is unset`)
27+
ErrNotInCluster = errors.New("unable to load in-cluster configuration, KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined")
28+
)
29+
30+
// New instantiates a Client. The stopCh is used for exiting retry loops
31+
// when closed.
32+
func New(logger hclog.Logger, stopCh <-chan struct{}) (*Client, error) {
33+
config, err := inClusterConfig()
34+
if err != nil {
35+
return nil, err
36+
}
37+
return &Client{
38+
logger: logger,
39+
config: config,
40+
stopCh: stopCh,
41+
}, nil
42+
}
43+
44+
// Client is a minimal Kubernetes client. We rolled our own because the existing
45+
// Kubernetes client-go library available externally has a high number of dependencies
46+
// and we thought it wasn't worth it for only two API calls. If at some point they break
47+
// the client into smaller modules, or if we add quite a few methods to this client, it may
48+
// be worthwhile to revisit that decision.
49+
type Client struct {
50+
logger hclog.Logger
51+
config *Config
52+
stopCh <-chan struct{}
53+
}
54+
55+
// GetPod gets a pod from the Kubernetes API.
56+
func (c *Client) GetPod(namespace, podName string) (*Pod, error) {
57+
endpoint := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", namespace, podName)
58+
method := http.MethodGet
59+
60+
// Validate that we received required parameters.
61+
if namespace == "" {
62+
return nil, ErrNamespaceUnset
63+
}
64+
if podName == "" {
65+
return nil, ErrPodNameUnset
66+
}
67+
68+
req, err := http.NewRequest(method, c.config.Host+endpoint, nil)
69+
if err != nil {
70+
return nil, err
71+
}
72+
pod := &Pod{}
73+
if err := c.do(req, pod); err != nil {
74+
return nil, err
75+
}
76+
return pod, nil
77+
}
78+
79+
// PatchPod updates the pod's tags to the given ones.
80+
// It does so non-destructively, or in other words, without tearing down
81+
// the pod.
82+
func (c *Client) PatchPod(namespace, podName string, patches ...*Patch) error {
83+
endpoint := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", namespace, podName)
84+
method := http.MethodPatch
85+
86+
// Validate that we received required parameters.
87+
if namespace == "" {
88+
return ErrNamespaceUnset
89+
}
90+
if podName == "" {
91+
return ErrPodNameUnset
92+
}
93+
if len(patches) == 0 {
94+
// No work to perform.
95+
return nil
96+
}
97+
98+
var jsonPatches []map[string]interface{}
99+
for _, patch := range patches {
100+
if patch.Operation == Unset {
101+
return errors.New("patch operation must be set")
102+
}
103+
jsonPatches = append(jsonPatches, map[string]interface{}{
104+
"op": patch.Operation,
105+
"path": patch.Path,
106+
"value": patch.Value,
107+
})
108+
}
109+
body, err := json.Marshal(jsonPatches)
110+
if err != nil {
111+
return err
112+
}
113+
req, err := http.NewRequest(method, c.config.Host+endpoint, bytes.NewReader(body))
114+
if err != nil {
115+
return err
116+
}
117+
req.Header.Set("Content-Type", "application/json-patch+json")
118+
return c.do(req, nil)
119+
}
120+
121+
// do executes the given request, retrying if necessary.
122+
func (c *Client) do(req *http.Request, ptrToReturnObj interface{}) error {
123+
// Finish setting up a valid request.
124+
retryableReq, err := retryablehttp.FromRequest(req)
125+
if err != nil {
126+
return err
127+
}
128+
129+
// Build a context that will call the cancelFunc when we receive
130+
// a stop from our stopChan. This allows us to exit from our retry
131+
// loop during a shutdown, rather than hanging.
132+
ctx, cancelFunc := context.WithCancel(context.Background())
133+
go func(stopCh <-chan struct{}) {
134+
<-stopCh
135+
cancelFunc()
136+
}(c.stopCh)
137+
retryableReq.WithContext(ctx)
138+
139+
retryableReq.Header.Set("Authorization", "Bearer "+c.config.BearerToken)
140+
retryableReq.Header.Set("Accept", "application/json")
141+
142+
client := &retryablehttp.Client{
143+
HTTPClient: cleanhttp.DefaultClient(),
144+
RetryWaitMin: RetryWaitMin,
145+
RetryWaitMax: RetryWaitMax,
146+
RetryMax: RetryMax,
147+
CheckRetry: c.getCheckRetry(req),
148+
Backoff: retryablehttp.DefaultBackoff,
149+
}
150+
client.HTTPClient.Transport = &http.Transport{
151+
TLSClientConfig: &tls.Config{
152+
RootCAs: c.config.CACertPool,
153+
},
154+
}
155+
156+
// Execute and retry the request. This client comes with exponential backoff and
157+
// jitter already rolled in.
158+
resp, err := client.Do(retryableReq)
159+
if err != nil {
160+
return err
161+
}
162+
defer func() {
163+
if err := resp.Body.Close(); err != nil {
164+
if c.logger.IsWarn() {
165+
// Failing to close response bodies can present as a memory leak so it's
166+
// important to surface it.
167+
c.logger.Warn(fmt.Sprintf("unable to close response body: %s", err))
168+
}
169+
}
170+
}()
171+
172+
// If we're not supposed to read out the body, we have nothing further
173+
// to do here.
174+
if ptrToReturnObj == nil {
175+
return nil
176+
}
177+
178+
// Attempt to read out the body into the given return object.
179+
return json.NewDecoder(resp.Body).Decode(ptrToReturnObj)
180+
}
181+
182+
func (c *Client) getCheckRetry(req *http.Request) retryablehttp.CheckRetry {
183+
return func(ctx context.Context, resp *http.Response, err error) (bool, error) {
184+
if resp == nil {
185+
return true, fmt.Errorf("nil response: %s", req.URL.RequestURI())
186+
}
187+
switch resp.StatusCode {
188+
case 200, 201, 202, 204:
189+
// Success.
190+
return false, nil
191+
case 401, 403:
192+
// Perhaps the token from our bearer token file has been refreshed.
193+
config, err := inClusterConfig()
194+
if err != nil {
195+
return false, err
196+
}
197+
if config.BearerToken == c.config.BearerToken {
198+
// It's the same token.
199+
return false, fmt.Errorf("bad status code: %s", sanitizedDebuggingInfo(req, resp.StatusCode))
200+
}
201+
c.config = config
202+
// Continue to try again, but return the error too in case the caller would rather read it out.
203+
return true, fmt.Errorf("bad status code: %s", sanitizedDebuggingInfo(req, resp.StatusCode))
204+
case 404:
205+
return false, &ErrNotFound{debuggingInfo: sanitizedDebuggingInfo(req, resp.StatusCode)}
206+
case 500, 502, 503, 504:
207+
// Could be transient.
208+
return true, fmt.Errorf("unexpected status code: %s", sanitizedDebuggingInfo(req, resp.StatusCode))
209+
}
210+
// Unexpected.
211+
return false, fmt.Errorf("unexpected status code: %s", sanitizedDebuggingInfo(req, resp.StatusCode))
212+
}
213+
}
214+
215+
type Pod struct {
216+
Metadata *Metadata `json:"metadata,omitempty"`
217+
}
218+
219+
type Metadata struct {
220+
Name string `json:"name,omitempty"`
221+
222+
// This map will be nil if no "labels" key was provided.
223+
// It will be populated but have a length of zero if the
224+
// key was provided, but no values.
225+
Labels map[string]string `json:"labels,omitempty"`
226+
}
227+
228+
type PatchOperation string
229+
230+
const (
231+
Unset PatchOperation = "unset"
232+
Add = "add"
233+
Replace = "replace"
234+
)
235+
236+
type Patch struct {
237+
Operation PatchOperation
238+
Path string
239+
Value interface{}
240+
}
241+
242+
type ErrNotFound struct {
243+
debuggingInfo string
244+
}
245+
246+
func (e *ErrNotFound) Error() string {
247+
return e.debuggingInfo
248+
}
249+
250+
// sanitizedDebuggingInfo provides a returnable string that can be used for debugging. This is intentionally somewhat vague
251+
// because we don't want to leak secrets that may be in a request or response body.
252+
func sanitizedDebuggingInfo(req *http.Request, respStatus int) string {
253+
return fmt.Sprintf("req method: %s, req url: %s, resp statuscode: %d", req.Method, req.URL, respStatus)
254+
}

0 commit comments

Comments
 (0)