* Add Virtual Kubelet provider for VIC Initial virtual kubelet provider for VMware VIC. This provider currently handles creating and starting of a pod VM via the VIC portlayer and persona server. Image store handling via the VIC persona server. This provider currently requires the feature/wolfpack branch of VIC. * Added pod stop and delete. Also added node capacity. Added the ability to stop and delete pod VMs via VIC. Also retrieve node capacity information from the VCH. * Cleanup and readme file Some file clean up and added a Readme.md markdown file for the VIC provider. * Cleaned up errors, added function comments, moved operation code 1. Cleaned up error handling. Set standard for creating errors. 2. Added method prototype comments for all interface functions. 3. Moved PodCreator, PodStarter, PodStopper, and PodDeleter to a new folder. * Add mocking code and unit tests for podcache, podcreator, and podstarter Used the unit test framework used in VIC to handle assertions in the provider's unit test. Mocking code generated using OSS project mockery, which is compatible with the testify assertion framework. * Vendored packages for the VIC provider Requires feature/wolfpack branch of VIC and a few specific commit sha of projects used within VIC. * Implementation of POD Stopper and Deleter unit tests (#4) * Updated files for initial PR
467 lines
15 KiB
Go
467 lines
15 KiB
Go
// Copyright 2017 VMware, Inc. 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 common
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gopkg.in/urfave/cli.v1"
|
|
|
|
"github.com/vmware/vic/pkg/certificate"
|
|
"github.com/vmware/vic/pkg/errors"
|
|
"github.com/vmware/vic/pkg/trace"
|
|
)
|
|
|
|
// CertFactory has all input parameters for vic-machine certificate commands needed to create a certificate
|
|
type CertFactory struct {
|
|
Networks
|
|
|
|
CertPath string
|
|
DisplayName string
|
|
Scert string
|
|
Skey string
|
|
Ccert string
|
|
Ckey string
|
|
Cacert string
|
|
Cakey string
|
|
ClientCert *tls.Certificate
|
|
ClientCAsArg cli.StringSlice `arg:"tls-ca"`
|
|
ClientCAs []byte
|
|
EnvFile string
|
|
Cname string
|
|
Org cli.StringSlice
|
|
KeySize int
|
|
NoTLS bool
|
|
NoTLSverify bool
|
|
KeyPEM []byte
|
|
CertPEM []byte
|
|
NoSaveToDisk bool
|
|
}
|
|
|
|
func (c *CertFactory) CertFlags() []cli.Flag {
|
|
return []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "tls-server-key",
|
|
Value: "",
|
|
Usage: "Virtual Container Host private key file (server certificate)",
|
|
Destination: &c.Skey,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "tls-server-cert",
|
|
Value: "",
|
|
Usage: "Virtual Container Host x509 certificate file (server certificate)",
|
|
Destination: &c.Scert,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "tls-cname",
|
|
Value: "",
|
|
Usage: "Common Name to use in generated CA certificate when requiring client certificate authentication",
|
|
Destination: &c.Cname,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "tls-cert-path",
|
|
Value: "",
|
|
Usage: "The path to check for existing certificates and in which to save generated certificates. Defaults to './<vch name>/'",
|
|
Destination: &c.CertPath,
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "no-tlsverify, kv",
|
|
Usage: "Disable authentication via client certificates - for more tls options see advanced help (-x)",
|
|
Destination: &c.NoTLSverify,
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "organization",
|
|
Usage: "A list of identifiers to record in the generated certificates. Defaults to VCH name and IP/FQDN if not provided.",
|
|
Value: &c.Org,
|
|
Hidden: true,
|
|
},
|
|
cli.IntFlag{
|
|
Name: "certificate-key-size, ksz",
|
|
Usage: "Size of key to use when generating certificates",
|
|
Value: 2048,
|
|
Destination: &c.KeySize,
|
|
Hidden: true,
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "tls-ca, ca",
|
|
Usage: "Specify a list of certificate authority files to use for client verification",
|
|
Value: &c.ClientCAsArg,
|
|
Hidden: true,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (c *CertFactory) ProcessCertificates(op trace.Operation, displayName string, force bool, debug int) error {
|
|
// set up the locations for the certificates and env file
|
|
if c.CertPath == "" {
|
|
c.CertPath = displayName
|
|
}
|
|
c.EnvFile = fmt.Sprintf("%s/%s.env", c.CertPath, displayName)
|
|
|
|
// check for insecure case
|
|
if c.NoTLS {
|
|
op.Warn("Configuring without TLS - all communications will be insecure")
|
|
return nil
|
|
}
|
|
|
|
if c.Scert != "" && c.Skey == "" {
|
|
return cli.NewExitError("key and cert should be specified at the same time", 1)
|
|
}
|
|
if c.Scert == "" && c.Skey != "" {
|
|
return cli.NewExitError("key and cert should be specified at the same time", 1)
|
|
}
|
|
|
|
// if we've not got a specific CommonName but do have a static IP then go with that.
|
|
if c.Cname == "" {
|
|
if c.ClientNetworkIP != "" {
|
|
c.Cname = c.ClientNetworkIP
|
|
op.Infof("Using client-network-ip as cname where needed - use --tls-cname to override: %s", c.Cname)
|
|
} else if c.PublicNetworkIP != "" && (c.PublicNetworkName == c.ClientNetworkName || c.ClientNetworkName == "") {
|
|
c.Cname = c.PublicNetworkIP
|
|
op.Infof("Using public-network-ip as cname where needed - use --tls-cname to override: %s", c.Cname)
|
|
} else if c.ManagementNetworkIP != "" && (c.ManagementNetworkName == c.ClientNetworkName || (c.ClientNetworkName == "" && c.ManagementNetworkName == c.PublicNetworkName)) {
|
|
c.Cname = c.ManagementNetworkIP
|
|
op.Infof("Using management-network-ip as cname where needed - use --tls-cname to override: %s", c.Cname)
|
|
}
|
|
|
|
if c.Cname != "" {
|
|
// Strip network mask from IP address if set.
|
|
// #nosec: Errors unhandled.
|
|
if cnameIP, _, _ := net.ParseCIDR(c.Cname); cnameIP != nil {
|
|
c.Cname = cnameIP.String()
|
|
}
|
|
}
|
|
}
|
|
|
|
// load what certificates we can
|
|
cas, keypair, err := c.loadCertificates(op, debug)
|
|
if err != nil {
|
|
op.Errorf("Unable to load certificates: %s", err)
|
|
if !force {
|
|
return err
|
|
}
|
|
|
|
op.Warnf("Ignoring error loading certificates due to --force")
|
|
cas = nil
|
|
keypair = nil
|
|
err = nil
|
|
}
|
|
|
|
// we need to generate some part of the certificate configuration
|
|
gcas, gkeypair, err := c.generateCertificates(op, keypair == nil, !c.NoTLSverify && len(cas) == 0)
|
|
if err != nil {
|
|
op.Error("cannot continue: unable to generate certificates")
|
|
return err
|
|
}
|
|
|
|
if keypair != nil {
|
|
c.KeyPEM = keypair.KeyPEM
|
|
c.CertPEM = keypair.CertPEM
|
|
} else if gkeypair != nil {
|
|
c.KeyPEM = gkeypair.KeyPEM
|
|
c.CertPEM = gkeypair.CertPEM
|
|
}
|
|
|
|
if len(cas) == 0 {
|
|
cas = gcas
|
|
}
|
|
|
|
if len(c.KeyPEM) == 0 {
|
|
return errors.New("Failed to load or generate server certificates")
|
|
}
|
|
|
|
if len(cas) == 0 && !c.NoTLSverify {
|
|
return errors.New("Failed to load or generate certificate authority")
|
|
}
|
|
|
|
// do we have key, cert, and --no-tlsverify
|
|
if c.NoTLSverify || len(cas) == 0 {
|
|
op.Warnf("Configuring without TLS verify - certificate-based authentication disabled")
|
|
return nil
|
|
}
|
|
|
|
c.ClientCAs = cas
|
|
return nil
|
|
}
|
|
|
|
// loadCertificates returns the client CA pool and the keypair for server certificates on success
|
|
func (c *CertFactory) loadCertificates(op trace.Operation, debug int) ([]byte, *certificate.KeyPair, error) {
|
|
defer trace.End(trace.Begin("", op))
|
|
|
|
// reads each of the files specified, assuming that they are PEM encoded certs,
|
|
// and constructs a byte array suitable for passing to CertPool.AppendCertsFromPEM
|
|
var certs []byte
|
|
for _, f := range c.ClientCAsArg {
|
|
b, err := ioutil.ReadFile(f)
|
|
if err != nil {
|
|
err = errors.Errorf("Failed to load authority from file %s: %s", f, err)
|
|
return nil, nil, err
|
|
}
|
|
|
|
certs = append(certs, b...)
|
|
op.Infof("Loaded CA from %s", f)
|
|
}
|
|
|
|
var keypair *certificate.KeyPair
|
|
// default names
|
|
skey := filepath.Join(c.CertPath, certificate.ServerKey)
|
|
scert := filepath.Join(c.CertPath, certificate.ServerCert)
|
|
ca := filepath.Join(c.CertPath, certificate.CACert)
|
|
ckey := filepath.Join(c.CertPath, certificate.ClientKey)
|
|
ccert := filepath.Join(c.CertPath, certificate.ClientCert)
|
|
|
|
// if specific files are supplied, use those
|
|
explicit := false
|
|
if c.Scert != "" && c.Skey != "" {
|
|
explicit = true
|
|
skey = c.Skey
|
|
scert = c.Scert
|
|
}
|
|
|
|
// load the server certificate
|
|
keypair = certificate.NewKeyPair(scert, skey, nil, nil)
|
|
if err := keypair.LoadCertificate(); err != nil {
|
|
if explicit || !os.IsNotExist(err) {
|
|
// if these files were explicit paths, or anything other than not found, fail
|
|
op.Errorf("Failed to load certificate: %s", err)
|
|
return certs, nil, err
|
|
}
|
|
|
|
op.Debugf("Unable to locate existing server certificate in cert path")
|
|
return nil, nil, nil
|
|
}
|
|
|
|
// check that any supplied cname matches the server cert CN
|
|
cert, err := keypair.Certificate()
|
|
if err != nil {
|
|
op.Errorf("Failed to parse certificate: %s", err)
|
|
return certs, nil, err
|
|
}
|
|
|
|
if cert.Leaf == nil {
|
|
op.Warnf("Failed to load x509 leaf: Unable to confirm server certificate cname matches provided cname %q. Continuing...", c.Cname)
|
|
} else {
|
|
// We just do a direct equality check here - trying to be clever is liable to lead to hard
|
|
// to diagnose errors
|
|
if cert.Leaf.Subject.CommonName != c.Cname {
|
|
op.Errorf("Provided cname does not match that in existing server certificate: %s", cert.Leaf.Subject.CommonName)
|
|
if debug > 2 {
|
|
op.Debugf("Certificate does not match provided cname: %#+v", cert.Leaf)
|
|
}
|
|
return certs, nil, fmt.Errorf("cname option doesn't match existing server certificate in certificate path %s", c.CertPath)
|
|
}
|
|
}
|
|
|
|
op.Infof("Loaded server certificate %s", scert)
|
|
c.Skey = skey
|
|
c.Scert = scert
|
|
|
|
// only try for CA certificate if no-tlsverify has NOT been specified and we haven't already loaded an authority cert
|
|
if !c.NoTLSverify && len(certs) == 0 {
|
|
b, err := ioutil.ReadFile(ca)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
op.Debugf("Unable to locate existing CA in cert path")
|
|
return certs, keypair, nil
|
|
}
|
|
|
|
// if the CA exists but cannot be loaded then it's an error
|
|
op.Errorf("Failed to load authority from certificate path %s: %s", c.CertPath, err)
|
|
return certs, keypair, errors.New("failed to load certificate authority")
|
|
}
|
|
|
|
c.Cacert = ca
|
|
|
|
op.Infof("Loaded CA with default name from certificate path %s", c.CertPath)
|
|
certs = b
|
|
|
|
// load client certs - we ensure the client certs validate with the provided CA or ignore any we find
|
|
cpair := certificate.NewKeyPair(ccert, ckey, nil, nil)
|
|
if err := cpair.LoadCertificate(); err != nil {
|
|
op.Warnf("Unable to load client certificate - validation of API endpoint will be best effort only: %s", err)
|
|
}
|
|
|
|
clientCert, err := certificate.VerifyClientCert(certs, cpair)
|
|
if err != nil {
|
|
switch err.(type) {
|
|
case certificate.CertParseError, certificate.CreateCAPoolError:
|
|
op.Debug(err)
|
|
case certificate.CertVerifyError:
|
|
op.Warnf("%s - continuing without client certificate", err)
|
|
}
|
|
|
|
return certs, keypair, nil
|
|
}
|
|
|
|
c.Ckey = ckey
|
|
c.Ccert = ccert
|
|
c.ClientCert = clientCert
|
|
|
|
op.Infof("Loaded client certificate with default name from certificate path %s", c.CertPath)
|
|
}
|
|
|
|
return certs, keypair, nil
|
|
}
|
|
|
|
func (c *CertFactory) generateCertificates(op trace.Operation, server bool, client bool) ([]byte, *certificate.KeyPair, error) {
|
|
defer trace.End(trace.Begin("", op))
|
|
|
|
if !server && !client {
|
|
op.Debug("Not generating server or client certs, nothing for generateCertificates to do")
|
|
return nil, nil, nil
|
|
}
|
|
|
|
var certs []byte
|
|
// generate the certs and keys with names conforming the default the docker client expects
|
|
files, err := ioutil.ReadDir(c.CertPath)
|
|
if len(files) > 0 {
|
|
return nil, nil, fmt.Errorf("Specified directory to store certificates is not empty. Specify a new path in which to store generated certificates using --tls-cert-path or remove the contents of \"%s\" and run vic-machine again.", c.CertPath)
|
|
}
|
|
|
|
if !c.NoSaveToDisk {
|
|
err = os.MkdirAll(c.CertPath, 0700)
|
|
if err != nil {
|
|
op.Errorf("Unable to make directory \"%s\" to hold certificates (set via --tls-cert-path)", c.CertPath)
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
c.Skey = filepath.Join(c.CertPath, certificate.ServerKey)
|
|
c.Scert = filepath.Join(c.CertPath, certificate.ServerCert)
|
|
|
|
c.Ckey = filepath.Join(c.CertPath, certificate.ClientKey)
|
|
c.Ccert = filepath.Join(c.CertPath, certificate.ClientCert)
|
|
|
|
cakey := filepath.Join(c.CertPath, certificate.CAKey)
|
|
c.Cacert = filepath.Join(c.CertPath, certificate.CACert)
|
|
|
|
if server && !client {
|
|
op.Infof("Generating self-signed certificate/key pair - private key in %s", c.Skey)
|
|
keypair := certificate.NewKeyPair(c.Scert, c.Skey, nil, nil)
|
|
err := keypair.CreateSelfSigned(c.Cname, nil, c.KeySize)
|
|
if err != nil {
|
|
op.Errorf("Failed to generate self-signed certificate: %s", err)
|
|
return nil, nil, err
|
|
}
|
|
if !c.NoSaveToDisk {
|
|
if err = keypair.SaveCertificate(); err != nil {
|
|
op.Errorf("Failed to save server certificates: %s", err)
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
return certs, keypair, nil
|
|
}
|
|
|
|
// client auth path
|
|
if c.Cname == "" {
|
|
op.Error("Common Name must be provided when generating certificates for client authentication:")
|
|
op.Info(" --tls-cname=<FQDN or static IP> # for the appliance VM")
|
|
op.Info(" --tls-cname=<*.yourdomain.com> # if DNS has entries in that form for DHCP addresses (less secure)")
|
|
op.Info(" --no-tlsverify # disables client authentication (anyone can connect to the VCH)")
|
|
op.Info(" --no-tls # disables TLS entirely")
|
|
op.Info("")
|
|
|
|
return certs, nil, errors.New("provide Common Name for server certificate")
|
|
}
|
|
|
|
// for now re-use the display name as the organisation if unspecified
|
|
if len(c.Org) == 0 {
|
|
c.Org = []string{c.DisplayName}
|
|
}
|
|
if len(c.Org) == 1 && !strings.HasPrefix(c.Cname, "*") {
|
|
// Add in the cname if it's not a wildcard
|
|
c.Org = append(c.Org, c.Cname)
|
|
}
|
|
|
|
// Certificate authority
|
|
op.Infof("Generating CA certificate/key pair - private key in %s", cakey)
|
|
cakp := certificate.NewKeyPair(c.Cacert, cakey, nil, nil)
|
|
err = cakp.CreateRootCA(c.Cname, c.Org, c.KeySize)
|
|
if err != nil {
|
|
op.Errorf("Failed to generate CA: %s", err)
|
|
return nil, nil, err
|
|
}
|
|
if !c.NoSaveToDisk {
|
|
if err = cakp.SaveCertificate(); err != nil {
|
|
op.Errorf("Failed to save CA certificates: %s", err)
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
// Server certificates
|
|
var skp *certificate.KeyPair
|
|
if server {
|
|
op.Infof("Generating server certificate/key pair - private key in %s", c.Skey)
|
|
skp = certificate.NewKeyPair(c.Scert, c.Skey, nil, nil)
|
|
err = skp.CreateServerCertificate(c.Cname, c.Org, c.KeySize, cakp)
|
|
if err != nil {
|
|
op.Errorf("Failed to generate server certificates: %s", err)
|
|
return nil, nil, err
|
|
}
|
|
if !c.NoSaveToDisk {
|
|
if err = skp.SaveCertificate(); err != nil {
|
|
op.Errorf("Failed to save server certificates: %s", err)
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Client certificates
|
|
if client {
|
|
op.Infof("Generating client certificate/key pair - private key in %s", c.Ckey)
|
|
ckp := certificate.NewKeyPair(c.Ccert, c.Ckey, nil, nil)
|
|
err = ckp.CreateClientCertificate(c.Cname, c.Org, c.KeySize, cakp)
|
|
if err != nil {
|
|
op.Errorf("Failed to generate client certificates: %s", err)
|
|
return nil, nil, err
|
|
}
|
|
if !c.NoSaveToDisk {
|
|
if err = ckp.SaveCertificate(); err != nil {
|
|
op.Errorf("Failed to save client certificates: %s", err)
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
c.ClientCert, err = ckp.Certificate()
|
|
if err != nil {
|
|
op.Warnf("Failed to stash client certificate for later application level validation: %s", err)
|
|
}
|
|
|
|
// If openssl is present, try to generate a browser friendly pfx file (a bundle of the public certificate AND the private key)
|
|
// The pfx file can be imported directly into keychains for client certificate authentication
|
|
certPath := filepath.Clean(c.CertPath)
|
|
args := strings.Split(fmt.Sprintf("pkcs12 -export -out %[1]s/cert.pfx -inkey %[1]s/key.pem -in %[1]s/cert.pem -certfile %[1]s/ca.pem -password pass:", certPath), " ")
|
|
// #nosec: Subprocess launching with variable
|
|
pfx := exec.Command("openssl", args...)
|
|
out, err := pfx.CombinedOutput()
|
|
if err != nil {
|
|
op.Debug(out)
|
|
op.Warnf("Failed to generate browser friendly PFX client certificate: %s", err)
|
|
} else {
|
|
op.Infof("Generated browser friendly PFX client certificate - certificate in %s/cert.pfx", certPath)
|
|
}
|
|
}
|
|
|
|
return cakp.CertPEM, skp, nil
|
|
}
|