Create nodetasks.IssueCert()

This commit is contained in:
John Gardiner Myers 2020-05-21 22:37:01 -07:00
parent 367177f272
commit 8b9145f6c4
11 changed files with 247 additions and 129 deletions

View File

@ -18,13 +18,10 @@ package model
import (
"crypto/x509/pkix"
"fmt"
"os"
"path/filepath"
"k8s.io/klog"
"k8s.io/kops/pkg/pki"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/nodeup/nodetasks"
)
// EtcdManagerTLSBuilder configures TLS support for etcd-manager
@ -69,48 +66,24 @@ func (b *EtcdManagerTLSBuilder) Build(ctx *fi.ModelBuilderContext) error {
}
// We also dynamically generate the client keypair for apiserver
if err := b.buildKubeAPIServerKeypair(); err != nil {
if err := b.buildKubeAPIServerKeypair(ctx); err != nil {
return err
}
return nil
}
func (b *EtcdManagerTLSBuilder) buildKubeAPIServerKeypair() error {
req := &pki.IssueCertRequest{
func (b *EtcdManagerTLSBuilder) buildKubeAPIServerKeypair(c *fi.ModelBuilderContext) error {
name := "etcd-client"
issueCert := &nodetasks.IssueCert{
Name: name,
Signer: "etcd-clients-ca",
Type: "client",
Subject: pkix.Name{
CommonName: "kube-apiserver",
},
MinValidDays: 455,
}
dir := "/etc/kubernetes/pki/kube-apiserver"
name := "etcd-client"
humanName := dir + "/" + name
klog.Infof("signing certificate for %q", humanName)
cert, privateKey, etcdClientsCACertificate, err := pki.IssueCert(req, b.KeyStore)
if err != nil {
return fmt.Errorf("error signing certificate for %q: %v", humanName, err)
}
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("error creating directories %q: %v", dir, err)
}
{
p := filepath.Join(dir, "etcd-ca.crt")
if err := etcdClientsCACertificate.WriteToFile(p, 0644); err != nil {
return fmt.Errorf("error writing certificate key file %q: %v", p, err)
}
}
p := filepath.Join(dir, name)
if err := cert.WriteToFile(p+".crt", 0644); err != nil {
return fmt.Errorf("error writing certificate key file %q: %v", p+".crt", err)
}
if err := privateKey.WriteToFile(p+".key", 0600); err != nil {
return fmt.Errorf("error writing private key file %q: %v", p+".key", err)
}
c.AddTask(issueCert)
issueCert.AddFileTasks(c, "/etc/kubernetes/pki/kube-apiserver", name, "etcd-ca", nil)
return nil
}

View File

@ -19,12 +19,9 @@ package model
import (
"crypto/x509/pkix"
"fmt"
"path/filepath"
corev1 "k8s.io/api/core/v1"
"k8s.io/klog"
"k8s.io/kops/pkg/apis/nodeup"
"k8s.io/kops/pkg/pki"
"k8s.io/kops/pkg/wellknownusers"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/nodeup/nodetasks"
@ -85,63 +82,16 @@ func (b *KubeAPIServerBuilder) addHealthcheckSidecarTasks(c *fi.ModelBuilderCont
})
}
req := &pki.IssueCertRequest{
issueCert := &nodetasks.IssueCert{
Name: id,
Signer: fi.CertificateId_CA,
Type: "client",
Subject: pkix.Name{
CommonName: id,
},
MinValidDays: 455,
}
klog.Infof("signing certificate for %q", id)
clientCert, clientKey, _, err := pki.IssueCert(req, b.KeyStore)
if err != nil {
return err
}
c.AddTask(&nodetasks.File{
Path: filepath.Join(secretsDir),
Type: nodetasks.FileType_Directory,
Mode: s("0755"),
})
clientCertBytes, err := clientCert.AsBytes()
if err != nil {
return err
}
c.AddTask(&nodetasks.File{
Path: filepath.Join(secretsDir, "client.crt"),
Contents: fi.NewBytesResource(clientCertBytes),
Type: nodetasks.FileType_File,
Mode: s("0644"),
Owner: s(userName),
})
clientKeyBytes, err := clientKey.AsBytes()
if err != nil {
return err
}
c.AddTask(&nodetasks.File{
Path: filepath.Join(secretsDir, "client.key"),
Contents: fi.NewBytesResource(clientKeyBytes),
Type: nodetasks.FileType_File,
Mode: s("0600"),
Owner: s(userName),
})
cert, err := b.GetCert(fi.CertificateId_CA)
if err != nil {
return err
}
c.AddTask(&nodetasks.File{
Path: filepath.Join(secretsDir, "ca.crt"),
Contents: fi.NewBytesResource(cert),
Type: nodetasks.FileType_File,
Mode: s("0644"),
Owner: s(userName),
})
c.AddTask(issueCert)
issueCert.AddFileTasks(c, secretsDir, "client", "ca", s(userName))
return nil
}

