mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 19:56:01 +00:00
Add status subresource to Ingress
This commit is contained in:
parent
a5fc5fd0c4
commit
eae56c3b2d
@ -1994,3 +1994,24 @@ func ValidatePodLogOptions(opts *api.PodLogOptions) errs.ValidationErrorList {
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateLoadBalancerStatus validates required fields on a LoadBalancerStatus
|
||||
func ValidateLoadBalancerStatus(status *api.LoadBalancerStatus) errs.ValidationErrorList {
|
||||
allErrs := errs.ValidationErrorList{}
|
||||
for _, ingress := range status.Ingress {
|
||||
if len(ingress.IP) > 0 {
|
||||
if isIP := (net.ParseIP(ingress.IP) != nil); !isIP {
|
||||
allErrs = append(allErrs, errs.NewFieldInvalid("ingress.ip", ingress.IP, "must be an IP address"))
|
||||
}
|
||||
}
|
||||
if len(ingress.Hostname) > 0 {
|
||||
if valid, errMsg := NameIsDNSSubdomain(ingress.Hostname, false); !valid {
|
||||
allErrs = append(allErrs, errs.NewFieldInvalid("ingress.hostname", ingress.Hostname, errMsg))
|
||||
}
|
||||
if isIP := (net.ParseIP(ingress.Hostname) != nil); isIP {
|
||||
allErrs = append(allErrs, errs.NewFieldInvalid("ingress.hostname", ingress.Hostname, "must be a DNS name, not ip address"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
@ -447,6 +447,14 @@ func ValidateIngressUpdate(oldIngress, ingress *extensions.Ingress) errs.Validat
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateIngressStatusUpdate tests if required fields in the Ingress are set when updating status.
|
||||
func ValidateIngressStatusUpdate(ingress, oldIngress *extensions.Ingress) errs.ValidationErrorList {
|
||||
allErrs := errs.ValidationErrorList{}
|
||||
allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&ingress.ObjectMeta, &oldIngress.ObjectMeta).Prefix("metadata")...)
|
||||
allErrs = append(allErrs, apivalidation.ValidateLoadBalancerStatus(&ingress.Status.LoadBalancer).Prefix("status.loadBalancer")...)
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func validateIngressRules(IngressRules []extensions.IngressRule) errs.ValidationErrorList {
|
||||
allErrs := errs.ValidationErrorList{}
|
||||
if len(IngressRules) == 0 {
|
||||
|
@ -951,6 +951,98 @@ func TestValidateIngress(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateIngressStatusUpdate(t *testing.T) {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: util.IntOrString{Kind: util.IntstrInt, IntVal: 80},
|
||||
}
|
||||
|
||||
newValid := func() extensions.Ingress {
|
||||
return extensions.Ingress{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: api.NamespaceDefault,
|
||||
ResourceVersion: "9",
|
||||
},
|
||||
Spec: extensions.IngressSpec{
|
||||
Backend: &extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: util.IntOrString{Kind: util.IntstrInt, IntVal: 80},
|
||||
},
|
||||
Rules: []extensions.IngressRule{
|
||||
{
|
||||
Host: "foo.bar.com",
|
||||
IngressRuleValue: extensions.IngressRuleValue{
|
||||
HTTP: &extensions.HTTPIngressRuleValue{
|
||||
Paths: []extensions.HTTPIngressPath{
|
||||
{
|
||||
Path: "/foo",
|
||||
Backend: defaultBackend,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: extensions.IngressStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{
|
||||
{IP: "127.0.0.1", Hostname: "foo.bar.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
oldValue := newValid()
|
||||
newValue := newValid()
|
||||
newValue.Status = extensions.IngressStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{
|
||||
{IP: "127.0.0.2", Hostname: "foo.com"},
|
||||
},
|
||||
},
|
||||
}
|
||||
invalidIP := newValid()
|
||||
invalidIP.Status = extensions.IngressStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{
|
||||
{IP: "abcd", Hostname: "foo.com"},
|
||||
},
|
||||
},
|
||||
}
|
||||
invalidHostname := newValid()
|
||||
invalidHostname.Status = extensions.IngressStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{
|
||||
{IP: "127.0.0.1", Hostname: "127.0.0.1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
errs := ValidateIngressStatusUpdate(&newValue, &oldValue)
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("Unexpected error %v", errs)
|
||||
}
|
||||
|
||||
errorCases := map[string]extensions.Ingress{
|
||||
"status.loadBalancer.ingress.ip: invalid value": invalidIP,
|
||||
"status.loadBalancer.ingress.hostname: invalid value": invalidHostname,
|
||||
}
|
||||
for k, v := range errorCases {
|
||||
errs := ValidateIngressStatusUpdate(&v, &oldValue)
|
||||
if len(errs) == 0 {
|
||||
t.Errorf("expected failure for %s", k)
|
||||
} else {
|
||||
s := strings.Split(k, ":")
|
||||
err := errs[0].(*errors.ValidationError)
|
||||
if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) {
|
||||
t.Errorf("unexpected error: %v, expected: %s", errs[0], k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClusterAutoscaler(t *testing.T) {
|
||||
successCases := []extensions.ClusterAutoscaler{
|
||||
{
|
||||
|
@ -1032,7 +1032,7 @@ func (m *Master) experimental(c *Config) *apiserver.APIGroupVersion {
|
||||
daemonSetStorage, daemonSetStatusStorage := daemonetcd.NewREST(dbClient("daemonsets"))
|
||||
deploymentStorage := deploymentetcd.NewStorage(dbClient("deployments"))
|
||||
jobStorage, jobStatusStorage := jobetcd.NewREST(dbClient("jobs"))
|
||||
ingressStorage := ingressetcd.NewREST(dbClient("ingress"))
|
||||
ingressStorage, ingressStatusStorage := ingressetcd.NewREST(dbClient("ingress"))
|
||||
|
||||
thirdPartyControl := ThirdPartyController{
|
||||
master: m,
|
||||
@ -1058,6 +1058,7 @@ func (m *Master) experimental(c *Config) *apiserver.APIGroupVersion {
|
||||
strings.ToLower("jobs"): jobStorage,
|
||||
strings.ToLower("jobs/status"): jobStatusStorage,
|
||||
strings.ToLower("ingress"): ingressStorage,
|
||||
strings.ToLower("ingress/status"): ingressStatusStorage,
|
||||
}
|
||||
|
||||
expMeta := latest.GroupOrDie("extensions")
|
||||
|
@ -38,7 +38,7 @@ type REST struct {
|
||||
}
|
||||
|
||||
// NewREST returns a RESTStorage object that will work against replication controllers.
|
||||
func NewREST(s storage.Interface) *REST {
|
||||
func NewREST(s storage.Interface) (*REST, *StatusREST) {
|
||||
store := &etcdgeneric.Etcd{
|
||||
NewFunc: func() runtime.Object { return &extensions.Ingress{} },
|
||||
|
||||
@ -72,6 +72,21 @@ func NewREST(s storage.Interface) *REST {
|
||||
|
||||
Storage: s,
|
||||
}
|
||||
|
||||
return &REST{store}
|
||||
statusStore := *store
|
||||
statusStore.UpdateStrategy = ingress.StatusStrategy
|
||||
return &REST{store}, &StatusREST{store: &statusStore}
|
||||
}
|
||||
|
||||
// StatusREST implements the REST endpoint for changing the status of an ingress
|
||||
type StatusREST struct {
|
||||
store *etcdgeneric.Etcd
|
||||
}
|
||||
|
||||
func (r *StatusREST) New() runtime.Object {
|
||||
return &extensions.Ingress{}
|
||||
}
|
||||
|
||||
// Update alters the status subset of an object.
|
||||
func (r *StatusREST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) {
|
||||
return r.store.Update(ctx, obj)
|
||||
}
|
||||
|
@ -29,10 +29,10 @@ import (
|
||||
"k8s.io/kubernetes/pkg/util"
|
||||
)
|
||||
|
||||
func newStorage(t *testing.T) (*REST, *tools.FakeEtcdClient) {
|
||||
func newStorage(t *testing.T) (*REST, *StatusREST, *tools.FakeEtcdClient) {
|
||||
etcdStorage, fakeClient := registrytest.NewEtcdStorage(t, "extensions")
|
||||
ingressStorage := NewREST(etcdStorage)
|
||||
return ingressStorage, fakeClient
|
||||
ingressStorage, statusStorage := NewREST(etcdStorage)
|
||||
return ingressStorage, statusStorage, fakeClient
|
||||
}
|
||||
|
||||
var (
|
||||
@ -107,7 +107,7 @@ func validIngress() *extensions.Ingress {
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
storage, fakeClient := newStorage(t)
|
||||
storage, _, fakeClient := newStorage(t)
|
||||
test := registrytest.New(t, fakeClient, storage.Etcd)
|
||||
ingress := validIngress()
|
||||
noDefaultBackendAndRules := validIngress()
|
||||
@ -125,7 +125,7 @@ func TestCreate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
storage, fakeClient := newStorage(t)
|
||||
storage, _, fakeClient := newStorage(t)
|
||||
test := registrytest.New(t, fakeClient, storage.Etcd)
|
||||
test.TestUpdate(
|
||||
// valid
|
||||
@ -161,25 +161,25 @@ func TestUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
storage, fakeClient := newStorage(t)
|
||||
storage, _, fakeClient := newStorage(t)
|
||||
test := registrytest.New(t, fakeClient, storage.Etcd)
|
||||
test.TestDelete(validIngress())
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
storage, fakeClient := newStorage(t)
|
||||
storage, _, fakeClient := newStorage(t)
|
||||
test := registrytest.New(t, fakeClient, storage.Etcd)
|
||||
test.TestGet(validIngress())
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
storage, fakeClient := newStorage(t)
|
||||
storage, _, fakeClient := newStorage(t)
|
||||
test := registrytest.New(t, fakeClient, storage.Etcd)
|
||||
test.TestList(validIngress())
|
||||
}
|
||||
|
||||
func TestWatch(t *testing.T) {
|
||||
storage, fakeClient := newStorage(t)
|
||||
storage, _, fakeClient := newStorage(t)
|
||||
test := registrytest.New(t, fakeClient, storage.Etcd)
|
||||
test.TestWatch(
|
||||
validIngress(),
|
||||
@ -201,3 +201,5 @@ func TestWatch(t *testing.T) {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// TODO TestUpdateStatus
|
||||
|
@ -47,6 +47,7 @@ func (ingressStrategy) NamespaceScoped() bool {
|
||||
// PrepareForCreate clears the status of an Ingress before creation.
|
||||
func (ingressStrategy) PrepareForCreate(obj runtime.Object) {
|
||||
ingress := obj.(*extensions.Ingress)
|
||||
// create cannot set status
|
||||
ingress.Status = extensions.IngressStatus{}
|
||||
|
||||
ingress.Generation = 1
|
||||
@ -56,7 +57,8 @@ func (ingressStrategy) PrepareForCreate(obj runtime.Object) {
|
||||
func (ingressStrategy) PrepareForUpdate(obj, old runtime.Object) {
|
||||
newIngress := obj.(*extensions.Ingress)
|
||||
oldIngress := old.(*extensions.Ingress)
|
||||
//TODO: Clear Ingress status once we have a sub-resource.
|
||||
// Update is not allowed to set status
|
||||
newIngress.Status = oldIngress.Status
|
||||
|
||||
// Any changes to the spec increment the generation number, any changes to the
|
||||
// status should reflect the generation number of the corresponding object.
|
||||
@ -114,3 +116,22 @@ func MatchIngress(label labels.Selector, field fields.Selector) generic.Matcher
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type ingressStatusStrategy struct {
|
||||
ingressStrategy
|
||||
}
|
||||
|
||||
var StatusStrategy = ingressStatusStrategy{Strategy}
|
||||
|
||||
// PrepareForUpdate clears fields that are not allowed to be set by end users on update of status
|
||||
func (ingressStatusStrategy) PrepareForUpdate(obj, old runtime.Object) {
|
||||
newIngress := obj.(*extensions.Ingress)
|
||||
oldIngress := old.(*extensions.Ingress)
|
||||
// status changes are not allowed to update spec
|
||||
newIngress.Spec = oldIngress.Spec
|
||||
}
|
||||
|
||||
// ValidateUpdate is the default update validation for an end user updating status
|
||||
func (ingressStatusStrategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) fielderrors.ValidationErrorList {
|
||||
return validation.ValidateIngressStatusUpdate(obj.(*extensions.Ingress), old.(*extensions.Ingress))
|
||||
}
|
||||
|
130
pkg/registry/ingress/strategy_test.go
Normal file
130
pkg/registry/ingress/strategy_test.go
Normal file
@ -0,0 +1,130 @@
|
||||
/*
|
||||
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
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/util"
|
||||
)
|
||||
|
||||
func newIngress() extensions.Ingress {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: util.IntOrString{Kind: util.IntstrInt, IntVal: 80},
|
||||
}
|
||||
return extensions.Ingress{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: api.NamespaceDefault,
|
||||
},
|
||||
Spec: extensions.IngressSpec{
|
||||
Backend: &extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: util.IntOrString{Kind: util.IntstrInt, IntVal: 80},
|
||||
},
|
||||
Rules: []extensions.IngressRule{
|
||||
{
|
||||
Host: "foo.bar.com",
|
||||
IngressRuleValue: extensions.IngressRuleValue{
|
||||
HTTP: &extensions.HTTPIngressRuleValue{
|
||||
Paths: []extensions.HTTPIngressPath{
|
||||
{
|
||||
Path: "/foo",
|
||||
Backend: defaultBackend,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: extensions.IngressStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{
|
||||
{IP: "127.0.0.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressStrategy(t *testing.T) {
|
||||
ctx := api.NewDefaultContext()
|
||||
if !Strategy.NamespaceScoped() {
|
||||
t.Errorf("Ingress must be namespace scoped")
|
||||
}
|
||||
if Strategy.AllowCreateOnUpdate() {
|
||||
t.Errorf("Ingress should not allow create on update")
|
||||
}
|
||||
|
||||
ingress := newIngress()
|
||||
Strategy.PrepareForCreate(&ingress)
|
||||
if len(ingress.Status.LoadBalancer.Ingress) != 0 {
|
||||
t.Error("Ingress should not allow setting status on create")
|
||||
}
|
||||
errs := Strategy.Validate(ctx, &ingress)
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("Unexpected error validating %v", errs)
|
||||
}
|
||||
invalidIngress := newIngress()
|
||||
invalidIngress.ResourceVersion = "4"
|
||||
invalidIngress.Spec = extensions.IngressSpec{}
|
||||
Strategy.PrepareForUpdate(&invalidIngress, &ingress)
|
||||
errs = Strategy.ValidateUpdate(ctx, &invalidIngress, &ingress)
|
||||
if len(errs) == 0 {
|
||||
t.Errorf("Expected a validation error")
|
||||
}
|
||||
if invalidIngress.ResourceVersion != "4" {
|
||||
t.Errorf("Incoming resource version on update should not be mutated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressStatusStrategy(t *testing.T) {
|
||||
ctx := api.NewDefaultContext()
|
||||
if !StatusStrategy.NamespaceScoped() {
|
||||
t.Errorf("Ingress must be namespace scoped")
|
||||
}
|
||||
if StatusStrategy.AllowCreateOnUpdate() {
|
||||
t.Errorf("Ingress should not allow create on update")
|
||||
}
|
||||
oldIngress := newIngress()
|
||||
newIngress := newIngress()
|
||||
oldIngress.ResourceVersion = "4"
|
||||
newIngress.ResourceVersion = "4"
|
||||
newIngress.Spec.Backend.ServiceName = "ignore"
|
||||
newIngress.Status = extensions.IngressStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{
|
||||
{IP: "127.0.0.2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
StatusStrategy.PrepareForUpdate(&newIngress, &oldIngress)
|
||||
if newIngress.Status.LoadBalancer.Ingress[0].IP != "127.0.0.2" {
|
||||
t.Errorf("Ingress status updates should allow change of status fields")
|
||||
}
|
||||
if newIngress.Spec.Backend.ServiceName != "default-backend" {
|
||||
t.Errorf("PrepareForUpdate should have preserved old spec")
|
||||
}
|
||||
errs := StatusStrategy.ValidateUpdate(ctx, &newIngress, &oldIngress)
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("Unexpected error %v", errs)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user