diff --git a/cmd/localkube/cmd/start.go b/cmd/localkube/cmd/start.go index fd7e16beab..d9f636fb47 100644 --- a/cmd/localkube/cmd/start.go +++ b/cmd/localkube/cmd/start.go @@ -56,8 +56,12 @@ func init() { func SetupServer(s *localkube.LocalkubeServer) { - err := s.GenerateCerts() + hostIP, err := s.GetHostIP() if err != nil { + fmt.Println("Error getting host IP!") + panic(err) + } + if err := s.GenerateCerts(hostIP); err != nil { fmt.Println("Failed to create certificates!") panic(err) } diff --git a/pkg/localkube/localkube.go b/pkg/localkube/localkube.go index 06bd9032cc..e14a495852 100644 --- a/pkg/localkube/localkube.go +++ b/pkg/localkube/localkube.go @@ -17,7 +17,10 @@ limitations under the License. package localkube import ( + "crypto/x509" + "encoding/pem" "fmt" + "io/ioutil" "net" "path" @@ -82,20 +85,52 @@ func (lk LocalkubeServer) GetHostIP() (net.IP, error) { return utilnet.ChooseBindAddress(net.ParseIP("0.0.0.0")) } -func (lk LocalkubeServer) GenerateCerts() error { +func (lk LocalkubeServer) loadCert(path string) (*x509.Certificate, error) { + contents, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + decoded, _ := pem.Decode(contents) + if decoded == nil { + return nil, fmt.Errorf("Unable to decode certificate.") + } - if util.CanReadFile(lk.GetPublicKeyCertPath()) && util.CanReadFile(lk.GetPrivateKeyCertPath()) { + return x509.ParseCertificate(decoded.Bytes) +} + +func (lk LocalkubeServer) shouldGenerateCerts(hostIP net.IP) bool { + if !(util.CanReadFile(lk.GetPublicKeyCertPath()) && + util.CanReadFile(lk.GetPrivateKeyCertPath())) { + fmt.Println("Regenerating certs because the files aren't readable.") + return true + } + + cert, err := lk.loadCert(lk.GetPublicKeyCertPath()) + if err != nil { + fmt.Println("Regenerating certs because there was an error loading the certificate: ", err) + return true + } + + for _, certIP := range cert.IPAddresses { + if certIP.Equal(hostIP) { + return false + } + } + fmt.Printf( + "Regenerating certs because the IP didn't match. Got %s, expected %s", + cert.IPAddresses, hostIP) + return true +} + +func (lk LocalkubeServer) GenerateCerts(hostIP net.IP) error { + + if !lk.shouldGenerateCerts(hostIP) { fmt.Println("Using these existing certs: ", lk.GetPublicKeyCertPath(), lk.GetPrivateKeyCertPath()) return nil } alternateIPs := []net.IP{lk.ServiceClusterIPRange.IP} alternateDNS := []string{fmt.Sprintf("%s.%s", "kubernetes.default.svc", lk.DNSDomain), "kubernetes.default.svc", "kubernetes.default", "kubernetes"} - hostIP, err := lk.GetHostIP() - if err != nil { - fmt.Println("Failed to get host IP: ", err) - return err - } if err := utilcrypto.GenerateSelfSignedCert(hostIP.String(), lk.GetPublicKeyCertPath(), lk.GetPrivateKeyCertPath(), alternateIPs, alternateDNS); err != nil { fmt.Println("Failed to create certs: ", err) diff --git a/pkg/localkube/localkube_test.go b/pkg/localkube/localkube_test.go new file mode 100644 index 0000000000..fbd73f53e6 --- /dev/null +++ b/pkg/localkube/localkube_test.go @@ -0,0 +1,123 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 localkube + +import ( + "io/ioutil" + "net" + "os" + "path/filepath" + "testing" + + "k8s.io/minikube/pkg/minikube/tests" +) + +var testIP = net.ParseIP("1.2.3.4") + +func TestGenerateCerts(t *testing.T) { + tempDir := tests.MakeTempDir() + defer os.RemoveAll(tempDir) + os.Mkdir(filepath.Join(tempDir, "certs"), 0777) + + _, ipRange, _ := net.ParseCIDR("10.0.0.0/24") + lk := LocalkubeServer{ + LocalkubeDirectory: tempDir, + ServiceClusterIPRange: *ipRange, + } + + if err := lk.GenerateCerts(testIP); err != nil { + t.Fatalf("Unexpected error generating certs: %s", err) + } + + for _, f := range []string{"apiserver.crt", "apiserver.key"} { + p := filepath.Join(tempDir, "certs", f) + _, err := os.Stat(p) + if os.IsNotExist(err) { + t.Fatalf("Certificate not created: %s", p) + } + } + cert, err := lk.loadCert(filepath.Join(tempDir, "certs", "apiserver.crt")) + if err != nil { + t.Fatalf("Error parsing cert: %s", err) + } + if !cert.IPAddresses[0].Equal(testIP) { + t.Fatalf("IP mismatch: %s != %s.", cert.IPAddresses[0], testIP) + } +} + +func TestShouldGenerateCertsNoFiles(t *testing.T) { + lk := LocalkubeServer{LocalkubeDirectory: "baddir"} + if !lk.shouldGenerateCerts(testIP) { + t.Fatalf("No certs exist, we should generate.") + } +} + +func TestShouldGenerateCertsOneFile(t *testing.T) { + tempDir := tests.MakeTempDir() + defer os.RemoveAll(tempDir) + os.Mkdir(filepath.Join(tempDir, "certs"), 0777) + ioutil.WriteFile(filepath.Join(tempDir, "certs", "apiserver.crt"), []byte(""), 0644) + lk := LocalkubeServer{LocalkubeDirectory: tempDir} + if !lk.shouldGenerateCerts(testIP) { + t.Fatalf("Not all certs exist, we should generate.") + } +} + +func TestShouldGenerateCertsBadFiles(t *testing.T) { + tempDir := tests.MakeTempDir() + defer os.RemoveAll(tempDir) + os.Mkdir(filepath.Join(tempDir, "certs"), 0777) + for _, f := range []string{"apiserver.crt", "apiserver.key"} { + ioutil.WriteFile(filepath.Join(tempDir, "certs", f), []byte(""), 0644) + } + lk := LocalkubeServer{LocalkubeDirectory: tempDir} + if !lk.shouldGenerateCerts(testIP) { + t.Fatalf("Certs are badly formatted, we should generate.") + } +} + +func TestShouldGenerateCertsMismatchedIP(t *testing.T) { + tempDir := tests.MakeTempDir() + defer os.RemoveAll(tempDir) + os.Mkdir(filepath.Join(tempDir, "certs"), 0777) + + _, ipRange, _ := net.ParseCIDR("10.0.0.0/24") + lk := LocalkubeServer{ + LocalkubeDirectory: tempDir, + ServiceClusterIPRange: *ipRange, + } + lk.GenerateCerts(testIP) + if !lk.shouldGenerateCerts(net.ParseIP("4.3.2.1")) { + t.Fatalf("IPs don't match, we should generate.") + } +} + +func TestShouldNotGenerateCerts(t *testing.T) { + tempDir := tests.MakeTempDir() + defer os.RemoveAll(tempDir) + os.Mkdir(filepath.Join(tempDir, "certs"), 0777) + + _, ipRange, _ := net.ParseCIDR("10.0.0.0/24") + lk := LocalkubeServer{ + LocalkubeDirectory: tempDir, + ServiceClusterIPRange: *ipRange, + } + lk.GenerateCerts(testIP) + if lk.shouldGenerateCerts(testIP) { + t.Fatalf("IPs match, we should not generate.") + } +} diff --git a/pkg/minikube/sshutil/sshutil.go b/pkg/minikube/sshutil/sshutil.go deleted file mode 100644 index 5623b29f93..0000000000 --- a/pkg/minikube/sshutil/sshutil.go +++ /dev/null @@ -1,114 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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 sshutil - -import ( - "bufio" - "fmt" - "io" - "os" - - "github.com/docker/machine/libmachine/drivers" - machinessh "github.com/docker/machine/libmachine/ssh" - "golang.org/x/crypto/ssh" -) - -// SSHSession provides methods for running commands on a host. -type SSHSession interface { - Close() error - StdinPipe() (io.WriteCloser, error) - Start(cmd string) error - Wait() error -} - -// NewSSHSession returns an SSHSession object for running commands. -func NewSSHSession(d drivers.Driver) (SSHSession, error) { - h, err := newSSHHost(d) - if err != nil { - return nil, err - - } - auth := &machinessh.Auth{} - if h.SSHKeyPath != "" { - auth.Keys = []string{h.SSHKeyPath} - } - config, err := machinessh.NewNativeConfig(h.Username, auth) - if err != nil { - return nil, err - } - - client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", h.IP, h.Port), &config) - if err != nil { - return nil, err - } - session, err := client.NewSession() - if err != nil { - return nil, err - } - return session, nil -} - -// Transfer uses an SSH session to copy a file to the remote machine. -func Transfer(localpath, remotepath string, r SSHSession) error { - f, err := os.Open(localpath) - if err != nil { - return err - } - reader := bufio.NewReader(f) - - cmd := fmt.Sprintf("cat > %s", remotepath) - stdin, err := r.StdinPipe() - if err != nil { - return err - } - - if err := r.Start(cmd); err != nil { - return err - } - _, err = io.Copy(stdin, reader) - stdin.Close() - if err != nil { - return err - } - - return r.Wait() -} - -type sshHost struct { - IP string - Port int - SSHKeyPath string - Username string -} - -func newSSHHost(d drivers.Driver) (*sshHost, error) { - - ip, err := d.GetSSHHostname() - if err != nil { - return nil, err - } - port, err := d.GetSSHPort() - if err != nil { - return nil, err - } - return &sshHost{ - IP: ip, - Port: port, - SSHKeyPath: d.GetSSHKeyPath(), - Username: d.GetSSHUsername(), - }, nil -} diff --git a/pkg/minikube/sshutil/sshutil_test.go b/pkg/minikube/sshutil/sshutil_test.go deleted file mode 100644 index 7257194c01..0000000000 --- a/pkg/minikube/sshutil/sshutil_test.go +++ /dev/null @@ -1,130 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -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 sshutil - -import ( - "fmt" - "io/ioutil" - "os" - "path" - "strings" - "testing" - - "github.com/docker/machine/libmachine/drivers" - - "k8s.io/minikube/pkg/minikube/tests" -) - -func TestNewSSHSession(t *testing.T) { - s, _ := tests.NewSSHServer() - port, err := s.Start() - if err != nil { - t.Fatalf("Error starting ssh server: %s", err) - } - d := &tests.MockDriver{ - Port: port, - BaseDriver: drivers.BaseDriver{ - IPAddress: "127.0.0.1", - SSHKeyPath: "", - }, - } - session, err := NewSSHSession(d) - if err != nil { - t.Fatalf("Unexpected error: %s", err) - } - - if !s.Connected { - t.Fatalf("Error!") - } - - cmd := "foo" - session.Start(cmd) - session.Wait() - - if !strings.Contains(s.Commands[0], cmd) { - t.Fatalf("Expected command: %s, got %s", cmd, s.Commands[0]) - } -} - -func TestNewSSHHost(t *testing.T) { - sshKeyPath := "mypath" - ip := "localhost" - user := "myuser" - d := tests.MockDriver{ - BaseDriver: drivers.BaseDriver{ - IPAddress: ip, - SSHUser: user, - SSHKeyPath: sshKeyPath, - }, - } - - h, err := newSSHHost(&d) - if err != nil { - t.Fatalf("Unexpected error creating host: %s", err) - } - - if h.SSHKeyPath != sshKeyPath { - t.Fatalf("%s != %s", h.SSHKeyPath, sshKeyPath) - } - if h.Username != user { - t.Fatalf("%s != %s", h.Username, user) - } - if h.IP != ip { - t.Fatalf("%s != %s", h.IP, ip) - } -} - -func TestNewSSHHostError(t *testing.T) { - d := tests.MockDriver{HostError: true} - - _, err := newSSHHost(&d) - if err == nil { - t.Fatal("Expected error creating host, got nil") - } -} - -func TestTransfer(t *testing.T) { - s, _ := tests.NewSSHServer() - port, err := s.Start() - if err != nil { - t.Fatalf("Error starting ssh server: %s", err) - } - d := &tests.MockDriver{ - Port: port, - BaseDriver: drivers.BaseDriver{ - IPAddress: "127.0.0.1", - SSHKeyPath: "", - }, - } - session, err := NewSSHSession(d) - if err != nil { - t.Fatalf("Unexpected error: %s", err) - } - tempDir := tests.MakeTempDir() - defer os.RemoveAll(tempDir) - - src := path.Join(tempDir, "foo") - dest := "bar" - ioutil.WriteFile(src, []byte("testcontents"), 0644) - if err := Transfer(src, dest, session); err != nil { - t.Fatalf("Unexpected error: %s", err) - } - cmd := fmt.Sprintf("cat > %s", dest) - if !strings.Contains(s.Commands[0], cmd) { - t.Fatalf("Expected command: %s, got %s", cmd, s.Commands[0]) - } -} diff --git a/pkg/minikube/tests/driver_mock.go b/pkg/minikube/tests/driver_mock.go index 4dc79d1c50..c4e48a16ea 100644 --- a/pkg/minikube/tests/driver_mock.go +++ b/pkg/minikube/tests/driver_mock.go @@ -29,8 +29,6 @@ type MockDriver struct { drivers.BaseDriver CurrentState state.State RemoveError bool - HostError bool - Port int } // Create creates a MockDriver instance @@ -39,30 +37,14 @@ func (driver *MockDriver) Create() error { return nil } -func (driver *MockDriver) GetIP() (string, error) { - return driver.BaseDriver.GetIP() -} - // GetCreateFlags returns the flags used to create a MockDriver func (driver *MockDriver) GetCreateFlags() []mcnflag.Flag { return []mcnflag.Flag{} } -func (driver *MockDriver) GetSSHPort() (int, error) { - return driver.Port, nil -} - // GetSSHHostname returns the hostname for SSH func (driver *MockDriver) GetSSHHostname() (string, error) { - if driver.HostError { - return "", fmt.Errorf("Error getting host!") - } - return "localhost", nil -} - -// GetSSHHostname returns the hostname for SSH -func (driver *MockDriver) GetSSHKeyPath() string { - return driver.BaseDriver.SSHKeyPath + return "", nil } // GetState returns the state of the driver diff --git a/pkg/minikube/tests/ssh_mock.go b/pkg/minikube/tests/ssh_mock.go deleted file mode 100644 index f10b0f9365..0000000000 --- a/pkg/minikube/tests/ssh_mock.go +++ /dev/null @@ -1,93 +0,0 @@ -package tests - -import ( - "crypto/rand" - "crypto/rsa" - "io" - "io/ioutil" - "net" - "strconv" - - "golang.org/x/crypto/ssh" -) - -// SSHServer provides a mock SSH Server for testing. Commands are stored, not executed. -type SSHServer struct { - Config *ssh.ServerConfig - // Commands stores the raw commands executed against the server. - Commands []string - Connected bool -} - -// NewSSHServer returns a NewSSHServer instance, ready for use. -func NewSSHServer() (*SSHServer, error) { - s := &SSHServer{} - s.Config = &ssh.ServerConfig{ - NoClientAuth: true, - } - - private, err := rsa.GenerateKey(rand.Reader, 2014) - if err != nil { - return nil, err - } - signer, err := ssh.NewSignerFromKey(private) - if err != nil { - return nil, err - } - s.Config.AddHostKey(signer) - return s, nil -} - -// Start starts the mock SSH Server, and returns the port it's listening on. -func (s *SSHServer) Start() (int, error) { - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - return 0, err - } - - // Main loop, listen for connections and store the commands. - go func() { - for { - nConn, err := listener.Accept() - if err != nil { - return - } - - _, chans, reqs, err := ssh.NewServerConn(nConn, s.Config) - if err != nil { - return - } - // The incoming Request channel must be serviced. - go ssh.DiscardRequests(reqs) - - // Service the incoming Channel channel. - for newChannel := range chans { - channel, requests, err := newChannel.Accept() - s.Connected = true - if err != nil { - return - } - - req := <-requests - req.Reply(true, nil) - s.Commands = append(s.Commands, string(req.Payload)) - channel.SendRequest("exit-status", false, []byte{0, 0, 0, 0}) - - // Discard anything that comes in over stdin. - io.Copy(ioutil.Discard, channel) - channel.Close() - } - } - }() - - // Parse and return the port. - _, p, err := net.SplitHostPort(listener.Addr().String()) - if err != nil { - return 0, err - } - port, err := strconv.Atoi(p) - if err != nil { - return 0, err - } - return port, nil -}