Add Venafi Issuer implementation

Signed-off-by: James Munnelly <james@munnelly.eu>
This commit is contained in:
James Munnelly 2019-01-23 15:53:25 +00:00
parent c3f696fa1a
commit cb7dbc830c
8 changed files with 813 additions and 0 deletions

View File

@ -33,6 +33,7 @@ filegroup(
"//pkg/issuer/fake:all-srcs",
"//pkg/issuer/selfsigned:all-srcs",
"//pkg/issuer/vault:all-srcs",
"//pkg/issuer/venafi:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],

View File

@ -0,0 +1,61 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"issue.go",
"setup.go",
"venafi.go",
],
importpath = "github.com/jetstack/cert-manager/pkg/issuer/venafi",
visibility = ["//visibility:public"],
deps = [
"//pkg/api/util:go_default_library",
"//pkg/apis/certmanager/v1alpha1:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/issuer:go_default_library",
"//pkg/util/pki:go_default_library",
"//vendor/github.com/Venafi/vcert:go_default_library",
"//vendor/github.com/Venafi/vcert/pkg/certificate:go_default_library",
"//vendor/github.com/Venafi/vcert/pkg/endpoint:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/client-go/listers/core/v1:go_default_library",
"//vendor/k8s.io/klog: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"],
)
go_test(
name = "go_default_test",
srcs = [
"connector_test.go",
"fixture_test.go",
"issue_test.go",
],
embed = [":go_default_library"],
deps = [
"//pkg/apis/certmanager/v1alpha1:go_default_library",
"//pkg/controller/test:go_default_library",
"//pkg/issuer:go_default_library",
"//pkg/util:go_default_library",
"//pkg/util/pki:go_default_library",
"//test/unit/gen:go_default_library",
"//vendor/github.com/Venafi/vcert/pkg/certificate:go_default_library",
"//vendor/github.com/Venafi/vcert/pkg/endpoint:go_default_library",
"//vendor/github.com/Venafi/vcert/pkg/venafi/fake:go_default_library",
"//vendor/github.com/kr/pretty:go_default_library",
],
)

View File

