From 16921f961abc1d8ab830d64a9e6b10367f2ce437 Mon Sep 17 00:00:00 2001 From: Prashanth Balasubramanian Date: Tue, 22 Sep 2015 11:26:36 -0700 Subject: [PATCH] Ingress registry --- .../experimental/validation/validation.go | 28 +++ pkg/master/master.go | 3 + pkg/registry/ingress/doc.go | 17 ++ pkg/registry/ingress/etcd/etcd.go | 77 +++++++ pkg/registry/ingress/etcd/etcd_test.go | 206 ++++++++++++++++++ pkg/registry/ingress/strategy.go | 106 +++++++++ 6 files changed, 437 insertions(+) create mode 100644 pkg/registry/ingress/doc.go create mode 100644 pkg/registry/ingress/etcd/etcd.go create mode 100755 pkg/registry/ingress/etcd/etcd_test.go create mode 100644 pkg/registry/ingress/strategy.go diff --git a/pkg/apis/experimental/validation/validation.go b/pkg/apis/experimental/validation/validation.go index f998520962..0d36959761 100644 --- a/pkg/apis/experimental/validation/validation.go +++ b/pkg/apis/experimental/validation/validation.go @@ -371,3 +371,31 @@ func ValidateJobStatusUpdate(oldStatus, status experimental.JobStatus) errs.Vali allErrs = append(allErrs, ValidateJobStatus(&status)...) return allErrs } + +// ValidateIngressName validates that the given name can be used as an Ingress name. +func ValidateIngressName(name string, prefix bool) (bool, string) { + return apivalidation.NameIsDNSSubdomain(name, prefix) +} + +// ValidateIngressSpec tests if required fields in the IngressSpec are set. +func ValidateIngressSpec(spec *experimental.IngressSpec) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + // TODO: Validate IngressHosts/Rules etc. + return allErrs +} + +// ValidateIngress tests if required fields in the Ingress are set. +func ValidateIngress(ingress *experimental.Ingress) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&ingress.ObjectMeta, true, ValidateIngressName).Prefix("metadata")...) + allErrs = append(allErrs, ValidateIngressSpec(&ingress.Spec).Prefix("spec")...) + return allErrs +} + +// ValidateIngressUpdate tests if required fields in the Ingress are set. +func ValidateIngressUpdate(oldController, ingress *experimental.Ingress) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&ingress.ObjectMeta, &oldController.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateIngressSpec(&ingress.Spec).Prefix("spec")...) + return allErrs +} diff --git a/pkg/master/master.go b/pkg/master/master.go index 71d4afbf80..85ca856f4c 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -54,6 +54,7 @@ import ( endpointsetcd "k8s.io/kubernetes/pkg/registry/endpoint/etcd" eventetcd "k8s.io/kubernetes/pkg/registry/event/etcd" expcontrolleretcd "k8s.io/kubernetes/pkg/registry/experimental/controller/etcd" + ingressetcd "k8s.io/kubernetes/pkg/registry/ingress/etcd" jobetcd "k8s.io/kubernetes/pkg/registry/job/etcd" limitrangeetcd "k8s.io/kubernetes/pkg/registry/limitrange/etcd" "k8s.io/kubernetes/pkg/registry/namespace" @@ -961,6 +962,7 @@ func (m *Master) experimental(c *Config) *apiserver.APIGroupVersion { daemonSetStorage, daemonSetStatusStorage := daemonetcd.NewREST(c.ExpDatabaseStorage) deploymentStorage := deploymentetcd.NewStorage(c.ExpDatabaseStorage) jobStorage, jobStatusStorage := jobetcd.NewREST(c.ExpDatabaseStorage) + ingressStorage := ingressetcd.NewREST(c.ExpDatabaseStorage) thirdPartyControl := ThirdPartyController{ master: m, @@ -984,6 +986,7 @@ func (m *Master) experimental(c *Config) *apiserver.APIGroupVersion { strings.ToLower("deployments/scale"): deploymentStorage.Scale, strings.ToLower("jobs"): jobStorage, strings.ToLower("jobs/status"): jobStatusStorage, + strings.ToLower("ingress"): ingressStorage, } expMeta := latest.GroupOrDie("experimental") diff --git a/pkg/registry/ingress/doc.go b/pkg/registry/ingress/doc.go new file mode 100644 index 0000000000..5a6272cc54 --- /dev/null +++ b/pkg/registry/ingress/doc.go @@ -0,0 +1,17 @@ +/* +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 ingress diff --git a/pkg/registry/ingress/etcd/etcd.go b/pkg/registry/ingress/etcd/etcd.go new file mode 100644 index 0000000000..5614bec2e1 --- /dev/null +++ b/pkg/registry/ingress/etcd/etcd.go @@ -0,0 +1,77 @@ +/* +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 etcd + +import ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/experimental" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/generic" + etcdgeneric "k8s.io/kubernetes/pkg/registry/generic/etcd" + ingress "k8s.io/kubernetes/pkg/registry/ingress" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/storage" +) + +const ( + IngressPath string = "/ingress" +) + +// rest implements a RESTStorage for replication controllers against etcd +type REST struct { + *etcdgeneric.Etcd +} + +// NewREST returns a RESTStorage object that will work against replication controllers. +func NewREST(s storage.Interface) *REST { + store := &etcdgeneric.Etcd{ + NewFunc: func() runtime.Object { return &experimental.Ingress{} }, + + // NewListFunc returns an object capable of storing results of an etcd list. + NewListFunc: func() runtime.Object { return &experimental.IngressList{} }, + // Produces a ingress that etcd understands, to the root of the resource + // by combining the namespace in the context with the given prefix + KeyRootFunc: func(ctx api.Context) string { + return etcdgeneric.NamespaceKeyRootFunc(ctx, IngressPath) + }, + // Produces a ingress that etcd understands, to the resource by combining + // the namespace in the context with the given prefix + KeyFunc: func(ctx api.Context, name string) (string, error) { + return etcdgeneric.NamespaceKeyFunc(ctx, IngressPath, name) + }, + // Retrieve the name field of a replication controller + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*experimental.Ingress).Name, nil + }, + // Used to match objects based on labels/fields for list and watch + PredicateFunc: func(label labels.Selector, field fields.Selector) generic.Matcher { + return ingress.MatchIngress(label, field) + }, + EndpointName: "ingress", + + // Used to validate controller creation + CreateStrategy: ingress.Strategy, + + // Used to validate controller updates + UpdateStrategy: ingress.Strategy, + + Storage: s, + } + + return &REST{store} +} diff --git a/pkg/registry/ingress/etcd/etcd_test.go b/pkg/registry/ingress/etcd/etcd_test.go new file mode 100755 index 0000000000..52625b9892 --- /dev/null +++ b/pkg/registry/ingress/etcd/etcd_test.go @@ -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 etcd + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/experimental" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/registrytest" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/tools" + "k8s.io/kubernetes/pkg/util" +) + +func newStorage(t *testing.T) (*REST, *tools.FakeEtcdClient) { + etcdStorage, fakeClient := registrytest.NewEtcdStorage(t, "experimental") + ingressStorage := NewREST(etcdStorage) + return ingressStorage, fakeClient +} + +var ( + namespace = "foo-namespace" + name = "foo-ingress" + defaultHostname = "foo.bar.com" + defaultBackendName = "default-backend" + defaultBackendPort = util.IntOrString{Kind: util.IntstrInt, IntVal: 80} + defaultLoadBalancer = "127.0.0.1" + defaultPath = "/foo" + defaultPathMap = map[string]string{defaultPath: defaultBackendName} +) + +type IngressRuleValues map[string]string + +func toHTTPIngressPaths(pathMap map[string]string) []experimental.HTTPIngressPath { + httpPaths := []experimental.HTTPIngressPath{} + for path, backend := range pathMap { + httpPaths = append(httpPaths, experimental.HTTPIngressPath{ + Path: path, + Backend: experimental.IngressBackend{ + ServiceName: backend, + ServicePort: defaultBackendPort, + }, + }) + } + return httpPaths +} + +func toIngressRules(hostRules map[string]IngressRuleValues) []experimental.IngressRule { + rules := []experimental.IngressRule{} + for host, pathMap := range hostRules { + rules = append(rules, experimental.IngressRule{ + Host: host, + IngressRuleValue: experimental.IngressRuleValue{ + HTTP: &experimental.HTTPIngressRuleValue{ + Paths: toHTTPIngressPaths(pathMap), + }, + }, + }) + } + return rules +} + +func newIngress(pathMap map[string]string) *experimental.Ingress { + return &experimental.Ingress{ + ObjectMeta: api.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: experimental.IngressSpec{ + Backend: &experimental.IngressBackend{ + ServiceName: defaultBackendName, + ServicePort: defaultBackendPort, + }, + Rules: toIngressRules(map[string]IngressRuleValues{ + defaultHostname: pathMap, + }), + }, + Status: experimental.IngressStatus{ + LoadBalancer: api.LoadBalancerStatus{ + Ingress: []api.LoadBalancerIngress{ + {IP: defaultLoadBalancer}, + }, + }, + }, + } +} + +func validNewIngress() *experimental.Ingress { + return newIngress(defaultPathMap) +} + +var validIngress = *validNewIngress() + +func TestCreate(t *testing.T) { + storage, fakeClient := newStorage(t) + test := registrytest.New(t, fakeClient, storage.Etcd) + ingress := validNewIngress() + noDefaultBackendAndRules := validNewIngress() + noDefaultBackendAndRules.Spec.Backend = &experimental.IngressBackend{} + noDefaultBackendAndRules.Spec.Rules = []experimental.IngressRule{} + badPath := validIngress() + badPath.Spec.Rules = toIngressRules(map[string]IngressRuleValues{ + "foo.bar.com": {"/invalid[": "svc"}}) + test.TestCreate( + // valid + ingress, + // TODO: Add invalid Ingress tests once we have validation. + noDefaultBackendAndRules, + noIngressHosts, + ) +} + +func TestUpdate(t *testing.T) { + storage, fakeClient := newStorage(t) + test := registrytest.New(t, fakeClient, storage.Etcd) + test.TestUpdate( + // valid + validNewIngress(), + // updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*experimental.Ingress) + object.Spec.Rules = toIngressRules(map[string]IngressRuleValues{ + "bar.foo.com": {"/bar": defaultBackendName}, + }) + return object + }, + // invalid updateFunc: ObjeceMeta is not to be tampered with. + func(obj runtime.Object) runtime.Object { + object := obj.(*experimental.Ingress) + object.UID = "newUID" + return object + }, + + func(obj runtime.Object) runtime.Object { + object := obj.(*experimental.Ingress) + object.Name = "" + return object + }, + + func(obj runtime.Object) runtime.Object { + object := obj.(*experimental.Ingress) + object.Spec.Rules = toIngressRules(map[string]IngressRuleValues{ + "foo.bar.com": {"/invalid[": "svc"}}) + return object + }, + ) +} + +func TestDelete(t *testing.T) { + storage, fakeClient := newStorage(t) + test := registrytest.New(t, fakeClient, storage.Etcd) + test.TestDelete(validNewIngress()) +} + +func TestGet(t *testing.T) { + storage, fakeClient := newStorage(t) + test := registrytest.New(t, fakeClient, storage.Etcd) + test.TestGet(validNewIngress()) +} + +func TestList(t *testing.T) { + storage, fakeClient := newStorage(t) + test := registrytest.New(t, fakeClient, storage.Etcd) + test.TestList(validNewIngress()) +} + +func TestWatch(t *testing.T) { + storage, fakeClient := newStorage(t) + test := registrytest.New(t, fakeClient, storage.Etcd) + test.TestWatch( + validNewIngress(), + // matching labels + []labels.Set{}, + // not matching labels + []labels.Set{ + {"a": "c"}, + {"foo": "bar"}, + }, + // matching fields + []fields.Set{ + {"metadata.name": name}, + }, + // not matching fields + []fields.Set{ + {"metadata.name": "bar"}, + {"name": name}, + }, + ) +} diff --git a/pkg/registry/ingress/strategy.go b/pkg/registry/ingress/strategy.go new file mode 100644 index 0000000000..76e5ff09df --- /dev/null +++ b/pkg/registry/ingress/strategy.go @@ -0,0 +1,106 @@ +/* +Copyright 2014 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 ingress + +import ( + "fmt" + "reflect" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/experimental" + "k8s.io/kubernetes/pkg/apis/experimental/validation" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/generic" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/fielderrors" +) + +// ingressStrategy implements verification logic for Replication Ingresss. +type ingressStrategy struct { + runtime.ObjectTyper + api.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating Replication Ingress objects. +var Strategy = ingressStrategy{api.Scheme, api.SimpleNameGenerator} + +// NamespaceScoped returns true because all Ingress' need to be within a namespace. +func (ingressStrategy) NamespaceScoped() bool { + return true +} + +func (ingressStrategy) PrepareForCreate(obj runtime.Object) { +} + +func (ingressStrategy) PrepareForUpdate(obj, old runtime.Object) { + newIngress := obj.(*experimental.Ingress) + oldIngress := old.(*experimental.Ingress) + + // Any changes to the spec increment the generation number, any changes to the + // status should reflect the generation number of the corresponding object. + // See api.ObjectMeta description for more information on Generation. + if !reflect.DeepEqual(oldIngress.Spec, newIngress.Spec) { + newIngress.Generation = oldIngress.Generation + 1 + } + +} + +func (ingressStrategy) Validate(ctx api.Context, obj runtime.Object) fielderrors.ValidationErrorList { + ingress := obj.(*experimental.Ingress) + err := validation.ValidateIngress(ingress) + return err +} + +func (ingressStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (ingressStrategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) fielderrors.ValidationErrorList { + validationErrorList := validation.ValidateIngress(obj.(*experimental.Ingress)) + updateErrorList := validation.ValidateIngressUpdate(old.(*experimental.Ingress), obj.(*experimental.Ingress)) + return append(validationErrorList, updateErrorList...) +} + +func (ingressStrategy) AllowUnconditionalUpdate() bool { + return true +} + +// IngressToSelectableFields returns a label set that represents the object. +func IngressToSelectableFields(ingress *experimental.Ingress) fields.Set { + return fields.Set{ + "metadata.name": ingress.Name, + } +} + +// MatchIngress is the filter used by the generic etcd backend to ingress +// watch events from etcd to clients of the apiserver only interested in specific +// labels/fields. +func MatchIngress(label labels.Selector, field fields.Selector) generic.Matcher { + return &generic.SelectionPredicate{ + Label: label, + Field: field, + GetAttrs: func(obj runtime.Object) (labels.Set, fields.Set, error) { + ingress, ok := obj.(*experimental.Ingress) + if !ok { + return nil, nil, fmt.Errorf("Given object is not an Ingress.") + } + return labels.Set(ingress.ObjectMeta.Labels), IngressToSelectableFields(ingress), nil + }, + } +}