diff --git a/cmd/kubediscovery/kubediscovery.go b/cmd/kubediscovery/kubediscovery.go index fb74fde3344..3194edb05bf 100644 --- a/cmd/kubediscovery/kubediscovery.go +++ b/cmd/kubediscovery/kubediscovery.go @@ -21,19 +21,23 @@ import ( ) func main() { - - // Make sure the CA cert for the cluster exists and is readable. - // We are expecting a base64 encoded version of the cert PEM as this is how - // the cert would most likely be provided via kubernetes secrets. - if _, err := os.Stat(kd.CAPath); os.IsNotExist(err) { - log.Fatalf("CA does not exist: %s", kd.CAPath) + // Make sure we can load critical files, and be nice to the user by + // printing descriptive error message when we fail. + for desc, path := range map[string]string{ + "root CA certificate": kd.CAPath, + "token map file": kd.TokenMapPath, + "list of API endpoints": kd.EndpointListPath, + } { + if _, err := os.Stat(path); os.IsNotExist(err) { + log.Fatalf("%s does not exist: %s", desc, path) + } + // Test read permissions + file, err := os.Open(path) + if err != nil { + log.Fatalf("Unable to open %s (%q [%s])", desc, path, err) + } + file.Close() } - // Test read permissions - file, err := os.Open(kd.CAPath) - if err != nil { - log.Fatalf("ERROR: Unable to read %s", kd.CAPath) - } - file.Close() router := kd.NewRouter() log.Printf("Listening for requests on port 9898.") diff --git a/pkg/kubediscovery/handlers.go b/pkg/kubediscovery/handlers.go index 15ad86386d8..d4d7305db50 100644 --- a/pkg/kubediscovery/handlers.go +++ b/pkg/kubediscovery/handlers.go @@ -14,45 +14,23 @@ package kubediscovery import ( "encoding/base64" - "encoding/hex" "encoding/json" "errors" "fmt" "io/ioutil" "log" "net/http" - "os" "github.com/square/go-jose" ) -// TODO: Just using a hardcoded token for now. -const tempTokenId string = "TOKENID" -const tempToken string = "EF1BA4F26DDA9FE2" +const secretPath = "/tmp/secret" // CAPath is the expected location of our cluster's CA to be distributed to // clients looking to connect. Because we expect to use kubernetes secrets // for the time being, this file is expected to be a base64 encoded version // of the normal cert PEM. -const CAPath = "/tmp/secret/ca.pem" - -// tokenLoader is an interface for abstracting how we validate -// token IDs and lookup their corresponding token. -type tokenLoader interface { - // Lookup returns the token for a given token ID, or an error if the token ID - // does not exist. Both token and it's ID are expected to be hex encoded strings. - Lookup(tokenId string) (string, error) -} - -type hardcodedTokenLoader struct { -} - -func (tl *hardcodedTokenLoader) Lookup(tokenId string) (string, error) { - if tokenId == tempTokenId { - return tempToken, nil - } - return "", errors.New(fmt.Sprintf("invalid token: %s", tokenId)) -} +const CAPath = secretPath + "/ca.pem" // caLoader is an interface for abstracting how we load the CA certificates // for the cluster. @@ -63,42 +41,94 @@ type caLoader interface { // fsCALoader is a caLoader for loading the PEM encoded CA from // /tmp/secret/ca.pem. type fsCALoader struct { + certData string } func (cl *fsCALoader) LoadPEM() (string, error) { - file, err := os.Open(CAPath) - if err != nil { - return "", err + if cl.certData != "" { + data, err := ioutil.ReadFile(CAPath) + if err != nil { + return "", err + } + + cl.certData = base64.StdEncoding.EncodeToString(data) } - data, err := ioutil.ReadAll(file) - if err != nil { - return "", err - } + return cl.certData, nil +} - return string(data), nil +const TokenMapPath = secretPath + "/token-map.json" +const EndpointListPath = secretPath + "/endpoint-list.json" + +// tokenLoader is an interface for abstracting how we validate +// token IDs and lookup their corresponding token. +type tokenLoader interface { + // Lookup returns the token for a given token ID, or an error if the token ID + // does not exist. Both token and it's ID are expected be strings. + LoadAndLookup(tokenID string) (string, error) +} + +type jsonFileTokenLoader struct { + tokenMap map[string]string +} + +func (tl *jsonFileTokenLoader) LoadAndLookup(tokenID string) (string, error) { + if len(tl.tokenMap) == 0 { + data, err := ioutil.ReadFile(TokenMapPath) + if err != nil { + return "", err + } + if err := json.Unmarshal(data, &tl.tokenMap); err != nil { + return "", err + } + } + if val, ok := tl.tokenMap[tokenID]; ok { + return val, nil + } + return "", errors.New(fmt.Sprintf("invalid token: %s", tokenID)) +} + +type endpointsLoader interface { + LoadList() ([]string, error) +} + +type jsonFileEnpointsLoader struct { + endpoints []string +} + +func (el *jsonFileEnpointsLoader) LoadList() ([]string, error) { + if len(el.endpoints) == 0 { + data, err := ioutil.ReadFile(EndpointListPath) + if err != nil { + return nil, err + } + if err := json.Unmarshal(data, &el.endpoints); err != nil { + return nil, err + } + } + return el.endpoints, nil } // ClusterInfoHandler implements the http.ServeHTTP method and allows us to // mock out portions of the request handler in tests. type ClusterInfoHandler struct { - tokenLoader tokenLoader - caLoader caLoader + tokenLoader tokenLoader + caLoader caLoader + endpointsLoader endpointsLoader } func NewClusterInfoHandler() *ClusterInfoHandler { - tl := hardcodedTokenLoader{} - cl := fsCALoader{} return &ClusterInfoHandler{ - tokenLoader: &tl, - caLoader: &cl, + tokenLoader: &jsonFileTokenLoader{}, + caLoader: &fsCALoader{}, + endpointsLoader: &jsonFileEnpointsLoader{}, } } func (cih *ClusterInfoHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { - tokenId := req.FormValue("token-id") - log.Printf("Got token ID: %s", tokenId) - token, err := cih.tokenLoader.Lookup(tokenId) + tokenID := req.FormValue("token-id") + log.Printf("Got token ID: %s", tokenID) + token, err := cih.tokenLoader.LoadAndLookup(tokenID) if err != nil { log.Printf("Invalid token: %s", err) http.Error(resp, "Forbidden", http.StatusForbidden) @@ -106,32 +136,42 @@ func (cih *ClusterInfoHandler) ServeHTTP(resp http.ResponseWriter, req *http.Req } log.Printf("Loaded token: %s", token) + // TODO probably should not leak server-side errors to the client caPEM, err := cih.caLoader.LoadPEM() - caB64 := base64.StdEncoding.EncodeToString([]byte(caPEM)) - if err != nil { - http.Error(resp, "Error encoding CA", http.StatusInternalServerError) + err = fmt.Errorf("Error loading root CA certificate data: %s", err) + log.Println(err) + http.Error(resp, err.Error(), http.StatusInternalServerError) + return + } + + endpoints, err := cih.endpointsLoader.LoadList() + if err != nil { + err = fmt.Errorf("Error loading list of API endpoints: %s", err) + log.Println(err) + http.Error(resp, err.Error(), http.StatusInternalServerError) return } clusterInfo := ClusterInfo{ - Type: "ClusterInfo", - Version: "v1", - RootCertificates: caB64, + CertificateAuthorities: []string{caPEM}, + Endpoints: endpoints, } // Instantiate an signer using HMAC-SHA256. - hmacTestKey := fromHexBytes(token) - signer, err := jose.NewSigner(jose.HS256, hmacTestKey) + signer, err := jose.NewSigner(jose.HS256, []byte(token)) if err != nil { - http.Error(resp, fmt.Sprintf("Error creating JWS signer: %s", err), http.StatusInternalServerError) + err = fmt.Errorf("Error creating JWS signer: %s", err) + log.Println(err) + http.Error(resp, err.Error(), http.StatusInternalServerError) return } payload, err := json.Marshal(clusterInfo) if err != nil { - http.Error(resp, fmt.Sprintf("Error serializing clusterInfo to JSON: %s", err), - http.StatusInternalServerError) + err = fmt.Errorf("Error serializing clusterInfo to JSON: %s", err) + log.Println(err) + http.Error(resp, err.Error(), http.StatusInternalServerError) return } @@ -140,8 +180,9 @@ func (cih *ClusterInfoHandler) ServeHTTP(resp http.ResponseWriter, req *http.Req // indicate a problem in an underlying cryptographic primitive. jws, err := signer.Sign(payload) if err != nil { - http.Error(resp, fmt.Sprintf("Error signing clusterInfo to JSON: %s", err), - http.StatusInternalServerError) + err = fmt.Errorf("Error signing clusterInfo with JWS: %s", err) + log.Println(err) + http.Error(resp, err.Error(), http.StatusInternalServerError) return } @@ -153,13 +194,3 @@ func (cih *ClusterInfoHandler) ServeHTTP(resp http.ResponseWriter, req *http.Req resp.Write([]byte(serialized)) } - -// TODO: Move into test package -// TODO: Should we use base64 instead? -func fromHexBytes(base16 string) []byte { - val, err := hex.DecodeString(base16) - if err != nil { - panic(fmt.Sprintf("Invalid test data: %s", err)) - } - return val -} diff --git a/pkg/kubediscovery/model.go b/pkg/kubediscovery/model.go index 41234ec91f7..d0aaedffba3 100644 --- a/pkg/kubediscovery/model.go +++ b/pkg/kubediscovery/model.go @@ -12,10 +12,9 @@ limitations under the License. */ package kubediscovery -// TODO: Sync with kubeadm api type type ClusterInfo struct { - Type string - Version string - RootCertificates string `json:"rootCertificates"` - // TODO: ClusterID, Endpoints + // TODO Kind, apiVersion + // TODO clusterId, fetchedTime, expiredTime + CertificateAuthorities []string `json:"certificateAuthorities,omitempty"` + Endpoints []string `json:"endpoints,omitempty"` }