Adds Vault CertificateSigningRequest Issuer controller
Signed-off-by: joshvanl <vleeuwenjoshua@gmail.com>
This commit is contained in:
parent
59c2a2d9f4
commit
f5b609e446
@ -73,6 +73,7 @@ filegroup(
|
||||
"//pkg/controller/certificatesigningrequests/fake:all-srcs",
|
||||
"//pkg/controller/certificatesigningrequests/selfsigned:all-srcs",
|
||||
"//pkg/controller/certificatesigningrequests/util:all-srcs",
|
||||
"//pkg/controller/certificatesigningrequests/vault:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
|
||||
68
pkg/controller/certificatesigningrequests/vault/BUILD.bazel
Normal file
68
pkg/controller/certificatesigningrequests/vault/BUILD.bazel
Normal file
@ -0,0 +1,68 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["vault.go"],
|
||||
importpath = "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/vault",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/api/util:go_default_library",
|
||||
"//pkg/apis/certmanager/v1:go_default_library",
|
||||
"//pkg/apis/experimental/v1alpha1:go_default_library",
|
||||
"//pkg/controller:go_default_library",
|
||||
"//pkg/controller/certificatesigningrequests:go_default_library",
|
||||
"//pkg/controller/certificatesigningrequests/util:go_default_library",
|
||||
"//pkg/internal/vault:go_default_library",
|
||||
"//pkg/logs:go_default_library",
|
||||
"//pkg/util/pki:go_default_library",
|
||||
"@io_k8s_api//certificates/v1:go_default_library",
|
||||
"@io_k8s_api//core/v1:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/api/errors:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
|
||||
"@io_k8s_client_go//kubernetes/typed/certificates/v1:go_default_library",
|
||||
"@io_k8s_client_go//listers/core/v1:go_default_library",
|
||||
"@io_k8s_client_go//tools/record:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["vault_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/api/util:go_default_library",
|
||||
"//pkg/apis/certmanager:go_default_library",
|
||||
"//pkg/apis/certmanager/v1:go_default_library",
|
||||
"//pkg/apis/meta/v1:go_default_library",
|
||||
"//pkg/controller/certificatesigningrequests:go_default_library",
|
||||
"//pkg/controller/certificatesigningrequests/util:go_default_library",
|
||||
"//pkg/controller/test:go_default_library",
|
||||
"//pkg/internal/vault:go_default_library",
|
||||
"//pkg/internal/vault/fake:go_default_library",
|
||||
"//test/unit/gen:go_default_library",
|
||||
"@io_k8s_api//authorization/v1:go_default_library",
|
||||
"@io_k8s_api//certificates/v1:go_default_library",
|
||||
"@io_k8s_api//core/v1:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/api/errors:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/runtime:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/runtime/schema:go_default_library",
|
||||
"@io_k8s_client_go//listers/core/v1:go_default_library",
|
||||
"@io_k8s_client_go//testing:go_default_library",
|
||||
"@io_k8s_utils//clock/testing:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
156
pkg/controller/certificatesigningrequests/vault/vault.go
Normal file
156
pkg/controller/certificatesigningrequests/vault/vault.go
Normal file
@ -0,0 +1,156 @@
|
||||
/*
|
||||
Copyright 2021 The cert-manager 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 vault
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
certificatesv1 "k8s.io/api/certificates/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1"
|
||||
corelisters "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/record"
|
||||
|
||||
apiutil "github.com/jetstack/cert-manager/pkg/api/util"
|
||||
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
|
||||
experimentalapi "github.com/jetstack/cert-manager/pkg/apis/experimental/v1alpha1"
|
||||
controllerpkg "github.com/jetstack/cert-manager/pkg/controller"
|
||||
"github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests"
|
||||
"github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/util"
|
||||
internalvault "github.com/jetstack/cert-manager/pkg/internal/vault"
|
||||
logf "github.com/jetstack/cert-manager/pkg/logs"
|
||||
"github.com/jetstack/cert-manager/pkg/util/pki"
|
||||
)
|
||||
|
||||
const (
|
||||
CSRControllerName = "certificatesigningrequests-issuer-vault"
|
||||
)
|
||||
|
||||
type signingFn func(*x509.Certificate, *x509.Certificate, crypto.PublicKey, interface{}) ([]byte, *x509.Certificate, error)
|
||||
|
||||
// Vault is a controller for signing Kubernetes CertificateSigningRequest
|
||||
// using Vault Issuers.
|
||||
type Vault struct {
|
||||
issuerOptions controllerpkg.IssuerOptions
|
||||
secretsLister corelisters.SecretLister
|
||||
|
||||
recorder record.EventRecorder
|
||||
|
||||
certClient certificatesclient.CertificateSigningRequestInterface
|
||||
clientBuilder internalvault.ClientBuilder
|
||||
}
|
||||
|
||||
func init() {
|
||||
// create certificate signing request controller for vault issuer
|
||||
controllerpkg.Register(CSRControllerName, func(ctx *controllerpkg.Context) (controllerpkg.Interface, error) {
|
||||
return controllerpkg.NewBuilder(ctx, CSRControllerName).
|
||||
For(certificatesigningrequests.New(apiutil.IssuerVault, NewVault(ctx))).
|
||||
Complete()
|
||||
})
|
||||
}
|
||||
|
||||
func NewVault(ctx *controllerpkg.Context) *Vault {
|
||||
return &Vault{
|
||||
issuerOptions: ctx.IssuerOptions,
|
||||
secretsLister: ctx.KubeSharedInformerFactory.Core().V1().Secrets().Lister(),
|
||||
recorder: ctx.Recorder,
|
||||
certClient: ctx.Client.CertificatesV1().CertificateSigningRequests(),
|
||||
clientBuilder: internalvault.New,
|
||||
}
|
||||
}
|
||||
|
||||
// Sign attempts to sign the given CertificateSigningRequest based on the
|
||||
// provided Vault Issuer or ClusterIssuer. This function will update the
|
||||
// resource if signing was successful. Returns an error which, if not nil,
|
||||
// should trigger a retry.
|
||||
func (v *Vault) Sign(ctx context.Context, csr *certificatesv1.CertificateSigningRequest, issuerObj cmapi.GenericIssuer) error {
|
||||
log := logf.FromContext(ctx, "sign")
|
||||
log = logf.WithRelatedResource(log, issuerObj)
|
||||
|
||||
resourceNamespace := v.issuerOptions.ResourceNamespace(issuerObj)
|
||||
|
||||
client, err := v.clientBuilder(resourceNamespace, v.secretsLister, issuerObj)
|
||||
if apierrors.IsNotFound(err) {
|
||||
message := "Required secret resource not found"
|
||||
log.Error(err, message)
|
||||
v.recorder.Event(csr, corev1.EventTypeWarning, "SecretNotFound", message)
|
||||
util.CertificateSigningRequestSetFailed(csr, "SecretNotFound", message)
|
||||
_, err := v.certClient.UpdateStatus(ctx, csr, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
message := fmt.Sprintf("Failed to initialise vault client for signing: %s", err)
|
||||
log.Error(err, message)
|
||||
v.recorder.Event(csr, corev1.EventTypeWarning, "ErrorVaultInit", message)
|
||||
return err
|
||||
}
|
||||
|
||||
duration, err := pki.DurationFromCertificateSigningRequest(csr)
|
||||
if err != nil {
|
||||
message := fmt.Sprintf("Failed to parse requested duration: %s", err)
|
||||
log.Error(err, message)
|
||||
v.recorder.Event(csr, corev1.EventTypeWarning, "ErrorParseDuration", message)
|
||||
util.CertificateSigningRequestSetFailed(csr, "ErrorParseDuration", message)
|
||||
_, err := v.certClient.UpdateStatus(ctx, csr, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
certPEM, caPEM, err := client.Sign(csr.Spec.Request, duration)
|
||||
if err != nil {
|
||||
message := fmt.Sprintf("Vault failed to sign: %s", err)
|
||||
log.Error(err, message)
|
||||
v.recorder.Event(csr, corev1.EventTypeWarning, "ErrorSigning", message)
|
||||
util.CertificateSigningRequestSetFailed(csr, "ErrorSigning", message)
|
||||
_, err := v.certClient.UpdateStatus(ctx, csr, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
log.V(logf.DebugLevel).Info("certificate issued")
|
||||
|
||||
// Update the status.certificate first so that the sync from updating will
|
||||
// not cause another issuance before setting the CA.
|
||||
csr.Status.Certificate = certPEM
|
||||
csr, err = v.certClient.UpdateStatus(ctx, csr, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
message := "Error updating certificate"
|
||||
v.recorder.Eventf(csr, corev1.EventTypeWarning, "ErrorUpdate", "%s: %s", message, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if csr.Annotations == nil {
|
||||
csr.Annotations = make(map[string]string)
|
||||
}
|
||||
csr.Annotations[experimentalapi.CertificateSigningRequestCAAnnotationKey] = base64.StdEncoding.EncodeToString(caPEM)
|
||||
_, err = v.certClient.Update(ctx, csr, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
message := fmt.Sprintf("Error setting %q", experimentalapi.CertificateSigningRequestCAAnnotationKey)
|
||||
v.recorder.Eventf(csr, corev1.EventTypeWarning, "ErrorCAUpdate", "%s: %s", message, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.V(logf.DebugLevel).Info("vault certificate issued")
|
||||
v.recorder.Event(csr, corev1.EventTypeNormal, "CertificateIssued", "Certificate signed successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
460
pkg/controller/certificatesigningrequests/vault/vault_test.go
Normal file
460
pkg/controller/certificatesigningrequests/vault/vault_test.go
Normal file
@ -0,0 +1,460 @@
|
||||
/*
|
||||
Copyright 2021 The cert-manager 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 vault
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
authzv1 "k8s.io/api/authorization/v1"
|
||||
certificatesv1 "k8s.io/api/certificates/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
corelisters "k8s.io/client-go/listers/core/v1"
|
||||
coretesting "k8s.io/client-go/testing"
|
||||
fakeclock "k8s.io/utils/clock/testing"
|
||||
|
||||
apiutil "github.com/jetstack/cert-manager/pkg/api/util"
|
||||
"github.com/jetstack/cert-manager/pkg/apis/certmanager"
|
||||
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
|
||||
cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
|
||||
"github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests"
|
||||
"github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/util"
|
||||
testpkg "github.com/jetstack/cert-manager/pkg/controller/test"
|
||||
internalvault "github.com/jetstack/cert-manager/pkg/internal/vault"
|
||||
fakevault "github.com/jetstack/cert-manager/pkg/internal/vault/fake"
|
||||
"github.com/jetstack/cert-manager/test/unit/gen"
|
||||
)
|
||||
|
||||
var (
|
||||
fixedClockStart = time.Now()
|
||||
fixedClock = fakeclock.NewFakeClock(fixedClockStart)
|
||||
)
|
||||
|
||||
func TestProcessItem(t *testing.T) {
|
||||
metaFixedClockStart := metav1.NewTime(fixedClockStart)
|
||||
util.Clock = fixedClock
|
||||
|
||||
baseIssuer := gen.Issuer("test-issuer",
|
||||
gen.SetIssuerVault(cmapi.VaultIssuer{
|
||||
Auth: cmapi.VaultAuth{
|
||||
Kubernetes: &cmapi.VaultKubernetesAuth{
|
||||
Path: "/v1/kubernetes",
|
||||
Role: "kube-pki",
|
||||
SecretRef: cmmeta.SecretKeySelector{
|
||||
Key: "token",
|
||||
LocalObjectReference: cmmeta.LocalObjectReference{
|
||||
Name: "sa-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
gen.AddIssuerCondition(cmapi.IssuerCondition{
|
||||
Type: cmapi.IssuerConditionReady,
|
||||
Status: cmmeta.ConditionTrue,
|
||||
}),
|
||||
)
|
||||
|
||||
csrPEM, _, err := gen.CSR(x509.RSA)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
baseCSR := gen.CertificateSigningRequest("test-cr",
|
||||
gen.SetCertificateSigningRequestRequest(csrPEM),
|
||||
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/default-unit-test-ns.test-issuer"),
|
||||
gen.SetCertificateSigningRequestDuration("1440h"),
|
||||
gen.SetCertificateSigningRequestUsername("user-1"),
|
||||
gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}),
|
||||
gen.SetCertificateSigningRequestUID("uid-1"),
|
||||
gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{
|
||||
"extra": []string{"1", "2"},
|
||||
}),
|
||||
)
|
||||
|
||||
tests := map[string]struct {
|
||||
builder *testpkg.Builder
|
||||
csr *certificatesv1.CertificateSigningRequest
|
||||
clientBuilder internalvault.ClientBuilder
|
||||
expectedErr bool
|
||||
}{
|
||||
"a CertificateSigningRequest without an approved condition should do nothing": {
|
||||
csr: gen.CertificateSigningRequestFrom(baseCSR),
|
||||
builder: &testpkg.Builder{
|
||||
CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
|
||||
},
|
||||
},
|
||||
"a CertificateSigningRequest with a denied condition should do nothing": {
|
||||
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
||||
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1.CertificateDenied,
|
||||
Status: corev1.ConditionTrue,
|
||||
}),
|
||||
),
|
||||
builder: &testpkg.Builder{
|
||||
CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
|
||||
ExpectedEvents: []string{},
|
||||
ExpectedActions: nil,
|
||||
},
|
||||
},
|
||||
"an approved CSR where the vault client builder returns a not found error should mark as Failed": {
|
||||
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
||||
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1.CertificateApproved,
|
||||
Status: corev1.ConditionTrue,
|
||||
}),
|
||||
),
|
||||
clientBuilder: func(_ string, _ corelisters.SecretLister, _ cmapi.GenericIssuer) (internalvault.Interface, error) {
|
||||
return nil, apierrors.NewNotFound(schema.GroupResource{}, "test-secret")
|
||||
},
|
||||
builder: &testpkg.Builder{
|
||||
CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
|
||||
ExpectedEvents: []string{
|
||||
"Warning SecretNotFound Required secret resource not found",
|
||||
},
|
||||
ExpectedActions: []testpkg.Action{
|
||||
testpkg.NewAction(coretesting.NewCreateAction(
|
||||
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
||||
"",
|
||||
&authzv1.SubjectAccessReview{
|
||||
Spec: authzv1.SubjectAccessReviewSpec{
|
||||
User: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string]authzv1.ExtraValue{
|
||||
"extra": []string{"1", "2"},
|
||||
},
|
||||
UID: "uid-1",
|
||||
|
||||
ResourceAttributes: &authzv1.ResourceAttributes{
|
||||
Group: certmanager.GroupName,
|
||||
Resource: "signers",
|
||||
Verb: "reference",
|
||||
Namespace: baseIssuer.Namespace,
|
||||
Name: baseIssuer.Name,
|
||||
Version: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
)),
|
||||
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
|
||||
certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
|
||||
"status",
|
||||
"",
|
||||
gen.CertificateSigningRequestFrom(baseCSR.DeepCopy(),
|
||||
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1.CertificateApproved,
|
||||
Status: corev1.ConditionTrue,
|
||||
}),
|
||||
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1.CertificateFailed,
|
||||
Status: corev1.ConditionTrue,
|
||||
Reason: "SecretNotFound",
|
||||
Message: "Required secret resource not found",
|
||||
LastTransitionTime: metaFixedClockStart,
|
||||
LastUpdateTime: metaFixedClockStart,
|
||||
}),
|
||||
),
|
||||
)),
|
||||
},
|
||||
},
|
||||
},
|
||||
"an approved CSR where the vault client builder returns a generic error should return error to retry": {
|
||||
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
||||
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1.CertificateApproved,
|
||||
Status: corev1.ConditionTrue,
|
||||
}),
|
||||
),
|
||||
clientBuilder: func(_ string, _ corelisters.SecretLister, _ cmapi.GenericIssuer) (internalvault.Interface, error) {
|
||||
return nil, errors.New("generic error")
|
||||
},
|
||||
expectedErr: true,
|
||||
builder: &testpkg.Builder{
|
||||
CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
|
||||
ExpectedEvents: []string{
|
||||
"Warning ErrorVaultInit Failed to initialise vault client for signing: generic error",
|
||||
},
|
||||
ExpectedActions: []testpkg.Action{
|
||||
testpkg.NewAction(coretesting.NewCreateAction(
|
||||
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
||||
"",
|
||||
&authzv1.SubjectAccessReview{
|
||||
Spec: authzv1.SubjectAccessReviewSpec{
|
||||
User: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string]authzv1.ExtraValue{
|
||||
"extra": []string{"1", "2"},
|
||||
},
|
||||
UID: "uid-1",
|
||||
|
||||
ResourceAttributes: &authzv1.ResourceAttributes{
|
||||
Group: certmanager.GroupName,
|
||||
Resource: "signers",
|
||||
Verb: "reference",
|
||||
Namespace: baseIssuer.Namespace,
|
||||
Name: baseIssuer.Name,
|
||||
Version: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
)),
|
||||
},
|
||||
},
|
||||
},
|
||||
"an approved CSR which has an invalid duration string should be marked as Failed": {
|
||||
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
||||
gen.SetCertificateSigningRequestDuration("bad-duration"),
|
||||
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1.CertificateApproved,
|
||||
Status: corev1.ConditionTrue,
|
||||
}),
|
||||
),
|
||||
clientBuilder: func(_ string, _ corelisters.SecretLister, _ cmapi.GenericIssuer) (internalvault.Interface, error) {
|
||||
return fakevault.New(), nil
|
||||
},
|
||||
builder: &testpkg.Builder{
|
||||
CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
|
||||
ExpectedEvents: []string{
|
||||
`Warning ErrorParseDuration Failed to parse requested duration: failed to parse requested duration on annotation "experimental.cert-manager.io/request-duration": time: invalid duration "bad-duration"`,
|
||||
},
|
||||
ExpectedActions: []testpkg.Action{
|
||||
testpkg.NewAction(coretesting.NewCreateAction(
|
||||
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
||||
"",
|
||||
&authzv1.SubjectAccessReview{
|
||||
Spec: authzv1.SubjectAccessReviewSpec{
|
||||
User: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string]authzv1.ExtraValue{
|
||||
"extra": []string{"1", "2"},
|
||||
},
|
||||
UID: "uid-1",
|
||||
|
||||
ResourceAttributes: &authzv1.ResourceAttributes{
|
||||
Group: certmanager.GroupName,
|
||||
Resource: "signers",
|
||||
Verb: "reference",
|
||||
Namespace: baseIssuer.Namespace,
|
||||
Name: baseIssuer.Name,
|
||||
Version: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
)),
|
||||
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
|
||||
certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
|
||||
"status",
|
||||
"",
|
||||
gen.CertificateSigningRequestFrom(baseCSR.DeepCopy(),
|
||||
gen.SetCertificateSigningRequestDuration("bad-duration"),
|
||||
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1.CertificateApproved,
|
||||
Status: corev1.ConditionTrue,
|
||||
}),
|
||||
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1.CertificateFailed,
|
||||
Status: corev1.ConditionTrue,
|
||||
Reason: "ErrorParseDuration",
|
||||
Message: `Failed to parse requested duration: failed to parse requested duration on annotation "experimental.cert-manager.io/request-duration": time: invalid duration "bad-duration"`,
|
||||
LastTransitionTime: metaFixedClockStart,
|
||||
LastUpdateTime: metaFixedClockStart,
|
||||
}),
|
||||
),
|
||||
)),
|
||||
},
|
||||
},
|
||||
},
|
||||
"an approved CSR which errors when invoking sign on the vault client should mark the CSR as Failed": {
|
||||
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
||||
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1.CertificateApproved,
|
||||
Status: corev1.ConditionTrue,
|
||||
}),
|
||||
),
|
||||
clientBuilder: func(_ string, _ corelisters.SecretLister, _ cmapi.GenericIssuer) (internalvault.Interface, error) {
|
||||
return fakevault.New().WithSign(nil, nil, errors.New("sign error")), nil
|
||||
},
|
||||
builder: &testpkg.Builder{
|
||||
CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
|
||||
ExpectedEvents: []string{
|
||||
"Warning ErrorSigning Vault failed to sign: sign error",
|
||||
},
|
||||
ExpectedActions: []testpkg.Action{
|
||||
testpkg.NewAction(coretesting.NewCreateAction(
|
||||
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
||||
"",
|
||||
&authzv1.SubjectAccessReview{
|
||||
Spec: authzv1.SubjectAccessReviewSpec{
|
||||
User: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string]authzv1.ExtraValue{
|
||||
"extra": []string{"1", "2"},
|
||||
},
|
||||
UID: "uid-1",
|
||||
|
||||
ResourceAttributes: &authzv1.ResourceAttributes{
|
||||
Group: certmanager.GroupName,
|
||||
Resource: "signers",
|
||||
Verb: "reference",
|
||||
Namespace: baseIssuer.Namespace,
|
||||
Name: baseIssuer.Name,
|
||||
Version: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
)),
|
||||
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
|
||||
certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
|
||||
"status",
|
||||
"",
|
||||
gen.CertificateSigningRequestFrom(baseCSR.DeepCopy(),
|
||||
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1.CertificateApproved,
|
||||
Status: corev1.ConditionTrue,
|
||||
}),
|
||||
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1.CertificateFailed,
|
||||
Status: corev1.ConditionTrue,
|
||||
Reason: "ErrorSigning",
|
||||
Message: "Vault failed to sign: sign error",
|
||||
LastTransitionTime: metaFixedClockStart,
|
||||
LastUpdateTime: metaFixedClockStart,
|
||||
}),
|
||||
),
|
||||
)),
|
||||
},
|
||||
},
|
||||
},
|
||||
"an approved CSR which successfully signs, should update the Certificate and CA fields": {
|
||||
csr: gen.CertificateSigningRequestFrom(baseCSR,
|
||||
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1.CertificateApproved,
|
||||
Status: corev1.ConditionTrue,
|
||||
}),
|
||||
),
|
||||
clientBuilder: func(_ string, _ corelisters.SecretLister, _ cmapi.GenericIssuer) (internalvault.Interface, error) {
|
||||
return fakevault.New().WithSign([]byte("signed-cert"), []byte("signing-ca"), nil), nil
|
||||
},
|
||||
builder: &testpkg.Builder{
|
||||
CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
|
||||
ExpectedEvents: []string{
|
||||
"Normal CertificateIssued Certificate signed successfully",
|
||||
},
|
||||
ExpectedActions: []testpkg.Action{
|
||||
testpkg.NewAction(coretesting.NewCreateAction(
|
||||
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
|
||||
"",
|
||||
&authzv1.SubjectAccessReview{
|
||||
Spec: authzv1.SubjectAccessReviewSpec{
|
||||
User: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string]authzv1.ExtraValue{
|
||||
"extra": []string{"1", "2"},
|
||||
},
|
||||
UID: "uid-1",
|
||||
|
||||
ResourceAttributes: &authzv1.ResourceAttributes{
|
||||
Group: certmanager.GroupName,
|
||||
Resource: "signers",
|
||||
Verb: "reference",
|
||||
Namespace: baseIssuer.Namespace,
|
||||
Name: baseIssuer.Name,
|
||||
Version: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
)),
|
||||
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
|
||||
certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
|
||||
"status",
|
||||
"",
|
||||
gen.CertificateSigningRequestFrom(baseCSR.DeepCopy(),
|
||||
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1.CertificateApproved,
|
||||
Status: corev1.ConditionTrue,
|
||||
}),
|
||||
gen.SetCertificateSigningRequestCertificate([]byte("signed-cert")),
|
||||
),
|
||||
)),
|
||||
testpkg.NewAction(coretesting.NewUpdateAction(
|
||||
certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
|
||||
"",
|
||||
gen.CertificateSigningRequestFrom(baseCSR.DeepCopy(),
|
||||
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1.CertificateApproved,
|
||||
Status: corev1.ConditionTrue,
|
||||
}),
|
||||
gen.SetCertificateSigningRequestCertificate([]byte("signed-cert")),
|
||||
gen.SetCertificateSigningRequestCA([]byte("signing-ca")),
|
||||
),
|
||||
)),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if test.csr != nil {
|
||||
test.builder.KubeObjects = append(test.builder.KubeObjects, test.csr)
|
||||
}
|
||||
|
||||
fixedClock.SetTime(fixedClockStart)
|
||||
test.builder.Clock = fixedClock
|
||||
test.builder.T = t
|
||||
test.builder.Init()
|
||||
|
||||
// Always return true for SubjectAccessReviews in tests
|
||||
test.builder.FakeKubeClient().PrependReactor("create", "*", func(action coretesting.Action) (bool, runtime.Object, error) {
|
||||
if action.GetResource() != authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews") {
|
||||
return false, nil, nil
|
||||
}
|
||||
return true, &authzv1.SubjectAccessReview{
|
||||
Status: authzv1.SubjectAccessReviewStatus{
|
||||
Allowed: true,
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
|
||||
defer test.builder.Stop()
|
||||
|
||||
vault := NewVault(test.builder.Context)
|
||||
vault.clientBuilder = test.clientBuilder
|
||||
|
||||
controller := certificatesigningrequests.New(apiutil.IssuerVault, vault)
|
||||
controller.Register(test.builder.Context)
|
||||
test.builder.Start()
|
||||
|
||||
err := controller.ProcessItem(context.Background(), test.csr.Name)
|
||||
if err != nil && !test.expectedErr {
|
||||
t.Errorf("expected to not get an error, but got: %v", err)
|
||||
}
|
||||
if err == nil && test.expectedErr {
|
||||
t.Errorf("expected to get an error but did not get one")
|
||||
}
|
||||
|
||||
test.builder.CheckAndFinish(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -31,15 +31,9 @@ import (
|
||||
// GenerateTemplateFromCertificateSigningRequest will create an
|
||||
// *x509.Certificate from the given CertificateSigningRequest resource
|
||||
func GenerateTemplateFromCertificateSigningRequest(csr *certificatesv1.CertificateSigningRequest) (*x509.Certificate, error) {
|
||||
duration := cmapi.DefaultCertificateDuration
|
||||
requestedDuration, ok := csr.Annotations[experimentalapi.CertificateSigningRequestDurationAnnotationKey]
|
||||
if ok {
|
||||
dur, err := time.ParseDuration(requestedDuration)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse requested duration on annotation %q: %w",
|
||||
experimentalapi.CertificateSigningRequestDurationAnnotationKey, err)
|
||||
}
|
||||
duration = dur
|
||||
duration, err := DurationFromCertificateSigningRequest(csr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ku, eku, err := BuildKeyUsagesKube(csr.Spec.Usages)
|
||||
@ -52,6 +46,25 @@ func GenerateTemplateFromCertificateSigningRequest(csr *certificatesv1.Certifica
|
||||
return GenerateTemplateFromCSRPEMWithUsages(csr.Spec.Request, duration, isCA, ku, eku)
|
||||
}
|
||||
|
||||
// DurationFromCertificateSigningRequest will return the time.Duration of the
|
||||
// requested duration on the CertificateSigningRequest. If the annotation is
|
||||
// empty, will return the cert-manager default certificate duration
|
||||
func DurationFromCertificateSigningRequest(csr *certificatesv1.CertificateSigningRequest) (time.Duration, error) {
|
||||
requestedDuration, ok := csr.Annotations[experimentalapi.CertificateSigningRequestDurationAnnotationKey]
|
||||
if !ok {
|
||||
// Return default certificate duration if one not requested
|
||||
return cmapi.DefaultCertificateDuration, nil
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(requestedDuration)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("failed to parse requested duration on annotation %q: %w",
|
||||
experimentalapi.CertificateSigningRequestDurationAnnotationKey, err)
|
||||
}
|
||||
|
||||
return duration, nil
|
||||
}
|
||||
|
||||
func BuildKeyUsagesKube(usages []certificatesv1.KeyUsage) (x509.KeyUsage, []x509.ExtKeyUsage, error) {
|
||||
var unk []certificatesv1.KeyUsage
|
||||
if len(usages) == 0 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user