Add "componentstatus" to API for easier cluster health check.

pull/6/head
Fabio Yeon 2015-04-15 12:23:02 -07:00
parent aca8452a21
commit 951a125751
26 changed files with 700 additions and 46 deletions

View File

@ -242,6 +242,7 @@ _kubectl_get()
must_have_one_flag=() must_have_one_flag=()
must_have_one_noun=() must_have_one_noun=()
must_have_one_noun+=("componentstatus")
must_have_one_noun+=("endpoints") must_have_one_noun+=("endpoints")
must_have_one_noun+=("event") must_have_one_noun+=("event")
must_have_one_noun+=("limitrange") must_have_one_noun+=("limitrange")

View File

@ -138,13 +138,17 @@ func kindToResource(kind string, mixedCase bool) (plural, singular string) {
} else { } else {
singular = strings.ToLower(kind) singular = strings.ToLower(kind)
} }
switch string(singular[len(singular)-1]) { if strings.HasSuffix(singular, "status") {
case "s": plural = strings.TrimSuffix(singular, "status") + "statuses"
plural = singular } else {
case "y": switch string(singular[len(singular)-1]) {
plural = strings.TrimSuffix(singular, "y") + "ies" case "s":
default: plural = singular
plural = singular + "s" case "y":
plural = strings.TrimSuffix(singular, "y") + "ies"
default:
plural = singular + "s"
}
} }
return return
} }
@ -215,7 +219,7 @@ func (m *DefaultRESTMapper) RESTMapping(kind string, versions ...string) (*RESTM
return nil, fmt.Errorf("the provided version %q has no relevant versions", version) return nil, fmt.Errorf("the provided version %q has no relevant versions", version)
} }
return &RESTMapping{ retVal := &RESTMapping{
Resource: resource, Resource: resource,
APIVersion: version, APIVersion: version,
Kind: kind, Kind: kind,
@ -224,7 +228,9 @@ func (m *DefaultRESTMapper) RESTMapping(kind string, versions ...string) (*RESTM
Codec: interfaces.Codec, Codec: interfaces.Codec,
ObjectConvertor: interfaces.ObjectConvertor, ObjectConvertor: interfaces.ObjectConvertor,
MetadataAccessor: interfaces.MetadataAccessor, MetadataAccessor: interfaces.MetadataAccessor,
}, nil }
return retVal, nil
} }
// aliasToResource is used for mapping aliases to resources // aliasToResource is used for mapping aliases to resources

View File

@ -60,6 +60,8 @@ func init() {
&PodLogOptions{}, &PodLogOptions{},
&PodExecOptions{}, &PodExecOptions{},
&PodProxyOptions{}, &PodProxyOptions{},
&ComponentStatus{},
&ComponentStatusList{},
) )
// Legacy names are supported // Legacy names are supported
Scheme.AddKnownTypeWithName("", "Minion", &Node{}) Scheme.AddKnownTypeWithName("", "Minion", &Node{})
@ -101,3 +103,5 @@ func (*ListOptions) IsAnAPIObject() {}
func (*PodLogOptions) IsAnAPIObject() {} func (*PodLogOptions) IsAnAPIObject() {}
func (*PodExecOptions) IsAnAPIObject() {} func (*PodExecOptions) IsAnAPIObject() {}
func (*PodProxyOptions) IsAnAPIObject() {} func (*PodProxyOptions) IsAnAPIObject() {}
func (*ComponentStatus) IsAnAPIObject() {}
func (*ComponentStatusList) IsAnAPIObject() {}

View File

@ -1827,3 +1827,34 @@ func AddToNodeAddresses(addresses *[]NodeAddress, addAddresses ...NodeAddress) {
} }
} }
} }
// Type and constants for component health validation.
type ComponentConditionType string
// These are the valid conditions for the component.
const (
ComponentHealthy ComponentConditionType = "Healthy"
)
type ComponentCondition struct {
Type ComponentConditionType `json:"type" description:"the type of condition"`
Status ConditionStatus `json:"status" description:"the status of this condition"`
Message string `json:"message,omitempty" description:"health check message received from the component"`
Error string `json:"error,omitempty" description:"error code from health check attempt (if any)"`
}
// ComponentStatus (and ComponentStatusList) holds the cluster validation info.
type ComponentStatus struct {
TypeMeta `json:",inline"`
ObjectMeta `json:"metadata,omitempty"`
Name string `json:"name,omitempty" description:"name of the component"`
Conditions []ComponentCondition `json:"conditions,omitempty" description:"list of component condition objects"`
}
type ComponentStatusList struct {
TypeMeta `json:",inline"`
ListMeta `json:"metadata,omitempty"`
Items []ComponentStatus `json:"items" description:"items is a list of component status objects"`
}

