Link ingress-shim into main controller binary

This commit is contained in:
James Munnelly 2018-04-23 12:43:15 +01:00
parent b43f294a0a
commit fdb8f2bf40
13 changed files with 81 additions and 390 deletions

View File

@ -124,7 +124,11 @@ func buildControllerContext(opts *options.ControllerOptions) (*controller.Contex
ClusterIssuerAmbientCredentials: opts.ClusterIssuerAmbientCredentials,
IssuerAmbientCredentials: opts.IssuerAmbientCredentials,
}),
ClusterResourceNamespace: opts.ClusterResourceNamespace,
ClusterResourceNamespace: opts.ClusterResourceNamespace,
DefaultIssuerName: opts.DefaultIssuerName,
DefaultIssuerKind: opts.DefaultIssuerKind,
DefaultACMEIssuerChallengeType: opts.DefaultACMEIssuerChallengeType,
DefaultACMEIssuerDNS01ProviderName: opts.DefaultACMEIssuerDNS01ProviderName,
}, kubeCfg, nil
}

View File

@ -23,6 +23,12 @@ type ControllerOptions struct {
ClusterIssuerAmbientCredentials bool
IssuerAmbientCredentials bool
// Default issuer/certificates details consumed by ingress-shim
DefaultIssuerName string
DefaultIssuerKind string
DefaultACMEIssuerChallengeType string
DefaultACMEIssuerDNS01ProviderName string
}
const (
@ -37,6 +43,11 @@ const (
defaultClusterIssuerAmbientCredentials = true
defaultIssuerAmbientCredentials = false
defaultTLSACMEIssuerName = ""
defaultTLSACMEIssuerKind = "Issuer"
defaultACMEIssuerChallengeType = "http01"
defaultACMEIssuerDNS01ProviderName = ""
)
var (
@ -45,15 +56,19 @@ var (
func NewControllerOptions() *ControllerOptions {
return &ControllerOptions{
APIServerHost: defaultAPIServerHost,
ClusterResourceNamespace: defaultClusterResourceNamespace,
LeaderElect: defaultLeaderElect,
LeaderElectionNamespace: defaultLeaderElectionNamespace,
LeaderElectionLeaseDuration: defaultLeaderElectionLeaseDuration,
LeaderElectionRenewDeadline: defaultLeaderElectionRenewDeadline,
LeaderElectionRetryPeriod: defaultLeaderElectionRetryPeriod,
ClusterIssuerAmbientCredentials: defaultClusterIssuerAmbientCredentials,
IssuerAmbientCredentials: defaultIssuerAmbientCredentials,
APIServerHost: defaultAPIServerHost,
ClusterResourceNamespace: defaultClusterResourceNamespace,
LeaderElect: defaultLeaderElect,
LeaderElectionNamespace: defaultLeaderElectionNamespace,
LeaderElectionLeaseDuration: defaultLeaderElectionLeaseDuration,
LeaderElectionRenewDeadline: defaultLeaderElectionRenewDeadline,
LeaderElectionRetryPeriod: defaultLeaderElectionRetryPeriod,
ClusterIssuerAmbientCredentials: defaultClusterIssuerAmbientCredentials,
IssuerAmbientCredentials: defaultIssuerAmbientCredentials,
DefaultIssuerName: defaultTLSACMEIssuerName,
DefaultIssuerKind: defaultTLSACMEIssuerKind,
DefaultACMEIssuerChallengeType: defaultACMEIssuerChallengeType,
DefaultACMEIssuerDNS01ProviderName: defaultACMEIssuerDNS01ProviderName,
}
}
@ -96,8 +111,23 @@ func (s *ControllerOptions) AddFlags(fs *pflag.FlagSet) {
"When this flag is enabled, the following sources for credentials are also used: "+
"AWS - All sources the Go SDK defaults to, notably including any EC2 IAM roles available via instance metadata.")
fs.StringVar(&s.DefaultIssuerName, "default-issuer-name", defaultTLSACMEIssuerName, ""+
"Name of the Issuer to use when the tls is requested but issuer name is not specified on the ingress resource.")
fs.StringVar(&s.DefaultIssuerKind, "default-issuer-kind", defaultTLSACMEIssuerKind, ""+
"Kind of the Issuer to use when the tls is requested but issuer kind is not specified on the ingress resource.")
fs.StringVar(&s.DefaultACMEIssuerChallengeType, "default-acme-issuer-challenge-type", defaultACMEIssuerChallengeType, ""+
"The ACME challenge type to use when tls is requested for an ACME Issuer but is not specified on the ingress resource.")
fs.StringVar(&s.DefaultACMEIssuerDNS01ProviderName, "default-acme-issuer-dns01-provider-name", defaultACMEIssuerDNS01ProviderName, ""+
"Required if --default-acme-issuer-challenge-type is set to dns01. The DNS01 provider to use for ingresses using ACME dns01 "+
"validation that do not explicitly state a dns provider.")
}
func (o *ControllerOptions) Validate() error {
switch o.DefaultIssuerKind {
case "Issuer":
case "ClusterIssuer":
default:
return fmt.Errorf("invalid default issuer kind: %v", o.DefaultIssuerKind)
}
return nil
}

View File

@ -13,6 +13,7 @@ import (
"github.com/jetstack/cert-manager/cmd/controller/app/options"
_ "github.com/jetstack/cert-manager/pkg/controller/certificates"
_ "github.com/jetstack/cert-manager/pkg/controller/clusterissuers"
_ "github.com/jetstack/cert-manager/pkg/controller/ingress-shim"
_ "github.com/jetstack/cert-manager/pkg/controller/issuers"
_ "github.com/jetstack/cert-manager/pkg/issuer/acme"
_ "github.com/jetstack/cert-manager/pkg/issuer/ca"

View File

@ -1,19 +0,0 @@
# ingress-shim
This is a small binary that can be run alongside any cert-manager deployment
in order to automatically create Certificate resources for Ingresses when a
particular annotation is found on an ingress resource.
This allows users to consume certificates from cert-manager without having to
manually create Certificate resources, i.e. in a similar fashion to [kube-lego](https://github.com/jetstack/kube-lego).
It has been developed outside of the core of cert-manager as it is an
experiment to assess the best way to implement this sort of functionality.
## Project status
This project is experimental, and thus should not be relied upon in a
production environment. This tool may change in backwards incompatible ways.
In the future, the functionality of this tool may be merged into cert-manager
itself to provide a more seamless experience for users.

View File

@ -1,158 +0,0 @@
package main
import (
"fmt"
"os"
"sync"
"time"
"github.com/golang/glog"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/leaderelection"
"k8s.io/client-go/tools/leaderelection/resourcelock"
"k8s.io/client-go/tools/record"
"github.com/jetstack/cert-manager/cmd/ingress-shim/controller"
"github.com/jetstack/cert-manager/cmd/ingress-shim/options"
clientset "github.com/jetstack/cert-manager/pkg/client/clientset/versioned"
intscheme "github.com/jetstack/cert-manager/pkg/client/clientset/versioned/scheme"
informers "github.com/jetstack/cert-manager/pkg/client/informers/externalversions"
"github.com/jetstack/cert-manager/pkg/util/kube"
kubeinformers "k8s.io/client-go/informers"
)
const controllerAgentName = "ingress-shim-controller"
func Run(opts *options.ControllerOptions, stopCh <-chan struct{}) {
ctrl, kubeCfg, err := buildController(opts, stopCh)
if err != nil {
glog.Fatalf(err.Error())
}
run := func(_ <-chan struct{}) {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
err := ctrl.Run(2, stopCh)
if err != nil {
glog.Fatalf("error running controller: %s", err.Error())
}
}()
<-stopCh
glog.Infof("Waiting for controller to exit...")
wg.Wait()
glog.Fatalf("Control loops exited")
}
if !opts.LeaderElect {
run(stopCh)
return
}
leaderElectionClient, err := kubernetes.NewForConfig(rest.AddUserAgent(kubeCfg, "leader-election"))
if err != nil {
glog.Fatalf("error creating leader election client: %s", err.Error())
}
startLeaderElection(opts, leaderElectionClient, ctrl.Recorder, run)
panic("unreachable")
}
func buildController(opts *options.ControllerOptions, stopCh <-chan struct{}) (*controller.Controller, *rest.Config, error) {
// Load the users Kubernetes config
kubeCfg, err := kube.KubeConfig(opts.APIServerHost)
if err != nil {
return nil, nil, fmt.Errorf("error creating rest config: %s", err.Error())
}
// Create a Navigator api client
intcl, err := clientset.NewForConfig(kubeCfg)
if err != nil {
return nil, nil, fmt.Errorf("error creating internal group client: %s", err.Error())
}
// Create a Kubernetes api client
cl, err := kubernetes.NewForConfig(kubeCfg)
if err != nil {
return nil, nil, fmt.Errorf("error creating kubernetes client: %s", err.Error())
}
// Create event broadcaster
// Add cert-manager types to the default Kubernetes Scheme so Events can be
// logged properly
intscheme.AddToScheme(scheme.Scheme)
glog.V(4).Info("Creating event broadcaster")
eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartLogging(glog.V(4).Infof)
eventBroadcaster.StartRecordingToSink(&corev1.EventSinkImpl{Interface: cl.CoreV1().Events("")})
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: controllerAgentName})
// We only create SharedInformerFactories for the --namespace specified to
// watch. If this namespace is blank (i.e. the default, watch all
// namespaces) then the factories will watch all namespaces.
// If it is specified, all operations relating to ClusterIssuer resources
// should be disabled and thus we don't need to also create factories for
// the --cluster-resource-namespace.
sharedInformerFactory := informers.NewSharedInformerFactory(intcl, time.Second*30)
kubeSharedInformerFactory := kubeinformers.NewSharedInformerFactory(cl, time.Second*30)
ctrl := controller.New(
sharedInformerFactory.Certmanager().V1alpha1().Certificates(),
kubeSharedInformerFactory.Extensions().V1beta1().Ingresses(),
sharedInformerFactory.Certmanager().V1alpha1().Issuers(),
sharedInformerFactory.Certmanager().V1alpha1().ClusterIssuers(),
cl,
intcl,
recorder,
opts,
)
sharedInformerFactory.Start(stopCh)
kubeSharedInformerFactory.Start(stopCh)
return ctrl, kubeCfg, nil
}
func startLeaderElection(opts *options.ControllerOptions, leaderElectionClient kubernetes.Interface, recorder record.EventRecorder, run func(<-chan struct{})) {
// Identity used to distinguish between multiple controller manager instances
id, err := os.Hostname()
if err != nil {
glog.Fatalf("error getting hostname: %s", err.Error())
}
// Lock required for leader election
rl := resourcelock.ConfigMapLock{
ConfigMapMeta: metav1.ObjectMeta{
Namespace: opts.LeaderElectionNamespace,
Name: "ingress-shim-controller",
},
Client: leaderElectionClient.CoreV1(),
LockConfig: resourcelock.ResourceLockConfig{
Identity: id + "-external-ingress-shim-controller",
EventRecorder: recorder,
},
}
// Try and become the leader and start controller manager loops
leaderelection.RunOrDie(leaderelection.LeaderElectionConfig{
Lock: &rl,
LeaseDuration: opts.LeaderElectionLeaseDuration,
RenewDeadline: opts.LeaderElectionRenewDeadline,
RetryPeriod: opts.LeaderElectionRetryPeriod,
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: run,
OnStoppedLeading: func() {
glog.Fatalf("leaderelection lost")
},
},
})
}

