diff --git a/pkg/api/helper_test.go b/pkg/api/helper_test.go index 798092e80e4..ac0f7680ee7 100644 --- a/pkg/api/helper_test.go +++ b/pkg/api/helper_test.go @@ -43,7 +43,11 @@ var apiObjectFuzzer = fuzz.New().NilChance(.5).NumElements(1, 1).Funcs( // only when all 8 bytes are set. j.ResourceVersion = c.RandUint64() >> 8 j.SelfLink = c.RandString() - j.CreationTimestamp = c.RandString() + + var sec, nsec int64 + c.Fuzz(&sec) + c.Fuzz(&nsec) + j.CreationTimestamp = util.Unix(sec, nsec).Rfc3339Copy() }, func(intstr *util.IntOrString, c fuzz.Continue) { // util.IntOrString will panic if its kind is set wrong. @@ -113,6 +117,7 @@ func runTest(t *testing.T, source interface{}) { t.Errorf("%v: %v (%#v)", name, err, source) return } + obj2, err := Decode(data) if err != nil { t.Errorf("%v: %v", name, err) diff --git a/pkg/api/types.go b/pkg/api/types.go index 5b98f1b2738..0438096a917 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -195,12 +195,12 @@ type Event struct { // JSONBase is shared by all objects sent to, or returned from the client type JSONBase struct { - Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` - ID string `json:"id,omitempty" yaml:"id,omitempty"` - CreationTimestamp string `json:"creationTimestamp,omitempty" yaml:"creationTimestamp,omitempty"` - SelfLink string `json:"selfLink,omitempty" yaml:"selfLink,omitempty"` - ResourceVersion uint64 `json:"resourceVersion,omitempty" yaml:"resourceVersion,omitempty"` - APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + ID string `json:"id,omitempty" yaml:"id,omitempty"` + CreationTimestamp util.Time `json:"creationTimestamp,omitempty" yaml:"creationTimestamp,omitempty"` + SelfLink string `json:"selfLink,omitempty" yaml:"selfLink,omitempty"` + ResourceVersion uint64 `json:"resourceVersion,omitempty" yaml:"resourceVersion,omitempty"` + APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` } // PodStatus represents a status of a pod. diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index 158c220d160..ee87d770b64 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -198,12 +198,12 @@ type Event struct { // JSONBase is shared by all objects sent to, or returned from the client type JSONBase struct { - Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` - ID string `json:"id,omitempty" yaml:"id,omitempty"` - CreationTimestamp string `json:"creationTimestamp,omitempty" yaml:"creationTimestamp,omitempty"` - SelfLink string `json:"selfLink,omitempty" yaml:"selfLink,omitempty"` - ResourceVersion uint64 `json:"resourceVersion,omitempty" yaml:"resourceVersion,omitempty"` - APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + ID string `json:"id,omitempty" yaml:"id,omitempty"` + CreationTimestamp util.Time `json:"creationTimestamp,omitempty" yaml:"creationTimestamp,omitempty"` + SelfLink string `json:"selfLink,omitempty" yaml:"selfLink,omitempty"` + ResourceVersion uint64 `json:"resourceVersion,omitempty" yaml:"resourceVersion,omitempty"` + APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` } // PodStatus represents a status of a pod. diff --git a/pkg/conversion/converter.go b/pkg/conversion/converter.go index c496bc5e841..27bbb51cecf 100644 --- a/pkg/conversion/converter.go +++ b/pkg/conversion/converter.go @@ -88,7 +88,7 @@ func (c *Converter) Register(conversionFunc interface{}) error { type FieldMatchingFlags int const ( - // Loop through destiation fields, search for matching source + // Loop through destination fields, search for matching source // field to copy it from. Source fields with no corresponding // destination field will be ignored. If SourceToDest is // specified, this flag is ignored. If niether is specified, diff --git a/pkg/registry/controller/storage.go b/pkg/registry/controller/storage.go index cd3c05c06a3..5b1b7316f1c 100644 --- a/pkg/registry/controller/storage.go +++ b/pkg/registry/controller/storage.go @@ -24,6 +24,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" "code.google.com/p/go-uuid/uuid" @@ -61,6 +62,9 @@ func (rs *RegistryStorage) Create(obj interface{}) (<-chan interface{}, error) { if errs := api.ValidateReplicationController(controller); len(errs) > 0 { return nil, fmt.Errorf("Validation errors: %v", errs) } + + controller.CreationTimestamp = util.Now() + return apiserver.MakeAsync(func() (interface{}, error) { err := rs.registry.CreateController(*controller) if err != nil { diff --git a/pkg/registry/minion/storage.go b/pkg/registry/minion/storage.go index 188ca1de859..27580983aa3 100644 --- a/pkg/registry/minion/storage.go +++ b/pkg/registry/minion/storage.go @@ -22,6 +22,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) // RegistryStorage implements the RESTStorage interface, backed by a MinionRegistry. @@ -44,6 +45,9 @@ func (rs *RegistryStorage) Create(obj interface{}) (<-chan interface{}, error) { if minion.ID == "" { return nil, fmt.Errorf("ID should not be empty: %#v", minion) } + + minion.CreationTimestamp = util.Now() + return apiserver.MakeAsync(func() (interface{}, error) { err := rs.registry.Insert(minion.ID) if err != nil { diff --git a/pkg/registry/pod/storage.go b/pkg/registry/pod/storage.go index b84035fe872..1dc1ea01c0e 100644 --- a/pkg/registry/pod/storage.go +++ b/pkg/registry/pod/storage.go @@ -28,6 +28,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/scheduler" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "code.google.com/p/go-uuid/uuid" "github.com/golang/glog" @@ -76,6 +77,9 @@ func (rs *RegistryStorage) Create(obj interface{}) (<-chan interface{}, error) { if errs := api.ValidatePod(pod); len(errs) > 0 { return nil, fmt.Errorf("Validation errors: %v", errs) } + + pod.CreationTimestamp = util.Now() + return apiserver.MakeAsync(func() (interface{}, error) { if err := rs.scheduleAndCreatePod(*pod); err != nil { return nil, err diff --git a/pkg/util/time.go b/pkg/util/time.go new file mode 100644 index 00000000000..ad710b86367 --- /dev/null +++ b/pkg/util/time.go @@ -0,0 +1,97 @@ +package util + +import ( + "encoding/json" + "time" +) + +// Time is a wrapper around time.Time which supports correct +// marshaling to YAML and JSON. Wrappers are provided for many +// of the factory methods that the time package offers. +type Time struct { + time.Time +} + +// Date returns the Time corresponding to the supplied parameters +// by wrapping time.Date. +func Date(year int, month time.Month, day, hour, min, sec, nsec int, loc *time.Location) Time { + return Time{time.Date(year, month, day, hour, min, sec, nsec, loc)} +} + +// Now returns the current local time. +func Now() Time { + return Time{time.Now()} +} + +// Unix returns the local time corresponding to the given Unix time +// by wrapping time.Unix. +func Unix(sec int64, nsec int64) Time { + return Time{time.Unix(sec, nsec)} +} + +// Rfc3339Copy returns a copy of the Time at second-level precision. +func (t Time) Rfc3339Copy() Time { + copied, _ := time.Parse(time.RFC3339, t.Format(time.RFC3339)) + return Time{copied} +} + +// UnmarshalJSON implements the json.Unmarshaller interface. +func (t *Time) UnmarshalJSON(b []byte) error { + if len(b) == 4 && string(b) == "null" { + t.Time = time.Time{} + return nil + } + + var str string + json.Unmarshal(b, &str) + + pt, err := time.Parse(time.RFC3339, str) + if err != nil { + return err + } + + t.Time = pt + return nil +} + +// MarshalJSON implements the json.Marshaler interface. +func (t Time) MarshalJSON() ([]byte, error) { + if t.IsZero() { + // Encode unset/nil objects as JSON's "null". + return []byte("null"), nil + } + + return json.Marshal(t.Format(time.RFC3339)) +} + +// SetYAML implements the yaml.Setter interface. +func (t *Time) SetYAML(tag string, value interface{}) bool { + if value == nil { + t.Time = time.Time{} + return true + } + + str, ok := value.(string) + if !ok { + return false + } + + pt, err := time.Parse(time.RFC3339, str) + if err != nil { + return false + } + + t.Time = pt + return true +} + +// GetYAML implements the yaml.Getter interface. +func (t Time) GetYAML() (tag string, value interface{}) { + if t.IsZero() { + value = "null" + return + } + + value = t.Format(time.RFC3339) + return tag, value +} diff --git a/pkg/util/time_test.go b/pkg/util/time_test.go new file mode 100644 index 00000000000..0ad0d9f81f5 --- /dev/null +++ b/pkg/util/time_test.go @@ -0,0 +1,126 @@ +package util + +import ( + "encoding/json" + "reflect" + "testing" + "time" + + "gopkg.in/v1/yaml" +) + +type TimeHolder struct { + T Time `json:"t" yaml:"t"` +} + +func TestTimeMarshalYAML(t *testing.T) { + cases := []struct { + input Time + result string + }{ + {Time{}, "t: \"null\"\n"}, + {Date(1998, time.May, 5, 5, 5, 5, 50, time.UTC), "t: 1998-05-05T05:05:05Z\n"}, + {Date(1998, time.May, 5, 5, 5, 5, 0, time.UTC), "t: 1998-05-05T05:05:05Z\n"}, + } + + for _, c := range cases { + input := TimeHolder{c.input} + result, err := yaml.Marshal(&input) + if err != nil { + t.Errorf("Failed to marshal input: '%v': %v", input, err) + } + if string(result) != c.result { + t.Errorf("Failed to marshal input: '%v': expected %+v, got %q", input, c.result, string(result)) + } + } +} + +func TestTimeUnmarshalYAML(t *testing.T) { + cases := []struct { + input string + result Time + }{ + {"t: \"null\"\n", Time{}}, + {"t: 1998-05-05T05:05:05Z\n", Date(1998, time.May, 5, 5, 5, 5, 0, time.UTC)}, + } + + for _, c := range cases { + var result TimeHolder + if err := yaml.Unmarshal([]byte(c.input), &result); err != nil { + t.Errorf("Failed to unmarshal input '%v': %v", c.input, err) + } + if result.T != c.result { + t.Errorf("Failed to unmarshal input '%v': expected %+v, got %+v", c.input, c.result, result) + } + } +} + +func TestTimeMarshalJSON(t *testing.T) { + cases := []struct { + input Time + result string + }{ + {Time{}, "{\"t\":null}"}, + {Date(1998, time.May, 5, 5, 5, 5, 50, time.UTC), "{\"t\":\"1998-05-05T05:05:05Z\"}"}, + {Date(1998, time.May, 5, 5, 5, 5, 0, time.UTC), "{\"t\":\"1998-05-05T05:05:05Z\"}"}, + } + + for _, c := range cases { + input := TimeHolder{c.input} + result, err := json.Marshal(&input) + if err != nil { + t.Errorf("Failed to marshal input: '%v': %v", input, err) + } + if string(result) != c.result { + t.Errorf("Failed to marshal input: '%v': expected %+v, got %q", input, c.result, string(result)) + } + } +} + +func TestTimeUnmarshalJSON(t *testing.T) { + cases := []struct { + input string + result Time + }{ + {"{\"t\":null}", Time{}}, + {"{\"t\":\"1998-05-05T05:05:05Z\"}", Date(1998, time.May, 5, 5, 5, 5, 0, time.UTC)}, + } + + for _, c := range cases { + var result TimeHolder + if err := json.Unmarshal([]byte(c.input), &result); err != nil { + t.Errorf("Failed to unmarshal input '%v': %v", c.input, err) + } + if result.T != c.result { + t.Errorf("Failed to unmarshal input '%v': expected %+v, got %+v", c.input, c.result, result) + } + } +} + +func TestTimeMarshalJSONUnmarshalYAML(t *testing.T) { + cases := []struct { + input Time + }{ + {Time{}}, + {Date(1998, time.May, 5, 5, 5, 5, 50, time.UTC).Rfc3339Copy()}, + {Date(1998, time.May, 5, 5, 5, 5, 0, time.UTC).Rfc3339Copy()}, + } + + for _, c := range cases { + input := TimeHolder{c.input} + jsonMarshalled, err := json.Marshal(&input) + if err != nil { + t.Errorf("1: Failed to marshal input: '%v': %v", input, err) + } + + var result TimeHolder + err = yaml.Unmarshal(jsonMarshalled, &result) + if err != nil { + t.Errorf("2: Failed to unmarshall '%+v': %v", string(jsonMarshalled), err) + } + + if !reflect.DeepEqual(input, result) { + t.Errorf("3: Failed to marshal input '%+v': got %+v", input, result) + } + } +} diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index 5bd72518245..5e68b628132 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -101,10 +101,10 @@ func TestIntOrStringUnmarshalYAML(t *testing.T) { for _, c := range cases { var result IntOrStringHolder if err := yaml.Unmarshal([]byte(c.input), &result); err != nil { - t.Errorf("Failed to unmarshal: %v", err) + t.Errorf("Failed to unmarshal input '%v': %v", c.input, err) } if result.IOrS != c.result { - t.Errorf("Failed to unmarshal IntOrString: got %+v", result) + t.Errorf("Failed to unmarshal input '%v': expected: %+v, got %+v", c.input, c.result, result) } } } @@ -122,10 +122,10 @@ func TestIntOrStringMarshalYAML(t *testing.T) { input := IntOrStringHolder{c.input} result, err := yaml.Marshal(&input) if err != nil { - t.Errorf("Failed to marshal: %v", err) + t.Errorf("Failed to marshal input '%v': %v", input, err) } if string(result) != c.result { - t.Errorf("Failed to marshal IntOrString: got %q", string(result)) + t.Errorf("Failed to marshal input '%v': expected: %+v, got %q", input, c.result, string(result)) } } } @@ -142,10 +142,10 @@ func TestIntOrStringUnmarshalJSON(t *testing.T) { for _, c := range cases { var result IntOrStringHolder if err := json.Unmarshal([]byte(c.input), &result); err != nil { - t.Errorf("Failed to unmarshal: %v", err) + t.Errorf("Failed to unmarshal input '%v': %v", c.input, err) } if result.IOrS != c.result { - t.Errorf("Failed to unmarshal IntOrString: got %+v", result) + t.Errorf("Failed to unmarshal input '%v': expected %+v, got %+v", c.input, c.result, result) } } } @@ -163,10 +163,10 @@ func TestIntOrStringMarshalJSON(t *testing.T) { input := IntOrStringHolder{c.input} result, err := json.Marshal(&input) if err != nil { - t.Errorf("Failed to marshal: %v", err) + t.Errorf("Failed to marshal input '%v': %v", input, err) } if string(result) != c.result { - t.Errorf("Failed to marshal IntOrString: got %q", string(result)) + t.Errorf("Failed to marshal input '%v': expected: %+v, got %q", input, c.result, string(result)) } } }