Merge remote-tracking branch 'origin/master' into azureendpoint

Signed-off-by: Stuart Hu <shijiehu@improbable.io>
This commit is contained in:
Stuart Hu 2019-07-04 15:18:38 +08:00
commit 9bdb275f49
52 changed files with 1197 additions and 419 deletions

1
.bazelversion Normal file
View File

@ -0,0 +1 @@
0.25.3

View File

@ -1,31 +1,19 @@
load("@io_bazel_rules_docker//container:container.bzl", "container_bundle")
load("@io_bazel_rules_docker//contrib:push-all.bzl", "docker_push")
load("//hack:def.bzl", "multiarch_bundle")
# export WORKSPACE so workspace_binary rules can be used outside the root
exports_files(["WORKSPACE"])
# gazelle:proto disable_global
# this target creates targets:
# 'images' (all arch and os)
# 'images.linux-amd64'
# 'images.linux-arm64'
# 'images.linux-arm'
multiarch_bundle(
container_bundle(
name = "images",
arch = [
"amd64",
"arm64",
"arm",
],
images = {
"{STABLE_DOCKER_REPO}/cert-manager-controller-{arch}:{STABLE_DOCKER_TAG}": "//cmd/controller:image",
"{STABLE_DOCKER_REPO}/cert-manager-acmesolver-{arch}:{STABLE_DOCKER_TAG}": "//cmd/acmesolver:image",
"{STABLE_DOCKER_REPO}/cert-manager-webhook-{arch}:{STABLE_DOCKER_TAG}": "//cmd/webhook:image",
"{STABLE_DOCKER_REPO}/cert-manager-cainjector-{arch}:{STABLE_DOCKER_TAG}": "//cmd/cainjector:image",
"{STABLE_DOCKER_REPO}/cert-manager-controller:{STABLE_DOCKER_TAG}": "//cmd/controller:image",
"{STABLE_DOCKER_REPO}/cert-manager-acmesolver:{STABLE_DOCKER_TAG}": "//cmd/acmesolver:image",
"{STABLE_DOCKER_REPO}/cert-manager-webhook:{STABLE_DOCKER_TAG}": "//cmd/webhook:image",
"{STABLE_DOCKER_REPO}/cert-manager-cainjector:{STABLE_DOCKER_TAG}": "//cmd/cainjector:image",
},
os = ["linux"],
)
docker_push(

View File

@ -142,6 +142,7 @@ images:
bazel run //hack/release -- \
--repo-root "$$(pwd)" \
--images \
--images.export=true \
--images.goarch="amd64" \
--app-version="$(APP_VERSION)" \
--docker-repo="$(DOCKER_REPO)"

View File

@ -1,3 +1,8 @@
workspace(
# How this workspace would be referenced with absolute labels from another workspace
name = "cert_manager",
)
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
@ -6,8 +11,11 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "new_git_repository")
## Load rules_go and dependencies
http_archive(
name = "io_bazel_rules_go",
url = "https://github.com/bazelbuild/rules_go/releases/download/0.18.2/rules_go-0.18.2.tar.gz",
sha256 = "31f959ecf3687f6e0bb9d01e1e7a7153367ecd82816c9c0ae149cd0e5a92bf8c",
urls = [
"https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/rules_go/releases/download/0.18.6/rules_go-0.18.6.tar.gz",
"https://github.com/bazelbuild/rules_go/releases/download/0.18.6/rules_go-0.18.6.tar.gz",
],
sha256 = "f04d2373bcaf8aa09bccb08a98a57e721306c8f6043a2a0ee610fd6853dcde3d",
)
load("@io_bazel_rules_go//go:deps.bzl", "go_rules_dependencies", "go_register_toolchains")
@ -36,16 +44,18 @@ gazelle_dependencies()
## Load kubernetes repo-infra for tools like kazel
git_repository(
name = "io_kubernetes_build",
commit = "df02ded38f9506e5bbcbf21702034b4fef815f2f",
remote = "https://github.com/kubernetes/repo-infra.git",
commit = "8036538b4efe3443d25e6325cabdc367a1926f7d",
# Use justinsb's fork until https://github.com/kubernetes/repo-infra/pull/114 merges
remote = "https://github.com/justinsb/repo-infra.git",
shallow_since = "1561083176 -0400",
)
## Load rules_docker and depdencies, for working with docker images
git_repository(
name = "io_bazel_rules_docker",
remote = "https://github.com/bazelbuild/rules_docker.git",
commit = "3732c9d05315bef6a3dbd195c545d6fea3b86880",
shallow_since = "1547471117 +0100",
commit = "80ea3aae060077e5fe0cdef1a5c570d4b7622100",
shallow_since = "1561646721 -0700",
)
load(
@ -114,6 +124,7 @@ container_pull(
registry = "gcr.io",
repository = "kubernetes-helm/tiller",
tag = "v2.10.0",
digest = "sha256:2a3dd484ecfcf9343994e0f6c2af0a6faf1af7f7e499905793643f91e90edcb3",
)
## Install 'kind', for creating kubernetes-in-docker clusters
@ -143,6 +154,9 @@ container_pull(
registry = "quay.io",
repository = "kubernetes-ingress-controller/nginx-ingress-controller",
tag = "0.23.0",
# For some reason, the suggested sha256 returns an error when fetched from
# quay.io by digest.
# digest = "sha256:f7f08fdbbeddaf3179829c662da360a3feac1ecf8c4b1305949fffd8c8f59879",
)
container_pull(
@ -150,6 +164,7 @@ container_pull(
registry = "k8s.gcr.io",
repository = "defaultbackend",
tag = "1.4",
digest = "sha256:865b0c35e6da393b8e80b7e3799f777572399a4cff047eb02a81fa6e7a48ed4b",
)
## Fetch vault for use during e2e tests
@ -160,6 +175,7 @@ container_pull(
registry = "index.docker.io",
repository = "library/vault",
tag = "0.9.3",
digest = "sha256:27a564c725f4f6fa72a618add6b0c3294431ed6b5e912ee042822b35b91064c3",
)
## Fetch kind images used during e2e tests
@ -182,6 +198,7 @@ container_pull(
registry = "index.docker.io",
repository = "kindest/node",
tag = "v1.13.4",
digest = "sha256:842ffccc3ba7674f71815d40fdfd18bc8a98d18130dcfd58bc15c857593f1e15",
)
## Fetch kubectl for use during e2e tests
@ -322,7 +339,10 @@ new_git_repository(
# We use this specific revision as it contains changes that allow us to
# specify custom paths when building documentation.
commit = "28714834053271ebb5a6a5fe22af29f98fc0b6d0",
shallow_since = "1556994488 +0100",
build_file_content = """
exports_files(["brodoc.js"])
filegroup(
name = "all-srcs",
srcs = glob(["**/*"]),
@ -342,28 +362,32 @@ filegroup(
""",
)
# Setup npm for brodocs doc generation
git_repository(
# Install the nodejs "bootstrap" package
# This provides the basic tools for running and packaging nodejs programs in Bazel
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "build_bazel_rules_nodejs",
remote = "https://github.com/bazelbuild/rules_nodejs.git",
commit = "11271418a6bbd2529170270a7e61dcc5167bb16d",
shallow_since = "1554849870 -0700",
sha256 = "395b7568f20822c13fc5abc65b1eced637446389181fda3a108fdd6ff2cac1e9",
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.29.2/rules_nodejs-0.29.2.tar.gz"],
)
load("@build_bazel_rules_nodejs//:defs.bzl", "node_repositories")
# TODO: do we need to specify this package.json in node_repositories as well as
# in npm_install?
node_repositories(package_json = ["@brodocs//:package.json"])
# The npm_install rule runs yarn anytime the package.json or package-lock.json file changes.
# It also extracts any Bazel rules distributed in an npm package.
load("@build_bazel_rules_nodejs//:defs.bzl", "npm_install")
npm_install(
# Name this npm so that Bazel Label references look like @brodocs_modules//package
name = "brodocs_modules",
package_json = "@brodocs//:package.json",
package_lock_json = "//docs/generated/reference/generate/bin:package-lock.json",
)
# Install any Bazel rules which were extracted earlier by the npm_install rule.
load("@brodocs_modules//:install_bazel_dependencies.bzl", "install_bazel_dependencies")
install_bazel_dependencies()
# Load the controller-tools repository in order to build the crd generator tool
go_repository(
name = "io_kubernetes_sigs_controller-tools",

View File

@ -1,9 +1,9 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
load("//hack:def.bzl", "multiarch_image")
load("//hack:def.bzl", "image")
# Expands to target names such as 'image.linux-amd64', 'image.linux-arm64'
multiarch_image(
image(
name = "image",
binary = ":acmesolver",
component = "acmesolver",
visibility = ["//visibility:public"],
)
@ -22,6 +22,7 @@ go_library(
go_binary(
name = "acmesolver",
embed = [":go_default_library"],
pure = "on",
visibility = ["//visibility:public"],
)

View File

@ -1,9 +1,9 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
load("//hack:def.bzl", "multiarch_image")
load("//hack:def.bzl", "image")
# Expands to target names such as 'image.linux-amd64', 'image.linux-arm64'
multiarch_image(
image(
name = "image",
binary = ":cainjector",
component = "cainjector",
visibility = ["//visibility:public"],
)
@ -32,6 +32,7 @@ go_library(
go_binary(
name = "cainjector",
embed = [":go_default_library"],
pure = "on",
visibility = ["//visibility:public"],
)

View File

@ -1,9 +1,9 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
load("//hack:def.bzl", "multiarch_image")
load("//hack:def.bzl", "image")
# Expands to target names such as 'image.linux-amd64', 'image.linux-arm64'
multiarch_image(
image(
name = "image",
binary = ":controller",
component = "controller",
visibility = ["//visibility:public"],
)
@ -43,6 +43,7 @@ go_library(
go_binary(
name = "controller",
embed = [":go_default_library"],
pure = "on",
visibility = ["//visibility:public"],
)

View File

@ -1,9 +1,9 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
load("//hack:def.bzl", "multiarch_image")
load("//hack:def.bzl", "image")
# Expands to target names such as 'image.linux-amd64', 'image.linux-arm64'
multiarch_image(
image(
name = "image",
binary = ":webhook",
component = "webhook",
visibility = ["//visibility:public"],
)
@ -23,6 +23,7 @@ go_library(
go_binary(
name = "webhook",
embed = [":go_default_library"],
pure = "on",
visibility = ["//visibility:public"],
)

View File

@ -24,6 +24,9 @@ rules:
- apiGroups: ["apiregistration.k8s.io"]
resources: ["apiservices"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding

View File

@ -750,6 +750,11 @@ spec:
properties:
acme:
properties:
lastRegisteredEmail:
description: LastRegisteredEmail is the email associated with the
latest registered ACME account, in order to track changes made
to registered account associated with the Issuer
type: string
uri:
description: URI is the unique account identifier, which can also
be used to retrieve account details from the CA
@ -1029,6 +1034,11 @@ spec:
properties:
acme:
properties:
lastRegisteredEmail:
description: LastRegisteredEmail is the email associated with the
latest registered ACME account, in order to track changes made
to registered account associated with the Issuer
type: string
uri:
description: URI is the unique account identifier, which can also
be used to retrieve account details from the CA

View File

@ -82,6 +82,10 @@ Deploy that version with helm
$ helm install \
--set image.tag=canary \
--set image.pullPolicy=Never \
--set cainjector.image.tag=canary \
--set cainjector.pullPolicy=Never \
--set webhook.image.tag=canary \
--set webhook.pullPolicy=Never \
--name cert-manager \
./deploy/charts/cert-manager

View File

@ -58,11 +58,11 @@ genrule(
srcs = [
":__internal_brodocs_out",
"@brodocs//:static",
"@brodocs_modules//node_modules/jquery:jquery__files",
"@brodocs_modules//node_modules/bootstrap:bootstrap__files",
"@brodocs_modules//node_modules/font-awesome:font-awesome__files",
"@brodocs_modules//node_modules/highlight.js:highlight.js__files",
"@brodocs_modules//node_modules/jquery.scrollto:jquery.scrollto__files",
"@brodocs_modules//jquery:jquery__contents",
"@brodocs_modules//bootstrap:bootstrap__contents",
"@brodocs_modules//font-awesome:font-awesome__contents",
"@brodocs_modules//highlight.js:highlight.js__contents",
"@brodocs_modules//jquery.scrollto:jquery.scrollto__contents",
],
outs = ["generated.tar.gz"],
cmd = "; ".join([

View File

@ -45,7 +45,7 @@ Certificate resources:
to issue a self signed root CA certificate.
* certificate/cert-manager-webhook-ca - A self-signed root CA certificate
which is used to sign certificates for the webhook pod.
* issue/cert-manager-webhook-ca - A CA Issuer that is used to issue
* issuer/cert-manager-webhook-ca - A CA Issuer that is used to issue
certificates used by the webhook pod to serve with.
* certificate/cert-manager-webhook-webhook-tls - A TLS certificate issued by the
root CA above, served by the webhook.

View File

@ -76,7 +76,7 @@ The Certificate will be issued using the issuer named ``ca-issuer`` in the
issues, leading to the working duration of a certificate to be less than
the full duration of the certificate. For example, Let's Encrypt sets it
to be one hour before issue time, so the actual *working duration* of the
certificate is 89 days, 23 hours (the *full duration* remains 90 days).
certificate is 89 days, 23 hours (the *full duration* remains 90 days).
A full list of the fields supported on the Certificate resource can be found in
the `API reference documentation`_.
@ -101,6 +101,9 @@ After the real, valid certificate has been obtained, cert-manager will replace
the temporary self signed certificate with the valid one, **but will retain the
same private key**.
You can disable issuing temporary certificate by setting feature gate flag
``--feature-gates=IssueTemporaryCertificate=false``
.. toctree::
:maxdepth: 2

View File

@ -16,98 +16,30 @@ load("@io_bazel_rules_docker//container:image.bzl", "container_image")
load("@io_bazel_rules_docker//container:bundle.bzl", "container_bundle")
load("@io_bazel_rules_docker//go:image.bzl", "go_image")
## stamped_image is a macro for creating :app and :image targets
def stamped_image(
name, # use "image"
base = None,
user = "1000",
stamp = True, # stamp by default, but allow overrides
**kwargs):
go_image(
name = "%s.app" % name,
base = base,
embed = [":go_default_library"],
goarch = "amd64",
goos = "linux",
pure = "on",
)
container_image(
name = name,
base = ":%s.app" % name,
user = user,
stamp = stamp,
**kwargs)
def multiarch_image(
def image(
name,
component,
goarch = ["amd64", "arm64", "arm"],
goos = ["linux"],
binary,
user = "1000",
stamp = True,
**kwargs):
for arch in goarch:
for os in goos:
go_image(
name = "%s.app_%s-%s" % (name, os, arch),
base = "@static_base//image",
embed = [":go_default_library"],
goarch = arch,
goos = os,
pure = "on",
)
go_image(
name = "%s.app" % name,
base = "@static_base//image",
binary = binary,
)
container_image(
name = "%s.%s-%s" % (name, os, arch),
base = "%s.app_%s-%s" % (name, os, arch),
user = user,
stamp = stamp,
**kwargs)
suffix = ""
if arch != "amd64":
suffix = "-%s" % arch
container_bundle(
name = "%s.%s-%s.export" % (name, os, arch),
images = {
("{STABLE_DOCKER_REPO}/cert-manager-%s%s:{STABLE_APP_VERSION}" % (component, suffix)): ":%s.%s-%s" % (name, os, arch),
("{STABLE_DOCKER_REPO}/cert-manager-%s%s:{STABLE_APP_GIT_COMMIT}" % (component, suffix)): ":%s.%s-%s" % (name, os, arch),
},
)
container_image(
name = name,
base = "%s.%s-%s" % (name, goos[0], goarch[0]),
**kwargs)
def multiarch_bundle(
name,
images,
os = ["linux"],
arch = ["amd64", "arm64", "arm"],
**kwargs):
all_images = {}
for a in arch:
for o in os:
oa_images = {}
for (k, v) in images.items():
image_name = k.replace("{arch}", a)
image_name = image_name.replace("{os}", o)
oa_images[image_name] = "%s.%s-%s" % (v, o, a)
container_bundle(
name = "%s.%s-%s" % (name, o, a),
images = oa_images,
**kwargs)
all_images.update(oa_images)
container_image(
name = name,
base = "%s.app" % name,
user = user,
stamp = stamp,
**kwargs)
container_bundle(
name = name,
images = all_images,
**kwargs)
name = name + ".export",
images = {
component + ":{STABLE_APP_GIT_COMMIT}": ":" + name,
},
)

View File

@ -68,7 +68,7 @@ func (g *Bazel) Cmd(ctx context.Context, args ...string) *exec.Cmd {
fmt.Sprintf("APP_VERSION=%s", flags.Default.AppVersion),
fmt.Sprintf("APP_GIT_COMMIT=%s", flags.Default.GitCommitRef),
)
log.Info("set command environment variables", "env", cmd.Env)
log.V(logf.LogLevelTrace).Info("set command environment variables", "env", cmd.Env)
cmd.Dir = flags.Default.RepoRoot
return cmd
}

View File

@ -34,7 +34,6 @@ import (
var (
Default = &Plugin{}
supportedGoArch = []string{"amd64", "arm64", "arm"}
supportedComponents = []string{"acmesolver", "controller", "webhook", "cainjector"}
log = logf.Log.WithName("images")
)
@ -72,20 +71,6 @@ func (g *Plugin) AddFlags(fs *flag.FlagSet) {
func (g *Plugin) Validate() []error {
var errs []error
// validate goarch flag
for _, a := range g.GoArch {
valid := false
for _, sa := range supportedGoArch {
if a == sa {
valid = true
break
}
}
if !valid {
errs = append(errs, fmt.Errorf("invalid goarch value %q", a))
}
}
// validate components flag
for _, a := range g.Components {
valid := false
@ -175,19 +160,18 @@ func (g *Plugin) Complete() error {
func (g *Plugin) build(ctx context.Context) (imageTargets, error) {
targets := g.generateTargets()
bazelTargets := targets.bazelTargets()
log := log.WithValues("images", bazelTargets)
log.Info("building bazel image targets")
// only support building docker images for linux for now
os := "linux"
for _, arch := range g.GoArch {
filteredTargets := targets.withOSArch(os, arch)
bazelTargets := filteredTargets.bazelTargets()
log := log.WithValues("images", bazelTargets)
log.Info("building bazel image targets")
// set the os and arch to linux/amd64
// whilst we might be building cross-arch binaries, cgo only depends on
// particular OS settings and not arch, so just by setting this to 'linux'
// we can fix cross builds on platforms other than linux
// if we support alternate OS values in future, this will need updating
// with a call to BuildPlatformE per *OS*.
err := bazel.Default.BuildPlatformE(ctx, log, "linux", "amd64", bazelTargets...)
if err != nil {
return nil, fmt.Errorf("error building docker images (%v): %v", targets, err)
err := bazel.Default.BuildPlatformE(ctx, log, os, arch, bazelTargets...)
if err != nil {
return nil, fmt.Errorf("error building docker images (%v): %v", targets, err)
}
}
g.built = true
@ -197,22 +181,26 @@ func (g *Plugin) build(ctx context.Context) (imageTargets, error) {
func (g *Plugin) exportToDocker(ctx context.Context) error {
targets := g.generateTargets()
log.WithValues("images", targets.bazelExportTargets()).Info("exporting images to docker daemon")
for _, target := range targets.bazelExportTargets() {
log := log.WithValues("target", target)
for _, target := range targets {
log := log.WithValues("target", target.name, "os", target.os, "arch", target.arch)
log.Info("exporting image to docker daemon")
// set the os and arch to linux/amd64
// whilst we might be building cross-arch binaries, cgo only depends on
// particular OS settings and not arch, so just by setting this to 'linux'
// we can fix cross builds on platforms other than linux
// if we support alternate OS values in future, this will need updating
// with a call to BuildPlatformE per *OS*.
err := bazel.Default.RunPlatformE(ctx, log, "linux", "amd64", target)
exportTarget := target.bazelExportTarget()
err := bazel.Default.RunPlatformE(ctx, log, target.os, target.arch, exportTarget)
if err != nil {
return fmt.Errorf("error exporting image %q to docker daemon: %v", target, err)
}
for _, taggedImage := range target.taggedImageNames() {
log.Info("tagging image", "tag", taggedImage)
cmd := exec.CommandContext(ctx, "docker", "tag", target.exportedImageName(), taggedImage)
err := util.RunE(log, cmd)
if err != nil {
return err
}
}
}
log.WithValues("images", targets.exportedImageNames()).Info("exported all docker images")
log.WithValues("images", targets.taggedImageNames()).Info("exported all docker images")
return nil
}
@ -237,10 +225,7 @@ func (p *Plugin) pushImages(ctx context.Context, targets imageTargets) error {
return err
}
var images []string
for _, t := range targets {
images = append(images, t.exportedImageNames()...)
}
images := targets.taggedImageNames()
log.WithValues("images", images).Info("pushing docker images")
for _, img := range images {
@ -279,10 +264,28 @@ func (i imageTargets) bazelExportTargets() []string {
return out
}
func (i imageTargets) taggedImageNames() []string {
out := make([]string, 0)
for _, target := range i {
out = append(out, target.taggedImageNames()...)
}
return out
}
func (i imageTargets) exportedImageNames() []string {
out := make([]string, 0)
for _, target := range i {
out = append(out, target.exportedImageNames()...)
out = append(out, target.taggedImageNames()...)
}
return out
}
func (i imageTargets) withOSArch(os, arch string) imageTargets {
out := make(imageTargets, 0)
for _, target := range i {
if target.os == os && target.arch == arch {
out = append(out, target)
}
}
return out
}
@ -292,14 +295,18 @@ type imageTarget struct {
}
func (i imageTarget) bazelTarget() string {
return fmt.Sprintf("//cmd/%s:image.%s-%s", i.name, i.os, i.arch)
return fmt.Sprintf("//cmd/%s:image", i.name)
}
func (i imageTarget) bazelExportTarget() string {
return fmt.Sprintf("//cmd/%s:image.%s-%s.export", i.name, i.os, i.arch)
return fmt.Sprintf("//cmd/%s:image.export", i.name)
}
func (i imageTarget) exportedImageNames() []string {
func (i imageTarget) exportedImageName() string {
return fmt.Sprintf("%s:%s", i.name, flags.Default.GitCommitRef)
}
func (i imageTarget) taggedImageNames() []string {
if i.arch == "amd64" {
return []string{
fmt.Sprintf("%s/cert-manager-%s:%s", flags.Default.DockerRepo, i.name, flags.Default.AppVersion),

View File

@ -42,6 +42,7 @@ type FakeACME struct {
FakeHTTP01ChallengeResponse func(token string) (string, error)
FakeDNS01ChallengeRecord func(token string) (string, error)
FakeDiscover func(ctx context.Context) (acme.Directory, error)
FakeUpdateAccount func(ctx context.Context, a *acme.Account) (*acme.Account, error)
}
func (f *FakeACME) CreateOrder(ctx context.Context, order *acme.Order) (*acme.Order, error) {
@ -143,3 +144,10 @@ func (f *FakeACME) Discover(ctx context.Context) (acme.Directory, error) {
// empty directory here will be fine
return acme.Directory{}, nil
}
func (f *FakeACME) UpdateAccount(ctx context.Context, a *acme.Account) (*acme.Account, error) {
if f.FakeUpdateAccount != nil {
return f.FakeUpdateAccount(ctx, a)
}
return nil, fmt.Errorf("UpdateAccount not implemented")
}

View File

@ -37,6 +37,7 @@ type Interface interface {
HTTP01ChallengeResponse(token string) (string, error)
DNS01ChallengeRecord(token string) (string, error)
Discover(ctx context.Context) (acme.Directory, error)
UpdateAccount(ctx context.Context, a *acme.Account) (*acme.Account, error)
}
var _ Interface = &acme.Client{}

View File

@ -103,3 +103,8 @@ func (l *Logger) Discover(ctx context.Context) (acme.Directory, error) {
klog.Infof("Calling Discover")
return l.baseCl.Discover(ctx)
}
func (l *Logger) UpdateAccount(ctx context.Context, a *acme.Account) (*acme.Account, error) {
klog.Infof("Calling UpdateAccount")
return l.baseCl.UpdateAccount(ctx, a)
}

View File

@ -8,6 +8,7 @@ go_library(
deps = [
"//pkg/acme/webhook/apis/acme/v1alpha1:go_default_library",
"//pkg/apis/certmanager/v1alpha1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",

View File

@ -17,6 +17,7 @@ limitations under the License.
package api
import (
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -37,6 +38,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{
whapi.AddToScheme,
kscheme.AddToScheme,
apireg.AddToScheme,
apiext.AddToScheme,
}
// AddToScheme adds all types of this clientset into the given scheme. This allows composition

View File

@ -542,6 +542,12 @@ type ACMEIssuerStatus struct {
// account details from the CA
// +optional
URI string `json:"uri,omitempty"`
// LastRegisteredEmail is the email associated with the latest registered
// ACME account, in order to track changes made to registered account
// associated with the Issuer
// +optional
LastRegisteredEmail string `json:"lastRegisteredEmail,omitempty"`
}
// IssuerCondition contains condition information for an Issuer.

View File

@ -17,6 +17,7 @@ go_library(
"//vendor/github.com/go-logr/logr:go_default_library",
"//vendor/k8s.io/api/admissionregistration/v1beta1:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",

View File

@ -144,6 +144,10 @@ func (r *genericInjectReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro
// fetch the target object
target := r.injector.NewTarget()
if err := r.Client.Get(ctx, req.NamespacedName, target.AsObject()); err != nil {
if dropNotFound(err) == nil {
// don't requeue on deletions, which yield a non-found object
return ctrl.Result{}, nil
}
log.Error(err, "unable to fetch target object to inject into")
return ctrl.Result{}, err
}

View File

@ -18,6 +18,7 @@ package cainjector
import (
admissionreg "k8s.io/api/admissionregistration/v1beta1"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
apireg "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
)
@ -93,3 +94,27 @@ func (t *apiServiceTarget) SetCA(data []byte) {
}
// TODO(directxman12): conversion webhooks
// crdConversionInjector knows how to create an InjectTarget for CRD conversion webhooks
type crdConversionInjector struct{}
func (i crdConversionInjector) NewTarget() InjectTarget {
return &crdConversionTarget{}
}
// crdConversionTarget knows how to set CA data for the conversion webhook in CRDs
type crdConversionTarget struct {
obj apiext.CustomResourceDefinition
}
func (t *crdConversionTarget) AsObject() runtime.Object {
return &t.obj
}
func (t *crdConversionTarget) SetCA(data []byte) {
if t.obj.Spec.Conversion == nil || t.obj.Spec.Conversion.Strategy != apiext.WebhookConverter {
return
}
if t.obj.Spec.Conversion.WebhookClientConfig == nil {
t.obj.Spec.Conversion.WebhookClientConfig = &apiext.WebhookClientConfig{}
}
t.obj.Spec.Conversion.WebhookClientConfig.CABundle = data
}

View File

@ -21,6 +21,7 @@ import (
admissionreg "k8s.io/api/admissionregistration/v1beta1"
corev1 "k8s.io/api/core/v1"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
apireg "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
ctrl "sigs.k8s.io/controller-runtime"
@ -56,7 +57,13 @@ var (
listType: &apireg.APIServiceList{},
}
injectorSetups = []injectorSetup{MutatingWebhookSetup, ValidatingWebhookSetup, APIServiceSetup}
CRDSetup = injectorSetup{
resourceName: "customresourcedefinition",
injector: crdConversionInjector{},
listType: &apiext.CustomResourceDefinitionList{},
}
injectorSetups = []injectorSetup{MutatingWebhookSetup, ValidatingWebhookSetup, APIServiceSetup, CRDSetup}
ControllerNames []string
)

View File

@ -16,6 +16,7 @@ go_library(
"//pkg/client/clientset/versioned:go_default_library",
"//pkg/client/listers/certmanager/v1alpha1:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/feature:go_default_library",
"//pkg/issuer:go_default_library",
"//pkg/logs:go_default_library",
"//pkg/metrics:go_default_library",
@ -31,6 +32,7 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/listers/core/v1:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",

View File

@ -33,11 +33,13 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/tools/cache"
apiutil "github.com/jetstack/cert-manager/pkg/api/util"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/validation"
"github.com/jetstack/cert-manager/pkg/feature"
"github.com/jetstack/cert-manager/pkg/issuer"
logf "github.com/jetstack/cert-manager/pkg/logs"
"github.com/jetstack/cert-manager/pkg/util"
@ -404,6 +406,8 @@ func (c *controller) updateSecret(ctx context.Context, crt *v1alpha1.Certificate
if err != nil {
return nil, fmt.Errorf("invalid certificate data: %v", err)
}
case !utilfeature.DefaultFeatureGate.Enabled(feature.IssueTemporaryCertificate):
break
case isTemporaryCertificate(existingCert):
matches, err := pki.PublicKeyMatchesCertificate(privKey.Public(), existingCert)
if err == nil && matches {
@ -435,11 +439,13 @@ func (c *controller) updateSecret(ctx context.Context, crt *v1alpha1.Certificate
// TODO: move metadata setting out of this method, and support
// retrospectively adding metadata annotations on every Sync iteration and
// not just when a new certificate is issued
secret.Annotations[v1alpha1.IssuerNameAnnotationKey] = crt.Spec.IssuerRef.Name
secret.Annotations[v1alpha1.IssuerKindAnnotationKey] = issuerKind(crt)
secret.Annotations[v1alpha1.CommonNameAnnotationKey] = x509Cert.Subject.CommonName
secret.Annotations[v1alpha1.AltNamesAnnotationKey] = strings.Join(x509Cert.DNSNames, ",")
secret.Annotations[v1alpha1.IPSANAnnotationKey] = strings.Join(pki.IPAddressesToString(x509Cert.IPAddresses), ",")
if x509Cert != nil {
secret.Annotations[v1alpha1.IssuerNameAnnotationKey] = crt.Spec.IssuerRef.Name
secret.Annotations[v1alpha1.IssuerKindAnnotationKey] = issuerKind(crt)
secret.Annotations[v1alpha1.CommonNameAnnotationKey] = x509Cert.Subject.CommonName
secret.Annotations[v1alpha1.AltNamesAnnotationKey] = strings.Join(x509Cert.DNSNames, ",")
secret.Annotations[v1alpha1.IPSANAnnotationKey] = strings.Join(pki.IPAddressesToString(x509Cert.IPAddresses), ",")
}
// Always set the certificate name label on the target secret
secret.Labels[v1alpha1.CertificateNameKey] = crt.Name

View File

@ -26,6 +26,11 @@ const (
//
// ValidateCAA enables CAA checking when issuing certificates
ValidateCAA feature.Feature = "ValidateCAA"
// beta: v0.8.1
//
// IssueTemporaryCertificate enables issuing temporary certificates
IssueTemporaryCertificate feature.Feature = "IssueTemporaryCertificate"
)
func init() {
@ -36,5 +41,6 @@ func init() {
// To add a new feature, define a key for it above and add it here. The features will be
// available throughout Kubernetes binaries.
var defaultKubernetesFeatureGates = map[feature.Feature]feature.FeatureSpec{
ValidateCAA: {Default: false, PreRelease: feature.Alpha},
ValidateCAA: {Default: false, PreRelease: feature.Alpha},
IssueTemporaryCertificate: {Default: true, PreRelease: feature.Beta},
}

View File

@ -57,6 +57,7 @@ func (h *HTTP01Solver) Listen(ctx context.Context) error {
)
if r.URL.EscapedPath() == "/" || r.URL.EscapedPath() == "/healthz" {
log.Info("responding OK to health check")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.WriteHeader(http.StatusOK)
return
}
@ -84,6 +85,7 @@ func (h *HTTP01Solver) Listen(ctx context.Context) error {
}
log.Info("got successful challenge request, writing key")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, h.Key)
})

View File

@ -23,7 +23,7 @@ import (
"net/url"
"strings"
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -40,12 +40,14 @@ import (
const (
errorAccountRegistrationFailed = "ErrRegisterACMEAccount"
errorAccountVerificationFailed = "ErrVerifyACMEAccount"
errorAccountUpdateFailed = "ErrUpdateACMEAccount"
successAccountRegistered = "ACMEAccountRegistered"
successAccountVerified = "ACMEAccountVerified"
messageAccountRegistrationFailed = "Failed to register ACME account: "
messageAccountVerificationFailed = "Failed to verify ACME account: "
messageAccountUpdateFailed = "Failed to update ACME account:"
messageAccountRegistered = "The ACME account was registered with the ACME server"
messageAccountVerified = "The ACME account was verified with the ACME server"
)
@ -147,12 +149,14 @@ func (a *Acme) Setup(ctx context.Context) error {
Status: v1alpha1.ConditionTrue,
})
// If the Host components of the server URL and the account URL match, then
// If the Host components of the server URL and the account URL match,
// and the cached email matches the registered email, then
// we skip re-checking the account status to save excess calls to the
// ACME api.
if hasReadyCondition &&
a.issuer.GetStatus().ACMEStatus().URI != "" &&
parsedAccountURL.Host == parsedServerURL.Host {
parsedAccountURL.Host == parsedServerURL.Host &&
a.issuer.GetStatus().ACMEStatus().LastRegisteredEmail == a.issuer.GetSpec().ACME.Email {
log.Info("skipping re-verifying ACME account as cached registration " +
"details look sufficient")
return nil
@ -192,9 +196,57 @@ func (a *Acme) Setup(ctx context.Context) error {
return err
}
// if we got an account successfully, we must check if the registered
// email is the same as in the issuer spec
// if no email was specified, then registeredEmail will remain empty
registeredEmail := ""
if len(account.Contact) > 0 {
registeredEmail = strings.Replace(account.Contact[0], "mailto:", "", 1)
}
// if they are different, we update the account
specEmail := a.issuer.GetSpec().ACME.Email
if registeredEmail != specEmail {
log.Info("Updating ACME account with email %s", specEmail)
emailurl := []string(nil)
if a.issuer.GetSpec().ACME.Email != "" {
emailurl = []string{fmt.Sprintf("mailto:%s", strings.ToLower(specEmail))}
}
account.Contact = emailurl
account, err = cl.UpdateAccount(ctx, account)
if err != nil {
s := messageAccountUpdateFailed + err.Error()
log.Error(err, "failed to update ACME account")
a.Recorder.Event(a.issuer, v1.EventTypeWarning, errorAccountUpdateFailed, s)
apiutil.SetIssuerCondition(a.issuer, v1alpha1.IssuerConditionReady, v1alpha1.ConditionFalse, errorAccountUpdateFailed, s)
acmeErr, ok := err.(*acmeapi.Error)
// If this is not an ACME error, we will simply return it and retry later
if !ok {
return err
}
// If the status code is 400 (BadRequest), we will *not* retry this registration
// as it implies that something about the request (i.e. email address or private key)
// is invalid.
if acmeErr.StatusCode >= 400 && acmeErr.StatusCode < 500 {
log.Error(acmeErr, "skipping updating account email as a "+
"BadRequest response was returned from the ACME server")
return nil
}
// Otherwise if we receive anything other than a 400, we will retry.
return err
}
}
log.Info("verified existing registration with ACME server")
apiutil.SetIssuerCondition(a.issuer, v1alpha1.IssuerConditionReady, v1alpha1.ConditionTrue, successAccountRegistered, messageAccountRegistered)
a.issuer.GetStatus().ACMEStatus().URI = account.URL
a.issuer.GetStatus().ACMEStatus().LastRegisteredEmail = registeredEmail
return nil
}

View File

@ -43,6 +43,7 @@ go_test(
name = "go_default_test",
srcs = [
"issue_test.go",
"sign_test.go",
"util_test.go",
],
embed = [":go_default_library"],

View File

@ -90,26 +90,14 @@ func (c *CA) Issue(ctx context.Context, crt *v1alpha1.Certificate) (*issuer.Issu
return nil, err
}
caCert := caCerts[0]
template.PublicKey = signeePublicKey
// sign and encode the certificate
certPem, _, err := pki.SignCertificate(template, caCert, signeePublicKey, caKey)
resp, err := pki.SignCSRTemplate(caCerts, caKey, template)
if err != nil {
log.Error(err, "error signing certificate")
c.Recorder.Eventf(crt, corev1.EventTypeWarning, "ErrorSigning", "Error signing certificate: %v", err)
return nil, err
}
// encode the chain
// TODO: replace caCerts with caCerts[1:]?
chainPem, err := pki.EncodeX509Chain(caCerts)
if err != nil {
log.Error(err, "error encoding x509 certificate chain")
return nil, err
}
certPem = append(certPem, chainPem...)
// Encode output private key and CA cert ready for return
keyPem, err := pki.EncodePrivateKey(signeeKey, crt.Spec.KeyEncoding)
if err != nil {
@ -117,20 +105,9 @@ func (c *CA) Issue(ctx context.Context, crt *v1alpha1.Certificate) (*issuer.Issu
c.Recorder.Eventf(crt, corev1.EventTypeWarning, "ErrorPrivateKey", "Error encoding private key: %v", err)
return nil, err
}
// encode the CA certificate to be bundled in the output
caPem, err := pki.EncodeX509(caCerts[0])
if err != nil {
log.Error(err, "error encoding certificate")
c.Recorder.Eventf(crt, corev1.EventTypeWarning, "ErrorSigning", "Error encoding certificate: %v", err)
return nil, err
}
resp.PrivateKey = keyPem
log.Info("certificate issued")
return &issuer.IssueResponse{
PrivateKey: keyPem,
Certificate: certPem,
CA: caPem,
}, nil
return resp, nil
}

View File

@ -86,6 +86,33 @@ func allFieldsSetCheck(expectedCA []byte) func(t *testing.T, s *caFixture, args
if resp.PrivateKey == nil {
t.Errorf("expected new private key to be generated")
}
certificatesFieldsSetCheck(expectedCA)(t, s, args...)
}
}
func noPrivateKeyFieldsSetCheck(expectedCA []byte) func(t *testing.T, s *caFixture, args ...interface{}) {
return func(t *testing.T, s *caFixture, args ...interface{}) {
resp := args[1].(*issuer.IssueResponse)
if resp == nil {
t.Errorf("no response given, got=%s", resp)
return
}
if len(resp.PrivateKey) > 0 {
t.Errorf("expected no new private key to be generated but got: %s",
resp.PrivateKey)
}
certificatesFieldsSetCheck(expectedCA)(t, s, args...)
}
}
func certificatesFieldsSetCheck(expectedCA []byte) func(t *testing.T, s *caFixture, args ...interface{}) {
return func(t *testing.T, s *caFixture, args ...interface{}) {
resp := args[1].(*issuer.IssueResponse)
if resp.Certificate == nil {
t.Errorf("expected new certificate to be issued")
}

View File

@ -18,12 +18,46 @@ package ca
import (
"context"
"errors"
corev1 "k8s.io/api/core/v1"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/issuer"
logf "github.com/jetstack/cert-manager/pkg/logs"
"github.com/jetstack/cert-manager/pkg/util/kube"
"github.com/jetstack/cert-manager/pkg/util/pki"
)
func (c *CA) Sign(ctx context.Context, cr *v1alpha1.CertificateRequest) (*issuer.IssueResponse, error) {
return nil, errors.New("sign not implemented by CA issuer")
log := logf.FromContext(ctx, "sign")
// get a copy of the CA certificate named on the Issuer
caCerts, caKey, err := kube.SecretTLSKeyPair(ctx, c.secretsLister, c.resourceNamespace, c.issuer.GetSpec().CA.SecretName)
if err != nil {
log := logf.WithRelatedResourceName(log, c.issuer.GetSpec().CA.SecretName, c.resourceNamespace, "Secret")
log.Info("error getting signing CA for Issuer")
// We're fine to return errors here and later since upon a retry the issuer
// will be marked as 'not ready' - therefore this codepath wont be reached
// again
return nil, err
}
template, err := pki.GenerateTemplateFromCertificateRequest(cr)
if err != nil {
log.Error(err, "error generating certificate template")
c.Recorder.Eventf(cr, corev1.EventTypeWarning, "ErrorSigning", "Error generating certificate template: %v", err)
return nil, err
}
resp, err := pki.SignCSRTemplate(caCerts, caKey, template)
if err != nil {
log.Error(err, "error signing certificate")
c.Recorder.Eventf(cr, corev1.EventTypeWarning, "ErrorSigning", "Error signing certificate: %v", err)
return nil, err
}
log.Info("certificate issued")
return resp, nil
}

257
pkg/issuer/ca/sign_test.go Normal file
View File

@ -0,0 +1,257 @@
/*
Copyright 2019 The Jetstack cert-manager contributors.
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 ca
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"testing"
"time"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
testpkg "github.com/jetstack/cert-manager/pkg/controller/test"
"github.com/jetstack/cert-manager/pkg/issuer"
"github.com/jetstack/cert-manager/pkg/util/pki"
"github.com/jetstack/cert-manager/test/unit/gen"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func generateCSR(t *testing.T, secretKey crypto.Signer) ([]byte, error) {
asn1Subj, _ := asn1.Marshal(pkix.Name{
CommonName: "test",
}.ToRDNSequence())
template := x509.CertificateRequest{
RawSubject: asn1Subj,
SignatureAlgorithm: x509.SHA256WithRSA,
}
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, secretKey)
if err != nil {
return nil, err
}
csr := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes})
return csr, nil
}
func generateSelfSignedCertFromCR(t *testing.T, cr *v1alpha1.CertificateRequest, key crypto.Signer,
duration time.Duration) (derBytes, pemBytes []byte) {
template, err := pki.GenerateTemplateFromCertificateRequest(cr)
if err != nil {
t.Errorf("error generating template: %v", err)
}
derBytes, err = x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
if err != nil {
t.Errorf("error signing cert: %v", err)
t.FailNow()
}
pemByteBuffer := bytes.NewBuffer([]byte{})
err = pem.Encode(pemByteBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
if err != nil {
t.Errorf("failed to encode cert: %v", err)
t.FailNow()
}
return derBytes, pemByteBuffer.Bytes()
}
func TestSign(t *testing.T) {
// Build root RSA CA
rsaPK := generateRSAPrivateKey(t)
rsaPKBytes := pki.EncodePKCS1PrivateKey(rsaPK)
caCSR, err := generateCSR(t, rsaPK)
if err != nil {
t.Errorf("failed to generate CA CSR: %s", err)
t.FailNow()
}
rootRSACR := gen.CertificateRequest("test-root-ca",
gen.SetCertificateRequestCSR(caCSR),
gen.SetCertificateRequestIsCA(true),
gen.SetCertificateRequestDuration(&metav1.Duration{Duration: time.Hour * 24 * 60}),
)
// generate a self signed root ca valid for 60d
_, rsaPEMCert := generateSelfSignedCertFromCR(t, rootRSACR, rsaPK, time.Hour*24*60)
rootRSACASecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "root-ca-secret",
Namespace: gen.DefaultTestNamespace,
},
Data: map[string][]byte{
corev1.TLSPrivateKeyKey: rsaPKBytes,
corev1.TLSCertKey: rsaPEMCert,
},
}
rootRSANoCASecret := rootRSACASecret.DeepCopy()
rootRSANoCASecret.Data[corev1.TLSCertKey] = make([]byte, 0)
rootRSANoKeySecret := rootRSACASecret.DeepCopy()
rootRSANoKeySecret.Data[corev1.TLSPrivateKeyKey] = make([]byte, 0)
tests := map[string]caFixture{
"sign a CertificateRequest": {
Issuer: gen.Issuer("ca-issuer",
gen.SetIssuerCA(v1alpha1.CAIssuer{SecretName: "root-ca-secret"}),
),
CertificateRequest: gen.CertificateRequest("test-cr",
gen.SetCertificateRequestIsCA(true),
gen.SetCertificateRequestCSR(caCSR),
),
Builder: &testpkg.Builder{
KubeObjects: []runtime.Object{rootRSACASecret},
CertManagerObjects: []runtime.Object{},
},
// we are not expecting key on response
CheckFn: noPrivateKeyFieldsSetCheck(rsaPEMCert),
Err: false,
},
"fail to find CA tls key pair": {
Issuer: gen.Issuer("ca-issuer",
gen.SetIssuerCA(v1alpha1.CAIssuer{SecretName: "root-ca-secret"}),
),
CertificateRequest: gen.CertificateRequest("test-cr",
gen.SetCertificateRequestIsCA(true),
gen.SetCertificateRequestCSR(caCSR),
),
Builder: &testpkg.Builder{
KubeObjects: []runtime.Object{},
CertManagerObjects: []runtime.Object{},
},
CheckFn: func(t *testing.T, s *caFixture, args ...interface{}) {
err := args[2].(error)
notFoundErr := `secret "root-ca-secret" not found`
if err == nil || err.Error() != notFoundErr {
t.Errorf("unexpected error, exp='%s' got='%+v'", notFoundErr, err)
}
resp := args[1].(*issuer.IssueResponse)
if resp != nil {
t.Errorf("unexpected response, exp='nil' got='%+v'", resp)
}
},
Err: true,
},
"given bad CSR should fail Certificate generation": {
Issuer: gen.Issuer("ca-issuer",
gen.SetIssuerCA(v1alpha1.CAIssuer{SecretName: "root-ca-secret"}),
),
CertificateRequest: gen.CertificateRequest("test-cr",
gen.SetCertificateRequestIsCA(true),
gen.SetCertificateRequestCSR([]byte("bad-csr")),
),
Builder: &testpkg.Builder{
KubeObjects: []runtime.Object{rootRSACASecret},
CertManagerObjects: []runtime.Object{},
},
CheckFn: func(t *testing.T, s *caFixture, args ...interface{}) {
err := args[2].(error)
decodeErr := "failed to decode csr from certificate request resource default-unit-test-ns/test-cr"
if err == nil || err.Error() != decodeErr {
t.Errorf("unexpected error, exp='%s' got='%+v'", decodeErr, err)
}
resp := args[1].(*issuer.IssueResponse)
if resp != nil {
t.Errorf("unexpected response, exp='nil' got='%+v'", resp)
}
},
Err: true,
},
"no CA certificate should fail a signing": {
Issuer: gen.Issuer("ca-issuer",
gen.SetIssuerCA(v1alpha1.CAIssuer{SecretName: "root-ca-secret"}),
),
CertificateRequest: gen.CertificateRequest("test-cr",
gen.SetCertificateRequestIsCA(true),
gen.SetCertificateRequestCSR(caCSR),
),
Builder: &testpkg.Builder{
KubeObjects: []runtime.Object{rootRSANoCASecret},
CertManagerObjects: []runtime.Object{},
},
CheckFn: func(t *testing.T, s *caFixture, args ...interface{}) {
err := args[2].(error)
noCertError := "error decoding cert PEM block"
if err == nil || err.Error() != noCertError {
t.Errorf("unexpected error, exp='%s' got='%+v'", noCertError, err)
}
resp := args[1].(*issuer.IssueResponse)
if resp != nil {
t.Errorf("unexpected response, exp='nil' got='%+v'", resp)
}
},
Err: true,
},
"no CA key should fail a signing": {
Issuer: gen.Issuer("ca-issuer",
gen.SetIssuerCA(v1alpha1.CAIssuer{SecretName: "root-ca-secret"}),
),
CertificateRequest: gen.CertificateRequest("test-cr",
gen.SetCertificateRequestIsCA(true),
gen.SetCertificateRequestCSR(caCSR),
),
Builder: &testpkg.Builder{
KubeObjects: []runtime.Object{rootRSANoKeySecret},
CertManagerObjects: []runtime.Object{},
},
CheckFn: func(t *testing.T, s *caFixture, args ...interface{}) {
err := args[2].(error)
noKeyError := "error decoding private key PEM block"
if err == nil || err.Error() != noKeyError {
t.Errorf("unexpected error, exp='%s' got='%+v'", noKeyError, err)
}
resp := args[1].(*issuer.IssueResponse)
if resp != nil {
t.Errorf("unexpected response, exp='nil' got='%+v'", resp)
}
},
Err: true,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
if test.Builder == nil {
test.Builder = &testpkg.Builder{}
}
test.Setup(t)
crCopy := test.CertificateRequest.DeepCopy()
resp, err := test.CA.Sign(test.Ctx, crCopy)
if err != nil && !test.Err {
t.Errorf("Expected function to not error, but got: %v", err)
}
if err == nil && test.Err {
t.Errorf("Expected function to get an error, but got: %v", err)
}
test.Finish(t, crCopy, resp, err)
})
}
}

View File

@ -33,8 +33,9 @@ type caFixture struct {
CA *CA
*test.Builder
Issuer v1alpha1.GenericIssuer
Certificate *v1alpha1.Certificate
Issuer v1alpha1.GenericIssuer
Certificate *v1alpha1.Certificate
CertificateRequest *v1alpha1.CertificateRequest
PreFn func(*testing.T, *caFixture)
CheckFn func(*testing.T, *caFixture, ...interface{})

View File

@ -11,6 +11,7 @@ go_library(
visibility = ["//visibility:public"],
deps = [
"//pkg/apis/certmanager/v1alpha1:go_default_library",
"//pkg/issuer:go_default_library",
"//pkg/util/errors:go_default_library",
],
)

View File

@ -23,12 +23,14 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net"
"time"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/issuer"
)
// CommonNameForCertificate returns the common name that should be used for the
@ -194,6 +196,55 @@ func GenerateTemplate(crt *v1alpha1.Certificate) (*x509.Certificate, error) {
}, nil
}
// GenerateTemplate will create a x509.Certificate for the given
// CertificateRequest resource
func GenerateTemplateFromCertificateRequest(cr *v1alpha1.CertificateRequest) (*x509.Certificate, error) {
block, _ := pem.Decode(cr.Spec.CSRPEM)
if block == nil {
return nil, fmt.Errorf("failed to decode csr from certificate request resource %s/%s",
cr.Namespace, cr.Name)
}
csr, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil {
return nil, err
}
if err := csr.CheckSignature(); err != nil {
return nil, err
}
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, fmt.Errorf("failed to generate serial number: %s", err.Error())
}
certDuration := v1alpha1.DefaultCertificateDuration
if cr.Spec.Duration != nil {
certDuration = cr.Spec.Duration.Duration
}
keyUsages := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
return &x509.Certificate{
Version: csr.Version,
BasicConstraintsValid: true,
SerialNumber: serialNumber,
PublicKeyAlgorithm: csr.PublicKeyAlgorithm,
PublicKey: csr.PublicKey,
IsCA: cr.Spec.IsCA,
Subject: csr.Subject,
NotBefore: time.Now(),
NotAfter: time.Now().Add(certDuration),
// see http://golang.org/pkg/crypto/x509/#KeyUsage
KeyUsage: keyUsages,
DNSNames: csr.DNSNames,
IPAddresses: csr.IPAddresses,
URIs: csr.URIs,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}, nil
}
// SignCertificate returns a signed x509.Certificate object for the given
// *v1alpha1.Certificate crt.
// publicKey is the public key of the signee, and signerKey is the private
@ -221,6 +272,41 @@ func SignCertificate(template *x509.Certificate, issuerCert *x509.Certificate, p
return pemBytes.Bytes(), cert, err
}
// SignCSRTemplate signs a certificate template usually based upon a CSR. This
// function expects all fields to be present in the certificate template,
// including it's public key.
func SignCSRTemplate(caCerts []*x509.Certificate, caKey crypto.Signer, template *x509.Certificate) (*issuer.IssueResponse, error) {
if len(caCerts) == 0 {
return nil, errors.New("no CA certificates given to sign CSR template")
}
caCert := caCerts[0]
certPem, _, err := SignCertificate(template, caCert, template.PublicKey, caKey)
if err != nil {
return nil, err
}
chainPem, err := EncodeX509Chain(caCerts)
if err != nil {
return nil, err
}
certPem = append(certPem, chainPem...)
// encode the CA certificate to be bundled in the output
caPem, err := EncodeX509(caCerts[0])
if err != nil {
return nil, err
}
return &issuer.IssueResponse{
Certificate: certPem,
CA: caPem,
}, nil
}
// EncodeCSR calls x509.CreateCertificateRequest to sign the given CSR template.
// It returns a DER encoded signed CSR.
func EncodeCSR(template *x509.CertificateRequest, key crypto.Signer) ([]byte, error) {

View File

@ -54,7 +54,7 @@ func applyDefaults(f *fixture) {
runfiles := os.Getenv("TEST_SRCDIR")
if f.binariesPath == "" {
if runfiles != "" {
f.binariesPath = runfiles + "/__main__/hack/bin"
f.binariesPath = runfiles + "/cert_manager/hack/bin"
}
}
if f.jsonConfig == nil {

View File

@ -16,12 +16,13 @@ container_bundle(
"{STABLE_DOCKER_REPO}/cert-manager-webhook:{STABLE_DOCKER_TAG}": "//cmd/webhook:image",
"{STABLE_DOCKER_REPO}/cert-manager-cainjector:{STABLE_DOCKER_TAG}": "//cmd/cainjector:image",
},
tags = ["manual"],
)
# we add this rule so users can `bazel build //test/e2e` to run a
# platform-independent version of the e2e test binary
genrule(
name = "e2e.test",
name = "e2e",
testonly = True,
srcs = [":go_default_test"],
outs = ["e2e.test"],

View File

@ -3,6 +3,7 @@ genrule(
srcs = ["@io_kubernetes_sigs_kind//:kind"],
outs = ["kind"],
cmd = "cp $(SRCS) $@",
tags = ["manual"],
visibility = ["//visibility:public"],
)
@ -14,6 +15,7 @@ genrule(
}),
outs = ["kubectl-1.11"],
cmd = "cp $(SRCS) $@",
tags = ["manual"],
visibility = ["//visibility:public"],
)
@ -25,6 +27,7 @@ genrule(
}),
outs = ["kubectl-1.12"],
cmd = "cp $(SRCS) $@",
tags = ["manual"],
visibility = ["//visibility:public"],
)
@ -36,6 +39,7 @@ genrule(
}),
outs = ["kubectl-1.13"],
cmd = "cp $(SRCS) $@",
tags = ["manual"],
visibility = ["//visibility:public"],
)

View File

@ -1,15 +1,21 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary")
load("@io_bazel_rules_docker//go:image.bzl", "go_image")
# gazelle:ignore
go_image(
name = "image",
base = "@static_base//image",
embed = ["@org_letsencrypt_pebble//cmd/pebble:go_default_library"],
goarch = "amd64",
goos = "linux",
pure = "on",
binary = ":app",
visibility = ["//visibility:public"],
)
go_binary(
name = "app",
embed = ["@org_letsencrypt_pebble//cmd/pebble:go_default_library"],
pure = "on",
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),

View File

@ -27,6 +27,7 @@ go_library(
"//vendor/k8s.io/api/authorization/v1:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/api/rbac/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
@ -36,6 +37,7 @@ go_library(
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/scheme:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
"//vendor/k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1:go_default_library",
"//vendor/sigs.k8s.io/controller-runtime/pkg/client:go_default_library",
],
)

View File

@ -4,10 +4,7 @@ load("@io_bazel_rules_docker//go:image.bzl", "go_image")
go_image(
name = "image",
base = "@static_base//image",
embed = [":go_default_library"],
goarch = "amd64",
goos = "linux",
pure = "on",
binary = ":sample",
visibility = ["//visibility:public"],
)
@ -27,6 +24,7 @@ go_library(
go_binary(
name = "sample",
embed = [":go_default_library"],
pure = "on",
visibility = ["//visibility:public"],
)

View File

@ -24,12 +24,14 @@ import (
"k8s.io/api/core/v1"
api "k8s.io/api/core/v1"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apiextcs "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
kscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
apireg "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
@ -50,6 +52,8 @@ var Scheme = runtime.NewScheme()
func init() {
kscheme.AddToScheme(Scheme)
certmgrscheme.AddToScheme(Scheme)
apiext.AddToScheme(Scheme)
apireg.AddToScheme(Scheme)
}
// DefaultConfig contains the default shared config the is likely parsed from

View File

@ -33,6 +33,7 @@ import (
const invalidACMEURL = "http://not-a-real-acme-url.com"
const testingACMEEmail = "test@example.com"
const testingACMEEmailAlternative = "another-test@example.com"
const testingACMEPrivateKey = "test-acme-private-key"
var _ = framework.CertManagerDescribe("ACME Issuer", func() {
@ -190,4 +191,79 @@ var _ = framework.CertManagerDescribe("ACME Issuer", func() {
})
Expect(err).NotTo(HaveOccurred())
})
It("should handle updates to the email field", func() {
acmeURL := pebble.Details().Host
acmeIssuer := util.NewCertManagerACMEIssuer(issuerName, acmeURL, testingACMEEmail, testingACMEPrivateKey)
By("Creating an Issuer")
acmeIssuer, err := f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name).Create(acmeIssuer)
Expect(err).NotTo(HaveOccurred())
By("Waiting for Issuer to become Ready")
err = util.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name),
acmeIssuer.Name,
v1alpha1.IssuerCondition{
Type: v1alpha1.IssuerConditionReady,
Status: v1alpha1.ConditionTrue,
})
Expect(err).NotTo(HaveOccurred())
By("Verifying the ACME account URI is set")
err = util.WaitForIssuerStatusFunc(f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name),
acmeIssuer.Name,
func(i *v1alpha1.Issuer) (bool, error) {
if i.GetStatus().ACMEStatus().URI == "" {
return false, nil
}
return true, nil
})
Expect(err).NotTo(HaveOccurred())
By("Verifying ACME account private key exists")
secret, err := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Get(testingACMEPrivateKey, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())
if len(secret.Data) != 1 {
Fail("Expected 1 key in ACME account private key secret, but there was %d", len(secret.Data))
}
By("Verifying the ACME account email has been registered")
err = util.WaitForIssuerStatusFunc(f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name),
acmeIssuer.Name,
func(i *v1alpha1.Issuer) (bool, error) {
registeredEmail := i.GetStatus().ACMEStatus().LastRegisteredEmail
if registeredEmail == testingACMEEmail {
return true, nil
}
return false, nil
})
Expect(err).NotTo(HaveOccurred())
By("Changing the email field")
acmeIssuer, err = f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name).Get(acmeIssuer.Name, metav1.GetOptions{})
acmeIssuer.Spec.ACME.Email = testingACMEEmailAlternative
acmeIssuer, err = f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name).Update(acmeIssuer)
Expect(err).NotTo(HaveOccurred())
By("Waiting for Issuer to become Ready")
err = util.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name),
acmeIssuer.Name,
v1alpha1.IssuerCondition{
Type: v1alpha1.IssuerConditionReady,
Status: v1alpha1.ConditionTrue,
})
Expect(err).NotTo(HaveOccurred())
By("Verifying the changed ACME account email has been registered")
err = util.WaitForIssuerStatusFunc(f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name),
acmeIssuer.Name,
func(i *v1alpha1.Issuer) (bool, error) {
registeredEmail := i.GetStatus().ACMEStatus().LastRegisteredEmail
if registeredEmail == testingACMEEmailAlternative {
return true, nil
}
return false, nil
})
Expect(err).NotTo(HaveOccurred())
})
})

View File

@ -14,9 +14,11 @@ go_library(
"//vendor/github.com/onsi/gomega:go_default_library",
"//vendor/k8s.io/api/admissionregistration/v1beta1:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1:go_default_library",
],
)

View File

@ -28,47 +28,211 @@ import (
"github.com/jetstack/cert-manager/test/e2e/util"
admissionreg "k8s.io/api/admissionregistration/v1beta1"
corev1 "k8s.io/api/core/v1"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
apireg "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
)
type injectableTest struct {
makeInjectable func(namePrefix string) runtime.Object
getCAs func(runtime.Object) [][]byte
subject string
disabled string
}
var _ = framework.CertManagerDescribe("CA Injector", func() {
f := framework.NewDefaultFramework("ca-injector")
issuerName := "inject-cert-issuer"
Context("for validating webhooks", func() {
var hookToCleanUp runtime.Object
BeforeEach(func() {
By("creating a self-signing issuer")
issuer := util.NewCertManagerSelfSignedIssuer(issuerName)
issuer.Namespace = f.Namespace.Name
Expect(f.CRClient.Create(context.Background(), issuer)).To(Succeed())
injectorContext := func(subj string, test *injectableTest) {
Context("for "+subj+"s", func() {
var toCleanup runtime.Object
By("Waiting for Issuer to become Ready")
err := util.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name),
issuerName,
certmanager.IssuerCondition{
Type: certmanager.IssuerConditionReady,
Status: certmanager.ConditionTrue,
})
Expect(err).NotTo(HaveOccurred())
})
BeforeEach(func() {
By("creating a self-signing issuer")
issuer := util.NewCertManagerSelfSignedIssuer(issuerName)
issuer.Namespace = f.Namespace.Name
Expect(f.CRClient.Create(context.Background(), issuer)).To(Succeed())
By("Waiting for Issuer to become Ready")
err := util.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name),
issuerName,
certmanager.IssuerCondition{
Type: certmanager.IssuerConditionReady,
Status: certmanager.ConditionTrue,
})
Expect(err).NotTo(HaveOccurred())
})
AfterEach(func() {
if toCleanup == nil {
return
}
Expect(f.CRClient.Delete(context.Background(), toCleanup)).To(Succeed())
})
generalSetup := func(namePrefix string) (runtime.Object, certmanager.Certificate, corev1.Secret) {
By("creating a " + subj + " pointing to a cert")
injectable := test.makeInjectable(namePrefix)
Expect(f.CRClient.Create(context.Background(), injectable)).To(Succeed())
toCleanup = injectable
By("creating a certificate")
secretName := types.NamespacedName{Name: "serving-certs-data", Namespace: f.Namespace.Name}
cert := util.NewCertManagerBasicCertificate("serving-certs", secretName.Name, issuerName, certmanager.IssuerKind, nil, nil)
cert.Namespace = f.Namespace.Name
Expect(f.CRClient.Create(context.Background(), cert)).To(Succeed())
By("grabbing the corresponding secret")
var secret corev1.Secret
Eventually(func() error { return f.CRClient.Get(context.Background(), secretName, &secret) }, "10s", "2s").Should(Succeed())
By("checking that all webhooks have a populated CA")
caData := secret.Data["ca.crt"]
expectedLen := len(test.getCAs(injectable))
expectedCAs := make([][]byte, expectedLen)
for i := range expectedCAs {
expectedCAs[i] = caData
}
Eventually(func() ([][]byte, error) {
newInjectable := injectable.DeepCopyObject()
if err := f.CRClient.Get(context.Background(), types.NamespacedName{Name: injectable.(metav1.Object).GetName()}, newInjectable); err != nil {
return nil, err
}
return test.getCAs(newInjectable), nil
}, "10s", "2s").Should(Equal(expectedCAs))
return injectable, *cert, secret
AfterEach(func() {
if hookToCleanUp == nil {
return
}
Expect(f.CRClient.Delete(context.Background(), hookToCleanUp)).To(Succeed())
})
setupHook := func(hookPrefix string) (admissionreg.ValidatingWebhookConfiguration, certmanager.Certificate, corev1.Secret) {
By("creating a validating webhook pointing to a cert")
It("should inject the CA data into all CA fields", func() {
if test.disabled != "" {
Skip(test.disabled)
}
generalSetup("injected")
})
It("should not inject CA into non-annotated objects", func() {
if test.disabled != "" {
Skip(test.disabled)
}
By("creating a validating webhook not pointing to a cert")
injectable := test.makeInjectable("non-injected")
injectable.(metav1.Object).SetAnnotations(map[string]string{}) // wipe out the inject annotation
Expect(f.CRClient.Create(context.Background(), injectable)).To(Succeed())
toCleanup = injectable
By("expecting the CA data to remain in place")
expectedCAs := test.getCAs(injectable)
Consistently(func() ([][]byte, error) {
newInjectable := injectable.DeepCopyObject()
if err := f.CRClient.Get(context.Background(), types.NamespacedName{Name: injectable.(metav1.Object).GetName()}, newInjectable); err != nil {
return nil, err
}
return test.getCAs(newInjectable), nil
}).Should(Equal(expectedCAs))
})
It("should update data when the certificate changes", func() {
if test.disabled != "" {
Skip(test.disabled)
}
injectable, cert, _ := generalSetup("changed")
By("grabbing the latest copy of the cert")
Expect(f.CRClient.Get(context.Background(), types.NamespacedName{Name: cert.Name, Namespace: cert.Namespace}, &cert)).To(Succeed())
By("changing the name of the corresponding secret in the cert")
secretName := types.NamespacedName{Name: "other-data", Namespace: f.Namespace.Name}
cert.Spec.SecretName = secretName.Name
Expect(f.CRClient.Update(context.Background(), &cert)).To(Succeed())
By("grabbing the new secret")
var secret corev1.Secret
Eventually(func() error { return f.CRClient.Get(context.Background(), secretName, &secret) }, "10s", "2s").Should(Succeed())
By("verifying that the hooks have the new data")
caData := secret.Data["ca.crt"]
expectedLen := len(test.getCAs(injectable))
expectedCAs := make([][]byte, expectedLen)
for i := range expectedCAs {
expectedCAs[i] = caData
}
Eventually(func() ([][]byte, error) {
newInjectable := injectable.DeepCopyObject()
if err := f.CRClient.Get(context.Background(), types.NamespacedName{Name: injectable.(metav1.Object).GetName()}, newInjectable); err != nil {
return nil, err
}
return test.getCAs(newInjectable), nil
}, "10s", "2s").Should(Equal(expectedCAs))
})
It("should ignore objects with invalid annotations", func() {
if test.disabled != "" {
Skip(test.disabled)
}
By("creating a validating webhook with an invalid cert name")
injectable := test.makeInjectable("invalid")
injectable.(metav1.Object).SetAnnotations(map[string]string{
injctrl.WantInjectAnnotation: "serving-certs", // an invalid annotation
})
Expect(f.CRClient.Create(context.Background(), injectable)).To(Succeed())
toCleanup = injectable
By("expecting the CA data to remain in place")
expectedCAs := test.getCAs(injectable)
Consistently(func() ([][]byte, error) {
newInjectable := injectable.DeepCopyObject()
if err := f.CRClient.Get(context.Background(), types.NamespacedName{Name: injectable.(metav1.Object).GetName()}, newInjectable); err != nil {
return nil, err
}
return test.getCAs(newInjectable), nil
}).Should(Equal(expectedCAs))
})
It("should inject the apiserver CA if the inject-apiserver-ca annotation is present", func() {
if test.disabled != "" {
Skip(test.disabled)
}
if len(f.KubeClientConfig.CAData) == 0 {
Skip("skipping test as the kube client CA bundle is not set")
}
By("creating a vaidating webhook with the inject-apiserver-ca annotation")
injectable := test.makeInjectable("apiserver-ca")
injectable.(metav1.Object).SetAnnotations(map[string]string{
injctrl.WantInjectAPIServerCAAnnotation: "true",
})
Expect(f.CRClient.Create(context.Background(), injectable)).To(Succeed())
toCleanup = injectable
By("checking that all webhooks have a populated CA")
caData := f.KubeClientConfig.CAData
expectedLen := len(test.getCAs(injectable))
expectedCAs := make([][]byte, expectedLen)
for i := range expectedCAs {
expectedCAs[i] = caData
}
Eventually(func() ([][]byte, error) {
newInjectable := injectable.DeepCopyObject()
if err := f.CRClient.Get(context.Background(), types.NamespacedName{Name: injectable.(metav1.Object).GetName()}, newInjectable); err != nil {
return nil, err
}
return test.getCAs(newInjectable), nil
}, "10s", "2s").Should(Equal(expectedCAs))
})
})
}
injectorContext("validating webhook", &injectableTest{
makeInjectable: func(namePrefix string) runtime.Object {
someURL := "https://localhost:8675"
hook := admissionreg.ValidatingWebhookConfiguration{
return &admissionreg.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: hookPrefix + "-hook",
Name: namePrefix + "-hook",
Annotations: map[string]string{
injctrl.WantInjectAnnotation: types.NamespacedName{Name: "serving-certs", Namespace: f.Namespace.Name}.String(),
},
@ -91,157 +255,25 @@ var _ = framework.CertManagerDescribe("CA Injector", func() {
},
},
}
Expect(f.CRClient.Create(context.Background(), &hook)).To(Succeed())
hookToCleanUp = &hook
By("creating a certificate")
secretName := types.NamespacedName{Name: "serving-certs-data", Namespace: f.Namespace.Name}
cert := util.NewCertManagerBasicCertificate("serving-certs", secretName.Name, issuerName, certmanager.IssuerKind, nil, nil)
cert.Namespace = f.Namespace.Name
Expect(f.CRClient.Create(context.Background(), cert)).To(Succeed())
By("grabbing the corresponding secret")
var secret corev1.Secret
Eventually(func() error { return f.CRClient.Get(context.Background(), secretName, &secret) }, "10s", "2s").Should(Succeed())
By("checking that all webhooks have a populated CA")
caData := secret.Data["ca.crt"]
Eventually(func() ([][]byte, error) {
var newHook admissionreg.ValidatingWebhookConfiguration
if err := f.CRClient.Get(context.Background(), types.NamespacedName{Name: hook.Name}, &newHook); err != nil {
return nil, err
}
return [][]byte{newHook.Webhooks[0].ClientConfig.CABundle, newHook.Webhooks[1].ClientConfig.CABundle}, nil
}, "10s", "2s").Should(Equal([][]byte{caData, caData}))
return hook, *cert, secret
}
It("should inject a CA into all webhook slots", func() {
setupHook("injected")
})
It("should not inject a CA into webhooks that aren't annotated", func() {
By("creating a validating webhook not pointing to a cert")
someURL := "https://localhost:8675"
hook := admissionreg.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: "non-injected-hook",
},
Webhooks: []admissionreg.Webhook{
{
Name: "hook1.fake.k8s.io",
ClientConfig: admissionreg.WebhookClientConfig{
URL: &someURL,
CABundle: []byte("ca data 1"),
},
},
{
Name: "hook2.fake.k8s.io",
ClientConfig: admissionreg.WebhookClientConfig{
Service: &admissionreg.ServiceReference{
Name: "some-svc",
Namespace: f.Namespace.Name,
},
CABundle: []byte("ca data 2"),
},
},
},
},
getCAs: func(obj runtime.Object) [][]byte {
hook := obj.(*admissionreg.ValidatingWebhookConfiguration)
res := make([][]byte, len(hook.Webhooks))
for i, webhook := range hook.Webhooks {
res[i] = webhook.ClientConfig.CABundle
}
Expect(f.CRClient.Create(context.Background(), &hook)).To(Succeed())
hookToCleanUp = &hook
return res
},
})
By("expecting the CA data to remain in place")
Consistently(func() ([][]byte, error) {
var newHook admissionreg.ValidatingWebhookConfiguration
if err := f.CRClient.Get(context.Background(), types.NamespacedName{Name: hook.Name}, &newHook); err != nil {
return nil, err
}
return [][]byte{newHook.Webhooks[0].ClientConfig.CABundle, newHook.Webhooks[1].ClientConfig.CABundle}, nil
}).Should(Equal([][]byte{hook.Webhooks[0].ClientConfig.CABundle, hook.Webhooks[1].ClientConfig.CABundle}))
})
It("should update the webhooks when the certificate is changed", func() {
hook, cert, _ := setupHook("changed")
By("grabbing the latest copy of the cert")
Expect(f.CRClient.Get(context.Background(), types.NamespacedName{Name: cert.Name, Namespace: cert.Namespace}, &cert)).To(Succeed())
By("changing the name of the corresponding secret in the cert")
secretName := types.NamespacedName{Name: "other-data", Namespace: f.Namespace.Name}
cert.Spec.SecretName = secretName.Name
Expect(f.CRClient.Update(context.Background(), &cert)).To(Succeed())
By("grabbing the new secret")
var secret corev1.Secret
Eventually(func() error { return f.CRClient.Get(context.Background(), secretName, &secret) }, "10s", "2s").Should(Succeed())
By("verifying that the hooks have the new data")
caData := secret.Data["ca.crt"]
Eventually(func() ([][]byte, error) {
var newHook admissionreg.ValidatingWebhookConfiguration
if err := f.CRClient.Get(context.Background(), types.NamespacedName{Name: hook.Name}, &newHook); err != nil {
return nil, err
}
return [][]byte{newHook.Webhooks[0].ClientConfig.CABundle, newHook.Webhooks[1].ClientConfig.CABundle}, nil
}, "10s", "2s").Should(Equal([][]byte{caData, caData}))
})
It("should ignore webhooks with invalid annotations", func() {
By("creating a validating webhook with an invalid cert name")
injectorContext("mutating webhook", &injectableTest{
makeInjectable: func(namePrefix string) runtime.Object {
someURL := "https://localhost:8675"
hook := admissionreg.ValidatingWebhookConfiguration{
return &admissionreg.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: "invalid-hook",
Name: namePrefix + "-hook",
Annotations: map[string]string{
injctrl.WantInjectAnnotation: "serving-certs",
},
},
Webhooks: []admissionreg.Webhook{
{
Name: "hook1.fake.k8s.io",
ClientConfig: admissionreg.WebhookClientConfig{
URL: &someURL,
CABundle: []byte("ca data 1"),
},
},
{
Name: "hook2.fake.k8s.io",
ClientConfig: admissionreg.WebhookClientConfig{
Service: &admissionreg.ServiceReference{
Name: "some-svc",
Namespace: f.Namespace.Name,
},
CABundle: []byte("ca data 2"),
},
},
},
}
Expect(f.CRClient.Create(context.Background(), &hook)).To(Succeed())
hookToCleanUp = &hook
By("expecting the CA data to remain in place")
Consistently(func() ([][]byte, error) {
var newHook admissionreg.ValidatingWebhookConfiguration
if err := f.CRClient.Get(context.Background(), types.NamespacedName{Name: hook.Name}, &newHook); err != nil {
return nil, err
}
return [][]byte{newHook.Webhooks[0].ClientConfig.CABundle, newHook.Webhooks[1].ClientConfig.CABundle}, nil
}).Should(Equal([][]byte{hook.Webhooks[0].ClientConfig.CABundle, hook.Webhooks[1].ClientConfig.CABundle}))
})
It("should inject the apiserver CA if the webhook as the inject-apiserver-ca annotation", func() {
if len(f.KubeClientConfig.CAData) == 0 {
Skip("skipping test as the kube client CA bundle is not set")
}
By("creating a vaidating webhook with the inject-apiserver-ca annotation")
someURL := "https://localhost:8675"
hook := admissionreg.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: "apiserver-ca-hook",
Annotations: map[string]string{
injctrl.WantInjectAPIServerCAAnnotation: "true",
injctrl.WantInjectAnnotation: types.NamespacedName{Name: "serving-certs", Namespace: f.Namespace.Name}.String(),
},
},
Webhooks: []admissionreg.Webhook{
@ -262,18 +294,79 @@ var _ = framework.CertManagerDescribe("CA Injector", func() {
},
},
}
Expect(f.CRClient.Create(context.Background(), &hook)).To(Succeed())
hookToCleanUp = &hook
},
getCAs: func(obj runtime.Object) [][]byte {
hook := obj.(*admissionreg.MutatingWebhookConfiguration)
res := make([][]byte, len(hook.Webhooks))
for i, webhook := range hook.Webhooks {
res[i] = webhook.ClientConfig.CABundle
}
return res
},
})
By("checking that all webhooks have a populated CA")
caData := f.KubeClientConfig.CAData
Eventually(func() ([][]byte, error) {
var newHook admissionreg.ValidatingWebhookConfiguration
if err := f.CRClient.Get(context.Background(), types.NamespacedName{Name: hook.Name}, &newHook); err != nil {
return nil, err
}
return [][]byte{newHook.Webhooks[0].ClientConfig.CABundle, newHook.Webhooks[1].ClientConfig.CABundle}, nil
}, "10s", "2s").Should(Equal([][]byte{caData, caData}))
})
// TODO(directxman12): enable ConversionWebhook feature on the test infra,
// re-enable this.
injectorContext("conversion webhook", &injectableTest{
makeInjectable: func(namePrefix string) runtime.Object {
someURL := "https://localhost:8675"
return &apiext.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "objs." + namePrefix + ".testing.certmanager.k8s.io",
Annotations: map[string]string{
injctrl.WantInjectAnnotation: types.NamespacedName{Name: "serving-certs", Namespace: f.Namespace.Name}.String(),
},
},
Spec: apiext.CustomResourceDefinitionSpec{
Group: namePrefix + ".testing.certmanager.k8s.io",
Version: "v1",
Conversion: &apiext.CustomResourceConversion{
Strategy: apiext.WebhookConverter,
WebhookClientConfig: &apiext.WebhookClientConfig{
URL: &someURL,
},
},
Names: apiext.CustomResourceDefinitionNames{
Kind: "Obj",
ListKind: "ObjList",
},
},
}
},
getCAs: func(obj runtime.Object) [][]byte {
crd := obj.(*apiext.CustomResourceDefinition)
if crd.Spec.Conversion == nil || crd.Spec.Conversion.WebhookClientConfig == nil {
return nil
}
return [][]byte{crd.Spec.Conversion.WebhookClientConfig.CABundle}
},
disabled: "ConversionWebhook feature not yet enabled on test infra",
})
injectorContext("api service", &injectableTest{
makeInjectable: func(namePrefix string) runtime.Object {
return &apireg.APIService{
ObjectMeta: metav1.ObjectMeta{
Name: "v1." + namePrefix + ".testing.certmanager.k8s.io",
Annotations: map[string]string{
injctrl.WantInjectAnnotation: types.NamespacedName{Name: "serving-certs", Namespace: f.Namespace.Name}.String(),
},
},
Spec: apireg.APIServiceSpec{
Service: &apireg.ServiceReference{
Name: "does-not-exit",
Namespace: "default",
},
Group: namePrefix + ".testing.certmanager.k8s.io",
Version: "v1",
GroupPriorityMinimum: 1,
VersionPriority: 1,
},
}
},
getCAs: func(obj runtime.Object) [][]byte {
apiSvc := obj.(*apireg.APIService)
return [][]byte{apiSvc.Spec.CABundle}
},
})
})

View File

@ -4,6 +4,7 @@ go_library(
name = "go_default_library",
srcs = [
"certificate.go",
"certificaterequest.go",
"challenge.go",
"doc.go",
"issuer.go",

View File

@ -0,0 +1,102 @@
/*
Copyright 2019 The Jetstack cert-manager contributors.
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 gen
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
)
type CertificateRequestModifier func(*v1alpha1.CertificateRequest)
func CertificateRequest(name string, mods ...CertificateRequestModifier) *v1alpha1.CertificateRequest {
c := &v1alpha1.CertificateRequest{
ObjectMeta: ObjectMeta(name),
}
for _, mod := range mods {
mod(c)
}
return c
}
func CertificateRequestFrom(cr *v1alpha1.CertificateRequest, mods ...CertificateRequestModifier) *v1alpha1.CertificateRequest {
cr = cr.DeepCopy()
for _, mod := range mods {
mod(cr)
}
return cr
}
// SetIssuer sets the CertificateRequest.spec.issuerRef field
func SetCertificateRequestIssuer(o v1alpha1.ObjectReference) CertificateRequestModifier {
return func(c *v1alpha1.CertificateRequest) {
c.Spec.IssuerRef = o
}
}
func SetCertificateRequestCSR(csr []byte) CertificateRequestModifier {
return func(cr *v1alpha1.CertificateRequest) {
cr.Spec.CSRPEM = csr
}
}
func SetCertificateRequestIsCA(isCA bool) CertificateRequestModifier {
return func(cr *v1alpha1.CertificateRequest) {
cr.Spec.IsCA = isCA
}
}
func SetCertificateRequestDuration(duration *metav1.Duration) CertificateRequestModifier {
return func(cr *v1alpha1.CertificateRequest) {
cr.Spec.Duration = duration
}
}
func SetCertificateRequestCA(ca []byte) CertificateRequestModifier {
return func(cr *v1alpha1.CertificateRequest) {
cr.Status.CA = ca
}
}
func SetCertificateRequestCertificate(cert []byte) CertificateRequestModifier {
return func(cr *v1alpha1.CertificateRequest) {
cr.Status.Certificate = cert
}
}
func SetCertificateRequestStatusCondition(c v1alpha1.CertificateRequestCondition) CertificateRequestModifier {
return func(cr *v1alpha1.CertificateRequest) {
if len(cr.Status.Conditions) == 0 {
cr.Status.Conditions = []v1alpha1.CertificateRequestCondition{c}
return
}
for i, existingC := range cr.Status.Conditions {
if existingC.Type == c.Type {
cr.Status.Conditions[i] = c
return
}
}
cr.Status.Conditions = append(cr.Status.Conditions, c)
}
}
func SetCertificateRequestNamespace(namespace string) CertificateRequestModifier {
return func(cr *v1alpha1.CertificateRequest) {
cr.ObjectMeta.Namespace = namespace
}
}