View File

@ -1,47 +0,0 @@
package main
import (
"flag"
"os"
"os/signal"
"syscall"
"github.com/golang/glog"
"github.com/jetstack/cert-manager/pkg/logs"
)
func main() {
logs.InitLogs()
defer logs.FlushLogs()
stopCh := SetupSignalHandler()
cmd := NewCommandStartController(stopCh)
cmd.Flags().AddGoFlagSet(flag.CommandLine)
flag.CommandLine.Parse([]string{})
if err := cmd.Execute(); err != nil {
glog.Fatal(err)
}
}
var shutdownSignals = []os.Signal{os.Interrupt, syscall.SIGTERM}
var onlyOneSignalHandler = make(chan struct{})
// SetupSignalHandler registered for SIGTERM and SIGINT. A stop channel is returned
// which is closed on one of these signals. If a second signal is caught, the program
// is terminated with exit code 1.
func SetupSignalHandler() (stopCh <-chan struct{}) {
close(onlyOneSignalHandler) // panics when called twice
stop := make(chan struct{})
c := make(chan os.Signal, 2)
signal.Notify(c, shutdownSignals...)
go func() {
<-c
close(stop)
<-c
os.Exit(1) // second signal. Exit directly.
}()
return stop
}

View File

@ -1,106 +0,0 @@
package options
import (
"fmt"
"time"
"github.com/spf13/pflag"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
)
type ControllerOptions struct {
APIServerHost string
LeaderElect bool
LeaderElectionNamespace string
LeaderElectionLeaseDuration time.Duration
LeaderElectionRenewDeadline time.Duration
LeaderElectionRetryPeriod time.Duration
DefaultIssuerName string
DefaultIssuerKind string
DefaultACMEIssuerChallengeType string
DefaultACMEIssuerDNS01ProviderName string
}
const (
defaultAPIServerHost = ""
defaultLeaderElect = true
defaultLeaderElectionNamespace = "kube-system"
defaultLeaderElectionLeaseDuration = 15 * time.Second
defaultLeaderElectionRenewDeadline = 10 * time.Second
defaultLeaderElectionRetryPeriod = 2 * time.Second
defaultTLSACMEIssuerName = ""
defaultTLSACMEIssuerKind = "Issuer"
defaultACMEIssuerChallengeType = "http01"
defaultACMEIssuerDNS01ProviderName = ""
)
func NewControllerOptions() *ControllerOptions {
return &ControllerOptions{
APIServerHost: defaultAPIServerHost,
LeaderElect: defaultLeaderElect,
LeaderElectionNamespace: defaultLeaderElectionNamespace,
LeaderElectionLeaseDuration: defaultLeaderElectionLeaseDuration,
LeaderElectionRenewDeadline: defaultLeaderElectionRenewDeadline,
LeaderElectionRetryPeriod: defaultLeaderElectionRetryPeriod,
DefaultIssuerName: defaultTLSACMEIssuerName,
DefaultIssuerKind: defaultTLSACMEIssuerKind,
DefaultACMEIssuerChallengeType: defaultACMEIssuerChallengeType,
DefaultACMEIssuerDNS01ProviderName: defaultACMEIssuerDNS01ProviderName,
}
}
func (s *ControllerOptions) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&s.APIServerHost, "master", defaultAPIServerHost, ""+
"Optional apiserver host address to connect to. If not specified, autoconfiguration "+
"will be attempted.")
fs.BoolVar(&s.LeaderElect, "leader-elect", true, ""+
"If true, ingress-annotation-controller will perform leader election between instances to ensure no more "+
"than one instance of cert-manager operates at a time.")
fs.StringVar(&s.LeaderElectionNamespace, "leader-election-namespace", defaultLeaderElectionNamespace, ""+
"Namespace used to perform leader election. Only used if leader election is enabled.")
fs.DurationVar(&s.LeaderElectionLeaseDuration, "leader-election-lease-duration", defaultLeaderElectionLeaseDuration, ""+
"The duration that non-leader candidates will wait after observing a leadership "+
"renewal until attempting to acquire leadership of a led but unrenewed leader "+
"slot. This is effectively the maximum duration that a leader can be stopped "+
"before it is replaced by another candidate. This is only applicable if leader "+
"election is enabled.")
fs.DurationVar(&s.LeaderElectionRenewDeadline, "leader-election-renew-deadline", defaultLeaderElectionRenewDeadline, ""+
"The interval between attempts by the acting master to renew a leadership slot "+
"before it stops leading. This must be less than or equal to the lease duration. "+
"This is only applicable if leader election is enabled.")
fs.DurationVar(&s.LeaderElectionRetryPeriod, "leader-election-retry-period", defaultLeaderElectionRetryPeriod, ""+
"The duration the clients should wait between attempting acquisition and renewal "+
"of a leadership. This is only applicable if leader election is enabled.")
fs.StringVar(&s.DefaultIssuerName, "default-issuer-name", defaultTLSACMEIssuerName, ""+
"Name of the Issuer to use when the tls is requested but issuer name is not specified on the ingress resource.")
fs.StringVar(&s.DefaultIssuerKind, "default-issuer-kind", defaultTLSACMEIssuerKind, ""+
"Kind of the Issuer to use when the tls is requested but issuer kind is not specified on the ingress resource.")
fs.StringVar(&s.DefaultACMEIssuerChallengeType, "default-acme-issuer-challenge-type", defaultACMEIssuerChallengeType, ""+
"The ACME challenge type to use when tls is requested for an ACME Issuer but is not specified on the ingress resource.")
fs.StringVar(&s.DefaultACMEIssuerDNS01ProviderName, "default-acme-issuer-dns01-provider-name", defaultACMEIssuerDNS01ProviderName, ""+
"Required if --default-acme-issuer-challenge-type is set to dns01. The DNS01 provider to use for ingresses using ACME dns01 "+
"validation that do not explicitly state a dns provider.")
}
func (o *ControllerOptions) Validate() error {
var errs []error
switch o.DefaultACMEIssuerChallengeType {
case "dns01", "http01", "":
default:
errs = append(errs, fmt.Errorf("--default-acme-issuer-challenge-type must be one of 'http01', 'dns01' or not set"))
}
if o.DefaultACMEIssuerChallengeType == "dns01" {
if o.DefaultACMEIssuerDNS01ProviderName == "" {
errs = append(errs, fmt.Errorf("--default-acme-issuer-dns01-provider-name must be set when --default-acme-issuer-challenge-type is set to dns01"))
}
}
return utilerrors.NewAggregate(errs)
}

