Rework keypair to fit our change model

We also remove another special-case context (pki), so that it is just
another object type.
This commit is contained in:
Justin Santa Barbara 2016-05-14 16:24:59 -04:00
parent ac220d2ba1
commit 1c97a94d87
8 changed files with 407 additions and 160 deletions

View File

@ -12,6 +12,7 @@ import (
"k8s.io/kube-deploy/upup/pkg/fi/cloudup/gce"
"k8s.io/kube-deploy/upup/pkg/fi/cloudup/gcetasks"
"k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform"
"k8s.io/kube-deploy/upup/pkg/fi/fitasks"
"k8s.io/kube-deploy/upup/pkg/fi/loader"
"k8s.io/kube-deploy/upup/pkg/fi/utils"
"os"
@ -154,6 +155,10 @@ func (c *CreateClusterCmd) Run() error {
c.Config.NodeUpTags = append(c.Config.NodeUpTags, "_jessie", "_debian_family", "_systemd")
l.AddTypes(map[string]interface{}{
"keypair": &fitasks.Keypair{},
})
switch c.Config.CloudProvider {
case "gce":
{

View File

@ -1,3 +1,3 @@
subject:
CommonName: kubecfg
type: client
keypair/kubecfg:
subject: cn=kubecfg
type: client

View File

@ -1,3 +1,3 @@
subject:
CommonName: kubelet
type: client
keypair/kubelet:
subject: cn=kubelet
type: client

View File

@ -1,12 +1,19 @@
subject:
CommonName: kubernetes-master
type: server
alternateNames:
- kubernetes
- kubernetes.default
- kubernetes.default.svc
- kubernetes.default.svc.{{ .DNSDomain }}
- {{ .MasterName }}
- {{ .MasterPublicIP }}
- {{ .MasterInternalIP }}
- {{ .WellKnownServiceIP 1 }}
keypair/master:
subject: cn=kubernetes-master
type: server
alternateNames:
- kubernetes
- kubernetes.default
- kubernetes.default.svc
- kubernetes.default.svc.{{ .DNSDomain }}
- "{{ .MasterName }}"
- "{{ .MasterInternalIP }}"
- "{{ .WellKnownServiceIP 1 }}"
- "{{ .MasterPublicIP }}"
{{ if eq .CloudProvider "aws" }}
alternateNameTasks:
- elasticIP/{{ .MasterName }}-ip
{{ else if eq .CloudProvider "gce" }}
alternateNameTasks:
- ipAddress/{{ .MasterName }}-ip
{{ end }}

View File

@ -7,7 +7,6 @@ import (
"github.com/golang/glog"
"io"
"k8s.io/kube-deploy/upup/pkg/fi"
"k8s.io/kube-deploy/upup/pkg/fi/fitasks"
"k8s.io/kube-deploy/upup/pkg/fi/loader"
"k8s.io/kube-deploy/upup/pkg/fi/nodeup"
"k8s.io/kube-deploy/upup/pkg/fi/utils"
@ -129,6 +128,8 @@ func (l *Loader) executeTemplate(key string, d string, args []string) (string, e
}
t.Funcs(funcMap)
t.Option("missingkey=zero")
context := l.config
_, err := t.Parse(d)
@ -136,8 +137,6 @@ func (l *Loader) executeTemplate(key string, d string, args []string) (string, e
return "", fmt.Errorf("error parsing template %q: %v", key, err)
}
t.Option("missingkey=zero")
var buffer bytes.Buffer
err = t.ExecuteTemplate(&buffer, key, context)
if err != nil {
@ -157,7 +156,6 @@ func (l *Loader) Build(baseDir string) (map[string]fi.Task, error) {
DefaultHandler: ignoreHandler,
Contexts: map[string]loader.Handler{
"resources": ignoreHandler,
"pki": ignoreHandler,
},
Extensions: map[string]loader.Handler{
".options": l.OptionsLoader.HandleOptions,
@ -180,7 +178,6 @@ func (l *Loader) Build(baseDir string) (map[string]fi.Task, error) {
DefaultHandler: l.objectHandler,
Contexts: map[string]loader.Handler{
"resources": l.resourceHandler,
"pki": l.pkiHandler,
},
Extensions: map[string]loader.Handler{
".options": ignoreHandler,
@ -285,27 +282,6 @@ func (l *Loader) resourceHandler(i *loader.TreeWalkItem) error {
return nil
}
func (l *Loader) pkiHandler(i *loader.TreeWalkItem) error {
contents, err := i.ReadString()
if err != nil {
return err
}
key := i.RelativePath
contents, err = l.executeTemplate(key, contents, nil)
if err != nil {
return err
}
task, err := fitasks.NewPKIKeyPairTask(key, contents, "")
if err != nil {
return err
}
l.tasks["pki/"+i.RelativePath] = task
return nil
}
func (l *Loader) objectHandler(i *loader.TreeWalkItem) error {
contents, err := i.ReadString()
if err != nil {

View File

@ -0,0 +1,136 @@
package fitasks
import (
"bytes"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"github.com/golang/glog"
"strings"
)
func pkixNameToString(name *pkix.Name) string {
seq := name.ToRDNSequence()
var s bytes.Buffer
for _, rdnSet := range seq {
for _, rdn := range rdnSet {
if s.Len() != 0 {
s.WriteString(",")
}
key := ""
t := rdn.Type
if len(t) == 4 && t[0] == 2 && t[1] == 5 && t[2] == 4 {
switch t[3] {
case 3:
key = "cn"
case 5:
key = "serial"
case 6:
key = "c"
case 7:
key = "l"
case 10:
key = "o"
case 11:
key = "ou"
}
}
if key == "" {
key = t.String()
}
s.WriteString(fmt.Sprintf("%v=%v", key, rdn.Value))
}
}
return s.String()
}
func parsePkixName(s string) (*pkix.Name, error) {
name := new(pkix.Name)
tokens := strings.Split(s, ",")
for _, token := range tokens {
token = strings.TrimSpace(token)
kv := strings.SplitN(token, "=", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("unrecognized token (expected k=v): %q", token)
}
k := strings.ToLower(kv[0])
v := kv[1]
switch k {
case "cn":
name.CommonName = v
default:
return nil, fmt.Errorf("unrecognized key %q in token %q", k, token)
}
}
return name, nil
}
var keyUsageStrings = map[x509.KeyUsage]string{
x509.KeyUsageDigitalSignature: "KeyUsageDigitalSignature",
x509.KeyUsageContentCommitment: "KeyUsageContentCommitment",
x509.KeyUsageKeyEncipherment: "KeyUsageKeyEncipherment",
x509.KeyUsageDataEncipherment: "KeyUsageDataEncipherment",
x509.KeyUsageKeyAgreement: "KeyUsageKeyAgreement",
x509.KeyUsageCertSign: "KeyUsageCertSign",
x509.KeyUsageCRLSign: "KeyUsageCRLSign",
x509.KeyUsageEncipherOnly: "KeyUsageEncipherOnly",
x509.KeyUsageDecipherOnly: "KeyUsageDecipherOnly",
}
func keyUsageToString(u x509.KeyUsage) []string {
var usages []string
for k, v := range keyUsageStrings {
if (u & k) != 0 {
usages = append(usages, v)
}
}
// TODO: Detect if there are other flags set?
return usages
}
func parseKeyUsage(s string) (x509.KeyUsage, bool) {
for k, v := range keyUsageStrings {
if v == s {
return k, true
}
}
return 0, false
}
var extKeyUsageStrings = map[x509.ExtKeyUsage]string{
x509.ExtKeyUsageAny: "ExtKeyUsageAny",
x509.ExtKeyUsageServerAuth: "ExtKeyUsageServerAuth",
x509.ExtKeyUsageClientAuth: "ExtKeyUsageClientAuth",
x509.ExtKeyUsageCodeSigning: "ExtKeyUsageCodeSigning",
x509.ExtKeyUsageEmailProtection: "ExtKeyUsageEmailProtection",
x509.ExtKeyUsageIPSECEndSystem: "ExtKeyUsageIPSECEndSystem",
x509.ExtKeyUsageIPSECTunnel: "ExtKeyUsageIPSECTunnel",
x509.ExtKeyUsageIPSECUser: "ExtKeyUsageIPSECUser",
x509.ExtKeyUsageTimeStamping: "ExtKeyUsageTimeStamping",
x509.ExtKeyUsageOCSPSigning: "ExtKeyUsageOCSPSigning",
x509.ExtKeyUsageMicrosoftServerGatedCrypto: "ExtKeyUsageMicrosoftServerGatedCrypto",
x509.ExtKeyUsageNetscapeServerGatedCrypto: "ExtKeyUsageNetscapeServerGatedCrypto",
}
func extKeyUsageToString(u x509.ExtKeyUsage) string {
s := extKeyUsageStrings[u]
if s == "" {
glog.Warningf("Unhandled ExtKeyUsage: %v", u)
s = fmt.Sprintf("ExtKeyUsage:%v", u)
}
return s
}
func parseExtKeyUsage(s string) (x509.ExtKeyUsage, bool) {
for k, v := range extKeyUsageStrings {
if v == s {
return k, true
}
}
return 0, false
}

View File

@ -0,0 +1,239 @@
package fitasks
import (
"crypto/x509"
"fmt"
"github.com/golang/glog"
"k8s.io/kube-deploy/upup/pkg/fi"
"net"
"sort"
"strings"
)
var wellKnownCertificateTypes = map[string]string{
"client": "ExtKeyUsageClientAuth,KeyUsageDigitalSignature",
"server": "ExtKeyUsageServerAuth,KeyUsageDigitalSignature,KeyUsageKeyEncipherment",
}
type Keypair struct {
Name string
Subject string `json:"subject"`
Type string `json:"type"`
AlternateNames []string `json:"alternateNames"`
AlternateNameTasks []fi.Task `json:"alternateNameTasks"`
}
func (e *Keypair) String() string {
return fi.TaskAsString(e)
}
func (e *Keypair) Find(c *fi.Context) (*Keypair, error) {
castore := c.CAStore
cert, err := castore.FindCert(e.Name)
if err != nil {
return nil, err
}
if cert == nil {
return nil, nil
}
key, err := castore.FindPrivateKey(e.Name)
if err != nil {
return nil, err
}
if key == nil {
return nil, fmt.Errorf("found cert in store, but did not find private key: %q", e.Name)
}
var alternateNames []string
alternateNames = append(alternateNames, cert.Certificate.DNSNames...)
alternateNames = append(alternateNames, cert.Certificate.EmailAddresses...)
for _, ip := range cert.Certificate.IPAddresses {
alternateNames = append(alternateNames, ip.String())
}
sort.Strings(alternateNames)
actual := &Keypair{
Name: e.Name,
Subject: pkixNameToString(&cert.Subject),
AlternateNames: alternateNames,
Type: buildTypeDescription(cert.Certificate),
}
return actual, nil
}
func (e *Keypair) Run(c *fi.Context) error {
err := e.normalize(c)
if err != nil {
return err
}
return fi.DefaultDeltaRunMethod(e, c)
}
func (e *Keypair) normalize(c *fi.Context) error {
var alternateNames []string
for _, s := range e.AlternateNames {
s = strings.TrimSpace(s)
if s == "" {
continue
}
alternateNames = append(alternateNames, s)
}
for _, task := range e.AlternateNameTasks {
if hasAddress, ok := task.(fi.HasAddress); ok {
address, err := hasAddress.FindAddress(c)
if err != nil {
return fmt.Errorf("error finding address for %v: %v", task, err)
}
if address == nil {
glog.Warningf("Task did not have an address: %v", task)
continue
}
glog.V(8).Infof("Resolved alternateName %q for %q", *address, task)
alternateNames = append(alternateNames, *address)
} else {
return fmt.Errorf("Unsupported type for AlternateNameDependencies: %v", task)
}
}
sort.Strings(alternateNames)
e.AlternateNames = alternateNames
e.AlternateNameTasks = nil
return nil
}
func (s *Keypair) CheckChanges(a, e, changes *Keypair) error {
if a != nil {
if changes.Name != "" {
return fi.CannotChangeField("Name")
}
}
return nil
}
func (_ *Keypair) Render(c *fi.Context, a, e, changes *Keypair) error {
castore := c.CAStore
template, err := buildCertificateTempate(e.Type)
if err != nil {
return err
}
subjectPkix, err := parsePkixName(e.Subject)
if err != nil {
return fmt.Errorf("error parsing Subject: %v", err)
}
if len(subjectPkix.ToRDNSequence()) == 0 {
return fmt.Errorf("Subject name was empty for SSL keypair %q", e.Name)
}
template.Subject = *subjectPkix
var alternateNames []string
alternateNames = append(alternateNames, e.AlternateNames...)
for _, san := range alternateNames {
san = strings.TrimSpace(san)
if san == "" {
continue
}
if ip := net.ParseIP(san); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, san)
}
}
createCertificate := false
if a == nil {
createCertificate = true
} else if changes != nil {
if changes.AlternateNames != nil {
createCertificate = true
} else {
glog.Warningf("Ignoring changes in key: %v", fi.DebugAsJsonString(changes))
}
}
if createCertificate {
glog.V(2).Infof("Creating PKI keypair %q", e.Name)
// TODO: Reuse private key if already exists?
privateKey, err := castore.CreatePrivateKey(e.Name)
if err != nil {
return err
}
cert, err := castore.IssueCert(e.Name, privateKey, template)
if err != nil {
return err
}
glog.V(8).Infof("created certificate %v", cert)
}
// TODO: Check correct subject / flags
return nil
}
func buildCertificateTempate(certificateType string) (*x509.Certificate, error) {
if expanded, found := wellKnownCertificateTypes[certificateType]; found {
certificateType = expanded
}
template := &x509.Certificate{
BasicConstraintsValid: true,
IsCA: false,
}
tokens := strings.Split(certificateType, ",")
for _, t := range tokens {
if strings.HasPrefix(t, "KeyUsage") {
ku, found := parseKeyUsage(t)
if !found {
return nil, fmt.Errorf("unrecognized certificate option: %v", t)
}
template.KeyUsage |= ku
} else if strings.HasPrefix(t, "ExtKeyUsage") {
ku, found := parseExtKeyUsage(t)
if !found {
return nil, fmt.Errorf("unrecognized certificate option: %v", t)
}
template.ExtKeyUsage = append(template.ExtKeyUsage, ku)
} else {
return nil, fmt.Errorf("unrecognized certificate option: %v", t)
}
}
return template, nil
}
func buildTypeDescription(cert *x509.Certificate) string {
var options []string
if cert.IsCA {
options = append(options, "CA")
}
options = append(options, keyUsageToString(cert.KeyUsage)...)
for _, extKeyUsage := range cert.ExtKeyUsage {
options = append(options, extKeyUsageToString(extKeyUsage))
}
sort.Strings(options)
s := strings.Join(options, ",")
for k, v := range wellKnownCertificateTypes {
if v == s {
s = k
}
}
return s
}

View File

@ -1,116 +0,0 @@
package fitasks
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"github.com/golang/glog"
"k8s.io/kube-deploy/upup/pkg/fi"
"k8s.io/kube-deploy/upup/pkg/fi/utils"
"net"
"strings"
)
const (
CertificateType_Client string = "client"
CertificateType_Server string = "server"
)
type PKIKeyPairTask struct {
Name string
Subject *pkix.Name `json:"subject"`
Type string `json:"type"`
AlternateNames []string `json:"alternateNames"`
}
func (t *PKIKeyPairTask) String() string {
return fmt.Sprintf("PKI: %s", t.Name)
}
func NewPKIKeyPairTask(name string, contents string, meta string) (fi.Task, error) {
t := &PKIKeyPairTask{Name: name}
if contents != "" {
err := utils.YamlUnmarshal([]byte(contents), t)
if err != nil {
return nil, fmt.Errorf("error parsing data for PKIKeyPairTask %q: %v", name, err)
}
}
if meta != "" {
return nil, fmt.Errorf("meta is not supported for PKIKeyPairTask")
}
return t, nil
}
func (t *PKIKeyPairTask) Run(c *fi.Context) error {
castore := c.CAStore
cert, err := castore.FindCert(t.Name)
if err != nil {
return err
}
if cert != nil {
key, err := castore.FindPrivateKey(t.Name)
if err != nil {
return err
}
if key == nil {
return fmt.Errorf("found cert in store, but did not find keypair: %q", t.Name)
}
}
if cert == nil {
glog.V(2).Infof("Creating PKI keypair %q", t.Name)
template := &x509.Certificate{
Subject: *t.Subject,
BasicConstraintsValid: true,
IsCA: false,
}
if len(t.Subject.ToRDNSequence()) == 0 {
return fmt.Errorf("Subject name was empty for SSL keypair %q", t.Name)
}
switch t.Type {
case CertificateType_Client:
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
template.KeyUsage = x509.KeyUsageDigitalSignature
break
case CertificateType_Server:
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
break
default:
return fmt.Errorf("unknown certificate type: %q", t.Type)
}
for _, san := range t.AlternateNames {
san = strings.TrimSpace(san)
if san == "" {
continue
}
if ip := net.ParseIP(san); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, san)
}
}
privateKey, err := castore.CreatePrivateKey(t.Name)
if err != nil {
return err
}
cert, err = castore.IssueCert(t.Name, privateKey, template)
if err != nil {
return err
}
}
// TODO: Check correct subject / flags
return nil
}