View File

@ -16,7 +16,6 @@ go_library(
"//nodeup/pkg/model:go_default_library",
"//pkg/apis/kops:go_default_library",
"//pkg/model/components:go_default_library",
"//pkg/pki:go_default_library",
"//pkg/systemd:go_default_library",
"//upup/pkg/fi:go_default_library",
"//upup/pkg/fi/nodeup/nodetasks:go_default_library",

View File

@ -19,14 +19,10 @@ package networking
import (
"crypto/x509/pkix"
"fmt"
"os"
"path/filepath"
"k8s.io/kops/nodeup/pkg/model"
"golang.org/x/sys/unix"
"k8s.io/klog"
"k8s.io/kops/pkg/pki"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/nodeup/nodetasks"
)
@ -127,41 +123,17 @@ func (b *CiliumBuilder) buildCiliumEtcdSecrets(c *fi.ModelBuilderContext) error
}
}
req := &pki.IssueCertRequest{
name := "etcd-client-cilium"
issueCert := &nodetasks.IssueCert{
Name: name,
Signer: "etcd-clients-ca-cilium",
Type: "client",
Subject: pkix.Name{
CommonName: "cilium",
},
MinValidDays: 455,
}
dir := "/etc/kubernetes/pki/cilium"
name := "etcd-client-cilium"
humanName := dir + "/" + name
klog.Infof("signing certificate for %q", humanName)
cert, privateKey, etcdClientsCACertificate, err := pki.IssueCert(req, b.KeyStore)
if err != nil {
return fmt.Errorf("error signing certificate for %q: %v", humanName, err)
}
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("error creating directories %q: %v", dir, err)
}
{
p := filepath.Join(dir, "etcd-ca.crt")
if err := etcdClientsCACertificate.WriteToFile(p, 0644); err != nil {
return fmt.Errorf("error writing certificate key file %q: %v", p, err)
}
}
p := filepath.Join(dir, name)
if err := cert.WriteToFile(p+".crt", 0644); err != nil {
return fmt.Errorf("error writing certificate key file %q: %v", p+".crt", err)
}
if err := privateKey.WriteToFile(p+".key", 0600); err != nil {
return fmt.Errorf("error writing private key file %q: %v", p+".key", err)
}
c.AddTask(issueCert)
issueCert.AddFileTasks(c, "/etc/kubernetes/pki/cilium", name, "etcd-ca", nil)
return nil
}

View File