View File

@ -68,6 +68,8 @@ func init() {
&PodLogOptions{}, &PodLogOptions{},
&PodExecOptions{}, &PodExecOptions{},
&PodProxyOptions{}, &PodProxyOptions{},
&ComponentStatus{},
&ComponentStatusList{},
) )
// Future names are supported // Future names are supported
api.Scheme.AddKnownTypeWithName("v1beta1", "Node", &Minion{}) api.Scheme.AddKnownTypeWithName("v1beta1", "Node", &Minion{})
@ -110,3 +112,5 @@ func (*ListOptions) IsAnAPIObject() {}
func (*PodLogOptions) IsAnAPIObject() {} func (*PodLogOptions) IsAnAPIObject() {}
func (*PodExecOptions) IsAnAPIObject() {} func (*PodExecOptions) IsAnAPIObject() {}
func (*PodProxyOptions) IsAnAPIObject() {} func (*PodProxyOptions) IsAnAPIObject() {}
func (*ComponentStatus) IsAnAPIObject() {}
func (*ComponentStatusList) IsAnAPIObject() {}

View File

@ -1592,3 +1592,32 @@ type GlusterfsVolumeSource struct {
// the Glusterfs volume to be mounted with read-only permissions // the Glusterfs volume to be mounted with read-only permissions
ReadOnly bool `json:"readOnly,omitempty" description:"Glusterfs volume to be mounted with read-only permissions"` ReadOnly bool `json:"readOnly,omitempty" description:"Glusterfs volume to be mounted with read-only permissions"`
} }
// Type and constants for component health validation.
type ComponentConditionType string
// These are the valid conditions for the component.
const (
ComponentHealthy ComponentConditionType = "Healthy"
)
type ComponentCondition struct {
Type ComponentConditionType `json:"type"`
Status ConditionStatus `json:"status"`
Message string `json:"message,omitempty" description:"health check message received from the component"`
Error string `json:"error,omitempty" description:"error code from health check attempt (if any)"`
}
// ComponentStatus (and ComponentStatusList) holds the cluster validation info.
type ComponentStatus struct {
TypeMeta `json:",inline"`
Name string `json:"name,omitempty" description:"name of the component"`
Conditions []ComponentCondition `json:"conditions,omitempty"`
}
type ComponentStatusList struct {
TypeMeta `json:",inline"`
Items []ComponentStatus `json:"items" description:"items is a list of component status objects"`
}

View File

@ -68,6 +68,8 @@ func init() {
&PodLogOptions{}, &PodLogOptions{},
&PodExecOptions{}, &PodExecOptions{},
&PodProxyOptions{}, &PodProxyOptions{},
&ComponentStatus{},
&ComponentStatusList{},
) )
// Future names are supported // Future names are supported
api.Scheme.AddKnownTypeWithName("v1beta2", "Node", &Minion{}) api.Scheme.AddKnownTypeWithName("v1beta2", "Node", &Minion{})
@ -110,3 +112,5 @@ func (*ListOptions) IsAnAPIObject() {}
func (*PodLogOptions) IsAnAPIObject() {} func (*PodLogOptions) IsAnAPIObject() {}
func (*PodExecOptions) IsAnAPIObject() {} func (*PodExecOptions) IsAnAPIObject() {}
func (*PodProxyOptions) IsAnAPIObject() {} func (*PodProxyOptions) IsAnAPIObject() {}
func (*ComponentStatus) IsAnAPIObject() {}
func (*ComponentStatusList) IsAnAPIObject() {}

View File

@ -1655,3 +1655,32 @@ type SecretList struct {
Items []Secret `json:"items" description:"items is a list of secret objects"` Items []Secret `json:"items" description:"items is a list of secret objects"`
} }
// Type and constants for component health validation.
type ComponentConditionType string
// These are the valid conditions for the component.
const (
ComponentHealthy ComponentConditionType = "Healthy"
)
type ComponentCondition struct {
Type ComponentConditionType `json:"type"`
Status ConditionStatus `json:"status"`
Message string `json:"message,omitempty" description:"health check message received from the component"`
Error string `json:"error,omitempty" description:"error code from health check attempt (if any)"`
}
// ComponentStatus (and ComponentStatusList) holds the cluster validation info.
type ComponentStatus struct {
TypeMeta `json:",inline"`
Name string `json:"name,omitempty" description:"name of the component"`
Conditions []ComponentCondition `json:"conditions,omitempty"`
}
type ComponentStatusList struct {
TypeMeta `json:",inline"`
Items []ComponentStatus `json:"items" description:"items is a list of component status objects"`
}