View File

@ -1,42 +0,0 @@
package main
import (
"fmt"
"github.com/golang/glog"
"github.com/spf13/cobra"
_ "k8s.io/client-go/plugin/pkg/client/auth"
"github.com/jetstack/cert-manager/cmd/ingress-shim/options"
"github.com/jetstack/cert-manager/pkg/util"
)
// NewCommandStartController is a CLI handler for starting ingress-shim-controller
func NewCommandStartController(stopCh <-chan struct{}) *cobra.Command {
o := options.NewControllerOptions()
cmd := &cobra.Command{
Use: "ingress-shim-controller",
Short: fmt.Sprintf("Automate creation of Certificate resources for Ingress (%s) (%s)", util.AppVersion, util.AppGitCommit),
Long: `
This is a small binary that can be run alongside any cert-manager deployment
in order to automatically create Certificate resources for Ingresses when a
particular annotation is found on an ingress resource.
This allows users to consume certificates from cert-manager without having to
manually create Certificate resources`,
// TODO: Refactor this function from this package
Run: func(cmd *cobra.Command, args []string) {
if err := o.Validate(); err != nil {
glog.Fatalf("error validating options: %s", err.Error())
}
Run(o, stopCh)
},
}
flags := cmd.Flags()
o.AddFlags(flags)
return cmd
}