@ -199,6 +199,8 @@ func (c *NodeUpCommand) Run(out io.Writer) error {
NodeupConfig: c.config,
}
var secretStore fi.SecretStore
var keyStore fi.Keystore
if c.cluster.Spec.SecretStore != "" {
klog.Infof("Building SecretStore at %q", c.cluster.Spec.SecretStore)
p, err := vfs.Context.BuildVfsPath(c.cluster.Spec.SecretStore)
@ -206,7 +208,8 @@ func (c *NodeUpCommand) Run(out io.Writer) error {
return fmt.Errorf("error building secret store path: %v", err)
}
modelContext.SecretStore = secrets.NewVFSSecretStore(c.cluster, p)
secretStore = secrets.NewVFSSecretStore(c.cluster, p)
modelContext.SecretStore = secretStore
} else {
return fmt.Errorf("SecretStore not set")
}
@ -219,6 +222,7 @@ func (c *NodeUpCommand) Run(out io.Writer) error {
}
modelContext.KeyStore = fi.NewVFSCAStore(c.cluster, p)
keyStore = modelContext.KeyStore
} else {
return fmt.Errorf("KeyStore not set")
}
@ -285,8 +289,6 @@ func (c *NodeUpCommand) Run(out io.Writer) error {
// Protokube load image task is in ProtokubeBuilder
var cloud fi.Cloud
var keyStore fi.Keystore
var secretStore fi.SecretStore
var target fi.Target
checkExisting := true

View File

@ -10,6 +10,7 @@ go_library(
"createsdir.go",
"file.go",
"group.go",
"issue_cert.go",
"load_image.go",
"package.go",
"service.go",
@ -20,6 +21,7 @@ go_library(
visibility = ["//visibility:public"],
deps = [
"//pkg/backoff:go_default_library",
"//pkg/pki:go_default_library",
"//upup/pkg/fi:go_default_library",
"//upup/pkg/fi/nodeup/cloudinit:go_default_library",
"//upup/pkg/fi/nodeup/local:go_default_library",
@ -37,9 +39,13 @@ go_test(
"archive_test.go",
"bindmount_test.go",
"file_test.go",
"issue_cert_test.go",
"loadimage_test.go",
"service_test.go",
],
embed = [":go_default_library"],
deps = ["//upup/pkg/fi:go_default_library"],
deps = [
"//upup/pkg/fi:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)

View File

@ -98,6 +98,10 @@ func (e *File) GetDependencies(tasks map[string]fi.Task) []fi.Task {
// Requires parent directories to be created
deps = append(deps, findCreatesDirParents(e.Path, tasks)...)
if hasDep, ok := e.Contents.(fi.HasDependencies); ok {
deps = append(deps, hasDep.GetDependencies(tasks)...)
}
// Requires other files to be created first
for _, f := range e.AfterFiles {
for _, v := range tasks {

View File

@ -0,0 +1,140 @@
/*
Copyright 2020 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 nodetasks
import (
"bytes"
"crypto/x509/pkix"
"fmt"
"io"
"path/filepath"
"k8s.io/klog"
"k8s.io/kops/pkg/pki"
"k8s.io/kops/upup/pkg/fi"
)
type IssueCert struct {
Name string
Signer string `json:"signer"`
Type string `json:"type"`
Subject pkix.Name `json:"subject"`
AlternateNames []string `json:"alternateNames,omitempty"`
cert *fi.TaskDependentResource
key *fi.TaskDependentResource
ca *fi.TaskDependentResource
}
var _ fi.Task = &IssueCert{}
var _ fi.HasName = &IssueCert{}
func (i *IssueCert) GetName() *string {
return &i.Name
}
func (i *IssueCert) SetName(name string) {
i.Name = name
}
// String returns a string representation, implementing the Stringer interface
func (i *IssueCert) String() string {
return fmt.Sprintf("IssueCert: %s", i.Name)
}
func (i *IssueCert) GetResources() (certResource, keyResource, caResource *fi.TaskDependentResource) {
if i.cert == nil {
i.cert = &fi.TaskDependentResource{Task: i}
i.key = &fi.TaskDependentResource{Task: i}
i.ca = &fi.TaskDependentResource{Task: i}
}
return i.cert, i.key, i.ca
}
func (i *IssueCert) AddFileTasks(c *fi.ModelBuilderContext, dir string, name string, caName string, owner *string) {
certResource, keyResource, caResource := i.GetResources()
c.AddTask(&File{
Path: dir,
Type: FileType_Directory,
Mode: fi.String("0755"),
})
c.AddTask(&File{
Path: filepath.Join(dir, name+".crt"),
Contents: certResource,
Type: FileType_File,
Mode: fi.String("0644"),
Owner: owner,
})
c.AddTask(&File{
Path: filepath.Join(dir, name+".key"),
Contents: keyResource,
Type: FileType_File,
Mode: fi.String("0600"),
Owner: owner,
})
if caName != "" {
c.AddTask(&File{
Path: filepath.Join(dir, caName+".crt"),
Contents: caResource,
Type: FileType_File,
Mode: fi.String("0644"),
Owner: owner,
})
}
}
func (e *IssueCert) Run(c *fi.Context) error {
req := &pki.IssueCertRequest{
Signer: e.Signer,
Type: e.Type,
Subject: e.Subject,
MinValidDays: 455,
}
klog.Infof("signing certificate for %q", e.Name)
certificate, privateKey, caCertificate, err := pki.IssueCert(req, c.Keystore)
if err != nil {
return err
}
certResource, keyResource, caResource := e.GetResources()
certResource.Resource = &asBytesResource{certificate}
keyResource.Resource = &asBytesResource{privateKey}
caResource.Resource = &asBytesResource{caCertificate}
return nil
}
type hasAsBytes interface {
AsBytes() ([]byte, error)
}
type asBytesResource struct {
hasAsBytes
}
func (a asBytesResource) Open() (io.Reader, error) {
data, err := a.AsBytes()
if err != nil {
return nil, err
}
return bytes.NewReader(data), nil
}

View File

@ -0,0 +1,53 @@
/*
Copyright 2020 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 nodetasks
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/kops/upup/pkg/fi"
)
func TestIssueCertFileDependencies(t *testing.T) {
context := &fi.ModelBuilderContext{
Tasks: make(map[string]fi.Task),
}
issue := &IssueCert{Name: "testCert"}
context.AddTask(issue)
issue.AddFileTasks(context, "/tmp", "testCert", "testCa", nil)
var taskNames []string
for name := range context.Tasks {
taskNames = append(taskNames, name)
}
assert.ElementsMatch(t, []string{"IssueCert/testCert", "File//tmp", "File//tmp/testCert.crt", "File//tmp/testCert.key", "File//tmp/testCa.crt"}, taskNames)
for _, fileName := range []string{"/tmp/testCert.crt", "/tmp/testCert.key", "/tmp/testCa.crt"} {
task := context.Tasks["File/"+fileName]
if !assert.NotNil(t, task) {
continue
}
deps := task.(fi.HasDependencies).GetDependencies(context.Tasks)
taskNames = nil
for _, task := range deps {
taskNames = append(taskNames, *task.(fi.HasName).GetName())
}
assert.ElementsMatch(t, []string{"testCert", "/tmp"}, taskNames)
}
}

View File

@ -64,13 +64,13 @@ func (p *Service) GetDependencies(tasks map[string]fi.Task) []fi.Task {
var deps []fi.Task
for _, v := range tasks {
// We assume that services depend on everything except for
// LoadImageTask. If there are any LoadImageTasks (e.g. we're
// LoadImageTask or IssueCert. If there are any LoadImageTasks (e.g. we're
// launching a custom Kubernetes build), they all depend on
// the "docker.service" Service task.
switch v.(type) {
case *File, *Package, *UpdatePackages, *UserTask, *GroupTask, *Chattr, *BindMount, *Archive:
deps = append(deps, v)
case *Service, *LoadImageTask:
case *Service, *LoadImageTask, *IssueCert:
// ignore
default:
klog.Warningf("Unhandled type %T in Service::GetDependencies: %v", v, v)

View File

@ -248,3 +248,22 @@ func WrapResource(r Resource) *ResourceHolder {
Resource: r,
}
}
type TaskDependentResource struct {
Resource Resource
Task Task
}
var _ Resource = &TaskDependentResource{}
var _ HasDependencies = &TaskDependentResource{}
func (r *TaskDependentResource) Open() (io.Reader, error) {
if r.Resource == nil {
return nil, fmt.Errorf("resource opened before it is ready")
}
return r.Resource.Open()
}
func (r *TaskDependentResource) GetDependencies(tasks map[string]Task) []Task {
return []Task{r.Task}
}