/* 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 ( "bytes" "encoding/base64" "encoding/json" "fmt" "os/exec" "strings" text_template "text/template" "github.com/golang/glog" "k8s.io/ingress/core/pkg/ingress" "k8s.io/ingress/core/pkg/watch" ) const ( slash = "/" defBufferSize = 65535 ) // Template ... type Template struct { tmpl *text_template.Template fw watch.FileWatcher s int tmplBuf *bytes.Buffer outCmdBuf *bytes.Buffer } //NewTemplate returns a new Template instance or an //error if the specified template file contains errors func NewTemplate(file string, onChange func()) (*Template, error) { tmpl, err := text_template.New("nginx.tmpl").Funcs(funcMap).ParseFiles(file) if err != nil { return nil, err } fw, err := watch.NewFileWatcher(file, onChange) if err != nil { return nil, err } return &Template{ 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() } // Write populates a buffer using a template with NGINX configuration // and the servers and upstreams created by Ingress rules func (t *Template) Write(conf map[string]interface{}, isValidTemplate func([]byte) error) ([]byte, error) { defer t.tmplBuf.Reset() defer t.outCmdBuf.Reset() if glog.V(3) { b, err := json.Marshal(conf) if err != nil { glog.Errorf("unexpected error: %v", err) } glog.Infof("NGINX configuration: %v", string(b)) } 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())) } // 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 content, nil } 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 s.Name } // buildLocation produces the location string, if the ingress has redirects // (specified through the ingress.kubernetes.io/rewrite-to annotation) func buildLocation(input interface{}) string { location, ok := input.(*ingress.Location) if !ok { return slash } path := location.Path if len(location.Redirect.Target) > 0 && location.Redirect.Target != path { return fmt.Sprintf("~* %s", path) } return path } func buildAuthLocation(input interface{}) string { location, ok := input.(*ingress.Location) if !ok { return "" } if location.ExternalAuth.URL == "" { return "" } str := base64.URLEncoding.EncodeToString([]byte(location.Path)) // avoid locations containing the = char str = strings.Replace(str, "=", "", -1) return fmt.Sprintf("/_external-auth-%v", str) } // buildProxyPass produces the proxy pass string, if the ingress has redirects // (specified through the ingress.kubernetes.io/rewrite-to annotation) // If the annotation ingress.kubernetes.io/add-base-url:"true" is specified it will // add a base tag in the head of the response from the service func buildProxyPass(input interface{}) string { location, ok := input.(*ingress.Location) if !ok { return "" } path := location.Path proto := "http" if location.SecureUpstream { proto = "https" } // defProxyPass returns the default proxy_pass, just the name of the upstream defProxyPass := fmt.Sprintf("proxy_pass %s://%s;", proto, location.Upstream.Name) // if the path in the ingress rule is equals to the target: no special rewrite if path == location.Redirect.Target { return defProxyPass } if path != slash && !strings.HasSuffix(path, slash) { path = fmt.Sprintf("%s/", path) } if len(location.Redirect.Target) > 0 { abu := "" if location.Redirect.AddBaseURL { bPath := location.Redirect.Target if !strings.HasSuffix(bPath, slash) { bPath = fmt.Sprintf("%s/", bPath) } abu = fmt.Sprintf(`subs_filter '
' '