@ -0,0 +1,75 @@
/*
Copyright 2018 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 venafi
import (
"github.com/Venafi/vcert/pkg/certificate"
"github.com/Venafi/vcert/pkg/endpoint"
"github.com/Venafi/vcert/pkg/venafi/fake"
)
type fakeConnector struct {
*fake.Connector
PingFunc func() error
ReadZoneConfigurationFunc func(string) (*endpoint.ZoneConfiguration, error)
RetrieveCertificateFunc func(*certificate.Request) (*certificate.PEMCollection, error)
RequestCertificateFunc func(*certificate.Request, string) (string, error)
RenewCertificateFunc func(*certificate.RenewalRequest) (string, error)
}
func (f fakeConnector) Default() *fakeConnector {
if f.Connector == nil {
f.Connector = fake.NewConnector(true, nil)
}
return &f
}
func (f *fakeConnector) Ping() (err error) {
if f.PingFunc != nil {
return f.PingFunc()
}
return f.Connector.Ping()
}
func (f *fakeConnector) ReadZoneConfiguration(zone string) (config *endpoint.ZoneConfiguration, err error) {
if f.ReadZoneConfigurationFunc != nil {
return f.ReadZoneConfigurationFunc(zone)
}
return f.Connector.ReadZoneConfiguration(zone)
}
func (f *fakeConnector) RetrieveCertificate(req *certificate.Request) (certificates *certificate.PEMCollection, err error) {
if f.RetrieveCertificateFunc != nil {
return f.RetrieveCertificateFunc(req)
}
return f.Connector.RetrieveCertificate(req)
}
func (f *fakeConnector) RequestCertificate(req *certificate.Request, zone string) (requestID string, err error) {
if f.RequestCertificateFunc != nil {
return f.RequestCertificateFunc(req, zone)
}
return f.Connector.RequestCertificate(req, zone)
}
func (f *fakeConnector) RenewCertificate(req *certificate.RenewalRequest) (requestID string, err error) {
if f.RenewCertificateFunc != nil {
return f.RenewCertificateFunc(req)
}
return f.Connector.RenewCertificate(req)
}

View File

@ -0,0 +1,107 @@
/*
Copyright 2018 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 venafi
import (
"context"
"testing"
vfake "github.com/Venafi/vcert/pkg/venafi/fake"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/controller/test"
)
type fixture struct {
Venafi *Venafi
*test.Builder
Issuer v1alpha1.GenericIssuer
Certificate *v1alpha1.Certificate
Client connector
PreFn func(*testing.T, *fixture)
CheckFn func(*testing.T, *fixture, ...interface{})
Err bool
Ctx context.Context
}
func (s *fixture) Setup(t *testing.T) {
if s.Issuer == nil {
s.Issuer = &v1alpha1.Issuer{
Spec: v1alpha1.IssuerSpec{
IssuerConfig: v1alpha1.IssuerConfig{
Venafi: &v1alpha1.VenafiIssuer{},
},
},
}
}
// if a custom client has not been provided, we will use the vcert fake
// which generates certificates and private keys by default
if s.Client == nil {
s.Client = vfake.NewConnector(true, nil)
}
if s.Ctx == nil {
s.Ctx = context.Background()
}
if s.Builder == nil {
// TODO: set default IssuerOptions
// defaultTestAcmeClusterResourceNamespace,
// defaultTestSolverImage,
// default dns01 nameservers
// ambient credentials settings
s.Builder = &test.Builder{}
}
s.Venafi = s.buildFakeVenafi(s.Builder, s.Issuer)
if s.PreFn != nil {
s.PreFn(t, s)
s.Builder.Sync()
}
}
func (s *fixture) Finish(t *testing.T, args ...interface{}) {
defer s.Builder.Stop()
if err := s.Builder.AllReactorsCalled(); err != nil {
t.Errorf("Not all expected reactors were called: %v", err)
}
if err := s.Builder.AllActionsExecuted(); err != nil {
t.Errorf(err.Error())
}
// resync listers before running checks
s.Builder.Sync()
// run custom checks
if s.CheckFn != nil {
s.CheckFn(t, s, args...)
}
}
func (s *fixture) buildFakeVenafi(b *test.Builder, issuer v1alpha1.GenericIssuer) *Venafi {
b.Start()
// TODO: replace this with a call to NewVenafi by somehow modifying it to allow
// injecting the fake venafi client.
v := &Venafi{
issuer: issuer,
Context: s.Context,
resourceNamespace: s.Context.IssuerOptions.ResourceNamespace(issuer),
secretsLister: s.Context.KubeSharedInformerFactory.Core().V1().Secrets().Lister(),
client: s.Client,
}
b.Sync()
return v
}

193
pkg/issuer/venafi/issue.go Normal file
View File

@ -0,0 +1,193 @@
/*
Copyright 2018 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 venafi
import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"strings"
"time"
"github.com/Venafi/vcert/pkg/certificate"
"github.com/Venafi/vcert/pkg/endpoint"
corev1 "k8s.io/api/core/v1"
"k8s.io/klog"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/issuer"
"github.com/jetstack/cert-manager/pkg/util/pki"
)
const (
reasonErrorPrivateKey = "PrivateKey"
)
// Issue will attempt to issue a new certificate from the Venafi Issuer.
// The control flow is as follows:
// - Attempt to retrieve the existing private key
// - If it does not exist, generate one
// - Generate a certificate template
// - Read the zone configuration from the Venafi server
// - Create a Venafi request based on the certificate template
// - Set defaults on the request based on the zone
// - Validate the request against the zone
// - Submit the request
// - Wait for the request to be fulfilled and the certificate to be available
func (v *Venafi) Issue(ctx context.Context, crt *v1alpha1.Certificate) (*issuer.IssueResponse, error) {
v.Recorder.Event(crt, corev1.EventTypeNormal, "Issuing", "Requesting new certificate...")
v.Recorder.Event(crt, corev1.EventTypeNormal, "GenerateKey", "Generating new private key")
// Always generate a new private key, as some Venafi configurations mandate
// unique private keys per issuance.
signeeKey, err := pki.GeneratePrivateKeyForCertificate(crt)
if err != nil {
klog.Errorf("Error getting private key %q for certificate: %v", crt.Spec.SecretName, err)
v.Recorder.Eventf(crt, corev1.EventTypeWarning, "PrivateKeyError", "Error generating certificate private key: %v", err)
// don't trigger a retry. An error from this function implies some
// invalid input parameters, and retrying without updating the
// resource will not help.
return nil, nil
}
// extract the public component of the key
signeePublicKey, err := pki.PublicKeyForPrivateKey(signeeKey)
if err != nil {
klog.Errorf("Error getting public key from private key: %v", err)
return nil, err
}
// We build a x509.Certificate as the vcert library has support for converting
// this into its own internal Certificate Request type.
tmpl, err := pki.GenerateTemplate(crt)
if err != nil {
return nil, err
}
// TODO: we need some way to detect fields are defaulted on the template,
// or otherwise move certificate/csr template defaulting into its own
// function within the PKI package.
// For now, we manually 'fix' the certificate template returned above
if len(crt.Spec.Organization) == 0 {
tmpl.Subject.Organization = []string{}
}
// set the PublicKey field on the certificate template so it can be checked
// by the vcert library
tmpl.PublicKey = signeePublicKey
// Retrieve a copy of the Venafi zone.
// This contains default values and policy control info that we can apply
// and check against locally.
zoneName := v.issuer.GetSpec().Venafi.Zone
zoneCfg, err := v.client.ReadZoneConfiguration(zoneName)
if err != nil {
v.Recorder.Eventf(crt, corev1.EventTypeWarning, "ReadZone", "Failed to read Venafi zone configuration: %v", err)
return nil, err
}
//// Begin building Venafi certificate Request
// Create a vcert Request structure
vreq := newVRequest(tmpl)
// Apply default values from the Venafi zone
zoneCfg.UpdateCertificateRequest(vreq)
err = zoneCfg.ValidateCertificateRequest(vreq)
if err != nil {
// TODO: set a certificate status condition instead of firing an event
// in case this step is particularly chatty
v.Recorder.Eventf(crt, corev1.EventTypeWarning, "Validate", "Failed to validate certificate against Venafi zone: %v", err)
return nil, err
}
// Generate the actual x509 CSR and set it on the vreq
err = certificate.GenerateRequest(vreq, signeeKey)
if err != nil {
v.Recorder.Eventf(crt, corev1.EventTypeWarning, "GenerateCSR", "Failed to generate a CSR for the certificate: %v", err)
return nil, err
}
// certificate.GenerateRequest above sets the CSR field as der encoded bytes
// however, the library actually requires this field to be PEM encoded.
// We decode the DER bytes and run them through the PEM encoder and re-set the
// field before actually calling RequestCertificate.
// TODO: make this weird behaviour go away
vreq.CSR = pem.EncodeToMemory(certificate.GetCertificateRequestPEMBlock(vreq.CSR))
// We mark the request as having a user provided CSR, as we have manually
// generated it in the lines above.
// Setting this will prevent a new private key being generated by vcert.
vreq.CsrOrigin = certificate.UserProvidedCSR
// TODO: better set the timeout here. Right now, we'll block for this amount of time.
vreq.Timeout = time.Minute * 5
// Actually send a request to the Venafi server for a certificate.
requestID, err := v.client.RequestCertificate(vreq, zoneName)
if err != nil {
v.Recorder.Eventf(crt, corev1.EventTypeWarning, "Request", "Failed to request a certificate from Venafi: %v", err)
return nil, err
}
// Set the PickupID so vcert does not have to look it up by the fingerprint
vreq.PickupID = requestID
// TODO: we probably need to check the error response here, as the certificate
// may still be provisioning.
// If so, we may *also* want to consider storing the pickup ID somewhere too
// so we can attempt to retrieve the certificate on the next sync (i.e. wait
// for issuance asynchronously).
pemCollection, err := v.client.RetrieveCertificate(vreq)
// Check some known error types
if err, ok := err.(endpoint.ErrCertificatePending); ok {
v.Recorder.Eventf(crt, corev1.EventTypeWarning, "Retrieve", "Failed to retrieve a certificate from Venafi, still pending: %v", err)
return nil, fmt.Errorf("Venafi certificate still pending: %v", err)
}
if err, ok := err.(endpoint.ErrRetrieveCertificateTimeout); ok {
v.Recorder.Eventf(crt, corev1.EventTypeWarning, "Retrieve", "Failed to retrieve a certificate from Venafi, timed out: %v", err)
return nil, fmt.Errorf("Timed out waiting for certificate: %v", err)
}
if err != nil {
v.Recorder.Eventf(crt, corev1.EventTypeWarning, "Retrieve", "Failed to retrieve a certificate from Venafi: %v", err)
return nil, err
}
// Encode the private key ready to be saved
pk, err := pki.EncodePrivateKey(signeeKey)
if err != nil {
return nil, err
}
// Construct the certificate chain and return the new keypair
cs := append([]string{pemCollection.Certificate}, pemCollection.Chain...)
chain := strings.Join(cs, "\n")
return &issuer.IssueResponse{
PrivateKey: pk,
Certificate: []byte(chain),
// TODO: obtain CA certificate somehow
// CA: []byte{},
}, nil
}
func newVRequest(cert *x509.Certificate) *certificate.Request {
req := certificate.NewRequest(cert)
// overwrite entire Subject block
req.Subject = cert.Subject
return req
}

View File

@ -0,0 +1,173 @@
/*
Copyright 2018 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 venafi
import (
"reflect"
"testing"
"github.com/Venafi/vcert/pkg/endpoint"
cmapi "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"
"github.com/jetstack/cert-manager/pkg/util/pki"
"github.com/jetstack/cert-manager/test/unit/gen"
"github.com/kr/pretty"
)
func checkCertificateIssued(t *testing.T, s *fixture, args ...interface{}) {
returnedCert := args[0].(*cmapi.Certificate)
resp := args[1].(*issuer.IssueResponse)
if err, ok := args[2].(error); ok && err != nil {
t.Errorf("expected no error to be returned, but got: %v", err)
return
}
if !reflect.DeepEqual(returnedCert, s.Certificate) {
t.Errorf("output was not as expected: %s", pretty.Diff(returnedCert, s.Certificate))
}
pk, err := pki.DecodePrivateKeyBytes(resp.PrivateKey)
if err != nil {
t.Errorf("unable to decode private key: %v", err)
return
}
crt, err := pki.DecodeX509CertificateBytes(resp.Certificate)
if err != nil {
t.Errorf("unable to decode x509 certificate: %v", err)
return
}
ok, err := pki.PublicKeyMatchesCertificate(pk.Public(), crt)
if err != nil {
t.Errorf("error checking private key: %v", err)
return
}
if !ok {
t.Errorf("private key does not match certificate")
}
// validate the common name is correct
expectedCN := pki.CommonNameForCertificate(s.Certificate)
if expectedCN != crt.Subject.CommonName {
t.Errorf("expected common name to be %q but it was %q", expectedCN, crt.Subject.CommonName)
}
// validate the dns names are correct
expectedDNSNames := pki.DNSNamesForCertificate(s.Certificate)
if !util.EqualUnsorted(crt.DNSNames, expectedDNSNames) {
t.Errorf("expected dns names to be %q but it was %q", expectedDNSNames, crt.DNSNames)
}
}
func TestIssue(t *testing.T) {
tests := map[string]fixture{
"obtain a certificate with a single dnsname specified": {
Certificate: gen.Certificate("testcrt",
gen.SetCertificateDNSNames("example.com"),
),
CheckFn: checkCertificateIssued,
Err: false,
},
"obtain a certificate with the organization field locked by the venafi zone": {
Certificate: gen.Certificate("testcrt",
gen.SetCertificateDNSNames("example.com"),
),
Client: fakeConnector{
ReadZoneConfigurationFunc: func(zone string) (*endpoint.ZoneConfiguration, error) {
return &endpoint.ZoneConfiguration{
Organization: "testing-org",
OrganizationLocked: true,
}, nil
},
}.Default(),
CheckFn: func(t *testing.T, s *fixture, args ...interface{}) {
checkCertificateIssued(t, s, args...)
resp := args[1].(*issuer.IssueResponse)
x509Cert, err := pki.DecodeX509CertificateBytes(resp.Certificate)
if err != nil {
t.Errorf("could not decode x509 certificate bytes: %v", err)
}
if x509Cert.Subject.Organization[0] != "testing-org" {
t.Errorf("expected organization field to be 'testing-org' but got: %s", x509Cert.Subject.Organization[0])
}
},
Err: false,
},
"obtain a certificate with the organization field defaulted by the venafi zone": {
Certificate: gen.Certificate("testcrt",
gen.SetCertificateDNSNames("example.com"),
),
Client: fakeConnector{
ReadZoneConfigurationFunc: func(zone string) (*endpoint.ZoneConfiguration, error) {
return &endpoint.ZoneConfiguration{
Organization: "testing-org",
OrganizationLocked: false,
}, nil
},
}.Default(),
CheckFn: func(t *testing.T, s *fixture, args ...interface{}) {
checkCertificateIssued(t, s, args...)
resp := args[1].(*issuer.IssueResponse)
x509Cert, err := pki.DecodeX509CertificateBytes(resp.Certificate)
if err != nil {
t.Errorf("could not decode x509 certificate bytes: %v", err)
}
if x509Cert.Subject.Organization[0] != "testing-org" {
t.Errorf("expected organization field to be 'testing-org' but got: %s", x509Cert.Subject.Organization[0])
}
},
Err: false,
},
"obtain a certificate with the organization field set by the certificate": {
Certificate: gen.Certificate("testcrt",
gen.SetCertificateDNSNames("example.com"),
gen.SetCertificateOrganization("testing-crt-org"),
),
CheckFn: func(t *testing.T, s *fixture, args ...interface{}) {
checkCertificateIssued(t, s, args...)
resp := args[1].(*issuer.IssueResponse)
x509Cert, err := pki.DecodeX509CertificateBytes(resp.Certificate)
if err != nil {
t.Errorf("could not decode x509 certificate bytes: %v", err)
}
if x509Cert.Subject.Organization[0] != "testing-crt-org" {
t.Errorf("expected organization field to be 'testing-crt-org' but got: %s", x509Cert.Subject.Organization[0])
}
},
Err: false,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
if test.Builder == nil {
test.Builder = &testpkg.Builder{}
}
test.Setup(t)
certCopy := test.Certificate.DeepCopy()
resp, err := test.Venafi.Issue(test.Ctx, certCopy)
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, certCopy, resp, err)
})
}
}

View File

@ -0,0 +1,52 @@
/*
Copyright 2018 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 venafi
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/klog"
apiutil "github.com/jetstack/cert-manager/pkg/api/util"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
)
func (v *Venafi) Setup(ctx context.Context) error {
err := v.client.Ping()
if err != nil {
klog.Infof("Issuer could not connect to endpoint with provided credentials. Issuer failed to connect to endpoint\n")
apiutil.SetIssuerCondition(v.issuer, v1alpha1.IssuerConditionReady, v1alpha1.ConditionFalse,
"ErrorPing", fmt.Sprintf("Failed to connect to Venafi endpoint"))
return fmt.Errorf("error verifying Venafi client: %s", err.Error())
}
// If it does not already have a 'ready' condition, we'll also log an event
// to make it really clear to users that this Issuer is ready.
if !apiutil.IssuerHasCondition(v.issuer, v1alpha1.IssuerCondition{
Type: v1alpha1.IssuerConditionReady,
Status: v1alpha1.ConditionTrue,
}) {
v.Recorder.Eventf(v.issuer, corev1.EventTypeNormal, "Ready", "Verified issuer with Venafi server")
}
klog.Info("Venafi issuer started")
apiutil.SetIssuerCondition(v.issuer, v1alpha1.IssuerConditionReady, v1alpha1.ConditionTrue, "Venafi issuer started", "Venafi issuer started")
return nil
}

151
pkg/issuer/venafi/venafi.go Normal file
View File

@ -0,0 +1,151 @@
/*
Copyright 2018 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 venafi
import (
"fmt"
"github.com/Venafi/vcert"
"github.com/Venafi/vcert/pkg/certificate"
"github.com/Venafi/vcert/pkg/endpoint"
corev1 "k8s.io/api/core/v1"
corelisters "k8s.io/client-go/listers/core/v1"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/controller"
"github.com/jetstack/cert-manager/pkg/issuer"
)
const (
tppUsernameKey = "username"
tppPasswordKey = "password"
defaultAPIKeyKey = "api-key"
)
// Venafi is a implementation of govcert library to manager certificates from TPP or Venafi Cloud
type Venafi struct {
issuer cmapi.GenericIssuer
*controller.Context
// Namespace in which to read resources related to this Issuer from.
// For Issuers, this will be the namespace of the Issuer.
// For ClusterIssuers, this will be the cluster resource namespace.
resourceNamespace string
secretsLister corelisters.SecretLister
client connector
}
// connector exposes a subset of the vcert Connector interface to make stubbing
// out its functionality during tests easier.
type connector interface {
Ping() (err error)
ReadZoneConfiguration(zone string) (config *endpoint.ZoneConfiguration, err error)
RequestCertificate(req *certificate.Request, zone string) (requestID string, err error)
RetrieveCertificate(req *certificate.Request) (certificates *certificate.PEMCollection, err error)
RenewCertificate(req *certificate.RenewalRequest) (requestID string, err error)
}
func NewVenafi(ctx *controller.Context, issuer cmapi.GenericIssuer) (issuer.Interface, error) {
secretsLister := ctx.KubeSharedInformerFactory.Core().V1().Secrets().Lister()
resourceNamespace := ctx.IssuerOptions.ResourceNamespace(issuer)
cfg, err := configForIssuer(issuer, secretsLister, resourceNamespace)
if err != nil {
ctx.Recorder.Eventf(issuer, corev1.EventTypeWarning, "FailedInit", "Failed to initialise issuer: %v", err)
return nil, err
}
client, err := vcert.NewClient(cfg)
if err != nil {
ctx.Recorder.Eventf(issuer, corev1.EventTypeWarning, "FailedInit", "Failed to create Venafi client: %v", err)
return nil, fmt.Errorf("error creating Venafi client: %s", err.Error())
}
return &Venafi{
issuer: issuer,
Context: ctx,
resourceNamespace: resourceNamespace,
secretsLister: secretsLister,
client: client,
}, nil
}
// configForIssuer will convert a cert-manager Venafi issuer into a vcert.Config
// that can be used to instantiate an API client.
func configForIssuer(iss cmapi.GenericIssuer, secretsLister corelisters.SecretLister, resourceNamespace string) (*vcert.Config, error) {
venCfg := iss.GetSpec().Venafi
switch {
case venCfg.TPP != nil:
tpp := venCfg.TPP
tppSecret, err := secretsLister.Secrets(resourceNamespace).Get(tpp.CredentialsRef.Name)
if err != nil {
return nil, fmt.Errorf("error loading TPP credentials: %v", err)
}
username := tppSecret.Data[tppUsernameKey]
password := tppSecret.Data[tppPasswordKey]
caBundle := ""
if len(tpp.CABundle) > 0 {
caBundle = string(tpp.CABundle)
}
return &vcert.Config{
ConnectorType: endpoint.ConnectorTypeTPP,
BaseUrl: tpp.URL,
Zone: venCfg.Zone,
LogVerbose: venCfg.Verbose,
ConnectionTrust: caBundle,
Credentials: &endpoint.Authentication{
User: string(username),
Password: string(password),
},
}, nil
case venCfg.Cloud != nil:
cloud := venCfg.Cloud
cloudSecret, err := secretsLister.Secrets(resourceNamespace).Get(cloud.APIKeySecretRef.Name)
if err != nil {
return nil, fmt.Errorf("error loading TPP credentials: %v", err)
}
k := defaultAPIKeyKey
if cloud.APIKeySecretRef.Key != "" {
k = cloud.APIKeySecretRef.Key
}
apiKey := cloudSecret.Data[k]
return &vcert.Config{
ConnectorType: endpoint.ConnectorTypeCloud,
BaseUrl: cloud.URL,
Zone: venCfg.Zone,
LogVerbose: venCfg.Verbose,
Credentials: &endpoint.Authentication{
APIKey: string(apiKey),
},
}, nil
default:
return nil, fmt.Errorf("neither Venafi Cloud or TPP configuration found")
}
}
func init() {
controller.RegisterIssuer(controller.IssuerVenafi, NewVenafi)
}