diff --git a/pkg/serviceaccount/externaljwt/plugin/keycache.go b/pkg/serviceaccount/externaljwt/plugin/keycache.go index 546b42f50f8..521e0f881e7 100644 --- a/pkg/serviceaccount/externaljwt/plugin/keycache.go +++ b/pkg/serviceaccount/externaljwt/plugin/keycache.go @@ -25,10 +25,10 @@ import ( "time" "golang.org/x/sync/singleflight" - "k8s.io/klog/v2" - "k8s.io/kubernetes/pkg/serviceaccount" externaljwtv1alpha1 "k8s.io/externaljwt/apis/v1alpha1" + "k8s.io/klog/v2" + "k8s.io/kubernetes/pkg/serviceaccount" externaljwtmetrics "k8s.io/kubernetes/pkg/serviceaccount/externaljwt/metrics" ) @@ -56,7 +56,7 @@ func newKeyCache(client externaljwtv1alpha1.ExternalJWTSignerClient) *keyCache { // InitialFill can be used to perform an initial fetch for keys get the // refresh interval as recommended by external signer. func (p *keyCache) initialFill(ctx context.Context) error { - if _, err := p.syncKeys(ctx); err != nil { + if err := p.syncKeys(ctx); err != nil { return fmt.Errorf("while performing initial cache fill: %w", err) } return nil @@ -66,7 +66,6 @@ func (p *keyCache) scheduleSync(ctx context.Context, keySyncTimeout time.Duratio timer := time.NewTimer(p.verificationKeys.Load().NextRefreshHint.Sub(time.Now())) defer timer.Stop() - var lastDataTimestamp time.Time for { select { case <-ctx.Done(): @@ -76,16 +75,11 @@ func (p *keyCache) scheduleSync(ctx context.Context, keySyncTimeout time.Duratio } timedCtx, cancel := context.WithTimeout(ctx, keySyncTimeout) - dataTimestamp, err := p.syncKeys(timedCtx) - if err != nil { + if err := p.syncKeys(timedCtx); err != nil { klog.Errorf("when syncing supported public keys(Stale set of keys will be supported): %v", err) timer.Reset(fallbackRefreshDuration) } else { timer.Reset(p.verificationKeys.Load().NextRefreshHint.Sub(time.Now())) - if lastDataTimestamp.IsZero() || !dataTimestamp.Equal(lastDataTimestamp) { - lastDataTimestamp = dataTimestamp - p.broadcastUpdate() - } } cancel() } @@ -115,7 +109,7 @@ func (p *keyCache) GetPublicKeys(ctx context.Context, keyID string) []serviceacc } // If we didn't find it, trigger a sync. - if _, err := p.syncKeys(ctx); err != nil { + if err := p.syncKeys(ctx); err != nil { klog.ErrorS(err, "Error while syncing keys") return []serviceaccount.PublicKey{} } @@ -152,8 +146,9 @@ func (p *keyCache) findKeyForKeyID(keyID string) ([]serviceaccount.PublicKey, bo // sync supported external keys. // completely re-writes the set of supported keys. -func (p *keyCache) syncKeys(ctx context.Context) (time.Time, error) { - val, err, _ := p.syncGroup.Do("", func() (any, error) { +func (p *keyCache) syncKeys(ctx context.Context) error { + _, err, _ := p.syncGroup.Do("", func() (any, error) { + oldPublicKeys := p.verificationKeys.Load() newPublicKeys, err := p.getTokenVerificationKeys(ctx) externaljwtmetrics.RecordFetchKeysAttempt(err) if err != nil { @@ -161,18 +156,38 @@ func (p *keyCache) syncKeys(ctx context.Context) (time.Time, error) { } p.verificationKeys.Store(newPublicKeys) - externaljwtmetrics.RecordKeyDataTimeStamp(newPublicKeys.DataTimestamp.Unix()) - return newPublicKeys, nil + if keysChanged(oldPublicKeys, newPublicKeys) { + p.broadcastUpdate() + } + + return nil, nil }) - if err != nil { - return time.Time{}, err + return err +} + +// keysChanged returns true if the data timestamp, key count, order of key ids or excludeFromOIDCDiscovery indicators +func keysChanged(oldPublicKeys, newPublicKeys *VerificationKeys) bool { + // If the timestamp changed, we changed + if !oldPublicKeys.DataTimestamp.Equal(newPublicKeys.DataTimestamp) { + return true } - - vk := val.(*VerificationKeys) - - return vk.DataTimestamp, nil + // Avoid deepequal checks on key content itself. + // If the number of keys changed, we changed + if len(oldPublicKeys.Keys) != len(newPublicKeys.Keys) { + return true + } + // If the order, key id, or oidc discovery flag changed, we changed. + for i := range oldPublicKeys.Keys { + if oldPublicKeys.Keys[i].KeyID != newPublicKeys.Keys[i].KeyID { + return true + } + if oldPublicKeys.Keys[i].ExcludeFromOIDCDiscovery != newPublicKeys.Keys[i].ExcludeFromOIDCDiscovery { + return true + } + } + return false } func (p *keyCache) broadcastUpdate() { @@ -180,7 +195,8 @@ func (p *keyCache) broadcastUpdate() { defer p.listenersLock.Unlock() for _, l := range p.listeners { - l.Enqueue() + // don't block on a slow listener + go l.Enqueue() } } diff --git a/pkg/serviceaccount/externaljwt/plugin/keycache_test.go b/pkg/serviceaccount/externaljwt/plugin/keycache_test.go index dddb4d3d44e..710e0741159 100644 --- a/pkg/serviceaccount/externaljwt/plugin/keycache_test.go +++ b/pkg/serviceaccount/externaljwt/plugin/keycache_test.go @@ -22,6 +22,7 @@ import ( "net" "os" "strings" + "sync/atomic" "testing" "time" @@ -31,6 +32,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/protobuf/types/known/timestamppb" + "k8s.io/apimachinery/pkg/util/wait" externaljwtv1alpha1 "k8s.io/externaljwt/apis/v1alpha1" "k8s.io/kubernetes/pkg/serviceaccount" ) @@ -167,7 +169,7 @@ func TestExternalPublicKeyGetter(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { ctx := context.Background() - sockname := fmt.Sprintf("@test-external-public-key-getter-%d.sock", i) + sockname := fmt.Sprintf("@test-external-public-key-getter-%d-%d.sock", time.Now().Nanosecond(), i) t.Cleanup(func() { _ = os.Remove(sockname) }) addr := &net.UnixAddr{Name: sockname, Net: "unix"} @@ -238,7 +240,7 @@ func TestExternalPublicKeyGetter(t *testing.T) { func TestInitialFill(t *testing.T) { ctx := context.Background() - sockname := "@test-initial-fill.sock" + sockname := fmt.Sprintf("@test-initial-fill-%d.sock", time.Now().Nanosecond()) t.Cleanup(func() { _ = os.Remove(sockname) }) addr := &net.UnixAddr{Name: sockname, Net: "unix"} @@ -304,7 +306,7 @@ func TestInitialFill(t *testing.T) { func TestReflectChanges(t *testing.T) { ctx := context.Background() - sockname := "@test-reflect-changes.sock" + sockname := fmt.Sprintf("@test-reflect-changes-%d.sock", time.Now().Nanosecond()) t.Cleanup(func() { _ = os.Remove(sockname) }) addr := &net.UnixAddr{Name: sockname, Net: "unix"} @@ -357,18 +359,25 @@ func TestReflectChanges(t *testing.T) { plugin := newPlugin("iss", clientConn, true) + dummyListener := &dummyListener{} + plugin.keyCache.AddListener(dummyListener) + + dummyListener.waitForCount(t, 0) if err := plugin.keyCache.initialFill(ctx); err != nil { t.Fatalf("Error during InitialFill: %v", err) } + dummyListener.waitForCount(t, 1) gotPubKeysT1 := plugin.keyCache.GetPublicKeys(ctx, "") if diff := cmp.Diff(gotPubKeysT1, wantPubKeysT1, cmpopts.SortSlices(sortPublicKeySlice)); diff != "" { t.Fatalf("Bad public keys; diff (-got +want)\n%s", diff) } - if _, err := plugin.keyCache.syncKeys(ctx); err != nil { + dummyListener.waitForCount(t, 1) + if err := plugin.keyCache.syncKeys(ctx); err != nil { t.Fatalf("Error while calling syncKeys: %v", err) } + dummyListener.waitForCount(t, 1) supportedKeysT2 := map[string]supportedKeyT{ "key-1": { @@ -396,12 +405,108 @@ func TestReflectChanges(t *testing.T) { backend.supportedKeys = supportedKeysT2 backend.keyLock.Unlock() - if _, err := plugin.keyCache.syncKeys(ctx); err != nil { + dummyListener.waitForCount(t, 1) + if err := plugin.keyCache.syncKeys(ctx); err != nil { t.Fatalf("Error while calling syncKeys: %v", err) } + dummyListener.waitForCount(t, 2) gotPubKeysT2 := plugin.keyCache.GetPublicKeys(ctx, "") if diff := cmp.Diff(gotPubKeysT2, wantPubKeysT2, cmpopts.SortSlices(sortPublicKeySlice)); diff != "" { t.Fatalf("Bad public keys; diff (-got +want)\n%s", diff) } + dummyListener.waitForCount(t, 2) +} + +type dummyListener struct { + count atomic.Int64 +} + +func (d *dummyListener) waitForCount(t *testing.T, expect int) { + t.Helper() + err := wait.PollUntilContextTimeout(context.Background(), time.Millisecond, 10*time.Second, true, func(_ context.Context) (bool, error) { + actual := int(d.count.Load()) + switch { + case actual > expect: + return false, fmt.Errorf("expected %d broadcasts, got %d broadcasts", expect, actual) + case actual == expect: + return true, nil + default: + t.Logf("expected %d broadcasts, got %d broadcasts, waiting...", expect, actual) + return false, nil + } + }) + if err != nil { + t.Fatal(err) + } +} + +func (d *dummyListener) Enqueue() { + d.count.Add(1) +} + +func TestKeysChanged(t *testing.T) { + testcases := []struct { + name string + oldKeys VerificationKeys + newKeys VerificationKeys + expect bool + }{ + { + name: "empty", + oldKeys: VerificationKeys{}, + newKeys: VerificationKeys{}, + expect: false, + }, + { + name: "identical", + oldKeys: VerificationKeys{DataTimestamp: time.Unix(1000, 0), Keys: []serviceaccount.PublicKey{{KeyID: "a"}, {KeyID: "b"}}}, + newKeys: VerificationKeys{DataTimestamp: time.Unix(1000, 0), Keys: []serviceaccount.PublicKey{{KeyID: "a"}, {KeyID: "b"}}}, + expect: false, + }, + { + name: "changed datatimestamp", + oldKeys: VerificationKeys{DataTimestamp: time.Unix(1000, 0), Keys: []serviceaccount.PublicKey{{KeyID: "a"}, {KeyID: "b"}}}, + newKeys: VerificationKeys{DataTimestamp: time.Unix(1001, 0), Keys: []serviceaccount.PublicKey{{KeyID: "a"}, {KeyID: "b"}}}, + expect: true, + }, + { + name: "reordered keyid", + oldKeys: VerificationKeys{DataTimestamp: time.Unix(1000, 0), Keys: []serviceaccount.PublicKey{{KeyID: "a"}, {KeyID: "b"}}}, + newKeys: VerificationKeys{DataTimestamp: time.Unix(1000, 0), Keys: []serviceaccount.PublicKey{{KeyID: "b"}, {KeyID: "a"}}}, + expect: true, + }, + { + name: "changed keyid", + oldKeys: VerificationKeys{DataTimestamp: time.Unix(1000, 0), Keys: []serviceaccount.PublicKey{{KeyID: "a"}}}, + newKeys: VerificationKeys{DataTimestamp: time.Unix(1000, 0), Keys: []serviceaccount.PublicKey{{KeyID: "b"}}}, + expect: true, + }, + { + name: "added key", + oldKeys: VerificationKeys{DataTimestamp: time.Unix(1000, 0), Keys: []serviceaccount.PublicKey{{KeyID: "a"}}}, + newKeys: VerificationKeys{DataTimestamp: time.Unix(1000, 0), Keys: []serviceaccount.PublicKey{{KeyID: "a"}, {KeyID: "b"}}}, + expect: true, + }, + { + name: "removed key", + oldKeys: VerificationKeys{DataTimestamp: time.Unix(1000, 0), Keys: []serviceaccount.PublicKey{{KeyID: "a"}, {KeyID: "b"}}}, + newKeys: VerificationKeys{DataTimestamp: time.Unix(1000, 0), Keys: []serviceaccount.PublicKey{{KeyID: "a"}}}, + expect: true, + }, + { + name: "changed oidc", + oldKeys: VerificationKeys{DataTimestamp: time.Unix(1000, 0), Keys: []serviceaccount.PublicKey{{KeyID: "a", ExcludeFromOIDCDiscovery: false}}}, + newKeys: VerificationKeys{DataTimestamp: time.Unix(1000, 0), Keys: []serviceaccount.PublicKey{{KeyID: "a", ExcludeFromOIDCDiscovery: true}}}, + expect: true, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + result := keysChanged(&tc.oldKeys, &tc.newKeys) + if result != tc.expect { + t.Errorf("got %v, expected %v", result, tc.expect) + } + }) + } } diff --git a/pkg/serviceaccount/externaljwt/plugin/plugin_test.go b/pkg/serviceaccount/externaljwt/plugin/plugin_test.go index 20a4f648a4b..106a42d7769 100644 --- a/pkg/serviceaccount/externaljwt/plugin/plugin_test.go +++ b/pkg/serviceaccount/externaljwt/plugin/plugin_test.go @@ -258,7 +258,7 @@ func TestExternalTokenGenerator(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { ctx := context.Background() - sockname := fmt.Sprintf("@test-external-token-generator-%d.sock", i) + sockname := fmt.Sprintf("@test-external-token-generator-%d-%d.sock", time.Now().Nanosecond(), i) t.Cleanup(func() { _ = os.Remove(sockname) }) addr := &net.UnixAddr{Name: sockname, Net: "unix"} diff --git a/pkg/serviceaccount/externaljwt/plugin/testing/v1alpha1/externalsigner_mock.go b/pkg/serviceaccount/externaljwt/plugin/testing/v1alpha1/externalsigner_mock.go index 146d1c59ff4..a3f19c4fe1e 100644 --- a/pkg/serviceaccount/externaljwt/plugin/testing/v1alpha1/externalsigner_mock.go +++ b/pkg/serviceaccount/externaljwt/plugin/testing/v1alpha1/externalsigner_mock.go @@ -28,7 +28,6 @@ import ( "net" "os" "sync" - "sync/atomic" "testing" "time" @@ -48,10 +47,12 @@ type MockSigner struct { SigningKeyID string SigningAlg string TokenType string - SupportedKeys atomic.Pointer[map[string]KeyT] - AckKeyFetch chan bool MaxTokenExpirationSeconds int64 + supportedKeys map[string]KeyT + supportedKeysLock sync.RWMutex + supportedKeysFetched *sync.Cond + FetchError error MetadataError error errorLock sync.RWMutex @@ -70,9 +71,9 @@ func NewMockSigner(t *testing.T, socketPath string) *MockSigner { m := &MockSigner{ socketPath: socketPath, server: server, - AckKeyFetch: make(chan bool), MaxTokenExpirationSeconds: 10 * 60, // 10m } + m.supportedKeysFetched = sync.NewCond(&m.supportedKeysLock) if err := m.Reset(); err != nil { t.Fatalf("failed to load keys for mock signer: %v", err) @@ -91,6 +92,22 @@ func NewMockSigner(t *testing.T, socketPath string) *MockSigner { return m } +func (m *MockSigner) GetSupportedKeys() map[string]KeyT { + m.supportedKeysLock.RLock() + defer m.supportedKeysLock.RUnlock() + return m.supportedKeys +} +func (m *MockSigner) SetSupportedKeys(keys map[string]KeyT) { + m.supportedKeysLock.Lock() + defer m.supportedKeysLock.Unlock() + m.supportedKeys = keys +} +func (m *MockSigner) WaitForSupportedKeysFetch() { + m.supportedKeysLock.Lock() + defer m.supportedKeysLock.Unlock() + m.supportedKeysFetched.Wait() +} + func (m *MockSigner) Sign(ctx context.Context, req *v1alpha1.SignJWTRequest) (*v1alpha1.SignJWTResponse, error) { header := &struct { @@ -132,18 +149,16 @@ func (m *MockSigner) FetchKeys(ctx context.Context, req *v1alpha1.FetchKeysReque keys := []*v1alpha1.Key{} - for id, k := range *m.SupportedKeys.Load() { + m.supportedKeysLock.RLock() + for id, k := range m.supportedKeys { keys = append(keys, &v1alpha1.Key{ KeyId: id, Key: k.Key, ExcludeFromOidcDiscovery: k.ExcludeFromOidcDiscovery, }) } - - select { - case <-m.AckKeyFetch: - default: - } + m.supportedKeysFetched.Broadcast() + m.supportedKeysLock.RUnlock() return &v1alpha1.FetchKeysResponse{ RefreshHintSeconds: 5, @@ -185,7 +200,7 @@ func (m *MockSigner) Reset() error { m.SigningKeyID = "kid-1" m.SigningAlg = "RS256" m.TokenType = "JWT" - m.SupportedKeys.Store(&map[string]KeyT{ + m.SetSupportedKeys(map[string]KeyT{ "kid-1": {Key: pub1}, "kid-2": {Key: pub2}, "kid-3": {Key: pub3}, diff --git a/test/integration/serviceaccount/external_jwt_signer_test.go b/test/integration/serviceaccount/external_jwt_signer_test.go index 8feb3065091..5d0ee8c775f 100644 --- a/test/integration/serviceaccount/external_jwt_signer_test.go +++ b/test/integration/serviceaccount/external_jwt_signer_test.go @@ -59,7 +59,7 @@ func TestExternalJWTSigningAndAuth(t *testing.T) { defer cancel() // create and start mock signer. - socketPath := "@mock-external-jwt-signer.sock" + socketPath := fmt.Sprintf("@mock-external-jwt-signer-%d.sock", time.Now().Nanosecond()) t.Cleanup(func() { _ = os.Remove(socketPath) }) mockSigner := v1alpha1testing.NewMockSigner(t, socketPath) defer mockSigner.CleanUp() @@ -121,14 +121,14 @@ func TestExternalJWTSigningAndAuth(t *testing.T) { mockSigner.SigningKeyID = "updated-kid-1" cpy := make(map[string]v1alpha1testing.KeyT) - for key, value := range *mockSigner.SupportedKeys.Load() { + for key, value := range mockSigner.GetSupportedKeys() { cpy[key] = value } cpy["updated-kid-1"] = v1alpha1testing.KeyT{ Key: pubKey1Bytes, ExcludeFromOidcDiscovery: true, } - mockSigner.SupportedKeys.Store(&cpy) + mockSigner.SetSupportedKeys(cpy) }, preValidationSignerUpdate: func() { /*no-op*/ }, wantTokenReqErr: fmt.Errorf("failed to generate token: while validating header: key used for signing JWT (kid: updated-kid-1) is excluded from OIDC discovery docs"), @@ -163,7 +163,7 @@ func TestExternalJWTSigningAndAuth(t *testing.T) { mockSigner.SigningKey = key1 }, preValidationSignerUpdate: func() { - mockSigner.SupportedKeys.Store(&map[string]v1alpha1testing.KeyT{}) + mockSigner.SetSupportedKeys(map[string]v1alpha1testing.KeyT{}) }, shouldPassAuth: false, }, @@ -174,12 +174,12 @@ func TestExternalJWTSigningAndAuth(t *testing.T) { }, preValidationSignerUpdate: func() { cpy := make(map[string]v1alpha1testing.KeyT) - for key, value := range *mockSigner.SupportedKeys.Load() { + for key, value := range mockSigner.GetSupportedKeys() { cpy[key] = value } cpy["kid-1"] = v1alpha1testing.KeyT{Key: pubKey1Bytes} - mockSigner.SupportedKeys.Store(&cpy) - mockSigner.AckKeyFetch <- true + mockSigner.SetSupportedKeys(cpy) + mockSigner.WaitForSupportedKeysFetch() }, shouldPassAuth: true, }, @@ -192,7 +192,7 @@ func TestExternalJWTSigningAndAuth(t *testing.T) { if err != nil { t.Fatalf("failed to reset signer for the test %q: %v", tc.desc, err) } - mockSigner.AckKeyFetch <- true + mockSigner.WaitForSupportedKeysFetch() // Adjust parameters on mock signer for the test. tc.preTestSignerUpdate() @@ -227,7 +227,7 @@ func TestExternalJWTSigningAndAuth(t *testing.T) { } if !tokenReviewResult.Status.Authenticated && tc.shouldPassAuth { - t.Fatal("Expected Authentication to succeed") + t.Fatalf("Expected Authentication to succeed, got %v", tokenReviewResult.Status.Error) } else if tokenReviewResult.Status.Authenticated && !tc.shouldPassAuth { t.Fatal("Expected Authentication to fail") } diff --git a/third_party/protobuf/google/protobuf/timestamp.proto b/third_party/protobuf/google/protobuf/timestamp.proto new file mode 100644 index 00000000000..d0698db6802 --- /dev/null +++ b/third_party/protobuf/google/protobuf/timestamp.proto @@ -0,0 +1,144 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/protobuf/types/known/timestamppb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "TimestampProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; + +// A Timestamp represents a point in time independent of any time zone or local +// calendar, encoded as a count of seconds and fractions of seconds at +// nanosecond resolution. The count is relative to an epoch at UTC midnight on +// January 1, 1970, in the proleptic Gregorian calendar which extends the +// Gregorian calendar backwards to year one. +// +// All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap +// second table is needed for interpretation, using a [24-hour linear +// smear](https://developers.google.com/time/smear). +// +// The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By +// restricting to that range, we ensure that we can convert to and from [RFC +// 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. +// +// # Examples +// +// Example 1: Compute Timestamp from POSIX `time()`. +// +// Timestamp timestamp; +// timestamp.set_seconds(time(NULL)); +// timestamp.set_nanos(0); +// +// Example 2: Compute Timestamp from POSIX `gettimeofday()`. +// +// struct timeval tv; +// gettimeofday(&tv, NULL); +// +// Timestamp timestamp; +// timestamp.set_seconds(tv.tv_sec); +// timestamp.set_nanos(tv.tv_usec * 1000); +// +// Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. +// +// FILETIME ft; +// GetSystemTimeAsFileTime(&ft); +// UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; +// +// // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z +// // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. +// Timestamp timestamp; +// timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); +// timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); +// +// Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. +// +// long millis = System.currentTimeMillis(); +// +// Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) +// .setNanos((int) ((millis % 1000) * 1000000)).build(); +// +// Example 5: Compute Timestamp from Java `Instant.now()`. +// +// Instant now = Instant.now(); +// +// Timestamp timestamp = +// Timestamp.newBuilder().setSeconds(now.getEpochSecond()) +// .setNanos(now.getNano()).build(); +// +// Example 6: Compute Timestamp from current time in Python. +// +// timestamp = Timestamp() +// timestamp.GetCurrentTime() +// +// # JSON Mapping +// +// In JSON format, the Timestamp type is encoded as a string in the +// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the +// format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" +// where {year} is always expressed using four digits while {month}, {day}, +// {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional +// seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), +// are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone +// is required. A proto3 JSON serializer should always use UTC (as indicated by +// "Z") when printing the Timestamp type and a proto3 JSON parser should be +// able to accept both UTC and other timezones (as indicated by an offset). +// +// For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past +// 01:30 UTC on January 15, 2017. +// +// In JavaScript, one can convert a Date object to this format using the +// standard +// [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) +// method. In Python, a standard `datetime.datetime` object can be converted +// to this format using +// [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with +// the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use +// the Joda Time's [`ISODateTimeFormat.dateTime()`]( +// http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime() +// ) to obtain a formatter capable of generating timestamps in this format. +// +message Timestamp { + // Represents seconds of UTC time since Unix epoch + // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + // 9999-12-31T23:59:59Z inclusive. + int64 seconds = 1; + + // Non-negative fractions of a second at nanosecond resolution. Negative + // second values with fractions must still have non-negative nanos values + // that count forward in time. Must be from 0 to 999,999,999 + // inclusive. + int32 nanos = 2; +} \ No newline at end of file