View File

@ -61,6 +61,8 @@ func init() {
&PodLogOptions{}, &PodLogOptions{},
&PodExecOptions{}, &PodExecOptions{},
&PodProxyOptions{}, &PodProxyOptions{},
&ComponentStatus{},
&ComponentStatusList{},
) )
// Legacy names are supported // Legacy names are supported
api.Scheme.AddKnownTypeWithName("v1beta3", "Minion", &Node{}) api.Scheme.AddKnownTypeWithName("v1beta3", "Minion", &Node{})
@ -102,3 +104,5 @@ func (*ListOptions) IsAnAPIObject() {}
func (*PodLogOptions) IsAnAPIObject() {} func (*PodLogOptions) IsAnAPIObject() {}
func (*PodExecOptions) IsAnAPIObject() {} func (*PodExecOptions) IsAnAPIObject() {}
func (*PodProxyOptions) IsAnAPIObject() {} func (*PodProxyOptions) IsAnAPIObject() {}
func (*ComponentStatus) IsAnAPIObject() {}
func (*ComponentStatusList) IsAnAPIObject() {}

View File

@ -1672,3 +1672,34 @@ type SecretList struct {
Items []Secret `json:"items" description:"items is a list of secret objects"` Items []Secret `json:"items" description:"items is a list of secret objects"`
} }
// Type and constants for component health validation.
type ComponentConditionType string
// These are the valid conditions for the component.
const (
ComponentHealthy ComponentConditionType = "Healthy"
)
type ComponentCondition struct {
Type ComponentConditionType `json:"type"`
Status ConditionStatus `json:"status"`
Message string `json:"message,omitempty" description:"health check message received from the component"`
Error string `json:"error,omitempty" description:"error code from health check attempt (if any)"`
}
// ComponentStatus (and ComponentStatusList) holds the cluster validation info.
type ComponentStatus struct {
TypeMeta `json:",inline"`
ObjectMeta `json:"metadata,omitempty"`
Name string `json:"name,omitempty" description:"name of the component"`
Conditions []ComponentCondition `json:"conditions,omitempty"`
}
type ComponentStatusList struct {
TypeMeta `json:",inline"`
ListMeta `json:"metadata,omitempty"`
Items []ComponentStatus `json:"items" description:"items is a list of component status objects"`
}

View File

@ -140,7 +140,8 @@ func (g *APIGroupVersion) InstallREST(container *restful.Container) error {
return errors.NewAggregate(registrationErrors) return errors.NewAggregate(registrationErrors)
} }
// TODO: Convert to go-restful // TODO: This endpoint is deprecated and should be removed at some point.
// Use "componentstatus" API instead.
func InstallValidator(mux Mux, servers func() map[string]Server) { func InstallValidator(mux Mux, servers func() map[string]Server) {
mux.Handle("/validate", NewValidator(servers)) mux.Handle("/validate", NewValidator(servers))
} }

View File

