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() { func main() {
// Make sure we can load critical files, and be nice to the user by
// Make sure the CA cert for the cluster exists and is readable. // printing descriptive error message when we fail.
// We are expecting a base64 encoded version of the cert PEM as this is how for desc, path := range map[string]string{
// the cert would most likely be provided via kubernetes secrets. "root CA certificate": kd.CAPath,
if _, err := os.Stat(kd.CAPath); os.IsNotExist(err) { "token map file": kd.TokenMapPath,
log.Fatalf("CA does not exist: %s", kd.CAPath) "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() router := kd.NewRouter()
log.Printf("Listening for requests on port 9898.") log.Printf("Listening for requests on port 9898.")

View File

@ -14,45 +14,23 @@ package kubediscovery
import ( import (
"encoding/base64" "encoding/base64"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"os"
"github.com/square/go-jose" "github.com/square/go-jose"
) )
// TODO: Just using a hardcoded token for now. const secretPath = "/tmp/secret"
const tempTokenId string = "TOKENID"
const tempToken string = "EF1BA4F26DDA9FE2"
// CAPath is the expected location of our cluster's CA to be distributed to // 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 // 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 // for the time being, this file is expected to be a base64 encoded version
// of the normal cert PEM. // of the normal cert PEM.
const CAPath = "/tmp/secret/ca.pem" const CAPath = secretPath + "/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))
}
// caLoader is an interface for abstracting how we load the CA certificates // caLoader is an interface for abstracting how we load the CA certificates
// for the cluster. // for the cluster.
@ -63,42 +41,94 @@ type caLoader interface {
// fsCALoader is a caLoader for loading the PEM encoded CA from // fsCALoader is a caLoader for loading the PEM encoded CA from
// /tmp/secret/ca.pem. // /tmp/secret/ca.pem.
type fsCALoader struct { type fsCALoader struct {
certData string
} }
func (cl *fsCALoader) LoadPEM() (string, error) { func (cl *fsCALoader) LoadPEM() (string, error) {
file, err := os.Open(CAPath) if cl.certData != "" {
if err != nil { data, err := ioutil.ReadFile(CAPath)
return "", err if err != nil {
return "", err
}
cl.certData = base64.StdEncoding.EncodeToString(data)
} }
data, err := ioutil.ReadAll(file) return cl.certData, nil
if err != nil { }
return "", err
}
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 // ClusterInfoHandler implements the http.ServeHTTP method and allows us to
// mock out portions of the request handler in tests. // mock out portions of the request handler in tests.
type ClusterInfoHandler struct { type ClusterInfoHandler struct {
tokenLoader tokenLoader tokenLoader tokenLoader
caLoader caLoader caLoader caLoader
endpointsLoader endpointsLoader
} }
func NewClusterInfoHandler() *ClusterInfoHandler { func NewClusterInfoHandler() *ClusterInfoHandler {
tl := hardcodedTokenLoader{}
cl := fsCALoader{}
return &ClusterInfoHandler{ return &ClusterInfoHandler{
tokenLoader: &tl, tokenLoader: &jsonFileTokenLoader{},
caLoader: &cl, caLoader: &fsCALoader{},
endpointsLoader: &jsonFileEnpointsLoader{},
} }
} }
func (cih *ClusterInfoHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { func (cih *ClusterInfoHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
tokenId := req.FormValue("token-id") tokenID := req.FormValue("token-id")
log.Printf("Got token ID: %s", tokenId) log.Printf("Got token ID: %s", tokenID)
token, err := cih.tokenLoader.Lookup(tokenId) token, err := cih.tokenLoader.LoadAndLookup(tokenID)
if err != nil { if err != nil {
log.Printf("Invalid token: %s", err) log.Printf("Invalid token: %s", err)
http.Error(resp, "Forbidden", http.StatusForbidden) 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) log.Printf("Loaded token: %s", token)
// TODO probably should not leak server-side errors to the client
caPEM, err := cih.caLoader.LoadPEM() caPEM, err := cih.caLoader.LoadPEM()
caB64 := base64.StdEncoding.EncodeToString([]byte(caPEM))
if err != nil { 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 return
} }
clusterInfo := ClusterInfo{ clusterInfo := ClusterInfo{
Type: "ClusterInfo", CertificateAuthorities: []string{caPEM},
Version: "v1", Endpoints: endpoints,
RootCertificates: caB64,
} }
// Instantiate an signer using HMAC-SHA256. // Instantiate an signer using HMAC-SHA256.
hmacTestKey := fromHexBytes(token) signer, err := jose.NewSigner(jose.HS256, []byte(token))
signer, err := jose.NewSigner(jose.HS256, hmacTestKey)
if err != nil { 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 return
} }
payload, err := json.Marshal(clusterInfo) payload, err := json.Marshal(clusterInfo)
if err != nil { if err != nil {
http.Error(resp, fmt.Sprintf("Error serializing clusterInfo to JSON: %s", err), err = fmt.Errorf("Error serializing clusterInfo to JSON: %s", err)
http.StatusInternalServerError) log.Println(err)
http.Error(resp, err.Error(), http.StatusInternalServerError)
return return
} }
@ -140,8 +180,9 @@ func (cih *ClusterInfoHandler) ServeHTTP(resp http.ResponseWriter, req *http.Req
// indicate a problem in an underlying cryptographic primitive. // indicate a problem in an underlying cryptographic primitive.
jws, err := signer.Sign(payload) jws, err := signer.Sign(payload)
if err != nil { if err != nil {
http.Error(resp, fmt.Sprintf("Error signing clusterInfo to JSON: %s", err), err = fmt.Errorf("Error signing clusterInfo with JWS: %s", err)
http.StatusInternalServerError) log.Println(err)
http.Error(resp, err.Error(), http.StatusInternalServerError)
return return
} }
@ -153,13 +194,3 @@ func (cih *ClusterInfoHandler) ServeHTTP(resp http.ResponseWriter, req *http.Req
resp.Write([]byte(serialized)) 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 package kubediscovery
// TODO: Sync with kubeadm api type
type ClusterInfo struct { type ClusterInfo struct {
Type string // TODO Kind, apiVersion
Version string // TODO clusterId, fetchedTime, expiredTime
RootCertificates string `json:"rootCertificates"` CertificateAuthorities []string `json:"certificateAuthorities,omitempty"`
// TODO: ClusterID, Endpoints Endpoints []string `json:"endpoints,omitempty"`
} }