From d17a236af3314572a092f430520fa35885a2a963 Mon Sep 17 00:00:00 2001 From: Devan Goodwin Date: Tue, 23 Aug 2016 15:29:40 -0300 Subject: [PATCH 1/3] Add standalone kube-discovery JWS discovery API. This is a standalone pod which will be configured by kubeadm for the time being. A token ID/token map, endpoints list, and CA cert are provided as secrets. Callers request the cluster info by shared secret (token ID), and if the token ID matches a JWS signed payload is returned using the other half of the shared secret to validate. --- cmd/kubediscovery/kubediscovery.go | 41 +++++++ discovery/Dockerfile | 6 ++ discovery/README.md | 39 +++++++ discovery/ca-secret.yaml | 7 ++ discovery/kubediscovery.yaml | 37 +++++++ pkg/kubediscovery/handlers.go | 165 +++++++++++++++++++++++++++++ pkg/kubediscovery/handlers_test.go | 100 +++++++++++++++++ pkg/kubediscovery/model.go | 21 ++++ pkg/kubediscovery/routes.go | 51 +++++++++ 9 files changed, 467 insertions(+) create mode 100644 cmd/kubediscovery/kubediscovery.go create mode 100644 discovery/Dockerfile create mode 100644 discovery/README.md create mode 100644 discovery/ca-secret.yaml create mode 100644 discovery/kubediscovery.yaml create mode 100644 pkg/kubediscovery/handlers.go create mode 100644 pkg/kubediscovery/handlers_test.go create mode 100644 pkg/kubediscovery/model.go create mode 100644 pkg/kubediscovery/routes.go diff --git a/cmd/kubediscovery/kubediscovery.go b/cmd/kubediscovery/kubediscovery.go new file mode 100644 index 00000000000..fb74fde3344 --- /dev/null +++ b/cmd/kubediscovery/kubediscovery.go @@ -0,0 +1,41 @@ +/* +Copyright 2016 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package main + +import ( + "log" + "net/http" + "os" + + kd "k8s.io/kubernetes/pkg/kubediscovery" +) + +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) + } + // 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.") + log.Fatal(http.ListenAndServe(":9898", router)) +} diff --git a/discovery/Dockerfile b/discovery/Dockerfile new file mode 100644 index 00000000000..2e500ec31b2 --- /dev/null +++ b/discovery/Dockerfile @@ -0,0 +1,6 @@ +FROM golang + +ADD kubediscovery /usr/bin/ +ENTRYPOINT /usr/bin/kubernetes-discovery + +EXPOSE 8080 diff --git a/discovery/README.md b/discovery/README.md new file mode 100644 index 00000000000..3ade93b4a18 --- /dev/null +++ b/discovery/README.md @@ -0,0 +1,39 @@ +# kubernetes-discovery +An initial implementation of a Kubernetes discovery service using JSON Web Signatures. + +This prototype is expected to be run by Kubernetes itself for the time being, +and will hopefully be merged into the core API at a later time. + +## Requirements + +Generate a CA cert save it to: /tmp/secret/ca.pem to run the service or unit tests. (will not be required for unit tests for long) Similarly when run within kubernetes we expect a secret to be provided at this location as well. (see below) + +## Build And Run From Source + +``` +$ make WHAT=cmd/kubediscovery +$ _output/local/bin/linux/amd64/kubediscovery +2016/08/23 19:17:28 Listening for requests on port 9898. + +``` + +## Running in Docker + +This image is published temporarily on Docker Hub as dgoodwin/kubediscovery + +`docker run --rm -p 9898:9898 -v /tmp/secret/ca.pem:/tmp/secret/ca.pem --name kubediscovery dgoodwin/kubediscovery` + +## Running in Kubernetes + +A dummy certificate is included in ca-secret.yaml. + +``` +kubectl create -f ca-secret.yaml +kubectl create -f kubediscovery.yaml +``` + +## Testing the API + +`curl "http://localhost:9898/cluster-info/v1/?token-id=TOKENID"` + +You should see JSON containing a signed payload. For code to verify and decode that payload see handler_test.go. diff --git a/discovery/ca-secret.yaml b/discovery/ca-secret.yaml new file mode 100644 index 00000000000..f0811c342fa --- /dev/null +++ b/discovery/ca-secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: ca-secret +type: Opaque +data: + ca.pem: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR4RENDQXF5Z0F3SUJBZ0lVV3pqUDl5RUk0eHlRSnBzVHVERU4yV2ROaUFzd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2FERUxNQWtHQTFVRUJoTUNWVk14RHpBTkJnTlZCQWdUQms5eVpXZHZiakVSTUE4R0ExVUVCeE1JVUc5eQpkR3hoYm1ReEV6QVJCZ05WQkFvVENrdDFZbVZ5Ym1WMFpYTXhDekFKQmdOVkJBc1RBa05CTVJNd0VRWURWUVFECkV3cExkV0psY201bGRHVnpNQjRYRFRFMk1EZ3hNVEUyTkRnd01Gb1hEVEl4TURneE1ERTJORGd3TUZvd2FERUwKTUFrR0ExVUVCaE1DVlZNeER6QU5CZ05WQkFnVEJrOXlaV2R2YmpFUk1BOEdBMVVFQnhNSVVHOXlkR3hoYm1ReApFekFSQmdOVkJBb1RDa3QxWW1WeWJtVjBaWE14Q3pBSkJnTlZCQXNUQWtOQk1STXdFUVlEVlFRREV3cExkV0psCmNtNWxkR1Z6TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF3QkhNOGN6anc0Q1cKK05wbklhV012RzZlcVhtelNZT20vbHdaNUhOMnVLck9xaTNHYUUyTjFKd2tzcGRmMXNOUGFZMHdPR2xkbURIZgoxSnlyTW8rUFdLVUVjWko1WGE4Vm02d2I0MlpjczN3MEp5dlEzWFJjaDQyMFJRWGRKayszcmMybWRvSVRkL0lmCnZjWms0N0RzQTMrQU5QSUlSTzdWRmZpS1JNRFpTUDR1OThnVjI2eW1zbjc0TzFVKzNVUHR1TEFTVTFLck9FTk4KR01FWG0ydTJpdmVvbTJrbjFlZTZuM1hCR1o2bU52cUNPdWUxRXdza0gvWkhoUVh1UDgyV1U5dVk0aGVORnoyQwpBNmR0Q0Q0c3Z6eHc3ZFQ2cVhsV0ZIWUYrc3VLVDhXNkczd3NkOWxzV0ZVY0ZWL0lwaTVobEVaTWprNFNoY3RqCjdpYnlrRURKM1FJREFRQUJvMll3WkRBT0JnTlZIUThCQWY4RUJBTUNBUVl3RWdZRFZSMFRBUUgvQkFnd0JnRUIKL3dJQkFqQWRCZ05WSFE0RUZnUVVOdnhRZ3o5ZTNXS2VscU1KTmZXNE1KUHYzc0V3SHdZRFZSMGpCQmd3Rm9BVQpOdnhRZ3o5ZTNXS2VscU1KTmZXNE1KUHYzc0V3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUp1TUhYUms1TEVyCmxET1p4Mm9aRUNQZ29reXMzSGJsM05oempXd2pncXdxNVN6a011V3QrUnVkdnRTK0FUQjFtTjRjYTN0eSt2bWcKT09heTkvaDZoditmSE5jZHpYdWR5dFZYZW1KN3F4ZFoxd25DUUcwdnpqOWRZY0xFSGpJWi94dU1jNlY3dnJ4YwpSc0preGp5aE01UXBmRHd0eVZKeGpkUmVBZ0huSyswTkNieHdtQ3cyRGIvOXpudm9LWGk4TEQwbkQzOFQxY3R3CmhmdGxwTmRoZXFNRlpEZXBuTUYwY2g2cHo5TFV5Mkh1cnhrV2dkWVNjY2VNU0hPTzBMcG4xeVVBMWczOTJhUjUKWk81Zm5KMW95Vm1LVWFCeDJCMndsSVlUSXlES1ZiMnY1UXNHbnYvRHVTMDZhcmVLTmsvTGpHRTRlMXlHOHJkcwpacnZHMzNvUmtEbz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= diff --git a/discovery/kubediscovery.yaml b/discovery/kubediscovery.yaml new file mode 100644 index 00000000000..feaa39de540 --- /dev/null +++ b/discovery/kubediscovery.yaml @@ -0,0 +1,37 @@ +apiVersion: extensions/v1beta1 +kind: ReplicaSet +metadata: + name: kubediscovery + # these labels can be applied automatically + # from the labels in the pod template if not set + # labels: + # app: guestbook + # tier: frontend +spec: + # this replicas value is default + # modify it according to your case + replicas: 1 + # selector can be applied automatically + # from the labels in the pod template if not set, + # but we are specifying the selector here to + # demonstrate its usage. + template: + metadata: + labels: + app: kubediscovery + spec: + hostNetwork: true + containers: + - name: kubediscovery + image: dgoodwin/kubediscovery + imagePullPolicy: IfNotPresent + ports: + - containerPort: 9898 + volumeMounts: + - name: ca-secret-vol + mountPath: /tmp/secret + readOnly: true + volumes: + - name: ca-secret-vol + secret: + secretName: ca-secret diff --git a/pkg/kubediscovery/handlers.go b/pkg/kubediscovery/handlers.go new file mode 100644 index 00000000000..15ad86386d8 --- /dev/null +++ b/pkg/kubediscovery/handlers.go @@ -0,0 +1,165 @@ +/* +Copyright 2016 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +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" + +// 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)) +} + +// caLoader is an interface for abstracting how we load the CA certificates +// for the cluster. +type caLoader interface { + LoadPEM() (string, error) +} + +// fsCALoader is a caLoader for loading the PEM encoded CA from +// /tmp/secret/ca.pem. +type fsCALoader struct { +} + +func (cl *fsCALoader) LoadPEM() (string, error) { + file, err := os.Open(CAPath) + if err != nil { + return "", err + } + + data, err := ioutil.ReadAll(file) + if err != nil { + return "", err + } + + return string(data), 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 +} + +func NewClusterInfoHandler() *ClusterInfoHandler { + tl := hardcodedTokenLoader{} + cl := fsCALoader{} + return &ClusterInfoHandler{ + tokenLoader: &tl, + caLoader: &cl, + } +} + +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) + if err != nil { + log.Printf("Invalid token: %s", err) + http.Error(resp, "Forbidden", http.StatusForbidden) + return + } + log.Printf("Loaded token: %s", token) + + caPEM, err := cih.caLoader.LoadPEM() + caB64 := base64.StdEncoding.EncodeToString([]byte(caPEM)) + + if err != nil { + http.Error(resp, "Error encoding CA", http.StatusInternalServerError) + return + } + + clusterInfo := ClusterInfo{ + Type: "ClusterInfo", + Version: "v1", + RootCertificates: caB64, + } + + // Instantiate an signer using HMAC-SHA256. + hmacTestKey := fromHexBytes(token) + signer, err := jose.NewSigner(jose.HS256, hmacTestKey) + if err != nil { + http.Error(resp, fmt.Sprintf("Error creating JWS signer: %s", err), 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) + return + } + + // Sign a sample payload. Calling the signer returns a protected JWS object, + // which can then be serialized for output afterwards. An error would + // 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) + return + } + + // Serialize the encrypted object using the full serialization format. + // Alternatively you can also use the compact format here by calling + // object.CompactSerialize() instead. + serialized := jws.FullSerialize() + + 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/handlers_test.go b/pkg/kubediscovery/handlers_test.go new file mode 100644 index 00000000000..2374fef911b --- /dev/null +++ b/pkg/kubediscovery/handlers_test.go @@ -0,0 +1,100 @@ +/* +Copyright 2016 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package kubediscovery + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/square/go-jose" +) + +func TestClusterInfoIndex(t *testing.T) { + tests := map[string]struct { + url string + expStatus int + }{ + "no token": { + "/cluster-info/v1/", + http.StatusForbidden, + }, + "valid token": { + fmt.Sprintf("/cluster-info/v1/?token-id=%s", tempTokenId), + http.StatusOK, + }, + "invalid token": { + "/cluster-info/v1/?token-id=JUNK", + http.StatusForbidden, + }, + } + + for name, test := range tests { + t.Logf("Running test: %s", name) + // Create a request to pass to our handler. We don't have any query parameters for now, so we'll + // pass 'nil' as the third parameter. + req, err := http.NewRequest("GET", test.url, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + // TODO: mock/stub here + handler := NewClusterInfoHandler() + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != test.expStatus { + t.Errorf("handler returned wrong status code: got %v want %v", + status, test.expStatus) + continue + } + + // If we were expecting valid status validate the body: + if test.expStatus == http.StatusOK { + var ci ClusterInfo + + body := string(rr.Body.Bytes()) + + // Parse the JSON web signature: + jws, err := jose.ParseSigned(body) + if err != nil { + t.Errorf("Error parsing JWS from request body: %s", err) + continue + } + + // Now we can verify the signature on the payload. An error here would + // indicate the the message failed to verify, e.g. because the signature was + // broken or the message was tampered with. + var clusterInfoBytes []byte + hmacTestKey := fromHexBytes(tempToken) + clusterInfoBytes, err = jws.Verify(hmacTestKey) + if err != nil { + t.Errorf("Error verifing signature: %s", err) + continue + } + + err = json.Unmarshal(clusterInfoBytes, &ci) + if err != nil { + t.Errorf("Unable to unmarshall payload to JSON: error=%s body=%s", err, rr.Body.String()) + continue + } + if ci.RootCertificates == "" { + t.Error("No root certificates in response") + continue + } + } + } +} diff --git a/pkg/kubediscovery/model.go b/pkg/kubediscovery/model.go new file mode 100644 index 00000000000..41234ec91f7 --- /dev/null +++ b/pkg/kubediscovery/model.go @@ -0,0 +1,21 @@ +/* +Copyright 2016 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +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 +} diff --git a/pkg/kubediscovery/routes.go b/pkg/kubediscovery/routes.go new file mode 100644 index 00000000000..c0df303b3db --- /dev/null +++ b/pkg/kubediscovery/routes.go @@ -0,0 +1,51 @@ +/* +Copyright 2016 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package kubediscovery + +import ( + "net/http" + + "github.com/gorilla/mux" +) + +type Route struct { + Name string + Method string + Pattern string + Handler http.Handler +} + +type Routes []Route + +var routes = Routes{ + Route{ + "ClusterInfoIndex", + "GET", + "/cluster-info/v1/", + NewClusterInfoHandler(), + }, +} + +func NewRouter() *mux.Router { + + router := mux.NewRouter().StrictSlash(true) + for _, route := range routes { + router. + Methods(route.Method). + Path(route.Pattern). + Name(route.Name). + Handler(route.Handler) + } + + return router +} From e3278d965ad88ee2a1ea2d5aae11b17f39788879 Mon Sep 17 00:00:00 2001 From: Ilya Dmitrichenko Date: Thu, 25 Aug 2016 10:59:24 +0100 Subject: [PATCH 2/3] Refactor and provide JSON file loaders for endpoints and tokens --- cmd/kubediscovery/kubediscovery.go | 28 ++--- pkg/kubediscovery/handlers.go | 159 +++++++++++++++++------------ pkg/kubediscovery/model.go | 9 +- 3 files changed, 115 insertions(+), 81 deletions(-) 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"` } From baebd7cfd962ae7dcf9ddf08f3964c4ce707ab81 Mon Sep 17 00:00:00 2001 From: Devan Goodwin Date: Thu, 25 Aug 2016 10:54:42 -0300 Subject: [PATCH 3/3] Expand on kube-discovery API and integrate container build. --- cluster/images/kube-discovery/Dockerfile | 18 ++ cluster/images/kube-discovery/Makefile | 53 +++++ cluster/images/kube-discovery/README.md | 45 ++++ .../kube-discovery/app}/handlers.go | 27 ++- cmd/kube-discovery/app/handlers_test.go | 208 ++++++++++++++++++ .../kube-discovery/app}/model.go | 8 +- .../kube-discovery/app}/routes.go | 8 +- cmd/kube-discovery/dummy.go | 24 -- .../kubediscovery.go | 8 +- discovery/Dockerfile | 6 - discovery/README.md | 39 ---- discovery/ca-secret.yaml | 7 - discovery/kubediscovery.yaml | 37 ---- pkg/kubediscovery/handlers_test.go | 100 --------- 14 files changed, 359 insertions(+), 229 deletions(-) create mode 100644 cluster/images/kube-discovery/Dockerfile create mode 100644 cluster/images/kube-discovery/Makefile create mode 100644 cluster/images/kube-discovery/README.md rename {pkg/kubediscovery => cmd/kube-discovery/app}/handlers.go (92%) create mode 100644 cmd/kube-discovery/app/handlers_test.go rename {pkg/kubediscovery => cmd/kube-discovery/app}/model.go (92%) rename {pkg/kubediscovery => cmd/kube-discovery/app}/routes.go (94%) delete mode 100644 cmd/kube-discovery/dummy.go rename cmd/{kubediscovery => kube-discovery}/kubediscovery.go (93%) delete mode 100644 discovery/Dockerfile delete mode 100644 discovery/README.md delete mode 100644 discovery/ca-secret.yaml delete mode 100644 discovery/kubediscovery.yaml delete mode 100644 pkg/kubediscovery/handlers_test.go diff --git a/cluster/images/kube-discovery/Dockerfile b/cluster/images/kube-discovery/Dockerfile new file mode 100644 index 00000000000..edd456334c6 --- /dev/null +++ b/cluster/images/kube-discovery/Dockerfile @@ -0,0 +1,18 @@ +# Copyright 2016 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM BASEIMAGE + +COPY kube-discovery /usr/local/bin +ENTRYPOINT "/usr/local/bin/kube-discovery" diff --git a/cluster/images/kube-discovery/Makefile b/cluster/images/kube-discovery/Makefile new file mode 100644 index 00000000000..d291f858886 --- /dev/null +++ b/cluster/images/kube-discovery/Makefile @@ -0,0 +1,53 @@ +# Copyright 2016 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Build the kube-discovery image. +# +# Requires a pre-built kube-discovery binary: +# build/run.sh /bin/bash -c "KUBE_BUILD_PLATFORMS=linux/ARCH make WHAT=cmd/kube-discovery" +# +# Usage: +# [ARCH=amd64] [REGISTRY="gcr.io/google_containers"] make (build|push) VERSION={some_released_version_of_kubernetes} + +REGISTRY?=gcr.io/google_containers +ARCH?=amd64 +TEMP_DIR:=$(shell mktemp -d) +VERSION?=1.0 + +ifeq ($(ARCH),amd64) + BASEIMAGE?=debian:jessie +endif +ifeq ($(ARCH),arm) + BASEIMAGE?=armel/debian:jessie +endif +ifeq ($(ARCH),arm64) + BASEIMAGE?=aarch64/debian:jessie +endif +ifeq ($(ARCH),ppc64le) + BASEIMAGE?=ppc64le/debian:jessie +endif + +all: build + +build: + cp -r ./* ${TEMP_DIR} + cp ../../../_output/dockerized/bin/linux/${ARCH}/kube-discovery ${TEMP_DIR} + cd ${TEMP_DIR} && sed -i.back "s|BASEIMAGE|${BASEIMAGE}|g" Dockerfile + docker build -t ${REGISTRY}/kube-discovery-${ARCH}:${VERSION} ${TEMP_DIR} + rm -rf "${TEMP_DIR}" + +push: build + gcloud docker push ${REGISTRY}/kube-discovery-${ARCH}:${VERSION} + +.PHONY: all diff --git a/cluster/images/kube-discovery/README.md b/cluster/images/kube-discovery/README.md new file mode 100644 index 00000000000..bc61db6fb7d --- /dev/null +++ b/cluster/images/kube-discovery/README.md @@ -0,0 +1,45 @@ +### kube-discovery + +An initial implementation of a Kubernetes discovery service using JSON Web Signatures. + +This prototype is configured by kubeadm and run within Kubernetes itself. + +## Requirements + +This pod expects the cluster CA, endpoints list, and token map to exist in /tmp/secret. This allows us to pass them in as kubernetes secrets when deployed as a pod. + +``` +$ cd /tmp/secret +$ ls +ca.pem endpoint-list.json token-map.json +$ cat endpoint-list.json +["http://192.168.1.5:8080", "http://192.168.1.6:8080"] +$ cat token-map.json +{ + "TOKENID": "ABCDEF1234123456" +} +``` + +## Build And Run From Source + +``` +$ build/run.sh /bin/bash -c "KUBE_BUILD_PLATFORMS=linux/amd64 make WHAT=cmd/kube-discovery" +$ _output/dockerized/bin/linux/amd64/kube-discovery +2016/08/23 19:17:28 Listening for requests on port 9898. + +``` + +## Running in Docker + +This image is published at: gcr.io/google_containers/kube-discovery + +`docker run -d -p 9898:9898 -v /tmp/secret/ca.pem:/tmp/secret/ca.pem -v /tmp/secret/endpoint-list.json:/tmp/secret/endpoint-list.json -v /tmp/secret/token-map.json:/tmp/secret/token-map.json --name kubediscovery gcr.io/google_containers/kube-discovery` + +## Testing the API + +`curl "http://localhost:9898/cluster-info/v1/?token-id=TOKENID"` + +You should see JSON containing a signed payload. For code to verify and decode that payload see handler_test.go. + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/cluster/images/kube-discovery/README.md?pixel)]() diff --git a/pkg/kubediscovery/handlers.go b/cmd/kube-discovery/app/handlers.go similarity index 92% rename from pkg/kubediscovery/handlers.go rename to cmd/kube-discovery/app/handlers.go index d4d7305db50..2d974a44448 100644 --- a/pkg/kubediscovery/handlers.go +++ b/cmd/kube-discovery/app/handlers.go @@ -1,19 +1,22 @@ /* -Copyright 2016 The Kubernetes Authors. +Copyright 2014 The Kubernetes Authors. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -package kubediscovery + +package discovery import ( - "encoding/base64" "encoding/json" "errors" "fmt" @@ -45,13 +48,13 @@ type fsCALoader struct { } func (cl *fsCALoader) LoadPEM() (string, error) { - if cl.certData != "" { + if cl.certData == "" { data, err := ioutil.ReadFile(CAPath) if err != nil { return "", err } - cl.certData = base64.StdEncoding.EncodeToString(data) + cl.certData = string(data) } return cl.certData, nil @@ -92,11 +95,11 @@ type endpointsLoader interface { LoadList() ([]string, error) } -type jsonFileEnpointsLoader struct { +type jsonFileEndpointsLoader struct { endpoints []string } -func (el *jsonFileEnpointsLoader) LoadList() ([]string, error) { +func (el *jsonFileEndpointsLoader) LoadList() ([]string, error) { if len(el.endpoints) == 0 { data, err := ioutil.ReadFile(EndpointListPath) if err != nil { @@ -121,7 +124,7 @@ func NewClusterInfoHandler() *ClusterInfoHandler { return &ClusterInfoHandler{ tokenLoader: &jsonFileTokenLoader{}, caLoader: &fsCALoader{}, - endpointsLoader: &jsonFileEnpointsLoader{}, + endpointsLoader: &jsonFileEndpointsLoader{}, } } @@ -130,7 +133,7 @@ func (cih *ClusterInfoHandler) ServeHTTP(resp http.ResponseWriter, req *http.Req log.Printf("Got token ID: %s", tokenID) token, err := cih.tokenLoader.LoadAndLookup(tokenID) if err != nil { - log.Printf("Invalid token: %s", err) + log.Print(err) http.Error(resp, "Forbidden", http.StatusForbidden) return } @@ -138,6 +141,7 @@ func (cih *ClusterInfoHandler) ServeHTTP(resp http.ResponseWriter, req *http.Req // TODO probably should not leak server-side errors to the client caPEM, err := cih.caLoader.LoadPEM() + log.Printf("Loaded CA: %s", caPEM) if err != nil { err = fmt.Errorf("Error loading root CA certificate data: %s", err) log.Println(err) @@ -159,7 +163,10 @@ func (cih *ClusterInfoHandler) ServeHTTP(resp http.ResponseWriter, req *http.Req } // Instantiate an signer using HMAC-SHA256. - signer, err := jose.NewSigner(jose.HS256, []byte(token)) + hmacKey := []byte(token) + + log.Printf("Key is %d bytes long", len(hmacKey)) + signer, err := jose.NewSigner(jose.HS256, hmacKey) if err != nil { err = fmt.Errorf("Error creating JWS signer: %s", err) log.Println(err) diff --git a/cmd/kube-discovery/app/handlers_test.go b/cmd/kube-discovery/app/handlers_test.go new file mode 100644 index 00000000000..2aaaa597d90 --- /dev/null +++ b/cmd/kube-discovery/app/handlers_test.go @@ -0,0 +1,208 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package discovery + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/square/go-jose" +) + +type mockTokenLoader struct { + tokenID string + token string +} + +func (tl *mockTokenLoader) LoadAndLookup(tokenID string) (string, error) { + if tokenID == tl.tokenID { + return tl.token, nil + } + return "", errors.New(fmt.Sprintf("invalid token: %s", tokenID)) +} + +const mockEndpoint1 = "https://192.168.1.5:8080" +const mockEndpoint2 = "https://192.168.1.6:8080" + +type mockEndpointsLoader struct { +} + +func (el *mockEndpointsLoader) LoadList() ([]string, error) { + return []string{mockEndpoint1, mockEndpoint2}, nil +} + +const mockCA = "---BEGIN------END---DUMMYDATA" + +type mockCALoader struct { +} + +func (cl *mockCALoader) LoadPEM() (string, error) { + return mockCA, nil +} + +const mockTokenID = "AAAAAA" +const mockToken = "9537434E638E4378" + +const mockTokenIDCustom = "SHAREDSECRET" +const mockTokenCustom = "VERYSECRETTOKEN" + +func TestClusterInfoIndex(t *testing.T) { + longToken := strings.Repeat("a", 1000) + tests := map[string]struct { + tokenID string // token ID the mock loader will use + token string // token the mock loader will use + reqTokenID string // token ID the will request with + reqToken string // token the caller will validate response with + expStatus int + expVerifyFailure bool + }{ + "no token": { + tokenID: mockTokenID, + token: mockToken, + reqTokenID: "", + reqToken: "", + expStatus: http.StatusForbidden, + }, + "valid token ID": { + tokenID: mockTokenID, + token: mockToken, + reqTokenID: mockTokenID, + reqToken: mockToken, + expStatus: http.StatusOK, + }, + "valid arbitrary string token": { + tokenID: mockTokenIDCustom, + token: mockTokenCustom, + reqTokenID: mockTokenIDCustom, + reqToken: mockTokenCustom, + expStatus: http.StatusOK, + }, + "valid arbitrary long string token": { + tokenID: "LONGTOKENTEST", + token: longToken, + reqTokenID: "LONGTOKENTEST", + reqToken: longToken, + expStatus: http.StatusOK, + }, + "invalid token ID": { + tokenID: mockTokenID, + token: mockToken, + reqTokenID: "BADTOKENID", + reqToken: mockToken, + expStatus: http.StatusForbidden, + }, + "invalid token": { + tokenID: mockTokenID, + token: mockToken, + reqTokenID: mockTokenID, + reqToken: "badtoken", + expStatus: http.StatusOK, + expVerifyFailure: true, + }, + } + + for name, test := range tests { + t.Logf("Running test: %s", name) + tokenLoader := &mockTokenLoader{test.tokenID, test.token} + // Create a request to pass to our handler. We don't have any query parameters for now, so we'll + // pass 'nil' as the third parameter. + url := "/cluster-info/v1/" + if test.tokenID != "" { + url = fmt.Sprintf("%s?token-id=%s", url, test.reqTokenID) + } + req, err := http.NewRequest("GET", url, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := &ClusterInfoHandler{ + tokenLoader: tokenLoader, + caLoader: &mockCALoader{}, + endpointsLoader: &mockEndpointsLoader{}, + } + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != test.expStatus { + t.Errorf("handler returned wrong status code: got %v want %v", + status, test.expStatus) + continue + } + + // If we were expecting valid status validate the body: + if test.expStatus == http.StatusOK { + var ci ClusterInfo + + body := string(rr.Body.Bytes()) + + // Parse the JSON web signature: + jws, err := jose.ParseSigned(body) + if err != nil { + t.Errorf("Error parsing JWS from request body: %s", err) + continue + } + + // Now we can verify the signature on the payload. An error here would + // indicate the the message failed to verify, e.g. because the signature was + // broken or the message was tampered with. + var clusterInfoBytes []byte + hmacTestKey := []byte(test.reqToken) + clusterInfoBytes, err = jws.Verify(hmacTestKey) + + if test.expVerifyFailure { + if err == nil { + t.Errorf("Signature verification did not fail as expected.") + } + // We are done the test here either way. + continue + } + + if err != nil { + t.Errorf("Error verifing signature: %s", err) + continue + } + + err = json.Unmarshal(clusterInfoBytes, &ci) + if err != nil { + t.Errorf("Unable to unmarshall payload to JSON: error=%s body=%s", err, rr.Body.String()) + continue + } + if len(ci.Endpoints) != 2 { + t.Errorf("Expected 2 endpoints, got: %d", len(ci.Endpoints)) + } + if mockEndpoint1 != ci.Endpoints[0] { + t.Errorf("Unexpected endpoint: %s", ci.Endpoints[0]) + } + if mockEndpoint2 != ci.Endpoints[1] { + t.Errorf("Unexpected endpoint: %s", ci.Endpoints[1]) + } + + if len(ci.CertificateAuthorities) != 1 { + t.Errorf("Expected 1 root certificate, got: %d", len(ci.CertificateAuthorities)) + } + if ci.CertificateAuthorities[0] != mockCA { + t.Errorf("Expected CA: %s, got: %s", mockCA, ci.CertificateAuthorities[0]) + } + } + } +} diff --git a/pkg/kubediscovery/model.go b/cmd/kube-discovery/app/model.go similarity index 92% rename from pkg/kubediscovery/model.go rename to cmd/kube-discovery/app/model.go index d0aaedffba3..652c00f0ab5 100644 --- a/pkg/kubediscovery/model.go +++ b/cmd/kube-discovery/app/model.go @@ -1,16 +1,20 @@ /* -Copyright 2016 The Kubernetes Authors. +Copyright 2014 The Kubernetes Authors. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -package kubediscovery + +package discovery type ClusterInfo struct { // TODO Kind, apiVersion diff --git a/pkg/kubediscovery/routes.go b/cmd/kube-discovery/app/routes.go similarity index 94% rename from pkg/kubediscovery/routes.go rename to cmd/kube-discovery/app/routes.go index c0df303b3db..55dfa461962 100644 --- a/pkg/kubediscovery/routes.go +++ b/cmd/kube-discovery/app/routes.go @@ -1,16 +1,20 @@ /* -Copyright 2016 The Kubernetes Authors. +Copyright 2014 The Kubernetes Authors. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -package kubediscovery + +package discovery import ( "net/http" diff --git a/cmd/kube-discovery/dummy.go b/cmd/kube-discovery/dummy.go deleted file mode 100644 index 17711df5045..00000000000 --- a/cmd/kube-discovery/dummy.go +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright 2014 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import ( - _ "github.com/square/go-jose" -) - -func main() { -} diff --git a/cmd/kubediscovery/kubediscovery.go b/cmd/kube-discovery/kubediscovery.go similarity index 93% rename from cmd/kubediscovery/kubediscovery.go rename to cmd/kube-discovery/kubediscovery.go index 3194edb05bf..b37e602a1f9 100644 --- a/cmd/kubediscovery/kubediscovery.go +++ b/cmd/kube-discovery/kubediscovery.go @@ -1,15 +1,19 @@ /* -Copyright 2016 The Kubernetes Authors. +Copyright 2014 The Kubernetes Authors. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + package main import ( @@ -17,7 +21,7 @@ import ( "net/http" "os" - kd "k8s.io/kubernetes/pkg/kubediscovery" + kd "k8s.io/kubernetes/cmd/kube-discovery/app" ) func main() { diff --git a/discovery/Dockerfile b/discovery/Dockerfile deleted file mode 100644 index 2e500ec31b2..00000000000 --- a/discovery/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM golang - -ADD kubediscovery /usr/bin/ -ENTRYPOINT /usr/bin/kubernetes-discovery - -EXPOSE 8080 diff --git a/discovery/README.md b/discovery/README.md deleted file mode 100644 index 3ade93b4a18..00000000000 --- a/discovery/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# kubernetes-discovery -An initial implementation of a Kubernetes discovery service using JSON Web Signatures. - -This prototype is expected to be run by Kubernetes itself for the time being, -and will hopefully be merged into the core API at a later time. - -## Requirements - -Generate a CA cert save it to: /tmp/secret/ca.pem to run the service or unit tests. (will not be required for unit tests for long) Similarly when run within kubernetes we expect a secret to be provided at this location as well. (see below) - -## Build And Run From Source - -``` -$ make WHAT=cmd/kubediscovery -$ _output/local/bin/linux/amd64/kubediscovery -2016/08/23 19:17:28 Listening for requests on port 9898. - -``` - -## Running in Docker - -This image is published temporarily on Docker Hub as dgoodwin/kubediscovery - -`docker run --rm -p 9898:9898 -v /tmp/secret/ca.pem:/tmp/secret/ca.pem --name kubediscovery dgoodwin/kubediscovery` - -## Running in Kubernetes - -A dummy certificate is included in ca-secret.yaml. - -``` -kubectl create -f ca-secret.yaml -kubectl create -f kubediscovery.yaml -``` - -## Testing the API - -`curl "http://localhost:9898/cluster-info/v1/?token-id=TOKENID"` - -You should see JSON containing a signed payload. For code to verify and decode that payload see handler_test.go. diff --git a/discovery/ca-secret.yaml b/discovery/ca-secret.yaml deleted file mode 100644 index f0811c342fa..00000000000 --- a/discovery/ca-secret.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: ca-secret -type: Opaque -data: - ca.pem: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR4RENDQXF5Z0F3SUJBZ0lVV3pqUDl5RUk0eHlRSnBzVHVERU4yV2ROaUFzd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2FERUxNQWtHQTFVRUJoTUNWVk14RHpBTkJnTlZCQWdUQms5eVpXZHZiakVSTUE4R0ExVUVCeE1JVUc5eQpkR3hoYm1ReEV6QVJCZ05WQkFvVENrdDFZbVZ5Ym1WMFpYTXhDekFKQmdOVkJBc1RBa05CTVJNd0VRWURWUVFECkV3cExkV0psY201bGRHVnpNQjRYRFRFMk1EZ3hNVEUyTkRnd01Gb1hEVEl4TURneE1ERTJORGd3TUZvd2FERUwKTUFrR0ExVUVCaE1DVlZNeER6QU5CZ05WQkFnVEJrOXlaV2R2YmpFUk1BOEdBMVVFQnhNSVVHOXlkR3hoYm1ReApFekFSQmdOVkJBb1RDa3QxWW1WeWJtVjBaWE14Q3pBSkJnTlZCQXNUQWtOQk1STXdFUVlEVlFRREV3cExkV0psCmNtNWxkR1Z6TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF3QkhNOGN6anc0Q1cKK05wbklhV012RzZlcVhtelNZT20vbHdaNUhOMnVLck9xaTNHYUUyTjFKd2tzcGRmMXNOUGFZMHdPR2xkbURIZgoxSnlyTW8rUFdLVUVjWko1WGE4Vm02d2I0MlpjczN3MEp5dlEzWFJjaDQyMFJRWGRKayszcmMybWRvSVRkL0lmCnZjWms0N0RzQTMrQU5QSUlSTzdWRmZpS1JNRFpTUDR1OThnVjI2eW1zbjc0TzFVKzNVUHR1TEFTVTFLck9FTk4KR01FWG0ydTJpdmVvbTJrbjFlZTZuM1hCR1o2bU52cUNPdWUxRXdza0gvWkhoUVh1UDgyV1U5dVk0aGVORnoyQwpBNmR0Q0Q0c3Z6eHc3ZFQ2cVhsV0ZIWUYrc3VLVDhXNkczd3NkOWxzV0ZVY0ZWL0lwaTVobEVaTWprNFNoY3RqCjdpYnlrRURKM1FJREFRQUJvMll3WkRBT0JnTlZIUThCQWY4RUJBTUNBUVl3RWdZRFZSMFRBUUgvQkFnd0JnRUIKL3dJQkFqQWRCZ05WSFE0RUZnUVVOdnhRZ3o5ZTNXS2VscU1KTmZXNE1KUHYzc0V3SHdZRFZSMGpCQmd3Rm9BVQpOdnhRZ3o5ZTNXS2VscU1KTmZXNE1KUHYzc0V3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUp1TUhYUms1TEVyCmxET1p4Mm9aRUNQZ29reXMzSGJsM05oempXd2pncXdxNVN6a011V3QrUnVkdnRTK0FUQjFtTjRjYTN0eSt2bWcKT09heTkvaDZoditmSE5jZHpYdWR5dFZYZW1KN3F4ZFoxd25DUUcwdnpqOWRZY0xFSGpJWi94dU1jNlY3dnJ4YwpSc0preGp5aE01UXBmRHd0eVZKeGpkUmVBZ0huSyswTkNieHdtQ3cyRGIvOXpudm9LWGk4TEQwbkQzOFQxY3R3CmhmdGxwTmRoZXFNRlpEZXBuTUYwY2g2cHo5TFV5Mkh1cnhrV2dkWVNjY2VNU0hPTzBMcG4xeVVBMWczOTJhUjUKWk81Zm5KMW95Vm1LVWFCeDJCMndsSVlUSXlES1ZiMnY1UXNHbnYvRHVTMDZhcmVLTmsvTGpHRTRlMXlHOHJkcwpacnZHMzNvUmtEbz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= diff --git a/discovery/kubediscovery.yaml b/discovery/kubediscovery.yaml deleted file mode 100644 index feaa39de540..00000000000 --- a/discovery/kubediscovery.yaml +++ /dev/null @@ -1,37 +0,0 @@ -apiVersion: extensions/v1beta1 -kind: ReplicaSet -metadata: - name: kubediscovery - # these labels can be applied automatically - # from the labels in the pod template if not set - # labels: - # app: guestbook - # tier: frontend -spec: - # this replicas value is default - # modify it according to your case - replicas: 1 - # selector can be applied automatically - # from the labels in the pod template if not set, - # but we are specifying the selector here to - # demonstrate its usage. - template: - metadata: - labels: - app: kubediscovery - spec: - hostNetwork: true - containers: - - name: kubediscovery - image: dgoodwin/kubediscovery - imagePullPolicy: IfNotPresent - ports: - - containerPort: 9898 - volumeMounts: - - name: ca-secret-vol - mountPath: /tmp/secret - readOnly: true - volumes: - - name: ca-secret-vol - secret: - secretName: ca-secret diff --git a/pkg/kubediscovery/handlers_test.go b/pkg/kubediscovery/handlers_test.go deleted file mode 100644 index 2374fef911b..00000000000 --- a/pkg/kubediscovery/handlers_test.go +++ /dev/null @@ -1,100 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package kubediscovery - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/square/go-jose" -) - -func TestClusterInfoIndex(t *testing.T) { - tests := map[string]struct { - url string - expStatus int - }{ - "no token": { - "/cluster-info/v1/", - http.StatusForbidden, - }, - "valid token": { - fmt.Sprintf("/cluster-info/v1/?token-id=%s", tempTokenId), - http.StatusOK, - }, - "invalid token": { - "/cluster-info/v1/?token-id=JUNK", - http.StatusForbidden, - }, - } - - for name, test := range tests { - t.Logf("Running test: %s", name) - // Create a request to pass to our handler. We don't have any query parameters for now, so we'll - // pass 'nil' as the third parameter. - req, err := http.NewRequest("GET", test.url, nil) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - // TODO: mock/stub here - handler := NewClusterInfoHandler() - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != test.expStatus { - t.Errorf("handler returned wrong status code: got %v want %v", - status, test.expStatus) - continue - } - - // If we were expecting valid status validate the body: - if test.expStatus == http.StatusOK { - var ci ClusterInfo - - body := string(rr.Body.Bytes()) - - // Parse the JSON web signature: - jws, err := jose.ParseSigned(body) - if err != nil { - t.Errorf("Error parsing JWS from request body: %s", err) - continue - } - - // Now we can verify the signature on the payload. An error here would - // indicate the the message failed to verify, e.g. because the signature was - // broken or the message was tampered with. - var clusterInfoBytes []byte - hmacTestKey := fromHexBytes(tempToken) - clusterInfoBytes, err = jws.Verify(hmacTestKey) - if err != nil { - t.Errorf("Error verifing signature: %s", err) - continue - } - - err = json.Unmarshal(clusterInfoBytes, &ci) - if err != nil { - t.Errorf("Unable to unmarshall payload to JSON: error=%s body=%s", err, rr.Body.String()) - continue - } - if ci.RootCertificates == "" { - t.Error("No root certificates in response") - continue - } - } - } -}