Refactor and provide JSON file loaders for endpoints and tokens

This commit is contained in:
Ilya Dmitrichenko 2016-08-25 10:59:24 +01:00 committed by Devan Goodwin
parent d17a236af3
commit e3278d965a
3 changed files with 115 additions and 81 deletions

View File

@ -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.")

View File

@ -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
}

View File

@ -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"`
}