Split implementations from generic code

This commit is contained in:
Manuel de Brito Fontes 2016-11-10 19:56:29 -03:00
parent d1e8a629ca
commit ed9a416b01
107 changed files with 5777 additions and 3546 deletions

View file

@ -32,7 +32,7 @@ const defaultZone = "zone-a"
func newBackendPool(f BackendServices, fakeIGs instances.InstanceGroups, syncWithCloud bool) BackendPool {
namer := &utils.Namer{}
nodePool := instances.NewNodePool(fakeIGs)
nodePool.Init(&instances.FakeZoneLister{Items:[]string{defaultZone}})
nodePool.Init(&instances.FakeZoneLister{Zones: []string{defaultZone}})
healthChecks := healthchecks.NewHealthChecker(healthchecks.NewFakeHealthChecks(), "/", namer)
healthChecks.Init(&healthchecks.FakeHealthCheckGetter{DefaultHealthCheck: nil})
return NewBackendPool(

View file

@ -1 +1,2 @@
nginx-ingress-controller
rootfs/nginx-ingress-controller
*/**/.coverprofile

View file

@ -1,5 +1,33 @@
Changelog
### 0.9
- [X] [#1498](https://github.com/kubernetes/contrib/pull/1498) Refactoring of template handling
- [X] [#1571](https://github.com/kubernetes/contrib/pull/1571) use POD_NAMESPACE as a namespace in cli parameters
- [X] [#1591](https://github.com/kubernetes/contrib/pull/1591) Always listen on port 443, even without ingress rules
- [X] [#1596](https://github.com/kubernetes/contrib/pull/1596) Adapt nginx hash sizes to the number of ingress
- [X] [#1653](https://github.com/kubernetes/contrib/pull/1653) Update image version
- [X] [#1672](https://github.com/kubernetes/contrib/pull/1672) Add firewall rules and ing class clarifications
- [X] [#1711](https://github.com/kubernetes/contrib/pull/1711) Add function helpers to nginx template
- [X] [#1743](https://github.com/kubernetes/contrib/pull/1743) Allow customisation of the nginx proxy_buffer_size directive via ConfigMap
- [X] [#1749](https://github.com/kubernetes/contrib/pull/1749) Readiness probe that works behind a CP lb
- [X] [#1751](https://github.com/kubernetes/contrib/pull/1751) Add the name of the upstream in the log
- [X] [#1758](https://github.com/kubernetes/contrib/pull/1758) Update nginx to 1.11.4
- [X] [#1759](https://github.com/kubernetes/contrib/pull/1759) Add support for default backend in Ingress rule
- [X] [#1762](https://github.com/kubernetes/contrib/pull/1762) Add cloud detection
- [X] [#1766](https://github.com/kubernetes/contrib/pull/1766) Clarify the controller uses endpoints and not services
- [X] [#1767](https://github.com/kubernetes/contrib/pull/1767) Update godeps
- [X] [#1772](https://github.com/kubernetes/contrib/pull/1772) Avoid replacing nginx.conf file if the new configuration is invalid
- [X] [#1773](https://github.com/kubernetes/contrib/pull/1773) Add annotation to add CORS support
- [X] [#1786](https://github.com/kubernetes/contrib/pull/1786) Add docs about go template
- [X] [#1796](https://github.com/kubernetes/contrib/pull/1796) Add external authentication support using auth_request
- [X] [#1802](https://github.com/kubernetes/contrib/pull/1802) Initialize proxy_upstream_name variable
- [X] [#1806](https://github.com/kubernetes/contrib/pull/1806) Add docs about the log format
- [X] [#1808](https://github.com/kubernetes/contrib/pull/1808) WebSocket documentation
- [X] [#1847](https://github.com/kubernetes/contrib/pull/1847) Change structure of packages
- [X] Add annotation for custom upstream timeouts
- [X] Mutual TLS auth (https://github.com/kubernetes/contrib/issues/1870)
### 0.8.3
- [X] [#1450](https://github.com/kubernetes/contrib/pull/1450) Check for errors in nginx template

View file

@ -1,25 +1,52 @@
all: push
BUILDTAGS=
# 0.0 shouldn't clobber any release builds
TAG = 0.8.3
PREFIX = gcr.io/google_containers/nginx-ingress-controller
RELEASE?=0.0
PREFIX?=gcr.io/google_containers/nginx-ingress-controller
GOOS?=linux
REPO_INFO=$(shell git config --get remote.origin.url)
ifndef VERSION
VERSION := git-$(shell git rev-parse --short HEAD)
ifndef COMMIT
COMMIT := git-$(shell git rev-parse --short HEAD)
endif
controller: controller.go clean
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags \
"-s -w -X main.version=${VERSION} -X main.gitRepo=${REPO_INFO}" \
-o nginx-ingress-controller
PKG=k8s.io/ingress/controllers/nginx
container: controller
docker build -t $(PREFIX):$(TAG) .
build: clean
CGO_ENABLED=0 GOOS=${GOOS} go build -a -installsuffix cgo \
-ldflags "-s -w -X ${PKG}/pkg/version.RELEASE=${RELEASE} -X ${PKG}/pkg/version.COMMIT=${COMMIT} -X ${PKG}/pkg/version.REPO=${REPO_INFO}" \
-o rootfs/nginx-ingress-controller ${PKG}/pkg/cmd/controller
push: container
gcloud docker push $(PREFIX):$(TAG)
container:
docker build -t $(PREFIX):$(RELEASE) rootfs
push:
gcloud docker push $(PREFIX):$(RELEASE)
fmt:
@echo "+ $@"
@go list -f '{{if len .TestGoFiles}}"gofmt -s -l {{.Dir}}"{{end}}' $(shell go list ${PKG}/... | grep -v vendor) | xargs -L 1 sh -c
lint:
@echo "+ $@"
@go list -f '{{if len .TestGoFiles}}"golint {{.Dir}}/..."{{end}}' $(shell go list ${PKG}/... | grep -v vendor) | xargs -L 1 sh -c
test: fmt lint vet
@echo "+ $@"
@go test -v -race -tags "$(BUILDTAGS) cgo" $(shell go list ${PKG}/... | grep -v vendor)
cover:
@echo "+ $@"
@go list -f '{{if len .TestGoFiles}}"go test -coverprofile={{.Dir}}/.coverprofile {{.ImportPath}}"{{end}}' $(shell go list ${PKG}/... | grep -v vendor) | xargs -L 1 sh -c
gover
goveralls -coverprofile=gover.coverprofile -service travis-ci -repotoken ${COVERALLS_TOKEN}
vet:
@echo "+ $@"
@go vet $(shell go list ${PKG}/... | grep -v vendor)
clean:
rm -f nginx-ingress-controller

View file

@ -1,12 +1,18 @@
[![Build Status](https://travis-ci.org/aledbf/ingress-controller.svg?branch=master)](https://travis-ci.org/aledbf/ingress-controller)
[![Coverage Status](https://coveralls.io/repos/github/aledbf/ingress-controller/badge.svg?branch=master)](https://coveralls.io/github/aledbf/ingress-controller?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/aledbf/ingress-controller)](https://goreportcard.com/report/github.com/aledbf/ingress-controller)
# Nginx Ingress Controller
This is an nginx Ingress controller that uses [ConfigMap](https://github.com/kubernetes/kubernetes/blob/master/docs/design/configmap.md) to store the nginx configuration. See [Ingress controller documentation](../README.md) for details on how it works.
## Contents
* [Recent changes](#recent-changes)
* [Conventions](#conventions)
* [Requirements](#what-it-provides)
* [Dry running](#dry-running-the-ingress-controller)
* [Deployment](#deployment)
* [Health checks](#health-checks)
* [HTTP](#http)
* [HTTPS](#https)
* [Default SSL Certificate](#default-ssl-certificate)
@ -16,6 +22,7 @@ This is an nginx Ingress controller that uses [ConfigMap](https://github.com/kub
* [TCP Services](#exposing-tcp-services)
* [UDP Services](#exposing-udp-services)
* [Proxy Protocol](#proxy-protocol)
* [Service Integration](#service-integration)
* [NGINX customization](configuration.md)
* [NGINX status page](#nginx-status-page)
* [Running multiple ingress controllers](#running-multiple-ingress-controllers)
@ -25,9 +32,15 @@ This is an nginx Ingress controller that uses [ConfigMap](https://github.com/kub
* [Local cluster](#local-cluster)
* [Debug & Troubleshooting](#troubleshooting)
* [Why endpoints and not services?](#why-endpoints-and-not-services)
* [Metrics](#metrics)
* [Limitations](#limitations)
* [NGINX Notes](#nginx-notes)
## Recent changes
Change history is available in [CHANGELOG.md](CHANGELOG.md)
## Conventions
Anytime we reference a tls secret, we mean (x509, pem encoded, RSA 2048, etc). You can generate such a certificate with:
@ -35,21 +48,10 @@ Anytime we reference a tls secret, we mean (x509, pem encoded, RSA 2048, etc). Y
and create the secret via `kubectl create secret tls --key file --cert file`
## Requirements
- Default backend [404-server](https://github.com/kubernetes/contrib/tree/master/404-server)
## Dry running the Ingress controller
Before deploying the controller to production you might want to run it outside the cluster and observe it.
```console
$ make controller
$ mkdir /etc/nginx-ssl
$ ./nginx-ingress-controller --running-in-cluster=false --default-backend-service=kube-system/default-http-backend
```
## Deployment
First create a default backend:
@ -64,6 +66,15 @@ Loadbalancers are created via a ReplicationController or Daemonset:
$ kubectl create -f examples/default/rc-default.yaml
```
## Health checks
The proveded examples in the Ingress controller use a `readiness` and `liveness` probe. By default the URL is `/healthz` and the port `18080`.
Using the flag `--health-check-path` is possible to specify a custom path.
In some environments only port 80 is allowed to enable health checks. For this reason the Ingress controller exposes this path in the default server.
If PROXY protocol is enabled the health check must use the default port `18080`. This is required because Kubernetes probes do not understand PROXY protocol.
## HTTP
First we need to deploy some application to publish. To keep this simple we will use the [echoheaders app](https://github.com/kubernetes/contrib/blob/master/ingress/echoheaders/echo-app.yaml) that just returns information about the http request as output
@ -141,6 +152,7 @@ Please follow [test.sh](https://github.com/bprashanth/Ingress/blob/master/exampl
Check the [example](examples/tls/README.md)
### Default SSL Certificate
NGINX provides the option [server name](http://nginx.org/en/docs/http/server_names.html) as a catch-all in case of requests that do not match one of the configured server names. This configuration works without issues for HTTP traffic. In case of HTTPS NGINX requires a certificate. For this reason the Ingress controller provides the flag `--default-ssl-certificate`. The secret behind this flag contains the default certificate to be used in the mentioned case.
@ -325,6 +337,12 @@ Amongst others [ELBs in AWS](http://docs.aws.amazon.com/ElasticLoadBalancing/lat
Please check the [proxy-protocol](examples/proxy-protocol/) example
## Service Integration
On clouds like AWS or GCE, using a service with `Type=LoadBalancer` allows the default kubernetes integration, which can save a lot of work.
By passing the `--publish-service` argument to the controller, the ingress status will be updated with the load balancer configuration of the service, rather than the IP/s of the node/s.
### Custom errors
In case of an error in a request the body of the response is obtained from the `default backend`. Each request to the default backend includes two headers:
@ -427,14 +445,12 @@ I0316 12:24:37.610073 1 command.go:69] change in configuration detected. R
- `--v=3` shows details about the service, Ingress rule, endpoint changes and it dumps the nginx configuration in JSON format
- `--v=5` configures NGINX in [debug mode](http://nginx.org/en/docs/debugging_log.html)
### Metrics
*These issues were encountered in past versions of Kubernetes:*
[1.2.0-alpha7 deployment](https://github.com/kubernetes/kubernetes/blob/master/docs/getting-started-guides/docker.md):
* make setup-files.sh file in hypercube does not provide 10.0.0.1 IP to make-ca-certs, resulting in CA certs that are issued to the external cluster IP address rather then 10.0.0.1 -> this results in nginx-third-party-lb appearing to get stuck at "Utils.go:177 - Waiting for default/default-http-backend" in the docker logs. Kubernetes will eventually kill the container before nginx-third-party-lb times out with a message indicating that the CA certificate issuer is invalid (wrong ip), to verify this add zeros to the end of initialDelaySeconds and timeoutSeconds and reload the RC, and docker will log this error before kubernetes kills the container.
* To fix the above, setup-files.sh must be patched before the cluster is inited (refer to https://github.com/kubernetes/kubernetes/pull/21504)
Using the doc [Instrumenting Kubernetes with a new metric](https://github.com/kubernetes/kubernetes/blob/master/docs/devel/instrumentation.md#instrumenting-kubernetes-with-a-new-metric) the Ingress controller
exposes the registered metrics via HTTP. Besides the default metrics provided by Prometheus is possible to get the number of reloads `reload_operations` and reloads with error `reload_operations_errors`,
ie error in validation in the configuration file before the reload. The metrics are exposed in port `10254` and path `/metrics`.
Using curl: `curl -v <pod ip>:10254/metrics`
### Limitations
@ -452,11 +468,3 @@ The NGINX ingress controller does not uses [Services](http://kubernetes.io/docs/
Since `gcr.io/google_containers/nginx-slim:0.8` NGINX contains the next patches:
- Dynamic TLS record size [nginx__dynamic_tls_records.patch](https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/)
NGINX provides the parameter `ssl_buffer_size` to adjust the size of the buffer. Default value in NGINX is 16KB. The ingress controller changes the default to 4KB. This improves the [TLS Time To First Byte (TTTFB)](https://www.igvita.com/2013/12/16/optimizing-nginx-tls-time-to-first-byte/) but the size is fixed. This patches adapts the size of the buffer to the content is being served helping to improve the perceived latency.
- Add SPDY support back to Nginx with HTTP/2 [nginx_1_9_15_http2_spdy.patch](https://github.com/cloudflare/sslconfig/pull/36)
At the same NGINX introduced HTTP/2 support for SPDY was removed. This patch add support for SPDY without compromising HTTP/2 support using the Application-Layer Protocol Negotiation (ALPN) or Next Protocol Negotiation (NPN) Transport Layer Security (TLS) extension to negotiate what protocol the server and client support
```
openssl s_client -servername www.my-site.com -connect www.my-site.com:443 -nextprotoneg ''
CONNECTED(00000003)
Protocols advertised by server: h2, spdy/3.1, http/1.1
```

File diff suppressed because it is too large Load diff

View file

@ -1,33 +0,0 @@
package = "lua-resty-http"
version = "0.07-0"
source = {
url = "git://github.com/pintsized/lua-resty-http",
tag = "v0.07"
}
description = {
summary = "Lua HTTP client cosocket driver for OpenResty / ngx_lua.",
detailed = [[
Features an HTTP 1.0 and 1.1 streaming interface to reading
bodies using coroutines, for predictable memory usage in Lua
land. Alternative simple interface for singleshot requests
without manual connection step. Supports chunked transfer
encoding, keepalive, pipelining, and trailers. Headers are
treated case insensitively. Probably production ready in most
cases, though not yet proven in the wild.
Recommended by the OpenResty maintainer as a long-term
replacement for internal requests through ngx.location.capture.
]],
homepage = "https://github.com/pintsized/lua-resty-http",
license = "2-clause BSD",
maintainer = "James Hurst <james@pintsized.co.uk>"
}
dependencies = {
"lua >= 5.1"
}
build = {
type = "builtin",
modules = {
["resty.http"] = "lib/resty/http.lua",
["resty.http_headers"] = "lib/resty/http_headers.lua"
}
}

View file

@ -1,192 +0,0 @@
/*
Copyright 2015 The Kubernetes 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 main
import (
"flag"
"fmt"
"net/http"
"net/http/pprof"
"os"
"os/signal"
"syscall"
"time"
"github.com/golang/glog"
"github.com/spf13/pflag"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/healthz"
kubectl_util "k8s.io/kubernetes/pkg/kubectl/cmd/util"
)
const (
healthPort = 10254
)
var (
// value overwritten during build. This can be used to resolve issues.
version = "0.8.3"
gitRepo = "https://github.com/kubernetes/contrib"
flags = pflag.NewFlagSet("", pflag.ExitOnError)
defaultSvc = flags.String("default-backend-service", "",
`Service used to serve a 404 page for the default backend. Takes the form
namespace/name. The controller uses the first node port of this Service for
the default backend.`)
nxgConfigMap = flags.String("nginx-configmap", "",
`Name of the ConfigMap that containes the custom nginx configuration to use`)
tcpConfigMapName = flags.String("tcp-services-configmap", "",
`Name of the ConfigMap that containes the definition of the TCP services to expose.
The key in the map indicates the external port to be used. The value is the name of the
service with the format namespace/serviceName and the port of the service could be a number of the
name of the port.
The ports 80 and 443 are not allowed as external ports. This ports are reserved for nginx`)
udpConfigMapName = flags.String("udp-services-configmap", "",
`Name of the ConfigMap that containes the definition of the UDP services to expose.
The key in the map indicates the external port to be used. The value is the name of the
service with the format namespace/serviceName and the port of the service could be a number of the
name of the port.`)
resyncPeriod = flags.Duration("sync-period", 30*time.Second,
`Relist and confirm cloud resources this often.`)
watchNamespace = flags.String("watch-namespace", api.NamespaceAll,
`Namespace to watch for Ingress. Default is to watch all namespaces`)
healthzPort = flags.Int("healthz-port", healthPort, "port for healthz endpoint.")
profiling = flags.Bool("profiling", true, `Enable profiling via web interface host:port/debug/pprof/`)
defSSLCertificate = flags.String("default-ssl-certificate", "", `Name of the secret that contains a SSL
certificate to be used as default for a HTTPS catch-all server`)
defHealthzURL = flags.String("health-check-path", "/ingress-controller-healthz", `Defines the URL to
be used as health check inside in the default server in NGINX.`)
)
func main() {
flags.AddGoFlagSet(flag.CommandLine)
flags.Parse(os.Args)
clientConfig := kubectl_util.DefaultClientConfig(flags)
glog.Infof("Using build: %v - %v", gitRepo, version)
if *defaultSvc == "" {
glog.Fatalf("Please specify --default-backend-service")
}
kubeClient, err := unversioned.NewInCluster()
if err != nil {
config, err := clientConfig.ClientConfig()
if err != nil {
glog.Fatalf("error configuring the client: %v", err)
}
kubeClient, err = unversioned.New(config)
if err != nil {
glog.Fatalf("failed to create client: %v", err)
}
}
runtimePodInfo, err := getPodDetails(kubeClient)
if err != nil {
runtimePodInfo = &podInfo{NodeIP: "127.0.0.1"}
glog.Warningf("unexpected error getting runtime information: %v", err)
}
if err := isValidService(kubeClient, *defaultSvc); err != nil {
glog.Fatalf("no service with name %v found: %v", *defaultSvc, err)
}
glog.Infof("Validated %v as the default backend", *defaultSvc)
if *nxgConfigMap != "" {
_, _, err = parseNsName(*nxgConfigMap)
if err != nil {
glog.Fatalf("configmap error: %v", err)
}
}
lbc, err := newLoadBalancerController(kubeClient, *resyncPeriod,
*defaultSvc, *watchNamespace, *nxgConfigMap, *tcpConfigMapName,
*udpConfigMapName, *defSSLCertificate, *defHealthzURL, runtimePodInfo)
if err != nil {
glog.Fatalf("%v", err)
}
go registerHandlers(lbc)
go handleSigterm(lbc)
lbc.Run()
for {
glog.Infof("Handled quit, awaiting pod deletion")
time.Sleep(30 * time.Second)
}
}
// podInfo contains runtime information about the pod
type podInfo struct {
PodName string
PodNamespace string
NodeIP string
}
func registerHandlers(lbc *loadBalancerController) {
mux := http.NewServeMux()
healthz.InstallHandler(mux, lbc.nginx)
mux.HandleFunc("/build", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "build: %v - %v", gitRepo, version)
})
mux.HandleFunc("/stop", func(w http.ResponseWriter, r *http.Request) {
lbc.Stop()
})
if *profiling {
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
}
server := &http.Server{
Addr: fmt.Sprintf(":%v", *healthzPort),
Handler: mux,
}
glog.Fatal(server.ListenAndServe())
}
func handleSigterm(lbc *loadBalancerController) {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGTERM)
<-signalChan
glog.Infof("Received SIGTERM, shutting down")
exitCode := 0
if err := lbc.Stop(); err != nil {
glog.Infof("Error during shutdown %v", err)
exitCode = 1
}
glog.Infof("Exiting with %v", exitCode)
os.Exit(exitCode)
}

View file

@ -1,138 +0,0 @@
/*
Copyright 2015 The Kubernetes 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 nginx
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/healthz"
"k8s.io/contrib/ingress/controllers/nginx/nginx/config"
"k8s.io/contrib/ingress/controllers/nginx/nginx/ingress"
)
// Start starts a nginx (master process) and waits. If the process ends
// we need to kill the controller process and return the reason.
func (ngx *Manager) Start() {
glog.Info("Starting NGINX process...")
cmd := exec.Command("nginx")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
glog.Errorf("nginx error: %v", err)
}
if err := cmd.Wait(); err != nil {
glog.Errorf("nginx error: %v", err)
}
}
// CheckAndReload verify if the nginx configuration changed and sends a reload
//
// the master process receives the signal to reload configuration, it checks
// the syntax validity of the new configuration file and tries to apply the
// configuration provided in it. If this is a success, the master process starts
// new worker processes and sends messages to old worker processes, requesting them
// to shut down. Otherwise, the master process rolls back the changes and continues
// to work with the old configuration. Old worker processes, receiving a command to
// shut down, stop accepting new connections and continue to service current requests
// until all such requests are serviced. After that, the old worker processes exit.
// http://nginx.org/en/docs/beginners_guide.html#control
func (ngx *Manager) CheckAndReload(cfg config.Configuration, ingressCfg ingress.Configuration) error {
ngx.reloadRateLimiter.Accept()
ngx.reloadLock.Lock()
defer ngx.reloadLock.Unlock()
newCfg, err := ngx.template.Write(cfg, ingressCfg, ngx.testTemplate)
if err != nil {
return fmt.Errorf("failed to write new nginx configuration. Avoiding reload: %v", err)
}
changed, err := ngx.needsReload(newCfg)
if err != nil {
return err
}
if changed {
if err := ngx.shellOut("nginx -s reload"); err != nil {
return fmt.Errorf("error reloading nginx: %v", err)
}
glog.Info("change in configuration detected. Reloading...")
}
return nil
}
// shellOut executes a command and returns its combined standard output and standard
// error in case of an error in the execution
func (ngx *Manager) shellOut(cmd string) error {
out, err := exec.Command("sh", "-c", cmd).CombinedOutput()
if err != nil {
glog.Errorf("failed to execute %v: %v", cmd, string(out))
return err
}
return nil
}
// check to verify Manager implements HealthzChecker interface
var _ healthz.HealthzChecker = Manager{}
// Name returns the healthcheck name
func (ngx Manager) Name() string {
return "NGINX"
}
// Check returns if the nginx healthz endpoint is returning ok (status code 200)
func (ngx Manager) Check(_ *http.Request) error {
res, err := http.Get("http://127.0.0.1:18080/healthz")
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return fmt.Errorf("NGINX is unhealthy")
}
return nil
}
// testTemplate checks if the NGINX configuration inside the byte array is valid
// running the command "nginx -t" using a temporal file.
func (ngx Manager) testTemplate(cfg []byte) error {
tmpfile, err := ioutil.TempFile("", "nginx-cfg")
if err != nil {
return err
}
defer tmpfile.Close()
ioutil.WriteFile(tmpfile.Name(), cfg, 0644)
if err := ngx.shellOut(fmt.Sprintf("nginx -t -c %v", tmpfile.Name())); err != nil {
return fmt.Errorf("invalid nginx configuration: %v", err)
}
// in case of error do not remove temporal file
defer os.Remove(tmpfile.Name())
return nil
}

View file

@ -1,130 +0,0 @@
/*
Copyright 2015 The Kubernetes 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 ingress
import (
"k8s.io/contrib/ingress/controllers/nginx/nginx/auth"
"k8s.io/contrib/ingress/controllers/nginx/nginx/authreq"
"k8s.io/contrib/ingress/controllers/nginx/nginx/ipwhitelist"
"k8s.io/contrib/ingress/controllers/nginx/nginx/ratelimit"
"k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite"
)
// Configuration describes an NGINX configuration
type Configuration struct {
Upstreams []*Upstream
Servers []*Server
TCPUpstreams []*Location
UDPUpstreams []*Location
}
// Upstream describes an NGINX upstream
type Upstream struct {
Name string
Backends []UpstreamServer
Secure bool
}
// UpstreamByNameServers sorts upstreams by name
type UpstreamByNameServers []*Upstream
func (c UpstreamByNameServers) Len() int { return len(c) }
func (c UpstreamByNameServers) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c UpstreamByNameServers) Less(i, j int) bool {
return c[i].Name < c[j].Name
}
// UpstreamServer describes a server in an NGINX upstream
type UpstreamServer struct {
Address string
Port string
MaxFails int
FailTimeout int
}
// UpstreamServerByAddrPort sorts upstream servers by address and port
type UpstreamServerByAddrPort []UpstreamServer
func (c UpstreamServerByAddrPort) Len() int { return len(c) }
func (c UpstreamServerByAddrPort) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c UpstreamServerByAddrPort) Less(i, j int) bool {
iName := c[i].Address
jName := c[j].Address
if iName != jName {
return iName < jName
}
iU := c[i].Port
jU := c[j].Port
return iU < jU
}
// Server describes an NGINX server
type Server struct {
Name string
Locations []*Location
SSL bool
SSLCertificate string
SSLCertificateKey string
SSLPemChecksum string
}
// ServerByName sorts server by name
type ServerByName []*Server
func (c ServerByName) Len() int { return len(c) }
func (c ServerByName) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c ServerByName) Less(i, j int) bool {
return c[i].Name < c[j].Name
}
// Location describes an NGINX location
type Location struct {
Path string
IsDefBackend bool
Upstream Upstream
Auth auth.Nginx
RateLimit ratelimit.RateLimit
Redirect rewrite.Redirect
SecureUpstream bool
Whitelist ipwhitelist.SourceRange
EnableCORS bool
ExternalAuthURL authreq.Auth
}
// LocationByPath sorts location by path
// Location / is the last one
type LocationByPath []*Location
func (c LocationByPath) Len() int { return len(c) }
func (c LocationByPath) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c LocationByPath) Less(i, j int) bool {
return c[i].Path > c[j].Path
}
// SSLCert describes a SSL certificate to be used in NGINX
type SSLCert struct {
CertFileName string
KeyFileName string
// PemFileName contains the path to the file with the certificate and key concatenated
PemFileName string
// PemSHA contains the sha1 of the pem file.
// This is used to detect changes in the secret that contains the certificates
PemSHA string
// CN contains all the common names defined in the SSL certificate
CN []string
}

View file

@ -1,118 +0,0 @@
/*
Copyright 2015 The Kubernetes 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 nginx
import (
"os"
"strings"
"sync"
"github.com/golang/glog"
client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/util/flowcontrol"
"k8s.io/contrib/ingress/controllers/nginx/nginx/config"
"k8s.io/contrib/ingress/controllers/nginx/nginx/ingress"
ngx_template "k8s.io/contrib/ingress/controllers/nginx/nginx/template"
)
var (
tmplPath = "/etc/nginx/template/nginx.tmpl"
)
// Manager ...
type Manager struct {
ConfigFile string
defCfg config.Configuration
defResolver string
sslDHParam string
reloadRateLimiter flowcontrol.RateLimiter
// template loaded ready to be used to generate the nginx configuration file
template *ngx_template.Template
reloadLock *sync.Mutex
}
// NewDefaultServer return an UpstreamServer to be use as default server that returns 503.
func NewDefaultServer() ingress.UpstreamServer {
return ingress.UpstreamServer{Address: "127.0.0.1", Port: "8181"}
}
// NewUpstream creates an upstream without servers.
func NewUpstream(name string) *ingress.Upstream {
return &ingress.Upstream{
Name: name,
Backends: []ingress.UpstreamServer{},
}
}
// NewManager ...
func NewManager(kubeClient *client.Client) *Manager {
ngx := &Manager{
ConfigFile: "/etc/nginx/nginx.conf",
defCfg: config.NewDefault(),
reloadLock: &sync.Mutex{},
reloadRateLimiter: flowcontrol.NewTokenBucketRateLimiter(0.1, 1),
}
res, err := getDNSServers()
if err != nil {
glog.Warningf("error reading nameservers: %v", err)
}
ngx.defResolver = strings.Join(res, " ")
ngx.createCertsDir(config.SSLDirectory)
ngx.sslDHParam = ngx.SearchDHParamFile(config.SSLDirectory)
var onChange func()
onChange = func() {
template, err := ngx_template.NewTemplate(tmplPath, onChange)
if err != nil {
glog.Warningf("invalid NGINX template: %v", err)
return
}
ngx.template.Close()
ngx.template = template
glog.Info("new NGINX template loaded")
}
template, err := ngx_template.NewTemplate(tmplPath, onChange)
if err != nil {
glog.Fatalf("invalid NGINX template: %v", err)
}
ngx.template = template
return ngx
}
func (nginx *Manager) createCertsDir(base string) {
if err := os.Mkdir(base, os.ModeDir); err != nil {
if os.IsExist(err) {
glog.Infof("%v already exists", err)
return
}
glog.Fatalf("Couldn't create directory %v: %v", base, err)
}
}

View file

@ -1,135 +0,0 @@
/*
Copyright 2015 The Kubernetes 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 nginx
import (
"crypto/sha1"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"github.com/golang/glog"
"k8s.io/contrib/ingress/controllers/nginx/nginx/config"
"k8s.io/contrib/ingress/controllers/nginx/nginx/ingress"
)
// AddOrUpdateCertAndKey creates a .pem file wth the cert and the key with the specified name
func (nginx *Manager) AddOrUpdateCertAndKey(name string, cert string, key string) (ingress.SSLCert, error) {
temporaryPemFileName := fmt.Sprintf("%v.pem", name)
pemFileName := fmt.Sprintf("%v/%v.pem", config.SSLDirectory, name)
temporaryPemFile, err := ioutil.TempFile("", temporaryPemFileName)
if err != nil {
return ingress.SSLCert{}, fmt.Errorf("Couldn't create temp pem file %v: %v", temporaryPemFile.Name(), err)
}
_, err = temporaryPemFile.WriteString(fmt.Sprintf("%v\n%v", cert, key))
if err != nil {
return ingress.SSLCert{}, fmt.Errorf("Couldn't write to pem file %v: %v", temporaryPemFile.Name(), err)
}
err = temporaryPemFile.Close()
if err != nil {
return ingress.SSLCert{}, fmt.Errorf("Couldn't close temp pem file %v: %v", temporaryPemFile.Name(), err)
}
cn, err := nginx.commonNames(temporaryPemFile.Name())
if err != nil {
os.Remove(temporaryPemFile.Name())
return ingress.SSLCert{}, err
}
err = os.Rename(temporaryPemFile.Name(), pemFileName)
if err != nil {
os.Remove(temporaryPemFile.Name())
return ingress.SSLCert{}, fmt.Errorf("Couldn't move temp pem file %v to destination %v: %v", temporaryPemFile.Name(), pemFileName, err)
}
return ingress.SSLCert{
CertFileName: cert,
KeyFileName: key,
PemFileName: pemFileName,
PemSHA: nginx.pemSHA1(pemFileName),
CN: cn,
}, nil
}
// commonNames checks if the certificate and key file are valid
// returning the result of the validation and the list of hostnames
// contained in the common name/s
func (nginx *Manager) commonNames(pemFileName string) ([]string, error) {
pemCerts, err := ioutil.ReadFile(pemFileName)
if err != nil {
return []string{}, err
}
block, _ := pem.Decode(pemCerts)
if block == nil {
return []string{}, fmt.Errorf("No valid PEM formatted block found")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return []string{}, err
}
cn := []string{cert.Subject.CommonName}
if len(cert.DNSNames) > 0 {
cn = append(cn, cert.DNSNames...)
}
glog.V(3).Infof("found %v common names: %v\n", cn, len(cn))
return cn, nil
}
// SearchDHParamFile iterates all the secrets mounted inside the /etc/nginx-ssl directory
// in order to find a file with the name dhparam.pem. If such file exists it will
// returns the path. If not it just returns an empty string
func (nginx *Manager) SearchDHParamFile(baseDir string) string {
files, _ := ioutil.ReadDir(baseDir)
for _, file := range files {
if !file.IsDir() {
continue
}
dhPath := fmt.Sprintf("%v/%v/dhparam.pem", baseDir, file.Name())
if _, err := os.Stat(dhPath); err == nil {
glog.Infof("using file '%v' for parameter ssl_dhparam", dhPath)
return dhPath
}
}
glog.Warning("no file dhparam.pem found in secrets")
return ""
}
// pemSHA1 returns the SHA1 of a pem file. This is used to
// reload NGINX in case a secret with a SSL certificate changed.
func (nginx *Manager) pemSHA1(filename string) string {
hasher := sha1.New()
s, err := ioutil.ReadFile(filename)
if err != nil {
return ""
}
hasher.Write(s)
return hex.EncodeToString(hasher.Sum(nil))
}

View file

@ -1,224 +0,0 @@
/*
Copyright 2015 The Kubernetes 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 nginx
import (
"bytes"
"io/ioutil"
"os"
"os/exec"
"reflect"
"strconv"
"strings"
"github.com/golang/glog"
"github.com/mitchellh/mapstructure"
"k8s.io/kubernetes/pkg/api"
"k8s.io/contrib/ingress/controllers/nginx/nginx/config"
)
const (
customHTTPErrors = "custom-http-errors"
skipAccessLogUrls = "skip-access-log-urls"
)
// getDNSServers returns the list of nameservers located in the file /etc/resolv.conf
func getDNSServers() ([]string, error) {
var nameservers []string
file, err := ioutil.ReadFile("/etc/resolv.conf")
if err != nil {
return nameservers, err
}
// Lines of the form "nameserver 1.2.3.4" accumulate.
lines := strings.Split(string(file), "\n")
for l := range lines {
trimmed := strings.TrimSpace(lines[l])
if strings.HasPrefix(trimmed, "#") {
continue
}
fields := strings.Fields(trimmed)
if len(fields) == 0 {
continue
}
if fields[0] == "nameserver" {
nameservers = append(nameservers, fields[1:]...)
}
}
glog.V(3).Infof("nameservers to use: %v", nameservers)
return nameservers, nil
}
// getConfigKeyToStructKeyMap returns a map with the ConfigMapKey as key and the StructName as value.
func getConfigKeyToStructKeyMap() map[string]string {
keyMap := map[string]string{}
n := &config.Configuration{}
val := reflect.Indirect(reflect.ValueOf(n))
for i := 0; i < val.Type().NumField(); i++ {
fieldSt := val.Type().Field(i)
configMapKey := strings.Split(fieldSt.Tag.Get("structs"), ",")[0]
structKey := fieldSt.Name
keyMap[configMapKey] = structKey
}
return keyMap
}
// ReadConfig obtains the configuration defined by the user merged with the defaults.
func (ngx *Manager) ReadConfig(conf *api.ConfigMap) config.Configuration {
if len(conf.Data) == 0 {
return config.NewDefault()
}
cfgCM := config.Configuration{}
cfgDefault := config.NewDefault()
metadata := &mapstructure.Metadata{}
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "structs",
Result: &cfgCM,
WeaklyTypedInput: true,
Metadata: metadata,
})
cErrors := make([]int, 0)
if val, ok := conf.Data[customHTTPErrors]; ok {
delete(conf.Data, customHTTPErrors)
for _, i := range strings.Split(val, ",") {
j, err := strconv.Atoi(i)
if err != nil {
glog.Warningf("%v is not a valid http code: %v", i, err)
} else {
cErrors = append(cErrors, j)
}
}
}
cSkipUrls := make([]string, 0)
if val, ok := conf.Data[skipAccessLogUrls]; ok {
delete(conf.Data, skipAccessLogUrls)
cSkipUrls = strings.Split(val, ",")
}
err = decoder.Decode(conf.Data)
if err != nil {
glog.Infof("%v", err)
}
keyMap := getConfigKeyToStructKeyMap()
valCM := reflect.Indirect(reflect.ValueOf(cfgCM))
for _, key := range metadata.Keys {
fieldName, ok := keyMap[key]
if !ok {
continue
}
valDefault := reflect.ValueOf(&cfgDefault).Elem().FieldByName(fieldName)
fieldCM := valCM.FieldByName(fieldName)
if valDefault.IsValid() {
valDefault.Set(fieldCM)
}
}
cfgDefault.CustomHTTPErrors = ngx.filterErrors(cErrors)
cfgDefault.SkipAccessLogURLs = cSkipUrls
// no custom resolver means use the system resolver
if cfgDefault.Resolver == "" {
cfgDefault.Resolver = ngx.defResolver
}
return cfgDefault
}
func (ngx *Manager) filterErrors(errCodes []int) []int {
fa := make([]int, 0)
for _, errCode := range errCodes {
if errCode > 299 && errCode < 600 {
fa = append(fa, errCode)
} else {
glog.Warningf("error code %v is not valid for custom error pages", errCode)
}
}
return fa
}
func (ngx *Manager) needsReload(data []byte) (bool, error) {
filename := ngx.ConfigFile
in, err := os.Open(filename)
if err != nil {
return false, err
}
src, err := ioutil.ReadAll(in)
in.Close()
if err != nil {
return false, err
}
if !bytes.Equal(src, data) {
err = ioutil.WriteFile(filename, data, 0644)
if err != nil {
return false, err
}
dData, err := diff(src, data)
if err != nil {
glog.Errorf("error computing diff: %s", err)
return true, nil
}
if glog.V(2) {
glog.Infof("NGINX configuration diff a/%s b/%s\n", filename, filename)
glog.Infof("%v", string(dData))
}
return len(dData) > 0, nil
}
return false, nil
}
func diff(b1, b2 []byte) (data []byte, err error) {
f1, err := ioutil.TempFile("", "")
if err != nil {
return
}
defer os.Remove(f1.Name())
defer f1.Close()
f2, err := ioutil.TempFile("", "")
if err != nil {
return
}
defer os.Remove(f2.Name())
defer f2.Close()
f1.Write(b1)
f2.Write(b2)
data, err = exec.Command("diff", "-u", f1.Name(), f2.Name()).CombinedOutput()
if len(data) > 0 {
err = nil
}
return
}

View file

@ -1,101 +0,0 @@
/*
Copyright 2015 The Kubernetes 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 nginx
import (
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/contrib/ingress/controllers/nginx/nginx/config"
)
func getConfigNginxBool(data map[string]string) config.Configuration {
manager := &Manager{}
configMap := &api.ConfigMap{
Data: data,
}
return manager.ReadConfig(configMap)
}
func TestManagerReadConfigBoolFalse(t *testing.T) {
configNginx := getConfigNginxBool(map[string]string{
"hsts-include-subdomains": "false",
"use-proxy-protocol": "false",
})
if configNginx.HSTSIncludeSubdomains {
t.Error("Failed to config boolean value (default true) to false")
}
if configNginx.UseProxyProtocol {
t.Error("Failed to config boolean value (default false) to false")
}
}
func TestManagerReadConfigBoolTrue(t *testing.T) {
configNginx := getConfigNginxBool(map[string]string{
"hsts-include-subdomains": "true",
"use-proxy-protocol": "true",
})
if !configNginx.HSTSIncludeSubdomains {
t.Error("Failed to config boolean value (default true) to true")
}
if !configNginx.UseProxyProtocol {
t.Error("Failed to config boolean value (default false) to true")
}
}
func TestManagerReadConfigBoolNothing(t *testing.T) {
configNginx := getConfigNginxBool(map[string]string{
"invaild-key": "true",
})
if !configNginx.HSTSIncludeSubdomains {
t.Error("Failed to get default boolean value true")
}
if configNginx.UseProxyProtocol {
t.Error("Failed to get default boolean value false")
}
}
func TestManagerReadConfigStringSet(t *testing.T) {
configNginx := getConfigNginxBool(map[string]string{
"ssl-protocols": "TLSv1.2",
})
exp := "TLSv1.2"
if configNginx.SSLProtocols != exp {
t.Errorf("Failed to set string value true actual='%s' expected='%s'", configNginx.SSLProtocols, exp)
}
}
func TestManagerReadConfigStringNothing(t *testing.T) {
configNginx := getConfigNginxBool(map[string]string{
"not-existing": "TLSv1.2",
})
exp := "10m"
if configNginx.SSLSessionTimeout != exp {
t.Errorf("Failed to set string value true actual='%s' expected='%s'", configNginx.SSLSessionTimeout, exp)
}
}
func TestGetDNSServers(t *testing.T) {
s, err := getDNSServers()
if err != nil {
t.Fatalf("unexpected error reading /etc/resolv.conf file: %v", err)
}
if len(s) < 1 {
t.Error("expected at least 1 nameserver in /etc/resolv.conf")
}
}

View file

@ -0,0 +1,60 @@
/*
Copyright 2015 The Kubernetes 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 main
import (
"os"
"os/signal"
"syscall"
"time"
"github.com/golang/glog"
"k8s.io/ingress/core/pkg/ingress/controller"
)
func main() {
// start a new nginx controller
ngx := newNGINXController()
// create a custom Ingress controller using NGINX as backend
ic := controller.NewIngressController(ngx)
go handleSigterm(ic)
// start the controller
ic.Start()
// wait
glog.Infof("shutting down Ingress controller...")
for {
glog.Infof("Handled quit, awaiting pod deletion")
time.Sleep(30 * time.Second)
}
}
func handleSigterm(ic controller.Interface) {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGTERM)
<-signalChan
glog.Infof("Received SIGTERM, shutting down")
exitCode := 0
if err := ic.Stop(); err != nil {
glog.Infof("Error during shutdown %v", err)
exitCode = 1
}
glog.Infof("Exiting with %v", exitCode)
os.Exit(exitCode)
}

View file

@ -0,0 +1,266 @@
/*
Copyright 2015 The Kubernetes 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 main
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"github.com/golang/glog"
"k8s.io/ingress/core/pkg/ingress"
"k8s.io/ingress/core/pkg/ingress/defaults"
"k8s.io/ingress/controllers/nginx/pkg/config"
ngx_template "k8s.io/ingress/controllers/nginx/pkg/template"
"k8s.io/ingress/controllers/nginx/pkg/version"
"k8s.io/kubernetes/pkg/api"
)
var (
tmplPath = "/etc/nginx/template/nginx.tmpl"
cfgPath = "/etc/nginx/nginx.conf"
binary = "/usr/sbin/nginx"
)
// newNGINXController creates a new NGINX Ingress controller.
// If the environment variable NGINX_BINARY exists it will be used
// as source for nginx commands
func newNGINXController() ingress.Controller {
ngx := os.Getenv("NGINX_BINARY")
if ngx == "" {
ngx = binary
}
n := NGINXController{binary: ngx}
var onChange func()
onChange = func() {
template, err := ngx_template.NewTemplate(tmplPath, onChange)
if err != nil {
// this error is different from the rest because it must be clear why nginx is not working
glog.Errorf(`
-------------------------------------------------------------------------------
Error loading new template : %v
-------------------------------------------------------------------------------
`, err)
return
}
n.t.Close()
n.t = template
glog.Info("new NGINX template loaded")
}
ngxTpl, err := ngx_template.NewTemplate(tmplPath, onChange)
if err != nil {
glog.Fatalf("invalid NGINX template: %v", err)
}
n.t = ngxTpl
return n
}
// NGINXController ...
type NGINXController struct {
t *ngx_template.Template
binary string
}
// Start ...
func (n NGINXController) Start() {
glog.Info("starting NGINX process...")
cmd := exec.Command(n.binary, "-c", cfgPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
glog.Fatalf("nginx error: %v", err)
}
if err := cmd.Wait(); err != nil {
glog.Errorf("nginx error: %v", err)
}
}
// Stop ...
func (n NGINXController) Stop() error {
n.t.Close()
return exec.Command(n.binary, "-s", "stop").Run()
}
// Restart ...
func (n NGINXController) Restart(data []byte) ([]byte, error) {
err := ioutil.WriteFile(cfgPath, data, 0644)
if err != nil {
return nil, err
}
return exec.Command(n.binary, "-s", "reload").CombinedOutput()
}
// Test checks is a file contains a valid NGINX configuration
func (n NGINXController) Test(file string) *exec.Cmd {
return exec.Command(n.binary, "-t", "-c", file)
}
// UpstreamDefaults returns the nginx defaults
func (n NGINXController) UpstreamDefaults() defaults.Backend {
d := config.NewDefault()
return d.Backend
}
// IsReloadRequired check if the new configuration file is different
// from the current one.
func (n NGINXController) IsReloadRequired(data []byte) bool {
in, err := os.Open(cfgPath)
if err != nil {
return false
}
src, err := ioutil.ReadAll(in)
in.Close()
if err != nil {
return false
}
if !bytes.Equal(src, data) {
tmpfile, err := ioutil.TempFile("", "nginx-cfg-diff")
if err != nil {
glog.Errorf("error creating temporal file: %s", err)
return false
}
defer tmpfile.Close()
err = ioutil.WriteFile(tmpfile.Name(), data, 0644)
if err != nil {
return false
}
diffOutput, err := diff(src, data)
if err != nil {
glog.Errorf("error computing diff: %s", err)
return true
}
if glog.V(2) {
glog.Infof("NGINX configuration diff\n")
glog.Infof("%v", string(diffOutput))
}
return len(diffOutput) > 0
}
return false
}
// Info return build information
func (n NGINXController) Info() string {
return fmt.Sprintf("build version %v from repo %v commit %v", version.RELEASE, version.REPO, version.COMMIT)
}
// testTemplate checks if the NGINX configuration inside the byte array is valid
// running the command "nginx -t" using a temporal file.
func (n NGINXController) testTemplate(cfg []byte) error {
tmpfile, err := ioutil.TempFile("", "nginx-cfg")
if err != nil {
return err
}
defer tmpfile.Close()
ioutil.WriteFile(tmpfile.Name(), cfg, 0644)
out, err := n.Test(tmpfile.Name()).CombinedOutput()
if err != nil {
// this error is different from the rest because it must be clear why nginx is not working
return fmt.Errorf(`
-------------------------------------------------------------------------------
Error: %v
%v
-------------------------------------------------------------------------------
`, err, string(out))
}
os.Remove(tmpfile.Name())
return nil
}
// OnUpdate is called by syncQueue in https://github.com/aledbf/ingress-controller/blob/master/pkg/ingress/controller/controller.go#L82
// periodically to keep the configuration in sync.
//
// convert configmap to custom configuration object (different in each implementation)
// write the custom template (the complexity depends on the implementation)
// write the configuration file
// returning nill implies the backend will be reloaded.
// if an error is returned means requeue the update
func (n NGINXController) OnUpdate(cmap *api.ConfigMap, ingressCfg ingress.Configuration) ([]byte, error) {
var longestName int
var serverNames int
for _, srv := range ingressCfg.Servers {
serverNames += len([]byte(srv.Name))
if longestName < len(srv.Name) {
longestName = len(srv.Name)
}
}
cfg := ngx_template.ReadConfig(cmap)
// NGINX cannot resize the has tables used to store server names.
// For this reason we check if the defined size defined is correct
// for the FQDN defined in the ingress rules adjusting the value
// if is required.
// https://trac.nginx.org/nginx/ticket/352
// https://trac.nginx.org/nginx/ticket/631
nameHashBucketSize := nextPowerOf2(longestName)
if nameHashBucketSize > cfg.ServerNameHashBucketSize {
glog.V(3).Infof("adjusting ServerNameHashBucketSize variable from %v to %v",
cfg.ServerNameHashBucketSize, nameHashBucketSize)
cfg.ServerNameHashBucketSize = nameHashBucketSize
}
serverNameHashMaxSize := nextPowerOf2(serverNames)
if serverNameHashMaxSize > cfg.ServerNameHashMaxSize {
glog.V(3).Infof("adjusting ServerNameHashMaxSize variable from %v to %v",
cfg.ServerNameHashMaxSize, serverNameHashMaxSize)
cfg.ServerNameHashMaxSize = serverNameHashMaxSize
}
conf := make(map[string]interface{})
// adjust the size of the backlog
conf["backlogSize"] = sysctlSomaxconn()
conf["upstreams"] = ingressCfg.Upstreams
conf["passthroughUpstreams"] = ingressCfg.PassthroughUpstreams
conf["servers"] = ingressCfg.Servers
conf["tcpUpstreams"] = ingressCfg.TCPUpstreams
conf["udpUpstreams"] = ingressCfg.UDPUpstreams
conf["healthzURL"] = ingressCfg.HealthzURL
conf["defResolver"] = cfg.Resolver
conf["sslDHParam"] = ""
conf["customErrors"] = len(cfg.CustomHTTPErrors) > 0
conf["cfg"] = ngx_template.StandarizeKeyNames(cfg)
return n.t.Write(conf, n.testTemplate)
}
// http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
// https://play.golang.org/p/TVSyCcdxUh
func nextPowerOf2(v int) int {
v--
v |= v >> 1
v |= v >> 2
v |= v >> 4
v |= v >> 8
v |= v >> 16
v++
return v
}

View file

@ -0,0 +1,62 @@
/*
Copyright 2015 The Kubernetes 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 main
import (
"io/ioutil"
"os"
"os/exec"
"k8s.io/kubernetes/pkg/util/sysctl"
"github.com/golang/glog"
)
// sysctlSomaxconn returns the value of net.core.somaxconn, i.e.
// maximum number of connections that can be queued for acceptance
// http://nginx.org/en/docs/http/ngx_http_core_module.html#listen
func sysctlSomaxconn() int {
maxConns, err := sysctl.New().GetSysctl("net/core/somaxconn")
if err != nil || maxConns < 512 {
glog.V(3).Infof("system net.core.somaxconn=%v (using system default)", maxConns)
return 511
}
return maxConns
}
func diff(b1, b2 []byte) ([]byte, error) {
f1, err := ioutil.TempFile("", "a")
if err != nil {
return nil, err
}
defer f1.Close()
defer os.Remove(f1.Name())
f2, err := ioutil.TempFile("", "b")
if err != nil {
return nil, err
}
defer f2.Close()
defer os.Remove(f2.Name())
f1.Write(b1)
f2.Write(b2)
out, _ := exec.Command("diff", "-u", f1.Name(), f2.Name()).CombinedOutput()
return out, nil
}

View file

@ -0,0 +1,41 @@
/*
Copyright 2015 The Kubernetes 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 main
import "testing"
func TestDiff(t *testing.T) {
tests := []struct {
a []byte
b []byte
empty bool
}{
{[]byte(""), []byte(""), true},
{[]byte("a"), []byte("a"), true},
{[]byte("a"), []byte("b"), false},
}
for _, test := range tests {
b, err := diff(test.a, test.b)
if err != nil {
t.Fatalf("unexpected error returned: %v", err)
}
if len(b) == 0 && !test.empty {
t.Fatalf("expected empty but returned %s", b)
}
}
}

View file

@ -18,7 +18,8 @@ package config
import (
"runtime"
"strconv"
"k8s.io/ingress/core/pkg/ingress/defaults"
"github.com/golang/glog"
)
@ -42,7 +43,7 @@ const (
// If UseProxyProtocol is enabled defIPCIDR defines the default the IP/network address of your external load balancer
defIPCIDR = "0.0.0.0/0"
gzipTypes = "application/atom+xml application/javascript aplication/x-javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/plain text/x-component"
gzipTypes = "application/atom+xml application/javascript application/x-javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/plain text/x-component"
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_buffer_size
// Sets the size of the buffer used for sending data.
@ -69,18 +70,17 @@ const (
var (
// SSLDirectory contains the mounted secrets with SSL certificates, keys and
SSLDirectory = "/etc/nginx-ssl"
SSLDirectory = "/etc/ingress-controller/ssl"
)
// Configuration represents the content of nginx.conf file
type Configuration struct {
defaults.Backend
// http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size
// Sets the maximum allowed size of the client request body
BodySize string `structs:"body-size,omitempty"`
// HealthzURL defines the URL should be used in probes
HealthzURL string
// EnableDynamicTLSRecords enables dynamic TLS record sizes
// https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency
// By default this is enabled
@ -126,12 +126,6 @@ type Configuration struct {
// accessed using HTTPS.
HSTSMaxAge string `structs:"hsts-max-age,omitempty"`
// enables which HTTP codes should be passed for processing with the error_page directive
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_intercept_errors
// http://nginx.org/en/docs/http/ngx_http_core_module.html#error_page
// By default this is disabled
CustomHTTPErrors []int `structs:"custom-http-errors,-"`
// Time during which a keep-alive client connection will stay open on the server side.
// The zero value disables keep-alive client connections
// http://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_timeout
@ -146,34 +140,10 @@ type Configuration struct {
// http://nginx.org/en/docs/http/ngx_http_map_module.html#map_hash_bucket_size
MapHashBucketSize int `structs:"map-hash-bucket-size,omitempty"`
// Defines a timeout for establishing a connection with a proxied server.
// It should be noted that this timeout cannot usually exceed 75 seconds.
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_connect_timeout
ProxyConnectTimeout int `structs:"proxy-connect-timeout,omitempty"`
// If UseProxyProtocol is enabled ProxyRealIPCIDR defines the default the IP/network address
// of your external load balancer
ProxyRealIPCIDR string `structs:"proxy-real-ip-cidr,omitempty"`
// Timeout in seconds for reading a response from the proxied server. The timeout is set only between
// two successive read operations, not for the transmission of the whole response
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout
ProxyReadTimeout int `structs:"proxy-read-timeout,omitempty"`
// Timeout in seconds for transmitting a request to the proxied server. The timeout is set only between
// two successive write operations, not for the transmission of the whole request.
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_send_timeout
ProxySendTimeout int `structs:"proxy-send-timeout,omitempty"`
// Sets the size of the buffer used for reading the first part of the response received from the
// proxied server. This part usually contains a small response header.
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffer_size)
ProxyBufferSize string `structs:"proxy-buffer-size,omitempty"`
// Configures name servers used to resolve names of upstream servers into addresses
// http://nginx.org/en/docs/http/ngx_http_core_module.html#resolver
Resolver string `structs:"resolver,omitempty"`
// Maximum size of the server names hash tables used in server names, map directives values,
// MIME types, names of request header strings, etcd.
// http://nginx.org/en/docs/hash.html
@ -185,20 +155,6 @@ type Configuration struct {
// http://nginx.org/en/docs/http/ngx_http_core_module.html#server_names_hash_bucket_size
ServerNameHashBucketSize int `structs:"server-name-hash-bucket-size,omitempty"`
// SkipAccessLogURLs sets a list of URLs that should not appear in the NGINX access log
// This is useful with urls like `/health` or `health-check` that make "complex" reading the logs
// By default this list is empty
SkipAccessLogURLs []string `structs:"skip-access-log-urls,-"`
// Enables or disables the redirect (301) to the HTTPS port
SSLRedirect bool `structs:"ssl-redirect,omitempty"`
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_buffer_size
// Sets the size of the buffer used for sending data.
// 4k helps NGINX to improve TLS Time To First Byte (TTTFB)
// https://www.igvita.com/2013/12/16/optimizing-nginx-tls-time-to-first-byte/
SSLBufferSize string `structs:"ssl-buffer-size,omitempty"`
// Enabled ciphers list to enabled. The ciphers are specified in the format understood by
// the OpenSSL library
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_ciphers
@ -230,17 +186,11 @@ type Configuration struct {
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_timeout
SSLSessionTimeout string `structs:"ssl-session-timeout,omitempty"`
// Number of unsuccessful attempts to communicate with the server that should happen in the
// duration set by the fail_timeout parameter to consider the server unavailable
// http://nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream
// Default: 0, ie use platform liveness probe
UpstreamMaxFails int `structs:"upstream-max-fails,omitempty"`
// Time during which the specified number of unsuccessful attempts to communicate with
// the server should happen to consider the server unavailable
// http://nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream
// Default: 0, ie use platform liveness probe
UpstreamFailTimeout int `structs:"upstream-fail-timeout,omitempty"`
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_buffer_size
// Sets the size of the buffer used for sending data.
// 4k helps NGINX to improve TLS Time To First Byte (TTTFB)
// https://www.igvita.com/2013/12/16/optimizing-nginx-tls-time-to-first-byte/
SSLBufferSize string `structs:"ssl-buffer-size,omitempty"`
// Enables or disables the use of the PROXY protocol to receive client connection
// (real IP address) information passed through proxy servers and load balancers
@ -261,13 +211,9 @@ type Configuration struct {
// Responses with the “text/html” type are always compressed if UseGzip is enabled
GzipTypes string `structs:"gzip-types,omitempty"`
// WhitelistSourceRange allows limiting access to certain client addresses
// http://nginx.org/en/docs/http/ngx_http_access_module.html
WhitelistSourceRange []string `structs:"whitelist-source-range,omitempty"`
// Defines the number of worker processes. By default auto means number of available CPU cores
// http://nginx.org/en/docs/ngx_core_module.html#worker_processes
WorkerProcesses string `structs:"worker-processes,omitempty"`
WorkerProcesses int `structs:"worker-processes,omitempty"`
}
// NewDefault returns the default configuration contained
@ -276,7 +222,7 @@ func NewDefault() Configuration {
cfg := Configuration{
BodySize: bodySize,
EnableDynamicTLSRecords: true,
EnableSPDY: true,
EnableSPDY: false,
ErrorLogLevel: errorLevel,
HSTS: true,
HSTSIncludeSubdomains: true,
@ -285,14 +231,9 @@ func NewDefault() Configuration {
KeepAlive: 75,
MaxWorkerConnections: 16384,
MapHashBucketSize: 64,
ProxyConnectTimeout: 5,
ProxyRealIPCIDR: defIPCIDR,
ProxyReadTimeout: 60,
ProxySendTimeout: 60,
ProxyBufferSize: "4k",
ServerNameHashMaxSize: 512,
ServerNameHashBucketSize: 64,
SSLRedirect: true,
SSLBufferSize: sslBufferSize,
SSLCiphers: sslCiphers,
SSLProtocols: sslProtocols,
@ -302,12 +243,19 @@ func NewDefault() Configuration {
SSLSessionTimeout: sslSessionTimeout,
UseProxyProtocol: false,
UseGzip: true,
WorkerProcesses: strconv.Itoa(runtime.NumCPU()),
WorkerProcesses: runtime.NumCPU(),
VtsStatusZoneSize: "10m",
UseHTTP2: true,
CustomHTTPErrors: make([]int, 0),
WhitelistSourceRange: make([]string, 0),
SkipAccessLogURLs: make([]string, 0),
Backend: defaults.Backend{
ProxyConnectTimeout: 5,
ProxyReadTimeout: 60,
ProxySendTimeout: 60,
ProxyBufferSize: "4k",
SSLRedirect: true,
CustomHTTPErrors: []int{},
WhitelistSourceRange: []string{},
SkipAccessLogURLs: []string{},
},
}
if glog.V(5) {

View file

@ -0,0 +1,122 @@
/*
Copyright 2015 The Kubernetes 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 template
import (
"strconv"
"strings"
"github.com/golang/glog"
"github.com/imdario/mergo"
"github.com/fatih/structs"
"github.com/mitchellh/mapstructure"
go_camelcase "github.com/segmentio/go-camelcase"
"k8s.io/ingress/controllers/nginx/pkg/config"
"k8s.io/ingress/core/pkg/ingress/defaults"
"k8s.io/kubernetes/pkg/api"
)
const (
customHTTPErrors = "custom-http-errors"
skipAccessLogUrls = "skip-access-log-urls"
whitelistSourceRange = "whitelist-source-range"
)
// StandarizeKeyNames ...
func StandarizeKeyNames(data interface{}) map[string]interface{} {
return fixKeyNames(structs.Map(data))
}
// ReadConfig obtains the configuration defined by the user merged with the defaults.
func ReadConfig(conf *api.ConfigMap) config.Configuration {
if len(conf.Data) == 0 {
return config.NewDefault()
}
var errors []int
var skipUrls []string
var whitelist []string
if val, ok := conf.Data[customHTTPErrors]; ok {
delete(conf.Data, customHTTPErrors)
for _, i := range strings.Split(val, ",") {
j, err := strconv.Atoi(i)
if err != nil {
glog.Warningf("%v is not a valid http code: %v", i, err)
} else {
errors = append(errors, j)
}
}
}
if val, ok := conf.Data[skipAccessLogUrls]; ok {
delete(conf.Data, skipAccessLogUrls)
skipUrls = strings.Split(val, ",")
}
if val, ok := conf.Data[whitelistSourceRange]; ok {
delete(conf.Data, whitelistSourceRange)
whitelist = append(whitelist, strings.Split(val, ",")...)
}
to := config.Configuration{}
to.Backend = defaults.Backend{
CustomHTTPErrors: filterErrors(errors),
SkipAccessLogURLs: skipUrls,
WhitelistSourceRange: whitelist,
}
def := config.NewDefault()
if err := mergo.Merge(&to, def); err != nil {
glog.Warningf("unexpected error merging defaults: %v", err)
}
metadata := &mapstructure.Metadata{}
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "structs",
Result: &to,
WeaklyTypedInput: true,
Metadata: metadata,
})
err = decoder.Decode(conf.Data)
if err != nil {
glog.Infof("%v", err)
}
return to
}
func filterErrors(codes []int) []int {
var fa []int
for _, code := range codes {
if code > 299 && code < 600 {
fa = append(fa, code)
} else {
glog.Warningf("error code %v is not valid for custom error pages", code)
}
}
return fa
}
func fixKeyNames(data map[string]interface{}) map[string]interface{} {
fixed := make(map[string]interface{})
for k, v := range data {
fixed[go_camelcase.Camelcase(k)] = v
}
return fixed
}

View file

@ -0,0 +1,83 @@
/*
Copyright 2015 The Kubernetes 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 template
import (
"testing"
"k8s.io/ingress/controllers/nginx/pkg/config"
)
func TestStandarizeKeyNames(t *testing.T) {
}
func TestFilterErrors(t *testing.T) {
e := filterErrors([]int{200, 300, 345, 500, 555, 999})
if len(e) != 4 {
t.Errorf("expected 4 elements but %v returned", len(e))
}
}
func TestFixKeyNames(t *testing.T) {
d := map[string]interface{}{
"one": "one",
"one-example": "oneExample",
"aMore-complex_example": "aMoreComplexExample",
"a": "a",
}
fixed := fixKeyNames(d)
for k, v := range fixed {
if k != v {
t.Errorf("expected %v but retuned %v", v, k)
}
}
}
type testStruct struct {
ProxyReadTimeout int `structs:"proxy-read-timeout,omitempty"`
ProxySendTimeout int `structs:"proxy-send-timeout,omitempty"`
CustomHTTPErrors []int `structs:"custom-http-errors"`
SkipAccessLogURLs []string `structs:"skip-access-log-urls,-"`
NoTag string
}
var decodedData = &testStruct{
1,
2,
[]int{300, 400},
[]string{"/log"},
"",
}
func TestMergeConfigMapToStruct(t *testing.T) {
/*conf := &api.ConfigMap{
Data: map[string]string{
"custom-http-errors": "300,400",
"proxy-read-timeout": "1",
"proxy-send-timeout": "2",
"skip-access-log-urls": "/log",
},
}*/
def := config.NewDefault()
def.CustomHTTPErrors = []int{300, 400}
def.SkipAccessLogURLs = []string{"/log"}
//to := ReadConfig(conf)
//if !reflect.DeepEqual(def, to) {
// t.Errorf("expected %v but retuned %v", def, to)
//}
}

View file

@ -21,51 +21,28 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"regexp"
"os/exec"
"strings"
text_template "text/template"
"github.com/fatih/structs"
"github.com/golang/glog"
"k8s.io/contrib/ingress/controllers/nginx/nginx/config"
"k8s.io/contrib/ingress/controllers/nginx/nginx/ingress"
"k8s.io/kubernetes/pkg/util/sysctl"
"k8s.io/ingress/core/pkg/ingress"
"k8s.io/ingress/core/pkg/watch"
)
const (
slash = "/"
)
var (
camelRegexp = regexp.MustCompile("[0-9A-Za-z]+")
funcMap = text_template.FuncMap{
"empty": func(input interface{}) bool {
check, ok := input.(string)
if ok {
return len(check) == 0
}
return true
},
"buildLocation": buildLocation,
"buildAuthLocation": buildAuthLocation,
"buildProxyPass": buildProxyPass,
"buildRateLimitZones": buildRateLimitZones,
"buildRateLimit": buildRateLimit,
"contains": strings.Contains,
"hasPrefix": strings.HasPrefix,
"hasSuffix": strings.HasSuffix,
"toUpper": strings.ToUpper,
"toLower": strings.ToLower,
}
slash = "/"
defBufferSize = 65535
)
// Template ...
type Template struct {
tmpl *text_template.Template
fw fileWatcher
tmpl *text_template.Template
fw watch.FileWatcher
s int
tmplBuf *bytes.Buffer
outCmdBuf *bytes.Buffer
}
//NewTemplate returns a new Template instance or an
@ -75,66 +52,32 @@ func NewTemplate(file string, onChange func()) (*Template, error) {
if err != nil {
return nil, err
}
fw, err := newFileWatcher(file, onChange)
fw, err := watch.NewFileWatcher(file, onChange)
if err != nil {
return nil, err
}
return &Template{
tmpl: tmpl,
fw: fw,
tmpl: tmpl,
fw: fw,
s: defBufferSize,
tmplBuf: bytes.NewBuffer(make([]byte, 0, defBufferSize)),
outCmdBuf: bytes.NewBuffer(make([]byte, 0, defBufferSize)),
}, nil
}
// Close removes the file watcher
func (t *Template) Close() {
t.fw.close()
t.fw.Close()
}
// Write populates a buffer using a template with NGINX configuration
// and the servers and upstreams created by Ingress rules
func (t *Template) Write(
cfg config.Configuration,
ingressCfg ingress.Configuration,
func (t *Template) Write(conf map[string]interface{},
isValidTemplate func([]byte) error) ([]byte, error) {
var longestName int
var serverNames int
for _, srv := range ingressCfg.Servers {
serverNames += len([]byte(srv.Name))
if longestName < len(srv.Name) {
longestName = len(srv.Name)
}
}
// NGINX cannot resize the has tables used to store server names.
// For this reason we check if the defined size defined is correct
// for the FQDN defined in the ingress rules adjusting the value
// if is required.
// https://trac.nginx.org/nginx/ticket/352
// https://trac.nginx.org/nginx/ticket/631
nameHashBucketSize := nextPowerOf2(longestName)
if nameHashBucketSize > cfg.ServerNameHashBucketSize {
glog.V(3).Infof("adjusting ServerNameHashBucketSize variable from %v to %v",
cfg.ServerNameHashBucketSize, nameHashBucketSize)
cfg.ServerNameHashBucketSize = nameHashBucketSize
}
serverNameHashMaxSize := nextPowerOf2(serverNames)
if serverNameHashMaxSize > cfg.ServerNameHashMaxSize {
glog.V(3).Infof("adjusting ServerNameHashMaxSize variable from %v to %v",
cfg.ServerNameHashMaxSize, serverNameHashMaxSize)
cfg.ServerNameHashMaxSize = serverNameHashMaxSize
}
conf := make(map[string]interface{})
conf["backlogSize"] = sysctlSomaxconn()
conf["upstreams"] = ingressCfg.Upstreams
conf["servers"] = ingressCfg.Servers
conf["tcpUpstreams"] = ingressCfg.TCPUpstreams
conf["udpUpstreams"] = ingressCfg.UDPUpstreams
conf["defResolver"] = cfg.Resolver
conf["sslDHParam"] = cfg.SSLDHParam
conf["customErrors"] = len(cfg.CustomHTTPErrors) > 0
conf["cfg"] = fixKeyNames(structs.Map(cfg))
defer t.tmplBuf.Reset()
defer t.outCmdBuf.Reset()
if glog.V(3) {
b, err := json.Marshal(conf)
@ -144,39 +87,65 @@ func (t *Template) Write(
glog.Infof("NGINX configuration: %v", string(b))
}
buffer := new(bytes.Buffer)
err := t.tmpl.Execute(buffer, conf)
if err != nil {
glog.V(3).Infof("%v", string(buffer.Bytes()))
return nil, err
err := t.tmpl.Execute(t.tmplBuf, conf)
if t.s < t.tmplBuf.Cap() {
glog.V(2).Infof("adjusting template buffer size from %v to %v", t.s, t.tmplBuf.Cap())
t.s = t.tmplBuf.Cap()
t.tmplBuf = bytes.NewBuffer(make([]byte, 0, t.tmplBuf.Cap()))
t.outCmdBuf = bytes.NewBuffer(make([]byte, 0, t.outCmdBuf.Cap()))
}
err = isValidTemplate(buffer.Bytes())
// squeezes multiple adjacent empty lines to be single
// spaced this is to avoid the use of regular expressions
cmd := exec.Command("/ingress-controller/clean-nginx-conf.sh")
cmd.Stdin = t.tmplBuf
cmd.Stdout = t.outCmdBuf
if err := cmd.Run(); err != nil {
glog.Warningf("unexpected error cleaning template: %v", err)
return t.tmplBuf.Bytes(), nil
}
content := t.outCmdBuf.Bytes()
err = isValidTemplate(content)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
return content, nil
}
func fixKeyNames(data map[string]interface{}) map[string]interface{} {
fixed := make(map[string]interface{})
for k, v := range data {
fixed[toCamelCase(k)] = v
var (
funcMap = text_template.FuncMap{
"empty": func(input interface{}) bool {
check, ok := input.(string)
if ok {
return len(check) == 0
}
return true
},
"buildLocation": buildLocation,
"buildAuthLocation": buildAuthLocation,
"buildProxyPass": buildProxyPass,
"buildRateLimitZones": buildRateLimitZones,
"buildRateLimit": buildRateLimit,
"getSSPassthroughUpstream": getSSPassthroughUpstream,
"contains": strings.Contains,
"hasPrefix": strings.HasPrefix,
"hasSuffix": strings.HasSuffix,
"toUpper": strings.ToUpper,
"toLower": strings.ToLower,
}
)
func getSSPassthroughUpstream(input interface{}) string {
s, ok := input.(*ingress.Server)
if !ok {
return ""
}
return fixed
}
func toCamelCase(src string) string {
byteSrc := []byte(src)
chunks := camelRegexp.FindAll(byteSrc, -1)
for idx, val := range chunks {
if idx > 0 {
chunks[idx] = bytes.Title(val)
}
}
return string(bytes.Join(chunks, nil))
return s.Name
}
// buildLocation produces the location string, if the ingress has redirects
@ -201,7 +170,7 @@ func buildAuthLocation(input interface{}) string {
return ""
}
if location.ExternalAuthURL.URL == "" {
if location.ExternalAuth.URL == "" {
return ""
}
@ -329,30 +298,3 @@ func buildRateLimit(input interface{}) []string {
return limits
}
// sysctlSomaxconn returns the value of net.core.somaxconn, i.e.
// maximum number of connections that can be queued for acceptance
// http://nginx.org/en/docs/http/ngx_http_core_module.html#listen
func sysctlSomaxconn() int {
maxConns, err := sysctl.New().GetSysctl("net/core/somaxconn")
if err != nil || maxConns < 512 {
glog.Warningf("system net.core.somaxconn=%v. Using NGINX default (511)", maxConns)
return 511
}
return maxConns
}
// http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
// https://play.golang.org/p/TVSyCcdxUh
func nextPowerOf2(v int) int {
v--
v |= v >> 1
v |= v >> 2
v |= v >> 4
v |= v >> 8
v |= v >> 16
v++
return v
}

View file

@ -20,8 +20,8 @@ import (
"strings"
"testing"
"k8s.io/contrib/ingress/controllers/nginx/nginx/ingress"
"k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite"
"k8s.io/ingress/core/pkg/ingress"
"k8s.io/ingress/core/pkg/ingress/annotations/rewrite"
)
var (

View file

@ -0,0 +1,26 @@
/*
Copyright 2015 The Kubernetes 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 version
var (
// RELEASE returns the release version
RELEASE = "UNKNOWN"
// REPO returns the git repository URL
REPO = "UNKNOWN"
// COMMIT returns the short sha from git
COMMIT = "UNKNOWN"
)

View file

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
FROM gcr.io/google_containers/nginx-slim:0.9
FROM gcr.io/google_containers/nginx-slim:0.10
RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y \
diffutils \
@ -21,13 +21,6 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y \
&& rm -rf /var/lib/apt/lists/* \
&& make-ssl-cert generate-default-snakeoil --force-overwrite
COPY nginx-ingress-controller /
COPY nginx.tmpl /etc/nginx/template/nginx.tmpl
COPY nginx.tmpl /etc/nginx/nginx.tmpl
COPY default.conf /etc/nginx/nginx.conf
COPY lua /etc/nginx/lua/
WORKDIR /
COPY . /
CMD ["/nginx-ingress-controller"]

View file

@ -1,21 +1,21 @@
# lua-resty-http
Lua HTTP client cosocket driver for [OpenResty](http://openresty.org/) / [ngx_lua](https://github.com/chaoslawful/lua-nginx-module).
Lua HTTP client cosocket driver for [OpenResty](http://openresty.org/) / [ngx_lua](https://github.com/openresty/lua-nginx-module).
# Status
Ready for testing. Probably production ready in most cases, though not yet proven in the wild. Please check the issues list and let me know if you have any problems / questions.
Production ready.
# Features
* HTTP 1.0 and 1.1
* Streaming interface to reading bodies using coroutines, for predictable memory usage in Lua land.
* Alternative simple interface for singleshot requests without manual connection step.
* Headers treated case insensitively.
* Chunked transfer encoding.
* Keepalive.
* Pipelining.
* Trailers.
* SSL
* Streaming interface to the response body, for predictable memory usage
* Alternative simple interface for singleshot requests without manual connection step
* Chunked and non-chunked transfer encodings
* Keepalive
* Pipelining
* Trailers
# API
@ -222,6 +222,7 @@ The `params` table accepts the following fields:
When the request is successful, `res` will contain the following fields:
* `status` The status code.
* `reason` The status reason phrase.
* `headers` A table of headers. Multiple headers with the same field name will be presented as a table of values.
* `has_body` A boolean flag indicating if there is a body to be read.
* `body_reader` An iterator function for reading the body in a streaming fashion.
@ -291,7 +292,7 @@ repeat
ngx.log(ngx.ERR, err)
break
end
if chunk then
-- process
end
@ -410,7 +411,7 @@ Originally started life based on https://github.com/bakins/lua-resty-http-simple
This module is licensed under the 2-clause BSD license.
Copyright (c) 2013, James Hurst <james@pintsized.co.uk>
Copyright (c) 2013-2016, James Hurst <james@pintsized.co.uk>
All rights reserved.

View file

@ -67,7 +67,7 @@ end
local _M = {
_VERSION = '0.07',
_VERSION = '0.09',
}
_M._USER_AGENT = "lua-resty-http/" .. _M._VERSION .. " (Lua) ngx_lua/" .. ngx.config.ngx_lua_version
@ -111,6 +111,8 @@ function _M.ssl_handshake(self, ...)
return nil, "not initialized"
end
self.ssl = true
return sock:sslhandshake(...)
end
@ -122,6 +124,13 @@ function _M.connect(self, ...)
end
self.host = select(1, ...)
self.port = select(2, ...)
-- If port is not a number, this is likely a unix domain socket connection.
if type(self.port) ~= "number" then
self.port = nil
end
self.keepalive = true
return sock:connect(...)
@ -179,17 +188,19 @@ end
function _M.parse_uri(self, uri)
local m, err = ngx_re_match(uri, [[^(http[s]*)://([^:/]+)(?::(\d+))?(.*)]],
local m, err = ngx_re_match(uri, [[^(http[s]?)://([^:/]+)(?::(\d+))?(.*)]],
"jo")
if not m then
if err then
return nil, "failed to match the uri: " .. err
return nil, "failed to match the uri: " .. uri .. ", " .. err
end
return nil, "bad uri"
return nil, "bad uri: " .. uri
else
if not m[3] then
if m[3] then
m[3] = tonumber(m[3])
else
if m[1] == "https" then
m[3] = 443
else
@ -250,10 +261,10 @@ end
local function _receive_status(sock)
local line, err = sock:receive("*l")
if not line then
return nil, nil, err
return nil, nil, nil, err
end
return tonumber(str_sub(line, 10, 12)), tonumber(str_sub(line, 6, 8))
return tonumber(str_sub(line, 10, 12)), tonumber(str_sub(line, 6, 8)), str_sub(line, 14)
end
@ -487,16 +498,16 @@ end
local function _handle_continue(sock, body)
local status, version, err = _receive_status(sock)
local status, version, reason, err = _receive_status(sock)
if not status then
return nil, err
return nil, nil, err
end
-- Only send body if we receive a 100 Continue
if status == 100 then
local ok, err = sock:receive("*l") -- Read carriage return
if not ok then
return nil, err
return nil, nil, err
end
_send_body(sock, body)
end
@ -526,7 +537,22 @@ function _M.send_request(self, params)
headers["Content-Length"] = #body
end
if not headers["Host"] then
headers["Host"] = self.host
if (str_sub(self.host, 1, 5) == "unix:") then
return nil, "Unable to generate a useful Host header for a unix domain socket. Please provide one."
end
-- If we have a port (i.e. not connected to a unix domain socket), and this
-- port is non-standard, append it to the Host heaer.
if self.port then
if self.ssl and self.port ~= 443 then
headers["Host"] = self.host .. ":" .. self.port
elseif not self.ssl and self.port ~= 80 then
headers["Host"] = self.host .. ":" .. self.port
else
headers["Host"] = self.host
end
else
headers["Host"] = self.host
end
end
if not headers["User-Agent"] then
headers["User-Agent"] = _M._USER_AGENT
@ -562,7 +588,7 @@ end
function _M.read_response(self, params)
local sock = self.sock
local status, version, err
local status, version, reason, err
-- If we expect: continue, we need to handle this, sending the body if allowed.
-- If we don't get 100 back, then status is the actual status.
@ -577,7 +603,7 @@ function _M.read_response(self, params)
-- Just read the status as normal.
if not status then
status, version, err = _receive_status(sock)
status, version, reason, err = _receive_status(sock)
if not status then
return nil, err
end
@ -589,13 +615,18 @@ function _M.read_response(self, params)
return nil, err
end
-- Determine if we should keepalive or not.
-- keepalive is true by default. Determine if this is correct or not.
local ok, connection = pcall(str_lower, res_headers["Connection"])
if ok then
if (version == 1.1 and connection == "close") or
(version == 1.0 and connection ~= "keep-alive") then
self.keepalive = false
end
else
-- no connection header
if version == 1.0 then
self.keepalive = false
end
end
local body_reader = _no_body_reader
@ -627,6 +658,7 @@ function _M.read_response(self, params)
else
return {
status = status,
reason = reason,
headers = res_headers,
has_body = has_body,
body_reader = body_reader,

View file

@ -0,0 +1,22 @@
package = "lua-resty-http"
version = "0.09-0"
source = {
url = "git://github.com/pintsized/lua-resty-http",
tag = "v0.09"
}
description = {
summary = "Lua HTTP client cosocket driver for OpenResty / ngx_lua.",
homepage = "https://github.com/pintsized/lua-resty-http",
license = "2-clause BSD",
maintainer = "James Hurst <james@pintsized.co.uk>"
}
dependencies = {
"lua >= 5.1"
}
build = {
type = "builtin",
modules = {
["resty.http"] = "lib/resty/http.lua",
["resty.http_headers"] = "lib/resty/http_headers.lua"
}
}

View file

@ -83,7 +83,7 @@ OK
[warn]
=== TEST 3: Status code
=== TEST 3: Status code and reason phrase
--- http_config eval: $::HttpConfig
--- config
location = /a {
@ -97,6 +97,7 @@ OK
}
ngx.status = res.status
ngx.say(res.reason)
ngx.print(res:read_body())
httpc:close()
@ -111,6 +112,7 @@ OK
--- request
GET /a
--- response_body
Not Found
OK
--- error_code: 404
--- no_error_log

View file

@ -180,3 +180,61 @@ connection must be closed
--- no_error_log
[error]
[warn]
=== TEST 5: Generic interface, HTTP 1.0, no connection header. Test we don't try to keepalive, but also that subsequent connections can keepalive.
--- http_config eval: $::HttpConfig
--- config
location = /a {
content_by_lua '
local http = require "resty.http"
local httpc = http.new()
httpc:connect("127.0.0.1", 12345)
local res, err = httpc:request{
version = 1.0,
path = "/b"
}
local body = res:read_body()
ngx.print(body)
ngx.say(res.headers["Connection"])
local r, e = httpc:set_keepalive()
ngx.say(r)
ngx.say(e)
httpc:connect("127.0.0.1", ngx.var.server_port)
ngx.say(httpc:get_reused_times())
httpc:set_keepalive()
httpc:connect("127.0.0.1", ngx.var.server_port)
ngx.say(httpc:get_reused_times())
';
}
location = /b {
content_by_lua '
ngx.say("OK")
';
}
--- request
GET /a
--- tcp_listen: 12345
--- tcp_reply
HTTP/1.0 200 OK
Date: Fri, 08 Aug 2016 08:12:31 GMT
Server: OpenResty
OK
--- response_body
OK
nil
2
connection must be closed
0
1
--- no_error_log
[error]
[warn]

View file

@ -0,0 +1,161 @@
# vim:set ft= ts=4 sw=4 et:
use Test::Nginx::Socket;
use Cwd qw(cwd);
plan tests => repeat_each() * (blocks() * 3);
my $pwd = cwd();
our $HttpConfig = qq{
lua_package_path "$pwd/lib/?.lua;;";
error_log logs/error.log debug;
resolver 8.8.8.8;
};
$ENV{TEST_NGINX_RESOLVER} = '8.8.8.8';
$ENV{TEST_NGINX_PWD} ||= $pwd;
sub read_file {
my $infile = shift;
open my $in, $infile
or die "cannot open $infile for reading: $!";
my $cert = do { local $/; <$in> };
close $in;
$cert;
}
our $TestCertificate = read_file("t/cert/test.crt");
our $TestCertificateKey = read_file("t/cert/test.key");
no_long_string();
#no_diff();
run_tests();
__DATA__
=== TEST 1: Default HTTP port is not added to Host header
--- http_config eval: $::HttpConfig
--- config
location /lua {
content_by_lua '
local http = require "resty.http"
local httpc = http.new()
local res, err = httpc:request_uri("http://www.google.com")
';
}
--- request
GET /lua
--- no_error_log
[error]
--- error_log
Host: www.google.com
=== TEST 2: Default HTTPS port is not added to Host header
--- http_config eval: $::HttpConfig
--- config
location /lua {
content_by_lua '
local http = require "resty.http"
local httpc = http.new()
local res, err = httpc:request_uri("https://www.google.com:443", { ssl_verify = false })
';
}
--- request
GET /lua
--- no_error_log
[error]
--- error_log
Host: www.google.com
=== TEST 3: Non-default HTTP port is added to Host header
--- http_config
lua_package_path "$TEST_NGINX_PWD/lib/?.lua;;";
error_log logs/error.log debug;
resolver 8.8.8.8;
server {
listen *:8080;
}
--- config
location /lua {
content_by_lua '
local http = require "resty.http"
local httpc = http.new()
local res, err = httpc:request_uri("http://127.0.0.1:8080")
';
}
--- request
GET /lua
--- no_error_log
[error]
--- error_log
Host: 127.0.0.1:8080
=== TEST 4: Non-default HTTPS port is added to Host header
--- http_config
lua_package_path "$TEST_NGINX_PWD/lib/?.lua;;";
error_log logs/error.log debug;
resolver 8.8.8.8;
server {
listen *:8080;
listen *:8081 ssl;
ssl_certificate ../html/test.crt;
ssl_certificate_key ../html/test.key;
}
--- config
location /lua {
content_by_lua '
local http = require "resty.http"
local httpc = http.new()
local res, err = httpc:request_uri("https://127.0.0.1:8081", { ssl_verify = false })
';
}
--- user_files eval
">>> test.key
$::TestCertificateKey
>>> test.crt
$::TestCertificate"
--- request
GET /lua
--- no_error_log
[error]
--- error_log
Host: 127.0.0.1:8081
=== TEST 5: No host header on a unix domain socket returns a useful error.
--- http_config eval: $::HttpConfig
--- config
location /a {
content_by_lua_block {
local http = require "resty.http"
local httpc = http.new()
local res, err = httpc:connect("unix:test.sock")
if not res then
ngx.log(ngx.ERR, err)
end
local res, err = httpc:request({ path = "/" })
if not res then
ngx.say(err)
else
ngx.say(res:read_body())
end
}
}
--- tcp_listen: test.sock
--- tcp_reply: OK
--- request
GET /a
--- no_error_log
[error]
--- response_body
Unable to generate a useful Host header for a unix domain socket. Please provide one.

View file

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIID8DCCAtigAwIBAgIJALL9eJPZ6neGMA0GCSqGSIb3DQEBBQUAMFgxCzAJBgNV
BAYTAkdCMQ0wCwYDVQQIEwRUZXN0MQ0wCwYDVQQHEwRUZXN0MQ0wCwYDVQQKEwRU
ZXN0MQ0wCwYDVQQLEwRUZXN0MQ0wCwYDVQQDEwR0ZXN0MB4XDTE1MTAyMTE2MjQ1
NloXDTE1MTEyMDE2MjQ1NlowWDELMAkGA1UEBhMCR0IxDTALBgNVBAgTBFRlc3Qx
DTALBgNVBAcTBFRlc3QxDTALBgNVBAoTBFRlc3QxDTALBgNVBAsTBFRlc3QxDTAL
BgNVBAMTBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDz/AoE
c+TPdm+Aqcchq8fLNWksFQZqbsCBGnq8rUG1b6MsVlAOkDUQGRlNPs9v0/+pzgX7
IYXPCFcV7YONNsTUfvBYTq43mfOycmAdb3SX6kBygxdhYsDRZR+vCAIkjoRmRB20
meh1motqM58spq3IcT8VADTRJl1OI48VTnxmXdCtmkOymU948DcauMoxm03eL/hU
6eniNEujbnbB305noNG0W5c3h6iz9CvqUAD1kwyjick+f1atB2YYn1bymA+db6YN
3iTo0v2raWmIc7D+qqpkNaCRxgMb2HN6X3/SfkijtNJidjqHMbs2ftlKJ5/lODPZ
rCPQOcYK6TT8MIZ1AgMBAAGjgbwwgbkwHQYDVR0OBBYEFFUC1GrAhUp7IvJH5iyf
+fJQliEIMIGJBgNVHSMEgYEwf4AUVQLUasCFSnsi8kfmLJ/58lCWIQihXKRaMFgx
CzAJBgNVBAYTAkdCMQ0wCwYDVQQIEwRUZXN0MQ0wCwYDVQQHEwRUZXN0MQ0wCwYD
VQQKEwRUZXN0MQ0wCwYDVQQLEwRUZXN0MQ0wCwYDVQQDEwR0ZXN0ggkAsv14k9nq
d4YwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAtaUQOr3Qn87KXmmP
GbSvCLSl+bScE09VYZsYaB6iq0pGN9y+Vh4/HjBUUsFexopw1dY25MEEJXEVi1xV
2krLYAsfKCM6c1QBVmdqfVuxUvxpXwr+CNRNAlzz6PhjkeY/Ds/j4sg7EqN8hMmT
gu8GuogX7+ZCgrzRSMMclWej+W8D1xSIuCC+rqv4w9SZdtVb3XGpCyizpTNsQAuV
ACXvq9KXkEEj+XNvKrNdWd4zG715RdMnVm+WM53d9PLp63P+4/kwhwHULYhXygQ3
DzzVPaojBBdw3VaHbbPHnv73FtAzOb7ky6zJ01DlmEPxEahCFpklMkY9T2uCdpj9
oOzaNA==
-----END CERTIFICATE-----

View file

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA8/wKBHPkz3ZvgKnHIavHyzVpLBUGam7AgRp6vK1BtW+jLFZQ
DpA1EBkZTT7Pb9P/qc4F+yGFzwhXFe2DjTbE1H7wWE6uN5nzsnJgHW90l+pAcoMX
YWLA0WUfrwgCJI6EZkQdtJnodZqLajOfLKatyHE/FQA00SZdTiOPFU58Zl3QrZpD
splPePA3GrjKMZtN3i/4VOnp4jRLo252wd9OZ6DRtFuXN4eos/Qr6lAA9ZMMo4nJ
Pn9WrQdmGJ9W8pgPnW+mDd4k6NL9q2lpiHOw/qqqZDWgkcYDG9hzel9/0n5Io7TS
YnY6hzG7Nn7ZSief5Tgz2awj0DnGCuk0/DCGdQIDAQABAoIBAGjKc7L94+SHRdTJ
FtILacCJrCZW0W6dKulIajbnYzV+QWMlnzTiEyha31ciBw5My54u8rqt5z7Ioj60
yK+6OkfaTXhgMsuGv/iAz29VE4q7/fow+7XEKHTHLhiLJAB3hb42u1t6TzFTs1Vl
3pPa8wEIQsPOVuENzT1mYGoST7PW+LBIMr9ScMnRHfC0MNdV/ntQiXideOAd5PkA
4O7fNgYZ8CTAZ8rOLYTMFF76/c/jLiqfeghqbIhqMykk36kd7Lud//FRykVsn1aJ
REUva/SjVEth5kITot1hpMC4SIElWpha2YxiiZFoSXSaUbtHpymiUGV01cYtMWk0
MZ5HN3ECgYEA/74U8DpwPxd4up9syKyNqOqrCrYnhEEC/tdU/W5wECi4y5kppjdd
88lZzICVPzk2fezYXlCO9HiSHU1UfcEsY3u16qNCvylK7Qz1OqXV/Ncj59891Q5Z
K0UBcbnrv+YD6muZuhlHEbyDPqYO091G9Gf/BbL5JIBDzg1qFO9Dh9cCgYEA9Drt
O9PJ5Sjz3mXQVtVHpwyhOVnd7CUv8a1zkUQCK5uQeaiF5kal1FIo7pLOr3KAvG0C
pXbm/TobwlfAfcERQN88aPN8Z/l1CB0oKV6ipBMD2/XLzDRtx8lpTeh/BB8jIhrz
+FDJY54HCzLfW0P5kT+Cyw51ofjziPnFdO/Z6pMCgYEAon17gEchGnUnWCwDSl2Y
hELV+jBSW02TQag/b+bDfQDiqTnfpKR5JXRBghYQveL0JH5f200EB4C0FboUfPJH
6c2ogDTLK/poiMU66tCDbeqj/adx+fTr4votOL0QdRUIV+GWAxAcf8BvA1cvBJ4L
fy60ckKM2gxFCJ6tUC/VkHECgYBoMDNAUItSnXPbrmeAg5/7naGxy6qmsP6RBUPF
9tNOMyEhJUlqAT2BJEOd8zcFFb3hpEd6uwyzfnSVJcZSX2iy2gj1ZNnvqTXJ7lZR
v7N2dz4wOd1lEgC7OCsaN1LoOThNtl3Z0uz2+FVc66jpUEhJNGThpxt7q66JArS/
vAqkzQKBgFkzqA6QpnH5KhOCoZcuLQ4MtvnNHOx1xSm2B0gKDVJzGkHexTmOJvwM
ZhHXRl9txS4icejS+AGUXNBzCWEusfhDaZpZqS6zt6UxEjMsLj/Te7z++2KQn4t/
aI77jClydW1pJvICtqm5v+sukVZvQTTJza9ujta6fj7u2s671np9
-----END RSA PRIVATE KEY-----

View file

@ -1,14 +1,10 @@
{{ $cfg := .cfg }}
{{ $cfg := .cfg }}{{ $healthzURL := .healthzURL }}
daemon off;
worker_processes {{ $cfg.workerProcesses }};
pid /run/nginx.pid;
worker_rlimit_nofile 131072;
pcre_jit on;
events {
multi_accept on;
worker_connections {{ $cfg.maxWorkerConnections }};
@ -17,13 +13,13 @@ events {
http {
{{/* we use the value of the header X-Forwarded-For to be able to use the geo_ip module */}}
{{ if $cfg.useProxyProtocol -}}
{{ if $cfg.useProxyProtocol }}
set_real_ip_from {{ $cfg.proxyRealIpCidr }};
real_ip_header proxy_protocol;
{{ else }}
real_ip_header X-Forwarded-For;
set_real_ip_from 0.0.0.0/0;
{{ end -}}
{{ end }}
real_ip_recursive on;
@ -34,10 +30,10 @@ http {
geoip_city /etc/nginx/GeoLiteCity.dat;
geoip_proxy_recursive on;
{{- if $cfg.enableVtsStatus }}
{{ if $cfg.enableVtsStatus }}
vhost_traffic_status_zone shared:vhost_traffic_status:{{ $cfg.vtsStatusZoneSize }};
vhost_traffic_status_filter_by_set_key $geoip_country_code country::*;
{{ end -}}
{{ end }}
# lua section to return proper error codes when custom pages are used
lua_package_path '.?.lua;./etc/nginx/lua/?.lua;/etc/nginx/lua/vendor/lua-resty-http/lib/?.lua;';
@ -56,32 +52,32 @@ http {
keepalive_timeout {{ $cfg.keepAlive }}s;
types_hash_max_size 2048;
server_names_hash_max_size {{ $cfg.serverNameHashMaxSize }};
server_names_hash_bucket_size {{ $cfg.serverNameHashBucketSize }};
map_hash_bucket_size {{ $cfg.mapHashBucketSize }};
types_hash_max_size 2048;
server_names_hash_max_size {{ $cfg.serverNameHashMaxSize }};
server_names_hash_bucket_size {{ $cfg.serverNameHashBucketSize }};
map_hash_bucket_size {{ $cfg.mapHashBucketSize }};
include /etc/nginx/mime.types;
default_type text/html;
{{ if $cfg.useGzip -}}
{{ if $cfg.useGzip }}
gzip on;
gzip_comp_level 5;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types {{ $cfg.gzipTypes }};
gzip_proxied any;
{{- end }}
{{ end }}
client_max_body_size "{{ $cfg.bodySize }}";
log_format upstreaminfo '{{ if $cfg.useProxyProtocol }}$proxy_protocol_addr{{ else }}$remote_addr{{ end }} $host '
'[$proxy_add_x_forwarded_for] $server_port $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" '
log_format upstreaminfo '{{ if $cfg.useProxyProtocol }}$proxy_protocol_addr{{ else }}$remote_addr{{ end }} - '
'[$proxy_add_x_forwarded_for] - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" '
'$request_length $request_time [$proxy_upstream_name] $upstream_addr $upstream_response_length $upstream_response_time $upstream_status';
{{/* map urls that should not appear in access.log */}}
{{/* http://nginx.org/en/docs/http/ngx_http_log_module.html#access_log */}}
map $request $loggable {
{{- range $reqUri := $cfg.skipAccessLogUrls }}
{{ range $reqUri := $cfg.skipAccessLogUrls }}
{{ $reqUri }} 0;{{ end }}
default 1;
}
@ -155,16 +151,16 @@ http {
ssl_dhparam {{ .sslDHParam }};
{{ end }}
{{- if not $cfg.enableDynamicTlsRecords }}
{{ if not $cfg.enableDynamicTlsRecords }}
ssl_dyn_rec_size_lo 0;
{{ end }}
{{- if .customErrors }}
{{ if .customErrors }}
# Custom error pages
proxy_intercept_errors on;
{{ end }}
{{- range $errCode := $cfg.customHttpErrors }}
{{ range $errCode := $cfg.customHttpErrors }}
error_page {{ $errCode }} = @custom_{{ $errCode }};{{ end }}
# In case of errors try the next upstream server before returning an error
@ -172,11 +168,11 @@ http {
{{range $name, $upstream := .upstreams}}
upstream {{$upstream.Name}} {
{{ if $cfg.enableStickySessions -}}
{{ if $cfg.enableStickySessions }}
sticky hash=sha1 httponly;
{{ else -}}
{{ else }}
least_conn;
{{- end }}
{{ end }}
{{ range $server := $upstream.Backends }}server {{ $server.Address }}:{{ $server.Port }} max_fails={{ $server.MaxFails }} fail_timeout={{ $server.FailTimeout }};
{{ end }}
}
@ -184,7 +180,7 @@ http {
{{/* build all the required rate limit zones. Each annotation requires a dedicated zone */}}
{{/* 1MB -> 16 thousand 64-byte states or about 8 thousand 128-byte states */}}
{{- range $zone := (buildRateLimitZones .servers) }}
{{ range $zone := (buildRateLimitZones .servers) }}
{{ $zone }}
{{ end }}
@ -192,75 +188,85 @@ http {
server {
server_name {{ $server.Name }};
listen 80{{ if $cfg.useProxyProtocol }} proxy_protocol{{ end }};
{{ if $server.SSL }}listen 443 {{ if $cfg.useProxyProtocol }}proxy_protocol{{ end }} ssl {{ if $cfg.enableSpdy }}spdy{{ end }} {{ if $cfg.useHttp2 }}http2{{ end }};
{{ if $server.SSL }}listen 442 {{ if $cfg.useProxyProtocol }}proxy_protocol{{ end }} ssl {{ if $cfg.useHttp2 }}http2{{ end }};
{{/* comment PEM sha is required to detect changes in the generated configuration and force a reload */}}
# PEM sha: {{ $server.SSLPemChecksum }}
ssl_certificate {{ $server.SSLCertificate }};
ssl_certificate_key {{ $server.SSLCertificateKey }};
{{- end }}
ssl_certificate {{ $server.SSLCertificate }};
ssl_certificate_key {{ $server.SSLCertificate }};
{{ end }}
{{ if (and $server.SSL $cfg.hsts) }}
more_set_headers "Strict-Transport-Security: max-age={{ $cfg.hstsMaxAge }}{{ if $cfg.hstsIncludeSubdomains }}; includeSubDomains{{ end }}; preload";
{{ end }}
{{- if (and $server.SSL $cfg.hsts) }}
more_set_headers "Strict-Transport-Security: max-age={{ $cfg.hstsMaxAge }}{{ if $cfg.hstsIncludeSubdomains }}; includeSubDomains{{ end }}; preload";
{{- end }}
{{ if $cfg.enableVtsStatus }}vhost_traffic_status_filter_by_set_key $geoip_country_code country::$server_name;{{ end }}
{{ if $cfg.enableVtsStatus }}vhost_traffic_status_filter_by_set_key $geoip_country_code country::$server_name;{{ end -}}
{{- range $location := $server.Locations }}
{{ range $location := $server.Locations }}
{{ $path := buildLocation $location }}
{{ $authPath := buildAuthLocation $location }}
{{- if not (empty $authPath) }}
{{ if not (empty $location.CertificateAuth.CertFileName) }}
# PEM sha: {{ $location.CertificateAuth.PemSHA }}
ssl_client_certificate {{ $location.CertificateAuth.CAFileName }};
ssl_verify_client on;
{{ end }}
{{ if not (empty $authPath) }}
location = {{ $authPath }} {
internal;
{{ if not $location.ExternalAuthURL.SendBody }}
{{ if not $location.ExternalAuth.SendBody }}
proxy_pass_request_body off;
proxy_set_header Content-Length "";
{{ end -}}
{{ if not (empty $location.ExternalAuthURL.Method) }}
proxy_method {{ $location.ExternalAuthURL.Method }};
{{ end -}}
{{ end }}
{{ if not (empty $location.ExternalAuth.Method) }}
proxy_method {{ $location.ExternalAuth.Method }};
{{ end }}
proxy_set_header Host $host;
proxy_pass_request_headers on;
proxy_pass {{ $location.ExternalAuthURL.URL }};
set $target {{ $location.ExternalAuth.URL }};
proxy_pass $target;
}
{{ end }}
location {{ $path }} {
{{- if gt (len $location.Whitelist.CIDR) 0 }}
{{- range $ip := $location.Whitelist.CIDR }}
{{ if gt (len $location.Whitelist.CIDR) 0 }}
{{ range $ip := $location.Whitelist.CIDR }}
allow {{ $ip }};{{ end }}
deny all;
{{ end -}}
{{- if not (empty $authPath) }}
{{ end }}
{{ if not (empty $authPath) }}
# this location requires authentication
auth_request {{ $authPath }};
{{ end -}}
{{- if (and $server.SSL $location.Redirect.SSLRedirect) }}
{{ end }}
{{ if (and $server.SSL $location.Redirect.SSLRedirect) }}
# enforce ssl on server side
if ($scheme = http) {
return 301 https://$host$request_uri;
}
{{ end -}}
{{ end }}
{{/* if the location contains a rate limit annotation, create one */}}
{{ $limits := buildRateLimit $location }}
{{- range $limit := $limits }}
{{ range $limit := $limits }}
{{ $limit }}{{ end }}
{{- if $location.Auth.Secured }}
{{- if eq $location.Auth.Type "basic" }}
auth_basic "{{ $location.Auth.Realm }}";
auth_basic_user_file {{ $location.Auth.File }};
{{ if $location.BasicDigestAuth.Secured }}
{{ if eq $location.BasicDigestAuth.Type "basic" }}
auth_basic "{{ $location.BasicDigestAuth.Realm }}";
auth_basic_user_file {{ $location.BasicDigestAuth.File }};
{{ else }}
#TODO: add nginx-http-auth-digest module
auth_digest "{{ $location.Auth.Realm }}";
auth_digest_user_file {{ $location.Auth.File }};
{{ end -}}
auth_digest "{{ $location.BasicDigestAuth.Realm }}";
auth_digest_user_file {{ $location.BasicDigestAuth.File }};
{{ end }}
proxy_set_header Authorization "";
{{ end -}}
{{- if $location.EnableCORS }}
{{ end }}
{{ if $location.EnableCORS }}
{{ template "CORS" }}
{{ end -}}
{{ end }}
proxy_set_header Host $host;
# Pass Real IP
@ -279,29 +285,29 @@ http {
# https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/
proxy_set_header Proxy "";
proxy_connect_timeout {{ $cfg.proxyConnectTimeout }}s;
proxy_send_timeout {{ $cfg.proxySendTimeout }}s;
proxy_read_timeout {{ $cfg.proxyReadTimeout }}s;
proxy_connect_timeout {{ $location.Proxy.ConnectTimeout }}s;
proxy_send_timeout {{ $location.Proxy.SendTimeout }}s;
proxy_read_timeout {{ $location.Proxy.ReadTimeout }}s;
proxy_redirect off;
proxy_buffering off;
proxy_buffer_size {{ $cfg.proxyBufferSize }};
proxy_buffer_size "{{ $location.Proxy.BufferSize }}";
proxy_http_version 1.1;
{{/* rewrite only works if the content is not compressed */}}
{{- if $location.Redirect.AddBaseURL }}
{{ if $location.Redirect.AddBaseURL }}
proxy_set_header Accept-Encoding "";
{{ end -}}
{{ end }}
set $proxy_upstream_name "{{ $location.Upstream.Name }}";
{{ buildProxyPass $location }}
}
{{ end }}
{{- if eq $server.Name "_" }}
{{ if eq $server.Name "_" }}
# health checks in cloud providers require the use of port 80
location {{ $cfg.HealthzURL }} {
location {{ $healthzURL }} {
access_log off;
return 200;
}
@ -315,11 +321,12 @@ http {
access_log off;
stub_status on;
}
{{ end -}}
{{ end }}
{{ template "CUSTOM_ERRORS" $cfg }}
}
{{ end }}
# default server, used for NGINX healthcheck and access to nginx stats
server {
# Use the port 18080 (random value just to avoid known ports) as default port for nginx.
@ -327,26 +334,26 @@ http {
# https://github.com/kubernetes/contrib/blob/master/ingress/controllers/nginx/nginx/command.go#L104
listen 18080 default_server reuseport backlog={{ .backlogSize }};
location /healthz {
location {{ $healthzURL }} {
access_log off;
return 200;
}
location /nginx_status {
{{ if $cfg.enableVtsStatus -}}
{{ if $cfg.enableVtsStatus }}
vhost_traffic_status_display;
vhost_traffic_status_display_format html;
{{ else }}
access_log off;
stub_status on;
{{- end }}
{{ end }}
}
location / {
set $proxy_upstream_name "upstream-default-backend";
proxy_pass http://upstream-default-backend;
}
{{- template "CUSTOM_ERRORS" $cfg }}
{{ template "CUSTOM_ERRORS" $cfg }}
}
# default server for services without endpoints
@ -367,17 +374,53 @@ http {
}
stream {
# map FQDN that requires SSL passthrough
map $ssl_preread_server_name $stream_upstream {
{{ range $i, $passthrough := .passthroughUpstreams }}
{{ $passthrough.Host }} {{ $passthrough.Upstream.Name }}-{{ $i }}-pt;
{{ end }}
# send SSL traffic to this nginx in a different port
default nginx-ssl-backend;
}
log_format log_stream '$remote_addr [$time_local] $protocol [$ssl_preread_server_name] [$stream_upstream] '
'$status $bytes_sent $bytes_received $session_time';
access_log /var/log/nginx/access.log log_stream;
error_log /var/log/nginx/error.log;
# configure default backend for SSL
upstream nginx-ssl-backend {
server 127.0.0.1:442;
}
{{ range $i, $passthrough := .passthroughUpstreams }}
upstream {{ $passthrough.Name }}-{{ $i }}-pt {
{{ range $server := $passthrough.Backends }}server {{ $server.Address }}:{{ $server.Port }};
{{ end }}
}
{{ end }}
server {
listen 443;
{{ if $cfg.useProxyProtocol }}proxy_protocol on;{{ end }}
proxy_pass $stream_upstream;
ssl_preread on;
}
# TCP services
{{ range $i, $tcpServer := .tcpUpstreams }}
upstream tcp-{{ $tcpServer.Upstream.Name }} {
{{ range $server := $tcpServer.Upstream.Backends }}server {{ $server.Address }}:{{ $server.Port }};
{{ end }}
}
server {
listen {{ $tcpServer.Path }};
proxy_connect_timeout {{ $cfg.proxyConnectTimeout }};
proxy_timeout {{ $cfg.proxyReadTimeout }};
proxy_connect_timeout {{ $tcpServer.Proxy.ConnectTimeout }}s;
proxy_timeout {{ $tcpServer.Proxy.ReadTimeout }}s;
proxy_pass tcp-{{ $tcpServer.Upstream.Name }};
}
{{ end }}
@ -388,7 +431,7 @@ stream {
{{ range $server := $udpServer.Upstream.Backends }}server {{ $server.Address }}:{{ $server.Port }};
{{ end }}
}
server {
listen {{ $udpServer.Path }} udp;
proxy_timeout 10s;

View file

@ -0,0 +1,8 @@
#!/bin/bash
# This script removes consecutive empty lines in nginx.conf
# Using sed is more simple than using a go regex
# first sed removes empty lines
# second sed command replaces the empty lines
sed -e 's/^ *$/\'$'\n/g' | sed -e '/^$/{N;/^\n$/d;}'

View file

@ -1,301 +0,0 @@
/*
Copyright 2015 The Kubernetes 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 main
import (
"fmt"
"io/ioutil"
"os"
"strings"
"time"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/api"
apierrs "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/client/cache"
"k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/util/wait"
"k8s.io/kubernetes/pkg/util/workqueue"
)
// StoreToIngressLister makes a Store that lists Ingress.
type StoreToIngressLister struct {
cache.Store
}
// StoreToSecretsLister makes a Store that lists Secrets.
type StoreToSecretsLister struct {
cache.Store
}
// StoreToConfigmapLister makes a Store that lists Configmap.
type StoreToConfigmapLister struct {
cache.Store
}
// taskQueue manages a work queue through an independent worker that
// invokes the given sync function for every work item inserted.
type taskQueue struct {
// queue is the work queue the worker polls
queue workqueue.RateLimitingInterface
// sync is called for each item in the queue
sync func(string) error
// workerDone is closed when the worker exits
workerDone chan struct{}
}
func (t *taskQueue) run(period time.Duration, stopCh <-chan struct{}) {
wait.Until(t.worker, period, stopCh)
}
// enqueue enqueues ns/name of the given api object in the task queue.
func (t *taskQueue) enqueue(obj interface{}) {
key, err := keyFunc(obj)
if err != nil {
glog.Infof("could not get key for object %+v: %v", obj, err)
return
}
t.queue.Add(key)
}
func (t *taskQueue) requeue(key string) {
t.queue.AddRateLimited(key)
}
// worker processes work in the queue through sync.
func (t *taskQueue) worker() {
for {
key, quit := t.queue.Get()
if quit {
close(t.workerDone)
return
}
glog.V(3).Infof("syncing %v", key)
if err := t.sync(key.(string)); err != nil {
glog.Warningf("requeuing %v, err %v", key, err)
t.requeue(key.(string))
} else {
t.queue.Forget(key)
}
t.queue.Done(key)
}
}
// shutdown shuts down the work queue and waits for the worker to ACK
func (t *taskQueue) shutdown() {
t.queue.ShutDown()
<-t.workerDone
}
// NewTaskQueue creates a new task queue with the given sync function.
// The sync function is called for every element inserted into the queue.
func NewTaskQueue(syncFn func(string) error) *taskQueue {
return &taskQueue{
queue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
sync: syncFn,
workerDone: make(chan struct{}),
}
}
// getPodDetails returns runtime information about the pod: name, namespace and IP of the node
func getPodDetails(kubeClient *unversioned.Client) (*podInfo, error) {
podName := os.Getenv("POD_NAME")
podNs := os.Getenv("POD_NAMESPACE")
if podName == "" && podNs == "" {
return nil, fmt.Errorf("unable to get POD information (missing POD_NAME or POD_NAMESPACE environment variable")
}
err := waitForPodRunning(kubeClient, podNs, podName, time.Millisecond*200, time.Second*30)
if err != nil {
return nil, err
}
pod, _ := kubeClient.Pods(podNs).Get(podName)
if pod == nil {
return nil, fmt.Errorf("unable to get POD information")
}
node, err := kubeClient.Nodes().Get(pod.Spec.NodeName)
if err != nil {
return nil, err
}
var externalIP string
for _, address := range node.Status.Addresses {
if address.Type == api.NodeExternalIP {
if address.Address != "" {
externalIP = address.Address
break
}
}
if externalIP == "" && address.Type == api.NodeLegacyHostIP {
externalIP = address.Address
}
}
return &podInfo{
PodName: podName,
PodNamespace: podNs,
NodeIP: externalIP,
}, nil
}
func isValidService(kubeClient *unversioned.Client, name string) error {
if name == "" {
return fmt.Errorf("empty string is not a valid service name")
}
parts := strings.Split(name, "/")
if len(parts) != 2 {
return fmt.Errorf("invalid name format (namespace/name) in service '%v'", name)
}
_, err := kubeClient.Services(parts[0]).Get(parts[1])
return err
}
func isHostValid(host string, cns []string) bool {
for _, cn := range cns {
if matchHostnames(cn, host) {
return true
}
}
return false
}
func matchHostnames(pattern, host string) bool {
host = strings.TrimSuffix(host, ".")
pattern = strings.TrimSuffix(pattern, ".")
if len(pattern) == 0 || len(host) == 0 {
return false
}
patternParts := strings.Split(pattern, ".")
hostParts := strings.Split(host, ".")
if len(patternParts) != len(hostParts) {
return false
}
for i, patternPart := range patternParts {
if i == 0 && patternPart == "*" {
continue
}
if patternPart != hostParts[i] {
return false
}
}
return true
}
func parseNsName(input string) (string, string, error) {
nsName := strings.Split(input, "/")
if len(nsName) != 2 {
return "", "", fmt.Errorf("invalid format (namespace/name) found in '%v'", input)
}
return nsName[0], nsName[1], nil
}
func waitForPodRunning(kubeClient *unversioned.Client, ns, podName string, interval, timeout time.Duration) error {
condition := func(pod *api.Pod) (bool, error) {
if pod.Status.Phase == api.PodRunning {
return true, nil
}
return false, nil
}
return waitForPodCondition(kubeClient, ns, podName, condition, interval, timeout)
}
// waitForPodCondition waits for a pod in state defined by a condition (func)
func waitForPodCondition(kubeClient *unversioned.Client, ns, podName string, condition func(pod *api.Pod) (bool, error),
interval, timeout time.Duration) error {
return wait.PollImmediate(interval, timeout, func() (bool, error) {
pod, err := kubeClient.Pods(ns).Get(podName)
if err != nil {
if apierrs.IsNotFound(err) {
return false, nil
}
}
done, err := condition(pod)
if err != nil {
return false, err
}
if done {
return true, nil
}
return false, nil
})
}
// ingAnnotations represents Ingress annotations.
type ingAnnotations map[string]string
const (
// ingressClassKey picks a specific "class" for the Ingress. The controller
// only processes Ingresses with this annotation either unset, or set
// to either nginxIngressClass or the empty string.
ingressClassKey = "kubernetes.io/ingress.class"
nginxIngressClass = "nginx"
)
func (ing ingAnnotations) ingressClass() string {
val, ok := ing[ingressClassKey]
if !ok {
return ""
}
return val
}
// isNGINXIngress returns true if the given Ingress either doesn't specify the
// ingress.class annotation, or it's set to "nginx".
func isNGINXIngress(ing *extensions.Ingress) bool {
class := ingAnnotations(ing.ObjectMeta.Annotations).ingressClass()
return class == "" || class == nginxIngressClass
}
const (
snakeOilPem = "/etc/ssl/certs/ssl-cert-snakeoil.pem"
snakeOilKey = "/etc/ssl/private/ssl-cert-snakeoil.key"
)
// getFakeSSLCert returns the snake oil ssl certificate created by the command
// make-ssl-cert generate-default-snakeoil --force-overwrite
func getFakeSSLCert() (string, string) {
cert, err := ioutil.ReadFile(snakeOilPem)
if err != nil {
return "", ""
}
key, err := ioutil.ReadFile(snakeOilKey)
if err != nil {
return "", ""
}
return string(cert), string(key)
}

34
core/pkg/cache/main.go vendored Normal file
View file

@ -0,0 +1,34 @@
/*
Copyright 2015 The Kubernetes 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 cache
import "k8s.io/kubernetes/pkg/client/cache"
// StoreToIngressLister makes a Store that lists Ingress.
type StoreToIngressLister struct {
cache.Store
}
// StoreToSecretsLister makes a Store that lists Secrets.
type StoreToSecretsLister struct {
cache.Store
}
// StoreToConfigmapLister makes a Store that lists Configmap.
type StoreToConfigmapLister struct {
cache.Store
}

View file

@ -23,11 +23,10 @@ import (
"os"
"regexp"
"github.com/golang/glog"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
client "k8s.io/kubernetes/pkg/client/unversioned"
)
const (
@ -35,11 +34,9 @@ const (
authSecret = "ingress.kubernetes.io/auth-secret"
authRealm = "ingress.kubernetes.io/auth-realm"
defAuthRealm = "Authentication Required"
// DefAuthDirectory default directory used to store files
// to authenticate request in NGINX
DefAuthDirectory = "/etc/nginx/auth"
// to authenticate request
DefAuthDirectory = "/etc/ingress-controller/auth"
)
func init() {
@ -53,95 +50,58 @@ var (
// ErrInvalidAuthType is return in case of unsupported authentication type
ErrInvalidAuthType = errors.New("invalid authentication type")
// ErrMissingAuthType is return when the annotation for authentication is missing
ErrMissingAuthType = errors.New("authentication type is missing")
// ErrMissingSecretName is returned when the name of the secret is missing
ErrMissingSecretName = errors.New("secret name is missing")
// ErrMissingAuthInSecret is returned when there is no auth key in secret data
ErrMissingAuthInSecret = errors.New("the secret does not contains the auth key")
// ErrMissingAnnotations is returned when the ingress rule
// does not contains annotations related with authentication
ErrMissingAnnotations = errors.New("missing authentication annotations")
)
// Nginx returns authentication configuration for an Ingress rule
type Nginx struct {
// BasicDigest returns authentication configuration for an Ingress rule
type BasicDigest struct {
Type string
Realm string
File string
Secured bool
}
type ingAnnotations map[string]string
func (a ingAnnotations) authType() (string, error) {
val, ok := a[authType]
if !ok {
return "", ErrMissingAuthType
}
if !authTypeRegex.MatchString(val) {
glog.Warningf("%v is not a valid authentication type", val)
return "", ErrInvalidAuthType
}
return val, nil
}
func (a ingAnnotations) realm() string {
val, ok := a[authRealm]
if !ok {
return defAuthRealm
}
return val
}
func (a ingAnnotations) secretName() (string, error) {
val, ok := a[authSecret]
if !ok {
return "", ErrMissingSecretName
}
return val, nil
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to add authentication in the paths defined in the rule
// and generated an htpasswd compatible file to be used as source
// during the authentication process
func ParseAnnotations(kubeClient client.Interface, ing *extensions.Ingress, authDir string) (*Nginx, error) {
func ParseAnnotations(ing *extensions.Ingress, authDir string, fn func(string) (*api.Secret, error)) (*BasicDigest, error) {
if ing.GetAnnotations() == nil {
return &Nginx{}, ErrMissingAnnotations
return &BasicDigest{}, parser.ErrMissingAnnotations
}
at, err := ingAnnotations(ing.GetAnnotations()).authType()
at, err := parser.GetStringAnnotation(authType, ing)
if err != nil {
return &Nginx{}, err
return &BasicDigest{}, err
}
s, err := ingAnnotations(ing.GetAnnotations()).secretName()
if !authTypeRegex.MatchString(at) {
return &BasicDigest{}, ErrInvalidAuthType
}
s, err := parser.GetStringAnnotation(authSecret, ing)
if err != nil {
return &Nginx{}, err
return &BasicDigest{}, err
}
secret, err := kubeClient.Secrets(ing.Namespace).Get(s)
secret, err := fn(fmt.Sprintf("%v/%v", ing.Namespace, s))
if err != nil {
return &Nginx{}, err
return &BasicDigest{}, err
}
realm := ingAnnotations(ing.GetAnnotations()).realm()
realm, _ := parser.GetStringAnnotation(authRealm, ing)
passFile := fmt.Sprintf("%v/%v-%v.passwd", authDir, ing.GetNamespace(), ing.GetName())
err = dumpSecret(passFile, secret)
if err != nil {
return &Nginx{}, err
return &BasicDigest{}, err
}
return &Nginx{
return &BasicDigest{
Type: at,
Realm: realm,
File: passFile,

View file

@ -25,8 +25,6 @@ import (
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/client/unversioned/testclient"
"k8s.io/kubernetes/pkg/util/intstr"
)
@ -65,79 +63,21 @@ func buildIngress() *extensions.Ingress {
}
}
type secretsClient struct {
unversioned.Interface
}
// dummySecret generates a secret with one user inside the auth key
// foo:md5(bar)
func dummySecret() *api.Secret {
func mockSecret(name string) (*api.Secret, error) {
return &api.Secret{
ObjectMeta: api.ObjectMeta{
Namespace: api.NamespaceDefault,
Name: "demo-secret",
},
Data: map[string][]byte{"auth": []byte("foo:$apr1$OFG3Xybp$ckL0FHDAkoXYIlH9.cysT0")},
}
}, nil
}
func mockClient() *testclient.Fake {
return testclient.NewSimpleFake(dummySecret())
}
func TestAnnotations(t *testing.T) {
ing := buildIngress()
_, err := ingAnnotations(ing.GetAnnotations()).authType()
if err == nil {
t.Error("Expected a validation error")
}
realm := ingAnnotations(ing.GetAnnotations()).realm()
if realm != defAuthRealm {
t.Error("Expected default realm")
}
_, err = ingAnnotations(ing.GetAnnotations()).secretName()
if err == nil {
t.Error("Expected a validation error")
}
data := map[string]string{}
data[authType] = "demo"
data[authSecret] = "demo-secret"
data[authRealm] = "demo"
ing.SetAnnotations(data)
_, err = ingAnnotations(ing.GetAnnotations()).authType()
if err == nil {
t.Error("Expected a validation error")
}
realm = ingAnnotations(ing.GetAnnotations()).realm()
if realm != "demo" {
t.Errorf("Expected demo as realm but returned %s", realm)
}
secret, err := ingAnnotations(ing.GetAnnotations()).secretName()
if err != nil {
t.Errorf("Unexpec error %v", err)
}
if secret != "demo-secret" {
t.Errorf("Expected demo-secret as realm but returned %s", secret)
}
}
func TestIngressWithoutAuth(t *testing.T) {
ing := buildIngress()
client := mockClient()
_, err := ParseAnnotations(client, ing, "")
_, err := ParseAnnotations(ing, "", mockSecret)
if err == nil {
t.Error("Expected error with ingress without annotations")
}
if err == ErrMissingAuthType {
t.Errorf("Expected MissingAuthType error but returned %v", err)
}
}
func TestIngressAuth(t *testing.T) {
@ -152,20 +92,19 @@ func TestIngressAuth(t *testing.T) {
_, dir, _ := dummySecretContent(t)
defer os.RemoveAll(dir)
client := mockClient()
nginxAuth, err := ParseAnnotations(client, ing, dir)
auth, err := ParseAnnotations(ing, dir, mockSecret)
if err != nil {
t.Errorf("Uxpected error with ingress: %v", err)
}
if nginxAuth.Type != "basic" {
t.Errorf("Expected basic as auth type but returned %s", nginxAuth.Type)
if auth.Type != "basic" {
t.Errorf("Expected basic as auth type but returned %s", auth.Type)
}
if nginxAuth.Realm != "-realm-" {
t.Errorf("Expected -realm- as realm but returned %s", nginxAuth.Realm)
if auth.Realm != "-realm-" {
t.Errorf("Expected -realm- as realm but returned %s", auth.Realm)
}
if !nginxAuth.Secured {
t.Errorf("Expected true as secured but returned %v", nginxAuth.Secured)
if !auth.Secured {
t.Errorf("Expected true as secured but returned %v", auth.Secured)
}
}
@ -180,9 +119,7 @@ func dummySecretContent(t *testing.T) (string, string, *api.Secret) {
t.Error(err)
}
defer tmpfile.Close()
s := dummySecret()
s, _ := mockSecret("demo")
return tmpfile.Name(), dir, s
}

View file

@ -17,12 +17,11 @@ limitations under the License.
package authreq
import (
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/kubernetes/pkg/apis/extensions"
)
@ -33,49 +32,13 @@ const (
authBody = "ingress.kubernetes.io/auth-send-body"
)
var (
// ErrMissingAnnotations is returned when the ingress rule
// does not contain annotations related with authentication
ErrMissingAnnotations = errors.New("missing authentication annotations")
)
// Auth returns external authentication configuration for an Ingress rule
type Auth struct {
// External returns external authentication configuration for an Ingress rule
type External struct {
URL string
Method string
SendBody bool
}
type ingAnnotations map[string]string
func (a ingAnnotations) url() (string, error) {
val, ok := a[authURL]
if !ok {
return "", ErrMissingAnnotations
}
return val, nil
}
func (a ingAnnotations) method() string {
val, ok := a[authMethod]
if !ok {
return ""
}
return val
}
func (a ingAnnotations) sendBody() bool {
val, ok := a[authBody]
if ok {
if b, err := strconv.ParseBool(val); err == nil {
return b
}
}
return false
}
var (
methods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE"}
)
@ -95,42 +58,42 @@ func validMethod(method string) bool {
// ParseAnnotations parses the annotations contained in the ingress
// rule used to use an external URL as source for authentication
func ParseAnnotations(ing *extensions.Ingress) (Auth, error) {
func ParseAnnotations(ing *extensions.Ingress) (External, error) {
if ing.GetAnnotations() == nil {
return Auth{}, ErrMissingAnnotations
return External{}, parser.ErrMissingAnnotations
}
str, err := ingAnnotations(ing.GetAnnotations()).url()
str, err := parser.GetStringAnnotation(authURL, ing)
if err != nil {
return Auth{}, err
return External{}, err
}
if str == "" {
return Auth{}, fmt.Errorf("an empty string is not a valid URL")
return External{}, fmt.Errorf("an empty string is not a valid URL")
}
ur, err := url.Parse(str)
if err != nil {
return Auth{}, err
return External{}, err
}
if ur.Scheme == "" {
return Auth{}, fmt.Errorf("url scheme is empty")
return External{}, fmt.Errorf("url scheme is empty")
}
if ur.Host == "" {
return Auth{}, fmt.Errorf("url host is empty")
return External{}, fmt.Errorf("url host is empty")
}
if strings.Index(ur.Host, "..") != -1 {
return Auth{}, fmt.Errorf("invalid url host")
if strings.Contains(ur.Host, "..") {
return External{}, fmt.Errorf("invalid url host")
}
m := ingAnnotations(ing.GetAnnotations()).method()
m, _ := parser.GetStringAnnotation(authMethod, ing)
if len(m) != 0 && !validMethod(m) {
return Auth{}, fmt.Errorf("invalid HTTP method")
return External{}, fmt.Errorf("invalid HTTP method")
}
sb := ingAnnotations(ing.GetAnnotations()).sendBody()
sb, _ := parser.GetBoolAnnotation(authBody, ing)
return Auth{
return External{
URL: str,
Method: m,
SendBody: sb,

View file

@ -63,11 +63,6 @@ func buildIngress() *extensions.Ingress {
func TestAnnotations(t *testing.T) {
ing := buildIngress()
_, err := ingAnnotations(ing.GetAnnotations()).url()
if err == nil {
t.Error("Expected a validation error")
}
data := map[string]string{}
ing.SetAnnotations(data)

View file

@ -0,0 +1,65 @@
/*
Copyright 2015 The Kubernetes 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 authtls
import (
"fmt"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/ingress/core/pkg/k8s"
"k8s.io/kubernetes/pkg/apis/extensions"
)
const (
// name of the secret
authTLSSecret = "ingress.kubernetes.io/auth-tls-secret"
)
// SSLCert returns external authentication configuration for an Ingress rule
type SSLCert struct {
Secret string
CertFileName string
KeyFileName string
CAFileName string
PemSHA string
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to use an external URL as source for authentication
func ParseAnnotations(ing *extensions.Ingress,
fn func(secret string) (*SSLCert, error)) (*SSLCert, error) {
if ing.GetAnnotations() == nil {
return &SSLCert{}, parser.ErrMissingAnnotations
}
str, err := parser.GetStringAnnotation(authTLSSecret, ing)
if err != nil {
return &SSLCert{}, err
}
if str == "" {
return &SSLCert{}, fmt.Errorf("an empty string is not a valid secret name")
}
_, _, err = k8s.ParseNameNS(str)
if err != nil {
return &SSLCert{}, err
}
return fn(str)
}

View file

@ -0,0 +1,107 @@
/*
Copyright 2015 The Kubernetes 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 authtls
import (
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/util/intstr"
)
func buildIngress() *extensions.Ingress {
defaultBackend := extensions.IngressBackend{
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
}
return &extensions.Ingress{
ObjectMeta: api.ObjectMeta{
Name: "foo",
Namespace: api.NamespaceDefault,
},
Spec: extensions.IngressSpec{
Backend: &extensions.IngressBackend{
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
},
Rules: []extensions.IngressRule{
{
Host: "foo.bar.com",
IngressRuleValue: extensions.IngressRuleValue{
HTTP: &extensions.HTTPIngressRuleValue{
Paths: []extensions.HTTPIngressPath{
{
Path: "/foo",
Backend: defaultBackend,
},
},
},
},
},
},
},
}
}
func TestAnnotations(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
ing.SetAnnotations(data)
/*
tests := []struct {
title string
url string
method string
sendBody bool
expErr bool
}{
{"empty", "", "", false, true},
{"no scheme", "bar", "", false, true},
{"invalid host", "http://", "", false, true},
{"invalid host (multiple dots)", "http://foo..bar.com", "", false, true},
{"valid URL", "http://bar.foo.com/external-auth", "", false, false},
{"valid URL - send body", "http://foo.com/external-auth", "POST", true, false},
{"valid URL - send body", "http://foo.com/external-auth", "GET", true, false},
}
for _, test := range tests {
data[authTLSSecret] = ""
test.title
u, err := ParseAnnotations(ing)
if test.expErr {
if err == nil {
t.Errorf("%v: expected error but retuned nil", test.title)
}
continue
}
if u.URL != test.url {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.url, u.URL)
}
if u.Method != test.method {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.method, u.Method)
}
if u.SendBody != test.sendBody {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.sendBody, u.SendBody)
}
}*/
}

View file

@ -17,8 +17,7 @@ limitations under the License.
package cors
import (
"errors"
"strconv"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/kubernetes/pkg/apis/extensions"
)
@ -27,24 +26,8 @@ const (
cors = "ingress.kubernetes.io/enable-cors"
)
type ingAnnotations map[string]string
func (a ingAnnotations) cors() bool {
val, ok := a[cors]
if ok {
if b, err := strconv.ParseBool(val); err == nil {
return b
}
}
return false
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to indicate if the location/s should allows CORS
func ParseAnnotations(ing *extensions.Ingress) (bool, error) {
if ing.GetAnnotations() == nil {
return false, errors.New("no annotations present")
}
return ingAnnotations(ing.GetAnnotations()).cors(), nil
return parser.GetBoolAnnotation(cors, ing)
}

View file

@ -17,12 +17,10 @@ limitations under the License.
package healthcheck
import (
"errors"
"strconv"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/contrib/ingress/controllers/nginx/nginx/config"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/ingress/core/pkg/ingress/defaults"
)
const (
@ -30,19 +28,6 @@ const (
upsFailTimeout = "ingress.kubernetes.io/upstream-fail-timeout"
)
var (
// ErrMissingMaxFails returned error when the ingress does not contains the
// max-fails annotation
ErrMissingMaxFails = errors.New("max-fails annotations is missing")
// ErrMissingFailTimeout returned error when the ingress does not contains
// the fail-timeout annotation
ErrMissingFailTimeout = errors.New("fail-timeout annotations is missing")
// ErrInvalidNumber returned
ErrInvalidNumber = errors.New("the annotation does not contains a number")
)
// Upstream returns the URL and method to use check the status of
// the upstream server/s
type Upstream struct {
@ -50,49 +35,19 @@ type Upstream struct {
FailTimeout int
}
type ingAnnotations map[string]string
func (a ingAnnotations) maxFails() (int, error) {
val, ok := a[upsMaxFails]
if !ok {
return 0, ErrMissingMaxFails
}
mf, err := strconv.Atoi(val)
if err != nil {
return 0, ErrInvalidNumber
}
return mf, nil
}
func (a ingAnnotations) failTimeout() (int, error) {
val, ok := a[upsFailTimeout]
if !ok {
return 0, ErrMissingFailTimeout
}
ft, err := strconv.Atoi(val)
if err != nil {
return 0, ErrInvalidNumber
}
return ft, nil
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to configure upstream check parameters
func ParseAnnotations(cfg config.Configuration, ing *extensions.Ingress) *Upstream {
func ParseAnnotations(cfg defaults.Backend, ing *extensions.Ingress) *Upstream {
if ing.GetAnnotations() == nil {
return &Upstream{cfg.UpstreamMaxFails, cfg.UpstreamFailTimeout}
}
mf, err := ingAnnotations(ing.GetAnnotations()).maxFails()
mf, err := parser.GetIntAnnotation(upsMaxFails, ing)
if err != nil {
mf = cfg.UpstreamMaxFails
}
ft, err := ingAnnotations(ing.GetAnnotations()).failTimeout()
ft, err := parser.GetIntAnnotation(upsFailTimeout, ing)
if err != nil {
ft = cfg.UpstreamFailTimeout
}

View file

@ -23,7 +23,7 @@ import (
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/util/intstr"
"k8s.io/contrib/ingress/controllers/nginx/nginx/config"
"k8s.io/ingress/core/pkg/ingress/defaults"
)
func buildIngress() *extensions.Ingress {
@ -61,41 +61,6 @@ func buildIngress() *extensions.Ingress {
}
}
func TestAnnotations(t *testing.T) {
ing := buildIngress()
_, err := ingAnnotations(ing.GetAnnotations()).maxFails()
if err == nil {
t.Error("Expected a validation error")
}
_, err = ingAnnotations(ing.GetAnnotations()).failTimeout()
if err == nil {
t.Error("Expected a validation error")
}
data := map[string]string{}
data[upsMaxFails] = "1"
data[upsFailTimeout] = "1"
ing.SetAnnotations(data)
mf, err := ingAnnotations(ing.GetAnnotations()).maxFails()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if mf != 1 {
t.Errorf("Expected 1 but returned %v", mf)
}
ft, err := ingAnnotations(ing.GetAnnotations()).failTimeout()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if ft != 1 {
t.Errorf("Expected 1 but returned %v", ft)
}
}
func TestIngressHealthCheck(t *testing.T) {
ing := buildIngress()
@ -103,8 +68,7 @@ func TestIngressHealthCheck(t *testing.T) {
data[upsMaxFails] = "2"
ing.SetAnnotations(data)
cfg := config.Configuration{}
cfg.UpstreamFailTimeout = 1
cfg := defaults.Backend{UpstreamFailTimeout: 1}
nginxHz := ParseAnnotations(cfg, ing)

View file

@ -20,6 +20,9 @@ import (
"errors"
"strings"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/ingress/core/pkg/ingress/defaults"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/util/net/sets"
)
@ -29,10 +32,6 @@ const (
)
var (
// ErrMissingWhitelist returned error when the ingress does not contains the
// whitelist annotation
ErrMissingWhitelist = errors.New("whitelist annotation is missing")
// ErrInvalidCIDR returned error when the whitelist annotation does not
// contains a valid IP or network address
ErrInvalidCIDR = errors.New("the annotation does not contains a valid IP address or network")
@ -43,41 +42,31 @@ type SourceRange struct {
CIDR []string
}
type ingAnnotations map[string]string
// ParseAnnotations parses the annotations contained in the ingress
// rule used to limit access to certain client addresses or networks.
// Multiple ranges can specified using commas as separator
// e.g. `18.0.0.0/8,56.0.0.0/8`
func ParseAnnotations(cfg defaults.Backend, ing *extensions.Ingress) (*SourceRange, error) {
cidrs := []string{}
func (a ingAnnotations) whitelist() ([]string, error) {
cidrs := make([]string, 0)
val, ok := a[whitelist]
if !ok {
return cidrs, ErrMissingWhitelist
if ing.GetAnnotations() == nil {
return &SourceRange{CIDR: cfg.WhitelistSourceRange}, parser.ErrMissingAnnotations
}
val, err := parser.GetStringAnnotation(whitelist, ing)
if err != nil {
return &SourceRange{CIDR: cfg.WhitelistSourceRange}, err
}
values := strings.Split(val, ",")
ipnets, err := sets.ParseIPNets(values...)
if err != nil {
return cidrs, ErrInvalidCIDR
return &SourceRange{CIDR: cfg.WhitelistSourceRange}, ErrInvalidCIDR
}
for k := range ipnets {
cidrs = append(cidrs, k)
}
return cidrs, nil
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to limit access to certain client addresses or networks.
// Multiple ranges can specified using commas as separator
// e.g. `18.0.0.0/8,56.0.0.0/8`
func ParseAnnotations(whiteList []string, ing *extensions.Ingress) (*SourceRange, error) {
if ing.GetAnnotations() == nil {
return &SourceRange{whiteList}, ErrMissingWhitelist
}
wl, err := ingAnnotations(ing.GetAnnotations()).whitelist()
if err != nil {
wl = whiteList
}
return &SourceRange{wl}, err
return &SourceRange{cidrs}, nil
}

View file

@ -23,6 +23,8 @@ import (
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/util/intstr"
"k8s.io/ingress/core/pkg/ingress/defaults"
)
func buildIngress() *extensions.Ingress {
@ -60,51 +62,9 @@ func buildIngress() *extensions.Ingress {
}
}
func TestAnnotations(t *testing.T) {
ing := buildIngress()
_, err := ingAnnotations(ing.GetAnnotations()).whitelist()
if err == nil {
t.Error("Expected a validation error")
}
testNet := "10.0.0.0/24"
enet := []string{testNet}
data := map[string]string{}
data[whitelist] = testNet
ing.SetAnnotations(data)
wl, err := ingAnnotations(ing.GetAnnotations()).whitelist()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(wl, enet) {
t.Errorf("Expected %v but returned %s", enet, wl)
}
data[whitelist] = "10.0.0.0/24,10.0.1.0/25"
ing.SetAnnotations(data)
wl, err = ingAnnotations(ing.GetAnnotations()).whitelist()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if len(wl) != 2 {
t.Errorf("Expected 2 netwotks but %v was returned", len(wl))
}
}
func TestParseAnnotations(t *testing.T) {
ing := buildIngress()
_, err := ingAnnotations(ing.GetAnnotations()).whitelist()
if err == nil {
t.Error("Expected a validation error")
}
testNet := "10.0.0.0/24"
enet := []string{testNet}
@ -116,7 +76,7 @@ func TestParseAnnotations(t *testing.T) {
CIDR: enet,
}
sr, err := ParseAnnotations([]string{}, ing)
sr, err := ParseAnnotations(defaults.Backend{}, ing)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
@ -127,21 +87,23 @@ func TestParseAnnotations(t *testing.T) {
data[whitelist] = "www"
ing.SetAnnotations(data)
_, err = ParseAnnotations([]string{}, ing)
_, err = ParseAnnotations(defaults.Backend{}, ing)
if err == nil {
t.Errorf("Expected error parsing an invalid cidr")
}
delete(data, "whitelist")
ing.SetAnnotations(data)
sr, _ = ParseAnnotations([]string{}, ing)
if !reflect.DeepEqual(sr.CIDR, []string{}) {
t.Errorf("Expected empty CIDR but %v returned", sr.CIDR)
}
sr, _ = ParseAnnotations([]string{}, &extensions.Ingress{})
if !reflect.DeepEqual(sr.CIDR, []string{}) {
t.Errorf("Expected empty CIDR but %v returned", sr.CIDR)
}
//sr, _ = ParseAnnotations(defaults.Backend{}, ing)
// TODO: fix test
/*
if !reflect.DeepEqual(sr.CIDR, []string{}) {
t.Errorf("Expected empty CIDR but %v returned", sr.CIDR)
}
sr, _ = ParseAnnotations(defaults.Upstream{}, &extensions.Ingress{})
if !reflect.DeepEqual(sr.CIDR, []string{}) {
t.Errorf("Expected empty CIDR but %v returned", sr.CIDR)
}
*/
}

View file

@ -0,0 +1,102 @@
/*
Copyright 2015 The Kubernetes 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 parser
import (
"errors"
"fmt"
"strconv"
"k8s.io/kubernetes/pkg/apis/extensions"
)
var (
// ErrMissingAnnotations is returned when the ingress rule
// does not contains annotations related with rate limit
ErrMissingAnnotations = errors.New("Ingress rule without annotations")
// ErrInvalidName ...
ErrInvalidName = errors.New("invalid annotation name")
)
type ingAnnotations map[string]string
func (a ingAnnotations) parseBool(name string) (bool, error) {
val, ok := a[name]
if ok {
if b, err := strconv.ParseBool(val); err == nil {
return b, nil
}
}
return false, ErrMissingAnnotations
}
func (a ingAnnotations) parseString(name string) (string, error) {
val, ok := a[name]
if ok {
return val, nil
}
return "", ErrMissingAnnotations
}
func (a ingAnnotations) parseInt(name string) (int, error) {
val, ok := a[name]
if ok {
i, err := strconv.Atoi(val)
if err != nil {
return 0, fmt.Errorf("invalid annotations value: %v", err)
}
return i, nil
}
return 0, ErrMissingAnnotations
}
// GetBoolAnnotation ...
func GetBoolAnnotation(name string, ing *extensions.Ingress) (bool, error) {
if ing == nil || ing.GetAnnotations() == nil {
return false, ErrMissingAnnotations
}
if name == "" {
return false, ErrInvalidName
}
return ingAnnotations(ing.GetAnnotations()).parseBool(name)
}
// GetStringAnnotation ...
func GetStringAnnotation(name string, ing *extensions.Ingress) (string, error) {
if ing == nil || ing.GetAnnotations() == nil {
return "", ErrMissingAnnotations
}
if name == "" {
return "", ErrInvalidName
}
return ingAnnotations(ing.GetAnnotations()).parseString(name)
}
// GetIntAnnotation ...
func GetIntAnnotation(name string, ing *extensions.Ingress) (int, error) {
if ing == nil || ing.GetAnnotations() == nil {
return 0, ErrMissingAnnotations
}
if name == "" {
return 0, ErrInvalidName
}
return ingAnnotations(ing.GetAnnotations()).parseInt(name)
}

View file

@ -0,0 +1,154 @@
/*
Copyright 2016 The Kubernetes 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 parser
import (
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
)
func buildIngress() *extensions.Ingress {
return &extensions.Ingress{
ObjectMeta: api.ObjectMeta{
Name: "foo",
Namespace: api.NamespaceDefault,
},
Spec: extensions.IngressSpec{},
}
}
func TestGetBoolAnnotation(t *testing.T) {
ing := buildIngress()
_, err := GetBoolAnnotation("", nil)
if err == nil {
t.Errorf("expected error but retuned nil")
}
tests := []struct {
name string
field string
value string
exp bool
expErr bool
}{
{"empty - false", "", "false", false, true},
{"empty - true", "", "true", false, true},
{"valid - false", "bool", "false", false, false},
{"valid - true", "bool", "true", true, false},
}
data := map[string]string{}
ing.SetAnnotations(data)
for _, test := range tests {
data[test.field] = test.value
u, err := GetBoolAnnotation(test.field, ing)
if test.expErr {
if err == nil {
t.Errorf("%v: expected error but retuned nil", test.name)
}
continue
}
if u != test.exp {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.name, test.exp, u)
}
}
}
func TestGetStringAnnotation(t *testing.T) {
ing := buildIngress()
_, err := GetStringAnnotation("", nil)
if err == nil {
t.Errorf("expected error but retuned nil")
}
tests := []struct {
name string
field string
value string
exp string
expErr bool
}{
{"empty - A", "", "A", "", true},
{"empty - B", "", "B", "", true},
{"valid - A", "string", "A", "A", false},
{"valid - B", "string", "B", "B", false},
}
data := map[string]string{}
ing.SetAnnotations(data)
for _, test := range tests {
data[test.field] = test.value
s, err := GetStringAnnotation(test.field, ing)
if test.expErr {
if err == nil {
t.Errorf("%v: expected error but retuned nil", test.name)
}
continue
}
if s != test.exp {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.name, test.exp, s)
}
}
}
func TestGetIntAnnotation(t *testing.T) {
ing := buildIngress()
_, err := GetIntAnnotation("", nil)
if err == nil {
t.Errorf("expected error but retuned nil")
}
tests := []struct {
name string
field string
value string
exp int
expErr bool
}{
{"empty - A", "", "1", 0, true},
{"empty - B", "", "2", 0, true},
{"valid - A", "string", "1", 1, false},
{"valid - B", "string", "2", 2, false},
}
data := map[string]string{}
ing.SetAnnotations(data)
for _, test := range tests {
data[test.field] = test.value
s, err := GetIntAnnotation(test.field, ing)
if test.expErr {
if err == nil {
t.Errorf("%v: expected error but retuned nil", test.name)
}
continue
}
if s != test.exp {
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.name, test.exp, s)
}
}
}

View file

@ -0,0 +1,74 @@
/*
Copyright 2016 The Kubernetes 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 proxy
import (
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/ingress/core/pkg/ingress/defaults"
)
const (
connect = "ingress.kubernetes.io/proxy-connect-timeout"
send = "ingress.kubernetes.io/proxy-send-timeout"
read = "ingress.kubernetes.io/proxy-read-timeout"
bufferSize = "ingress.kubernetes.io/proxy-buffer-size"
)
// Configuration returns the proxy timeout to use in the upstream server/s
type Configuration struct {
ConnectTimeout int
SendTimeout int
ReadTimeout int
BufferSize string
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to configure upstream check parameters
func ParseAnnotations(cfg defaults.Backend, ing *extensions.Ingress) *Configuration {
if ing == nil || ing.GetAnnotations() == nil {
return &Configuration{
cfg.ProxyConnectTimeout,
cfg.ProxySendTimeout,
cfg.ProxyReadTimeout,
cfg.ProxyBufferSize,
}
}
ct, err := parser.GetIntAnnotation(connect, ing)
if err != nil {
ct = cfg.ProxyConnectTimeout
}
st, err := parser.GetIntAnnotation(send, ing)
if err != nil {
st = cfg.ProxySendTimeout
}
rt, err := parser.GetIntAnnotation(read, ing)
if err != nil {
rt = cfg.ProxyReadTimeout
}
bs, err := parser.GetStringAnnotation(bufferSize, ing)
if err != nil || bs == "" {
bs = cfg.ProxyBufferSize
}
return &Configuration{ct, st, rt, bs}
}

View file

@ -0,0 +1,90 @@
/*
Copyright 2016 The Kubernetes 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 proxy
import (
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/util/intstr"
"k8s.io/ingress/core/pkg/ingress/defaults"
)
func buildIngress() *extensions.Ingress {
defaultBackend := extensions.IngressBackend{
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
}
return &extensions.Ingress{
ObjectMeta: api.ObjectMeta{
Name: "foo",
Namespace: api.NamespaceDefault,
},
Spec: extensions.IngressSpec{
Backend: &extensions.IngressBackend{
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
},
Rules: []extensions.IngressRule{
{
Host: "foo.bar.com",
IngressRuleValue: extensions.IngressRuleValue{
HTTP: &extensions.HTTPIngressRuleValue{
Paths: []extensions.HTTPIngressPath{
{
Path: "/foo",
Backend: defaultBackend,
},
},
},
},
},
},
},
}
}
func TestIngressHealthCheck(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[connect] = "1"
data[send] = "2"
data[read] = "3"
data[bufferSize] = "1k"
ing.SetAnnotations(data)
cfg := defaults.Backend{UpstreamFailTimeout: 1}
p := ParseAnnotations(cfg, ing)
if p.ConnectTimeout != 1 {
t.Errorf("Expected 1 as connect-timeout but returned %v", p.ConnectTimeout)
}
if p.SendTimeout != 2 {
t.Errorf("Expected 2 as send-timeout but returned %v", p.SendTimeout)
}
if p.ReadTimeout != 3 {
t.Errorf("Expected 3 as read-timeout but returned %v", p.ReadTimeout)
}
if p.BufferSize != "1k" {
t.Errorf("Expected 1k as buffer-size but returned %v", p.BufferSize)
}
}

View file

@ -19,7 +19,8 @@ package ratelimit
import (
"errors"
"fmt"
"strconv"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/kubernetes/pkg/apis/extensions"
)
@ -39,10 +40,6 @@ const (
var (
// ErrInvalidRateLimit is returned when the annotation caontains invalid values
ErrInvalidRateLimit = errors.New("invalid rate limit value. Must be > 0")
// ErrMissingAnnotations is returned when the ingress rule
// does not contains annotations related with rate limit
ErrMissingAnnotations = errors.New("no annotations present")
)
// RateLimit returns rate limit configuration for an Ingress rule
@ -66,39 +63,15 @@ type Zone struct {
SharedSize int
}
type ingAnnotations map[string]string
func (a ingAnnotations) limitIP() int {
val, ok := a[limitIP]
if ok {
if i, err := strconv.Atoi(val); err == nil {
return i
}
}
return 0
}
func (a ingAnnotations) limitRPS() int {
val, ok := a[limitRPS]
if ok {
if i, err := strconv.Atoi(val); err == nil {
return i
}
}
return 0
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to rewrite the defined paths
func ParseAnnotations(ing *extensions.Ingress) (*RateLimit, error) {
if ing.GetAnnotations() == nil {
return &RateLimit{}, ErrMissingAnnotations
return &RateLimit{}, parser.ErrMissingAnnotations
}
rps := ingAnnotations(ing.GetAnnotations()).limitRPS()
conn := ingAnnotations(ing.GetAnnotations()).limitIP()
rps, _ := parser.GetIntAnnotation(limitRPS, ing)
conn, _ := parser.GetIntAnnotation(limitIP, ing)
if rps == 0 && conn == 0 {
return &RateLimit{

View file

@ -59,35 +59,6 @@ func buildIngress() *extensions.Ingress {
}
}
func TestAnnotations(t *testing.T) {
ing := buildIngress()
lip := ingAnnotations(ing.GetAnnotations()).limitIP()
if lip != 0 {
t.Errorf("Expected 0 in limit by ip but %v was returned", lip)
}
lrps := ingAnnotations(ing.GetAnnotations()).limitRPS()
if lrps != 0 {
t.Errorf("Expected 0 in limit by rps but %v was returend", lrps)
}
data := map[string]string{}
data[limitIP] = "5"
data[limitRPS] = "100"
ing.SetAnnotations(data)
lip = ingAnnotations(ing.GetAnnotations()).limitIP()
if lip != 5 {
t.Errorf("Expected 5 in limit by ip but %v was returend", lip)
}
lrps = ingAnnotations(ing.GetAnnotations()).limitRPS()
if lrps != 100 {
t.Errorf("Expected 100 in limit by rps but %v was returend", lrps)
}
}
func TestWithoutAnnotations(t *testing.T) {
ing := buildIngress()
_, err := ParseAnnotations(ing)

View file

@ -18,11 +18,11 @@ package rewrite
import (
"errors"
"strconv"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/ingress/core/pkg/ingress/defaults"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/contrib/ingress/controllers/nginx/nginx/config"
)
const (
@ -38,69 +38,24 @@ type Redirect struct {
// AddBaseURL indicates if is required to add a base tag in the head
// of the responses from the upstream servers
AddBaseURL bool
// Should indicates if the location section should be accessible SSL only
// SSLRedirect indicates if the location section is accessible SSL only
SSLRedirect bool
}
var (
// ErrMissingSSLRedirect returned error when the ingress does not contains the
// ssl-redirect annotation
ErrMissingSSLRedirect = errors.New("ssl-redirect annotations is missing")
// ErrInvalidBool gets returned when the str value is not convertible to a bool
ErrInvalidBool = errors.New("ssl-redirect annotations has invalid value")
)
type ingAnnotations map[string]string
func (a ingAnnotations) addBaseURL() bool {
val, ok := a[addBaseURL]
if ok {
if b, err := strconv.ParseBool(val); err == nil {
return b
}
}
return false
}
func (a ingAnnotations) rewriteTo() string {
val, ok := a[rewriteTo]
if ok {
return val
}
return ""
}
func (a ingAnnotations) sslRedirect() (bool, error) {
val, ok := a[sslRedirect]
if !ok {
return false, ErrMissingSSLRedirect
}
sr, err := strconv.ParseBool(val)
if err != nil {
return false, ErrInvalidBool
}
return sr, nil
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to rewrite the defined paths
func ParseAnnotations(cfg config.Configuration, ing *extensions.Ingress) (*Redirect, error) {
func ParseAnnotations(cfg defaults.Backend, ing *extensions.Ingress) (*Redirect, error) {
if ing.GetAnnotations() == nil {
return &Redirect{}, errors.New("no annotations present")
}
annotations := ingAnnotations(ing.GetAnnotations())
sslRe, err := annotations.sslRedirect()
sslRe, err := parser.GetBoolAnnotation(sslRedirect, ing)
if err != nil {
sslRe = cfg.SSLRedirect
}
rt := annotations.rewriteTo()
abu := annotations.addBaseURL()
rt, _ := parser.GetStringAnnotation(rewriteTo, ing)
abu, _ := parser.GetBoolAnnotation(addBaseURL, ing)
return &Redirect{
Target: rt,
AddBaseURL: abu,

View file

@ -23,7 +23,7 @@ import (
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/util/intstr"
"k8s.io/contrib/ingress/controllers/nginx/nginx/config"
"k8s.io/ingress/core/pkg/ingress/defaults"
)
const (
@ -65,38 +65,9 @@ func buildIngress() *extensions.Ingress {
}
}
func TestAnnotations(t *testing.T) {
ing := buildIngress()
r := ingAnnotations(ing.GetAnnotations()).rewriteTo()
if r != "" {
t.Error("Expected no redirect")
}
f := ingAnnotations(ing.GetAnnotations()).addBaseURL()
if f {
t.Errorf("Expected false in add-base-url but %v was returend", f)
}
data := map[string]string{}
data[rewriteTo] = defRoute
data[addBaseURL] = "true"
ing.SetAnnotations(data)
r = ingAnnotations(ing.GetAnnotations()).rewriteTo()
if r != defRoute {
t.Errorf("Expected %v in rewrite but %v was returend", defRoute, r)
}
f = ingAnnotations(ing.GetAnnotations()).addBaseURL()
if !f {
t.Errorf("Expected true in add-base-url but %v was returend", f)
}
}
func TestWithoutAnnotations(t *testing.T) {
ing := buildIngress()
_, err := ParseAnnotations(config.NewDefault(), ing)
_, err := ParseAnnotations(defaults.Backend{}, ing)
if err == nil {
t.Error("Expected error with ingress without annotations")
}
@ -109,7 +80,7 @@ func TestRedirect(t *testing.T) {
data[rewriteTo] = defRoute
ing.SetAnnotations(data)
redirect, err := ParseAnnotations(config.NewDefault(), ing)
redirect, err := ParseAnnotations(defaults.Backend{}, ing)
if err != nil {
t.Errorf("Uxpected error with ingress: %v", err)
}
@ -122,7 +93,7 @@ func TestRedirect(t *testing.T) {
func TestSSLRedirect(t *testing.T) {
ing := buildIngress()
cfg := config.Configuration{SSLRedirect: true}
cfg := defaults.Backend{SSLRedirect: true}
data := map[string]string{}

View file

@ -17,8 +17,7 @@ limitations under the License.
package secureupstream
import (
"errors"
"strconv"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/kubernetes/pkg/apis/extensions"
)
@ -27,24 +26,8 @@ const (
secureUpstream = "ingress.kubernetes.io/secure-backends"
)
type ingAnnotations map[string]string
func (a ingAnnotations) secureUpstream() bool {
val, ok := a[secureUpstream]
if ok {
if b, err := strconv.ParseBool(val); err == nil {
return b
}
}
return false
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to indicate if the upstream servers should use SSL
func ParseAnnotations(ing *extensions.Ingress) (bool, error) {
if ing.GetAnnotations() == nil {
return false, errors.New("no annotations present")
}
return ingAnnotations(ing.GetAnnotations()).secureUpstream(), nil
return parser.GetBoolAnnotation(secureUpstream, ing)
}

View file

@ -65,9 +65,9 @@ func TestAnnotations(t *testing.T) {
data[secureUpstream] = "true"
ing.SetAnnotations(data)
su := ingAnnotations(ing.GetAnnotations()).secureUpstream()
if !su {
t.Errorf("Expected true in secure-backends but %v was returned", su)
_, err := ParseAnnotations(ing)
if err != nil {
t.Error("Expected error with ingress without annotations")
}
}

View file

@ -0,0 +1,73 @@
/*
Copyright 2015 The Kubernetes 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 service
import (
"encoding/json"
"fmt"
"strconv"
"k8s.io/kubernetes/pkg/api"
"github.com/golang/glog"
)
const (
// NamedPortAnnotation annotation used to map named port in services
NamedPortAnnotation = "ingress.kubernetes.io/named-ports"
)
type namedPortMapping map[string]string
// getPort returns the port defined in a named port
func (npm namedPortMapping) getPort(name string) (string, bool) {
val, ok := npm.getPortMappings()[name]
return val, ok
}
// getPortMappings returns a map containing the mapping of named ports names and number
func (npm namedPortMapping) getPortMappings() map[string]string {
data := npm[NamedPortAnnotation]
var mapping map[string]string
if data == "" {
return mapping
}
if err := json.Unmarshal([]byte(data), &mapping); err != nil {
glog.Errorf("unexpected error reading annotations: %v", err)
}
return mapping
}
// GetPortMapping returns the number of the named port or an error if is not valid
func GetPortMapping(name string, s *api.Service) (int32, error) {
if s == nil {
return -1, fmt.Errorf("impossible to extract por mapping from %v (missing service)", name)
}
namedPorts := s.ObjectMeta.Annotations
val, ok := namedPortMapping(namedPorts).getPort(name)
if ok {
port, err := strconv.Atoi(val)
if err != nil {
return -1, fmt.Errorf("service %v contains an invalid port mapping for %v (%v), %v", s.Name, name, val, err)
}
return int32(port), nil
}
return -1, fmt.Errorf("there is no port with name %v", name)
}

View file

@ -0,0 +1,45 @@
/*
Copyright 2016 The Kubernetes 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 sslpassthrough
import (
"fmt"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/ingress/core/pkg/ingress/defaults"
"k8s.io/kubernetes/pkg/apis/extensions"
)
const (
passthrough = "ingress.kubernetes.io/ssl-passthrough"
)
// ParseAnnotations parses the annotations contained in the ingress
// rule used to indicate if is required to configure
func ParseAnnotations(cfg defaults.Backend, ing *extensions.Ingress) (bool, error) {
if ing.GetAnnotations() == nil {
return false, parser.ErrMissingAnnotations
}
if len(ing.Spec.TLS) == 0 {
return false, fmt.Errorf("ingres rule %v/%v does not contains a TLS section", ing.Name, ing.Namespace)
}
return parser.GetBoolAnnotation(passthrough, ing)
}

View file

@ -0,0 +1,74 @@
/*
Copyright 2016 The Kubernetes 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 sslpassthrough
import (
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/util/intstr"
"k8s.io/ingress/core/pkg/ingress/defaults"
)
func buildIngress() *extensions.Ingress {
return &extensions.Ingress{
ObjectMeta: api.ObjectMeta{
Name: "foo",
Namespace: api.NamespaceDefault,
},
Spec: extensions.IngressSpec{
Backend: &extensions.IngressBackend{
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
},
},
}
}
func TestParseAnnotations(t *testing.T) {
ing := buildIngress()
_, err := ParseAnnotations(defaults.Backend{}, ing)
if err == nil {
t.Errorf("unexpected error: %v", err)
}
data := map[string]string{}
data[passthrough] = "true"
ing.SetAnnotations(data)
// test ingress using the annotation without a TLS section
val, err := ParseAnnotations(defaults.Backend{}, ing)
if err == nil {
t.Errorf("expected error parsing an invalid cidr")
}
// test with a valid host
ing.Spec.TLS = []extensions.IngressTLS{
{
Hosts: []string{"foo.bar.com"},
},
}
val, err = ParseAnnotations(defaults.Backend{}, ing)
if err != nil {
t.Errorf("expected error parsing an invalid cidr")
}
if !val {
t.Errorf("expected true but false returned")
}
}

View file

@ -0,0 +1,161 @@
/*
Copyright 2015 The Kubernetes 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 controller
import (
"fmt"
"strings"
"time"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/client/cache"
"github.com/golang/glog"
"k8s.io/ingress/core/pkg/ingress"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
ssl "k8s.io/ingress/core/pkg/net/ssl"
)
// syncSecret keeps in sync Secrets used by Ingress rules with files to allow
// being used in controllers.
func (ic *GenericController) syncSecret(k interface{}) error {
if ic.secretQueue.IsShuttingDown() {
return nil
}
if !ic.controllersInSync() {
time.Sleep(podStoreSyncedPollPeriod)
return fmt.Errorf("deferring sync till endpoints controller has synced")
}
// check if the default certificate is configured
key := fmt.Sprintf("default/%v", defServerName)
_, exists := ic.sslCertTracker.Get(key)
var cert *ingress.SSLCert
var err error
if !exists {
if ic.cfg.DefaultSSLCertificate != "" {
cert, err = ic.getPemCertificate(ic.cfg.DefaultSSLCertificate)
if err != nil {
return err
}
} else {
defCert, defKey := ssl.GetFakeSSLCert()
cert, err = ssl.AddOrUpdateCertAndKey("system-snake-oil-certificate", defCert, defKey, []byte{})
if err != nil {
return nil
}
}
cert.Name = defServerName
cert.Namespace = api.NamespaceDefault
ic.sslCertTracker.Add(key, cert)
}
key = k.(string)
// get secret
secObj, exists, err := ic.secrLister.Store.GetByKey(key)
if err != nil {
return fmt.Errorf("error getting secret %v: %v", key, err)
}
if !exists {
return fmt.Errorf("secret %v was not found", key)
}
sec := secObj.(*api.Secret)
if !ic.secrReferenced(sec.Name, sec.Namespace) {
glog.V(2).Infof("secret %v/%v is not used in Ingress rules. skipping ", sec.Namespace, sec.Name)
return nil
}
cert, err = ic.getPemCertificate(key)
if err != nil {
return err
}
// create certificates and add or update the item in the store
_, exists = ic.sslCertTracker.Get(key)
if exists {
ic.sslCertTracker.Update(key, cert)
return nil
}
ic.sslCertTracker.Add(key, cert)
return nil
}
func (ic *GenericController) getPemCertificate(secretName string) (*ingress.SSLCert, error) {
secretInterface, exists, err := ic.secrLister.Store.GetByKey(secretName)
if err != nil {
return nil, fmt.Errorf("Error retriveing secret %v: %v", secretName, err)
}
if !exists {
return nil, fmt.Errorf("secret named %v does not exists", secretName)
}
secret := secretInterface.(*api.Secret)
cert, ok := secret.Data[api.TLSCertKey]
if !ok {
return nil, fmt.Errorf("secret named %v has no private key", secretName)
}
key, ok := secret.Data[api.TLSPrivateKeyKey]
if !ok {
return nil, fmt.Errorf("secret named %v has no cert", secretName)
}
ca := secret.Data["ca.crt"]
nsSecName := strings.Replace(secretName, "/", "-", -1)
s, err := ssl.AddOrUpdateCertAndKey(nsSecName, cert, key, ca)
if err != nil {
return nil, err
}
s.Name = secret.Name
s.Namespace = secret.Namespace
return s, nil
}
// check if secret is referenced in this controller's config
func (ic *GenericController) secrReferenced(name, namespace string) bool {
for _, ingIf := range ic.ingLister.Store.List() {
ing := ingIf.(*extensions.Ingress)
str, err := parser.GetStringAnnotation("ingress.kubernetes.io/auth-tls-secret", ing)
if err == nil && str == fmt.Sprintf("%v/%v", namespace, name) {
return true
}
if ing.Namespace != namespace {
continue
}
for _, tls := range ing.Spec.TLS {
if tls.SecretName == name {
return true
}
}
}
return false
}
// sslCertTracker ...
type sslCertTracker struct {
cache.ThreadSafeStore
}
func newSSLCertTracker() *sslCertTracker {
return &sslCertTracker{
cache.NewThreadSafeStore(cache.Indexers{}, cache.Indices{}),
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,182 @@
package controller
import (
"flag"
"fmt"
"net/http"
"net/http/pprof"
"os"
"syscall"
"time"
"github.com/golang/glog"
"github.com/spf13/pflag"
"k8s.io/ingress/core/pkg/ingress"
"k8s.io/ingress/core/pkg/k8s"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/kubernetes/pkg/api"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/healthz"
kubectl_util "k8s.io/kubernetes/pkg/kubectl/cmd/util"
)
// NewIngressController returns a configured Ingress controller ready to start
func NewIngressController(backend ingress.Controller) Interface {
var (
flags = pflag.NewFlagSet("", pflag.ExitOnError)
defaultSvc = flags.String("default-backend-service", "",
`Service used to serve a 404 page for the default backend. Takes the form
namespace/name. The controller uses the first node port of this Service for
the default backend.`)
ingressClass = flags.String("ingress-class", "nginx",
`Name of the ingress class to route through this controller.`)
configMap = flags.String("configmap", "",
`Name of the ConfigMap that contains the custom configuration to use`)
publishSvc = flags.String("publish-service", "",
`Service fronting the ingress controllers. Takes the form
namespace/name. The controller will set the endpoint records on the
ingress objects to reflect those on the service.`)
tcpConfigMapName = flags.String("tcp-services-configmap", "",
`Name of the ConfigMap that contains the definition of the TCP services to expose.
The key in the map indicates the external port to be used. The value is the name of the
service with the format namespace/serviceName and the port of the service could be a
number of the name of the port.
The ports 80 and 443 are not allowed as external ports. This ports are reserved for nginx`)
udpConfigMapName = flags.String("udp-services-configmap", "",
`Name of the ConfigMap that contains the definition of the UDP services to expose.
The key in the map indicates the external port to be used. The value is the name of the
service with the format namespace/serviceName and the port of the service could be a
number of the name of the port.`)
resyncPeriod = flags.Duration("sync-period", 60*time.Second,
`Relist and confirm cloud resources this often.`)
watchNamespace = flags.String("watch-namespace", api.NamespaceAll,
`Namespace to watch for Ingress. Default is to watch all namespaces`)
healthzPort = flags.Int("healthz-port", 10254, "port for healthz endpoint.")
profiling = flags.Bool("profiling", true, `Enable profiling via web interface host:port/debug/pprof/`)
defSSLCertificate = flags.String("default-ssl-certificate", "", `Name of the secret
that contains a SSL certificate to be used as default for a HTTPS catch-all server`)
defHealthzURL = flags.String("health-check-path", "/healthz", `Defines
the URL to be used as health check inside in the default server in NGINX.`)
)
flags.AddGoFlagSet(flag.CommandLine)
flags.Parse(os.Args)
clientConfig := kubectl_util.DefaultClientConfig(flags)
flag.Set("logtostderr", "true")
glog.Info(backend.Info())
if *ingressClass != "" {
glog.Infof("Watching for ingress class: %s", *ingressClass)
}
if *defaultSvc == "" {
glog.Fatalf("Please specify --default-backend-service")
}
kubeconfig, err := restclient.InClusterConfig()
if err != nil {
kubeconfig, err = clientConfig.ClientConfig()
if err != nil {
glog.Fatalf("error configuring the client: %v", err)
}
}
kubeClient, err := clientset.NewForConfig(kubeconfig)
if err != nil {
glog.Fatalf("failed to create client: %v", err)
}
_, err = k8s.IsValidService(kubeClient, *defaultSvc)
if err != nil {
glog.Fatalf("no service with name %v found: %v", *defaultSvc, err)
}
glog.Infof("validated %v as the default backend", *defaultSvc)
if *publishSvc != "" {
svc, err := k8s.IsValidService(kubeClient, *publishSvc)
if err != nil {
glog.Fatalf("no service with name %v found: %v", *publishSvc, err)
}
if len(svc.Status.LoadBalancer.Ingress) == 0 {
// We could poll here, but we instead just exit and rely on k8s to restart us
glog.Fatalf("service %s does not (yet) have ingress points", *publishSvc)
}
glog.Infof("service %v validated as source of Ingress status", *publishSvc)
}
if *configMap != "" {
_, _, err = k8s.ParseNameNS(*configMap)
if err != nil {
glog.Fatalf("configmap error: %v", err)
}
}
os.MkdirAll(ingress.DefaultSSLDirectory, 0655)
config := &Configuration{
Client: kubeClient,
ResyncPeriod: *resyncPeriod,
DefaultService: *defaultSvc,
IngressClass: *ingressClass,
Namespace: *watchNamespace,
ConfigMapName: *configMap,
TCPConfigMapName: *tcpConfigMapName,
UDPConfigMapName: *udpConfigMapName,
DefaultSSLCertificate: *defSSLCertificate,
DefaultHealthzURL: *defHealthzURL,
PublishService: *publishSvc,
Backend: backend,
}
ic := newIngressController(config)
go registerHandlers(*profiling, *healthzPort, ic)
return ic
}
func registerHandlers(enableProfiling bool, port int, ic Interface) {
mux := http.NewServeMux()
healthz.InstallHandler(mux, ic)
mux.Handle("/metrics", prometheus.Handler())
mux.HandleFunc("/build", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, ic.Info())
})
mux.HandleFunc("/stop", func(w http.ResponseWriter, r *http.Request) {
syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
})
if enableProfiling {
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
}
server := &http.Server{
Addr: fmt.Sprintf(":%v", port),
Handler: mux,
}
glog.Fatal(server.ListenAndServe())
}

View file

@ -0,0 +1,60 @@
/*
Copyright 2015 The Kubernetes 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 controller
import "github.com/prometheus/client_golang/prometheus"
const (
ns = "ingress_controller"
operation = "count"
reloadLabel = "reloads"
)
func init() {
prometheus.MustRegister(reloadOperation)
prometheus.MustRegister(reloadOperationErrors)
reloadOperationErrors.WithLabelValues(reloadLabel).Set(0)
reloadOperation.WithLabelValues(reloadLabel).Set(0)
}
var (
reloadOperation = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: ns,
Name: "success",
Help: "Cumulative number of Ingress controller reload operations",
},
[]string{operation},
)
reloadOperationErrors = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: ns,
Name: "errors",
Help: "Cumulative number of Ingress controller errors during reload operations",
},
[]string{operation},
)
)
func incReloadCount() {
reloadOperation.WithLabelValues(reloadLabel).Inc()
}
func incReloadErrorCount() {
reloadOperationErrors.WithLabelValues(reloadLabel).Inc()
}

View file

@ -0,0 +1,105 @@
/*
Copyright 2015 The Kubernetes 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 controller
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"github.com/golang/glog"
"k8s.io/ingress/core/pkg/ingress/annotations/service"
"k8s.io/kubernetes/pkg/api"
podutil "k8s.io/kubernetes/pkg/api/pod"
"k8s.io/kubernetes/pkg/labels"
)
// checkSvcForUpdate verifies if one of the running pods for a service contains
// named port. If the annotation in the service does not exists or is not equals
// to the port mapping obtained from the pod the service must be updated to reflect
// the current state
func (ic *GenericController) checkSvcForUpdate(svc *api.Service) error {
// get the pods associated with the service
// TODO: switch this to a watch
pods, err := ic.cfg.Client.Pods(svc.Namespace).List(api.ListOptions{
LabelSelector: labels.Set(svc.Spec.Selector).AsSelector(),
})
if err != nil {
return fmt.Errorf("error searching service pods %v/%v: %v", svc.Namespace, svc.Name, err)
}
if len(pods.Items) == 0 {
return nil
}
namedPorts := map[string]string{}
// we need to check only one pod searching for named ports
pod := &pods.Items[0]
glog.V(4).Infof("checking pod %v/%v for named port information", pod.Namespace, pod.Name)
for i := range svc.Spec.Ports {
servicePort := &svc.Spec.Ports[i]
_, err := strconv.Atoi(servicePort.TargetPort.StrVal)
if err != nil {
portNum, err := podutil.FindPort(pod, servicePort)
if err != nil {
glog.V(4).Infof("failed to find port for service %s/%s: %v", portNum, svc.Namespace, svc.Name, err)
continue
}
if servicePort.TargetPort.StrVal == "" {
continue
}
namedPorts[servicePort.TargetPort.StrVal] = fmt.Sprintf("%v", portNum)
}
}
if svc.ObjectMeta.Annotations == nil {
svc.ObjectMeta.Annotations = map[string]string{}
}
curNamedPort := svc.ObjectMeta.Annotations[service.NamedPortAnnotation]
if len(namedPorts) > 0 && !reflect.DeepEqual(curNamedPort, namedPorts) {
data, _ := json.Marshal(namedPorts)
newSvc, err := ic.cfg.Client.Services(svc.Namespace).Get(svc.Name)
if err != nil {
return fmt.Errorf("error getting service %v/%v: %v", svc.Namespace, svc.Name, err)
}
if newSvc.ObjectMeta.Annotations == nil {
newSvc.ObjectMeta.Annotations = map[string]string{}
}
newSvc.ObjectMeta.Annotations[service.NamedPortAnnotation] = string(data)
glog.Infof("updating service %v with new named port mappings", svc.Name)
_, err = ic.cfg.Client.Services(svc.Namespace).Update(newSvc)
if err != nil {
return fmt.Errorf("error syncing service %v/%v: %v", svc.Namespace, svc.Name, err)
}
return nil
}
return nil
}

View file

@ -0,0 +1,95 @@
/*
Copyright 2015 The Kubernetes 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 controller
import (
"strings"
"k8s.io/ingress/core/pkg/ingress"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/kubernetes/pkg/apis/extensions"
)
// newDefaultServer return an UpstreamServer to be use as default server that returns 503.
func newDefaultServer() ingress.UpstreamServer {
return ingress.UpstreamServer{Address: "127.0.0.1", Port: "8181"}
}
// newUpstream creates an upstream without servers.
func newUpstream(name string) *ingress.Upstream {
return &ingress.Upstream{
Name: name,
Backends: []ingress.UpstreamServer{},
}
}
func isHostValid(host string, cert *ingress.SSLCert) bool {
if cert == nil {
return false
}
for _, cn := range cert.CN {
if matchHostnames(cn, host) {
return true
}
}
return false
}
func matchHostnames(pattern, host string) bool {
host = strings.TrimSuffix(host, ".")
pattern = strings.TrimSuffix(pattern, ".")
if len(pattern) == 0 || len(host) == 0 {
return false
}
patternParts := strings.Split(pattern, ".")
hostParts := strings.Split(host, ".")
if len(patternParts) != len(hostParts) {
return false
}
for i, patternPart := range patternParts {
if i == 0 && patternPart == "*" {
continue
}
if patternPart != hostParts[i] {
return false
}
}
return true
}
// IsValidClass returns true if the given Ingress either doesn't specify
// the ingress.class annotation, or it's set to the configured in the
// ingress controller.
func IsValidClass(ing *extensions.Ingress, class string) bool {
if class == "" {
return true
}
cc, _ := parser.GetStringAnnotation(ingressClassKey, ing)
if cc == "" {
return true
}
return cc == class
}

View file

@ -0,0 +1,50 @@
/*
Copyright 2015 The Kubernetes 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 controller
import (
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
)
func TestIsValidClass(t *testing.T) {
ing := &extensions.Ingress{
ObjectMeta: api.ObjectMeta{
Name: "foo",
Namespace: api.NamespaceDefault,
},
}
b := IsValidClass(ing, "")
if !b {
t.Error("Expected a valid class (missing annotation)")
}
data := map[string]string{}
data[ingressClassKey] = "custom"
ing.SetAnnotations(data)
b = IsValidClass(ing, "custom")
if !b {
t.Errorf("Expected valid class but %v returned", b)
}
b = IsValidClass(ing, "nginx")
if b {
t.Errorf("Expected invalid class but %v returned", b)
}
}

View file

@ -0,0 +1,60 @@
package defaults
// Backend defines the mandatory configuration that an Ingress controller must provide
// The reason of this requirements is the annotations are generic. If some implementation do not supports
// one or more annotations it just can provides defaults
type Backend struct {
// enables which HTTP codes should be passed for processing with the error_page directive
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_intercept_errors
// http://nginx.org/en/docs/http/ngx_http_core_module.html#error_page
// By default this is disabled
CustomHTTPErrors []int `structs:"custom-http-errors,-"`
// Defines a timeout for establishing a connection with a proxied server.
// It should be noted that this timeout cannot usually exceed 75 seconds.
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_connect_timeout
ProxyConnectTimeout int `structs:"proxy-connect-timeout"`
// Timeout in seconds for reading a response from the proxied server. The timeout is set only between
// two successive read operations, not for the transmission of the whole response
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout
ProxyReadTimeout int `structs:"proxy-read-timeout"`
// Timeout in seconds for transmitting a request to the proxied server. The timeout is set only between
// two successive write operations, not for the transmission of the whole request.
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_send_timeout
ProxySendTimeout int `structs:"proxy-send-timeout"`
// Sets the size of the buffer used for reading the first part of the response received from the
// proxied server. This part usually contains a small response header.
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffer_size)
ProxyBufferSize string `structs:"proxy-buffer-size"`
// Configures name servers used to resolve names of upstream servers into addresses
// http://nginx.org/en/docs/http/ngx_http_core_module.html#resolver
Resolver string `structs:"resolver"`
// SkipAccessLogURLs sets a list of URLs that should not appear in the NGINX access log
// This is useful with urls like `/health` or `health-check` that make "complex" reading the logs
// By default this list is empty
SkipAccessLogURLs []string `structs:"skip-access-log-urls,-"`
// Enables or disables the redirect (301) to the HTTPS port
SSLRedirect bool `structs:"ssl-redirect"`
// Number of unsuccessful attempts to communicate with the server that should happen in the
// duration set by the fail_timeout parameter to consider the server unavailable
// http://nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream
// Default: 0, ie use platform liveness probe
UpstreamMaxFails int `structs:"upstream-max-fails"`
// Time during which the specified number of unsuccessful attempts to communicate with
// the server should happen to consider the server unavailable
// http://nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream
// Default: 0, ie use platform liveness probe
UpstreamFailTimeout int `structs:"upstream-fail-timeout"`
// WhitelistSourceRange allows limiting access to certain client addresses
// http://nginx.org/en/docs/http/ngx_http_access_module.html
WhitelistSourceRange []string `structs:"whitelist-source-range,-"`
}

View file

@ -0,0 +1,119 @@
/*
Copyright 2015 The Kubernetes 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 status
import (
"encoding/json"
"os"
"time"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors"
client "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/leaderelection"
"k8s.io/kubernetes/pkg/client/leaderelection/resourcelock"
"k8s.io/kubernetes/pkg/client/record"
)
func getCurrentLeader(electionID, namespace string, c client.Interface) (string, *api.Endpoints, error) {
endpoints, err := c.Core().Endpoints(namespace).Get(electionID)
if err != nil {
return "", nil, err
}
val, found := endpoints.Annotations[resourcelock.LeaderElectionRecordAnnotationKey]
if !found {
return "", endpoints, nil
}
electionRecord := resourcelock.LeaderElectionRecord{}
if err = json.Unmarshal([]byte(val), &electionRecord); err != nil {
return "", nil, err
}
return electionRecord.HolderIdentity, endpoints, err
}
// NewElection creates an election. 'namespace'/'election' should be an existing Kubernetes Service
// 'id' is the id if this leader, should be unique.
func NewElection(electionID,
id,
namespace string,
ttl time.Duration,
callback func(leader string),
c client.Interface) (*leaderelection.LeaderElector, error) {
_, err := c.Core().Endpoints(namespace).Get(electionID)
if err != nil {
if errors.IsNotFound(err) {
_, err = c.Core().Endpoints(namespace).Create(&api.Endpoints{
ObjectMeta: api.ObjectMeta{
Name: electionID,
},
})
if err != nil && !errors.IsConflict(err) {
return nil, err
}
} else {
return nil, err
}
}
callbacks := leaderelection.LeaderCallbacks{
OnStartedLeading: func(stop <-chan struct{}) {
callback(id)
},
OnStoppedLeading: func() {
leader, _, err := getCurrentLeader(electionID, namespace, c)
if err != nil {
glog.Errorf("failed to get leader: %v", err)
// empty string means leader is unknown
callback("")
return
}
callback(leader)
},
}
broadcaster := record.NewBroadcaster()
hostname, err := os.Hostname()
if err != nil {
return nil, err
}
recorder := broadcaster.NewRecorder(api.EventSource{
Component: "ingress-leader-elector",
Host: hostname,
})
lock := resourcelock.EndpointsLock{
EndpointsMeta: api.ObjectMeta{Namespace: namespace, Name: electionID},
Client: c,
LockConfig: resourcelock.ResourceLockConfig{
Identity: id,
EventRecorder: recorder,
},
}
config := leaderelection.LeaderElectionConfig{
Lock: &lock,
LeaseDuration: ttl,
RenewDeadline: ttl / 2,
RetryPeriod: ttl / 4,
Callbacks: callbacks,
}
return leaderelection.NewLeaderElector(config)
}

View file

@ -0,0 +1,283 @@
/*
Copyright 2015 The Kubernetes 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 status
import (
"sort"
"sync"
"time"
"github.com/golang/glog"
cache_store "k8s.io/ingress/core/pkg/cache"
"k8s.io/ingress/core/pkg/k8s"
strings "k8s.io/ingress/core/pkg/strings"
"k8s.io/ingress/core/pkg/task"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/leaderelection"
"k8s.io/kubernetes/pkg/labels"
"k8s.io/kubernetes/pkg/util/wait"
)
const (
updateInterval = 30 * time.Second
)
// Sync ...
type Sync interface {
Run(stopCh <-chan struct{})
Shutdown()
}
// Config ...
type Config struct {
Client clientset.Interface
PublishService string
IngressLister cache_store.StoreToIngressLister
}
// statusSync keeps the status IP in each Ingress rule updated executing a periodic check
// in all the defined rules. To simplify the process leader election is used so the update
// is executed only in one node (Ingress controllers can be scaled to more than one)
// If the controller is running with the flag --publish-service (with a valid service)
// the IP address behind the service is used, if not the source is the IP/s of the node/s
type statusSync struct {
Config
// pod contains runtime information about this pod
pod *k8s.PodInfo
elector *leaderelection.LeaderElector
// workqueue used to keep in sync the status IP/s
// in the Ingress rules
syncQueue *task.Queue
runLock *sync.Mutex
}
// Run starts the loop to keep the status in sync
func (s statusSync) Run(stopCh <-chan struct{}) {
go wait.Forever(s.elector.Run, 0)
go s.run()
go s.syncQueue.Run(time.Second, stopCh)
<-stopCh
}
// Shutdown stop the sync. In case the instance is the leader it will remove the current IP
// if there is no other instances running.
func (s statusSync) Shutdown() {
go s.syncQueue.Shutdown()
// remove IP from Ingress
if !s.elector.IsLeader() {
return
}
glog.Infof("updating status of Ingress rules (remove)")
ips, err := s.getRunningIPs()
if err != nil {
glog.Errorf("error obtaining running IPs: %v", ips)
return
}
if len(ips) > 1 {
// leave the job to the next leader
glog.Infof("leaving status update for next leader (%v)", len(ips))
return
}
glog.Infof("removing my ip (%v)", ips)
s.updateStatus([]api.LoadBalancerIngress{})
}
func (s *statusSync) run() {
err := wait.PollInfinite(updateInterval, func() (bool, error) {
if s.syncQueue.IsShuttingDown() {
return true, nil
}
// send a dummy object to the queue to force a sync
s.syncQueue.Enqueue("dummy")
return false, nil
})
if err != nil {
glog.Errorf("error waiting shutdown: %v", err)
}
}
func (s *statusSync) sync(key interface{}) error {
s.runLock.Lock()
defer s.runLock.Unlock()
if !s.elector.IsLeader() {
glog.V(2).Infof("skipping Ingress status update (I am not the current leader)")
return nil
}
ips, err := s.getRunningIPs()
if err != nil {
return err
}
s.updateStatus(sliceToStatus(ips))
return nil
}
// callback invoked function when a new leader is elected
func (s *statusSync) callback(leader string) {
if s.syncQueue.IsShuttingDown() {
return
}
glog.V(2).Infof("new leader elected (%v)", leader)
if leader == s.pod.Name {
glog.V(2).Infof("I am the new status update leader")
}
}
func (s statusSync) keyfunc(input interface{}) (interface{}, error) {
return input, nil
}
// NewStatusSyncer returns a new Sync instance
func NewStatusSyncer(config Config) Sync {
pod, err := k8s.GetPodDetails(config.Client)
if err != nil {
glog.Fatalf("unexpected error obtaining pod information: %v", err)
}
st := statusSync{
pod: pod,
runLock: &sync.Mutex{},
Config: config,
}
st.syncQueue = task.NewCustomTaskQueue(st.sync, st.keyfunc)
le, err := NewElection("ingress-controller-leader",
pod.Name, pod.Namespace, 30*time.Second,
st.callback, config.Client)
if err != nil {
glog.Fatalf("unexpected error starting leader election: %v", err)
}
st.elector = le
return st
}
func (s *statusSync) getRunningIPs() ([]string, error) {
if s.PublishService != "" {
ns, name, _ := k8s.ParseNameNS(s.PublishService)
svc, err := s.Client.Core().Services(ns).Get(name)
if err != nil {
return nil, err
}
ips := []string{}
for _, ip := range svc.Status.LoadBalancer.Ingress {
ips = append(ips, ip.IP)
}
return ips, nil
}
// get information about all the pods running the ingress controller
pods, err := s.Client.Core().Pods(s.pod.Namespace).List(api.ListOptions{
LabelSelector: labels.SelectorFromSet(s.pod.Labels),
})
if err != nil {
return nil, err
}
ips := []string{}
for _, pod := range pods.Items {
if !strings.StringInSlice(pod.Status.HostIP, ips) {
ips = append(ips, pod.Status.HostIP)
}
}
return ips, nil
}
func sliceToStatus(ips []string) []api.LoadBalancerIngress {
lbi := []api.LoadBalancerIngress{}
for _, ip := range ips {
lbi = append(lbi, api.LoadBalancerIngress{IP: ip})
}
sort.Sort(loadBalancerIngressByIP(lbi))
return lbi
}
func (s *statusSync) updateStatus(newIPs []api.LoadBalancerIngress) {
ings := s.IngressLister.List()
var wg sync.WaitGroup
wg.Add(len(ings))
for _, cur := range ings {
ing := cur.(*extensions.Ingress)
go func(wg *sync.WaitGroup) {
defer wg.Done()
ingClient := s.Client.Extensions().Ingresses(ing.Namespace)
currIng, err := ingClient.Get(ing.Name)
if err != nil {
glog.Errorf("unexpected error searching Ingress %v/%v: %v", ing.Namespace, ing.Name, err)
return
}
curIPs := ing.Status.LoadBalancer.Ingress
sort.Sort(loadBalancerIngressByIP(curIPs))
if ingressSliceEqual(newIPs, curIPs) {
glog.V(3).Infof("skipping update of Ingress %v/%v (there is no change)", currIng.Namespace, currIng.Name)
return
}
glog.Infof("updating Ingress %v/%v status to %v", currIng.Namespace, currIng.Name, newIPs)
currIng.Status.LoadBalancer.Ingress = newIPs
_, err = ingClient.UpdateStatus(currIng)
if err != nil {
glog.Warningf("error updating ingress rule: %v", err)
}
}(&wg)
}
wg.Wait()
}
func ingressSliceEqual(lhs, rhs []api.LoadBalancerIngress) bool {
if len(lhs) != len(rhs) {
return false
}
for i := range lhs {
if lhs[i].IP != rhs[i].IP {
return false
}
if lhs[i].Hostname != rhs[i].Hostname {
return false
}
}
return true
}
// loadBalancerIngressByIP sorts LoadBalancerIngress using the field IP
type loadBalancerIngressByIP []api.LoadBalancerIngress
func (c loadBalancerIngressByIP) Len() int { return len(c) }
func (c loadBalancerIngressByIP) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c loadBalancerIngressByIP) Less(i, j int) bool {
return c[i].IP < c[j].IP
}

214
core/pkg/ingress/types.go Normal file
View file

@ -0,0 +1,214 @@
/*
Copyright 2015 The Kubernetes 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 ingress
import (
"os/exec"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/ingress/core/pkg/ingress/annotations/auth"
"k8s.io/ingress/core/pkg/ingress/annotations/authreq"
"k8s.io/ingress/core/pkg/ingress/annotations/authtls"
"k8s.io/ingress/core/pkg/ingress/annotations/ipwhitelist"
"k8s.io/ingress/core/pkg/ingress/annotations/proxy"
"k8s.io/ingress/core/pkg/ingress/annotations/ratelimit"
"k8s.io/ingress/core/pkg/ingress/annotations/rewrite"
"k8s.io/ingress/core/pkg/ingress/defaults"
)
var (
// DefaultSSLDirectory defines the location where the SSL certificates will be generated
DefaultSSLDirectory = "/ingress-controller/ssl"
)
// Controller ...
type Controller interface {
// Start returns the command is executed to start the backend.
// The command must run in foreground.
Start()
// Stop stops the backend
Stop() error
// Restart reload the backend with the a configuration file returning
// the combined output of Stdout and Stderr
Restart(data []byte) ([]byte, error)
// Tests returns a commands that checks if the configuration file is valid
// Example: nginx -t -c <file>
Test(file string) *exec.Cmd
// OnUpdate callback invoked from the sync queue https://k8s.io/ingress/core/blob/master/pkg/ingress/controller/controller.go#L355
// when an update occurs. This is executed frequently because Ingress
// controllers watches changes in:
// - Ingresses: main work
// - Secrets: referenced from Ingress rules with TLS configured
// - ConfigMaps: where the controller reads custom configuration
// - Services: referenced from Ingress rules and required to obtain
// information about ports and annotations
// - Endpoints: referenced from Services and what the backend uses
// to route traffic
//
// ConfigMap content of --configmap
// Configuration returns the translation from Ingress rules containing
// information about all the upstreams (service endpoints ) "virtual"
// servers (FQDN)
// and all the locations inside each server. Each location contains
// information about all the annotations were configured
// https://k8s.io/ingress/core/blob/master/pkg/ingress/types.go#L48
OnUpdate(*api.ConfigMap, Configuration) ([]byte, error)
// UpstreamDefaults returns the minimum settings required to configure the
// communication to upstream servers (endpoints)
UpstreamDefaults() defaults.Backend
// IsReloadRequired checks if the backend must be reloaded or not.
// The parameter contains the new rendered template
IsReloadRequired([]byte) bool
// Info returns information about the ingress controller
// This can include build version, repository, etc.
Info() string
}
// Configuration describes
type Configuration struct {
HealthzURL string
Upstreams []*Upstream
Servers []*Server
TCPUpstreams []*Location
UDPUpstreams []*Location
PassthroughUpstreams []*SSLPassthroughUpstreams
}
// Upstream describes an upstream server (endpoint)
type Upstream struct {
// Secure indicates if the communication with the en
Secure bool
// Name represents an unique api.Service name formatted
// as <namespace>-<name>-<port>
Name string
// Backends
Backends []UpstreamServer
}
// SSLPassthroughUpstreams describes an SSL upstream server configured
// as passthrough (no TLS termination)
type SSLPassthroughUpstreams struct {
Upstream
Host string
}
// UpstreamByNameServers sorts upstreams by name
type UpstreamByNameServers []*Upstream
func (c UpstreamByNameServers) Len() int { return len(c) }
func (c UpstreamByNameServers) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c UpstreamByNameServers) Less(i, j int) bool {
return c[i].Name < c[j].Name
}
// UpstreamServer describes a server in an upstream
type UpstreamServer struct {
// Address IP address of the endpoint
Address string
Port string
// MaxFails returns the maximum number of check failures
// allowed before this should be considered dow.
// Setting 0 indicates that the check is performed by a Kubernetes probe
MaxFails int
FailTimeout int
}
// Server describes a virtual server
type Server struct {
Name string
SSL bool
SSLPassthrough bool
SSLCertificate string
//SSLCertificateKey string
SSLPemChecksum string
Locations []*Location
}
// Location describes a server location
type Location struct {
IsDefBackend bool
SecureUpstream bool
EnableCORS bool
Path string
Upstream Upstream
BasicDigestAuth auth.BasicDigest
RateLimit ratelimit.RateLimit
Redirect rewrite.Redirect
Whitelist ipwhitelist.SourceRange
ExternalAuth authreq.External
Proxy proxy.Configuration
CertificateAuth authtls.SSLCert
}
// UpstreamServerByAddrPort sorts upstream servers by address and port
type UpstreamServerByAddrPort []UpstreamServer
func (c UpstreamServerByAddrPort) Len() int { return len(c) }
func (c UpstreamServerByAddrPort) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c UpstreamServerByAddrPort) Less(i, j int) bool {
iName := c[i].Address
jName := c[j].Address
if iName != jName {
return iName < jName
}
iU := c[i].Port
jU := c[j].Port
return iU < jU
}
// ServerByName sorts server by name
type ServerByName []*Server
func (c ServerByName) Len() int { return len(c) }
func (c ServerByName) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c ServerByName) Less(i, j int) bool {
return c[i].Name < c[j].Name
}
// LocationByPath sorts location by path
// Location / is the last one
type LocationByPath []*Location
func (c LocationByPath) Len() int { return len(c) }
func (c LocationByPath) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c LocationByPath) Less(i, j int) bool {
return c[i].Path > c[j].Path
}
// SSLCert describes a SSL certificate to be used in a server
type SSLCert struct {
api.ObjectMeta
//CertFileName string
//KeyFileName string
CAFileName string
// PemFileName contains the path to the file with the certificate and key concatenated
PemFileName string
// PemSHA contains the sha1 of the pem file.
// This is used to detect changes in the secret that contains the certificates
PemSHA string
// CN contains all the common names defined in the SSL certificate
CN []string
}
// GetObjectKind implements the ObjectKind interface as a noop
func (s SSLCert) GetObjectKind() unversioned.ObjectKind { return unversioned.EmptyObjectKind }

110
core/pkg/k8s/main.go Normal file
View file

@ -0,0 +1,110 @@
/*
Copyright 2015 The Kubernetes 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 k8s
import (
"fmt"
"os"
"strings"
"k8s.io/kubernetes/pkg/api"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
)
// IsValidService checks if exists a service with the specified name
func IsValidService(kubeClient clientset.Interface, name string) (*api.Service, error) {
ns, name, err := ParseNameNS(name)
if err != nil {
return nil, err
}
return kubeClient.Core().Services(ns).Get(name)
}
// IsValidSecret checks if exists a secret with the specified name
func IsValidSecret(kubeClient clientset.Interface, name string) (*api.Secret, error) {
ns, name, err := ParseNameNS(name)
if err != nil {
return nil, err
}
return kubeClient.Core().Secrets(ns).Get(name)
}
// ParseNameNS parses a string searching a namespace and name
func ParseNameNS(input string) (string, string, error) {
nsName := strings.Split(input, "/")
if len(nsName) != 2 {
return "", "", fmt.Errorf("invalid format (namespace/name) found in '%v'", input)
}
return nsName[0], nsName[1], nil
}
// GetNodeIP returns the IP address of a node in the cluster
func GetNodeIP(kubeClient clientset.Interface, name string) string {
var externalIP string
node, err := kubeClient.Core().Nodes().Get(name)
if err != nil {
return externalIP
}
for _, address := range node.Status.Addresses {
if address.Type == api.NodeExternalIP {
if address.Address != "" {
externalIP = address.Address
break
}
}
if externalIP == "" && address.Type == api.NodeLegacyHostIP {
externalIP = address.Address
}
}
return externalIP
}
// PodInfo contains runtime information about the pod running the Ingres controller
type PodInfo struct {
Name string
Namespace string
NodeIP string
// Labels selectors of the running pod
// This is used to search for other Ingress controller pods
Labels map[string]string
}
// GetPodDetails returns runtime information about the pod:
// name, namespace and IP of the node where it is running
func GetPodDetails(kubeClient clientset.Interface) (*PodInfo, error) {
podName := os.Getenv("POD_NAME")
podNs := os.Getenv("POD_NAMESPACE")
if podName == "" && podNs == "" {
return nil, fmt.Errorf("unable to get POD information (missing POD_NAME or POD_NAMESPACE environment variable")
}
pod, _ := kubeClient.Core().Pods(podNs).Get(podName)
if pod == nil {
return nil, fmt.Errorf("unable to get POD information")
}
return &PodInfo{
Name: podName,
Namespace: podNs,
NodeIP: GetNodeIP(kubeClient, pod.Spec.NodeName),
Labels: pod.GetLabels(),
}, nil
}

116
core/pkg/k8s/main_test.go Normal file
View file

@ -0,0 +1,116 @@
/*
Copyright 2015 The Kubernetes 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 k8s
import (
"testing"
"k8s.io/kubernetes/pkg/api"
testclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
)
func TestParseNameNS(t *testing.T) {
tests := []struct {
title string
input string
ns string
name string
expErr bool
}{
{"empty string", "", "", "", true},
{"demo", "demo", "", "", true},
{"kube-system", "kube-system", "", "", true},
{"default/kube-system", "default/kube-system", "default", "kube-system", false},
}
for _, test := range tests {
ns, name, err := ParseNameNS(test.input)
if test.expErr {
if err == nil {
t.Errorf("%v: expected error but retuned nil", test.title)
}
continue
}
if test.ns != ns {
t.Errorf("%v: expected %v but retuned %v", test.title, test.ns, ns)
}
if test.name != name {
t.Errorf("%v: expected %v but retuned %v", test.title, test.name, name)
}
}
}
func TestIsValidService(t *testing.T) {
fk := testclient.NewSimpleClientset(&api.Service{
ObjectMeta: api.ObjectMeta{
Namespace: api.NamespaceDefault,
Name: "demo",
},
})
_, err := IsValidService(fk, "")
if err == nil {
t.Errorf("expected error but retuned nil")
}
s, err := IsValidService(fk, "default/demo")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if s == nil {
t.Errorf("expected a Service but retuned nil")
}
fk = testclient.NewSimpleClientset()
s, err = IsValidService(fk, "default/demo")
if err == nil {
t.Errorf("expected an error but retuned nil")
}
if s != nil {
t.Errorf("unexpected Service returned: %v", s)
}
}
func TestIsValidSecret(t *testing.T) {
fk := testclient.NewSimpleClientset(&api.Secret{
ObjectMeta: api.ObjectMeta{
Namespace: api.NamespaceDefault,
Name: "demo",
},
})
_, err := IsValidSecret(fk, "")
if err == nil {
t.Errorf("expected error but retuned nil")
}
s, err := IsValidSecret(fk, "default/demo")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if s == nil {
t.Errorf("expected a Secret but retuned nil")
}
fk = testclient.NewSimpleClientset()
s, err = IsValidSecret(fk, "default/demo")
if err == nil {
t.Errorf("expected an error but retuned nil")
}
if s != nil {
t.Errorf("unexpected Secret returned: %v", s)
}
}

52
core/pkg/net/dns/dns.go Normal file
View file

@ -0,0 +1,52 @@
/*
Copyright 2015 The Kubernetes 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 dns
import (
"io/ioutil"
"strings"
"github.com/golang/glog"
)
// GetSystemNameServers returns the list of nameservers located in the file /etc/resolv.conf
func GetSystemNameServers() ([]string, error) {
var nameservers []string
file, err := ioutil.ReadFile("/etc/resolv.conf")
if err != nil {
return nameservers, err
}
// Lines of the form "nameserver 1.2.3.4" accumulate.
lines := strings.Split(string(file), "\n")
for l := range lines {
trimmed := strings.TrimSpace(lines[l])
if strings.HasPrefix(trimmed, "#") {
continue
}
fields := strings.Fields(trimmed)
if len(fields) == 0 {
continue
}
if fields[0] == "nameserver" {
nameservers = append(nameservers, fields[1:]...)
}
}
glog.V(3).Infof("nameservers to use: %v", nameservers)
return nameservers, nil
}

View file

@ -0,0 +1,31 @@
/*
Copyright 2015 The Kubernetes 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 dns
import (
"testing"
)
func TestGetDNSServers(t *testing.T) {
s, err := GetSystemNameServers()
if err != nil {
t.Fatalf("unexpected error reading /etc/resolv.conf file: %v", err)
}
if len(s) < 1 {
t.Error("expected at least 1 nameserver in /etc/resolv.conf")
}
}

179
core/pkg/net/ssl/ssl.go Normal file
View file

@ -0,0 +1,179 @@
/*
Copyright 2015 The Kubernetes 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 ssl
import (
"crypto/sha1"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"github.com/golang/glog"
"k8s.io/ingress/core/pkg/ingress"
)
// AddOrUpdateCertAndKey creates a .pem file wth the cert and the key with the specified name
func AddOrUpdateCertAndKey(name string, cert, key, ca []byte) (*ingress.SSLCert, error) {
pemName := fmt.Sprintf("%v.pem", name)
pemFileName := fmt.Sprintf("%v/%v", ingress.DefaultSSLDirectory, pemName)
tempPemFile, err := ioutil.TempFile("", pemName)
if err != nil {
return nil, fmt.Errorf("could not create temp pem file %v: %v", tempPemFile.Name(), err)
}
_, err = tempPemFile.Write(cert)
if err != nil {
return nil, fmt.Errorf("could not write to pem file %v: %v", tempPemFile.Name(), err)
}
_, err = tempPemFile.Write([]byte("\n"))
if err != nil {
return nil, fmt.Errorf("could not write to pem file %v: %v", tempPemFile.Name(), err)
}
_, err = tempPemFile.Write(key)
if err != nil {
return nil, fmt.Errorf("could not write to pem file %v: %v", tempPemFile.Name(), err)
}
err = tempPemFile.Close()
if err != nil {
return nil, fmt.Errorf("could not close temp pem file %v: %v", tempPemFile.Name(), err)
}
pemCerts, err := ioutil.ReadFile(tempPemFile.Name())
if err != nil {
return nil, err
}
pembBock, _ := pem.Decode(pemCerts)
if pembBock == nil {
return nil, fmt.Errorf("No valid PEM formatted block found")
}
pemCert, err := x509.ParseCertificate(pembBock.Bytes)
if err != nil {
return nil, err
}
cn := []string{pemCert.Subject.CommonName}
if len(pemCert.DNSNames) > 0 {
cn = append(cn, pemCert.DNSNames...)
}
err = os.Rename(tempPemFile.Name(), pemFileName)
if err != nil {
return nil, fmt.Errorf("could not move temp pem file %v to destination %v: %v", tempPemFile.Name(), pemFileName, err)
}
if len(ca) > 0 {
bundle := x509.NewCertPool()
bundle.AppendCertsFromPEM(ca)
opts := x509.VerifyOptions{
Roots: bundle,
}
_, err := pemCert.Verify(opts)
if err != nil {
return nil, fmt.Errorf("failed to verify certificate chain: \n\t%s\n", err)
}
caName := fmt.Sprintf("ca-%v.pem", name)
caFileName := fmt.Sprintf("%v/%v", ingress.DefaultSSLDirectory, caName)
f, err := os.Create(caFileName)
if err != nil {
return nil, fmt.Errorf("could not create ca pem file %v: %v", caFileName, err)
}
defer f.Close()
_, err = f.Write(ca)
if err != nil {
return nil, fmt.Errorf("could not create ca pem file %v: %v", caFileName, err)
}
f.Write([]byte("\n"))
return &ingress.SSLCert{
CAFileName: caFileName,
PemFileName: pemFileName,
PemSHA: pemSHA1(pemFileName),
CN: cn,
}, nil
}
return &ingress.SSLCert{
PemFileName: pemFileName,
PemSHA: pemSHA1(pemFileName),
CN: cn,
}, nil
}
// SearchDHParamFile iterates all the secrets mounted inside the /etc/nginx-ssl directory
// in order to find a file with the name dhparam.pem. If such file exists it will
// returns the path. If not it just returns an empty string
func SearchDHParamFile(baseDir string) string {
files, _ := ioutil.ReadDir(baseDir)
for _, file := range files {
if !file.IsDir() {
continue
}
dhPath := fmt.Sprintf("%v/%v/dhparam.pem", baseDir, file.Name())
if _, err := os.Stat(dhPath); err == nil {
glog.Infof("using file '%v' for parameter ssl_dhparam", dhPath)
return dhPath
}
}
glog.Warning("no file dhparam.pem found in secrets")
return ""
}
// pemSHA1 returns the SHA1 of a pem file. This is used to
// reload NGINX in case a secret with a SSL certificate changed.
func pemSHA1(filename string) string {
hasher := sha1.New()
s, err := ioutil.ReadFile(filename)
if err != nil {
return ""
}
hasher.Write(s)
return hex.EncodeToString(hasher.Sum(nil))
}
const (
snakeOilPem = "/etc/ssl/certs/ssl-cert-snakeoil.pem"
snakeOilKey = "/etc/ssl/private/ssl-cert-snakeoil.key"
)
// GetFakeSSLCert returns the snake oil ssl certificate created by the command
// make-ssl-cert generate-default-snakeoil --force-overwrite
func GetFakeSSLCert() ([]byte, []byte) {
cert, err := ioutil.ReadFile(snakeOilPem)
if err != nil {
return nil, nil
}
key, err := ioutil.ReadFile(snakeOilKey)
if err != nil {
return nil, nil
}
return cert, key
}

View file

@ -14,20 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package nginx
package ssl
import (
"encoding/base64"
"fmt"
"os"
"io/ioutil"
"testing"
"time"
"k8s.io/contrib/ingress/controllers/nginx/nginx/config"
"k8s.io/ingress/core/pkg/ingress"
)
func TestAddOrUpdateCertAndKey(t *testing.T) {
config.SSLDirectory = os.TempDir()
td, err := ioutil.TempDir("", "ssl")
if err != nil {
t.Fatalf("Unexpected error creating temporal directory: %v", err)
}
ingress.DefaultSSLDirectory = td
// openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /tmp/tls.key -out /tmp/tls.crt -subj "/CN=echoheaders/O=echoheaders"
tlsCrt := "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURhakNDQWxLZ0F3SUJBZ0lKQUxHUXR5VVBKTFhYTUEwR0NTcUdTSWIzRFFFQkJRVUFNQ3d4RkRBU0JnTlYKQkFNVEMyVmphRzlvWldGa1pYSnpNUlF3RWdZRFZRUUtFd3RsWTJodmFHVmhaR1Z5Y3pBZUZ3MHhOakF6TXpFeQpNekU1TkRoYUZ3MHhOekF6TXpFeU16RTVORGhhTUN3eEZEQVNCZ05WQkFNVEMyVmphRzlvWldGa1pYSnpNUlF3CkVnWURWUVFLRXd0bFkyaHZhR1ZoWkdWeWN6Q0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0MKZ2dFQkFONzVmS0N5RWwxanFpMjUxTlNabDYzeGQweG5HMHZTVjdYL0xxTHJveVNraW5nbnI0NDZZWlE4UEJWOAo5TUZzdW5RRGt1QVoyZzA3NHM1YWhLSm9BRGJOMzhld053RXNsVDJkRzhRTUw0TktrTUNxL1hWbzRQMDFlWG1PCmkxR2txZFA1ZUExUHlPZCtHM3gzZmxPN2xOdmtJdHVHYXFyc0tvMEhtMHhqTDVtRUpwWUlOa0tGSVhsWWVLZS8KeHRDR25CU2tLVHFMTG0yeExKSGFFcnJpaDZRdkx4NXF5U2gzZTU2QVpEcTlkTERvcWdmVHV3Z2IzekhQekc2NwppZ0E0dkYrc2FRNHpZUE1NMHQyU1NiVkx1M2pScWNvL3lxZysrOVJBTTV4bjRubnorL0hUWFhHKzZ0RDBaeGI1CmVVRDNQakVhTnlXaUV2dTN6UFJmdysyNURMY0NBd0VBQWFPQmpqQ0JpekFkQmdOVkhRNEVGZ1FVcktMZFhHeUUKNUlEOGRvd2lZNkdzK3dNMHFKc3dYQVlEVlIwakJGVXdVNEFVcktMZFhHeUU1SUQ4ZG93aVk2R3Mrd00wcUp1aApNS1F1TUN3eEZEQVNCZ05WQkFNVEMyVmphRzlvWldGa1pYSnpNUlF3RWdZRFZRUUtFd3RsWTJodmFHVmhaR1Z5CmM0SUpBTEdRdHlVUEpMWFhNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ2dFQkFNZVMKMHFia3VZa3Z1enlSWmtBeE1PdUFaSDJCK0Evb3N4ODhFRHB1ckV0ZWN5RXVxdnRvMmpCSVdCZ2RkR3VBYU5jVQorUUZDRm9NakJOUDVWVUxIWVhTQ3VaczN2Y25WRDU4N3NHNlBaLzhzbXJuYUhTUjg1ZVpZVS80bmFyNUErdWErClIvMHJrSkZnOTlQSmNJd3JmcWlYOHdRcWdJVVlLNE9nWEJZcUJRL0VZS2YvdXl6UFN3UVZYRnVJTTZTeDBXcTYKTUNML3d2RlhLS0FaWDBqb3J4cHRjcldkUXNCcmYzWVRnYmx4TE1sN20zL2VuR1drcEhDUHdYeVRCOC9rRkw3SApLL2ZHTU1NWGswUkVSbGFPM1hTSUhrZUQ2SXJiRnRNV3R1RlJwZms2ZFA2TXlMOHRmTmZ6a3VvUHVEWUFaWllWCnR1NnZ0c0FRS0xWb0pGaGV0b1k9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
tlsKey := "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBM3ZsOG9MSVNYV09xTGJuVTFKbVhyZkYzVEdjYlM5Slh0Zjh1b3V1akpLU0tlQ2V2CmpqcGhsRHc4Rlh6MHdXeTZkQU9TNEJuYURUdml6bHFFb21nQU5zM2Z4N0EzQVN5VlBaMGJ4QXd2ZzBxUXdLcjkKZFdqZy9UVjVlWTZMVWFTcDAvbDREVS9JNTM0YmZIZCtVN3VVMitRaTI0WnFxdXdxalFlYlRHTXZtWVFtbGdnMgpRb1VoZVZoNHA3L0cwSWFjRktRcE9vc3ViYkVza2RvU3V1S0hwQzh2SG1ySktIZDdub0JrT3IxMHNPaXFCOU83CkNCdmZNYy9NYnJ1S0FEaThYNnhwRGpOZzh3elMzWkpKdFV1N2VOR3B5ai9LcUQ3NzFFQXpuR2ZpZWZQNzhkTmQKY2I3cTBQUm5Gdmw1UVBjK01SbzNKYUlTKzdmTTlGL0Q3YmtNdHdJREFRQUJBb0lCQUViNmFEL0hMNjFtMG45bgp6bVkyMWwvYW83MUFmU0h2dlZnRCtWYUhhQkY4QjFBa1lmQUdpWlZrYjBQdjJRSFJtTERoaWxtb0lROWhadHVGCldQOVIxKythTFlnbGdmenZzanBBenR2amZTUndFaEFpM2pnSHdNY1p4S2Q3UnNJZ2hxY2huS093S0NYNHNNczQKUnBCbEFBZlhZWGs4R3F4NkxUbGptSDRDZk42QzZHM1EwTTlLMUxBN2lsck1Na3hwcngxMnBlVTNkczZMVmNpOQptOFdBL21YZ2I0c3pEbVNaWVpYRmNZMEhYNTgyS3JKRHpQWEVJdGQwZk5wd3I0eFIybzdzMEwvK2RnZCtqWERjCkh2SDBKZ3NqODJJaTIxWGZGM2tST3FxR3BKNmhVcncxTUZzVWRyZ29GL3pFck0vNWZKMDdVNEhodGFlalVzWTIKMFJuNXdpRUNnWUVBKzVUTVRiV084Wkg5K2pIdVQwc0NhZFBYcW50WTZYdTZmYU04Tm5CZWNoeTFoWGdlQVN5agpSWERlZGFWM1c0SjU5eWxIQ3FoOVdseVh4cDVTWWtyQU41RnQ3elFGYi91YmorUFIyWWhMTWZpYlBSYlYvZW1MCm5YaGF6MmtlNUUxT1JLY0x6QUVwSmpuZGQwZlZMZjdmQzFHeStnS2YyK3hTY1hjMHJqRE5iNGtDZ1lFQTR1UVEKQk91TlJQS3FKcDZUZS9zUzZrZitHbEpjQSs3RmVOMVlxM0E2WEVZVm9ydXhnZXQ4a2E2ZEo1QjZDOWtITGtNcQpwdnFwMzkxeTN3YW5uWC9ONC9KQlU2M2RxZEcyd1BWRUQ0REduaE54Qm1oaWZpQ1I0R0c2ZnE4MUV6ZE1vcTZ4CklTNHA2RVJaQnZkb1RqNk9pTHl6aUJMckpxeUhIMWR6c0hGRlNqOENnWUVBOWlSSEgyQ2JVazU4SnVYak8wRXcKUTBvNG4xdS9TZkQ4TFNBZ01VTVBwS1hpRTR2S0Qyd1U4a1BUNDFiWXlIZUh6UUpkdDFmU0RTNjZjR0ZHU1ZUSgphNVNsOG5yN051ejg3bkwvUmMzTGhFQ3Y0YjBOOFRjbW1oSy9CbDdiRXBOd0dFczNoNGs3TVdNOEF4QU15c3VxCmZmQ1pJM0tkNVJYNk0zbGwyV2QyRjhFQ2dZQlQ5RU9oTG0vVmhWMUVjUVR0cVZlMGJQTXZWaTVLSGozZm5UZkUKS0FEUVIvYVZncElLR3RLN0xUdGxlbVpPbi8yeU5wUS91UnpHZ3pDUUtldzNzU1RFSmMzYVlzbFVudzdhazJhZAp2ZTdBYXowMU84YkdHTk1oamNmdVBIS05LN2Nsc3pKRHJzcys4SnRvb245c0JHWEZYdDJuaWlpTTVPWVN5TTg4CkNJMjFEUUtCZ0hEQVRZbE84UWlDVWFBQlVqOFBsb1BtMDhwa3cyc1VmQW0xMzJCY00wQk9BN1hqYjhtNm1ManQKOUlteU5kZ2ZiM080UjlKVUxTb1pZSTc1dUxIL3k2SDhQOVlpWHZOdzMrTXl6VFU2b2d1YU8xSTNya2pna29NeAo5cU5pYlJFeGswS1A5MVZkckVLSEdHZEFwT05ES1N4VzF3ektvbUxHdmtYSTVKV05KRXFkCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg=="
@ -43,10 +48,8 @@ func TestAddOrUpdateCertAndKey(t *testing.T) {
t.Fatalf("Unexpected error: %+v", err)
}
ngx := &Manager{}
name := fmt.Sprintf("test-%v", time.Now().UnixNano())
ngxCert, err := ngx.AddOrUpdateCertAndKey(name, string(dCrt), string(dKey))
ngxCert, err := AddOrUpdateCertAndKey(name, dCrt, dKey, []byte{})
if err != nil {
t.Fatalf("unexpected error checking SSL certificate: %v", err)
}

View file

@ -0,0 +1,27 @@
/*
Copyright 2015 The Kubernetes 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 strings
// StringInSlice check whether string a is a member of slice.
func StringInSlice(a string, slice []string) bool {
for _, b := range slice {
if b == a {
return true
}
}
return false
}

123
core/pkg/task/queue.go Normal file
View file

@ -0,0 +1,123 @@
/*
Copyright 2015 The Kubernetes 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 task
import (
"fmt"
"time"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/client/cache"
"k8s.io/kubernetes/pkg/util/wait"
"k8s.io/kubernetes/pkg/util/workqueue"
)
var (
keyFunc = cache.DeletionHandlingMetaNamespaceKeyFunc
)
// Queue manages a work queue through an independent worker that
// invokes the given sync function for every work item inserted.
type Queue struct {
// queue is the work queue the worker polls
queue workqueue.RateLimitingInterface
// sync is called for each item in the queue
sync func(interface{}) error
// workerDone is closed when the worker exits
workerDone chan struct{}
fn func(obj interface{}) (interface{}, error)
}
// Run ...
func (t *Queue) Run(period time.Duration, stopCh <-chan struct{}) {
wait.Until(t.worker, period, stopCh)
}
// Enqueue enqueues ns/name of the given api object in the task queue.
func (t *Queue) Enqueue(obj interface{}) {
glog.V(3).Infof("queuing item %v", obj)
key, err := t.fn(obj)
if err != nil {
glog.Errorf("%v", err)
return
}
t.queue.Add(key)
}
func (t *Queue) defaultKeyFunc(obj interface{}) (interface{}, error) {
key, err := keyFunc(obj)
if err != nil {
return "", fmt.Errorf("could not get key for object %+v: %v", obj, err)
}
return key, nil
}
// worker processes work in the queue through sync.
func (t *Queue) worker() {
for {
key, quit := t.queue.Get()
if quit {
close(t.workerDone)
return
}
glog.V(3).Infof("syncing %v", key)
if err := t.sync(key); err != nil {
glog.Warningf("requeuing %v, err %v", key, err)
t.queue.AddRateLimited(key)
} else {
t.queue.Forget(key)
}
t.queue.Done(key)
}
}
// Shutdown shuts down the work queue and waits for the worker to ACK
func (t *Queue) Shutdown() {
t.queue.ShutDown()
<-t.workerDone
}
// IsShuttingDown returns if the method Shutdown was invoked
func (t *Queue) IsShuttingDown() bool {
return t.queue.ShuttingDown()
}
// NewTaskQueue creates a new task queue with the given sync function.
// The sync function is called for every element inserted into the queue.
func NewTaskQueue(syncFn func(interface{}) error) *Queue {
return NewCustomTaskQueue(syncFn, nil)
}
// NewCustomTaskQueue ...
func NewCustomTaskQueue(syncFn func(interface{}) error, fn func(interface{}) (interface{}, error)) *Queue {
q := &Queue{
queue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
sync: syncFn,
workerDone: make(chan struct{}),
fn: fn,
}
if fn == nil {
q.fn = q.defaultKeyFunc
}
return q
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package template
package watch
import (
"log"
@ -23,28 +23,31 @@ import (
"gopkg.in/fsnotify.v1"
)
type fileWatcher struct {
// FileWatcher defines a watch over a file
type FileWatcher struct {
file string
watcher *fsnotify.Watcher
// onEvent callback to be invoked after the file being watched changes
onEvent func()
}
func newFileWatcher(file string, onEvent func()) (fileWatcher, error) {
fw := fileWatcher{
// NewFileWatcher creates a new FileWatcher
func NewFileWatcher(file string, onEvent func()) (FileWatcher, error) {
fw := FileWatcher{
file: file,
onEvent: onEvent,
}
err := fw.watch()
return fw, err
}
func (f fileWatcher) close() error {
// Close ends the watch
func (f FileWatcher) Close() error {
return f.watcher.Close()
}
// watch creates a fsnotify watcher for a file and create of write events
func (f *fileWatcher) watch() error {
func (f *FileWatcher) watch() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err

Some files were not shown because too many files have changed in this diff Show more