@ -48,13 +48,39 @@ type validator struct {
rt http.RoundTripper rt http.RoundTripper
} }
type ServerStatus struct {
Component string `json:"component,omitempty"`
Health string `json:"health,omitempty"`
HealthCode probe.Result `json:"healthCode,omitempty"`
Msg string `json:"msg,omitempty"`
Err string `json:"err,omitempty"`
}
// TODO: can this use pkg/probe/http // TODO: can this use pkg/probe/http
func (s *Server) check(client httpGet) (probe.Result, string, error) { func (server *Server) DoServerCheck(rt http.RoundTripper) (probe.Result, string, error) {
var client *http.Client
scheme := "http://" scheme := "http://"
if s.EnableHTTPS { if server.EnableHTTPS {
// TODO(roberthbailey): The servers that use HTTPS are currently the
// kubelets, and we should be using a standard kubelet client library
// to talk to them rather than a separate http client.
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client = &http.Client{Transport: transport}
scheme = "https://" scheme = "https://"
} else {
client = &http.Client{Transport: rt}
} }
resp, err := client.Get(scheme + net.JoinHostPort(s.Addr, strconv.Itoa(s.Port)) + s.Path)
resp, err := client.Get(scheme + net.JoinHostPort(server.Addr, strconv.Itoa(server.Port)) + server.Path)
if err != nil { if err != nil {
return probe.Unknown, "", err return probe.Unknown, "", err
} }
@ -70,14 +96,6 @@ func (s *Server) check(client httpGet) (probe.Result, string, error) {
return probe.Success, string(data), nil return probe.Success, string(data), nil
} }
type ServerStatus struct {
Component string `json:"component,omitempty"`
Health string `json:"health,omitempty"`
HealthCode probe.Result `json:"healthCode,omitempty"`
Msg string `json:"msg,omitempty"`
Err string `json:"err,omitempty"`
}
func (v *validator) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (v *validator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
verb := "get" verb := "get"
apiResource := "" apiResource := ""
@ -88,21 +106,7 @@ func (v *validator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
reply := []ServerStatus{} reply := []ServerStatus{}
for name, server := range v.servers() { for name, server := range v.servers() {
transport := v.rt transport := v.rt
if server.EnableHTTPS { status, msg, err := server.DoServerCheck(transport)
// TODO(roberthbailey): The servers that use HTTPS are currently the
// kubelets, and we should be using a standard kubelet client library
// to talk to them rather than a separate http client.
transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
status, msg, err := server.check(&http.Client{Transport: transport})
var errorMsg string var errorMsg string
if err != nil { if err != nil {
errorMsg = err.Error() errorMsg = err.Error()

View File

@ -65,8 +65,8 @@ func TestValidate(t *testing.T) {
StatusCode: test.code, StatusCode: test.code,
}, },
} }
fake := &http.Client{Transport: fakeRT} // fake := &http.Client{Transport: fakeRT}
status, data, err := s.check(fake) status, data, err := s.DoServerCheck(fakeRT)
expect := fmt.Sprintf("http://%s:%d/healthz", s.Addr, s.Port) expect := fmt.Sprintf("http://%s:%d/healthz", s.Addr, s.Port)
if fakeRT.url != expect { if fakeRT.url != expect {
t.Errorf("expected %s, got %s", expect, fakeRT.url) t.Errorf("expected %s, got %s", expect, fakeRT.url)

View File

@ -43,6 +43,7 @@ type Interface interface {
NamespacesInterface NamespacesInterface
PersistentVolumesInterface PersistentVolumesInterface
PersistentVolumeClaimsNamespacer PersistentVolumeClaimsNamespacer
ComponentStatusesInterface
} }
func (c *Client) ReplicationControllers(namespace string) ReplicationControllerInterface { func (c *Client) ReplicationControllers(namespace string) ReplicationControllerInterface {
@ -92,6 +93,10 @@ func (c *Client) PersistentVolumeClaims(namespace string) PersistentVolumeClaimI
return newPersistentVolumeClaims(c, namespace) return newPersistentVolumeClaims(c, namespace)
} }
func (c *Client) ComponentStatuses() ComponentStatusInterface {
return newComponentStatuses(c)
}
// VersionInterface has a method to retrieve the server version. // VersionInterface has a method to retrieve the server version.
type VersionInterface interface { type VersionInterface interface {
ServerVersion() (*version.Info, error) ServerVersion() (*version.Info, error)
@ -137,6 +142,25 @@ func (c *Client) ServerAPIVersions() (*api.APIVersions, error) {
return &v, nil return &v, nil
} }
type ComponentValidatorInterface interface {
ValidateComponents() (*api.ComponentStatusList, error)
}
// ValidateComponents retrieves and parses the master's self-monitored cluster state.
// TODO: This should hit the versioned endpoint when that is implemented.
func (c *Client) ValidateComponents() (*api.ComponentStatusList, error) {
body, err := c.Get().AbsPath("/validate").DoRaw()
if err != nil {
return nil, err
}
statuses := []api.ComponentStatus{}
if err := json.Unmarshal(body, &statuses); err != nil {
return nil, fmt.Errorf("got '%s': %v", string(body), err)
}
return &api.ComponentStatusList{Items: statuses}, nil
}
// IsTimeout tests if this is a timeout error in the underlying transport. // IsTimeout tests if this is a timeout error in the underlying transport.
// This is unbelievably ugly. // This is unbelievably ugly.
// See: http://stackoverflow.com/questions/23494950/specifically-check-for-timeout-error for details // See: http://stackoverflow.com/questions/23494950/specifically-check-for-timeout-error for details

View File

@ -0,0 +1,63 @@
/*
Copyright 2015 Google Inc. 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 client
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
)
type ComponentStatusesInterface interface {
ComponentStatuses() ComponentStatusInterface
}
// ComponentStatusInterface contains methods to retrieve ComponentStatus
type ComponentStatusInterface interface {
List(label labels.Selector, field fields.Selector) (*api.ComponentStatusList, error)
Get(name string) (*api.ComponentStatus, error)
// TODO: It'd be nice to have watch support at some point
//Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error)
}
// componentStatuses implements ComponentStatusesInterface
type componentStatuses struct {
client *Client
}
func newComponentStatuses(c *Client) *componentStatuses {
return &componentStatuses{c}
}
func (c *componentStatuses) List(label labels.Selector, field fields.Selector) (result *api.ComponentStatusList, err error) {
result = &api.ComponentStatusList{}
err = c.client.Get().
Resource("componentStatuses").
LabelsSelectorParam(label).
FieldsSelectorParam(field).
Do().
Into(result)
return result, err
}
func (c *componentStatuses) Get(name string) (result *api.ComponentStatus, err error) {
result = &api.ComponentStatus{}
err = c.client.Get().Resource("componentStatuses").Name(name).Do().Into(result)
return
}

View File

@ -0,0 +1,47 @@
/*
Copyright 2015 Google Inc. 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 testclient
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
)
// Fake implements ComponentStatusInterface.
type FakeComponentStatuses struct {
Fake *Fake
}
func (c *FakeComponentStatuses) List(label labels.Selector, field fields.Selector) (result *api.ComponentStatusList, err error) {
obj, err := c.Fake.Invokes(FakeAction{Action: "list-componentstatuses"}, &api.ComponentStatusList{})
return obj.(*api.ComponentStatusList), err
}
func (c *FakeComponentStatuses) Get(name string) (*api.ComponentStatus, error) {
obj, err := c.Fake.Invokes(FakeAction{Action: "get-componentstatus", Value: name}, &api.ComponentStatus{})
// c.Actions = append(c.Actions, FakeAction{Action: "get-componentstatuses", Value: nil})
// testStatus := &api.ComponentStatus{
// Name: "test",
// Health: "ok",
// HealthCode: int(probe.Success),
// Message: "ok",
// Error: "",
// }
// return &api.ComponentStatusList{Items: []api.ComponentStatus{*testStatus}}, nil
return obj.(*api.ComponentStatus), err
}

View File

@ -125,3 +125,7 @@ func (c *Fake) ServerAPIVersions() (*api.APIVersions, error) {
c.Actions = append(c.Actions, FakeAction{Action: "get-apiversions", Value: nil}) c.Actions = append(c.Actions, FakeAction{Action: "get-apiversions", Value: nil})
return &api.APIVersions{Versions: []string{"v1beta1", "v1beta2"}}, nil return &api.APIVersions{Versions: []string{"v1beta1", "v1beta2"}}, nil
} }
func (c *Fake) ComponentStatuses() client.ComponentStatusInterface {
return &FakeComponentStatuses{Fake: c}
}

View File

@ -150,6 +150,13 @@ func NewAPIFactory() (*cmdutil.Factory, *testFactory, runtime.Codec) {
Object: func() (meta.RESTMapper, runtime.ObjectTyper) { Object: func() (meta.RESTMapper, runtime.ObjectTyper) {
return latest.RESTMapper, api.Scheme return latest.RESTMapper, api.Scheme
}, },
Client: func() (*client.Client, error) {
// Swap out the HTTP client out of the client with the fake's version.
fakeClient := t.Client.(*client.FakeRESTClient)
c := client.NewOrDie(t.ClientConfig)
c.Client = fakeClient.Client
return c, t.Err
},
RESTClient: func(*meta.RESTMapping) (resource.RESTClient, error) { RESTClient: func(*meta.RESTMapping) (resource.RESTClient, error) {
return t.Client, t.Err return t.Client, t.Err
}, },

View File

@ -34,7 +34,7 @@ const (
get_long = `Display one or many resources. get_long = `Display one or many resources.
Possible resources include pods (po), replication controllers (rc), services Possible resources include pods (po), replication controllers (rc), services
(svc), minions (mi), or events (ev). (svc), minions (mi), events (ev), or component statuses (cs).
By specifying the output as 'template' and providing a Go template as the value By specifying the output as 'template' and providing a Go template as the value
of the --template flag, you can filter the attributes of the fetched resource(s).` of the --template flag, you can filter the attributes of the fetched resource(s).`

View File

@ -87,6 +87,31 @@ func testData() (*api.PodList, *api.ServiceList, *api.ReplicationControllerList)
return pods, svc, rc return pods, svc, rc
} }
func testComponentStatusData() *api.ComponentStatusList {
good := &api.ComponentStatus{
Name: "servergood",
Conditions: []api.ComponentCondition{
{Type: api.ComponentHealthy, Status: api.ConditionTrue, Message: "ok", Error: "nil"},
},
}
bad := &api.ComponentStatus{
Name: "serverbad",
Conditions: []api.ComponentCondition{
{Type: api.ComponentHealthy, Status: api.ConditionFalse, Message: "", Error: "bad status: 500"},
},
}
unknown := &api.ComponentStatus{
Name: "serverunknown",
Conditions: []api.ComponentCondition{
{Type: api.ComponentHealthy, Status: api.ConditionUnknown, Message: "", Error: "fizzbuzz error"},
},
}
return &api.ComponentStatusList{
Items: []api.ComponentStatus{*good, *bad, *unknown},
}
}
// Verifies that schemas that are not in the master tree of Kubernetes can be retrieved via Get. // Verifies that schemas that are not in the master tree of Kubernetes can be retrieved via Get.
func TestGetUnknownSchemaObject(t *testing.T) { func TestGetUnknownSchemaObject(t *testing.T) {
f, tf, codec := NewTestFactory() f, tf, codec := NewTestFactory()
@ -188,6 +213,32 @@ func TestGetListObjects(t *testing.T) {
} }
} }
func TestGetListComponentStatus(t *testing.T) {
statuses := testComponentStatusData()
f, tf, codec := NewAPIFactory()
tf.Printer = &testPrinter{}
tf.Client = &client.FakeRESTClient{
Codec: codec,
Resp: &http.Response{StatusCode: 200, Body: objBody(codec, statuses)},
}
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
cmd := NewCmdGet(f, buf)
cmd.SetOutput(buf)
cmd.Run(cmd, []string{"componentstatuses"})
expected := []runtime.Object{statuses}
actual := tf.Printer.(*testPrinter).Objects
if !reflect.DeepEqual(expected, actual) {
t.Errorf("unexpected object: %#v %#v", expected, actual)
}
if len(buf.String()) == 0 {
t.Errorf("unexpected empty output")
}
}
func TestGetMultipleTypeObjects(t *testing.T) { func TestGetMultipleTypeObjects(t *testing.T) {
pods, svc, _ := testData() pods, svc, _ := testData()

View File

@ -96,17 +96,19 @@ func (e ShortcutExpander) VersionAndKindForResource(resource string) (defaultVer
// indeed a shortcut. Otherwise, will return resource unmodified. // indeed a shortcut. Otherwise, will return resource unmodified.
func expandResourceShortcut(resource string) string { func expandResourceShortcut(resource string) string {
shortForms := map[string]string{ shortForms := map[string]string{
"po": "pods", // Please keep this alphabetized
"rc": "replicationcontrollers", "cs": "componentstatus",
// DEPRECATED: will be removed before 1.0
"se": "services",
"svc": "services",
"mi": "minions",
"ev": "events", "ev": "events",
"limits": "limitRanges", "limits": "limitRanges",
"quota": "resourceQuotas", "mi": "minions",
"po": "pods",
"pv": "persistentVolumes", "pv": "persistentVolumes",
"pvc": "persistentVolumeClaims", "pvc": "persistentVolumeClaims",
"quota": "resourceQuotas",
"rc": "replicationcontrollers",
// DEPRECATED: will be removed before 1.0
"se": "services",
"svc": "services",
} }
if expanded, ok := shortForms[resource]; ok { if expanded, ok := shortForms[resource]; ok {
return expanded return expanded

View File

@ -256,6 +256,7 @@ var namespaceColumns = []string{"NAME", "LABELS", "STATUS"}
var secretColumns = []string{"NAME", "DATA"} var secretColumns = []string{"NAME", "DATA"}
var persistentVolumeColumns = []string{"NAME", "LABELS", "CAPACITY", "ACCESSMODES", "STATUS", "CLAIM"} var persistentVolumeColumns = []string{"NAME", "LABELS", "CAPACITY", "ACCESSMODES", "STATUS", "CLAIM"}
var persistentVolumeClaimColumns = []string{"NAME", "LABELS", "STATUS", "VOLUME"} var persistentVolumeClaimColumns = []string{"NAME", "LABELS", "STATUS", "VOLUME"}
var componentStatusColumns = []string{"NAME", "STATUS", "MESSAGE", "ERROR"}
// addDefaultHandlers adds print handlers for default Kubernetes types. // addDefaultHandlers adds print handlers for default Kubernetes types.
func (h *HumanReadablePrinter) addDefaultHandlers() { func (h *HumanReadablePrinter) addDefaultHandlers() {
@ -284,6 +285,8 @@ func (h *HumanReadablePrinter) addDefaultHandlers() {
h.Handler(persistentVolumeClaimColumns, printPersistentVolumeClaimList) h.Handler(persistentVolumeClaimColumns, printPersistentVolumeClaimList)
h.Handler(persistentVolumeColumns, printPersistentVolume) h.Handler(persistentVolumeColumns, printPersistentVolume)
h.Handler(persistentVolumeColumns, printPersistentVolumeList) h.Handler(persistentVolumeColumns, printPersistentVolumeList)
h.Handler(componentStatusColumns, printComponentStatus)
h.Handler(componentStatusColumns, printComponentStatusList)
} }
func (h *HumanReadablePrinter) unknown(data []byte, w io.Writer) error { func (h *HumanReadablePrinter) unknown(data []byte, w io.Writer) error {
@ -647,6 +650,36 @@ func printResourceQuotaList(list *api.ResourceQuotaList, w io.Writer) error {
return nil return nil
} }
func printComponentStatus(item *api.ComponentStatus, w io.Writer) error {
status := "Unknown"
message := ""
error := ""
for _, condition := range item.Conditions {
if condition.Type == api.ComponentHealthy {
if condition.Status == api.ConditionTrue {
status = "Healthy"
} else {
status = "Unhealthy"
}
message = condition.Message
error = condition.Error
break
}
}
_, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", item.Name, status, message, error)
return err
}
func printComponentStatusList(list *api.ComponentStatusList, w io.Writer) error {
for _, item := range list.Items {
if err := printComponentStatus(&item, w); err != nil {
return err
}
}
return nil
}
// PrintObj prints the obj in a human-friendly format according to the type of the obj. // PrintObj prints the obj in a human-friendly format according to the type of the obj.
func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) error { func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) error {
w := tabwriter.NewWriter(output, 10, 4, 3, ' ', 0) w := tabwriter.NewWriter(output, 10, 4, 3, ' ', 0)

View File

@ -43,6 +43,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/master/ports" "github.com/GoogleCloudPlatform/kubernetes/pkg/master/ports"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/componentstatus"
controlleretcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/controller/etcd" controlleretcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/controller/etcd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/endpoint" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/endpoint"
endpointsetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/endpoint/etcd" endpointsetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/endpoint/etcd"
@ -416,6 +417,8 @@ func (m *Master) init(c *Config) {
"persistentVolumes/status": persistentVolumeStatusStorage, "persistentVolumes/status": persistentVolumeStatusStorage,
"persistentVolumeClaims": persistentVolumeClaimStorage, "persistentVolumeClaims": persistentVolumeClaimStorage,
"persistentVolumeClaims/status": persistentVolumeClaimStatusStorage, "persistentVolumeClaims/status": persistentVolumeClaimStatusStorage,
"componentStatuses": componentstatus.NewStorage(func() map[string]apiserver.Server { return m.getServersToValidate(c) }),
} }
apiVersions := []string{"v1beta1", "v1beta2"} apiVersions := []string{"v1beta1", "v1beta2"}

View File

@ -0,0 +1,19 @@
/*
Copyright 2015 Google Inc. 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 componentstatus provides interfaces and implementation for retrieving cluster
// component status.
package componentstatus

View File

@ -0,0 +1,110 @@
/*
Copyright 2015 Google Inc. 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 componentstatus
import (
"fmt"
"net/http"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/probe"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
)
type REST struct {
GetServersToValidate func() map[string]apiserver.Server
rt http.RoundTripper
}
// NewStorage returns a new REST.
func NewStorage(serverRetriever func() map[string]apiserver.Server) *REST {
return &REST{
GetServersToValidate: serverRetriever,
rt: http.DefaultTransport,
}
}
func (rs *REST) New() runtime.Object {
return &api.ComponentStatus{}
}
func (rs *REST) NewList() runtime.Object {
return &api.ComponentStatusList{}
}
// Returns the list of component status. Note that the label and field are both ignored.
// Note that this call doesn't support labels or selectors.
func (rs *REST) List(ctx api.Context, label labels.Selector, field fields.Selector) (runtime.Object, error) {
servers := rs.GetServersToValidate()
// TODO: This should be parallelized.
reply := []api.ComponentStatus{}
for name, server := range servers {
status := rs.getComponentStatus(name, server)
reply = append(reply, *status)
}
return &api.ComponentStatusList{Items: reply}, nil
}
func (rs *REST) Get(ctx api.Context, name string) (runtime.Object, error) {
servers := rs.GetServersToValidate()
if server, ok := servers[name]; !ok {
return nil, fmt.Errorf("Component not found: %s", name)
} else {
return rs.getComponentStatus(name, server), nil
}
}
func ToConditionStatus(s probe.Result) api.ConditionStatus {
switch s {
case probe.Success:
return api.ConditionTrue
case probe.Failure:
return api.ConditionFalse
default:
return api.ConditionUnknown
}
}
func (rs *REST) getComponentStatus(name string, server apiserver.Server) *api.ComponentStatus {
transport := rs.rt
status, msg, err := server.DoServerCheck(transport)
var errorMsg string
if err != nil {
errorMsg = err.Error()
} else {
errorMsg = "nil"
}
c := &api.ComponentCondition{
Type: api.ComponentHealthy,
Status: ToConditionStatus(status),
Message: msg,
Error: errorMsg,
}
retVal := &api.ComponentStatus{
Name: name,
Conditions: []api.ComponentCondition{*c},
}
return retVal
}

View File

@ -0,0 +1,143 @@
/*
Copyright 2015 Google Inc. 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 componentstatus
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
type fakeRoundTripper struct {
err error
resp *http.Response
url string
}
func (f *fakeRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
f.url = req.URL.String()
return f.resp, f.err
}
type testResponse struct {
code int
data string
err error
}
func NewTestREST(resp testResponse) *REST {
return &REST{
GetServersToValidate: func() map[string]apiserver.Server {
return map[string]apiserver.Server{
"test1": {Addr: "testserver1", Port: 8000, Path: "/healthz"},
}
},
rt: &fakeRoundTripper{
err: resp.err,
resp: &http.Response{
Body: ioutil.NopCloser(bytes.NewBufferString(resp.data)),
StatusCode: resp.code,
},
},
}
}
func createTestStatus(name string, status api.ConditionStatus, msg string, err string) *api.ComponentStatus {
return &api.ComponentStatus{
Name: name,
Conditions: []api.ComponentCondition{
{Type: api.ComponentHealthy, Status: status, Message: msg, Error: err},
},
}
}
func TestList_NoError(t *testing.T) {
r := NewTestREST(testResponse{code: 200, data: "ok"})
got, err := r.List(api.NewContext(), labels.Everything(), fields.Everything())
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
expect := &api.ComponentStatusList{
Items: []api.ComponentStatus{*(createTestStatus("test1", api.ConditionTrue, "ok", "nil"))},
}
if e, a := expect, got; !reflect.DeepEqual(e, a) {
t.Errorf("Got unexpected object. Diff: %s", util.ObjectDiff(e, a))
}
}
func TestList_FailedCheck(t *testing.T) {
r := NewTestREST(testResponse{code: 500, data: ""})
got, err := r.List(api.NewContext(), labels.Everything(), fields.Everything())
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
expect := &api.ComponentStatusList{
Items: []api.ComponentStatus{
*(createTestStatus("test1", api.ConditionFalse, "", "unhealthy http status code: 500 ()"))},
}
if e, a := expect, got; !reflect.DeepEqual(e, a) {
t.Errorf("Got unexpected object. Diff: %s", util.ObjectDiff(e, a))
}
}
func TestList_UnknownError(t *testing.T) {
r := NewTestREST(testResponse{code: 500, data: "", err: fmt.Errorf("fizzbuzz error")})
got, err := r.List(api.NewContext(), labels.Everything(), fields.Everything())
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
expect := &api.ComponentStatusList{
Items: []api.ComponentStatus{
*(createTestStatus("test1", api.ConditionUnknown, "", "Get http://testserver1:8000/healthz: fizzbuzz error"))},
}
if e, a := expect, got; !reflect.DeepEqual(e, a) {
t.Errorf("Got unexpected object. Diff: %s", util.ObjectDiff(e, a))
}
}
func TestGet_NoError(t *testing.T) {
r := NewTestREST(testResponse{code: 200, data: "ok"})
got, err := r.Get(api.NewContext(), "test1")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
expect := createTestStatus("test1", api.ConditionTrue, "ok", "nil")
if e, a := expect, got; !reflect.DeepEqual(e, a) {
t.Errorf("Got unexpected object. Diff: %s", util.ObjectDiff(e, a))
}
}
func TestGet_BadName(t *testing.T) {
r := NewTestREST(testResponse{code: 200, data: "ok"})
_, err := r.Get(api.NewContext(), "invalidname")
if err == nil {
t.Fatalf("Expected error, but did not get one")
}
if !strings.Contains(err.Error(), "Component not found: invalidname") {
t.Fatalf("Got unexpected error: %v", err)
}
}