Skip to content

Commit 4c25cac

Browse files
goog-lukemcbroady
andcommitted
datastore: add EnableKeyConversion for compatibility with Cloud Datastore encoded keys (#192)
Adds compatibility with encoded keys generated by the Cloud Datastore package's (cloud.google.com/go/datastore) Key.Encode function. This package, and the Cloud Datastore package, both use b64-encoded protobufs as the key encoding format, however the protobufs are different, so care must be taken to try and decode to/from both proto formats. Co-authored-by: Luke <[email protected]> Co-authored-by: Chris Broadfoot <[email protected]>
1 parent 54a98f9 commit 4c25cac

File tree

6 files changed

+673
-0
lines changed

6 files changed

+673
-0
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,30 @@ A few APIs were cleaned up, and there are some differences:
7171
[blobstore package](https://google.golang.org/appengine/blobstore).
7272
* `appengine/socket` is not required on App Engine flexible environment / Managed VMs.
7373
Use the standard `net` package instead.
74+
75+
## Key Encode/Decode compatibiltiy to help with datastore library migrations
76+
77+
Key compatibility updates have been added to help customers transition from google.golang.org/appengine/datastore to cloud.google.com/go/datastore.
78+
The `EnableKeyConversion` enables automatic conversion from a key encoded with cloud.google.com/go/datastore to google.golang.org/appengine/datastore key type.
79+
80+
### Enabling key conversion
81+
82+
Enable key conversion by calling `EnableKeyConversion(ctx)` in the `/_ah/startup` handler for basic and manual scaling or any handler in automatic scaling.
83+
84+
#### 1. Basic or manual scaling
85+
86+
This startup handler will enable key conversion for all handlers in the service.
87+
88+
```
89+
http.HandleFunc("/_ah/start", func(w http.ResponseWriter, r *http.Request) {
90+
datastore.EnableKeyConversion(appengine.NewContext(r))
91+
})
92+
```
93+
94+
#### 2. Automatic scaling
95+
96+
`/_ah/start` is not supported for automatic scaling and `/_ah/warmup` is not guaranteed to run, so you must call `datastore.EnableKeyConversion(appengine.NewContext(r))`
97+
before you use code that needs key conversion.
98+
99+
You may want to add this to each of your handlers, or introduce middleware where it's called.
100+
`EnableKeyConversion` is safe for concurrent use. Any call to it after the first is ignored.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright 2019 Google Inc. All rights reserved.
2+
// Use of this source code is governed by the Apache 2.0
3+
// license that can be found in the LICENSE file.
4+
5+
// Package cloudpb is a subset of types and functions, copied from cloud.google.com/go/datastore.
6+
//
7+
// They are copied here to provide compatibility to decode keys generated by the cloud.google.com/go/datastore package.
8+
package cloudkey
9+
10+
import (
11+
"encoding/base64"
12+
"errors"
13+
"strings"
14+
15+
"github.com/golang/protobuf/proto"
16+
cloudpb "google.golang.org/appengine/datastore/internal/cloudpb"
17+
)
18+
19+
/////////////////////////////////////////////////////////////////////
20+
// Code below is copied from https://github.com/googleapis/google-cloud-go/blob/master/datastore/datastore.go
21+
/////////////////////////////////////////////////////////////////////
22+
23+
var (
24+
// ErrInvalidKey is returned when an invalid key is presented.
25+
ErrInvalidKey = errors.New("datastore: invalid key")
26+
)
27+
28+
/////////////////////////////////////////////////////////////////////
29+
// Code below is copied from https://github.com/googleapis/google-cloud-go/blob/master/datastore/key.go
30+
/////////////////////////////////////////////////////////////////////
31+
32+
// Key represents the datastore key for a stored entity.
33+
type Key struct {
34+
// Kind cannot be empty.
35+
Kind string
36+
// Either ID or Name must be zero for the Key to be valid.
37+
// If both are zero, the Key is incomplete.
38+
ID int64
39+
Name string
40+
// Parent must either be a complete Key or nil.
41+
Parent *Key
42+
43+
// Namespace provides the ability to partition your data for multiple
44+
// tenants. In most cases, it is not necessary to specify a namespace.
45+
// See docs on datastore multitenancy for details:
46+
// https://cloud.google.com/datastore/docs/concepts/multitenancy
47+
Namespace string
48+
}
49+
50+
// DecodeKey decodes a key from the opaque representation returned by Encode.
51+
func DecodeKey(encoded string) (*Key, error) {
52+
// Re-add padding.
53+
if m := len(encoded) % 4; m != 0 {
54+
encoded += strings.Repeat("=", 4-m)
55+
}
56+
57+
b, err := base64.URLEncoding.DecodeString(encoded)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
pKey := new(cloudpb.Key)
63+
if err := proto.Unmarshal(b, pKey); err != nil {
64+
return nil, err
65+
}
66+
return protoToKey(pKey)
67+
}
68+
69+
// valid returns whether the key is valid.
70+
func (k *Key) valid() bool {
71+
if k == nil {
72+
return false
73+
}
74+
for ; k != nil; k = k.Parent {
75+
if k.Kind == "" {
76+
return false
77+
}
78+
if k.Name != "" && k.ID != 0 {
79+
return false
80+
}
81+
if k.Parent != nil {
82+
if k.Parent.Incomplete() {
83+
return false
84+
}
85+
if k.Parent.Namespace != k.Namespace {
86+
return false
87+
}
88+
}
89+
}
90+
return true
91+
}
92+
93+
// Incomplete reports whether the key does not refer to a stored entity.
94+
func (k *Key) Incomplete() bool {
95+
return k.Name == "" && k.ID == 0
96+
}
97+
98+
// protoToKey decodes a protocol buffer representation of a key into an
99+
// equivalent *Key object. If the key is invalid, protoToKey will return the
100+
// invalid key along with ErrInvalidKey.
101+
func protoToKey(p *cloudpb.Key) (*Key, error) {
102+
var key *Key
103+
var namespace string
104+
if partition := p.PartitionId; partition != nil {
105+
namespace = partition.NamespaceId
106+
}
107+
for _, el := range p.Path {
108+
key = &Key{
109+
Namespace: namespace,
110+
Kind: el.Kind,
111+
ID: el.GetId(),
112+
Name: el.GetName(),
113+
Parent: key,
114+
}
115+
}
116+
if !key.valid() { // Also detects key == nil.
117+
return key, ErrInvalidKey
118+
}
119+
return key, nil
120+
}

0 commit comments

Comments
 (0)