Address comments. Move auth and healthcheck inside nginx package

This commit is contained in:
Manuel de Brito Fontes 2016-05-31 12:22:04 -04:00
parent 221b823ca7
commit 6b841edff0
8 changed files with 39 additions and 20 deletions

View file

@ -0,0 +1,167 @@
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 auth
import (
"errors"
"fmt"
"io/ioutil"
"os"
"regexp"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
client "k8s.io/kubernetes/pkg/client/unversioned"
)
const (
authType = "ingress.kubernetes.io/auth-type"
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"
)
func init() {
// TODO: check permissions required
os.MkdirAll(DefAuthDirectory, 0655)
}
var (
authTypeRegex = regexp.MustCompile(`basic|digest`)
// 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 {
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) {
if ing.GetAnnotations() == nil {
return &Nginx{}, ErrMissingAnnotations
}
at, err := ingAnnotations(ing.GetAnnotations()).authType()
if err != nil {
return &Nginx{}, err
}
s, err := ingAnnotations(ing.GetAnnotations()).secretName()
if err != nil {
return &Nginx{}, err
}
secret, err := kubeClient.Secrets(ing.Namespace).Get(s)
if err != nil {
return &Nginx{}, err
}
realm := ingAnnotations(ing.GetAnnotations()).realm()
passFile := fmt.Sprintf("%v/%v-%v.passwd", authDir, ing.GetNamespace(), ing.GetName())
err = dumpSecret(passFile, secret)
if err != nil {
return &Nginx{}, err
}
return &Nginx{
Type: at,
Realm: realm,
File: passFile,
Secured: true,
}, nil
}
// dumpSecret dumps the content of a secret into a file
// in the expected format for the specified authorization
func dumpSecret(filename string, secret *api.Secret) error {
val, ok := secret.Data["auth"]
if !ok {
return ErrMissingAuthInSecret
}
// TODO: check permissions required
err := ioutil.WriteFile(filename, val, 0777)
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,206 @@
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 auth
import (
"fmt"
"io/ioutil"
"os"
"testing"
"time"
"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"
)
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,
},
},
},
},
},
},
},
}
}
type secretsClient struct {
unversioned.Interface
}
// dummySecret generates a secret with one user inside the auth key
// foo:md5(bar)
func dummySecret() *api.Secret {
return &api.Secret{
ObjectMeta: api.ObjectMeta{
Namespace: api.NamespaceDefault,
Name: "demo-secret",
},
Data: map[string][]byte{"auth": []byte("foo:$apr1$OFG3Xybp$ckL0FHDAkoXYIlH9.cysT0")},
}
}
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.Error("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, "")
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) {
ing := buildIngress()
data := map[string]string{}
data[authType] = "basic"
data[authSecret] = "demo-secret"
data[authRealm] = "-realm-"
ing.SetAnnotations(data)
_, dir, _ := dummySecretContent(t)
defer os.RemoveAll(dir)
client := mockClient()
nginxAuth, err := ParseAnnotations(client, ing, dir)
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 nginxAuth.Realm != "-realm-" {
t.Errorf("Expected -realm- as realm but returned %s", nginxAuth.Realm)
}
if nginxAuth.Secured != true {
t.Errorf("Expected true as secured but returned %v", nginxAuth.Secured)
}
}
func dummySecretContent(t *testing.T) (string, string, *api.Secret) {
dir, err := ioutil.TempDir("", fmt.Sprintf("%v", time.Now().Unix()))
if err != nil {
t.Error(err)
}
tmpfile, err := ioutil.TempFile("", "example-")
if err != nil {
t.Error(err)
}
defer tmpfile.Close()
s := dummySecret()
return tmpfile.Name(), dir, s
}
func TestDumpSecret(t *testing.T) {
tmpfile, dir, s := dummySecretContent(t)
defer os.RemoveAll(dir)
sd := s.Data
s.Data = nil
err := dumpSecret(tmpfile, s)
if err == nil {
t.Errorf("Expected error with secret without auth")
}
s.Data = sd
err = dumpSecret(tmpfile, s)
if err != nil {
t.Errorf("Unexpected error creating htpasswd file %v: %v", tmpfile, err)
}
}

View file

@ -0,0 +1,101 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 healthcheck
import (
"errors"
"strconv"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/contrib/ingress/controllers/nginx/nginx"
)
const (
upsMaxFails = "ingress.kubernetes.io/upstream-max-fails"
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 {
MaxFails int
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 nginx.NginxConfiguration, ing *extensions.Ingress) *Upstream {
if ing.GetAnnotations() == nil {
return &Upstream{cfg.UpstreamMaxFails, cfg.UpstreamFailTimeout}
}
mf, err := ingAnnotations(ing.GetAnnotations()).maxFails()
if err != nil {
mf = cfg.UpstreamMaxFails
}
ft, err := ingAnnotations(ing.GetAnnotations()).failTimeout()
if err != nil {
ft = cfg.UpstreamFailTimeout
}
return &Upstream{mf, ft}
}

View file

@ -0,0 +1,112 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 healthcheck
import (
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/util/intstr"
"k8s.io/contrib/ingress/controllers/nginx/nginx"
)
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()
_, 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 mf != 1 {
t.Errorf("Expected 1 but returned %s", mf)
}
ft, err := ingAnnotations(ing.GetAnnotations()).failTimeout()
if ft != 1 {
t.Errorf("Expected 1 but returned %s", ft)
}
}
func TestIngressHealthCheck(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[upsMaxFails] = "2"
ing.SetAnnotations(data)
cfg := nginx.NginxConfiguration{}
cfg.UpstreamFailTimeout = 1
nginxHz := ParseAnnotations(cfg, ing)
if nginxHz.MaxFails != 2 {
t.Errorf("Expected 2 as max-fails but returned %v", nginxHz.MaxFails)
}
if nginxHz.FailTimeout != 1 {
t.Errorf("Expected 0 as fail-timeout but returned %v", nginxHz.FailTimeout)
}
}

View file

@ -17,6 +17,7 @@ limitations under the License.
package nginx
import (
"k8s.io/contrib/ingress/controllers/nginx/nginx/auth"
"k8s.io/contrib/ingress/controllers/nginx/nginx/ratelimit"
"k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite"
)
@ -93,8 +94,9 @@ type Location struct {
Path string
IsDefBackend bool
Upstream Upstream
Redirect rewrite.Redirect
Auth auth.Nginx
RateLimit ratelimit.RateLimit
Redirect rewrite.Redirect
}
// LocationByPath sorts location by path