diff --git a/README.md b/README.md index ec52ec0e..6433b5a0 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ ## Requirements - Kubernetes 1.2+ -- Compatible ingress controller (nginx or GCE see [here](#ingress-controllers)) +- Compatible ingress controller (nginx, HAProxy or GCE see [here](#ingress-controllers)) - Non-production use case :laughing: ## Usage @@ -33,6 +33,7 @@ * [GCE](examples/gce/README.md) * [nginx controller](examples/nginx/README.md) +* [HAProxy controller](/examples/haproxy) The default value of `LEGO_URL` is the Let's Encrypt **staging environment**. If you want to get "real" certificates you have to configure their production env. @@ -88,6 +89,11 @@ Please note: - available through image `gcr.io/google_containers/nginx-ingress-controller` - fully supports kube-lego from version 0.8 onwards +### [HAProxy Ingress controller](https://github.com/jcmoraisjr/haproxy-ingress) + +- available through image `quay.io/jcmoraisjr/haproxy-ingress` +- fully supports kube-lego from version 0.3 onwards + ### [GCE Loadbalancers](https://github.com/kubernetes/ingress/tree/master/controllers/gce) - you don't have to maintain the ingress controller yourself, you pay GCE to do that for you @@ -105,13 +111,16 @@ Please note: | `LEGO_SECRET_NAME` | n | `kube-lego-account` | Name of the secret in the same namespace that contains ACME account secret | | `LEGO_SERVICE_SELECTOR` | n | `kube-lego` | Set the service selector to the the kube-lego pod | | `LEGO_SERVICE_NAME_NGINX` | n | `kube-lego-nginx` | Service name for NGINX ingress | +| `LEGO_SERVICE_NAME_HAPROXY` | n | `kube-lego-haproxy` | Service name for HAProxy ingress | | `LEGO_SERVICE_NAME_GCE` | n | `kube-lego-gce` | Service name for GCE ingress | -| `LEGO_SUPPORTED_INGRESS_CLASS` | n | `nginx,gce` | Specify the supported ingress class | -| `LEGO_SUPPORTED_INGRESS_PROVIDER` | n | `nginx,gce` | Specify the supported ingress provider | +| `LEGO_SUPPORTED_INGRESS_CLASS` | n | `nginx,haproxy,gce` | Specify the supported ingress class | +| `LEGO_SUPPORTED_INGRESS_PROVIDER` | n | `nginx,haproxy,gce` | Specify the supported ingress provider | | `LEGO_INGRESS_NAME_NGINX` | n | `kube-lego-nginx` | Ingress name which contains the routing for HTTP verification for nginx ingress | +| `LEGO_INGRESS_NAME_HAPROXY` | n | `kube-lego-haproxy` | Ingress name which contains the routing for HTTP verification for HAProxy ingress | | `LEGO_PORT` | n | `8080` | Port where this daemon is listening for verifcation calls (HTTP method)| | `LEGO_CHECK_INTERVAL` | n | `8h` | Interval for periodically certificate checks (to find expired certs)| | `LEGO_MINIMUM_VALIDITY` | n | `720h` (30 days) | Request a renewal when the remaining certificate validity falls below that value| +| `LEGO_WAIT_CHALLENGE_URL` | n | Check instead wait | Time with suffix, eg `10s`, to wait for the updating of the challenge URL. This will skip the checking if declared. Useful if for some reason the container network cannot reach the public URL. | | `LEGO_DEFAULT_INGRESS_CLASS` | n | `nginx` | Default ingress class for resources without specification| | `LEGO_KUBE_API_URL` | n | `http://127.0.0.1:8080` | API server URL | | `LEGO_LOG_LEVEL` | n | `info` | Set log level (`debug`, `info`, `warn` or `error`) | @@ -122,6 +131,7 @@ Please note: ## Full deployment examples - [Nginx Ingress Controller](examples/nginx/) +- [HAProxy Ingress controller](/examples/haproxy) - [GCE Load Balancers](examples/gce/) ## Troubleshooting diff --git a/examples/haproxy/README.md b/examples/haproxy/README.md new file mode 100644 index 00000000..65ea2070 --- /dev/null +++ b/examples/haproxy/README.md @@ -0,0 +1,36 @@ +# kube-lego example + +This document demonstrates how to deploy kube-lego to the +[HAProxy Ingress](https://github.com/jcmoraisjr/haproxy-ingress) controller. + +## Deploy the Ingress controller + +Follow the [deployment instructions](https://github.com/kubernetes/ingress/tree/master/examples/deployment/haproxy) +including the deployment of the optional web app for testing. + +## Deploy kube-lego + +The following instruction will create the kube-lego deployment on it's own namespace. +Be aware that kube-lego creates it's related service on its own. + +* Change `LEGO_EMAIL` to your email address +* Uncomment `LEGO_URL` to use the production API + +```console +kubectl create ns kube-lego +kubectl create -f deployment.yaml +``` + +## Enable kube-lego in the testing application + +This will add a TLS secret name and tls-acme annotation to the ingress resource created +in the deployment instruction. + +* Change both `echo.example.com` to the public domain of your Ingress controller + +```console +kubectl replace -f app-ingress.yaml +``` + +The `app-tls` secret and the https url should be updated. Check the log output of +HAProxy Ingress and kube-lego pods if this doesn't happen. diff --git a/examples/haproxy/app-ingress.yaml b/examples/haproxy/app-ingress.yaml new file mode 100644 index 00000000..549dde37 --- /dev/null +++ b/examples/haproxy/app-ingress.yaml @@ -0,0 +1,20 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: app + annotations: + kubernetes.io/tls-acme: "true" + kubernetes.io/ingress.class: "haproxy" +spec: + tls: + - hosts: + - echo.example.com + secretName: app-tls + rules: + - host: echo.example.com + http: + paths: + - path: / + backend: + serviceName: http-svc + servicePort: 8080 diff --git a/examples/haproxy/lego-deployment.yaml b/examples/haproxy/lego-deployment.yaml new file mode 100644 index 00000000..b1f6a90d --- /dev/null +++ b/examples/haproxy/lego-deployment.yaml @@ -0,0 +1,45 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: kube-lego + namespace: kube-lego +spec: + selector: + matchLabels: + app: kube-lego + template: + metadata: + labels: + app: kube-lego + spec: + containers: + - name: kube-lego + ## HAProxy support isn't on the stable release yet! + image: jetstack/kube-lego:canary + imagePullPolicy: Always + ports: + - containerPort: 8080 + env: + ## Use HAProxy Ingress + - name: LEGO_DEFAULT_INGRESS_CLASS + value: haproxy + ## Specify your email address + - name: LEGO_EMAIL + value: you@example.com + ## Uncomment LEGO_URL to use the production API - default is to use staging + # - name: LEGO_URL + # value: https://acme-v01.api.letsencrypt.org/directory + - name: LEGO_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: LEGO_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + readinessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 5 + timeoutSeconds: 1 diff --git a/pkg/acme/cert_request.go b/pkg/acme/cert_request.go index 0553d93d..eeaeeec1 100644 --- a/pkg/acme/cert_request.go +++ b/pkg/acme/cert_request.go @@ -74,7 +74,13 @@ func (a *Acme) testReachablilty(domain string) error { } func (a *Acme) verifyDomain(domain string) (auth *acme.Authorization, err error) { - err = a.testReachablilty(domain) + sleep := a.kubelego.LegoWaitChallengeURL() + if sleep < 0 { + err = a.testReachablilty(domain) + } else { + time.Sleep(sleep) + err = nil + } if err != nil { return nil, fmt.Errorf("reachability test failed: %s", err) } diff --git a/pkg/kubelego/kubelego.go b/pkg/kubelego/kubelego.go index 411deb52..25157658 100644 --- a/pkg/kubelego/kubelego.go +++ b/pkg/kubelego/kubelego.go @@ -111,7 +111,10 @@ func (kl *KubeLego) Init() { kl.legoIngressProvider["gce"] = gce.New(kl) break case "nginx": - kl.legoIngressProvider["nginx"] = nginx.New(kl) + kl.legoIngressProvider["nginx"] = nginx.New(kl, "nginx", kl.legoIngressNameNginx, kl.legoServiceNameNginx) + break + case "haproxy": + kl.legoIngressProvider["haproxy"] = nginx.New(kl, "haproxy", kl.legoIngressNameHAProxy, kl.legoServiceNameHAProxy) break default: kl.Log().Warnf("Unsupported provider [%s], please add a handler in kubelego.go#Init()", provider) @@ -196,9 +199,6 @@ func (kl *KubeLego) LegoDefaultIngressClass() string { return kl.legoDefaultIngressClass } -func (kl *KubeLego) LegoIngressNameNginx() string { - return kl.legoIngressNameNginx -} func (kl *KubeLego) LegoSupportedIngressClass() []string { return kl.legoSupportedIngressClass } @@ -207,10 +207,6 @@ func (kl *KubeLego) LegoSupportedIngressProvider() []string { return kl.legoSupportedIngressProvider } -func (kl *KubeLego) LegoServiceNameNginx() string { - return kl.legoServiceNameNginx -} - func (kl *KubeLego) LegoServiceNameGce() string { return kl.legoServiceNameGce } @@ -219,6 +215,10 @@ func (kl *KubeLego) LegoMinimumValidity() time.Duration { return kl.legoMinimumValidity } +func (kl *KubeLego) LegoWaitChallengeURL() time.Duration { + return kl.legoWaitChallengeURL +} + func (kl *KubeLego) LegoCheckInterval() time.Duration { return kl.legoCheckInterval } @@ -281,6 +281,11 @@ func (kl *KubeLego) paramsLego() error { } } + kl.legoServiceNameHAProxy = os.Getenv("LEGO_SERVICE_NAME_HAPROXY") + if len(kl.legoServiceNameHAProxy) == 0 { + kl.legoServiceNameHAProxy = "kube-lego-haproxy" + } + kl.legoServiceNameGce = os.Getenv("LEGO_SERVICE_NAME_GCE") if len(kl.legoServiceNameGce) == 0 { kl.legoServiceNameGce = "kube-lego-gce" @@ -318,6 +323,11 @@ func (kl *KubeLego) paramsLego() error { } } + kl.legoIngressNameHAProxy = os.Getenv("LEGO_INGRESS_NAME_HAPROXY") + if len(kl.legoIngressNameHAProxy) == 0 { + kl.legoIngressNameHAProxy = "kube-lego-haproxy" + } + checkIntervalString := os.Getenv("LEGO_CHECK_INTERVAL") if len(checkIntervalString) == 0 { kl.legoCheckInterval = 8 * time.Hour @@ -351,6 +361,17 @@ func (kl *KubeLego) paramsLego() error { kl.legoMinimumValidity = d } + waitChallengeURL := os.Getenv("LEGO_WAIT_CHALLENGE_URL") + if len(waitChallengeURL) == 0 { + kl.legoWaitChallengeURL = -1 + } else { + w, err := time.ParseDuration(waitChallengeURL) + if err != nil { + return err + } + kl.legoWaitChallengeURL = w + } + httpPortStr := os.Getenv("LEGO_PORT") if len(httpPortStr) == 0 { kl.legoHTTPPort = intstr.FromInt(8080) diff --git a/pkg/kubelego/type.go b/pkg/kubelego/type.go index 69da9727..40765455 100644 --- a/pkg/kubelego/type.go +++ b/pkg/kubelego/type.go @@ -19,15 +19,18 @@ type KubeLego struct { legoEmail string legoSecretName string legoIngressNameNginx string + legoIngressNameHAProxy string legoNamespace string legoPodIP net.IP legoServiceNameNginx string + legoServiceNameHAProxy string legoServiceNameGce string legoSupportedIngressClass []string legoSupportedIngressProvider []string legoHTTPPort intstr.IntOrString legoCheckInterval time.Duration legoMinimumValidity time.Duration + legoWaitChallengeURL time.Duration legoDefaultIngressClass string legoDefaultIngressProvider string legoKubeApiURL string diff --git a/pkg/kubelego_const/consts.go b/pkg/kubelego_const/consts.go index 35b77a5f..57b878c6 100644 --- a/pkg/kubelego_const/consts.go +++ b/pkg/kubelego_const/consts.go @@ -23,7 +23,7 @@ const AnnotationSslRedirect = "ingress.kubernetes.io/ssl-redirect" const AnnotationKubeLegoManaged = "kubernetes.io/kube-lego-managed" const AnnotationWhitelistSourceRange = "ingress.kubernetes.io/whitelist-source-range" -var SupportedIngressClasses = []string{"nginx", "gce"} -var SupportedIngressProviders = []string{"nginx", "gce"} +var SupportedIngressClasses = []string{"nginx", "haproxy", "gce"} +var SupportedIngressProviders = []string{"nginx", "haproxy", "gce"} var AnnotationEnabled = "kubernetes.io/tls-acme" var LegoServiceSelector = "kube-lego" diff --git a/pkg/kubelego_const/interfaces.go b/pkg/kubelego_const/interfaces.go index 0c8c5743..fc4ebc68 100644 --- a/pkg/kubelego_const/interfaces.go +++ b/pkg/kubelego_const/interfaces.go @@ -20,14 +20,13 @@ type KubeLego interface { LegoURL() string LegoNamespace() string LegoWatchNamespace() string - LegoIngressNameNginx() string - LegoServiceNameNginx() string LegoServiceNameGce() string LegoDefaultIngressClass() string LegoSupportedIngressClass() []string LegoSupportedIngressProvider() []string LegoCheckInterval() time.Duration LegoMinimumValidity() time.Duration + LegoWaitChallengeURL() time.Duration LegoPodIP() net.IP IngressProvider(string) (IngressProvider, error) Version() string diff --git a/pkg/mocks/kubelego.go b/pkg/mocks/kubelego.go index 9d6bf485..36f72b0e 100644 --- a/pkg/mocks/kubelego.go +++ b/pkg/mocks/kubelego.go @@ -20,8 +20,6 @@ func DummyKubeLego(c *gomock.Controller) *MockKubeLego { kl.EXPECT().LegoNamespace().AnyTimes().Return("kube-lego") kl.EXPECT().LegoWatchNamespace().AnyTimes().Return(k8sApi.NamespaceAll) kl.EXPECT().LegoPodIP().AnyTimes().Return(net.ParseIP("1.2.3.4")) - kl.EXPECT().LegoIngressNameNginx().AnyTimes().Return("kube-lego-nginx") - kl.EXPECT().LegoServiceNameNginx().AnyTimes().Return("kube-lego-nginx") kl.EXPECT().LegoServiceNameGce().AnyTimes().Return("kube-lego-gce") kl.EXPECT().LegoDefaultIngressClass().AnyTimes().Return("nginx") kl.EXPECT().LegoDefaultIngressProvider().AnyTimes().Return("nginx") diff --git a/pkg/mocks/mocks.go b/pkg/mocks/mocks.go index 831f6f47..8ef7b03f 100644 --- a/pkg/mocks/mocks.go +++ b/pkg/mocks/mocks.go @@ -117,26 +117,6 @@ func (_mr *_MockKubeLegoRecorder) LegoWatchNamespace() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "LegoWatchNamespace") } -func (_m *MockKubeLego) LegoIngressNameNginx() string { - ret := _m.ctrl.Call(_m, "LegoIngressNameNginx") - ret0, _ := ret[0].(string) - return ret0 -} - -func (_mr *_MockKubeLegoRecorder) LegoIngressNameNginx() *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "LegoIngressNameNginx") -} - -func (_m *MockKubeLego) LegoServiceNameNginx() string { - ret := _m.ctrl.Call(_m, "LegoServiceNameNginx") - ret0, _ := ret[0].(string) - return ret0 -} - -func (_mr *_MockKubeLegoRecorder) LegoServiceNameNginx() *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "LegoServiceNameNginx") -} - func (_m *MockKubeLego) LegoServiceNameGce() string { ret := _m.ctrl.Call(_m, "LegoServiceNameGce") ret0, _ := ret[0].(string) @@ -207,6 +187,16 @@ func (_mr *_MockKubeLegoRecorder) LegoMinimumValidity() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "LegoMinimumValidity") } +func (_m *MockKubeLego) LegoWaitChallengeURL() time.Duration { + ret := _m.ctrl.Call(_m, "LegoWaitChallengeURL") + ret0, _ := ret[0].(time.Duration) + return ret0 +} + +func (_mr *_MockKubeLegoRecorder) LegoWaitChallengeURL() *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "LegoWaitChallengeURL") +} + func (_m *MockKubeLego) LegoPodIP() net.IP { ret := _m.ctrl.Call(_m, "LegoPodIP") ret0, _ := ret[0].(net.IP) diff --git a/pkg/provider/nginx/nginx.go b/pkg/provider/nginx/nginx.go index ef5988ed..792dcc87 100644 --- a/pkg/provider/nginx/nginx.go +++ b/pkg/provider/nginx/nginx.go @@ -14,16 +14,22 @@ import ( var _ kubelego.IngressProvider = &Nginx{} type Nginx struct { - kubelego kubelego.KubeLego - hosts map[string]bool - ingress kubelego.Ingress - service kubelego.Service + kubelego kubelego.KubeLego + hosts map[string]bool + ingress kubelego.Ingress + service kubelego.Service + providerName string + ingressName string + serviceName string } -func New(kl kubelego.KubeLego) *Nginx { +func New(kl kubelego.KubeLego, providerName string, ingressName string, serviceName string) *Nginx { return &Nginx{ - kubelego: kl, - hosts: map[string]bool{}, + kubelego: kl, + hosts: map[string]bool{}, + providerName: providerName, + ingressName: ingressName, + serviceName: serviceName, } } @@ -41,10 +47,10 @@ func (p *Nginx) Finalize() error { p.Log().Debug("finalize") if p.ingress == nil { - p.ingress = ingress.New(p.kubelego, p.kubelego.LegoNamespace(), p.kubelego.LegoIngressNameNginx()) + p.ingress = ingress.New(p.kubelego, p.kubelego.LegoNamespace(), p.ingressName) } if p.service == nil { - p.service = service.New(p.kubelego, p.kubelego.LegoNamespace(), p.kubelego.LegoServiceNameNginx()) + p.service = service.New(p.kubelego, p.kubelego.LegoNamespace(), p.serviceName) } if len(p.hosts) < 1 { @@ -100,7 +106,7 @@ func (p *Nginx) updateIngress() error { k8sExtensions.HTTPIngressPath{ Path: kubelego.AcmeHttpChallengePath, Backend: k8sExtensions.IngressBackend{ - ServiceName: p.kubelego.LegoServiceNameNginx(), + ServiceName: p.serviceName, ServicePort: p.kubelego.LegoHTTPPort(), }, }, @@ -123,7 +129,7 @@ func (p *Nginx) updateIngress() error { // TODO: use the ingres class as specified on the ingress we are // requesting a certificate for kubelego.AnnotationIngressClass: p.kubelego.LegoDefaultIngressClass(), - kubelego.AnnotationIngressProvider: "nginx", + kubelego.AnnotationIngressProvider: p.providerName, kubelego.AnnotationWhitelistSourceRange: "0.0.0.0/0,::/0", } diff --git a/pkg/provider/nginx/nginx_suite_test.go b/pkg/provider/nginx/nginx_suite_test.go index 1ff27256..053418a4 100644 --- a/pkg/provider/nginx/nginx_suite_test.go +++ b/pkg/provider/nginx/nginx_suite_test.go @@ -35,7 +35,7 @@ var _ = Describe("Nginx", func() { mockKl = mocks.DummyKubeLego(ctrlMock) - provider = New(mockKl) + provider = New(mockKl, "nginx", "kube-lego-nginx", "kube-lego-nginx") }) Describe("Process", func() {