View File

@ -35,4 +35,10 @@ type Context struct {
// ClusterResourceNamespace is the namespace to store resources created by
// non-namespaced resources (e.g. ClusterIssuer) in.
ClusterResourceNamespace string
// Default issuer/certificates details consumed by ingress-shim
DefaultIssuerName string
DefaultIssuerKind string
DefaultACMEIssuerChallengeType string
DefaultACMEIssuerDNS01ProviderName string
}

View File

@ -16,7 +16,6 @@ import (
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue"
"github.com/jetstack/cert-manager/cmd/ingress-shim/options"
cmv1alpha1 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
clientset "github.com/jetstack/cert-manager/pkg/client/clientset/versioned"
cminformers "github.com/jetstack/cert-manager/pkg/client/informers/externalversions/certmanager/v1alpha1"
@ -30,6 +29,12 @@ const (
ControllerName = "ingress-shim"
)
type defaults struct {
issuerName, issuerKind string
acmeIssuerChallengeType string
acmeIssuerDNS01ProviderName string
}
type Controller struct {
Client kubernetes.Interface
CMClient clientset.Interface
@ -46,7 +51,7 @@ type Controller struct {
queue workqueue.RateLimitingInterface
workerWg sync.WaitGroup
syncedFuncs []cache.InformerSynced
options *options.ControllerOptions
defaults defaults
}
// New returns a new Certificates controller. It sets up the informer handler
@ -59,9 +64,9 @@ func New(
client kubernetes.Interface,
cmClient clientset.Interface,
recorder record.EventRecorder,
options *options.ControllerOptions,
defaults defaults,
) *Controller {
ctrl := &Controller{Client: client, CMClient: cmClient, Recorder: recorder, options: options}
ctrl := &Controller{Client: client, CMClient: cmClient, Recorder: recorder, defaults: defaults}
ctrl.syncHandler = ctrl.processNextWorkItem
ctrl.queue = workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "ingresses")
@ -183,3 +188,20 @@ func (c *Controller) processNextWorkItem(ctx context.Context, key string) error
return c.Sync(ctx, crt)
}
var keyFunc = controllerpkg.KeyFunc
func init() {
controllerpkg.Register(ControllerName, func(ctx *controllerpkg.Context) controllerpkg.Interface {
return New(
ctx.SharedInformerFactory.Certmanager().V1alpha1().Certificates(),
ctx.KubeSharedInformerFactory.Extensions().V1beta1().Ingresses(),
ctx.SharedInformerFactory.Certmanager().V1alpha1().Issuers(),
ctx.SharedInformerFactory.Certmanager().V1alpha1().ClusterIssuers(),
ctx.Client,
ctx.CMClient,
ctx.Recorder,
defaults{ctx.DefaultIssuerName, ctx.DefaultIssuerKind, ctx.DefaultACMEIssuerChallengeType, ctx.DefaultACMEIssuerDNS01ProviderName},
).Run
})
}

View File

@ -181,7 +181,7 @@ func (c *Controller) setIssuerSpecificConfig(crt *v1alpha1.Certificate, issuer v
if issuer.GetSpec().ACME != nil {
challengeType, ok := ingAnnotations[acmeIssuerChallengeTypeAnnotation]
if !ok {
challengeType = c.options.DefaultACMEIssuerChallengeType
challengeType = c.defaults.acmeIssuerChallengeType
}
domainCfg := v1alpha1.ACMECertificateDomainConfig{
Domains: tls.Hosts,
@ -202,7 +202,7 @@ func (c *Controller) setIssuerSpecificConfig(crt *v1alpha1.Certificate, issuer v
case "dns01":
dnsProvider, ok := ingAnnotations[acmeIssuerDNS01ProviderNameAnnotation]
if !ok {
dnsProvider = c.options.DefaultACMEIssuerDNS01ProviderName
dnsProvider = c.defaults.acmeIssuerDNS01ProviderName
}
if dnsProvider == "" {
return fmt.Errorf("no acme issuer dns01 challenge provider specified")
@ -247,8 +247,8 @@ func shouldSync(ing *extv1beta1.Ingress) bool {
// Certificate created for the given Ingress resource. If one is not set, the
// default issuer given to the controller will be used.
func (c *Controller) issuerForIngress(ing *extv1beta1.Ingress) (name string, kind string) {
name = c.options.DefaultIssuerName
kind = c.options.DefaultIssuerKind
name = c.defaults.issuerName
kind = c.defaults.issuerKind
annotations := ing.Annotations
if annotations == nil {
annotations = map[string]string{}