initial commit
This commit is contained in:
parent
1506be59c6
commit
22fea6f43d
|
@ -0,0 +1,40 @@
|
|||
# Components Contrib
|
||||
|
||||
The purpose of Components Contrib is to provide open, community driven reusable components for building distributed applications.
|
||||
These components are being used by the [Actions](https://github.com/actionscore/actions) project, but are separate and decoupled from it.
|
||||
|
||||
Using components developers can interact with bindings, state stores, messaging systems, bindings and more without caring about the underlying implementation.
|
||||
|
||||
Available component types:
|
||||
|
||||
* [Input/Output Bindings](bindings/Readme.md)
|
||||
* [Pub Sub](pubsub/Readme.md)
|
||||
* [State Stores](state/Readme.md)
|
||||
* [Secret Stores](secretstores/Readme.md)
|
||||
|
||||
For documentation on how components are being used in Actions in a language/platform agnostic way, visit [Actions Docs](https://github.com/actionscore/docs).
|
||||
|
||||
## Developing components
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. The Go language environment [(instructions)](https://golang.org/doc/install).
|
||||
* Make sure that your GOPATH and PATH are configured correctly
|
||||
```bash
|
||||
export GOPATH=~/go
|
||||
export PATH=$PATH:$GOPATH/bin
|
||||
```
|
||||
|
||||
### Clone the repo
|
||||
|
||||
```bash
|
||||
cd $GOPATH/src
|
||||
mkdir -p github.com/actionscore/components-contrib
|
||||
git clone https://github.com/actionscore/components-contrib.git github.com/actionscore/components-contrib
|
||||
```
|
||||
|
||||
### Running tests
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
|
@ -0,0 +1,40 @@
|
|||
# Bindings
|
||||
|
||||
Bindings provide a common way to trigger an application with events from external systems, or invoke an external system with optional data payloads.
|
||||
Bindings are great for event-driven, on-demand compute and help reduce boilerplate code.
|
||||
|
||||
Currently supported bindings are:
|
||||
|
||||
* Kafka
|
||||
* RabbitMQ
|
||||
* AWS SQS
|
||||
* AWS SNS
|
||||
* Azure Eventhubs
|
||||
* Azure CosmosDB
|
||||
* Google Cloud Storage Bucket
|
||||
* HTTP
|
||||
* MQTT
|
||||
* AWS DynamoDB
|
||||
* Redis
|
||||
|
||||
## Implementing a new binding
|
||||
|
||||
A compliant binding needs to implement one or more interfaces, depending on the type of binding (Input or Output):
|
||||
|
||||
Input binding:
|
||||
|
||||
```
|
||||
type InputBinding interface {
|
||||
Init(metadata Metadata) error
|
||||
Read(handler func(*ReadResponse) error) error
|
||||
}
|
||||
```
|
||||
|
||||
Output binding:
|
||||
|
||||
```
|
||||
type OutputBinding interface {
|
||||
Init(metadata Metadata) error
|
||||
Write(req *WriteRequest) error
|
||||
}
|
||||
```
|
|
@ -0,0 +1,101 @@
|
|||
package cosmosdb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/a8m/documentdb"
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
)
|
||||
|
||||
// CosmosDB allows performing state operations on collections
|
||||
type CosmosDB struct {
|
||||
client *documentdb.DocumentDB
|
||||
collection *documentdb.Collection
|
||||
db *documentdb.Database
|
||||
partitionKey string
|
||||
}
|
||||
|
||||
type cosmosDBCredentials struct {
|
||||
URL string `json:"url"`
|
||||
MasterKey string `json:"masterKey"`
|
||||
Database string `json:"database"`
|
||||
Collection string `json:"collection"`
|
||||
PartitionKey string `json:"partitionKey"`
|
||||
}
|
||||
|
||||
// NewCosmosDB returns a new CosmosDB instance
|
||||
func NewCosmosDB() *CosmosDB {
|
||||
return &CosmosDB{}
|
||||
}
|
||||
|
||||
// Init performs CosmosDB connection parsing and connecting
|
||||
func (c *CosmosDB) Init(metadata bindings.Metadata) error {
|
||||
m, err := c.parseMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.partitionKey = m.PartitionKey
|
||||
client := documentdb.New(m.URL, &documentdb.Config{
|
||||
MasterKey: &documentdb.Key{
|
||||
Key: m.MasterKey,
|
||||
},
|
||||
})
|
||||
|
||||
dbs, err := client.QueryDatabases(&documentdb.Query{
|
||||
Query: fmt.Sprintf("SELECT * FROM ROOT r WHERE r.id='%s'", m.Database),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(dbs) == 0 {
|
||||
return fmt.Errorf("Database %s for CosmosDB state store not found", m.Database)
|
||||
}
|
||||
|
||||
c.db = &dbs[0]
|
||||
colls, err := client.QueryCollections(c.db.Self, &documentdb.Query{
|
||||
Query: fmt.Sprintf("SELECT * FROM ROOT r WHERE r.id='%s'", m.Collection),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(colls) == 0 {
|
||||
return fmt.Errorf("Collection %s for CosmosDB state store not found", m.Collection)
|
||||
}
|
||||
|
||||
c.collection = &colls[0]
|
||||
c.client = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CosmosDB) parseMetadata(metadata bindings.Metadata) (*cosmosDBCredentials, error) {
|
||||
connInfo := metadata.Properties
|
||||
b, err := json.Marshal(connInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var creds cosmosDBCredentials
|
||||
err = json.Unmarshal(b, &creds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &creds, nil
|
||||
}
|
||||
|
||||
func (c *CosmosDB) Write(req *bindings.WriteRequest) error {
|
||||
var obj interface{}
|
||||
err := json.Unmarshal(req.Data, &obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if val, ok := obj.(map[string]interface{})[c.partitionKey]; ok && val != "" {
|
||||
_, err = c.client.CreateDocument(c.collection.Self, obj, documentdb.PartitionKey(val))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("missing partitionKey field %s from request body", c.partitionKey)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package cosmosdb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseMetadata(t *testing.T) {
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = map[string]string{"Collection": "a", "Database": "a", "MasterKey": "a", "PartitionKey": "a", "URL": "a"}
|
||||
cosmosDB := CosmosDB{}
|
||||
meta, err := cosmosDB.parseMetadata(m)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "a", meta.Collection)
|
||||
assert.Equal(t, "a", meta.Database)
|
||||
assert.Equal(t, "a", meta.MasterKey)
|
||||
assert.Equal(t, "a", meta.PartitionKey)
|
||||
assert.Equal(t, "a", meta.URL)
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package dynamodb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb"
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
|
||||
)
|
||||
|
||||
//DynamoDB allows performing stateful operations on AWS DynamoDB
|
||||
type DynamoDB struct {
|
||||
client *dynamodb.DynamoDB
|
||||
table string
|
||||
}
|
||||
|
||||
type dynamoDBMetadata struct {
|
||||
Region string `json:"region"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
Table string `json:"table"`
|
||||
}
|
||||
|
||||
// NewDynamoDB returns a new DynamoDB instance
|
||||
func NewDynamoDB() *DynamoDB {
|
||||
return &DynamoDB{}
|
||||
}
|
||||
|
||||
// Init performs connection parsing for DynamoDB
|
||||
func (d *DynamoDB) Init(metadata bindings.Metadata) error {
|
||||
meta, err := d.getDynamoDBMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := d.getClient(meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.client = client
|
||||
d.table = meta.Table
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DynamoDB) Write(req *bindings.WriteRequest) error {
|
||||
var obj interface{}
|
||||
err := json.Unmarshal(req.Data, &obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
item, err := dynamodbattribute.MarshalMap(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
input := &dynamodb.PutItemInput{
|
||||
Item: item,
|
||||
TableName: aws.String(d.table),
|
||||
}
|
||||
|
||||
_, err = d.client.PutItem(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DynamoDB) getDynamoDBMetadata(spec bindings.Metadata) (*dynamoDBMetadata, error) {
|
||||
b, err := json.Marshal(spec.Properties)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var meta dynamoDBMetadata
|
||||
err = json.Unmarshal(b, &meta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &meta, nil
|
||||
}
|
||||
|
||||
func (d *DynamoDB) getClient(meta *dynamoDBMetadata) (*dynamodb.DynamoDB, error) {
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(meta.Region),
|
||||
Credentials: credentials.NewStaticCredentials(meta.AccessKey, meta.SecretKey, ""),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := dynamodb.New(sess)
|
||||
return c, nil
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package dynamodb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseMetadata(t *testing.T) {
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = map[string]string{"AccessKey": "a", "Region": "a", "SecretKey": "a", "Table": "a"}
|
||||
dy := DynamoDB{}
|
||||
meta, err := dy.getDynamoDBMetadata(m)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "a", meta.AccessKey)
|
||||
assert.Equal(t, "a", meta.Region)
|
||||
assert.Equal(t, "a", meta.SecretKey)
|
||||
assert.Equal(t, "a", meta.Table)
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
package eventhubs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
eventhub "github.com/Azure/azure-event-hubs-go"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
)
|
||||
|
||||
// AzureEventHubs allows sending/receiving Azure Event Hubs events
|
||||
type AzureEventHubs struct {
|
||||
hub *eventhub.Hub
|
||||
metadata *azureEventHubsMetadata
|
||||
}
|
||||
|
||||
type azureEventHubsMetadata struct {
|
||||
ConnectionString string `json:"connectionString"`
|
||||
ConsumerGroup string `json:"consumerGroup"`
|
||||
MessageAge string `json:"messageAge"`
|
||||
}
|
||||
|
||||
// NewAzureEventHubs returns a new Azure Event hubs instance
|
||||
func NewAzureEventHubs() *AzureEventHubs {
|
||||
return &AzureEventHubs{}
|
||||
}
|
||||
|
||||
// Init performs metadata init
|
||||
func (a *AzureEventHubs) Init(metadata bindings.Metadata) error {
|
||||
m, err := a.parseMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.metadata = m
|
||||
hub, err := eventhub.NewHubFromConnectionString(a.metadata.ConnectionString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.hub = hub
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AzureEventHubs) parseMetadata(metadata bindings.Metadata) (*azureEventHubsMetadata, error) {
|
||||
b, err := json.Marshal(metadata.Properties)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var eventHubsMetadata azureEventHubsMetadata
|
||||
err = json.Unmarshal(b, &eventHubsMetadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &eventHubsMetadata, nil
|
||||
}
|
||||
|
||||
// Write posts an event hubs message
|
||||
func (a *AzureEventHubs) Write(req *bindings.WriteRequest) error {
|
||||
err := a.hub.Send(context.Background(), &eventhub.Event{
|
||||
Data: req.Data,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read gets messages from eventhubs in a non-blocking fashion
|
||||
func (a *AzureEventHubs) Read(handler func(*bindings.ReadResponse) error) error {
|
||||
callback := func(c context.Context, event *eventhub.Event) error {
|
||||
if a.metadata.MessageAge != "" && event.SystemProperties != nil && event.SystemProperties.EnqueuedTime != nil {
|
||||
enqTime := *event.SystemProperties.EnqueuedTime
|
||||
d, err := time.ParseDuration(a.metadata.MessageAge)
|
||||
if err != nil {
|
||||
log.Errorf("error parsing duration: %s", err)
|
||||
return nil
|
||||
} else if time.Now().UTC().Sub(enqTime) > d {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if event != nil {
|
||||
handler(&bindings.ReadResponse{
|
||||
Data: event.Data,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
runtimeInfo, err := a.hub.GetRuntimeInformation(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ops := []eventhub.ReceiveOption{
|
||||
eventhub.ReceiveWithLatestOffset(),
|
||||
}
|
||||
|
||||
if a.metadata.ConsumerGroup != "" {
|
||||
log.Infof("eventhubs: using consumer group %s", a.metadata.ConsumerGroup)
|
||||
ops = append(ops, eventhub.ReceiveWithConsumerGroup(a.metadata.ConsumerGroup))
|
||||
|
||||
}
|
||||
|
||||
for _, partitionID := range runtimeInfo.PartitionIDs {
|
||||
_, err := a.hub.Receive(ctx, partitionID, callback, ops...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan, os.Interrupt, os.Kill)
|
||||
<-signalChan
|
||||
|
||||
a.hub.Close(context.Background())
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package eventhubs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseMetadata(t *testing.T) {
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = map[string]string{"connectionString": "a", "consumerGroup": "a", "messageAge": "a"}
|
||||
eh := AzureEventHubs{}
|
||||
meta, err := eh.parseMetadata(m)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "a", meta.ConnectionString)
|
||||
assert.Equal(t, "a", meta.ConsumerGroup)
|
||||
assert.Equal(t, "a", meta.MessageAge)
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package gcpbucket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"cloud.google.com/go/storage"
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
// GCPStorage allows saving data to GCP bucket storage
|
||||
type GCPStorage struct {
|
||||
metadata gcpMetadata
|
||||
client *storage.Client
|
||||
}
|
||||
|
||||
type gcpMetadata struct {
|
||||
Bucket string `json:"bucket"`
|
||||
Type string `json:"type"`
|
||||
ProjectID string `json:"project_id"`
|
||||
PrivateKeyID string `json:"private_key_id"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
ClientEmail string `json:"client_email"`
|
||||
ClientID string `json:"client_id"`
|
||||
AuthURI string `json:"auth_uri"`
|
||||
TokenURI string `json:"token_uri"`
|
||||
AuthProviderCertURL string `json:"auth_provider_x509_cert_url"`
|
||||
ClientCertURL string `json:"client_x509_cert_url"`
|
||||
}
|
||||
|
||||
// NewGCPStorage returns a new GCP storage instance
|
||||
func NewGCPStorage() *GCPStorage {
|
||||
return &GCPStorage{}
|
||||
}
|
||||
|
||||
// Init performs connction parsing
|
||||
func (g *GCPStorage) Init(metadata bindings.Metadata) error {
|
||||
b, err := g.parseMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var gm gcpMetadata
|
||||
err = json.Unmarshal(b, &gm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clientOptions := option.WithCredentialsJSON(b)
|
||||
ctx := context.Background()
|
||||
client, err := storage.NewClient(ctx, clientOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g.metadata = gm
|
||||
g.client = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GCPStorage) parseMetadata(metadata bindings.Metadata) ([]byte, error) {
|
||||
b, err := json.Marshal(metadata.Properties)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (g *GCPStorage) Write(req *bindings.WriteRequest) error {
|
||||
name := ""
|
||||
if val, ok := req.Metadata["name"]; ok && val != "" {
|
||||
name = val
|
||||
} else {
|
||||
name = uuid.NewV4().String()
|
||||
}
|
||||
h := g.client.Bucket(g.metadata.Bucket).Object(name).NewWriter(context.Background())
|
||||
defer h.Close()
|
||||
if _, err := h.Write(req.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package gcpbucket
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = map[string]string{"auth_provider_x509_cert_url": "a", "auth_uri": "a", "Bucket": "a", "client_x509_cert_url": "a", "client_email": "a", "client_id": "a", "private_key": "a",
|
||||
"private_key_id": "a", "project_id": "a", "token_uri": "a", "type": "a"}
|
||||
gs := GCPStorage{}
|
||||
b, err := gs.parseMetadata(m)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var gm gcpMetadata
|
||||
err = json.Unmarshal(b, &gm)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, "a", gm.AuthProviderCertURL)
|
||||
assert.Equal(t, "a", gm.AuthURI)
|
||||
assert.Equal(t, "a", gm.Bucket)
|
||||
assert.Equal(t, "a", gm.ClientCertURL)
|
||||
assert.Equal(t, "a", gm.ClientEmail)
|
||||
assert.Equal(t, "a", gm.ClientID)
|
||||
assert.Equal(t, "a", gm.PrivateKey)
|
||||
assert.Equal(t, "a", gm.PrivateKeyID)
|
||||
assert.Equal(t, "a", gm.ProjectID)
|
||||
assert.Equal(t, "a", gm.TokenURI)
|
||||
assert.Equal(t, "a", gm.Type)
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
)
|
||||
|
||||
// HTTPSource is a binding for an http url endpoint invocation
|
||||
type HTTPSource struct {
|
||||
metadata httpMetadata
|
||||
}
|
||||
|
||||
type httpMetadata struct {
|
||||
URL string `json:"url"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
// NewHTTP returns a new HTTPSource
|
||||
func NewHTTP() *HTTPSource {
|
||||
return &HTTPSource{}
|
||||
}
|
||||
|
||||
// Init performs metadata parsing
|
||||
func (h *HTTPSource) Init(metadata bindings.Metadata) error {
|
||||
b, err := json.Marshal(metadata.Properties)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var m httpMetadata
|
||||
err = json.Unmarshal(b, &m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.metadata = m
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HTTPSource) get(url string) ([]byte, error) {
|
||||
client := http.Client{Timeout: time.Second * 60}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp != nil && resp.Body != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (h *HTTPSource) Read(handler func(*bindings.ReadResponse) error) error {
|
||||
b, err := h.get(h.metadata.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler(&bindings.ReadResponse{
|
||||
Data: b,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HTTPSource) Write(req *bindings.WriteRequest) error {
|
||||
client := http.Client{Timeout: time.Second * 5}
|
||||
resp, err := client.Post(h.metadata.URL, "application/json; charset=utf-8", bytes.NewBuffer(req.Data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp != nil && resp.Body != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = map[string]string{"url": "a", "method": "a"}
|
||||
hs := HTTPSource{}
|
||||
err := hs.Init(m)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "a", hs.metadata.URL)
|
||||
assert.Equal(t, "a", hs.metadata.Method)
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package bindings
|
||||
|
||||
type InputBinding interface {
|
||||
Init(metadata Metadata) error
|
||||
Read(handler func(*ReadResponse) error) error
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
package kafka
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/Shopify/sarama"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
)
|
||||
|
||||
// Kafka allows reading/writing to a Kafka consumer group
|
||||
type Kafka struct {
|
||||
producer sarama.SyncProducer
|
||||
topics []string
|
||||
consumerGroup string
|
||||
brokers []string
|
||||
publishTopic string
|
||||
}
|
||||
|
||||
type kafkaMetadata struct {
|
||||
Brokers []string `json:"brokers"`
|
||||
Topics []string `json:"topics"`
|
||||
PublishTopic string `json:"publishTopic"`
|
||||
ConsumerGroup string `json:"consumerGroup"`
|
||||
}
|
||||
|
||||
type consumer struct {
|
||||
ready chan bool
|
||||
callback func(*bindings.ReadResponse) error
|
||||
}
|
||||
|
||||
func (consumer *consumer) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
|
||||
for message := range claim.Messages() {
|
||||
if consumer.callback != nil {
|
||||
err := consumer.callback(&bindings.ReadResponse{
|
||||
Data: message.Value,
|
||||
})
|
||||
if err == nil {
|
||||
session.MarkMessage(message, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (consumer *consumer) Setup(sarama.ConsumerGroupSession) error {
|
||||
close(consumer.ready)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewKafka returns a new kafka binding instance
|
||||
func NewKafka() *Kafka {
|
||||
return &Kafka{}
|
||||
}
|
||||
|
||||
// Init does metadata parsing and connection establishment
|
||||
func (k *Kafka) Init(metadata bindings.Metadata) error {
|
||||
meta, err := k.getKafkaMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p, err := k.getSyncProducer(meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k.brokers = meta.Brokers
|
||||
k.producer = p
|
||||
k.topics = meta.Topics
|
||||
k.publishTopic = meta.PublishTopic
|
||||
k.consumerGroup = meta.ConsumerGroup
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Kafka) Write(req *bindings.WriteRequest) error {
|
||||
_, _, err := k.producer.SendMessage(&sarama.ProducerMessage{
|
||||
Topic: k.publishTopic,
|
||||
Value: sarama.ByteEncoder(req.Data),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetKafkaMetadata returns new Kafka metadata
|
||||
func (k *Kafka) getKafkaMetadata(metadata bindings.Metadata) (*kafkaMetadata, error) {
|
||||
meta := kafkaMetadata{}
|
||||
meta.ConsumerGroup = metadata.Properties["consumerGroup"]
|
||||
meta.PublishTopic = metadata.Properties["publishTopic"]
|
||||
|
||||
if val, ok := metadata.Properties["brokers"]; ok && val != "" {
|
||||
meta.Brokers = strings.Split(val, ",")
|
||||
}
|
||||
if val, ok := metadata.Properties["topics"]; ok && val != "" {
|
||||
meta.Topics = strings.Split(val, ",")
|
||||
}
|
||||
return &meta, nil
|
||||
}
|
||||
|
||||
func (k *Kafka) getSyncProducer(meta *kafkaMetadata) (sarama.SyncProducer, error) {
|
||||
config := sarama.NewConfig()
|
||||
config.Producer.RequiredAcks = sarama.WaitForAll
|
||||
config.Producer.Retry.Max = 5
|
||||
config.Producer.Return.Successes = true
|
||||
|
||||
producer, err := sarama.NewSyncProducer(meta.Brokers, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return producer, nil
|
||||
}
|
||||
|
||||
func (k *Kafka) Read(handler func(*bindings.ReadResponse) error) error {
|
||||
config := sarama.NewConfig()
|
||||
config.Version = sarama.V1_0_0_0
|
||||
|
||||
consumer := consumer{
|
||||
callback: handler,
|
||||
ready: make(chan bool),
|
||||
}
|
||||
|
||||
client, err := sarama.NewConsumerGroup(k.brokers, k.consumerGroup, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
if err = client.Consume(ctx, k.topics, &consumer); err != nil {
|
||||
log.Errorf("error from consumer: %s", err)
|
||||
}
|
||||
// check if context was cancelled, signaling that the consumer should stop
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
consumer.ready = make(chan bool)
|
||||
}
|
||||
}()
|
||||
|
||||
<-consumer.ready
|
||||
|
||||
sigterm := make(chan os.Signal, 1)
|
||||
signal.Notify(sigterm, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigterm
|
||||
cancel()
|
||||
wg.Wait()
|
||||
if err = client.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (consumer *consumer) Cleanup(sarama.ConsumerGroupSession) error {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package kafka
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseMetadata(t *testing.T) {
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = map[string]string{"consumerGroup": "a", "publishTopic": "a", "brokers": "a", "topics": "a"}
|
||||
k := Kafka{}
|
||||
meta, err := k.getKafkaMetadata(m)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "a", meta.Brokers[0])
|
||||
assert.Equal(t, "a", meta.ConsumerGroup)
|
||||
assert.Equal(t, "a", meta.PublishTopic)
|
||||
assert.Equal(t, "a", meta.Topics[0])
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package bindings
|
||||
|
||||
// Metadata represents a set of binding specific properties
|
||||
type Metadata struct {
|
||||
Name string
|
||||
Properties map[string]string `json:"properties"`
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package mqtt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MQTT allows sending and receving data to/from an MQTT broker
|
||||
type MQTT struct {
|
||||
metadata *mqttMetadata
|
||||
client mqtt.Client
|
||||
}
|
||||
|
||||
// Metadata is the MQTT config
|
||||
type mqttMetadata struct {
|
||||
URL string `json:"url"`
|
||||
Topic string `json:"topic"`
|
||||
}
|
||||
|
||||
// NewMQTT returns a new MQTT instance
|
||||
func NewMQTT() *MQTT {
|
||||
return &MQTT{}
|
||||
}
|
||||
|
||||
// Init does MQTT connection parsing
|
||||
func (m *MQTT) Init(metadata bindings.Metadata) error {
|
||||
mqttMeta, err := m.getMQTTMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.metadata = mqttMeta
|
||||
if m.metadata.URL == "" {
|
||||
return errors.New("MQTT Error: URL required")
|
||||
}
|
||||
|
||||
if m.metadata.Topic == "" {
|
||||
return errors.New("MQTT error: topic required")
|
||||
}
|
||||
|
||||
uri, err := url.Parse(m.metadata.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := m.connect(uuid.New().String(), uri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.client = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MQTT) getMQTTMetadata(metadata bindings.Metadata) (*mqttMetadata, error) {
|
||||
b, err := json.Marshal(metadata.Properties)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var mqttMetadata mqttMetadata
|
||||
err = json.Unmarshal(b, &mqttMetadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mqttMetadata, nil
|
||||
}
|
||||
|
||||
func (m *MQTT) Write(req *bindings.WriteRequest) error {
|
||||
m.client.Publish(m.metadata.Topic, 0, false, string(req.Data))
|
||||
m.client.Disconnect(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MQTT) Read(handler func(*bindings.ReadResponse) error) error {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
m.client.Subscribe(m.metadata.Topic, 0, func(client mqtt.Client, msg mqtt.Message) {
|
||||
handler(&bindings.ReadResponse{
|
||||
Data: msg.Payload(),
|
||||
})
|
||||
})
|
||||
<-c
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MQTT) connect(clientID string, uri *url.URL) (mqtt.Client, error) {
|
||||
opts := m.createClientOptions(clientID, uri)
|
||||
client := mqtt.NewClient(opts)
|
||||
token := client.Connect()
|
||||
for !token.WaitTimeout(3 * time.Second) {
|
||||
}
|
||||
if err := token.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (m *MQTT) createClientOptions(clientID string, uri *url.URL) *mqtt.ClientOptions {
|
||||
opts := mqtt.NewClientOptions()
|
||||
opts.AddBroker(fmt.Sprintf("tcp://%s", uri.Host))
|
||||
opts.SetUsername(uri.User.Username())
|
||||
password, _ := uri.User.Password()
|
||||
opts.SetPassword(password)
|
||||
opts.SetClientID(clientID)
|
||||
return opts
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package mqtt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseMetadata(t *testing.T) {
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = map[string]string{"URL": "a", "Topic": "a"}
|
||||
mq := MQTT{}
|
||||
mm, err := mq.getMQTTMetadata(m)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "a", mm.URL)
|
||||
assert.Equal(t, "a", mm.Topic)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package bindings
|
||||
|
||||
type OutputBinding interface {
|
||||
Init(metadata Metadata) error
|
||||
Write(req *WriteRequest) error
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package rabbitmq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
"github.com/streadway/amqp"
|
||||
)
|
||||
|
||||
// RabbitMQ allows sending/receving data to/from RabbitMQ
|
||||
type RabbitMQ struct {
|
||||
connection *amqp.Connection
|
||||
channel *amqp.Channel
|
||||
metadata *rabbitMQMetadata
|
||||
}
|
||||
|
||||
// Metadata is the rabbitmq config
|
||||
type rabbitMQMetadata struct {
|
||||
QueueName string `json:"queueName"`
|
||||
Host string `json:"host"`
|
||||
Durable bool `json:"durable,string"`
|
||||
DeleteWhenUnused bool `json:"deleteWhenUnused,string"`
|
||||
}
|
||||
|
||||
// NewRabbitMQ returns a new rabbitmq instance
|
||||
func NewRabbitMQ() *RabbitMQ {
|
||||
return &RabbitMQ{}
|
||||
}
|
||||
|
||||
// Init does metadata parsing and connection creation
|
||||
func (r *RabbitMQ) Init(metadata bindings.Metadata) error {
|
||||
meta, err := r.getRabbitMQMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.metadata = meta
|
||||
|
||||
conn, err := amqp.Dial(meta.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch, err := conn.Channel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.connection = conn
|
||||
r.channel = ch
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RabbitMQ) Write(req *bindings.WriteRequest) error {
|
||||
q, err := r.channel.QueueDeclare(r.metadata.QueueName, r.metadata.Durable, r.metadata.DeleteWhenUnused, false, false, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.channel.Publish("", q.Name, false, false, amqp.Publishing{
|
||||
ContentType: "text/plain",
|
||||
Body: req.Data,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RabbitMQ) getRabbitMQMetadata(metadata bindings.Metadata) (*rabbitMQMetadata, error) {
|
||||
b, err := json.Marshal(metadata.Properties)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rabbitMQMeta rabbitMQMetadata
|
||||
err = json.Unmarshal(b, &rabbitMQMeta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rabbitMQMeta, nil
|
||||
}
|
||||
|
||||
func (r *RabbitMQ) Read(handler func(*bindings.ReadResponse) error) error {
|
||||
q, err := r.channel.QueueDeclare(r.metadata.QueueName, r.metadata.Durable, r.metadata.DeleteWhenUnused, false, false, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msgs, err := r.channel.Consume(
|
||||
q.Name,
|
||||
"",
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
forever := make(chan bool)
|
||||
|
||||
go func() {
|
||||
for d := range msgs {
|
||||
err := handler(&bindings.ReadResponse{
|
||||
Data: d.Body,
|
||||
})
|
||||
if err == nil {
|
||||
r.channel.Ack(d.DeliveryTag, false)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-forever
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package rabbitmq
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseMetadata(t *testing.T) {
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = map[string]string{"QueueName": "a", "Host": "a", "DeleteWhenUnused": "true", "Durable": "true"}
|
||||
r := RabbitMQ{}
|
||||
rm, err := r.getRabbitMQMetadata(m)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "a", rm.QueueName)
|
||||
assert.Equal(t, "a", rm.Host)
|
||||
assert.Equal(t, true, rm.DeleteWhenUnused)
|
||||
assert.Equal(t, true, rm.Durable)
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
|
||||
"github.com/joomcode/redispipe/redis"
|
||||
"github.com/joomcode/redispipe/redisconn"
|
||||
)
|
||||
|
||||
// Redis is a redis output binding
|
||||
type Redis struct {
|
||||
client *redis.SyncCtx
|
||||
}
|
||||
|
||||
type redisMetadata struct {
|
||||
Host string `json:"redisHost"`
|
||||
Password string `json:"redisPassword"`
|
||||
}
|
||||
|
||||
// NewRedis returns a new redis bindings instance
|
||||
func NewRedis() *Redis {
|
||||
return &Redis{}
|
||||
}
|
||||
|
||||
// Init performs metadata parsing and connection creation
|
||||
func (r *Redis) Init(metadata bindings.Metadata) error {
|
||||
m, err := r.parseMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := context.Background()
|
||||
opts := redisconn.Opts{
|
||||
DB: 0,
|
||||
Password: m.Password,
|
||||
}
|
||||
conn, err := redisconn.Connect(ctx, m.Host, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.client = &redis.SyncCtx{
|
||||
S: conn,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Redis) parseMetadata(metadata bindings.Metadata) (*redisMetadata, error) {
|
||||
connInfo := metadata.Properties
|
||||
b, err := json.Marshal(connInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var m redisMetadata
|
||||
err = json.Unmarshal(b, &m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (r *Redis) Write(req *bindings.WriteRequest) error {
|
||||
if val, ok := req.Metadata["key"]; ok && val != "" {
|
||||
key := fmt.Sprintf(val)
|
||||
res := r.client.Do(context.Background(), "SET", key, req.Data)
|
||||
if err := redis.AsError(res); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return errors.New("redis binding: missing key on write request metadata")
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package redis
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseMetadata(t *testing.T) {
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = map[string]string{"redisHost": "a", "redisPassword": "a"}
|
||||
r := Redis{}
|
||||
redisM, err := r.parseMetadata(m)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "a", redisM.Host)
|
||||
assert.Equal(t, "a", redisM.Password)
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package bindings
|
||||
|
||||
import (
|
||||
"github.com/actionscore/components-contrib/state"
|
||||
)
|
||||
|
||||
// ReadResponse is an the return object from an actions input binding
|
||||
type ReadResponse struct {
|
||||
Data []byte `json:"data"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
// WriteRequest is the object given to an actions output binding
|
||||
type WriteRequest struct {
|
||||
Data []byte `json:"data"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
// AppResponse is the object describing the response from user code after a bindings event
|
||||
type AppResponse struct {
|
||||
Data interface{} `json:"data"`
|
||||
To []string `json:"to"`
|
||||
State []state.SetRequest `json:"state"`
|
||||
Concurrency string `json:"concurrency"`
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package sns
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/sns"
|
||||
)
|
||||
|
||||
// AWSSNS is an AWS SNS binding
|
||||
type AWSSNS struct {
|
||||
client *sns.SNS
|
||||
topicARN string
|
||||
}
|
||||
|
||||
type snsMetadata struct {
|
||||
TopicArn string `json:"topicArn"`
|
||||
Region string `json:"region"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type dataPayload struct {
|
||||
Message interface{} `json:"message"`
|
||||
Subject interface{} `json:"subject"`
|
||||
}
|
||||
|
||||
// NewAWSSNS creates a new AWSSNS binding instance
|
||||
func NewAWSSNS() *AWSSNS {
|
||||
return &AWSSNS{}
|
||||
}
|
||||
|
||||
// Init does metadata parsing
|
||||
func (a *AWSSNS) Init(metadata bindings.Metadata) error {
|
||||
m, err := a.parseMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := a.getClient(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.client = client
|
||||
a.topicARN = m.TopicArn
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AWSSNS) parseMetadata(metadata bindings.Metadata) (*snsMetadata, error) {
|
||||
b, err := json.Marshal(metadata.Properties)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var m snsMetadata
|
||||
err = json.Unmarshal(b, &m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (a *AWSSNS) getClient(metadata *snsMetadata) (*sns.SNS, error) {
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(metadata.Region),
|
||||
Credentials: credentials.NewStaticCredentials(metadata.AccessKey, metadata.SecretKey, ""),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := sns.New(sess)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (a *AWSSNS) Write(req *bindings.WriteRequest) error {
|
||||
var payload dataPayload
|
||||
err := json.Unmarshal(req.Data, &payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("%v", payload.Message)
|
||||
subject := fmt.Sprintf("%v", payload.Subject)
|
||||
|
||||
input := &sns.PublishInput{
|
||||
Message: &msg,
|
||||
Subject: &subject,
|
||||
TopicArn: &a.topicARN,
|
||||
}
|
||||
|
||||
_, err = a.client.Publish(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package sns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseMetadata(t *testing.T) {
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = map[string]string{"TopicArn": "a", "Region": "a", "AccessKey": "a", "SecretKey": "a"}
|
||||
s := AWSSNS{}
|
||||
snsM, err := s.parseMetadata(m)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "a", snsM.TopicArn)
|
||||
assert.Equal(t, "a", snsM.Region)
|
||||
assert.Equal(t, "a", snsM.AccessKey)
|
||||
assert.Equal(t, "a", snsM.SecretKey)
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package sqs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/sqs"
|
||||
)
|
||||
|
||||
// AWSSQS allows receiving and sending data to/from AWS SQS
|
||||
type AWSSQS struct {
|
||||
Client *sqs.SQS
|
||||
QueueURL *string
|
||||
}
|
||||
|
||||
type sqsMetadata struct {
|
||||
QueueName string `json:"queueName"`
|
||||
Region string `json:"region"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
// NewAWSSQS returns a new AWS SQS instance
|
||||
func NewAWSSQS() *AWSSQS {
|
||||
return &AWSSQS{}
|
||||
}
|
||||
|
||||
// Init does metadata parsing and connection creation
|
||||
func (a *AWSSQS) Init(metadata bindings.Metadata) error {
|
||||
m, err := a.parseSQSMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := a.getClient(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
queueName := m.QueueName
|
||||
resultURL, err := client.GetQueueUrl(&sqs.GetQueueUrlInput{
|
||||
QueueName: aws.String(queueName),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.QueueURL = resultURL.QueueUrl
|
||||
a.Client = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AWSSQS) Write(req *bindings.WriteRequest) error {
|
||||
msgBody := string(req.Data)
|
||||
_, err := a.Client.SendMessage(&sqs.SendMessageInput{
|
||||
MessageBody: &msgBody,
|
||||
QueueUrl: a.QueueURL,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *AWSSQS) Read(handler func(*bindings.ReadResponse) error) error {
|
||||
for {
|
||||
result, err := a.Client.ReceiveMessage(&sqs.ReceiveMessageInput{
|
||||
QueueUrl: a.QueueURL,
|
||||
AttributeNames: aws.StringSlice([]string{
|
||||
"SentTimestamp",
|
||||
}),
|
||||
MaxNumberOfMessages: aws.Int64(1),
|
||||
MessageAttributeNames: aws.StringSlice([]string{
|
||||
"All",
|
||||
}),
|
||||
WaitTimeSeconds: aws.Int64(20),
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("Unable to receive message from queue %q, %v.", *a.QueueURL, err)
|
||||
}
|
||||
|
||||
if len(result.Messages) > 0 {
|
||||
for _, m := range result.Messages {
|
||||
body := m.Body
|
||||
res := bindings.ReadResponse{
|
||||
Data: []byte(*body),
|
||||
}
|
||||
err := handler(&res)
|
||||
if err == nil {
|
||||
msgHandle := m.ReceiptHandle
|
||||
|
||||
a.Client.DeleteMessage(&sqs.DeleteMessageInput{
|
||||
QueueUrl: a.QueueURL,
|
||||
ReceiptHandle: msgHandle,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * 50)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AWSSQS) parseSQSMetadata(metadata bindings.Metadata) (*sqsMetadata, error) {
|
||||
b, err := json.Marshal(metadata.Properties)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var m sqsMetadata
|
||||
err = json.Unmarshal(b, &m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (a *AWSSQS) getClient(metadata *sqsMetadata) (*sqs.SQS, error) {
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(metadata.Region),
|
||||
Credentials: credentials.NewStaticCredentials(metadata.AccessKey, metadata.SecretKey, ""),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := sqs.New(sess)
|
||||
return c, nil
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package sqs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/actionscore/components-contrib/bindings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseMetadata(t *testing.T) {
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = map[string]string{"QueueName": "a", "Region": "a", "AccessKey": "a", "SecretKey": "a"}
|
||||
s := AWSSQS{}
|
||||
sqsM, err := s.parseSQSMetadata(m)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "a", sqsM.QueueName)
|
||||
assert.Equal(t, "a", sqsM.Region)
|
||||
assert.Equal(t, "a", sqsM.AccessKey)
|
||||
assert.Equal(t, "a", sqsM.SecretKey)
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
module github.com/actionscore/components-contrib
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.0.0
|
||||
contrib.go.opencensus.io/exporter/ocagent v0.6.0 // indirect
|
||||
github.com/Azure/azure-event-hubs-go v1.3.1
|
||||
github.com/Azure/azure-sdk-for-go v29.0.0+incompatible
|
||||
github.com/Azure/go-autorest v12.0.0+incompatible
|
||||
github.com/DataDog/zstd v1.4.1 // indirect
|
||||
github.com/Shopify/sarama v1.23.1
|
||||
github.com/Sirupsen/logrus v1.0.6
|
||||
github.com/a8m/documentdb v1.2.0
|
||||
github.com/aws/aws-sdk-go v1.25.0
|
||||
github.com/eapache/go-resiliency v1.2.0 // indirect
|
||||
github.com/eclipse/paho.mqtt.golang v1.2.0
|
||||
github.com/frankban/quicktest v1.5.0 // indirect
|
||||
github.com/garyburd/redigo v1.6.0 // indirect
|
||||
github.com/go-redis/redis v6.15.5+incompatible
|
||||
github.com/google/btree v1.0.0 // indirect
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/jcmturner/gofork v1.0.0 // indirect
|
||||
github.com/joomcode/errorx v1.0.0 // indirect
|
||||
github.com/joomcode/redispipe v0.9.0
|
||||
github.com/json-iterator/go v1.1.7
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/mediocregopher/radix.v2 v0.0.0-20181115013041-b67df6e626f9 // indirect
|
||||
github.com/pierrec/lz4 v2.3.0+incompatible // indirect
|
||||
github.com/pkg/errors v0.8.1 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563 // indirect
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271
|
||||
github.com/stretchr/testify v1.4.0
|
||||
go.opencensus.io v0.22.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190926180335-cea2066c6411
|
||||
golang.org/x/exp v0.0.0-20190925190815-26a69ce95baf // indirect
|
||||
golang.org/x/net v0.0.0-20190926025831-c00fd9afed17 // indirect
|
||||
golang.org/x/sys v0.0.0-20190926180325-855e68c8590b // indirect
|
||||
golang.org/x/tools v0.0.0-20190926165942-a8d5d34286bd // indirect
|
||||
google.golang.org/api v0.10.0
|
||||
google.golang.org/appengine v1.6.3 // indirect
|
||||
google.golang.org/genproto v0.0.0-20190926190326-7ee9db18f195 // indirect
|
||||
google.golang.org/grpc v1.24.0 // indirect
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect
|
||||
gopkg.in/jcmturner/goidentity.v3 v3.0.0 // indirect
|
||||
gopkg.in/jcmturner/gokrb5.v7 v7.3.0 // indirect
|
||||
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719
|
||||
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab
|
||||
pack.ag/amqp v0.12.3 // indirect
|
||||
)
|
|
@ -0,0 +1,414 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3 h1:AVXDdKsrtX33oR9fbCMu/+c1o8Ofjq6Ku/MInaLVg5Y=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go/bigquery v1.0.1 h1:hL+ycaJpVE9M7nLoiXb/Pn10ENE2u+oddxbD8uu0ZVU=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0 h1:Kt+gOPPp2LEPWp8CSfxhsM8ik9CcyE/gYu+0r+RnZvM=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/pubsub v1.0.1 h1:W9tAK3E57P75u0XLLR82LZyw8VpAnhmyTOxW9qzmyj8=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0 h1:VV2nUM3wwLLGh9lSABFgZMjInyUbJeaRSE64WuAIQ+4=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
contrib.go.opencensus.io/exporter/ocagent v0.6.0 h1:Z1n6UAyr0QwM284yUuh5Zd8JlvxUGAhFZcgMJkMPrGM=
|
||||
contrib.go.opencensus.io/exporter/ocagent v0.6.0/go.mod h1:zmKjrJcdo0aYcVS7bmEeSEBLPA9YJp5bjrofdU3pIXs=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/Azure/azure-amqp-common-go v1.1.4 h1:DmPXxmLZwi/71CgRTZIKR6yiKEW3eC42S4gSBhfG7y0=
|
||||
github.com/Azure/azure-amqp-common-go v1.1.4/go.mod h1:FhZtXirFANw40UXI2ntweO+VOkfaw8s6vZxUiRhLYW8=
|
||||
github.com/Azure/azure-event-hubs-go v1.3.1 h1:vKw7tLOFJ8kVMkhNvOXZWz+3purRQ/WTe60+bavZ5qc=
|
||||
github.com/Azure/azure-event-hubs-go v1.3.1/go.mod h1:me2m3+0WC7G7JRBTWI5SQ81s2TYyOqgV3JIpYg86jZA=
|
||||
github.com/Azure/azure-pipeline-go v0.1.8/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg=
|
||||
github.com/Azure/azure-sdk-for-go v21.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/azure-sdk-for-go v29.0.0+incompatible h1:CYPU39ULbGjQBo3gXIqiWouK0C4F+Pt2Zx5CqGvqknE=
|
||||
github.com/Azure/azure-sdk-for-go v29.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/azure-storage-blob-go v0.0.0-20181023070848-cf01652132cc/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y=
|
||||
github.com/Azure/go-autorest v11.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest v11.1.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest v11.1.2+incompatible h1:viZ3tV5l4gE2Sw0xrasFHytCGtzYCrT+um/rrSQ1BfA=
|
||||
github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest v12.0.0+incompatible h1:N+VqClcomLGD/sHb3smbSYYtNMgKpVV3Cd5r5i8z6bQ=
|
||||
github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DataDog/zstd v1.3.6-0.20190409195224-796139022798/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
|
||||
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/Shopify/sarama v1.23.1 h1:XxJBCZEoWJtoWjf/xRbmGUpAmTZGnuuF0ON0EvxxBrs=
|
||||
github.com/Shopify/sarama v1.23.1/go.mod h1:XLH1GYJnLVE0XCr6KdJGVJRTwY30moWNJ4sERjXX6fs=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/Sirupsen/logrus v1.0.6 h1:HCAGQRk48dRVPA5Y+Yh0qdCSTzPOyU1tBJ7Q9YzotII=
|
||||
github.com/Sirupsen/logrus v1.0.6/go.mod h1:rmk17hk6i8ZSAJkSDa7nOxamrG+SP4P0mm+DAvExv4U=
|
||||
github.com/a8m/documentdb v1.2.0 h1:3ooHoXI6ww5d5Itr39V+bBmX4xm0nKrv0XMKbXw8vwE=
|
||||
github.com/a8m/documentdb v1.2.0/go.mod h1:4Z0mpi7fkyqjxUdGiNMO3vagyiUoiwLncaIX6AsW5z0=
|
||||
github.com/aws/aws-sdk-go v1.25.0 h1:MyXUdCesJLBvSSKYcaKeeEwxNUwUpG6/uqVYeH/Zzfo=
|
||||
github.com/aws/aws-sdk-go v1.25.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dimchansky/utfbom v1.0.0 h1:fGC2kkf4qOoKqZ4q7iIh+Vef4ubC1c38UDsEyZynZPc=
|
||||
github.com/dimchansky/utfbom v1.0.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-resiliency v1.2.0 h1:v7g92e/KSN71Rq7vSThKaWIq68fL4YHvWyiUKorFR1Q=
|
||||
github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t2y2qayIX0=
|
||||
github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts=
|
||||
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/fortytw2/leaktest v1.2.0 h1:cj6GCiwJDH7l3tMHLjZDo0QqPtrXJiWSI9JgpeQKw+Q=
|
||||
github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/frankban/quicktest v1.5.0 h1:Tb4jWdSpdjKzTUicPnY61PZxKbDoGa7ABbrReT3gQVY=
|
||||
github.com/frankban/quicktest v1.5.0/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
|
||||
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-redis/redis v6.15.5+incompatible h1:pLky8I0rgiblWfa8C1EV7fPEUv0aH6vKRaYHc/YRHVk=
|
||||
github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 h1:WSBJMqJbLxsn+bTCPyPYZfqHdJmc8MK4wrBjMft6BAM=
|
||||
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k=
|
||||
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
|
||||
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.4 h1:5xLhQjsk4zqPf9EHCrja2qFZMx+yBqkO3XgJ14bNnU0=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.4/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
|
||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||
github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8=
|
||||
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/joomcode/errorx v1.0.0 h1:RJAKLTy1Sv2Tszhu14m5RZP4VGRlhXutG/XlL1En5VM=
|
||||
github.com/joomcode/errorx v1.0.0/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ=
|
||||
github.com/joomcode/redispipe v0.9.0 h1:NukwwIvxhg6r2lVxa1RJhEZXYPZZF/OX9WZJk+2cK1Q=
|
||||
github.com/joomcode/redispipe v0.9.0/go.mod h1:4S/gpBCZ62pB/3+XLNWDH7jQnB0vxmpddAMBva2adpM=
|
||||
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 h1:K//n/AqR5HjG3qxbrBCL4vJPW0MVFSs9CPK1OOJdRME=
|
||||
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
|
||||
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mediocregopher/radix.v2 v0.0.0-20181115013041-b67df6e626f9 h1:ViNuGS149jgnttqhc6XQNPwdupEMBXqCx9wtlW7P3sA=
|
||||
github.com/mediocregopher/radix.v2 v0.0.0-20181115013041-b67df6e626f9/go.mod h1:fLRUbhbSd5Px2yKUaGYYPltlyxi1guJz1vCmo1RQL50=
|
||||
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3 h1:EooPXg51Tn+xmWPXJUGCnJhJSpeuMlBmfJVcqIRmmv8=
|
||||
github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.3.0+incompatible h1:CZzRn4Ut9GbUkHlQ7jqBXeZQV41ZSKWFc302ZU6lUTk=
|
||||
github.com/pierrec/lz4 v2.3.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563 h1:dY6ETXrvDG7Sa4vE8ZQG4yqWg6UnOcbqTAahkV813vQ=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sirupsen/logrus v1.1.1 h1:VzGj7lhU7KEB9e9gMpAV/v5XT2NVSvLJhJLCWbnkgXg=
|
||||
github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
|
||||
github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271 h1:WhxRHzgeVGETMlmVfqhRn8RIeeNoPr2Czh33I4Zdccw=
|
||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
|
||||
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
|
||||
go.opencensus.io v0.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.1 h1:8dP3SGL7MPB94crU3bEPplMPe83FI4EouesJUeFHv50=
|
||||
go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190926180335-cea2066c6411 h1:kuW9k4QvBJpRjC3rxEytsfIYPs8oGY3Jw7iR36h0FIY=
|
||||
golang.org/x/crypto v0.0.0-20190926180335-cea2066c6411/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20190925190815-26a69ce95baf h1:V3Pwu2Rbgu9M+TwZjWt4811gVksfbQRU7/rX1dUq8rI=
|
||||
golang.org/x/exp v0.0.0-20190925190815-26a69ce95baf/go.mod h1:/XYaSSBNneWTe8VNa9AjSawUDfGmbpmIqqzQT33BSc0=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac h1:8R1esu+8QioDxo4E4mX6bFztO+dMTM49DNAaWfO5OeY=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190926025831-c00fd9afed17 h1:qPnAdmjNA41t3QBTx2mFGf/SD1IoslhYu7AmdsVzCcs=
|
||||
golang.org/x/net v0.0.0-20190926025831-c00fd9afed17/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190926180325-855e68c8590b h1:/8GN4qrAmRZQXgjWZHj9z/UJI5vNqQhPtgcw02z2f+8=
|
||||
golang.org/x/sys v0.0.0-20190926180325-855e68c8590b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190925164712-ae58c0ff6b32/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190926165942-a8d5d34286bd h1:L7bTtbmMojUZYEAt0OrTU0Z/KmN+uX195615ty32gkA=
|
||||
golang.org/x/tools v0.0.0-20190926165942-a8d5d34286bd/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.10.0 h1:7tmAxx3oKE98VMZ+SBZzvYYWRQ9HODBxmC8mXUsraSQ=
|
||||
google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.3 h1:hvZejVcIxAKHR8Pq2gXaDggf6CWT1QEqO+JEBeOKCG8=
|
||||
google.golang.org/appengine v1.6.3/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20190926190326-7ee9db18f195 h1:dWzgMaXfaHsnkRKZ1l3iJLDmTEB40JMl/dqRbJX4D/o=
|
||||
google.golang.org/genproto v0.0.0-20190926190326-7ee9db18f195/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s=
|
||||
google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o=
|
||||
gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/jcmturner/aescts.v1 v1.0.1 h1:cVVZBK2b1zY26haWB4vbBiZrfFQnfbTVrE3xZq6hrEw=
|
||||
gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
|
||||
gopkg.in/jcmturner/dnsutils.v1 v1.0.1 h1:cIuC1OLRGZrld+16ZJvvZxVJeKPsvd5eUIvxfoN5hSM=
|
||||
gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=
|
||||
gopkg.in/jcmturner/goidentity.v3 v3.0.0 h1:1duIyWiTaYvVx3YX2CYtpJbUFd7/UuPYCfgXtQ3VTbI=
|
||||
gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=
|
||||
gopkg.in/jcmturner/gokrb5.v7 v7.2.3/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
|
||||
gopkg.in/jcmturner/gokrb5.v7 v7.3.0 h1:0709Jtq/6QXEuWRfAm260XqlpcwL1vxtO1tUE2qK8Z4=
|
||||
gopkg.in/jcmturner/gokrb5.v7 v7.3.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
|
||||
gopkg.in/jcmturner/rpc.v1 v1.1.0 h1:QHIUxTX1ISuAv9dD2wJ9HWQVuWDX/Zc0PfeC2tjc4rU=
|
||||
gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
k8s.io/api v0.0.0-20190620084959-7cf5895f2711 h1:BblVYz/wE5WtBsD/Gvu54KyBUTJMflolzc5I2DTvh50=
|
||||
k8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A=
|
||||
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719 h1:uV4S5IB5g4Nvi+TBVNf3e9L4wrirlwYJ6w88jUQxTUw=
|
||||
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA=
|
||||
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab h1:E8Fecph0qbNsAbijJJQryKu4Oi9QTp5cVpjTE+nqg6g=
|
||||
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab/go.mod h1:E95RaSlHr79aHaX0aGSwcPNfygDiPKOVXdmivCIZT0k=
|
||||
k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68=
|
||||
k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
|
||||
k8s.io/utils v0.0.0-20190221042446-c2654d5206da h1:ElyM7RPonbKnQqOcw7dG2IK5uvQQn3b/WPHqD5mBvP4=
|
||||
k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
|
||||
pack.ag/amqp v0.8.0/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4=
|
||||
pack.ag/amqp v0.11.0/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4=
|
||||
pack.ag/amqp v0.12.3 h1:OicIIXWRupB9cbzaPGu1ilS+ZeOzIS7TrGB67ECDDXc=
|
||||
pack.ag/amqp v0.12.3/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
|
@ -0,0 +1,19 @@
|
|||
# Pub Sub
|
||||
|
||||
Pub Subs provide a common way to interact with different message bus implementations to achieve reliable, high-scale scenarios based on event-driven async communications, while allowing users to opt-in to advanced capabilities using defined metadata.
|
||||
|
||||
Currently supported pub-subs are:
|
||||
|
||||
* Redis Streams
|
||||
|
||||
## Implementing a new Pub Sub
|
||||
|
||||
A compliant pub sub needs to implement the following interface:
|
||||
|
||||
```
|
||||
type PubSub interface {
|
||||
Init(metadata Metadata) error
|
||||
Publish(req *PublishRequest) error
|
||||
Subscribe(req SubscribeRequest, handler func(msg *NewMessage) error) error
|
||||
}
|
||||
```
|
|
@ -0,0 +1,49 @@
|
|||
package pubsub
|
||||
|
||||
import (
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultCloudEventType is the default event type for an Actions published event
|
||||
DefaultCloudEventType = "com.actions.event.sent"
|
||||
// CloudEventsSpecVersion is the specversion used by Actions for the cloud events implementation
|
||||
CloudEventsSpecVersion = "0.3"
|
||||
)
|
||||
|
||||
// CloudEventsEnvelope describes the Actions implementation of the Cloud Events spec
|
||||
// Spec details: https://github.com/cloudevents/spec/blob/master/spec.md
|
||||
type CloudEventsEnvelope struct {
|
||||
ID string `json:"id"`
|
||||
Source string `json:"source"`
|
||||
Type string `json:"type"`
|
||||
SpecVersion string `json:"specversion"`
|
||||
DataContentType string `json:"datacontenttype"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// NewCloudEventsEnvelope returns a new CloudEventsEnvelope
|
||||
func NewCloudEventsEnvelope(id, source, eventType string, data []byte) *CloudEventsEnvelope {
|
||||
if eventType == "" {
|
||||
eventType = DefaultCloudEventType
|
||||
}
|
||||
contentType := ""
|
||||
|
||||
var i interface{}
|
||||
err := jsoniter.Unmarshal(data, &i)
|
||||
if err != nil {
|
||||
i = string(data)
|
||||
contentType = "text/plain"
|
||||
} else {
|
||||
contentType = "application/json"
|
||||
}
|
||||
|
||||
return &CloudEventsEnvelope{
|
||||
ID: id,
|
||||
Source: source,
|
||||
Type: eventType,
|
||||
Data: i,
|
||||
SpecVersion: CloudEventsSpecVersion,
|
||||
DataContentType: contentType,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package pubsub
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateCloudEventsEnvelope(t *testing.T) {
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "eventType", nil)
|
||||
assert.NotNil(t, envelope)
|
||||
}
|
||||
|
||||
func TestCreateCloudEventsEnvelopeDefaults(t *testing.T) {
|
||||
t.Run("default event type", func(t *testing.T) {
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "", nil)
|
||||
assert.Equal(t, DefaultCloudEventType, envelope.Type)
|
||||
})
|
||||
|
||||
t.Run("non-default event type", func(t *testing.T) {
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "e1", nil)
|
||||
assert.Equal(t, "e1", envelope.Type)
|
||||
})
|
||||
|
||||
t.Run("spec version", func(t *testing.T) {
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "", nil)
|
||||
assert.Equal(t, CloudEventsSpecVersion, envelope.SpecVersion)
|
||||
})
|
||||
|
||||
t.Run("has data", func(t *testing.T) {
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "", []byte("data"))
|
||||
assert.Equal(t, "data", envelope.Data.(string))
|
||||
})
|
||||
|
||||
t.Run("string data content type", func(t *testing.T) {
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "", []byte("data"))
|
||||
assert.Equal(t, "text/plain", envelope.DataContentType)
|
||||
})
|
||||
|
||||
t.Run("json data content type", func(t *testing.T) {
|
||||
str := `{ "data": "1" }`
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "", []byte(str))
|
||||
assert.Equal(t, "application/json", envelope.DataContentType)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package pubsub
|
||||
|
||||
// Metadata represents a set of message-bus specific properties
|
||||
type Metadata struct {
|
||||
Properties map[string]string `json:"properties"`
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package pubsub
|
||||
|
||||
// PubSub is the interface for message buses
|
||||
type PubSub interface {
|
||||
Init(metadata Metadata) error
|
||||
Publish(req *PublishRequest) error
|
||||
Subscribe(req SubscribeRequest, handler func(msg *NewMessage) error) error
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package pubsub
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MockPubSub struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockPubSub) Init(metadata Metadata) error {
|
||||
args := m.Called(metadata)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockPubSub) Publish(req *PublishRequest) error {
|
||||
args := m.Called(req)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockPubSub) Subscribe(req SubscribeRequest, handler func(msg *NewMessage) error) error {
|
||||
args := m.Called(req, handler)
|
||||
return args.Error(0)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package redis
|
||||
|
||||
type metadata struct {
|
||||
host string
|
||||
password string
|
||||
consumerID string
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
package redis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
|
||||
"github.com/actionscore/components-contrib/pubsub"
|
||||
"github.com/go-redis/redis"
|
||||
)
|
||||
|
||||
const (
|
||||
host = "redisHost"
|
||||
password = "redisPassword"
|
||||
consumerID = "consumerID"
|
||||
)
|
||||
|
||||
type redisStreams struct {
|
||||
metadata metadata
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
// NewRedisStreams returns a new redis streams pub-sub implementation
|
||||
func NewRedisStreams() pubsub.PubSub {
|
||||
return &redisStreams{}
|
||||
}
|
||||
|
||||
func parseRedisMetadata(meta pubsub.Metadata) (metadata, error) {
|
||||
m := metadata{}
|
||||
if val, ok := meta.Properties[host]; ok && val != "" {
|
||||
m.host = val
|
||||
} else {
|
||||
return m, errors.New("redis streams error: missing host address")
|
||||
}
|
||||
|
||||
if val, ok := meta.Properties[password]; ok && val != "" {
|
||||
m.password = val
|
||||
}
|
||||
|
||||
if val, ok := meta.Properties[consumerID]; ok && val != "" {
|
||||
m.consumerID = val
|
||||
} else {
|
||||
return m, errors.New("redis streams error: missing consumerID")
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (r *redisStreams) Init(metadata pubsub.Metadata) error {
|
||||
m, err := parseRedisMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.metadata = m
|
||||
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: m.host,
|
||||
Password: m.password,
|
||||
DB: 0,
|
||||
MaxRetries: 3,
|
||||
MaxRetryBackoff: time.Second * 2,
|
||||
})
|
||||
|
||||
_, err = client.Ping().Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("redis streams: error connecting to redis at %s: %s", m.host, err)
|
||||
}
|
||||
|
||||
r.client = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redisStreams) Publish(req *pubsub.PublishRequest) error {
|
||||
_, err := r.client.XAdd(&redis.XAddArgs{
|
||||
Stream: req.Topic,
|
||||
Values: map[string]interface{}{"data": req.Data},
|
||||
}).Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("redis streams: error from publish: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redisStreams) Subscribe(req pubsub.SubscribeRequest, handler func(msg *pubsub.NewMessage) error) error {
|
||||
err := r.client.XGroupCreateMkStream(req.Topic, r.metadata.consumerID, "0").Err()
|
||||
if err != nil {
|
||||
log.Warnf("redis streams: %s", err)
|
||||
}
|
||||
go r.beginReadingFromStream(req.Topic, r.metadata.consumerID, handler)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redisStreams) readFromStream(stream, consumerID, start string) ([]redis.XStream, error) {
|
||||
res, err := r.client.XReadGroup(&redis.XReadGroupArgs{
|
||||
Group: consumerID,
|
||||
Consumer: consumerID,
|
||||
Streams: []string{stream, start},
|
||||
}).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *redisStreams) processStreams(consumerID string, streams []redis.XStream, handler func(msg *pubsub.NewMessage) error) {
|
||||
for _, s := range streams {
|
||||
for _, m := range s.Messages {
|
||||
msg := pubsub.NewMessage{
|
||||
Topic: s.Stream,
|
||||
}
|
||||
data, exists := m.Values["data"]
|
||||
if exists && data != nil {
|
||||
msg.Data = []byte(data.(string))
|
||||
}
|
||||
|
||||
err := handler(&msg)
|
||||
if err == nil {
|
||||
r.client.XAck(s.Stream, consumerID, m.ID).Result()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *redisStreams) beginReadingFromStream(stream, consumerID string, handler func(msg *pubsub.NewMessage) error) {
|
||||
// first read pending items in case of recovering from crash
|
||||
start := "0"
|
||||
|
||||
for {
|
||||
streams, err := r.readFromStream(stream, consumerID, start)
|
||||
if err != nil {
|
||||
log.Errorf("redis streams: error reading from stream %s: %s", stream, err)
|
||||
return
|
||||
}
|
||||
r.processStreams(consumerID, streams, handler)
|
||||
|
||||
//continue with new non received items
|
||||
start = ">"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package redis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/actionscore/components-contrib/pubsub"
|
||||
)
|
||||
|
||||
func getFakeProperties() map[string]string {
|
||||
return map[string]string{
|
||||
consumerID: "fakeConsumer",
|
||||
host: "fake.redis.com",
|
||||
password: "fakePassword",
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRedisMetadata(t *testing.T) {
|
||||
t.Run("metadata is correct", func(t *testing.T) {
|
||||
fakeProperties := getFakeProperties()
|
||||
|
||||
fakeMetaData := pubsub.Metadata{
|
||||
Properties: fakeProperties,
|
||||
}
|
||||
|
||||
// act
|
||||
m, err := parseRedisMetadata(fakeMetaData)
|
||||
|
||||
// assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, fakeProperties[host], m.host)
|
||||
assert.Equal(t, fakeProperties[password], m.password)
|
||||
assert.Equal(t, fakeProperties[consumerID], m.consumerID)
|
||||
})
|
||||
|
||||
t.Run("host is not given", func(t *testing.T) {
|
||||
fakeProperties := getFakeProperties()
|
||||
|
||||
fakeMetaData := pubsub.Metadata{
|
||||
Properties: fakeProperties,
|
||||
}
|
||||
fakeMetaData.Properties[host] = ""
|
||||
|
||||
// act
|
||||
m, err := parseRedisMetadata(fakeMetaData)
|
||||
|
||||
// assert
|
||||
assert.Error(t, errors.New("redis streams error: missing host address"), err)
|
||||
assert.Empty(t, m.host)
|
||||
assert.Empty(t, m.password)
|
||||
assert.Empty(t, m.consumerID)
|
||||
})
|
||||
|
||||
t.Run("consumerID is not given", func(t *testing.T) {
|
||||
fakeProperties := getFakeProperties()
|
||||
|
||||
fakeMetaData := pubsub.Metadata{
|
||||
Properties: fakeProperties,
|
||||
}
|
||||
fakeMetaData.Properties[consumerID] = ""
|
||||
|
||||
// act
|
||||
m, err := parseRedisMetadata(fakeMetaData)
|
||||
// assert
|
||||
assert.Error(t, errors.New("redis streams error: missing consumerID"), err)
|
||||
assert.Equal(t, fakeProperties[host], m.host)
|
||||
assert.Equal(t, fakeProperties[password], m.password)
|
||||
assert.Empty(t, m.consumerID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcessStreams(t *testing.T) {
|
||||
fakeConsumerID := "fakeConsumer"
|
||||
topicCount := 0
|
||||
messageCount := 0
|
||||
|
||||
fakeHandler := func(msg *pubsub.NewMessage) error {
|
||||
expectedTopic := fmt.Sprintf("Topic%d", topicCount)
|
||||
expectedData := fmt.Sprintf("testData%d", messageCount)
|
||||
|
||||
messageCount++
|
||||
if topicCount == 0 && messageCount >= 3 {
|
||||
topicCount = 1
|
||||
messageCount = 0
|
||||
}
|
||||
|
||||
// assert
|
||||
assert.Equal(t, expectedTopic, msg.Topic)
|
||||
assert.Equal(t, expectedData, string(msg.Data))
|
||||
|
||||
// return fake error to skip executing redis client command
|
||||
return errors.New("fake error")
|
||||
}
|
||||
|
||||
// act
|
||||
testRedisStream := &redisStreams{}
|
||||
testRedisStream.processStreams(fakeConsumerID, generateRedisStreamTestData(2, 3), fakeHandler)
|
||||
|
||||
// assert
|
||||
assert.Equal(t, 1, topicCount)
|
||||
assert.Equal(t, 3, messageCount)
|
||||
}
|
||||
|
||||
func generateRedisStreamTestData(topicCount, messageCount int) []redis.XStream {
|
||||
generateXMessage := func(id int) redis.XMessage {
|
||||
return redis.XMessage{
|
||||
ID: fmt.Sprintf("%d", id),
|
||||
Values: map[string]interface{}{
|
||||
"data": fmt.Sprintf("testData%d", id),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
xmessageArray := make([]redis.XMessage, messageCount)
|
||||
for i := range xmessageArray {
|
||||
xmessageArray[i] = generateXMessage(i)
|
||||
}
|
||||
|
||||
redisStreams := make([]redis.XStream, topicCount)
|
||||
for i := range redisStreams {
|
||||
redisStreams[i] = redis.XStream{
|
||||
Stream: fmt.Sprintf("Topic%d", i),
|
||||
Messages: xmessageArray,
|
||||
}
|
||||
}
|
||||
return redisStreams
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package pubsub
|
||||
|
||||
// PublishRequest is the request to publish a message
|
||||
type PublishRequest struct {
|
||||
Data []byte `json:"data"`
|
||||
Topic string `json:"topic"`
|
||||
}
|
||||
|
||||
// SubscribeRequest is the request to subscribe to a topic
|
||||
type SubscribeRequest struct {
|
||||
Topic string `json:"topic"`
|
||||
}
|
||||
|
||||
// NewMessage is an event arriving from a message bus instance
|
||||
type NewMessage struct {
|
||||
Data []byte `json:"data"`
|
||||
Topic string `json:"topic"`
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
# Secret Stores
|
||||
|
||||
Secret Stores provide a common way to interact with different secret stores, cloud/edge/commercial or open-source.
|
||||
|
||||
Currently supported secret stores are:
|
||||
|
||||
* Kubernetes
|
||||
* Azure KeyVault
|
||||
|
||||
## Implementing a new Secret Store
|
||||
|
||||
A compliant secret store needs to implement the following interface:
|
||||
|
||||
```
|
||||
type SecretStore interface {
|
||||
// Init authenticates with the actual secret store and performs other init operation
|
||||
Init(metadata Metadata) error
|
||||
// GetSecret retrieves a secret using a key and returns a map of decrypted string/string values
|
||||
GetSecret(req GetSecretRequest) (GetSecretResponse, error)
|
||||
}
|
||||
```
|
|
@ -0,0 +1,7 @@
|
|||
package secretstores
|
||||
|
||||
// GetSecretRequest describes a get secret request from a secret store
|
||||
type GetSecretRequest struct {
|
||||
Name string `json:"name"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package secretstores
|
||||
|
||||
// GetSecretResponse describes the response object for a secret returned from a secret store
|
||||
type GetSecretResponse struct {
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package keyvault
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/Azure/go-autorest/autorest/azure/auth"
|
||||
"golang.org/x/crypto/pkcs12"
|
||||
)
|
||||
|
||||
// ClientAuthorizer provides the options to get a bearer authorizer from a client certificate.
|
||||
type ClientAuthorizer struct {
|
||||
*auth.ClientCertificateConfig
|
||||
CertificateData []byte
|
||||
}
|
||||
|
||||
// NewClientAuthorizer creates an ClientAuthorizer object configured to obtain an Authorizer through Client Credentials.
|
||||
func NewClientAuthorizer(certificatePath string, certificateBytes []byte, certificatePassword string, clientID string, tenantID string) ClientAuthorizer {
|
||||
return ClientAuthorizer{
|
||||
&auth.ClientCertificateConfig{
|
||||
CertificatePath: certificatePath,
|
||||
CertificatePassword: certificatePassword,
|
||||
ClientID: clientID,
|
||||
TenantID: tenantID,
|
||||
Resource: azure.PublicCloud.ResourceIdentifiers.KeyVault,
|
||||
AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint,
|
||||
},
|
||||
certificateBytes,
|
||||
}
|
||||
}
|
||||
|
||||
// Authorizer gets an authorizer object from client certificate.
|
||||
func (c ClientAuthorizer) Authorizer() (autorest.Authorizer, error) {
|
||||
if c.ClientCertificateConfig.CertificatePath != "" {
|
||||
// in standalone mode, component yaml will pass cert path
|
||||
return c.ClientCertificateConfig.Authorizer()
|
||||
} else if len(c.CertificateData) > 0 {
|
||||
// in kubernetes mode, runtime will get the secret from K8S secret store and pass byte array
|
||||
spToken, err := c.ServicePrincipalTokenByCertBytes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get oauth token from certificate auth: %v", err)
|
||||
}
|
||||
return autorest.NewBearerAuthorizer(spToken), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("certificate is not given")
|
||||
}
|
||||
|
||||
// ServicePrincipalTokenByCertBytes gets the service principal token by CertificateBytes.
|
||||
func (c ClientAuthorizer) ServicePrincipalTokenByCertBytes() (*adal.ServicePrincipalToken, error) {
|
||||
oauthConfig, err := adal.NewOAuthConfig(c.AADEndpoint, c.TenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certificate, rsaPrivateKey, err := c.decodePkcs12(c.CertificateData, c.CertificatePassword)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode pkcs12 certificate while creating spt: %v", err)
|
||||
}
|
||||
return adal.NewServicePrincipalTokenFromCertificate(*oauthConfig, c.ClientID, certificate, rsaPrivateKey, c.Resource)
|
||||
}
|
||||
|
||||
func (c ClientAuthorizer) decodePkcs12(pkcs []byte, password string) (*x509.Certificate, *rsa.PrivateKey, error) {
|
||||
privateKey, certificate, err := pkcs12.Decode(pkcs, password)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rsaPrivateKey, isRsaKey := privateKey.(*rsa.PrivateKey)
|
||||
if !isRsaKey {
|
||||
return nil, nil, fmt.Errorf("PKCS#12 certificate must contain an RSA private key")
|
||||
}
|
||||
|
||||
return certificate, rsaPrivateKey, nil
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package keyvault
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
fakeTenantID = "14bec2db-7f9a-4f3d-97ca-2d384ac83389"
|
||||
fakeClientID = "04bec2db-7f9a-4f3d-97ca-3d384ac83389"
|
||||
|
||||
// Base64 encoded test pfx cert - Expire date: 09/19/2119
|
||||
testCert = "MIIKTAIBAzCCCgwGCSqGSIb3DQEHAaCCCf0Eggn5MIIJ9TCCBhYGCSqGSIb3DQEHAaCCBgcEggYDMIIF/zCCBfsGCyqGSIb3DQEMCgECoIIE/jCCBPowHAYKKoZIhvcNAQwBAzAOBAifAbe5KAL7IwICB9AEggTYZ3dAdDNqi5GoGJ/VfZhh8dxIIERUaC/SO5vKFhDfNu9VCQKF7Azr3eJ4cjzQmicfLd6FxJpB6d+8fbQuCcYPpTAdqf5zmLtZWMDWW8YZE0pV7b6sDZSw/NbT2zFhsx2uife6NnLK//Pj+GeALUDPfhVfqfLCfWZlCHxlbOipVZv9U4+TCVO2vyrGUq2XesT78cT+LhbHYkcrxTCsXNLWAvSJ9zXOIVA5HNS3Qv8pQJSSbqYVBbLk6FEbt5B3pk0xoA1hhM7dlCoGvPJ/ajvN3wAcEB5kmjJ4q59s2HeXloa7aAhXTFEkL2rZH+acgr1AO/DwcGXUqzJ2ooGYBfoqmgaXjydzyVLzYNccBGbzBR4Q0crMW6zDBXDlwvnLxmqZ7p05Ix9ZqISQyTm/DboNwQk1erOJd0fe6Brg1Dw4td6Uh/AXfM8m+XCGJFn79ZMCtd4rP8w9l008m8xe7rczSkMW0aRJVr0j3fFheene83jOHEB0q3KMKsVTkPWehnTGPj4TrsL+WwrmJpqrSloXMyaqvS9hvqAfPal0JI9taz6R5HFONaO6oi/ajpX3tYSX0rafQPKHmJpFLtJHYPopFYgP4akq8wKOCjq1IDg3ZW59G9nh8Vcw3IrAnr+C9iMgzPUvCHCinQK24cmbn5px6S0U0ARhY90KrSMFRyjvxNpZzc+A/AAaQ/wwuLVy1GyuZ2sRFyVSCTRMC6ZfXAUs+OijDO/B++BCdmqm5p5/aZpQYf1cb681AaDc/5XTHtCC3setYfpviMe1grvp4jaPVrjnG85pVenZJ0d+Xo7BnD38Ec5RsKpvtXIieiRIbnGqzTzxj/OU/cdglrKy8MLo6IJigXA6N3x14o4e3akq7cvLPRQZqlWyLqjlGnJdZKJlemFlOnDSluzwGBwwKF+PpXuRVSDhi/ARN3g8L+wVAQQMEylWJfK7sNDun41rimE8wGFjqlfZNVg/pCBKvw3p90pCkxVUEZBRrP1vaGzrIvOsMU/rrJqQU7Imv9y6nUrvHdcoRFUdbgWVWZus6VwTrgwRkfnPiLZo0r5Vh4kComH0+Tc4kgwbnnuQQWzn8J9Ur4Nu0MkknC/1jDwulq2XOIBPclmEPg9CSSwfKonyaRxz+3GoPy0kGdHwsOcXIq5qBIyiYAtM1g1cQLtOT16OCjapus+GIOLnItP2OAhO70dsTMUlsQSNEH+KxUxFb1pFuQGXnStmgZtHYI4LvC/d820tY0m0I6SgfabnoQpIXa6iInIt970awwyUP1P/6m9ie5bCRDWCj4R0bNiNQBjq9tHfO4xeGK+fUTyeU4OEBgiyisNVhijf6GlfPHKWwkInAN0WbS3UHHACjkP0jmRb70b/3VbWon/+K5S6bk2ohIDsbPPVolTvfMehRwKatqQTbTXlnDIHJQzk9SfHHWJzkrQXEIbXgGxHSHm5CmNetR/MYGlivjtGRVxOLr7Y1tK0GGEDMs9nhiSvlwWjAEuwIN+72T6Kx7hPRld1BvaTYLRYXfjnedo7D2AoR+8tGLWjU31rHJVua/JILjGC84ARCjk5LOFHOXUjOP1jJomh8ebjlVijNWP0gLUC14AE8UJsJ1Xi6xiNOTeMpeOIJl2kX81uvnNbQ0j4WajfXlox5eV+0iJ1yNfw5jGB6TATBgkqhkiG9w0BCRUxBgQEAQAAADBXBgkqhkiG9w0BCRQxSh5IADgAZABlADYANgA5AGEAYQAtADUAZgAyAGMALQA0ADIANgBmAC0AYQA3ADAANwAtADIANgBmADkAOAAwADAANAAwAGEAYQAwMHkGCSsGAQQBgjcRATFsHmoATQBpAGMAcgBvAHMAbwBmAHQAIABFAG4AaABhAG4AYwBlAGQAIABSAFMAQQAgAGEAbgBkACAAQQBFAFMAIABDAHIAeQBwAHQAbwBnAHIAYQBwAGgAaQBjACAAUAByAG8AdgBpAGQAZQByMIID1wYJKoZIhvcNAQcGoIIDyDCCA8QCAQAwggO9BgkqhkiG9w0BBwEwHAYKKoZIhvcNAQwBBjAOBAiT1ngppOJy/gICB9CAggOQt9iTz9CmP/3+EBQv3WM80jLHHyrkJM5nIckr+4fmcl3frhbZZajSf1eigjOaqWpz1cAu9KtSAb0Fa35AKr7r9du5SXwBxyYS6XzXsWekSrdvh3Dui0abXo/yh+lIfI/61sJLv5Gc7/DbJrwlHHOD1DR/ohmncAiSjGUYaO9/Y9xUV3cbzjZypqKkkbahaWVMC8+D9zUSkH64RUuLvSi5X5QKFsICNouBL1j/C2s3VZoyR9F0ajRCEMFnQsMfJ/1fP2iW/wwFIARBjphj1SaEaP3XkxQadslR0cwhf6Ujj/tXyd1zV5oI8rJ54r8eN5Vu8NxEX3kl+A7gCc9ACEC0klZ18mQUjb6eDpUSFM63/wx7ISDKaD7gyWCul1JwlUmYzvrRw8sAwjVEyXzc+n0oIOlk0lE6vk3mybkfcOxafRkdr0zVnd5L+XtV/V38sd3ExNojQgUDNy905PNTHdeVnvHt6E8XGNgGX7a/tB1r7Un3soL5Vjcuf/HMdyR57CF2lxFSrdZ1bNnw7Z1GJbQZHago2AovNw+BbBJfey0iuIRP+dgkIfle0nzl3E7T9jU0r2+GEQfN7YYjRL19XFX4n8kNpiTDDRxdNj/yKQDfC7f8prZY/yP8bJLaFBd+uoH+D4QKmWk7plwXTOLiNno9cOTrLYT48HCEghtBbnTgZglOg8eDZd35MR5KcCNWxVy/enEj3/BEtkH7qnJsxlFMu1WwAQzaVYK1u1sGCD8NGH2wtiJi0O5q+YsQItv7ia2x9lSL1JPagtRhxnIZbC5HaIx87bSrVY9XTrWlj9X0H+YSdbUrszRse+LLJkw6h8wXqBvrBKsxnPrfJyQWs3zqehk0FPF1pi+spoJzp7//nmZ5a7knRXYkxV++TiuX+RQSNR/cFxezEwR+2WUAJaJfPpSf06dp5M/gJNVJQGMNiLHCMc9w6CPLUFQA1FG5YdK8nFrSo0iclX7wAHWpCjkqHj7PgOT+Ia5qiOb2dN2GBWPh5N94PO15BLlS/9UUvGxvmWqmG3lpr3hP5B6OZdQl8lxBGc8KTq4GdoJrQ+Jmfej3LQa33mV5VZwJqdbH9iEHvUH2VYC8ru7r5drXBqP5IlZrkdIL5uzzaoHsnWtu0OKgjwRwXaAF24zM0GVXbueGXLXH3vwBwoO4GnDfJ0wN0qFEJBRexRdPP9JKjPfVmwbi89sx1zJMId3nCmetq5yGMDcwHzAHBgUrDgMCGgQUmQChLB4WJjopytxl4LNQ9NuCbPkEFO+tI0n+7a6hwK9hqzq7tghkXp08"
|
||||
)
|
||||
|
||||
func TestClientAuthorizer(t *testing.T) {
|
||||
testSecretStore := NewClientAuthorizer(
|
||||
"testfile",
|
||||
[]byte("testcert"),
|
||||
"1234",
|
||||
fakeClientID,
|
||||
fakeTenantID)
|
||||
|
||||
assert.Equal(t, "testfile", testSecretStore.CertificatePath)
|
||||
assert.Equal(t, []byte("testcert"), testSecretStore.CertificateData)
|
||||
assert.Equal(t, "1234", testSecretStore.CertificatePassword)
|
||||
assert.Equal(t, fakeClientID, testSecretStore.ClientID)
|
||||
assert.Equal(t, fakeTenantID, testSecretStore.TenantID)
|
||||
assert.Equal(t, "https://vault.azure.net", testSecretStore.Resource)
|
||||
assert.Equal(t, "https://login.microsoftonline.com/", testSecretStore.AADEndpoint)
|
||||
}
|
||||
|
||||
func TestAuthorizorWithCertFile(t *testing.T) {
|
||||
testCertFileName := "./.cert.pfx"
|
||||
certBytes := getTestCert()
|
||||
err := ioutil.WriteFile(testCertFileName, certBytes, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
testSecretStore := NewClientAuthorizer(
|
||||
testCertFileName,
|
||||
nil,
|
||||
"", // No password for cert
|
||||
fakeClientID,
|
||||
fakeTenantID)
|
||||
|
||||
authorizer, err := testSecretStore.Authorizer()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, authorizer)
|
||||
|
||||
err = os.Remove(testCertFileName)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAuthorizorWithCertBytes(t *testing.T) {
|
||||
t.Run("Certificate is valid", func(t *testing.T) {
|
||||
certBytes := getTestCert()
|
||||
testSecretStore := NewClientAuthorizer(
|
||||
"", // ignore
|
||||
certBytes,
|
||||
"", // No password for cert
|
||||
fakeClientID,
|
||||
fakeTenantID)
|
||||
|
||||
authorizer, err := testSecretStore.Authorizer()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, authorizer)
|
||||
})
|
||||
|
||||
t.Run("Certificate is invalid", func(t *testing.T) {
|
||||
certBytes := getTestCert()
|
||||
testSecretStore := NewClientAuthorizer(
|
||||
"", // ignore
|
||||
certBytes[0:20],
|
||||
"", // No password for cert
|
||||
fakeClientID,
|
||||
fakeTenantID)
|
||||
|
||||
_, err := testSecretStore.Authorizer()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func getTestCert() []byte {
|
||||
certBytes, _ := base64.StdEncoding.DecodeString(testCert)
|
||||
return certBytes
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package keyvault
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/actionscore/components-contrib/secretstores"
|
||||
|
||||
kv "github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault"
|
||||
)
|
||||
|
||||
// Keyvault secret store component metadata properties
|
||||
const (
|
||||
componentSPNCertificate = "spnCertificate"
|
||||
componentSPNCertificateFile = "spnCertificateFile"
|
||||
componentSPNCertificatePassword = "spnCertificatePassword"
|
||||
componentSPNClientID = "spnClientId"
|
||||
componentSPNTenantID = "spnTenantId"
|
||||
componentVaultName = "vaultName"
|
||||
)
|
||||
|
||||
type keyvaultSecretStore struct {
|
||||
vaultName string
|
||||
vaultClient kv.BaseClient
|
||||
clientAuthorizer ClientAuthorizer
|
||||
}
|
||||
|
||||
// NewAzureKeyvaultSecretStore returns a new Kubernetes secret store
|
||||
func NewAzureKeyvaultSecretStore() secretstores.SecretStore {
|
||||
return &keyvaultSecretStore{
|
||||
vaultName: "",
|
||||
vaultClient: kv.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// Init creates a Kubernetes client
|
||||
func (k *keyvaultSecretStore) Init(metadata secretstores.Metadata) error {
|
||||
props := metadata.Properties
|
||||
k.vaultName = props[componentVaultName]
|
||||
certFilePath := props[componentSPNCertificateFile]
|
||||
certBytes := []byte(props[componentSPNCertificate])
|
||||
certPassword := props[componentSPNCertificatePassword]
|
||||
|
||||
k.clientAuthorizer = NewClientAuthorizer(
|
||||
certFilePath,
|
||||
certBytes,
|
||||
certPassword,
|
||||
props[componentSPNClientID],
|
||||
props[componentSPNTenantID])
|
||||
|
||||
authorizer, err := k.clientAuthorizer.Authorizer()
|
||||
if err == nil {
|
||||
k.vaultClient.Authorizer = authorizer
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetSecret retrieves a secret using a key and returns a map of decrypted string/string values
|
||||
func (k *keyvaultSecretStore) GetSecret(req secretstores.GetSecretRequest) (secretstores.GetSecretResponse, error) {
|
||||
secretResp, err := k.vaultClient.GetSecret(context.Background(), k.getVaultURI(), req.Name, "")
|
||||
if err != nil {
|
||||
return secretstores.GetSecretResponse{Data: nil}, err
|
||||
}
|
||||
|
||||
secretValue := ""
|
||||
if secretResp.Value != nil {
|
||||
secretValue = *secretResp.Value
|
||||
}
|
||||
|
||||
return secretstores.GetSecretResponse{
|
||||
Data: map[string]string{
|
||||
secretstores.DefaultSecretRefKeyName: secretValue,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getVaultURI returns Azure Key Vault URI
|
||||
func (k *keyvaultSecretStore) getVaultURI() string {
|
||||
return fmt.Sprintf("https://%s.vault.azure.net", k.vaultName)
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"path/filepath"
|
||||
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/client-go/util/homedir"
|
||||
)
|
||||
|
||||
// GetKubeClient returns a kubernetes client
|
||||
func GetKubeClient() (*kubernetes.Clientset, error) {
|
||||
var kubeconfig *string
|
||||
if home := homedir.HomeDir(); home != "" {
|
||||
kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
|
||||
} else {
|
||||
kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
conf, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
conf, err = clientcmd.BuildConfigFromFlags("", *kubeconfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
clientset, err := kubernetes.NewForConfig(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clientset, nil
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/actionscore/components-contrib/secretstores"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
type kubernetesSecretStore struct {
|
||||
kubeClient kubernetes.Interface
|
||||
}
|
||||
|
||||
// NewKubernetesSecretStore returns a new Kubernetes secret store
|
||||
func NewKubernetesSecretStore() secretstores.SecretStore {
|
||||
return &kubernetesSecretStore{}
|
||||
}
|
||||
|
||||
// Init creates a Kubernetes client
|
||||
func (k *kubernetesSecretStore) Init(metadata secretstores.Metadata) error {
|
||||
client, err := GetKubeClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
k.kubeClient = client
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSecret retrieves a secret using a key and returns a map of decrypted string/string values
|
||||
func (k *kubernetesSecretStore) GetSecret(req secretstores.GetSecretRequest) (secretstores.GetSecretResponse, error) {
|
||||
resp := secretstores.GetSecretResponse{
|
||||
Data: map[string]string{},
|
||||
}
|
||||
namespace, err := k.getNamespaceFromMetadata(req.Metadata)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
secret, err := k.kubeClient.CoreV1().Secrets(namespace).Get(req.Name, meta_v1.GetOptions{})
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
for k, v := range secret.Data {
|
||||
resp.Data[k] = string(v)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (k *kubernetesSecretStore) getNamespaceFromMetadata(metadata map[string]string) (string, error) {
|
||||
if val, ok := metadata["namespace"]; ok && val != "" {
|
||||
return val, nil
|
||||
}
|
||||
return "", errors.New("namespace is missing on metadata")
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetNamespace(t *testing.T) {
|
||||
t.Run("has namespace", func(t *testing.T) {
|
||||
store := kubernetesSecretStore{}
|
||||
namespace := "a"
|
||||
|
||||
ns, err := store.getNamespaceFromMetadata(map[string]string{"namespace": namespace})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, namespace, ns)
|
||||
})
|
||||
|
||||
t.Run("no namespace", func(t *testing.T) {
|
||||
store := kubernetesSecretStore{}
|
||||
_, err := store.getNamespaceFromMetadata(map[string]string{})
|
||||
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "namespace is missing on metadata", err.Error())
|
||||
})
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package secretstores
|
||||
|
||||
// DefaultSecretRefKeyName is the default key if secretKeyRef.key is not given
|
||||
const DefaultSecretRefKeyName = "_value"
|
||||
|
||||
// Metadata contains a secretstore specific set of metadata properties
|
||||
type Metadata struct {
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package secretstores
|
||||
|
||||
// SecretStore is the interface for a component that handles secrets management
|
||||
type SecretStore interface {
|
||||
// Init authenticates with the actual secret store and performs other init operation
|
||||
Init(metadata Metadata) error
|
||||
// GetSecret retrieves a secret using a key and returns a map of decrypted string/string values
|
||||
GetSecret(req GetSecretRequest) (GetSecretResponse, error)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
# State Stores
|
||||
|
||||
State Stores provide a common way to interact with different data store implementations, and allow users to opt-in to advanced capabilities using defined metadata.
|
||||
|
||||
Currently supported state stores are:
|
||||
|
||||
* Azure CosmosDB
|
||||
* Redis
|
||||
|
||||
## Implementing a new State Store
|
||||
|
||||
A compliant state store needs to implement one or more interfaces: `StateStore` and `TransactionalStateStore`.
|
||||
|
||||
The interface for StateStore:
|
||||
|
||||
```
|
||||
type StateStore interface {
|
||||
Init(metadata Metadata) error
|
||||
Delete(req *DeleteRequest) error
|
||||
BulkDelete(req []DeleteRequest) error
|
||||
Get(req *GetRequest) (*GetResponse, error)
|
||||
Set(req *SetRequest) error
|
||||
BulkSet(req []SetRequest) error
|
||||
}
|
||||
```
|
||||
|
||||
The interface for TransactionalStateStore:
|
||||
|
||||
```
|
||||
type TransactionalStateStore interface {
|
||||
Init(metadata Metadata) error
|
||||
Multi(reqs []TransactionalRequest) error
|
||||
}
|
||||
```
|
|
@ -0,0 +1,196 @@
|
|||
package cosmosdb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
||||
"github.com/actionscore/components-contrib/state"
|
||||
|
||||
"github.com/a8m/documentdb"
|
||||
)
|
||||
|
||||
// StateStore is a CosmosDB state store
|
||||
type StateStore struct {
|
||||
client *documentdb.DocumentDB
|
||||
collection *documentdb.Collection
|
||||
db *documentdb.Database
|
||||
}
|
||||
|
||||
type credentials struct {
|
||||
URL string `json:"url"`
|
||||
MasterKey string `json:"masterKey"`
|
||||
Database string `json:"database"`
|
||||
Collection string `json:"collection"`
|
||||
}
|
||||
|
||||
// CosmosItem is a wrapper around a CosmosDB document
|
||||
type CosmosItem struct {
|
||||
documentdb.Document
|
||||
ID string `json:"id"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
// NewCosmosDBStateStore returns a new CosmosDB state store
|
||||
func NewCosmosDBStateStore() *StateStore {
|
||||
return &StateStore{}
|
||||
}
|
||||
|
||||
// Init does metadata and connection parsing
|
||||
func (c *StateStore) Init(metadata state.Metadata) error {
|
||||
connInfo := metadata.Properties
|
||||
b, err := json.Marshal(connInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var creds credentials
|
||||
err = json.Unmarshal(b, &creds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := documentdb.New(creds.URL, &documentdb.Config{
|
||||
MasterKey: &documentdb.Key{
|
||||
Key: creds.MasterKey,
|
||||
},
|
||||
})
|
||||
|
||||
dbs, err := client.QueryDatabases(&documentdb.Query{
|
||||
Query: fmt.Sprintf("SELECT * FROM ROOT r WHERE r.id='%s'", creds.Database),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(dbs) == 0 {
|
||||
return fmt.Errorf("Database %s for CosmosDB state store not found", creds.Database)
|
||||
}
|
||||
|
||||
c.db = &dbs[0]
|
||||
colls, err := client.QueryCollections(c.db.Self, &documentdb.Query{
|
||||
Query: fmt.Sprintf("SELECT * FROM ROOT r WHERE r.id='%s'", creds.Collection),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(colls) == 0 {
|
||||
return fmt.Errorf("Collection %s for CosmosDB state store not found", creds.Collection)
|
||||
}
|
||||
|
||||
c.collection = &colls[0]
|
||||
c.client = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retreives a CosmosDB item
|
||||
func (c *StateStore) Get(req *state.GetRequest) (*state.GetResponse, error) {
|
||||
key := req.Key
|
||||
|
||||
items := []CosmosItem{}
|
||||
options := []documentdb.CallOption{documentdb.PartitionKey(req.Key)}
|
||||
if req.Options.Consistency == state.Strong {
|
||||
options = append(options, documentdb.ConsistencyLevel(documentdb.Strong))
|
||||
}
|
||||
if req.Options.Consistency == state.Eventual {
|
||||
options = append(options, documentdb.ConsistencyLevel(documentdb.Eventual))
|
||||
}
|
||||
|
||||
_, err := c.client.QueryDocuments(
|
||||
c.collection.Self,
|
||||
documentdb.NewQuery("SELECT * FROM ROOT r WHERE r.id=@id", documentdb.P{"@id", key}),
|
||||
&items,
|
||||
options...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if len(items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b, err := jsoniter.ConfigFastest.Marshal(&items[0].Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &state.GetResponse{
|
||||
Data: b,
|
||||
ETag: items[0].Etag,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Set saves a CosmosDB item
|
||||
func (c *StateStore) Set(req *state.SetRequest) error {
|
||||
err := state.CheckSetRequestOptions(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options := []documentdb.CallOption{documentdb.PartitionKey(req.Key)}
|
||||
|
||||
if req.ETag != "" {
|
||||
options = append(options, documentdb.IfMatch((req.ETag)))
|
||||
}
|
||||
if req.Options.Consistency == state.Strong {
|
||||
options = append(options, documentdb.ConsistencyLevel(documentdb.Strong))
|
||||
}
|
||||
if req.Options.Consistency == state.Eventual {
|
||||
options = append(options, documentdb.ConsistencyLevel(documentdb.Eventual))
|
||||
}
|
||||
|
||||
_, err = c.client.UpsertDocument(c.collection.Self, CosmosItem{ID: req.Key, Value: req.Value}, options...)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BulkSet performs a bulk set operation
|
||||
func (c *StateStore) BulkSet(req []state.SetRequest) error {
|
||||
for _, s := range req {
|
||||
err := c.Set(&s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete performs a delete operation
|
||||
func (c *StateStore) Delete(req *state.DeleteRequest) error {
|
||||
err := state.CheckDeleteRequestOptions(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selfLink := fmt.Sprintf("dbs/%s/colls/%s/docs/%s", c.db.Id, c.collection.Id, req.Key)
|
||||
|
||||
options := []documentdb.CallOption{documentdb.PartitionKey(req.Key)}
|
||||
|
||||
if req.ETag != "" {
|
||||
options = append(options, documentdb.IfMatch((req.ETag)))
|
||||
}
|
||||
if req.Options.Consistency == state.Strong {
|
||||
options = append(options, documentdb.ConsistencyLevel(documentdb.Strong))
|
||||
}
|
||||
if req.Options.Consistency == state.Eventual {
|
||||
options = append(options, documentdb.ConsistencyLevel(documentdb.Eventual))
|
||||
}
|
||||
|
||||
_, err = c.client.DeleteDocument(selfLink, options...)
|
||||
return err
|
||||
}
|
||||
|
||||
// BulkDelete performs a bulk delete operation
|
||||
func (c *StateStore) BulkDelete(req []state.DeleteRequest) error {
|
||||
for _, r := range req {
|
||||
err := c.Delete(&r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package state
|
||||
|
||||
// Metadata contains a state store specific set of metadata properties
|
||||
type Metadata struct {
|
||||
Properties map[string]string `json:"properties"`
|
||||
}
|
|
@ -0,0 +1,277 @@
|
|||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/actionscore/components-contrib/state"
|
||||
|
||||
"github.com/joomcode/redispipe/redis"
|
||||
"github.com/joomcode/redispipe/redisconn"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
const (
|
||||
setQuery = "local var1 = redis.pcall(\"HGET\", KEYS[1], \"version\"); if type(var1) == \"table\" then redis.call(\"DEL\", KEYS[1]); end; if not var1 or type(var1)==\"table\" or var1 == \"\" or var1 == ARGV[1] or ARGV[1] == \"0\" then redis.call(\"HSET\", KEYS[1], \"data\", ARGV[2]) return redis.call(\"HINCRBY\", KEYS[1], \"version\", 1) else return error(\"failed to set key \" .. KEYS[1]) end"
|
||||
delQuery = "local var1 = redis.pcall(\"HGET\", KEYS[1], \"version\"); if not var1 or type(var1)==\"table\" or var1 == ARGV[1] then return redis.call(\"DEL\", KEYS[1]) else return error(\"failed to delete \" .. KEYS[1]) end"
|
||||
connectedSlavesReplicas = "connected_slaves:"
|
||||
infoReplicationDelimiter = "\r\n"
|
||||
)
|
||||
|
||||
// StateStore is a Redis state store
|
||||
type StateStore struct {
|
||||
client *redis.SyncCtx
|
||||
json jsoniter.API
|
||||
replicas int
|
||||
}
|
||||
|
||||
type credenials struct {
|
||||
Host string `json:"redisHost"`
|
||||
Password string `json:"redisPassword"`
|
||||
}
|
||||
|
||||
// NewRedisStateStore returns a new redis state store
|
||||
func NewRedisStateStore() *StateStore {
|
||||
return &StateStore{
|
||||
json: jsoniter.ConfigFastest,
|
||||
}
|
||||
}
|
||||
|
||||
// Init does metadata and connection parsing
|
||||
func (r *StateStore) Init(metadata state.Metadata) error {
|
||||
rand.Seed(time.Now().Unix())
|
||||
|
||||
connInfo := metadata.Properties
|
||||
b, err := json.Marshal(connInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var redisCreds credenials
|
||||
err = json.Unmarshal(b, &redisCreds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
opts := redisconn.Opts{
|
||||
DB: 0,
|
||||
Password: redisCreds.Password,
|
||||
}
|
||||
conn, err := redisconn.Connect(ctx, redisCreds.Host, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.client = &redis.SyncCtx{
|
||||
S: conn,
|
||||
}
|
||||
|
||||
r.replicas, err = r.getConnectedSlaves()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *StateStore) getConnectedSlaves() (int, error) {
|
||||
res := r.client.Do(context.Background(), "INFO replication")
|
||||
if err := redis.AsError(res); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Response example: https://redis.io/commands/info#return-value
|
||||
// # Replication\r\nrole:master\r\nconnected_slaves:1\r\n
|
||||
s, _ := strconv.Unquote(fmt.Sprintf("%q", res))
|
||||
if len(s) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return r.parseConnectedSlaves(s), nil
|
||||
|
||||
}
|
||||
|
||||
func (r *StateStore) parseConnectedSlaves(res string) int {
|
||||
infos := strings.Split(res, infoReplicationDelimiter)
|
||||
for _, info := range infos {
|
||||
if strings.Index(info, connectedSlavesReplicas) >= 0 {
|
||||
parsedReplicas, _ := strconv.ParseUint(info[len(connectedSlavesReplicas):], 10, 32)
|
||||
return int(parsedReplicas)
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *StateStore) deleteValue(req *state.DeleteRequest) error {
|
||||
res := r.client.Do(context.Background(), "EVAL", delQuery, 1, req.Key, req.ETag)
|
||||
|
||||
if err := redis.AsError(res); err != nil {
|
||||
return fmt.Errorf("failed to delete key '%s' due to ETag mismatch", req.Key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete performs a delete operation
|
||||
func (r *StateStore) Delete(req *state.DeleteRequest) error {
|
||||
err := state.CheckDeleteRequestOptions(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return state.DeleteWithRetries(r.deleteValue, req)
|
||||
}
|
||||
|
||||
// BulkDelete performs a bulk delete operation
|
||||
func (r *StateStore) BulkDelete(req []state.DeleteRequest) error {
|
||||
for _, re := range req {
|
||||
err := r.Delete(&re)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *StateStore) directGet(req *state.GetRequest) (*state.GetResponse, error) {
|
||||
res := r.client.Do(context.Background(), "GET", req.Key)
|
||||
if err := redis.AsError(res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res == nil {
|
||||
return &state.GetResponse{}, nil
|
||||
}
|
||||
|
||||
s, _ := strconv.Unquote(fmt.Sprintf("%q", res))
|
||||
return &state.GetResponse{
|
||||
Data: []byte(s),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get retrieves state from redis with a key
|
||||
func (r *StateStore) Get(req *state.GetRequest) (*state.GetResponse, error) {
|
||||
res := r.client.Do(context.Background(), "HGETALL", req.Key) // Prefer values with ETags
|
||||
if err := redis.AsError(res); err != nil {
|
||||
return r.directGet(req) //Falls back to original get
|
||||
}
|
||||
if res == nil {
|
||||
return &state.GetResponse{}, nil
|
||||
}
|
||||
vals := res.([]interface{})
|
||||
if len(vals) == 0 {
|
||||
return &state.GetResponse{}, nil
|
||||
}
|
||||
|
||||
data, version, err := r.getKeyVersion(vals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &state.GetResponse{
|
||||
Data: []byte(data),
|
||||
ETag: version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *StateStore) setValue(req *state.SetRequest) error {
|
||||
err := state.CheckSetRequestOptions(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ver, err := r.parseETag(req.ETag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.Options.Concurrency == state.LastWrite {
|
||||
ver = 0
|
||||
}
|
||||
|
||||
b, _ := r.json.Marshal(req.Value)
|
||||
|
||||
res := r.client.Do(context.Background(), "EVAL", setQuery, 1, req.Key, ver, b)
|
||||
if err := redis.AsError(res); err != nil {
|
||||
return fmt.Errorf("failed to set key '%s' due to ETag mismatch", req.Key)
|
||||
}
|
||||
|
||||
if req.Options.Consistency == state.Strong && r.replicas > 0 {
|
||||
res = r.client.Do(context.Background(), "WAIT", r.replicas, 1000)
|
||||
if err := redis.AsError(res); err != nil {
|
||||
return fmt.Errorf("timed out while wating for %d replicas to acknowledge", r.replicas)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set saves state into redis
|
||||
func (r *StateStore) Set(req *state.SetRequest) error {
|
||||
return state.SetWithRetries(r.setValue, req)
|
||||
}
|
||||
|
||||
// BulkSet performs a bulks save operation
|
||||
func (r *StateStore) BulkSet(req []state.SetRequest) error {
|
||||
for _, s := range req {
|
||||
err := r.Set(&s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Multi performs a transactional operation. succeeds only if all operations succeed, and fails if one or more operations fail
|
||||
func (r *StateStore) Multi(operations []state.TransactionalRequest) error {
|
||||
redisReqs := []redis.Request{}
|
||||
for _, o := range operations {
|
||||
if o.Operation == state.Upsert {
|
||||
req := o.Request.(state.SetRequest)
|
||||
b, _ := r.json.Marshal(req.Value)
|
||||
redisReqs = append(redisReqs, redis.Req("SET", req.Key, b))
|
||||
} else if o.Operation == state.Delete {
|
||||
req := o.Request.(state.DeleteRequest)
|
||||
redisReqs = append(redisReqs, redis.Req("DEL", req.Key))
|
||||
}
|
||||
}
|
||||
|
||||
_, err := r.client.SendTransaction(context.Background(), redisReqs)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *StateStore) getKeyVersion(vals []interface{}) (data string, version string, err error) {
|
||||
seenData := false
|
||||
seenVersion := false
|
||||
for i := 0; i < len(vals); i += 2 {
|
||||
field, _ := strconv.Unquote(fmt.Sprintf("%q", vals[i]))
|
||||
switch field {
|
||||
case "data":
|
||||
data, _ = strconv.Unquote(fmt.Sprintf("%q", vals[i+1]))
|
||||
seenData = true
|
||||
case "version":
|
||||
version, _ = strconv.Unquote(fmt.Sprintf("%q", vals[i+1]))
|
||||
seenVersion = true
|
||||
}
|
||||
}
|
||||
if !seenData || !seenVersion {
|
||||
return "", "", errors.New("required hash field 'data' or 'version' was not found")
|
||||
}
|
||||
return data, version, nil
|
||||
}
|
||||
|
||||
func (r *StateStore) parseETag(etag string) (int, error) {
|
||||
ver := 0
|
||||
var err error
|
||||
if etag != "" {
|
||||
ver, err = strconv.Atoi(etag)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
}
|
||||
return ver, nil
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package redis
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetKeyVersion(t *testing.T) {
|
||||
store := NewRedisStateStore()
|
||||
t.Run("With all required fields", func(t *testing.T) {
|
||||
key, ver, err := store.getKeyVersion([]interface{}{"data", "TEST_KEY", "version", "TEST_VER"})
|
||||
assert.Equal(t, nil, err, "failed to read all fields")
|
||||
assert.Equal(t, "TEST_KEY", key, "failed to read key")
|
||||
assert.Equal(t, "TEST_VER", ver, "failed to read version")
|
||||
})
|
||||
t.Run("With missing data", func(t *testing.T) {
|
||||
_, _, err := store.getKeyVersion([]interface{}{"version", "TEST_VER"})
|
||||
assert.NotNil(t, err, "failed to respond to missing data field")
|
||||
})
|
||||
t.Run("With missing version", func(t *testing.T) {
|
||||
_, _, err := store.getKeyVersion([]interface{}{"data", "TEST_KEY"})
|
||||
assert.NotNil(t, err, "failed to respond to missing version field")
|
||||
})
|
||||
t.Run("With all required fields - out of order", func(t *testing.T) {
|
||||
key, ver, err := store.getKeyVersion([]interface{}{"version", "TEST_VER", "dragon", "TEST_DRAGON", "data", "TEST_KEY"})
|
||||
assert.Equal(t, nil, err, "failed to read all fields")
|
||||
assert.Equal(t, "TEST_KEY", key, "failed to read key")
|
||||
assert.Equal(t, "TEST_VER", ver, "failed to read version")
|
||||
})
|
||||
t.Run("With no fields", func(t *testing.T) {
|
||||
_, _, err := store.getKeyVersion([]interface{}{})
|
||||
assert.NotNil(t, err, "failed to respond to missing fields")
|
||||
})
|
||||
t.Run("With wrong fields", func(t *testing.T) {
|
||||
_, _, err := store.getKeyVersion([]interface{}{"dragon", "TEST_DRAGON"})
|
||||
assert.NotNil(t, err, "failed to respond to missing fields")
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseEtag(t *testing.T) {
|
||||
store := NewRedisStateStore()
|
||||
t.Run("Empty ETag", func(t *testing.T) {
|
||||
ver, err := store.parseETag("")
|
||||
assert.Equal(t, nil, err, "failed to parse ETag")
|
||||
assert.Equal(t, 0, ver, "default version should be 0")
|
||||
})
|
||||
t.Run("Number ETag", func(t *testing.T) {
|
||||
ver, err := store.parseETag("354")
|
||||
assert.Equal(t, nil, err, "failed to parse ETag")
|
||||
assert.Equal(t, 354, ver, "version should be 254")
|
||||
})
|
||||
t.Run("String ETag", func(t *testing.T) {
|
||||
_, err := store.parseETag("dragon")
|
||||
assert.NotNil(t, err, "shouldn't recognize string ETag")
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseConnectedSlavs(t *testing.T) {
|
||||
store := NewRedisStateStore()
|
||||
|
||||
t.Run("Empty info", func(t *testing.T) {
|
||||
slaves := store.parseConnectedSlaves("")
|
||||
assert.Equal(t, 0, slaves, "connected slaves must be 0")
|
||||
})
|
||||
|
||||
t.Run("connectedSlaves property is not included", func(t *testing.T) {
|
||||
slaves := store.parseConnectedSlaves("# Replication\r\nrole:master\r\n")
|
||||
assert.Equal(t, 0, slaves, "connected slaves must be 0")
|
||||
})
|
||||
|
||||
t.Run("connectedSlaves is 2", func(t *testing.T) {
|
||||
slaves := store.parseConnectedSlaves("# Replication\r\nrole:master\r\nconnected_slaves:2\r\n")
|
||||
assert.Equal(t, 2, slaves, "connected slaves must be 2")
|
||||
})
|
||||
|
||||
t.Run("connectedSlaves is 1", func(t *testing.T) {
|
||||
slaves := store.parseConnectedSlaves("# Replication\r\nrole:master\r\nconnected_slaves:1")
|
||||
assert.Equal(t, 1, slaves, "connected slaves must be 1")
|
||||
})
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package state
|
||||
|
||||
import "time"
|
||||
|
||||
// GetRequest is the object describing a state fetch request
|
||||
type GetRequest struct {
|
||||
Key string `json:"key"`
|
||||
Options GetStateOption `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// GetStateOption controls how a state store reacts to a get request
|
||||
type GetStateOption struct {
|
||||
Consistency string `json:"consistency"` //"eventual, strong"
|
||||
}
|
||||
|
||||
// DeleteRequest is the object describing a delete state request
|
||||
type DeleteRequest struct {
|
||||
Key string `json:"key"`
|
||||
ETag string `json:"etag,omitempty"`
|
||||
Options DeleteStateOption `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// DeleteStateOption controls how a state store reacts to a delete request
|
||||
type DeleteStateOption struct {
|
||||
Concurrency string `json:"concurrency,omitempty"` //"concurrency"
|
||||
Consistency string `json:"consistency"` //"eventual, strong"
|
||||
RetryPolicy RetryPolicy `json:"retryPolicy,omitempty"`
|
||||
}
|
||||
|
||||
// SetRequest is the object describing an upsert request
|
||||
type SetRequest struct {
|
||||
Key string `json:"key"`
|
||||
Value interface{} `json:"value"`
|
||||
ETag string `json:"etag,omitempty"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
Options SetStateOption `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
//RetryPolicy describes how retries should be handled
|
||||
type RetryPolicy struct {
|
||||
Interval time.Duration `json:"interval"`
|
||||
Threshold int `josn:"threshold"`
|
||||
Pattern string `json:"pattern,omitempty"` //linear, exponential
|
||||
}
|
||||
|
||||
// SetStateOption controls how a state store reacts to a set request
|
||||
type SetStateOption struct {
|
||||
Concurrency string `json:"concurrency,omitempty"` //first-write, last-write
|
||||
Consistency string `json:"consistency"` //"eventual, strong"
|
||||
RetryPolicy RetryPolicy `json:"retryPolicy,omitempty"`
|
||||
}
|
||||
|
||||
// OperationType describes a CRUD operation performed against a state store
|
||||
type OperationType string
|
||||
|
||||
// Upsert is an update or create operation
|
||||
const Upsert OperationType = "upsert"
|
||||
|
||||
// Delete is a delete operation
|
||||
const Delete OperationType = "delete"
|
||||
|
||||
// TransactionalRequest describes a transactional operation against a state store that comprises multiple types of operations
|
||||
// The Request field is either a DeleteRequest or SetRequest
|
||||
type TransactionalRequest struct {
|
||||
Operation OperationType `json:"operation"`
|
||||
Request interface{} `json:"request"`
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package state
|
||||
|
||||
// GetResponse is the reqeust object for getting state
|
||||
type GetResponse struct {
|
||||
Data []byte `json:"data"`
|
||||
ETag string `json:"etag,omitempty"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
FirstWrite = "first-write"
|
||||
LastWrite = "last-write"
|
||||
Strong = "strong"
|
||||
Eventual = "eventual"
|
||||
Exponential = "exponential"
|
||||
Linear = "linear"
|
||||
)
|
||||
|
||||
// CheckSetRequestOptions checks if set request options use supported keywords
|
||||
func CheckSetRequestOptions(req *SetRequest) error {
|
||||
if req.Options.Concurrency != "" && req.Options.Concurrency != FirstWrite && req.Options.Concurrency != LastWrite {
|
||||
return fmt.Errorf("unrecognized concurrency model '%s'", req.Options.Concurrency)
|
||||
}
|
||||
if req.Options.Consistency != "" && req.Options.Consistency != Strong && req.Options.Consistency != Eventual {
|
||||
return fmt.Errorf("unrecognized consistency model '%s'", req.Options.Consistency)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckDeleteRequestOptions checks if delete request options use supported keywords
|
||||
func CheckDeleteRequestOptions(req *DeleteRequest) error {
|
||||
if req.Options.Concurrency != "" && req.Options.Concurrency != FirstWrite && req.Options.Concurrency != LastWrite {
|
||||
return fmt.Errorf("unrecognized concurrency model '%s'", req.Options.Concurrency)
|
||||
}
|
||||
if req.Options.Consistency != "" && req.Options.Consistency != Strong && req.Options.Consistency != Eventual {
|
||||
return fmt.Errorf("unrecognized consistency model '%s'", req.Options.Consistency)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetWithRetries handles SetRequest with retries
|
||||
func SetWithRetries(method func(req *SetRequest) error, req *SetRequest) error {
|
||||
switch req.Options.RetryPolicy.Pattern {
|
||||
case "":
|
||||
fallthrough
|
||||
case Linear:
|
||||
fallthrough
|
||||
case Exponential:
|
||||
if req.Options.RetryPolicy.Threshold > 0 {
|
||||
duration := req.Options.RetryPolicy.Interval
|
||||
for i := 0; i < req.Options.RetryPolicy.Threshold; i++ {
|
||||
err := method(req)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(duration)
|
||||
if req.Options.RetryPolicy.Pattern == Exponential {
|
||||
duration *= 2
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to set value after %d retries", req.Options.RetryPolicy.Threshold)
|
||||
} else {
|
||||
return method(req)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unrecognized retry patter '%s'", req.Options.RetryPolicy.Pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteWithRetries handles SetRequest with retries
|
||||
func DeleteWithRetries(method func(req *DeleteRequest) error, req *DeleteRequest) error {
|
||||
switch req.Options.RetryPolicy.Pattern {
|
||||
case "":
|
||||
fallthrough
|
||||
case Linear:
|
||||
fallthrough
|
||||
case Exponential:
|
||||
if req.Options.RetryPolicy.Threshold > 0 {
|
||||
duration := req.Options.RetryPolicy.Interval
|
||||
for i := 0; i < req.Options.RetryPolicy.Threshold; i++ {
|
||||
err := method(req)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(duration)
|
||||
if req.Options.RetryPolicy.Pattern == Exponential {
|
||||
duration *= 2
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to set value after %d retries", req.Options.RetryPolicy.Threshold)
|
||||
} else {
|
||||
return method(req)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unrecognized retry patter '%s'", req.Options.RetryPolicy.Pattern)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRetriPolicies(t *testing.T) {
|
||||
t.Run("set with no options", func(t *testing.T) {
|
||||
counter := 0
|
||||
SetWithRetries(func(req *SetRequest) error {
|
||||
counter++
|
||||
return nil
|
||||
}, &SetRequest{})
|
||||
assert.Equal(t, 1, counter, "should execute only once")
|
||||
})
|
||||
|
||||
t.Run("set with no retry policies", func(t *testing.T) {
|
||||
counter := 0
|
||||
SetWithRetries(func(req *SetRequest) error {
|
||||
counter++
|
||||
return nil
|
||||
}, &SetRequest{
|
||||
Options: SetStateOption{},
|
||||
})
|
||||
assert.Equal(t, 1, counter, "should execute only once")
|
||||
})
|
||||
|
||||
t.Run("set with empty retry policies", func(t *testing.T) {
|
||||
counter := 0
|
||||
SetWithRetries(func(req *SetRequest) error {
|
||||
counter++
|
||||
return nil
|
||||
}, &SetRequest{
|
||||
Options: SetStateOption{
|
||||
RetryPolicy: RetryPolicy{
|
||||
Interval: 0,
|
||||
Threshold: 0,
|
||||
Pattern: "",
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, 1, counter, "should execute only once")
|
||||
})
|
||||
|
||||
t.Run("bad policy", func(t *testing.T) {
|
||||
ret := SetWithRetries(func(req *SetRequest) error {
|
||||
return nil
|
||||
}, &SetRequest{
|
||||
Options: SetStateOption{
|
||||
RetryPolicy: RetryPolicy{
|
||||
Interval: 100,
|
||||
Threshold: 3,
|
||||
Pattern: "dummy",
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NotNil(t, ret, "should reject policy")
|
||||
})
|
||||
|
||||
t.Run("liner retry 3 times", func(t *testing.T) {
|
||||
counter := 0
|
||||
SetWithRetries(func(req *SetRequest) error {
|
||||
counter++
|
||||
if counter < 3 {
|
||||
return fmt.Errorf("BAD")
|
||||
}
|
||||
return nil
|
||||
}, &SetRequest{
|
||||
Options: SetStateOption{
|
||||
RetryPolicy: RetryPolicy{
|
||||
Interval: 100,
|
||||
Threshold: 3,
|
||||
Pattern: "linear",
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, 3, counter, "should execute 3 times")
|
||||
})
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package state
|
||||
|
||||
type StateStore interface {
|
||||
Init(metadata Metadata) error
|
||||
Delete(req *DeleteRequest) error
|
||||
BulkDelete(req []DeleteRequest) error
|
||||
Get(req *GetRequest) (*GetResponse, error)
|
||||
Set(req *SetRequest) error
|
||||
BulkSet(req []SetRequest) error
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package state
|
||||
|
||||
type TransactionalStateStore interface {
|
||||
Init(metadata Metadata) error
|
||||
Multi(reqs []TransactionalRequest) error
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,44 @@
|
|||
# Contributor Code of Conduct
|
||||
|
||||
As contributors and maintainers of this project,
|
||||
and in the interest of fostering an open and welcoming community,
|
||||
we pledge to respect all people who contribute through reporting issues,
|
||||
posting feature requests, updating documentation,
|
||||
submitting pull requests or patches, and other activities.
|
||||
|
||||
We are committed to making participation in this project
|
||||
a harassment-free experience for everyone,
|
||||
regardless of level of experience, gender, gender identity and expression,
|
||||
sexual orientation, disability, personal appearance,
|
||||
body size, race, ethnicity, age, religion, or nationality.
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery
|
||||
* Personal attacks
|
||||
* Trolling or insulting/derogatory comments
|
||||
* Public or private harassment
|
||||
* Publishing other's private information,
|
||||
such as physical or electronic
|
||||
addresses, without explicit permission
|
||||
* Other unethical or unprofessional conduct.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct.
|
||||
By adopting this Code of Conduct,
|
||||
project maintainers commit themselves to fairly and consistently
|
||||
applying these principles to every aspect of managing this project.
|
||||
Project maintainers who do not follow or enforce the Code of Conduct
|
||||
may be permanently removed from the project team.
|
||||
|
||||
This code of conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior
|
||||
may be reported by opening an issue
|
||||
or contacting one or more of the project maintainers.
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0,
|
||||
available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
|
||||
|
|
@ -0,0 +1,301 @@
|
|||
# Contributing
|
||||
|
||||
1. [Install Go](https://golang.org/dl/).
|
||||
1. Ensure that your `GOBIN` directory (by default `$(go env GOPATH)/bin`)
|
||||
is in your `PATH`.
|
||||
1. Check it's working by running `go version`.
|
||||
* If it doesn't work, check the install location, usually
|
||||
`/usr/local/go`, is on your `PATH`.
|
||||
|
||||
1. Sign one of the
|
||||
[contributor license agreements](#contributor-license-agreements) below.
|
||||
|
||||
1. Run `GO111MODULE=off go get golang.org/x/review/git-codereview` to install
|
||||
the code reviewing tool.
|
||||
|
||||
1. Ensure it's working by running `git codereview` (check your `PATH` if
|
||||
not).
|
||||
|
||||
1. If you would like, you may want to set up aliases for `git-codereview`,
|
||||
such that `git codereview change` becomes `git change`. See the
|
||||
[godoc](https://godoc.org/golang.org/x/review/git-codereview) for details.
|
||||
|
||||
* Should you run into issues with the `git-codereview` tool, please note
|
||||
that all error messages will assume that you have set up these aliases.
|
||||
|
||||
1. Change to a directory of your choosing and clone the repo.b
|
||||
|
||||
```
|
||||
cd ~/code
|
||||
git clone https://code.googlesource.com/gocloud
|
||||
```
|
||||
|
||||
* If you have already checked out the source, make sure that the remote
|
||||
`git` `origin` is https://code.googlesource.com/gocloud:
|
||||
|
||||
```
|
||||
git remote -v
|
||||
# ...
|
||||
git remote set-url origin https://code.googlesource.com/gocloud
|
||||
```
|
||||
|
||||
* The project uses [Go Modules](https://blog.golang.org/using-go-modules)
|
||||
for dependency management See
|
||||
[`gopls`](https://github.com/golang/go/wiki/gopls) for making your editor
|
||||
work with modules.
|
||||
|
||||
1. Change to the project directory:
|
||||
|
||||
```
|
||||
cd ~/code/gocloud
|
||||
```
|
||||
|
||||
1. Make sure your `git` auth is configured correctly by visiting
|
||||
https://code.googlesource.com, clicking "Generate Password" at the top-right,
|
||||
and following the directions. Otherwise, `git codereview mail` in the next step
|
||||
will fail.
|
||||
|
||||
1. Make changes then use `git codereview` to create a commit and create a Gerrit
|
||||
CL:
|
||||
|
||||
```
|
||||
git codereview change <name>
|
||||
# Make changes.
|
||||
git add ...
|
||||
git codereview change
|
||||
git codereview mail # If this fails, the error message will contain instructions to fix it.
|
||||
```
|
||||
|
||||
* This will create a new `git` branch for you to develop on. Once your
|
||||
change is merged, you can delete this branch.
|
||||
|
||||
1. As you make changes for code review, ammend the commit and re-mail the
|
||||
change:
|
||||
|
||||
```
|
||||
# Make more changes.
|
||||
git add ...
|
||||
git codereview change <name>
|
||||
git codereview mail
|
||||
```
|
||||
|
||||
* **Warning**: do not change the `Change-Id` at the bottom of the commit
|
||||
message - it's how Gerrit knows which change this is (or if it's new).
|
||||
|
||||
* When you fixes issues from code review, respond to each code review
|
||||
message then click **Reply** at the top of the page.
|
||||
|
||||
* Each new mailed amendment will create a new patch set for
|
||||
your change in Gerrit. Patch sets can be compared and reviewed.
|
||||
|
||||
* **Note**: if your change includes a breaking change, our breaking change
|
||||
detector will cause CI/CD to fail. If your breaking change is acceptable
|
||||
in some way, add a `BREAKING_CHANGE_ACCEPTABLE=<reason>` line to the commit
|
||||
message to cause the detector not to be run and to make it clear why that is
|
||||
acceptable.
|
||||
|
||||
## Integration Tests
|
||||
|
||||
In addition to the unit tests, you may run the integration test suite. These
|
||||
directions describe setting up your environment to run integration tests for
|
||||
_all_ packages: note that many of these instructions may be redundant if you
|
||||
intend only to run integration tests on a single package.
|
||||
|
||||
#### GCP Setup
|
||||
|
||||
To run the integrations tests, creation and configuration of two projects in
|
||||
the Google Developers Console is required: one specifically for Firestore
|
||||
integration tests, and another for all other integration tests. We'll refer to
|
||||
these projects as "general project" and "Firestore project".
|
||||
|
||||
After creating each project, you must [create a service account](https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount)
|
||||
for each project. Ensure the project-level **Owner**
|
||||
[IAM role](console.cloud.google.com/iam-admin/iam/project) role is added to
|
||||
each service account. During the creation of the service account, you should
|
||||
download the JSON credential file for use later.
|
||||
|
||||
Next, ensure the following APIs are enabled in the general project:
|
||||
|
||||
- BigQuery API
|
||||
- BigQuery Data Transfer API
|
||||
- Cloud Dataproc API
|
||||
- Cloud Dataproc Control API Private
|
||||
- Cloud Datastore API
|
||||
- Cloud Firestore API
|
||||
- Cloud Key Management Service (KMS) API
|
||||
- Cloud Natural Language API
|
||||
- Cloud OS Login API
|
||||
- Cloud Pub/Sub API
|
||||
- Cloud Resource Manager API
|
||||
- Cloud Spanner API
|
||||
- Cloud Speech API
|
||||
- Cloud Translation API
|
||||
- Cloud Video Intelligence API
|
||||
- Cloud Vision API
|
||||
- Compute Engine API
|
||||
- Compute Engine Instance Group Manager API
|
||||
- Container Registry API
|
||||
- Firebase Rules API
|
||||
- Google Cloud APIs
|
||||
- Google Cloud Deployment Manager V2 API
|
||||
- Google Cloud SQL
|
||||
- Google Cloud Storage
|
||||
- Google Cloud Storage JSON API
|
||||
- Google Compute Engine Instance Group Updater API
|
||||
- Google Compute Engine Instance Groups API
|
||||
- Kubernetes Engine API
|
||||
- Stackdriver Error Reporting API
|
||||
|
||||
Next, create a Datastore database in the general project, and a Firestore
|
||||
database in the Firestore project.
|
||||
|
||||
Finally, in the general project, create an API key for the translate API:
|
||||
|
||||
- Go to GCP Developer Console.
|
||||
- Navigate to APIs & Services > Credentials.
|
||||
- Click Create Credentials > API Key.
|
||||
- Save this key for use in `GCLOUD_TESTS_API_KEY` as described below.
|
||||
|
||||
#### Local Setup
|
||||
|
||||
Once the two projects are created and configured, set the following environment
|
||||
variables:
|
||||
|
||||
- `GCLOUD_TESTS_GOLANG_PROJECT_ID`: Developers Console project's ID (e.g.
|
||||
bamboo-shift-455) for the general project.
|
||||
- `GCLOUD_TESTS_GOLANG_KEY`: The path to the JSON key file of the general
|
||||
project's service account.
|
||||
- `GCLOUD_TESTS_GOLANG_FIRESTORE_PROJECT_ID`: Developers Console project's ID
|
||||
(e.g. doorway-cliff-677) for the Firestore project.
|
||||
- `GCLOUD_TESTS_GOLANG_FIRESTORE_KEY`: The path to the JSON key file of the
|
||||
Firestore project's service account.
|
||||
- `GCLOUD_TESTS_GOLANG_KEYRING`: The full name of the keyring for the tests,
|
||||
in the form
|
||||
"projects/P/locations/L/keyRings/R". The creation of this is described below.
|
||||
- `GCLOUD_TESTS_API_KEY`: API key for using the Translate API.
|
||||
- `GCLOUD_TESTS_GOLANG_ZONE`: Compute Engine zone.
|
||||
|
||||
Install the [gcloud command-line tool][gcloudcli] to your machine and use it to
|
||||
create some resources used in integration tests.
|
||||
|
||||
From the project's root directory:
|
||||
|
||||
``` sh
|
||||
# Sets the default project in your env.
|
||||
$ gcloud config set project $GCLOUD_TESTS_GOLANG_PROJECT_ID
|
||||
|
||||
# Authenticates the gcloud tool with your account.
|
||||
$ gcloud auth login
|
||||
|
||||
# Create the indexes used in the datastore integration tests.
|
||||
$ gcloud datastore indexes create datastore/testdata/index.yaml
|
||||
|
||||
# Creates a Google Cloud storage bucket with the same name as your test project,
|
||||
# and with the Stackdriver Logging service account as owner, for the sink
|
||||
# integration tests in logging.
|
||||
$ gsutil mb gs://$GCLOUD_TESTS_GOLANG_PROJECT_ID
|
||||
$ gsutil acl ch -g cloud-logs@google.com:O gs://$GCLOUD_TESTS_GOLANG_PROJECT_ID
|
||||
|
||||
# Creates a PubSub topic for integration tests of storage notifications.
|
||||
$ gcloud beta pubsub topics create go-storage-notification-test
|
||||
# Next, go to the Pub/Sub dashboard in GCP console. Authorize the user
|
||||
# "service-<numberic project id>@gs-project-accounts.iam.gserviceaccount.com"
|
||||
# as a publisher to that topic.
|
||||
|
||||
# Creates a Spanner instance for the spanner integration tests.
|
||||
$ gcloud beta spanner instances create go-integration-test --config regional-us-central1 --nodes 10 --description 'Instance for go client test'
|
||||
# NOTE: Spanner instances are priced by the node-hour, so you may want to
|
||||
# delete the instance after testing with 'gcloud beta spanner instances delete'.
|
||||
|
||||
$ export MY_KEYRING=some-keyring-name
|
||||
$ export MY_LOCATION=global
|
||||
# Creates a KMS keyring, in the same location as the default location for your
|
||||
# project's buckets.
|
||||
$ gcloud kms keyrings create $MY_KEYRING --location $MY_LOCATION
|
||||
# Creates two keys in the keyring, named key1 and key2.
|
||||
$ gcloud kms keys create key1 --keyring $MY_KEYRING --location $MY_LOCATION --purpose encryption
|
||||
$ gcloud kms keys create key2 --keyring $MY_KEYRING --location $MY_LOCATION --purpose encryption
|
||||
# Sets the GCLOUD_TESTS_GOLANG_KEYRING environment variable.
|
||||
$ export GCLOUD_TESTS_GOLANG_KEYRING=projects/$GCLOUD_TESTS_GOLANG_PROJECT_ID/locations/$MY_LOCATION/keyRings/$MY_KEYRING
|
||||
# Authorizes Google Cloud Storage to encrypt and decrypt using key1.
|
||||
gsutil kms authorize -p $GCLOUD_TESTS_GOLANG_PROJECT_ID -k $GCLOUD_TESTS_GOLANG_KEYRING/cryptoKeys/key1
|
||||
```
|
||||
|
||||
#### Running
|
||||
|
||||
Once you've done the necessary setup, you can run the integration tests by
|
||||
running:
|
||||
|
||||
``` sh
|
||||
$ go test -v cloud.google.com/go/...
|
||||
```
|
||||
|
||||
#### Replay
|
||||
|
||||
Some packages can record the RPCs during integration tests to a file for
|
||||
subsequent replay. To record, pass the `-record` flag to `go test`. The
|
||||
recording will be saved to the _package_`.replay` file. To replay integration
|
||||
tests from a saved recording, the replay file must be present, the `-short`
|
||||
flag must be passed to `go test`, and the `GCLOUD_TESTS_GOLANG_ENABLE_REPLAY`
|
||||
environment variable must have a non-empty value.
|
||||
|
||||
## Contributor License Agreements
|
||||
|
||||
Before we can accept your pull requests you'll need to sign a Contributor
|
||||
License Agreement (CLA):
|
||||
|
||||
- **If you are an individual writing original source code** and **you own the
|
||||
intellectual property**, then you'll need to sign an [individual CLA][indvcla].
|
||||
- **If you work for a company that wants to allow you to contribute your
|
||||
work**, then you'll need to sign a [corporate CLA][corpcla].
|
||||
|
||||
You can sign these electronically (just scroll to the bottom). After that,
|
||||
we'll be able to accept your pull requests.
|
||||
|
||||
## Contributor Code of Conduct
|
||||
|
||||
As contributors and maintainers of this project,
|
||||
and in the interest of fostering an open and welcoming community,
|
||||
we pledge to respect all people who contribute through reporting issues,
|
||||
posting feature requests, updating documentation,
|
||||
submitting pull requests or patches, and other activities.
|
||||
|
||||
We are committed to making participation in this project
|
||||
a harassment-free experience for everyone,
|
||||
regardless of level of experience, gender, gender identity and expression,
|
||||
sexual orientation, disability, personal appearance,
|
||||
body size, race, ethnicity, age, religion, or nationality.
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery
|
||||
* Personal attacks
|
||||
* Trolling or insulting/derogatory comments
|
||||
* Public or private harassment
|
||||
* Publishing other's private information,
|
||||
such as physical or electronic
|
||||
addresses, without explicit permission
|
||||
* Other unethical or unprofessional conduct.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct.
|
||||
By adopting this Code of Conduct,
|
||||
project maintainers commit themselves to fairly and consistently
|
||||
applying these principles to every aspect of managing this project.
|
||||
Project maintainers who do not follow or enforce the Code of Conduct
|
||||
may be permanently removed from the project team.
|
||||
|
||||
This code of conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior
|
||||
may be reported by opening an issue
|
||||
or contacting one or more of the project maintainers.
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0,
|
||||
available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
|
||||
|
||||
[gcloudcli]: https://developers.google.com/cloud/sdk/gcloud/
|
||||
[indvcla]: https://developers.google.com/open-source/cla/individual
|
||||
[corpcla]: https://developers.google.com/open-source/cla/corporate
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
|
@ -0,0 +1,227 @@
|
|||
# Google Cloud Client Libraries for Go
|
||||
|
||||
[](https://godoc.org/cloud.google.com/go)
|
||||
|
||||
Go packages for [Google Cloud Platform](https://cloud.google.com) services.
|
||||
|
||||
``` go
|
||||
import "cloud.google.com/go"
|
||||
```
|
||||
|
||||
To install the packages on your system, *do not clone the repo*. Instead use
|
||||
|
||||
```
|
||||
$ go get -u cloud.google.com/go/...
|
||||
```
|
||||
|
||||
**NOTE:** Some of these packages are under development, and may occasionally
|
||||
make backwards-incompatible changes.
|
||||
|
||||
**NOTE:** Github repo is a mirror of [https://code.googlesource.com/gocloud](https://code.googlesource.com/gocloud).
|
||||
|
||||
## Supported APIs
|
||||
|
||||
Google API | Status | Package
|
||||
------------------------------------------------|--------------|-----------------------------------------------------------
|
||||
[Asset][cloud-asset] | alpha | [`cloud.google.com/go/asset/v1beta`][cloud-asset-ref]
|
||||
[BigQuery][cloud-bigquery] | stable | [`cloud.google.com/go/bigquery`][cloud-bigquery-ref]
|
||||
[Bigtable][cloud-bigtable] | stable | [`cloud.google.com/go/bigtable`][cloud-bigtable-ref]
|
||||
[Cloudtasks][cloud-tasks] | stable | [`cloud.google.com/go/cloudtasks/apiv2`][cloud-tasks-ref]
|
||||
[Container][cloud-container] | stable | [`cloud.google.com/go/container/apiv1`][cloud-container-ref]
|
||||
[ContainerAnalysis][cloud-containeranalysis] | beta | [`cloud.google.com/go/containeranalysis/apiv1beta1`][cloud-containeranalysis-ref]
|
||||
[Dataproc][cloud-dataproc] | stable | [`cloud.google.com/go/dataproc/apiv1`][cloud-dataproc-ref]
|
||||
[Datastore][cloud-datastore] | stable | [`cloud.google.com/go/datastore`][cloud-datastore-ref]
|
||||
[Debugger][cloud-debugger] | alpha | [`cloud.google.com/go/debugger/apiv2`][cloud-debugger-ref]
|
||||
[Dialogflow][cloud-dialogflow] | alpha | [`cloud.google.com/go/dialogflow/apiv2`][cloud-dialogflow-ref]
|
||||
[Data Loss Prevention][cloud-dlp] | alpha | [`cloud.google.com/go/dlp/apiv2`][cloud-dlp-ref]
|
||||
[ErrorReporting][cloud-errors] | alpha | [`cloud.google.com/go/errorreporting`][cloud-errors-ref]
|
||||
[Firestore][cloud-firestore] | stable | [`cloud.google.com/go/firestore`][cloud-firestore-ref]
|
||||
[IAM][cloud-iam] | stable | [`cloud.google.com/go/iam`][cloud-iam-ref]
|
||||
[IoT][cloud-iot] | alpha | [`cloud.google.com/iot/apiv1`][cloud-iot-ref]
|
||||
[KMS][cloud-kms] | stable | [`cloud.google.com/go/kms`][cloud-kms-ref]
|
||||
[Natural Language][cloud-natural-language] | stable | [`cloud.google.com/go/language/apiv1`][cloud-natural-language-ref]
|
||||
[Logging][cloud-logging] | stable | [`cloud.google.com/go/logging`][cloud-logging-ref]
|
||||
[Monitoring][cloud-monitoring] | alpha | [`cloud.google.com/go/monitoring/apiv3`][cloud-monitoring-ref]
|
||||
[OS Login][cloud-oslogin] | alpha | [`cloud.google.com/go/oslogin/apiv1`][cloud-oslogin-ref]
|
||||
[Pub/Sub][cloud-pubsub] | stable | [`cloud.google.com/go/pubsub`][cloud-pubsub-ref]
|
||||
[Phishing Protection][cloud-phishingprotection] | alpha | [`cloud.google.com/go/phishingprotection/apiv1betad1`][cloud-phishingprotection-ref]
|
||||
[reCAPTCHA Enterprise][cloud-recaptcha] | alpha | [`cloud.google.com/go/recaptchaenterprise/apiv1betad1`][cloud-recaptcha-ref]
|
||||
[Memorystore][cloud-memorystore] | alpha | [`cloud.google.com/go/redis/apiv1`][cloud-memorystore-ref]
|
||||
[Scheduler][cloud-scheduler] | stable | [`cloud.google.com/go/scheduler/apiv1`][cloud-scheduler-ref]
|
||||
[Spanner][cloud-spanner] | stable | [`cloud.google.com/go/spanner`][cloud-spanner-ref]
|
||||
[Speech][cloud-speech] | stable | [`cloud.google.com/go/speech/apiv1`][cloud-speech-ref]
|
||||
[Storage][cloud-storage] | stable | [`cloud.google.com/go/storage`][cloud-storage-ref]
|
||||
[Talent][cloud-talent] | alpha | [`cloud.google.com/go/talent/apiv4beta1`][cloud-talent-ref]
|
||||
[Text To Speech][cloud-texttospeech] | alpha | [`cloud.google.com/go/texttospeech/apiv1`][cloud-texttospeech-ref]
|
||||
[Trace][cloud-trace] | alpha | [`cloud.google.com/go/trace/apiv2`][cloud-trace-ref]
|
||||
[Translate][cloud-translate] | stable | [`cloud.google.com/go/translate`][cloud-translate-ref]
|
||||
[Video Intelligence][cloud-video] | alpha | [`cloud.google.com/go/videointelligence/apiv1beta1`][cloud-video-ref]
|
||||
[Vision][cloud-vision] | stable | [`cloud.google.com/go/vision/apiv1`][cloud-vision-ref]
|
||||
|
||||
> **Alpha status**: the API is still being actively developed. As a
|
||||
> result, it might change in backward-incompatible ways and is not recommended
|
||||
> for production use.
|
||||
>
|
||||
> **Beta status**: the API is largely complete, but still has outstanding
|
||||
> features and bugs to be addressed. There may be minor backwards-incompatible
|
||||
> changes where necessary.
|
||||
>
|
||||
> **Stable status**: the API is mature and ready for production use. We will
|
||||
> continue addressing bugs and feature requests.
|
||||
|
||||
Documentation and examples are available at [godoc.org/cloud.google.com/go](godoc.org/cloud.google.com/go)
|
||||
|
||||
## Go Versions Supported
|
||||
|
||||
We support the two most recent major versions of Go. If Google App Engine uses
|
||||
an older version, we support that as well.
|
||||
|
||||
## Authorization
|
||||
|
||||
By default, each API will use [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials)
|
||||
for authorization credentials used in calling the API endpoints. This will allow your
|
||||
application to run in many environments without requiring explicit configuration.
|
||||
|
||||
[snip]:# (auth)
|
||||
```go
|
||||
client, err := storage.NewClient(ctx)
|
||||
```
|
||||
|
||||
To authorize using a
|
||||
[JSON key file](https://cloud.google.com/iam/docs/managing-service-account-keys),
|
||||
pass
|
||||
[`option.WithCredentialsFile`](https://godoc.org/google.golang.org/api/option#WithCredentialsFile)
|
||||
to the `NewClient` function of the desired package. For example:
|
||||
|
||||
[snip]:# (auth-JSON)
|
||||
```go
|
||||
client, err := storage.NewClient(ctx, option.WithCredentialsFile("path/to/keyfile.json"))
|
||||
```
|
||||
|
||||
You can exert more control over authorization by using the
|
||||
[`golang.org/x/oauth2`](https://godoc.org/golang.org/x/oauth2) package to
|
||||
create an `oauth2.TokenSource`. Then pass
|
||||
[`option.WithTokenSource`](https://godoc.org/google.golang.org/api/option#WithTokenSource)
|
||||
to the `NewClient` function:
|
||||
[snip]:# (auth-ts)
|
||||
```go
|
||||
tokenSource := ...
|
||||
client, err := storage.NewClient(ctx, option.WithTokenSource(tokenSource))
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome. Please, see the
|
||||
[CONTRIBUTING](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/master/CONTRIBUTING.md)
|
||||
document for details. We're using Gerrit for our code reviews. Please don't open pull
|
||||
requests against this repo, new pull requests will be automatically closed.
|
||||
|
||||
Please note that this project is released with a Contributor Code of Conduct.
|
||||
By participating in this project you agree to abide by its terms.
|
||||
See [Contributor Code of Conduct](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/master/CONTRIBUTING.md#contributor-code-of-conduct)
|
||||
for more information.
|
||||
|
||||
[cloud-datastore]: https://cloud.google.com/datastore/
|
||||
[cloud-datastore-ref]: https://godoc.org/cloud.google.com/go/datastore
|
||||
|
||||
[cloud-firestore]: https://cloud.google.com/firestore/
|
||||
[cloud-firestore-ref]: https://godoc.org/cloud.google.com/go/firestore
|
||||
|
||||
[cloud-pubsub]: https://cloud.google.com/pubsub/
|
||||
[cloud-pubsub-ref]: https://godoc.org/cloud.google.com/go/pubsub
|
||||
|
||||
[cloud-storage]: https://cloud.google.com/storage/
|
||||
[cloud-storage-ref]: https://godoc.org/cloud.google.com/go/storage
|
||||
|
||||
[cloud-bigtable]: https://cloud.google.com/bigtable/
|
||||
[cloud-bigtable-ref]: https://godoc.org/cloud.google.com/go/bigtable
|
||||
|
||||
[cloud-bigquery]: https://cloud.google.com/bigquery/
|
||||
[cloud-bigquery-ref]: https://godoc.org/cloud.google.com/go/bigquery
|
||||
|
||||
[cloud-logging]: https://cloud.google.com/logging/
|
||||
[cloud-logging-ref]: https://godoc.org/cloud.google.com/go/logging
|
||||
|
||||
[cloud-monitoring]: https://cloud.google.com/monitoring/
|
||||
[cloud-monitoring-ref]: https://godoc.org/cloud.google.com/go/monitoring/apiv3
|
||||
|
||||
[cloud-vision]: https://cloud.google.com/vision
|
||||
[cloud-vision-ref]: https://godoc.org/cloud.google.com/go/vision/apiv1
|
||||
|
||||
[cloud-language]: https://cloud.google.com/natural-language
|
||||
[cloud-language-ref]: https://godoc.org/cloud.google.com/go/language/apiv1
|
||||
|
||||
[cloud-oslogin]: https://cloud.google.com/compute/docs/oslogin/rest
|
||||
[cloud-oslogin-ref]: https://cloud.google.com/go/oslogin/apiv1
|
||||
|
||||
[cloud-speech]: https://cloud.google.com/speech
|
||||
[cloud-speech-ref]: https://godoc.org/cloud.google.com/go/speech/apiv1
|
||||
|
||||
[cloud-spanner]: https://cloud.google.com/spanner/
|
||||
[cloud-spanner-ref]: https://godoc.org/cloud.google.com/go/spanner
|
||||
|
||||
[cloud-translate]: https://cloud.google.com/translate
|
||||
[cloud-translate-ref]: https://godoc.org/cloud.google.com/go/translate
|
||||
|
||||
[cloud-video]: https://cloud.google.com/video-intelligence/
|
||||
[cloud-video-ref]: https://godoc.org/cloud.google.com/go/videointelligence/apiv1beta1
|
||||
|
||||
[cloud-errors]: https://cloud.google.com/error-reporting/
|
||||
[cloud-errors-ref]: https://godoc.org/cloud.google.com/go/errorreporting
|
||||
|
||||
[cloud-container]: https://cloud.google.com/containers/
|
||||
[cloud-container-ref]: https://godoc.org/cloud.google.com/go/container/apiv1
|
||||
|
||||
[cloud-debugger]: https://cloud.google.com/debugger/
|
||||
[cloud-debugger-ref]: https://godoc.org/cloud.google.com/go/debugger/apiv2
|
||||
|
||||
[cloud-dlp]: https://cloud.google.com/dlp/
|
||||
[cloud-dlp-ref]: https://godoc.org/cloud.google.com/go/dlp/apiv2beta1
|
||||
|
||||
[cloud-dataproc]: https://cloud.google.com/dataproc/
|
||||
[cloud-dataproc-ref]: https://godoc.org/cloud.google.com/go/dataproc/apiv1
|
||||
|
||||
[cloud-iam]: https://cloud.google.com/iam/
|
||||
[cloud-iam-ref]: https://godoc.org/cloud.google.com/go/iam
|
||||
|
||||
[cloud-kms]: https://cloud.google.com/kms/
|
||||
[cloud-kms-ref]: https://godoc.org/cloud.google.com/go/kms/apiv1
|
||||
|
||||
[cloud-natural-language]: https://cloud.google.com/natural-language/
|
||||
[cloud-natural-language-ref]: https://godoc.org/cloud.google.com/go/language/apiv1
|
||||
|
||||
[cloud-memorystore]: https://cloud.google.com/memorystore/
|
||||
[cloud-memorystore-ref]: https://godoc.org/cloud.google.com/go/redis/apiv1
|
||||
|
||||
[cloud-texttospeech]: https://cloud.google.com/texttospeech/
|
||||
[cloud-texttospeech-ref]: https://godoc.org/cloud.google.com/go/texttospeech/apiv1
|
||||
|
||||
[cloud-trace]: https://cloud.google.com/trace/
|
||||
[cloud-trace-ref]: https://godoc.org/cloud.google.com/go/trace/apiv2
|
||||
|
||||
[cloud-dialogflow]: https://cloud.google.com/dialogflow-enterprise/
|
||||
[cloud-dialogflow-ref]: https://godoc.org/cloud.google.com/go/dialogflow/apiv2
|
||||
|
||||
[cloud-containeranalysis]: https://cloud.google.com/container-registry/docs/container-analysis
|
||||
[cloud-containeranalysis-ref]: https://godoc.org/cloud.google.com/go/devtools/containeranalysis/apiv1beta1
|
||||
|
||||
[cloud-asset]: https://cloud.google.com/security-command-center/docs/how-to-asset-inventory
|
||||
[cloud-asset-ref]: https://godoc.org/cloud.google.com/go/asset/apiv1
|
||||
|
||||
[cloud-tasks]: https://cloud.google.com/tasks/
|
||||
[cloud-tasks-ref]: https://godoc.org/cloud.google.com/go/cloudtasks/apiv2
|
||||
|
||||
[cloud-scheduler]: https://cloud.google.com/scheduler
|
||||
[cloud-scheduler-ref]: https://godoc.org/cloud.google.com/go/scheduler/apiv1
|
||||
|
||||
[cloud-iot]: https://cloud.google.com/iot-core/
|
||||
[cloud-iot-ref]: https://godoc.org/cloud.google.com/go/iot/apiv1
|
||||
|
||||
[cloud-phishingprotection]: https://cloud.google.com/phishing-protection/
|
||||
[cloud-phishingprotection-ref]: https://cloud.google.com/go/phishingprotection/apiv1beta1
|
||||
|
||||
[cloud-recaptcha]: https://cloud.google.com/recaptcha-enterprise/
|
||||
[cloud-recaptcha-ref]: https://cloud.google.com/go/recaptchaenterprise/apiv1beta1
|
||||
|
||||
[cloud-talent]: https://cloud.google.com/solutions/talent-solution/
|
||||
[cloud-talent-ref]: https://godoc.org/cloud.google.com/go/talent/apiv4beta1
|
|
@ -0,0 +1,49 @@
|
|||
# How to release `cloud.google.com/go`
|
||||
|
||||
1. Determine the current release version with `git tag -l`. It should look
|
||||
something like `vX.Y.Z`. We'll call the current version `$CV` and the new
|
||||
version `$NV`.
|
||||
1. On master, run `git log $CV...` to list all the changes since the last
|
||||
release. NOTE: You must manually exclude changes from submodules [1].
|
||||
1. Edit `CHANGES.md` to include a summary of the changes.
|
||||
1. `cd internal/version && go generate && cd -`
|
||||
1. `./tidyall.sh`
|
||||
1. Mail the CL. When the CL is approved, submit it.
|
||||
1. Without submitting any other CLs:
|
||||
a. Switch to master.
|
||||
b. `git pull`
|
||||
c. Tag the repo with the next version: `git tag $NV`.
|
||||
d. Push the tag: `git push origin $NV`.
|
||||
1. Update [the releases page](https://github.com/googleapis/google-cloud-go/releases)
|
||||
with the new release, copying the contents of `CHANGES.md`.
|
||||
|
||||
# How to release a submodule
|
||||
|
||||
We have several submodules, including cloud.google.com/go/logging,
|
||||
cloud.google.com/go/datastore, and so on.
|
||||
|
||||
To release a submodule:
|
||||
|
||||
1. (these instructions assume we're releasing cloud.google.com/go/datastore -
|
||||
adjust accordingly)
|
||||
1. Determine the current release version with `git tag -l`. It should look
|
||||
something like `datastore/vX.Y.Z`. We'll call the current version `$CV` and
|
||||
the new version `$NV`, which should look something like `datastore/vX.Y+1.Z`
|
||||
(assuming a minor bump).
|
||||
1. On master, run `git log $CV.. -- datastore/` to list all the changes to the
|
||||
submodule directory since the last release.
|
||||
1. Edit `datastore/CHANGES.md` to include a summary of the changes.
|
||||
1. `./tidyall.sh`
|
||||
1. `cd internal/version && go generate && cd -`
|
||||
1. Mail the CL. When the CL is approved, submit it.
|
||||
1. Without submitting any other CLs:
|
||||
a. Switch to master.
|
||||
b. `git pull`
|
||||
c. Tag the repo with the next version: `git tag $NV`.
|
||||
d. Push the tag: `git push origin $NV`.
|
||||
1. Update [the releases page](https://github.com/googleapis/google-cloud-go/releases)
|
||||
with the new release, copying the contents of `datastore/CHANGES.md`.
|
||||
|
||||
# Appendix
|
||||
|
||||
1: This should get better as submodule tooling matures.
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "metadata",
|
||||
"name_pretty": "Google Compute Engine Metadata API",
|
||||
"product_documentation": "https://cloud.google.com/compute/docs/storing-retrieving-metadata",
|
||||
"client_documentation": "https://godoc.org/cloud.google.com/go/compute/metadata",
|
||||
"release_level": "ga",
|
||||
"language": "go",
|
||||
"repo": "googleapis/google-cloud-go",
|
||||
"distribution_name": "cloud.google.com/go/compute/metadata",
|
||||
"api_id": "compute:metadata",
|
||||
"requires_billing": false
|
||||
}
|
|
@ -0,0 +1,526 @@
|
|||
// Copyright 2014 Google LLC
|
||||
//
|
||||
// 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 metadata provides access to Google Compute Engine (GCE)
|
||||
// metadata and API service accounts.
|
||||
//
|
||||
// This package is a wrapper around the GCE metadata service,
|
||||
// as documented at https://developers.google.com/compute/docs/metadata.
|
||||
package metadata // import "cloud.google.com/go/compute/metadata"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// metadataIP is the documented metadata server IP address.
|
||||
metadataIP = "169.254.169.254"
|
||||
|
||||
// metadataHostEnv is the environment variable specifying the
|
||||
// GCE metadata hostname. If empty, the default value of
|
||||
// metadataIP ("169.254.169.254") is used instead.
|
||||
// This is variable name is not defined by any spec, as far as
|
||||
// I know; it was made up for the Go package.
|
||||
metadataHostEnv = "GCE_METADATA_HOST"
|
||||
|
||||
userAgent = "gcloud-golang/0.1"
|
||||
)
|
||||
|
||||
type cachedValue struct {
|
||||
k string
|
||||
trim bool
|
||||
mu sync.Mutex
|
||||
v string
|
||||
}
|
||||
|
||||
var (
|
||||
projID = &cachedValue{k: "project/project-id", trim: true}
|
||||
projNum = &cachedValue{k: "project/numeric-project-id", trim: true}
|
||||
instID = &cachedValue{k: "instance/id", trim: true}
|
||||
)
|
||||
|
||||
var (
|
||||
defaultClient = &Client{hc: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 2 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
ResponseHeaderTimeout: 2 * time.Second,
|
||||
},
|
||||
}}
|
||||
subscribeClient = &Client{hc: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 2 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
},
|
||||
}}
|
||||
)
|
||||
|
||||
// NotDefinedError is returned when requested metadata is not defined.
|
||||
//
|
||||
// The underlying string is the suffix after "/computeMetadata/v1/".
|
||||
//
|
||||
// This error is not returned if the value is defined to be the empty
|
||||
// string.
|
||||
type NotDefinedError string
|
||||
|
||||
func (suffix NotDefinedError) Error() string {
|
||||
return fmt.Sprintf("metadata: GCE metadata %q not defined", string(suffix))
|
||||
}
|
||||
|
||||
func (c *cachedValue) get(cl *Client) (v string, err error) {
|
||||
defer c.mu.Unlock()
|
||||
c.mu.Lock()
|
||||
if c.v != "" {
|
||||
return c.v, nil
|
||||
}
|
||||
if c.trim {
|
||||
v, err = cl.getTrimmed(c.k)
|
||||
} else {
|
||||
v, err = cl.Get(c.k)
|
||||
}
|
||||
if err == nil {
|
||||
c.v = v
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
onGCEOnce sync.Once
|
||||
onGCE bool
|
||||
)
|
||||
|
||||
// OnGCE reports whether this process is running on Google Compute Engine.
|
||||
func OnGCE() bool {
|
||||
onGCEOnce.Do(initOnGCE)
|
||||
return onGCE
|
||||
}
|
||||
|
||||
func initOnGCE() {
|
||||
onGCE = testOnGCE()
|
||||
}
|
||||
|
||||
func testOnGCE() bool {
|
||||
// The user explicitly said they're on GCE, so trust them.
|
||||
if os.Getenv(metadataHostEnv) != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
resc := make(chan bool, 2)
|
||||
|
||||
// Try two strategies in parallel.
|
||||
// See https://github.com/googleapis/google-cloud-go/issues/194
|
||||
go func() {
|
||||
req, _ := http.NewRequest("GET", "http://"+metadataIP, nil)
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
res, err := defaultClient.hc.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
resc <- false
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
resc <- res.Header.Get("Metadata-Flavor") == "Google"
|
||||
}()
|
||||
|
||||
go func() {
|
||||
addrs, err := net.LookupHost("metadata.google.internal")
|
||||
if err != nil || len(addrs) == 0 {
|
||||
resc <- false
|
||||
return
|
||||
}
|
||||
resc <- strsContains(addrs, metadataIP)
|
||||
}()
|
||||
|
||||
tryHarder := systemInfoSuggestsGCE()
|
||||
if tryHarder {
|
||||
res := <-resc
|
||||
if res {
|
||||
// The first strategy succeeded, so let's use it.
|
||||
return true
|
||||
}
|
||||
// Wait for either the DNS or metadata server probe to
|
||||
// contradict the other one and say we are running on
|
||||
// GCE. Give it a lot of time to do so, since the system
|
||||
// info already suggests we're running on a GCE BIOS.
|
||||
timer := time.NewTimer(5 * time.Second)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case res = <-resc:
|
||||
return res
|
||||
case <-timer.C:
|
||||
// Too slow. Who knows what this system is.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// There's no hint from the system info that we're running on
|
||||
// GCE, so use the first probe's result as truth, whether it's
|
||||
// true or false. The goal here is to optimize for speed for
|
||||
// users who are NOT running on GCE. We can't assume that
|
||||
// either a DNS lookup or an HTTP request to a blackholed IP
|
||||
// address is fast. Worst case this should return when the
|
||||
// metaClient's Transport.ResponseHeaderTimeout or
|
||||
// Transport.Dial.Timeout fires (in two seconds).
|
||||
return <-resc
|
||||
}
|
||||
|
||||
// systemInfoSuggestsGCE reports whether the local system (without
|
||||
// doing network requests) suggests that we're running on GCE. If this
|
||||
// returns true, testOnGCE tries a bit harder to reach its metadata
|
||||
// server.
|
||||
func systemInfoSuggestsGCE() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
// We don't have any non-Linux clues available, at least yet.
|
||||
return false
|
||||
}
|
||||
slurp, _ := ioutil.ReadFile("/sys/class/dmi/id/product_name")
|
||||
name := strings.TrimSpace(string(slurp))
|
||||
return name == "Google" || name == "Google Compute Engine"
|
||||
}
|
||||
|
||||
// Subscribe calls Client.Subscribe on a client designed for subscribing (one with no
|
||||
// ResponseHeaderTimeout).
|
||||
func Subscribe(suffix string, fn func(v string, ok bool) error) error {
|
||||
return subscribeClient.Subscribe(suffix, fn)
|
||||
}
|
||||
|
||||
// Get calls Client.Get on the default client.
|
||||
func Get(suffix string) (string, error) { return defaultClient.Get(suffix) }
|
||||
|
||||
// ProjectID returns the current instance's project ID string.
|
||||
func ProjectID() (string, error) { return defaultClient.ProjectID() }
|
||||
|
||||
// NumericProjectID returns the current instance's numeric project ID.
|
||||
func NumericProjectID() (string, error) { return defaultClient.NumericProjectID() }
|
||||
|
||||
// InternalIP returns the instance's primary internal IP address.
|
||||
func InternalIP() (string, error) { return defaultClient.InternalIP() }
|
||||
|
||||
// ExternalIP returns the instance's primary external (public) IP address.
|
||||
func ExternalIP() (string, error) { return defaultClient.ExternalIP() }
|
||||
|
||||
// Email calls Client.Email on the default client.
|
||||
func Email(serviceAccount string) (string, error) { return defaultClient.Email(serviceAccount) }
|
||||
|
||||
// Hostname returns the instance's hostname. This will be of the form
|
||||
// "<instanceID>.c.<projID>.internal".
|
||||
func Hostname() (string, error) { return defaultClient.Hostname() }
|
||||
|
||||
// InstanceTags returns the list of user-defined instance tags,
|
||||
// assigned when initially creating a GCE instance.
|
||||
func InstanceTags() ([]string, error) { return defaultClient.InstanceTags() }
|
||||
|
||||
// InstanceID returns the current VM's numeric instance ID.
|
||||
func InstanceID() (string, error) { return defaultClient.InstanceID() }
|
||||
|
||||
// InstanceName returns the current VM's instance ID string.
|
||||
func InstanceName() (string, error) { return defaultClient.InstanceName() }
|
||||
|
||||
// Zone returns the current VM's zone, such as "us-central1-b".
|
||||
func Zone() (string, error) { return defaultClient.Zone() }
|
||||
|
||||
// InstanceAttributes calls Client.InstanceAttributes on the default client.
|
||||
func InstanceAttributes() ([]string, error) { return defaultClient.InstanceAttributes() }
|
||||
|
||||
// ProjectAttributes calls Client.ProjectAttributes on the default client.
|
||||
func ProjectAttributes() ([]string, error) { return defaultClient.ProjectAttributes() }
|
||||
|
||||
// InstanceAttributeValue calls Client.InstanceAttributeValue on the default client.
|
||||
func InstanceAttributeValue(attr string) (string, error) {
|
||||
return defaultClient.InstanceAttributeValue(attr)
|
||||
}
|
||||
|
||||
// ProjectAttributeValue calls Client.ProjectAttributeValue on the default client.
|
||||
func ProjectAttributeValue(attr string) (string, error) {
|
||||
return defaultClient.ProjectAttributeValue(attr)
|
||||
}
|
||||
|
||||
// Scopes calls Client.Scopes on the default client.
|
||||
func Scopes(serviceAccount string) ([]string, error) { return defaultClient.Scopes(serviceAccount) }
|
||||
|
||||
func strsContains(ss []string, s string) bool {
|
||||
for _, v := range ss {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// A Client provides metadata.
|
||||
type Client struct {
|
||||
hc *http.Client
|
||||
}
|
||||
|
||||
// NewClient returns a Client that can be used to fetch metadata. All HTTP requests
|
||||
// will use the given http.Client instead of the default client.
|
||||
func NewClient(c *http.Client) *Client {
|
||||
return &Client{hc: c}
|
||||
}
|
||||
|
||||
// getETag returns a value from the metadata service as well as the associated ETag.
|
||||
// This func is otherwise equivalent to Get.
|
||||
func (c *Client) getETag(suffix string) (value, etag string, err error) {
|
||||
// Using a fixed IP makes it very difficult to spoof the metadata service in
|
||||
// a container, which is an important use-case for local testing of cloud
|
||||
// deployments. To enable spoofing of the metadata service, the environment
|
||||
// variable GCE_METADATA_HOST is first inspected to decide where metadata
|
||||
// requests shall go.
|
||||
host := os.Getenv(metadataHostEnv)
|
||||
if host == "" {
|
||||
// Using 169.254.169.254 instead of "metadata" here because Go
|
||||
// binaries built with the "netgo" tag and without cgo won't
|
||||
// know the search suffix for "metadata" is
|
||||
// ".google.internal", and this IP address is documented as
|
||||
// being stable anyway.
|
||||
host = metadataIP
|
||||
}
|
||||
u := "http://" + host + "/computeMetadata/v1/" + suffix
|
||||
req, _ := http.NewRequest("GET", u, nil)
|
||||
req.Header.Set("Metadata-Flavor", "Google")
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
res, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode == http.StatusNotFound {
|
||||
return "", "", NotDefinedError(suffix)
|
||||
}
|
||||
all, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return "", "", &Error{Code: res.StatusCode, Message: string(all)}
|
||||
}
|
||||
return string(all), res.Header.Get("Etag"), nil
|
||||
}
|
||||
|
||||
// Get returns a value from the metadata service.
|
||||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||
//
|
||||
// If the GCE_METADATA_HOST environment variable is not defined, a default of
|
||||
// 169.254.169.254 will be used instead.
|
||||
//
|
||||
// If the requested metadata is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
func (c *Client) Get(suffix string) (string, error) {
|
||||
val, _, err := c.getETag(suffix)
|
||||
return val, err
|
||||
}
|
||||
|
||||
func (c *Client) getTrimmed(suffix string) (s string, err error) {
|
||||
s, err = c.Get(suffix)
|
||||
s = strings.TrimSpace(s)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) lines(suffix string) ([]string, error) {
|
||||
j, err := c.Get(suffix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := strings.Split(strings.TrimSpace(j), "\n")
|
||||
for i := range s {
|
||||
s[i] = strings.TrimSpace(s[i])
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ProjectID returns the current instance's project ID string.
|
||||
func (c *Client) ProjectID() (string, error) { return projID.get(c) }
|
||||
|
||||
// NumericProjectID returns the current instance's numeric project ID.
|
||||
func (c *Client) NumericProjectID() (string, error) { return projNum.get(c) }
|
||||
|
||||
// InstanceID returns the current VM's numeric instance ID.
|
||||
func (c *Client) InstanceID() (string, error) { return instID.get(c) }
|
||||
|
||||
// InternalIP returns the instance's primary internal IP address.
|
||||
func (c *Client) InternalIP() (string, error) {
|
||||
return c.getTrimmed("instance/network-interfaces/0/ip")
|
||||
}
|
||||
|
||||
// Email returns the email address associated with the service account.
|
||||
// The account may be empty or the string "default" to use the instance's
|
||||
// main account.
|
||||
func (c *Client) Email(serviceAccount string) (string, error) {
|
||||
if serviceAccount == "" {
|
||||
serviceAccount = "default"
|
||||
}
|
||||
return c.getTrimmed("instance/service-accounts/" + serviceAccount + "/email")
|
||||
}
|
||||
|
||||
// ExternalIP returns the instance's primary external (public) IP address.
|
||||
func (c *Client) ExternalIP() (string, error) {
|
||||
return c.getTrimmed("instance/network-interfaces/0/access-configs/0/external-ip")
|
||||
}
|
||||
|
||||
// Hostname returns the instance's hostname. This will be of the form
|
||||
// "<instanceID>.c.<projID>.internal".
|
||||
func (c *Client) Hostname() (string, error) {
|
||||
return c.getTrimmed("instance/hostname")
|
||||
}
|
||||
|
||||
// InstanceTags returns the list of user-defined instance tags,
|
||||
// assigned when initially creating a GCE instance.
|
||||
func (c *Client) InstanceTags() ([]string, error) {
|
||||
var s []string
|
||||
j, err := c.Get("instance/tags")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.NewDecoder(strings.NewReader(j)).Decode(&s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// InstanceName returns the current VM's instance ID string.
|
||||
func (c *Client) InstanceName() (string, error) {
|
||||
host, err := c.Hostname()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.Split(host, ".")[0], nil
|
||||
}
|
||||
|
||||
// Zone returns the current VM's zone, such as "us-central1-b".
|
||||
func (c *Client) Zone() (string, error) {
|
||||
zone, err := c.getTrimmed("instance/zone")
|
||||
// zone is of the form "projects/<projNum>/zones/<zoneName>".
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return zone[strings.LastIndex(zone, "/")+1:], nil
|
||||
}
|
||||
|
||||
// InstanceAttributes returns the list of user-defined attributes,
|
||||
// assigned when initially creating a GCE VM instance. The value of an
|
||||
// attribute can be obtained with InstanceAttributeValue.
|
||||
func (c *Client) InstanceAttributes() ([]string, error) { return c.lines("instance/attributes/") }
|
||||
|
||||
// ProjectAttributes returns the list of user-defined attributes
|
||||
// applying to the project as a whole, not just this VM. The value of
|
||||
// an attribute can be obtained with ProjectAttributeValue.
|
||||
func (c *Client) ProjectAttributes() ([]string, error) { return c.lines("project/attributes/") }
|
||||
|
||||
// InstanceAttributeValue returns the value of the provided VM
|
||||
// instance attribute.
|
||||
//
|
||||
// If the requested attribute is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// InstanceAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
func (c *Client) InstanceAttributeValue(attr string) (string, error) {
|
||||
return c.Get("instance/attributes/" + attr)
|
||||
}
|
||||
|
||||
// ProjectAttributeValue returns the value of the provided
|
||||
// project attribute.
|
||||
//
|
||||
// If the requested attribute is not defined, the returned error will
|
||||
// be of type NotDefinedError.
|
||||
//
|
||||
// ProjectAttributeValue may return ("", nil) if the attribute was
|
||||
// defined to be the empty string.
|
||||
func (c *Client) ProjectAttributeValue(attr string) (string, error) {
|
||||
return c.Get("project/attributes/" + attr)
|
||||
}
|
||||
|
||||
// Scopes returns the service account scopes for the given account.
|
||||
// The account may be empty or the string "default" to use the instance's
|
||||
// main account.
|
||||
func (c *Client) Scopes(serviceAccount string) ([]string, error) {
|
||||
if serviceAccount == "" {
|
||||
serviceAccount = "default"
|
||||
}
|
||||
return c.lines("instance/service-accounts/" + serviceAccount + "/scopes")
|
||||
}
|
||||
|
||||
// Subscribe subscribes to a value from the metadata service.
|
||||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||
// The suffix may contain query parameters.
|
||||
//
|
||||
// Subscribe calls fn with the latest metadata value indicated by the provided
|
||||
// suffix. If the metadata value is deleted, fn is called with the empty string
|
||||
// and ok false. Subscribe blocks until fn returns a non-nil error or the value
|
||||
// is deleted. Subscribe returns the error value returned from the last call to
|
||||
// fn, which may be nil when ok == false.
|
||||
func (c *Client) Subscribe(suffix string, fn func(v string, ok bool) error) error {
|
||||
const failedSubscribeSleep = time.Second * 5
|
||||
|
||||
// First check to see if the metadata value exists at all.
|
||||
val, lastETag, err := c.getETag(suffix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fn(val, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ok := true
|
||||
if strings.ContainsRune(suffix, '?') {
|
||||
suffix += "&wait_for_change=true&last_etag="
|
||||
} else {
|
||||
suffix += "?wait_for_change=true&last_etag="
|
||||
}
|
||||
for {
|
||||
val, etag, err := c.getETag(suffix + url.QueryEscape(lastETag))
|
||||
if err != nil {
|
||||
if _, deleted := err.(NotDefinedError); !deleted {
|
||||
time.Sleep(failedSubscribeSleep)
|
||||
continue // Retry on other errors.
|
||||
}
|
||||
ok = false
|
||||
}
|
||||
lastETag = etag
|
||||
|
||||
if err := fn(val, ok); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error contains an error response from the server.
|
||||
type Error struct {
|
||||
// Code is the HTTP response status code.
|
||||
Code int
|
||||
// Message is the server response message.
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return fmt.Sprintf("compute: Received %d `%s`", e.Code, e.Message)
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright 2014 Google LLC
|
||||
//
|
||||
// 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 cloud is the root of the packages used to access Google Cloud
|
||||
Services. See https://godoc.org/cloud.google.com/go for a full list
|
||||
of sub-packages.
|
||||
|
||||
|
||||
Client Options
|
||||
|
||||
All clients in sub-packages are configurable via client options. These options are
|
||||
described here: https://godoc.org/google.golang.org/api/option.
|
||||
|
||||
|
||||
Authentication and Authorization
|
||||
|
||||
All the clients in sub-packages support authentication via Google Application Default
|
||||
Credentials (see https://cloud.google.com/docs/authentication/production), or
|
||||
by providing a JSON key file for a Service Account. See the authentication examples
|
||||
in this package for details.
|
||||
|
||||
|
||||
Timeouts and Cancellation
|
||||
|
||||
By default, all requests in sub-packages will run indefinitely, retrying on transient
|
||||
errors when correctness allows. To set timeouts or arrange for cancellation, use
|
||||
contexts. See the examples for details.
|
||||
|
||||
Do not attempt to control the initial connection (dialing) of a service by setting a
|
||||
timeout on the context passed to NewClient. Dialing is non-blocking, so timeouts
|
||||
would be ineffective and would only interfere with credential refreshing, which uses
|
||||
the same context.
|
||||
|
||||
|
||||
Connection Pooling
|
||||
|
||||
Connection pooling differs in clients based on their transport. Cloud
|
||||
clients either rely on HTTP or gRPC transports to communicate
|
||||
with Google Cloud.
|
||||
|
||||
Cloud clients that use HTTP (bigquery, compute, storage, and translate) rely on the
|
||||
underlying HTTP transport to cache connections for later re-use. These are cached to
|
||||
the default http.MaxIdleConns and http.MaxIdleConnsPerHost settings in
|
||||
http.DefaultTransport.
|
||||
|
||||
For gRPC clients (all others in this repo), connection pooling is configurable. Users
|
||||
of cloud client libraries may specify option.WithGRPCConnectionPool(n) as a client
|
||||
option to NewClient calls. This configures the underlying gRPC connections to be
|
||||
pooled and addressed in a round robin fashion.
|
||||
|
||||
|
||||
Using the Libraries with Docker
|
||||
|
||||
Minimal docker images like Alpine lack CA certificates. This causes RPCs to appear to
|
||||
hang, because gRPC retries indefinitely. See https://github.com/googleapis/google-cloud-go/issues/928
|
||||
for more information.
|
||||
|
||||
|
||||
Debugging
|
||||
|
||||
To see gRPC logs, set the environment variable GRPC_GO_LOG_SEVERITY_LEVEL. See
|
||||
https://godoc.org/google.golang.org/grpc/grpclog for more information.
|
||||
|
||||
For HTTP logging, set the GODEBUG environment variable to "http2debug=1" or "http2debug=2".
|
||||
|
||||
|
||||
Client Stability
|
||||
|
||||
Clients in this repository are considered alpha or beta unless otherwise
|
||||
marked as stable in the README.md. Semver is not used to communicate stability
|
||||
of clients.
|
||||
|
||||
Alpha and beta clients may change or go away without notice.
|
||||
|
||||
Clients marked stable will maintain compatibility with future versions for as
|
||||
long as we can reasonably sustain. Incompatible changes might be made in some
|
||||
situations, including:
|
||||
|
||||
- Security bugs may prompt backwards-incompatible changes.
|
||||
|
||||
- Situations in which components are no longer feasible to maintain without
|
||||
making breaking changes, including removal.
|
||||
|
||||
- Parts of the client surface may be outright unstable and subject to change.
|
||||
These parts of the surface will be labeled with the note, "It is EXPERIMENTAL
|
||||
and subject to change or removal without notice."
|
||||
*/
|
||||
package cloud // import "cloud.google.com/go"
|
|
@ -0,0 +1,27 @@
|
|||
module cloud.google.com/go
|
||||
|
||||
go 1.9
|
||||
|
||||
require (
|
||||
cloud.google.com/go/bigquery v1.0.1
|
||||
cloud.google.com/go/datastore v1.0.0
|
||||
cloud.google.com/go/pubsub v1.0.1
|
||||
github.com/golang/mock v1.3.1
|
||||
github.com/golang/protobuf v1.3.2
|
||||
github.com/google/go-cmp v0.3.0
|
||||
github.com/google/martian v2.1.0+incompatible
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f
|
||||
github.com/googleapis/gax-go/v2 v2.0.5
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024
|
||||
go.opencensus.io v0.22.0
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
golang.org/x/text v0.3.2
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff
|
||||
google.golang.org/api v0.9.0
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51
|
||||
google.golang.org/grpc v1.21.1
|
||||
honnef.co/go/tools v0.0.1-2019.2.3
|
||||
)
|
|
@ -0,0 +1,193 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go/bigquery v1.0.1 h1:hL+ycaJpVE9M7nLoiXb/Pn10ENE2u+oddxbD8uu0ZVU=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0 h1:Kt+gOPPp2LEPWp8CSfxhsM8ik9CcyE/gYu+0r+RnZvM=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/pubsub v1.0.1 h1:W9tAK3E57P75u0XLLR82LZyw8VpAnhmyTOxW9qzmyj8=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57 h1:eqyIo2HjKhKe/mJzTG8n4VqvLXIOEG+SLdDqX7xGtkY=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f h1:Jnx61latede7zDD3DiiP4gmNz33uK0U5HDUaF0a/HVQ=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4 h1:hU4mGcQI4DaAYW+IbTun+2qEZVFxK0ySjQLTbS0VQKc=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522 h1:OeRHuibLsmZkFj773W4LcfAGsSxJgfPONhr8cmO+eLA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979 h1:Agxu5KLo8o7Bb634SVDnhIfpTvxmzUwhbYAzBvXt6h4=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f h1:hX65Cu3JDlGH3uEdK7I99Ii+9kjD6mvnnpfLdEAH0x4=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac h1:8R1esu+8QioDxo4E4mX6bFztO+dMTM49DNAaWfO5OeY=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138 h1:H3uGjxCR/6Ds0Mjgyp7LMK81+LvmbvWWEnJhzk1Pi9E=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c h1:97SnQk1GYRXJgvwZ8fadnxDOWfKvkNQHH3CtZntPSrM=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0 h1:Dh6fw+p6FyRl5x/FvNswO1ji0lIGzm3KP8Y9VkS9PTE=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff h1:On1qIo75ByTwFJ4/W2bIqHcwJ9XAqtSWUs8GwRrIhtc=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0 h1:VGGbLNyPF7dvYHhcUGYBBGCRDDK0RRJAI6KCvo0CL+E=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0 h1:jbyannxz0XFD3zdjgrSUsaJbgpH4eTrkdhRChkHPfO8=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64 h1:iKtrH9Y8mcbADOP0YFaEMth7OfuHY9xHOwNj4znpM1A=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51 h1:Ex1mq5jaJof+kRnYi3SlYJ8KKa9Ao3NHyIT5XJ1gF6U=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a h1:/8zB6iBfHCl1qAnEAWwGPNrUvapuy6CPla1VM0k8hQw=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a h1:LJwr7TCTghdatWv40WobzlKXc9c4s8oGa7QKJUtHhWA=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "iam",
|
||||
"name_pretty": "Cloud Identify and Access Management API",
|
||||
"product_documentation": "https://cloud.google.com/iam",
|
||||
"client_documentation": "https://godoc.org/cloud.google.com/go/iam",
|
||||
"release_level": "ga",
|
||||
"language": "go",
|
||||
"repo": "googleapis/google-cloud-go",
|
||||
"distribution_name": "cloud.google.com/go/iam",
|
||||
"api_id": "iam.googleapis.com",
|
||||
"requires_billing": true
|
||||
}
|
|
@ -0,0 +1,315 @@
|
|||
// Copyright 2016 Google LLC
|
||||
//
|
||||
// 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 iam supports the resource-specific operations of Google Cloud
|
||||
// IAM (Identity and Access Management) for the Google Cloud Libraries.
|
||||
// See https://cloud.google.com/iam for more about IAM.
|
||||
//
|
||||
// Users of the Google Cloud Libraries will typically not use this package
|
||||
// directly. Instead they will begin with some resource that supports IAM, like
|
||||
// a pubsub topic, and call its IAM method to get a Handle for that resource.
|
||||
package iam
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
gax "github.com/googleapis/gax-go/v2"
|
||||
pb "google.golang.org/genproto/googleapis/iam/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// client abstracts the IAMPolicy API to allow multiple implementations.
|
||||
type client interface {
|
||||
Get(ctx context.Context, resource string) (*pb.Policy, error)
|
||||
Set(ctx context.Context, resource string, p *pb.Policy) error
|
||||
Test(ctx context.Context, resource string, perms []string) ([]string, error)
|
||||
}
|
||||
|
||||
// grpcClient implements client for the standard gRPC-based IAMPolicy service.
|
||||
type grpcClient struct {
|
||||
c pb.IAMPolicyClient
|
||||
}
|
||||
|
||||
var withRetry = gax.WithRetry(func() gax.Retryer {
|
||||
return gax.OnCodes([]codes.Code{
|
||||
codes.DeadlineExceeded,
|
||||
codes.Unavailable,
|
||||
}, gax.Backoff{
|
||||
Initial: 100 * time.Millisecond,
|
||||
Max: 60 * time.Second,
|
||||
Multiplier: 1.3,
|
||||
})
|
||||
})
|
||||
|
||||
func (g *grpcClient) Get(ctx context.Context, resource string) (*pb.Policy, error) {
|
||||
var proto *pb.Policy
|
||||
md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "resource", resource))
|
||||
ctx = insertMetadata(ctx, md)
|
||||
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error {
|
||||
var err error
|
||||
proto, err = g.c.GetIamPolicy(ctx, &pb.GetIamPolicyRequest{Resource: resource})
|
||||
return err
|
||||
}, withRetry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return proto, nil
|
||||
}
|
||||
|
||||
func (g *grpcClient) Set(ctx context.Context, resource string, p *pb.Policy) error {
|
||||
md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "resource", resource))
|
||||
ctx = insertMetadata(ctx, md)
|
||||
|
||||
return gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error {
|
||||
_, err := g.c.SetIamPolicy(ctx, &pb.SetIamPolicyRequest{
|
||||
Resource: resource,
|
||||
Policy: p,
|
||||
})
|
||||
return err
|
||||
}, withRetry)
|
||||
}
|
||||
|
||||
func (g *grpcClient) Test(ctx context.Context, resource string, perms []string) ([]string, error) {
|
||||
var res *pb.TestIamPermissionsResponse
|
||||
md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "resource", resource))
|
||||
ctx = insertMetadata(ctx, md)
|
||||
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error {
|
||||
var err error
|
||||
res, err = g.c.TestIamPermissions(ctx, &pb.TestIamPermissionsRequest{
|
||||
Resource: resource,
|
||||
Permissions: perms,
|
||||
})
|
||||
return err
|
||||
}, withRetry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.Permissions, nil
|
||||
}
|
||||
|
||||
// A Handle provides IAM operations for a resource.
|
||||
type Handle struct {
|
||||
c client
|
||||
resource string
|
||||
}
|
||||
|
||||
// InternalNewHandle is for use by the Google Cloud Libraries only.
|
||||
//
|
||||
// InternalNewHandle returns a Handle for resource.
|
||||
// The conn parameter refers to a server that must support the IAMPolicy service.
|
||||
func InternalNewHandle(conn *grpc.ClientConn, resource string) *Handle {
|
||||
return InternalNewHandleGRPCClient(pb.NewIAMPolicyClient(conn), resource)
|
||||
}
|
||||
|
||||
// InternalNewHandleGRPCClient is for use by the Google Cloud Libraries only.
|
||||
//
|
||||
// InternalNewHandleClient returns a Handle for resource using the given
|
||||
// grpc service that implements IAM as a mixin
|
||||
func InternalNewHandleGRPCClient(c pb.IAMPolicyClient, resource string) *Handle {
|
||||
return InternalNewHandleClient(&grpcClient{c: c}, resource)
|
||||
}
|
||||
|
||||
// InternalNewHandleClient is for use by the Google Cloud Libraries only.
|
||||
//
|
||||
// InternalNewHandleClient returns a Handle for resource using the given
|
||||
// client implementation.
|
||||
func InternalNewHandleClient(c client, resource string) *Handle {
|
||||
return &Handle{
|
||||
c: c,
|
||||
resource: resource,
|
||||
}
|
||||
}
|
||||
|
||||
// Policy retrieves the IAM policy for the resource.
|
||||
func (h *Handle) Policy(ctx context.Context) (*Policy, error) {
|
||||
proto, err := h.c.Get(ctx, h.resource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Policy{InternalProto: proto}, nil
|
||||
}
|
||||
|
||||
// SetPolicy replaces the resource's current policy with the supplied Policy.
|
||||
//
|
||||
// If policy was created from a prior call to Get, then the modification will
|
||||
// only succeed if the policy has not changed since the Get.
|
||||
func (h *Handle) SetPolicy(ctx context.Context, policy *Policy) error {
|
||||
return h.c.Set(ctx, h.resource, policy.InternalProto)
|
||||
}
|
||||
|
||||
// TestPermissions returns the subset of permissions that the caller has on the resource.
|
||||
func (h *Handle) TestPermissions(ctx context.Context, permissions []string) ([]string, error) {
|
||||
return h.c.Test(ctx, h.resource, permissions)
|
||||
}
|
||||
|
||||
// A RoleName is a name representing a collection of permissions.
|
||||
type RoleName string
|
||||
|
||||
// Common role names.
|
||||
const (
|
||||
Owner RoleName = "roles/owner"
|
||||
Editor RoleName = "roles/editor"
|
||||
Viewer RoleName = "roles/viewer"
|
||||
)
|
||||
|
||||
const (
|
||||
// AllUsers is a special member that denotes all users, even unauthenticated ones.
|
||||
AllUsers = "allUsers"
|
||||
|
||||
// AllAuthenticatedUsers is a special member that denotes all authenticated users.
|
||||
AllAuthenticatedUsers = "allAuthenticatedUsers"
|
||||
)
|
||||
|
||||
// A Policy is a list of Bindings representing roles
|
||||
// granted to members.
|
||||
//
|
||||
// The zero Policy is a valid policy with no bindings.
|
||||
type Policy struct {
|
||||
// TODO(jba): when type aliases are available, put Policy into an internal package
|
||||
// and provide an exported alias here.
|
||||
|
||||
// This field is exported for use by the Google Cloud Libraries only.
|
||||
// It may become unexported in a future release.
|
||||
InternalProto *pb.Policy
|
||||
}
|
||||
|
||||
// Members returns the list of members with the supplied role.
|
||||
// The return value should not be modified. Use Add and Remove
|
||||
// to modify the members of a role.
|
||||
func (p *Policy) Members(r RoleName) []string {
|
||||
b := p.binding(r)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Members
|
||||
}
|
||||
|
||||
// HasRole reports whether member has role r.
|
||||
func (p *Policy) HasRole(member string, r RoleName) bool {
|
||||
return memberIndex(member, p.binding(r)) >= 0
|
||||
}
|
||||
|
||||
// Add adds member member to role r if it is not already present.
|
||||
// A new binding is created if there is no binding for the role.
|
||||
func (p *Policy) Add(member string, r RoleName) {
|
||||
b := p.binding(r)
|
||||
if b == nil {
|
||||
if p.InternalProto == nil {
|
||||
p.InternalProto = &pb.Policy{}
|
||||
}
|
||||
p.InternalProto.Bindings = append(p.InternalProto.Bindings, &pb.Binding{
|
||||
Role: string(r),
|
||||
Members: []string{member},
|
||||
})
|
||||
return
|
||||
}
|
||||
if memberIndex(member, b) < 0 {
|
||||
b.Members = append(b.Members, member)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removes member from role r if it is present.
|
||||
func (p *Policy) Remove(member string, r RoleName) {
|
||||
bi := p.bindingIndex(r)
|
||||
if bi < 0 {
|
||||
return
|
||||
}
|
||||
bindings := p.InternalProto.Bindings
|
||||
b := bindings[bi]
|
||||
mi := memberIndex(member, b)
|
||||
if mi < 0 {
|
||||
return
|
||||
}
|
||||
// Order doesn't matter for bindings or members, so to remove, move the last item
|
||||
// into the removed spot and shrink the slice.
|
||||
if len(b.Members) == 1 {
|
||||
// Remove binding.
|
||||
last := len(bindings) - 1
|
||||
bindings[bi] = bindings[last]
|
||||
bindings[last] = nil
|
||||
p.InternalProto.Bindings = bindings[:last]
|
||||
return
|
||||
}
|
||||
// Remove member.
|
||||
// TODO(jba): worry about multiple copies of m?
|
||||
last := len(b.Members) - 1
|
||||
b.Members[mi] = b.Members[last]
|
||||
b.Members[last] = ""
|
||||
b.Members = b.Members[:last]
|
||||
}
|
||||
|
||||
// Roles returns the names of all the roles that appear in the Policy.
|
||||
func (p *Policy) Roles() []RoleName {
|
||||
if p.InternalProto == nil {
|
||||
return nil
|
||||
}
|
||||
var rns []RoleName
|
||||
for _, b := range p.InternalProto.Bindings {
|
||||
rns = append(rns, RoleName(b.Role))
|
||||
}
|
||||
return rns
|
||||
}
|
||||
|
||||
// binding returns the Binding for the suppied role, or nil if there isn't one.
|
||||
func (p *Policy) binding(r RoleName) *pb.Binding {
|
||||
i := p.bindingIndex(r)
|
||||
if i < 0 {
|
||||
return nil
|
||||
}
|
||||
return p.InternalProto.Bindings[i]
|
||||
}
|
||||
|
||||
func (p *Policy) bindingIndex(r RoleName) int {
|
||||
if p.InternalProto == nil {
|
||||
return -1
|
||||
}
|
||||
for i, b := range p.InternalProto.Bindings {
|
||||
if b.Role == string(r) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// memberIndex returns the index of m in b's Members, or -1 if not found.
|
||||
func memberIndex(m string, b *pb.Binding) int {
|
||||
if b == nil {
|
||||
return -1
|
||||
}
|
||||
for i, mm := range b.Members {
|
||||
if mm == m {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// insertMetadata inserts metadata into the given context
|
||||
func insertMetadata(ctx context.Context, mds ...metadata.MD) context.Context {
|
||||
out, _ := metadata.FromOutgoingContext(ctx)
|
||||
out = out.Copy()
|
||||
for _, md := range mds {
|
||||
for k, v := range md {
|
||||
out[k] = append(out[k], v...)
|
||||
}
|
||||
}
|
||||
return metadata.NewOutgoingContext(ctx, out)
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2017 Google LLC
|
||||
//
|
||||
// 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 internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/api/googleapi"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// Annotate prepends msg to the error message in err, attempting
|
||||
// to preserve other information in err, like an error code.
|
||||
//
|
||||
// Annotate panics if err is nil.
|
||||
//
|
||||
// Annotate knows about these error types:
|
||||
// - "google.golang.org/grpc/status".Status
|
||||
// - "google.golang.org/api/googleapi".Error
|
||||
// If the error is not one of these types, Annotate behaves
|
||||
// like
|
||||
// fmt.Errorf("%s: %v", msg, err)
|
||||
func Annotate(err error, msg string) error {
|
||||
if err == nil {
|
||||
panic("Annotate called with nil")
|
||||
}
|
||||
if s, ok := status.FromError(err); ok {
|
||||
p := s.Proto()
|
||||
p.Message = msg + ": " + p.Message
|
||||
return status.ErrorProto(p)
|
||||
}
|
||||
if g, ok := err.(*googleapi.Error); ok {
|
||||
g.Message = msg + ": " + g.Message
|
||||
return g
|
||||
}
|
||||
return fmt.Errorf("%s: %v", msg, err)
|
||||
}
|
||||
|
||||
// Annotatef uses format and args to format a string, then calls Annotate.
|
||||
func Annotatef(err error, format string, args ...interface{}) error {
|
||||
return Annotate(err, fmt.Sprintf(format, args...))
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
// Copyright 2016 Google LLC
|
||||
//
|
||||
// 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 optional provides versions of primitive types that can
|
||||
// be nil. These are useful in methods that update some of an API object's
|
||||
// fields.
|
||||
package optional
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
// Bool is either a bool or nil.
|
||||
Bool interface{}
|
||||
|
||||
// String is either a string or nil.
|
||||
String interface{}
|
||||
|
||||
// Int is either an int or nil.
|
||||
Int interface{}
|
||||
|
||||
// Uint is either a uint or nil.
|
||||
Uint interface{}
|
||||
|
||||
// Float64 is either a float64 or nil.
|
||||
Float64 interface{}
|
||||
|
||||
// Duration is either a time.Duration or nil.
|
||||
Duration interface{}
|
||||
)
|
||||
|
||||
// ToBool returns its argument as a bool.
|
||||
// It panics if its argument is nil or not a bool.
|
||||
func ToBool(v Bool) bool {
|
||||
x, ok := v.(bool)
|
||||
if !ok {
|
||||
doPanic("Bool", v)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// ToString returns its argument as a string.
|
||||
// It panics if its argument is nil or not a string.
|
||||
func ToString(v String) string {
|
||||
x, ok := v.(string)
|
||||
if !ok {
|
||||
doPanic("String", v)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// ToInt returns its argument as an int.
|
||||
// It panics if its argument is nil or not an int.
|
||||
func ToInt(v Int) int {
|
||||
x, ok := v.(int)
|
||||
if !ok {
|
||||
doPanic("Int", v)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// ToUint returns its argument as a uint.
|
||||
// It panics if its argument is nil or not a uint.
|
||||
func ToUint(v Uint) uint {
|
||||
x, ok := v.(uint)
|
||||
if !ok {
|
||||
doPanic("Uint", v)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// ToFloat64 returns its argument as a float64.
|
||||
// It panics if its argument is nil or not a float64.
|
||||
func ToFloat64(v Float64) float64 {
|
||||
x, ok := v.(float64)
|
||||
if !ok {
|
||||
doPanic("Float64", v)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// ToDuration returns its argument as a time.Duration.
|
||||
// It panics if its argument is nil or not a time.Duration.
|
||||
func ToDuration(v Duration) time.Duration {
|
||||
x, ok := v.(time.Duration)
|
||||
if !ok {
|
||||
doPanic("Duration", v)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func doPanic(capType string, v interface{}) {
|
||||
panic(fmt.Sprintf("optional.%s value should be %s, got %T", capType, strings.ToLower(capType), v))
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2016 Google LLC
|
||||
//
|
||||
// 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 internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
gax "github.com/googleapis/gax-go/v2"
|
||||
)
|
||||
|
||||
// Retry calls the supplied function f repeatedly according to the provided
|
||||
// backoff parameters. It returns when one of the following occurs:
|
||||
// When f's first return value is true, Retry immediately returns with f's second
|
||||
// return value.
|
||||
// When the provided context is done, Retry returns with an error that
|
||||
// includes both ctx.Error() and the last error returned by f.
|
||||
func Retry(ctx context.Context, bo gax.Backoff, f func() (stop bool, err error)) error {
|
||||
return retry(ctx, bo, f, gax.Sleep)
|
||||
}
|
||||
|
||||
func retry(ctx context.Context, bo gax.Backoff, f func() (stop bool, err error),
|
||||
sleep func(context.Context, time.Duration) error) error {
|
||||
var lastErr error
|
||||
for {
|
||||
stop, err := f()
|
||||
if stop {
|
||||
return err
|
||||
}
|
||||
// Remember the last "real" error from f.
|
||||
if err != nil && err != context.Canceled && err != context.DeadlineExceeded {
|
||||
lastErr = err
|
||||
}
|
||||
p := bo.Pause()
|
||||
if cerr := sleep(ctx, p); cerr != nil {
|
||||
if lastErr != nil {
|
||||
return Annotatef(lastErr, "retry failed with %v; last error", cerr)
|
||||
}
|
||||
return cerr
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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 trace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.opencensus.io/trace"
|
||||
"google.golang.org/api/googleapi"
|
||||
"google.golang.org/genproto/googleapis/rpc/code"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// StartSpan adds a span to the trace with the given name.
|
||||
func StartSpan(ctx context.Context, name string) context.Context {
|
||||
ctx, _ = trace.StartSpan(ctx, name)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// EndSpan ends a span with the given error.
|
||||
func EndSpan(ctx context.Context, err error) {
|
||||
span := trace.FromContext(ctx)
|
||||
if err != nil {
|
||||
span.SetStatus(toStatus(err))
|
||||
}
|
||||
span.End()
|
||||
}
|
||||
|
||||
// toStatus interrogates an error and converts it to an appropriate
|
||||
// OpenCensus status.
|
||||
func toStatus(err error) trace.Status {
|
||||
if err2, ok := err.(*googleapi.Error); ok {
|
||||
return trace.Status{Code: httpStatusCodeToOCCode(err2.Code), Message: err2.Message}
|
||||
} else if s, ok := status.FromError(err); ok {
|
||||
return trace.Status{Code: int32(s.Code()), Message: s.Message()}
|
||||
} else {
|
||||
return trace.Status{Code: int32(code.Code_UNKNOWN), Message: err.Error()}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(deklerk): switch to using OpenCensus function when it becomes available.
|
||||
// Reference: https://github.com/googleapis/googleapis/blob/26b634d2724ac5dd30ae0b0cbfb01f07f2e4050e/google/rpc/code.proto
|
||||
func httpStatusCodeToOCCode(httpStatusCode int) int32 {
|
||||
switch httpStatusCode {
|
||||
case 200:
|
||||
return int32(code.Code_OK)
|
||||
case 499:
|
||||
return int32(code.Code_CANCELLED)
|
||||
case 500:
|
||||
return int32(code.Code_UNKNOWN) // Could also be Code_INTERNAL, Code_DATA_LOSS
|
||||
case 400:
|
||||
return int32(code.Code_INVALID_ARGUMENT) // Could also be Code_OUT_OF_RANGE
|
||||
case 504:
|
||||
return int32(code.Code_DEADLINE_EXCEEDED)
|
||||
case 404:
|
||||
return int32(code.Code_NOT_FOUND)
|
||||
case 409:
|
||||
return int32(code.Code_ALREADY_EXISTS) // Could also be Code_ABORTED
|
||||
case 403:
|
||||
return int32(code.Code_PERMISSION_DENIED)
|
||||
case 401:
|
||||
return int32(code.Code_UNAUTHENTICATED)
|
||||
case 429:
|
||||
return int32(code.Code_RESOURCE_EXHAUSTED)
|
||||
case 501:
|
||||
return int32(code.Code_UNIMPLEMENTED)
|
||||
case 503:
|
||||
return int32(code.Code_UNAVAILABLE)
|
||||
default:
|
||||
return int32(code.Code_UNKNOWN)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: (odeke-em): perhaps just pass around spans due to the cost
|
||||
// incurred from using trace.FromContext(ctx) yet we could avoid
|
||||
// throwing away the work done by ctx, span := trace.StartSpan.
|
||||
func TracePrintf(ctx context.Context, attrMap map[string]interface{}, format string, args ...interface{}) {
|
||||
var attrs []trace.Attribute
|
||||
for k, v := range attrMap {
|
||||
var a trace.Attribute
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
a = trace.StringAttribute(k, v)
|
||||
case bool:
|
||||
a = trace.BoolAttribute(k, v)
|
||||
case int:
|
||||
a = trace.Int64Attribute(k, int64(v))
|
||||
case int64:
|
||||
a = trace.Int64Attribute(k, v)
|
||||
default:
|
||||
a = trace.StringAttribute(k, fmt.Sprintf("%#v", v))
|
||||
}
|
||||
attrs = append(attrs, a)
|
||||
}
|
||||
trace.FromContext(ctx).Annotatef(attrs, format, args...)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/bash
|
||||
# Copyright 2019 Google LLC
|
||||
#
|
||||
# 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.
|
||||
|
||||
today=$(date +%Y%m%d)
|
||||
|
||||
sed -i -r -e 's/const Repo = "([0-9]{8})"/const Repo = "'$today'"/' $GOFILE
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
// Copyright 2016 Google LLC
|
||||
//
|
||||
// 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.
|
||||
|
||||
//go:generate ./update_version.sh
|
||||
|
||||
// Package version contains version information for Google Cloud Client
|
||||
// Libraries for Go, as reported in request headers.
|
||||
package version
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Repo is the current version of the client libraries in this
|
||||
// repo. It should be a date in YYYYMMDD format.
|
||||
const Repo = "20190802"
|
||||
|
||||
// Go returns the Go runtime version. The returned string
|
||||
// has no whitespace.
|
||||
func Go() string {
|
||||
return goVersion
|
||||
}
|
||||
|
||||
var goVersion = goVer(runtime.Version())
|
||||
|
||||
const develPrefix = "devel +"
|
||||
|
||||
func goVer(s string) string {
|
||||
if strings.HasPrefix(s, develPrefix) {
|
||||
s = s[len(develPrefix):]
|
||||
if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
|
||||
s = s[:p]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
if strings.HasPrefix(s, "go1") {
|
||||
s = s[2:]
|
||||
var prerelease string
|
||||
if p := strings.IndexFunc(s, notSemverRune); p >= 0 {
|
||||
s, prerelease = s[:p], s[p:]
|
||||
}
|
||||
if strings.HasSuffix(s, ".") {
|
||||
s += "0"
|
||||
} else if strings.Count(s, ".") < 2 {
|
||||
s += ".0"
|
||||
}
|
||||
if prerelease != "" {
|
||||
s += "-" + prerelease
|
||||
}
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func notSemverRune(r rune) bool {
|
||||
return !strings.ContainsRune("0123456789.", r)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
(delete this for feature requests)
|
||||
|
||||
## Client
|
||||
|
||||
e.g. PubSub
|
||||
|
||||
## Describe Your Environment
|
||||
|
||||
e.g. Alpine Docker on GKE
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
e.g. Messages arrive really fast.
|
||||
|
||||
## Actual Behavior
|
||||
|
||||
e.g. Messages arrive really slowly.
|
|
@ -0,0 +1,163 @@
|
|||
#!/bin/bash
|
||||
# Copyright 2019 Google LLC
|
||||
#
|
||||
# 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.
|
||||
|
||||
# This script generates all GAPIC clients in this repo.
|
||||
# One-time setup:
|
||||
# cd path/to/googleapis # https://github.com/googleapis/googleapis
|
||||
# virtualenv env
|
||||
# . env/bin/activate
|
||||
# pip install googleapis-artman
|
||||
# deactivate
|
||||
#
|
||||
# Regenerate:
|
||||
# cd path/to/googleapis
|
||||
# . env/bin/activate
|
||||
# $GOPATH/src/cloud.google.com/go/regen-gapic.sh
|
||||
# deactivate
|
||||
#
|
||||
# Being in googleapis directory is important;
|
||||
# that's where we find YAML files and where artman puts the "artman-genfiles" directory.
|
||||
#
|
||||
# NOTE: This script does not generate the "raw" gRPC client found in google.golang.org/genproto.
|
||||
# To do that, use the regen.sh script in the genproto repo instead.
|
||||
|
||||
set -ex
|
||||
|
||||
APIS=(
|
||||
google/api/expr/artman_cel.yaml
|
||||
google/iam/artman_iam_admin.yaml
|
||||
google/cloud/asset/artman_cloudasset_v1beta1.yaml
|
||||
google/cloud/asset/artman_cloudasset_v1p2beta1.yaml
|
||||
google/cloud/asset/artman_cloudasset_v1.yaml
|
||||
google/iam/credentials/artman_iamcredentials_v1.yaml
|
||||
google/cloud/automl/artman_automl_v1beta1.yaml
|
||||
google/cloud/bigquery/datatransfer/artman_bigquerydatatransfer.yaml
|
||||
google/cloud/bigquery/storage/artman_bigquerystorage_v1beta1.yaml
|
||||
google/cloud/dataproc/artman_dataproc_v1.yaml
|
||||
google/cloud/dataproc/artman_dataproc_v1beta2.yaml
|
||||
google/cloud/dialogflow/artman_dialogflow_v2.yaml
|
||||
google/cloud/iot/artman_cloudiot.yaml
|
||||
google/cloud/irm/artman_irm_v1alpha2.yaml
|
||||
google/cloud/kms/artman_cloudkms.yaml
|
||||
google/cloud/language/artman_language_v1.yaml
|
||||
google/cloud/language/artman_language_v1beta2.yaml
|
||||
google/cloud/oslogin/artman_oslogin_v1.yaml
|
||||
google/cloud/oslogin/artman_oslogin_v1beta.yaml
|
||||
google/cloud/phishingprotection/artman_phishingprotection_v1beta1.yaml
|
||||
google/cloud/recaptchaenterprise/artman_recaptchaenterprise_v1beta1.yaml
|
||||
google/cloud/redis/artman_redis_v1beta1.yaml
|
||||
google/cloud/redis/artman_redis_v1.yaml
|
||||
google/cloud/scheduler/artman_cloudscheduler_v1beta1.yaml
|
||||
google/cloud/scheduler/artman_cloudscheduler_v1.yaml
|
||||
google/cloud/securitycenter/artman_securitycenter_v1beta1.yaml
|
||||
google/cloud/securitycenter/artman_securitycenter_v1.yaml
|
||||
google/cloud/speech/artman_speech_v1.yaml
|
||||
google/cloud/speech/artman_speech_v1p1beta1.yaml
|
||||
google/cloud/talent/artman_talent_v4beta1.yaml
|
||||
google/cloud/tasks/artman_cloudtasks_v2beta2.yaml
|
||||
google/cloud/tasks/artman_cloudtasks_v2beta3.yaml
|
||||
google/cloud/tasks/artman_cloudtasks_v2.yaml
|
||||
google/cloud/texttospeech/artman_texttospeech_v1.yaml
|
||||
google/cloud/videointelligence/artman_videointelligence_v1.yaml
|
||||
google/cloud/videointelligence/artman_videointelligence_v1beta1.yaml
|
||||
google/cloud/videointelligence/artman_videointelligence_v1beta2.yaml
|
||||
google/cloud/vision/artman_vision_v1.yaml
|
||||
google/cloud/vision/artman_vision_v1p1beta1.yaml
|
||||
google/cloud/webrisk/artman_webrisk_v1beta1.yaml
|
||||
google/devtools/artman_clouddebugger.yaml
|
||||
google/devtools/clouderrorreporting/artman_errorreporting.yaml
|
||||
google/devtools/cloudtrace/artman_cloudtrace_v1.yaml
|
||||
google/devtools/cloudtrace/artman_cloudtrace_v2.yaml
|
||||
|
||||
# The containeranalysis team wants manual changes in the auto-generated gapic.
|
||||
# So, let's remove it from the autogen list until we're ready to spend energy
|
||||
# generating and manually updating it.
|
||||
# google/devtools/containeranalysis/artman_containeranalysis_v1.yaml
|
||||
|
||||
google/devtools/containeranalysis/artman_containeranalysis_v1beta1.yaml
|
||||
google/firestore/artman_firestore.yaml
|
||||
google/firestore/admin/artman_firestore_v1.yaml
|
||||
|
||||
# See containeranalysis note above.
|
||||
# grafeas/artman_grafeas_v1.yaml
|
||||
|
||||
google/logging/artman_logging.yaml
|
||||
google/longrunning/artman_longrunning.yaml
|
||||
google/monitoring/artman_monitoring.yaml
|
||||
google/privacy/dlp/artman_dlp_v2.yaml
|
||||
google/pubsub/artman_pubsub.yaml
|
||||
google/spanner/admin/database/artman_spanner_admin_database.yaml
|
||||
google/spanner/admin/instance/artman_spanner_admin_instance.yaml
|
||||
google/spanner/artman_spanner.yaml
|
||||
)
|
||||
|
||||
for api in "${APIS[@]}"; do
|
||||
rm -rf artman-genfiles/*
|
||||
artman --config "$api" generate go_gapic
|
||||
cp -r artman-genfiles/gapi-*/cloud.google.com/go/* $GOPATH/src/cloud.google.com/go/
|
||||
done
|
||||
|
||||
microgen() {
|
||||
input=$1
|
||||
options="${@:2}"
|
||||
|
||||
# see https://github.com/googleapis/gapic-generator-go/blob/master/README.md#docker-wrapper for details
|
||||
docker run \
|
||||
--mount type=bind,source=$(pwd),destination=/conf,readonly \
|
||||
--mount type=bind,source=$(pwd)/$input,destination=/in/$input,readonly \
|
||||
--mount type=bind,source=$GOPATH/src,destination=/out \
|
||||
--rm \
|
||||
gcr.io/gapic-images/gapic-generator-go:latest \
|
||||
$options
|
||||
}
|
||||
|
||||
MICROAPIS=(
|
||||
# input proto directory | gapic-generator-go flag | gapic-service-config flag
|
||||
# "google/cloud/language/v1 --go-gapic-package cloud.google.com/go/language/apiv1;language --gapic-service-config google/cloud/language/language_v1.yaml"
|
||||
)
|
||||
|
||||
for api in "${MICROAPIS[@]}"; do
|
||||
microgen $api
|
||||
done
|
||||
|
||||
pushd $GOPATH/src/cloud.google.com/go/
|
||||
gofmt -s -d -l -w . && goimports -w .
|
||||
|
||||
# NOTE(pongad): `sed -i` doesn't work on Macs, because -i option needs an argument.
|
||||
# `-i ''` doesn't work on GNU, since the empty string is treated as a file name.
|
||||
# So we just create the backup and delete it after.
|
||||
ver=$(date +%Y%m%d)
|
||||
git ls-files -mo | while read modified; do
|
||||
dir=${modified%/*.*}
|
||||
find . -path "*/$dir/doc.go" -exec sed -i.backup -e "s/^const versionClient.*/const versionClient = \"$ver\"/" '{}' +
|
||||
done
|
||||
popd
|
||||
|
||||
|
||||
HASMANUAL=(
|
||||
errorreporting/apiv1beta1
|
||||
firestore/apiv1beta1
|
||||
firestore/apiv1
|
||||
logging/apiv2
|
||||
longrunning/autogen
|
||||
pubsub/apiv1
|
||||
spanner/apiv1
|
||||
trace/apiv1
|
||||
)
|
||||
for dir in "${HASMANUAL[@]}"; do
|
||||
find "$GOPATH/src/cloud.google.com/go/$dir" -name '*.go' -exec sed -i.backup -e 's/setGoogleClientInfo/SetGoogleClientInfo/g' '{}' '+'
|
||||
done
|
||||
|
||||
find $GOPATH/src/cloud.google.com/go/ -name '*.backup' -delete
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "storage",
|
||||
"name_pretty": "storage",
|
||||
"product_documentation": "https://cloud.google.com/storage",
|
||||
"client_documentation": "https://godoc.org/cloud.google.com/go/storage",
|
||||
"release_level": "ga",
|
||||
"language": "go",
|
||||
"repo": "googleapis/google-cloud-go",
|
||||
"distribution_name": "cloud.google.com/go/storage",
|
||||
"api_id": "storage:v2",
|
||||
"requires_billing": true
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
# Changes
|
||||
|
||||
## v1.0.0
|
||||
|
||||
This is the first tag to carve out storage as its own module. See:
|
||||
https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository.
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
|
@ -0,0 +1,32 @@
|
|||
## Cloud Storage [](https://godoc.org/cloud.google.com/go/storage)
|
||||
|
||||
- [About Cloud Storage](https://cloud.google.com/storage/)
|
||||
- [API documentation](https://cloud.google.com/storage/docs)
|
||||
- [Go client documentation](https://godoc.org/cloud.google.com/go/storage)
|
||||
- [Complete sample programs](https://github.com/GoogleCloudPlatform/golang-samples/tree/master/storage)
|
||||
|
||||
### Example Usage
|
||||
|
||||
First create a `storage.Client` to use throughout your application:
|
||||
|
||||
[snip]:# (storage-1)
|
||||
```go
|
||||
client, err := storage.NewClient(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
[snip]:# (storage-2)
|
||||
```go
|
||||
// Read the object1 from bucket.
|
||||
rc, err := client.Bucket("bucket").Object("object1").NewReader(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer rc.Close()
|
||||
body, err := ioutil.ReadAll(rc)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
|
@ -0,0 +1,335 @@
|
|||
// Copyright 2014 Google LLC
|
||||
//
|
||||
// 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 storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"cloud.google.com/go/internal/trace"
|
||||
"google.golang.org/api/googleapi"
|
||||
raw "google.golang.org/api/storage/v1"
|
||||
)
|
||||
|
||||
// ACLRole is the level of access to grant.
|
||||
type ACLRole string
|
||||
|
||||
const (
|
||||
RoleOwner ACLRole = "OWNER"
|
||||
RoleReader ACLRole = "READER"
|
||||
RoleWriter ACLRole = "WRITER"
|
||||
)
|
||||
|
||||
// ACLEntity refers to a user or group.
|
||||
// They are sometimes referred to as grantees.
|
||||
//
|
||||
// It could be in the form of:
|
||||
// "user-<userId>", "user-<email>", "group-<groupId>", "group-<email>",
|
||||
// "domain-<domain>" and "project-team-<projectId>".
|
||||
//
|
||||
// Or one of the predefined constants: AllUsers, AllAuthenticatedUsers.
|
||||
type ACLEntity string
|
||||
|
||||
const (
|
||||
AllUsers ACLEntity = "allUsers"
|
||||
AllAuthenticatedUsers ACLEntity = "allAuthenticatedUsers"
|
||||
)
|
||||
|
||||
// ACLRule represents a grant for a role to an entity (user, group or team) for a
|
||||
// Google Cloud Storage object or bucket.
|
||||
type ACLRule struct {
|
||||
Entity ACLEntity
|
||||
EntityID string
|
||||
Role ACLRole
|
||||
Domain string
|
||||
Email string
|
||||
ProjectTeam *ProjectTeam
|
||||
}
|
||||
|
||||
// ProjectTeam is the project team associated with the entity, if any.
|
||||
type ProjectTeam struct {
|
||||
ProjectNumber string
|
||||
Team string
|
||||
}
|
||||
|
||||
// ACLHandle provides operations on an access control list for a Google Cloud Storage bucket or object.
|
||||
type ACLHandle struct {
|
||||
c *Client
|
||||
bucket string
|
||||
object string
|
||||
isDefault bool
|
||||
userProject string // for requester-pays buckets
|
||||
}
|
||||
|
||||
// Delete permanently deletes the ACL entry for the given entity.
|
||||
func (a *ACLHandle) Delete(ctx context.Context, entity ACLEntity) (err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.ACL.Delete")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
if a.object != "" {
|
||||
return a.objectDelete(ctx, entity)
|
||||
}
|
||||
if a.isDefault {
|
||||
return a.bucketDefaultDelete(ctx, entity)
|
||||
}
|
||||
return a.bucketDelete(ctx, entity)
|
||||
}
|
||||
|
||||
// Set sets the role for the given entity.
|
||||
func (a *ACLHandle) Set(ctx context.Context, entity ACLEntity, role ACLRole) (err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.ACL.Set")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
if a.object != "" {
|
||||
return a.objectSet(ctx, entity, role, false)
|
||||
}
|
||||
if a.isDefault {
|
||||
return a.objectSet(ctx, entity, role, true)
|
||||
}
|
||||
return a.bucketSet(ctx, entity, role)
|
||||
}
|
||||
|
||||
// List retrieves ACL entries.
|
||||
func (a *ACLHandle) List(ctx context.Context) (rules []ACLRule, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.ACL.List")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
if a.object != "" {
|
||||
return a.objectList(ctx)
|
||||
}
|
||||
if a.isDefault {
|
||||
return a.bucketDefaultList(ctx)
|
||||
}
|
||||
return a.bucketList(ctx)
|
||||
}
|
||||
|
||||
func (a *ACLHandle) bucketDefaultList(ctx context.Context) ([]ACLRule, error) {
|
||||
var acls *raw.ObjectAccessControls
|
||||
var err error
|
||||
err = runWithRetry(ctx, func() error {
|
||||
req := a.c.raw.DefaultObjectAccessControls.List(a.bucket)
|
||||
a.configureCall(ctx, req)
|
||||
acls, err = req.Do()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toObjectACLRules(acls.Items), nil
|
||||
}
|
||||
|
||||
func (a *ACLHandle) bucketDefaultDelete(ctx context.Context, entity ACLEntity) error {
|
||||
return runWithRetry(ctx, func() error {
|
||||
req := a.c.raw.DefaultObjectAccessControls.Delete(a.bucket, string(entity))
|
||||
a.configureCall(ctx, req)
|
||||
return req.Do()
|
||||
})
|
||||
}
|
||||
|
||||
func (a *ACLHandle) bucketList(ctx context.Context) ([]ACLRule, error) {
|
||||
var acls *raw.BucketAccessControls
|
||||
var err error
|
||||
err = runWithRetry(ctx, func() error {
|
||||
req := a.c.raw.BucketAccessControls.List(a.bucket)
|
||||
a.configureCall(ctx, req)
|
||||
acls, err = req.Do()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toBucketACLRules(acls.Items), nil
|
||||
}
|
||||
|
||||
func (a *ACLHandle) bucketSet(ctx context.Context, entity ACLEntity, role ACLRole) error {
|
||||
acl := &raw.BucketAccessControl{
|
||||
Bucket: a.bucket,
|
||||
Entity: string(entity),
|
||||
Role: string(role),
|
||||
}
|
||||
err := runWithRetry(ctx, func() error {
|
||||
req := a.c.raw.BucketAccessControls.Update(a.bucket, string(entity), acl)
|
||||
a.configureCall(ctx, req)
|
||||
_, err := req.Do()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ACLHandle) bucketDelete(ctx context.Context, entity ACLEntity) error {
|
||||
return runWithRetry(ctx, func() error {
|
||||
req := a.c.raw.BucketAccessControls.Delete(a.bucket, string(entity))
|
||||
a.configureCall(ctx, req)
|
||||
return req.Do()
|
||||
})
|
||||
}
|
||||
|
||||
func (a *ACLHandle) objectList(ctx context.Context) ([]ACLRule, error) {
|
||||
var acls *raw.ObjectAccessControls
|
||||
var err error
|
||||
err = runWithRetry(ctx, func() error {
|
||||
req := a.c.raw.ObjectAccessControls.List(a.bucket, a.object)
|
||||
a.configureCall(ctx, req)
|
||||
acls, err = req.Do()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toObjectACLRules(acls.Items), nil
|
||||
}
|
||||
|
||||
func (a *ACLHandle) objectSet(ctx context.Context, entity ACLEntity, role ACLRole, isBucketDefault bool) error {
|
||||
type setRequest interface {
|
||||
Do(opts ...googleapi.CallOption) (*raw.ObjectAccessControl, error)
|
||||
Header() http.Header
|
||||
}
|
||||
|
||||
acl := &raw.ObjectAccessControl{
|
||||
Bucket: a.bucket,
|
||||
Entity: string(entity),
|
||||
Role: string(role),
|
||||
}
|
||||
var req setRequest
|
||||
if isBucketDefault {
|
||||
req = a.c.raw.DefaultObjectAccessControls.Update(a.bucket, string(entity), acl)
|
||||
} else {
|
||||
req = a.c.raw.ObjectAccessControls.Update(a.bucket, a.object, string(entity), acl)
|
||||
}
|
||||
a.configureCall(ctx, req)
|
||||
return runWithRetry(ctx, func() error {
|
||||
_, err := req.Do()
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (a *ACLHandle) objectDelete(ctx context.Context, entity ACLEntity) error {
|
||||
return runWithRetry(ctx, func() error {
|
||||
req := a.c.raw.ObjectAccessControls.Delete(a.bucket, a.object, string(entity))
|
||||
a.configureCall(ctx, req)
|
||||
return req.Do()
|
||||
})
|
||||
}
|
||||
|
||||
func (a *ACLHandle) configureCall(ctx context.Context, call interface{ Header() http.Header }) {
|
||||
vc := reflect.ValueOf(call)
|
||||
vc.MethodByName("Context").Call([]reflect.Value{reflect.ValueOf(ctx)})
|
||||
if a.userProject != "" {
|
||||
vc.MethodByName("UserProject").Call([]reflect.Value{reflect.ValueOf(a.userProject)})
|
||||
}
|
||||
setClientHeader(call.Header())
|
||||
}
|
||||
|
||||
func toObjectACLRules(items []*raw.ObjectAccessControl) []ACLRule {
|
||||
var rs []ACLRule
|
||||
for _, item := range items {
|
||||
rs = append(rs, toObjectACLRule(item))
|
||||
}
|
||||
return rs
|
||||
}
|
||||
|
||||
func toBucketACLRules(items []*raw.BucketAccessControl) []ACLRule {
|
||||
var rs []ACLRule
|
||||
for _, item := range items {
|
||||
rs = append(rs, toBucketACLRule(item))
|
||||
}
|
||||
return rs
|
||||
}
|
||||
|
||||
func toObjectACLRule(a *raw.ObjectAccessControl) ACLRule {
|
||||
return ACLRule{
|
||||
Entity: ACLEntity(a.Entity),
|
||||
EntityID: a.EntityId,
|
||||
Role: ACLRole(a.Role),
|
||||
Domain: a.Domain,
|
||||
Email: a.Email,
|
||||
ProjectTeam: toObjectProjectTeam(a.ProjectTeam),
|
||||
}
|
||||
}
|
||||
|
||||
func toBucketACLRule(a *raw.BucketAccessControl) ACLRule {
|
||||
return ACLRule{
|
||||
Entity: ACLEntity(a.Entity),
|
||||
EntityID: a.EntityId,
|
||||
Role: ACLRole(a.Role),
|
||||
Domain: a.Domain,
|
||||
Email: a.Email,
|
||||
ProjectTeam: toBucketProjectTeam(a.ProjectTeam),
|
||||
}
|
||||
}
|
||||
|
||||
func toRawObjectACL(rules []ACLRule) []*raw.ObjectAccessControl {
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
r := make([]*raw.ObjectAccessControl, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
r = append(r, rule.toRawObjectAccessControl("")) // bucket name unnecessary
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func toRawBucketACL(rules []ACLRule) []*raw.BucketAccessControl {
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
r := make([]*raw.BucketAccessControl, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
r = append(r, rule.toRawBucketAccessControl("")) // bucket name unnecessary
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ACLRule) toRawBucketAccessControl(bucket string) *raw.BucketAccessControl {
|
||||
return &raw.BucketAccessControl{
|
||||
Bucket: bucket,
|
||||
Entity: string(r.Entity),
|
||||
Role: string(r.Role),
|
||||
// The other fields are not settable.
|
||||
}
|
||||
}
|
||||
|
||||
func (r ACLRule) toRawObjectAccessControl(bucket string) *raw.ObjectAccessControl {
|
||||
return &raw.ObjectAccessControl{
|
||||
Bucket: bucket,
|
||||
Entity: string(r.Entity),
|
||||
Role: string(r.Role),
|
||||
// The other fields are not settable.
|
||||
}
|
||||
}
|
||||
|
||||
func toBucketProjectTeam(p *raw.BucketAccessControlProjectTeam) *ProjectTeam {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return &ProjectTeam{
|
||||
ProjectNumber: p.ProjectNumber,
|
||||
Team: p.Team,
|
||||
}
|
||||
}
|
||||
|
||||
func toObjectProjectTeam(p *raw.ObjectAccessControlProjectTeam) *ProjectTeam {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return &ProjectTeam{
|
||||
ProjectNumber: p.ProjectNumber,
|
||||
Team: p.Team,
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,228 @@
|
|||
// Copyright 2016 Google LLC
|
||||
//
|
||||
// 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 storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"cloud.google.com/go/internal/trace"
|
||||
raw "google.golang.org/api/storage/v1"
|
||||
)
|
||||
|
||||
// CopierFrom creates a Copier that can copy src to dst.
|
||||
// You can immediately call Run on the returned Copier, or
|
||||
// you can configure it first.
|
||||
//
|
||||
// For Requester Pays buckets, the user project of dst is billed, unless it is empty,
|
||||
// in which case the user project of src is billed.
|
||||
func (dst *ObjectHandle) CopierFrom(src *ObjectHandle) *Copier {
|
||||
return &Copier{dst: dst, src: src}
|
||||
}
|
||||
|
||||
// A Copier copies a source object to a destination.
|
||||
type Copier struct {
|
||||
// ObjectAttrs are optional attributes to set on the destination object.
|
||||
// Any attributes must be initialized before any calls on the Copier. Nil
|
||||
// or zero-valued attributes are ignored.
|
||||
ObjectAttrs
|
||||
|
||||
// RewriteToken can be set before calling Run to resume a copy
|
||||
// operation. After Run returns a non-nil error, RewriteToken will
|
||||
// have been updated to contain the value needed to resume the copy.
|
||||
RewriteToken string
|
||||
|
||||
// ProgressFunc can be used to monitor the progress of a multi-RPC copy
|
||||
// operation. If ProgressFunc is not nil and copying requires multiple
|
||||
// calls to the underlying service (see
|
||||
// https://cloud.google.com/storage/docs/json_api/v1/objects/rewrite), then
|
||||
// ProgressFunc will be invoked after each call with the number of bytes of
|
||||
// content copied so far and the total size in bytes of the source object.
|
||||
//
|
||||
// ProgressFunc is intended to make upload progress available to the
|
||||
// application. For example, the implementation of ProgressFunc may update
|
||||
// a progress bar in the application's UI, or log the result of
|
||||
// float64(copiedBytes)/float64(totalBytes).
|
||||
//
|
||||
// ProgressFunc should return quickly without blocking.
|
||||
ProgressFunc func(copiedBytes, totalBytes uint64)
|
||||
|
||||
// The Cloud KMS key, in the form projects/P/locations/L/keyRings/R/cryptoKeys/K,
|
||||
// that will be used to encrypt the object. Overrides the object's KMSKeyName, if
|
||||
// any.
|
||||
//
|
||||
// Providing both a DestinationKMSKeyName and a customer-supplied encryption key
|
||||
// (via ObjectHandle.Key) on the destination object will result in an error when
|
||||
// Run is called.
|
||||
DestinationKMSKeyName string
|
||||
|
||||
dst, src *ObjectHandle
|
||||
}
|
||||
|
||||
// Run performs the copy.
|
||||
func (c *Copier) Run(ctx context.Context) (attrs *ObjectAttrs, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Copier.Run")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
if err := c.src.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.dst.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.DestinationKMSKeyName != "" && c.dst.encryptionKey != nil {
|
||||
return nil, errors.New("storage: cannot use DestinationKMSKeyName with a customer-supplied encryption key")
|
||||
}
|
||||
// Convert destination attributes to raw form, omitting the bucket.
|
||||
// If the bucket is included but name or content-type aren't, the service
|
||||
// returns a 400 with "Required" as the only message. Omitting the bucket
|
||||
// does not cause any problems.
|
||||
rawObject := c.ObjectAttrs.toRawObject("")
|
||||
for {
|
||||
res, err := c.callRewrite(ctx, rawObject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.ProgressFunc != nil {
|
||||
c.ProgressFunc(uint64(res.TotalBytesRewritten), uint64(res.ObjectSize))
|
||||
}
|
||||
if res.Done { // Finished successfully.
|
||||
return newObject(res.Resource), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Copier) callRewrite(ctx context.Context, rawObj *raw.Object) (*raw.RewriteResponse, error) {
|
||||
call := c.dst.c.raw.Objects.Rewrite(c.src.bucket, c.src.object, c.dst.bucket, c.dst.object, rawObj)
|
||||
|
||||
call.Context(ctx).Projection("full")
|
||||
if c.RewriteToken != "" {
|
||||
call.RewriteToken(c.RewriteToken)
|
||||
}
|
||||
if c.DestinationKMSKeyName != "" {
|
||||
call.DestinationKmsKeyName(c.DestinationKMSKeyName)
|
||||
}
|
||||
if c.PredefinedACL != "" {
|
||||
call.DestinationPredefinedAcl(c.PredefinedACL)
|
||||
}
|
||||
if err := applyConds("Copy destination", c.dst.gen, c.dst.conds, call); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.dst.userProject != "" {
|
||||
call.UserProject(c.dst.userProject)
|
||||
} else if c.src.userProject != "" {
|
||||
call.UserProject(c.src.userProject)
|
||||
}
|
||||
if err := applySourceConds(c.src.gen, c.src.conds, call); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := setEncryptionHeaders(call.Header(), c.dst.encryptionKey, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := setEncryptionHeaders(call.Header(), c.src.encryptionKey, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var res *raw.RewriteResponse
|
||||
var err error
|
||||
setClientHeader(call.Header())
|
||||
err = runWithRetry(ctx, func() error { res, err = call.Do(); return err })
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.RewriteToken = res.RewriteToken
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ComposerFrom creates a Composer that can compose srcs into dst.
|
||||
// You can immediately call Run on the returned Composer, or you can
|
||||
// configure it first.
|
||||
//
|
||||
// The encryption key for the destination object will be used to decrypt all
|
||||
// source objects and encrypt the destination object. It is an error
|
||||
// to specify an encryption key for any of the source objects.
|
||||
func (dst *ObjectHandle) ComposerFrom(srcs ...*ObjectHandle) *Composer {
|
||||
return &Composer{dst: dst, srcs: srcs}
|
||||
}
|
||||
|
||||
// A Composer composes source objects into a destination object.
|
||||
//
|
||||
// For Requester Pays buckets, the user project of dst is billed.
|
||||
type Composer struct {
|
||||
// ObjectAttrs are optional attributes to set on the destination object.
|
||||
// Any attributes must be initialized before any calls on the Composer. Nil
|
||||
// or zero-valued attributes are ignored.
|
||||
ObjectAttrs
|
||||
|
||||
dst *ObjectHandle
|
||||
srcs []*ObjectHandle
|
||||
}
|
||||
|
||||
// Run performs the compose operation.
|
||||
func (c *Composer) Run(ctx context.Context) (attrs *ObjectAttrs, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Composer.Run")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
if err := c.dst.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(c.srcs) == 0 {
|
||||
return nil, errors.New("storage: at least one source object must be specified")
|
||||
}
|
||||
|
||||
req := &raw.ComposeRequest{}
|
||||
// Compose requires a non-empty Destination, so we always set it,
|
||||
// even if the caller-provided ObjectAttrs is the zero value.
|
||||
req.Destination = c.ObjectAttrs.toRawObject(c.dst.bucket)
|
||||
for _, src := range c.srcs {
|
||||
if err := src.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if src.bucket != c.dst.bucket {
|
||||
return nil, fmt.Errorf("storage: all source objects must be in bucket %q, found %q", c.dst.bucket, src.bucket)
|
||||
}
|
||||
if src.encryptionKey != nil {
|
||||
return nil, fmt.Errorf("storage: compose source %s.%s must not have encryption key", src.bucket, src.object)
|
||||
}
|
||||
srcObj := &raw.ComposeRequestSourceObjects{
|
||||
Name: src.object,
|
||||
}
|
||||
if err := applyConds("ComposeFrom source", src.gen, src.conds, composeSourceObj{srcObj}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SourceObjects = append(req.SourceObjects, srcObj)
|
||||
}
|
||||
|
||||
call := c.dst.c.raw.Objects.Compose(c.dst.bucket, c.dst.object, req).Context(ctx)
|
||||
if err := applyConds("ComposeFrom destination", c.dst.gen, c.dst.conds, call); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.dst.userProject != "" {
|
||||
call.UserProject(c.dst.userProject)
|
||||
}
|
||||
if c.PredefinedACL != "" {
|
||||
call.DestinationPredefinedAcl(c.PredefinedACL)
|
||||
}
|
||||
if err := setEncryptionHeaders(call.Header(), c.dst.encryptionKey, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var obj *raw.Object
|
||||
setClientHeader(call.Header())
|
||||
err = runWithRetry(ctx, func() error { obj, err = call.Do(); return err })
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newObject(obj), nil
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
// Copyright 2016 Google LLC
|
||||
//
|
||||
// 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 storage provides an easy way to work with Google Cloud Storage.
|
||||
Google Cloud Storage stores data in named objects, which are grouped into buckets.
|
||||
|
||||
More information about Google Cloud Storage is available at
|
||||
https://cloud.google.com/storage/docs.
|
||||
|
||||
See https://godoc.org/cloud.google.com/go for authentication, timeouts,
|
||||
connection pooling and similar aspects of this package.
|
||||
|
||||
All of the methods of this package use exponential backoff to retry calls that fail
|
||||
with certain errors, as described in
|
||||
https://cloud.google.com/storage/docs/exponential-backoff. Retrying continues
|
||||
indefinitely unless the controlling context is canceled or the client is closed. See
|
||||
context.WithTimeout and context.WithCancel.
|
||||
|
||||
|
||||
Creating a Client
|
||||
|
||||
To start working with this package, create a client:
|
||||
|
||||
ctx := context.Background()
|
||||
client, err := storage.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
The client will use your default application credentials.
|
||||
|
||||
If you only wish to access public data, you can create
|
||||
an unauthenticated client with
|
||||
|
||||
client, err := storage.NewClient(ctx, option.WithoutAuthentication())
|
||||
|
||||
Buckets
|
||||
|
||||
A Google Cloud Storage bucket is a collection of objects. To work with a
|
||||
bucket, make a bucket handle:
|
||||
|
||||
bkt := client.Bucket(bucketName)
|
||||
|
||||
A handle is a reference to a bucket. You can have a handle even if the
|
||||
bucket doesn't exist yet. To create a bucket in Google Cloud Storage,
|
||||
call Create on the handle:
|
||||
|
||||
if err := bkt.Create(ctx, projectID, nil); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
Note that although buckets are associated with projects, bucket names are
|
||||
global across all projects.
|
||||
|
||||
Each bucket has associated metadata, represented in this package by
|
||||
BucketAttrs. The third argument to BucketHandle.Create allows you to set
|
||||
the initial BucketAttrs of a bucket. To retrieve a bucket's attributes, use
|
||||
Attrs:
|
||||
|
||||
attrs, err := bkt.Attrs(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Printf("bucket %s, created at %s, is located in %s with storage class %s\n",
|
||||
attrs.Name, attrs.Created, attrs.Location, attrs.StorageClass)
|
||||
|
||||
Objects
|
||||
|
||||
An object holds arbitrary data as a sequence of bytes, like a file. You
|
||||
refer to objects using a handle, just as with buckets, but unlike buckets
|
||||
you don't explicitly create an object. Instead, the first time you write
|
||||
to an object it will be created. You can use the standard Go io.Reader
|
||||
and io.Writer interfaces to read and write object data:
|
||||
|
||||
obj := bkt.Object("data")
|
||||
// Write something to obj.
|
||||
// w implements io.Writer.
|
||||
w := obj.NewWriter(ctx)
|
||||
// Write some text to obj. This will either create the object or overwrite whatever is there already.
|
||||
if _, err := fmt.Fprintf(w, "This object contains text.\n"); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// Close, just like writing a file.
|
||||
if err := w.Close(); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
// Read it back.
|
||||
r, err := obj.NewReader(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer r.Close()
|
||||
if _, err := io.Copy(os.Stdout, r); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// Prints "This object contains text."
|
||||
|
||||
Objects also have attributes, which you can fetch with Attrs:
|
||||
|
||||
objAttrs, err := obj.Attrs(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Printf("object %s has size %d and can be read using %s\n",
|
||||
objAttrs.Name, objAttrs.Size, objAttrs.MediaLink)
|
||||
|
||||
ACLs
|
||||
|
||||
Both objects and buckets have ACLs (Access Control Lists). An ACL is a list of
|
||||
ACLRules, each of which specifies the role of a user, group or project. ACLs
|
||||
are suitable for fine-grained control, but you may prefer using IAM to control
|
||||
access at the project level (see
|
||||
https://cloud.google.com/storage/docs/access-control/iam).
|
||||
|
||||
To list the ACLs of a bucket or object, obtain an ACLHandle and call its List method:
|
||||
|
||||
acls, err := obj.ACL().List(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
for _, rule := range acls {
|
||||
fmt.Printf("%s has role %s\n", rule.Entity, rule.Role)
|
||||
}
|
||||
|
||||
You can also set and delete ACLs.
|
||||
|
||||
Conditions
|
||||
|
||||
Every object has a generation and a metageneration. The generation changes
|
||||
whenever the content changes, and the metageneration changes whenever the
|
||||
metadata changes. Conditions let you check these values before an operation;
|
||||
the operation only executes if the conditions match. You can use conditions to
|
||||
prevent race conditions in read-modify-write operations.
|
||||
|
||||
For example, say you've read an object's metadata into objAttrs. Now
|
||||
you want to write to that object, but only if its contents haven't changed
|
||||
since you read it. Here is how to express that:
|
||||
|
||||
w = obj.If(storage.Conditions{GenerationMatch: objAttrs.Generation}).NewWriter(ctx)
|
||||
// Proceed with writing as above.
|
||||
|
||||
Signed URLs
|
||||
|
||||
You can obtain a URL that lets anyone read or write an object for a limited time.
|
||||
You don't need to create a client to do this. See the documentation of
|
||||
SignedURL for details.
|
||||
|
||||
url, err := storage.SignedURL(bucketName, "shared-object", opts)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(url)
|
||||
|
||||
Errors
|
||||
|
||||
Errors returned by this client are often of the type [`googleapi.Error`](https://godoc.org/google.golang.org/api/googleapi#Error).
|
||||
These errors can be introspected for more information by type asserting to the richer `googleapi.Error` type. For example:
|
||||
|
||||
if e, ok := err.(*googleapi.Error); ok {
|
||||
if e.Code == 409 { ... }
|
||||
}
|
||||
*/
|
||||
package storage // import "cloud.google.com/go/storage"
|
|
@ -0,0 +1,14 @@
|
|||
module cloud.google.com/go/storage
|
||||
|
||||
go 1.9
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.46.3
|
||||
github.com/golang/protobuf v1.3.2
|
||||
github.com/google/go-cmp v0.3.0
|
||||
github.com/googleapis/gax-go/v2 v2.0.5
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
google.golang.org/api v0.9.0
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51
|
||||
google.golang.org/grpc v1.21.1
|
||||
)
|
|
@ -0,0 +1,156 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.1/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go/bigquery v1.0.1 h1:hL+ycaJpVE9M7nLoiXb/Pn10ENE2u+oddxbD8uu0ZVU=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0 h1:Kt+gOPPp2LEPWp8CSfxhsM8ik9CcyE/gYu+0r+RnZvM=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/firestore v1.0.0/go.mod h1:SdFEKccng5n2jTXm5x01uXEvi4MBzxWFR6YI781XSJI=
|
||||
cloud.google.com/go/pubsub v1.0.1 h1:W9tAK3E57P75u0XLLR82LZyw8VpAnhmyTOxW9qzmyj8=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979 h1:Agxu5KLo8o7Bb634SVDnhIfpTvxmzUwhbYAzBvXt6h4=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac h1:8R1esu+8QioDxo4E4mX6bFztO+dMTM49DNAaWfO5OeY=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff h1:On1qIo75ByTwFJ4/W2bIqHcwJ9XAqtSWUs8GwRrIhtc=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0 h1:jbyannxz0XFD3zdjgrSUsaJbgpH4eTrkdhRChkHPfO8=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51 h1:Ex1mq5jaJof+kRnYi3SlYJ8KKa9Ao3NHyIT5XJ1gF6U=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2017 Google LLC
|
||||
//
|
||||
// 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.
|
||||
|
||||
// +build go1.10
|
||||
|
||||
package storage
|
||||
|
||||
import "google.golang.org/api/googleapi"
|
||||
|
||||
func shouldRetry(err error) bool {
|
||||
switch e := err.(type) {
|
||||
case *googleapi.Error:
|
||||
// Retry on 429 and 5xx, according to
|
||||
// https://cloud.google.com/storage/docs/exponential-backoff.
|
||||
return e.Code == 429 || (e.Code >= 500 && e.Code < 600)
|
||||
case interface{ Temporary() bool }:
|
||||
return e.Temporary()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// 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.
|
||||
|
||||
// This file, and the cloud.google.com/go import, won't actually become part of
|
||||
// the resultant binary.
|
||||
// +build modhack
|
||||
|
||||
package storage
|
||||
|
||||
// Necessary for safely adding multi-module repo. See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository
|
||||
import _ "cloud.google.com/go"
|
|
@ -0,0 +1,387 @@
|
|||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// 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 storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"google.golang.org/api/iterator"
|
||||
raw "google.golang.org/api/storage/v1"
|
||||
)
|
||||
|
||||
// HMACState is the state of the HMAC key.
|
||||
type HMACState string
|
||||
|
||||
const (
|
||||
// Active is the status for an active key that can be used to sign
|
||||
// requests.
|
||||
Active HMACState = "ACTIVE"
|
||||
|
||||
// Inactive is the status for an inactive key thus requests signed by
|
||||
// this key will be denied.
|
||||
Inactive HMACState = "INACTIVE"
|
||||
|
||||
// Deleted is the status for a key that is deleted.
|
||||
// Once in this state the key cannot key cannot be recovered
|
||||
// and does not count towards key limits. Deleted keys will be cleaned
|
||||
// up later.
|
||||
Deleted HMACState = "DELETED"
|
||||
)
|
||||
|
||||
// HMACKey is the representation of a Google Cloud Storage HMAC key.
|
||||
//
|
||||
// HMAC keys are used to authenticate signed access to objects. To enable HMAC key
|
||||
// authentication, please visit https://cloud.google.com/storage/docs/migrating.
|
||||
//
|
||||
// This type is EXPERIMENTAL and subject to change or removal without notice.
|
||||
type HMACKey struct {
|
||||
// The HMAC's secret key.
|
||||
Secret string
|
||||
|
||||
// AccessID is the ID of the HMAC key.
|
||||
AccessID string
|
||||
|
||||
// Etag is the HTTP/1.1 Entity tag.
|
||||
Etag string
|
||||
|
||||
// ID is the ID of the HMAC key, including the ProjectID and AccessID.
|
||||
ID string
|
||||
|
||||
// ProjectID is the ID of the project that owns the
|
||||
// service account to which the key authenticates.
|
||||
ProjectID string
|
||||
|
||||
// ServiceAccountEmail is the email address
|
||||
// of the key's associated service account.
|
||||
ServiceAccountEmail string
|
||||
|
||||
// CreatedTime is the creation time of the HMAC key.
|
||||
CreatedTime time.Time
|
||||
|
||||
// UpdatedTime is the last modification time of the HMAC key metadata.
|
||||
UpdatedTime time.Time
|
||||
|
||||
// State is the state of the HMAC key.
|
||||
// It can be one of StateActive, StateInactive or StateDeleted.
|
||||
State HMACState
|
||||
}
|
||||
|
||||
// HMACKeyHandle helps provide access and management for HMAC keys.
|
||||
//
|
||||
// This type is EXPERIMENTAL and subject to change or removal without notice.
|
||||
type HMACKeyHandle struct {
|
||||
projectID string
|
||||
accessID string
|
||||
|
||||
raw *raw.ProjectsHmacKeysService
|
||||
}
|
||||
|
||||
// HMACKeyHandle creates a handle that will be used for HMACKey operations.
|
||||
//
|
||||
// This method is EXPERIMENTAL and subject to change or removal without notice.
|
||||
func (c *Client) HMACKeyHandle(projectID, accessID string) *HMACKeyHandle {
|
||||
return &HMACKeyHandle{
|
||||
projectID: projectID,
|
||||
accessID: accessID,
|
||||
raw: raw.NewProjectsHmacKeysService(c.raw),
|
||||
}
|
||||
}
|
||||
|
||||
// Get invokes an RPC to retrieve the HMAC key referenced by the
|
||||
// HMACKeyHandle's accessID.
|
||||
//
|
||||
// This method is EXPERIMENTAL and subject to change or removal without notice.
|
||||
func (hkh *HMACKeyHandle) Get(ctx context.Context) (*HMACKey, error) {
|
||||
call := hkh.raw.Get(hkh.projectID, hkh.accessID)
|
||||
setClientHeader(call.Header())
|
||||
|
||||
var metadata *raw.HmacKeyMetadata
|
||||
var err error
|
||||
err = runWithRetry(ctx, func() error {
|
||||
metadata, err = call.Context(ctx).Do()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hkPb := &raw.HmacKey{
|
||||
Metadata: metadata,
|
||||
}
|
||||
return pbHmacKeyToHMACKey(hkPb, false)
|
||||
}
|
||||
|
||||
// Delete invokes an RPC to delete the key referenced by accessID, on Google Cloud Storage.
|
||||
// Only inactive HMAC keys can be deleted.
|
||||
// After deletion, a key cannot be used to authenticate requests.
|
||||
//
|
||||
// This method is EXPERIMENTAL and subject to change or removal without notice.
|
||||
func (hkh *HMACKeyHandle) Delete(ctx context.Context) error {
|
||||
delCall := hkh.raw.Delete(hkh.projectID, hkh.accessID)
|
||||
setClientHeader(delCall.Header())
|
||||
|
||||
return runWithRetry(ctx, func() error {
|
||||
return delCall.Context(ctx).Do()
|
||||
})
|
||||
}
|
||||
|
||||
func pbHmacKeyToHMACKey(pb *raw.HmacKey, updatedTimeCanBeNil bool) (*HMACKey, error) {
|
||||
pbmd := pb.Metadata
|
||||
if pbmd == nil {
|
||||
return nil, errors.New("field Metadata cannot be nil")
|
||||
}
|
||||
createdTime, err := time.Parse(time.RFC3339, pbmd.TimeCreated)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("field CreatedTime: %v", err)
|
||||
}
|
||||
updatedTime, err := time.Parse(time.RFC3339, pbmd.Updated)
|
||||
if err != nil && !updatedTimeCanBeNil {
|
||||
return nil, fmt.Errorf("field UpdatedTime: %v", err)
|
||||
}
|
||||
|
||||
hmk := &HMACKey{
|
||||
AccessID: pbmd.AccessId,
|
||||
Secret: pb.Secret,
|
||||
Etag: pbmd.Etag,
|
||||
ID: pbmd.Id,
|
||||
State: HMACState(pbmd.State),
|
||||
ProjectID: pbmd.ProjectId,
|
||||
CreatedTime: createdTime,
|
||||
UpdatedTime: updatedTime,
|
||||
|
||||
ServiceAccountEmail: pbmd.ServiceAccountEmail,
|
||||
}
|
||||
|
||||
return hmk, nil
|
||||
}
|
||||
|
||||
// CreateHMACKey invokes an RPC for Google Cloud Storage to create a new HMACKey.
|
||||
//
|
||||
// This method is EXPERIMENTAL and subject to change or removal without notice.
|
||||
func (c *Client) CreateHMACKey(ctx context.Context, projectID, serviceAccountEmail string) (*HMACKey, error) {
|
||||
if projectID == "" {
|
||||
return nil, errors.New("storage: expecting a non-blank projectID")
|
||||
}
|
||||
if serviceAccountEmail == "" {
|
||||
return nil, errors.New("storage: expecting a non-blank service account email")
|
||||
}
|
||||
|
||||
svc := raw.NewProjectsHmacKeysService(c.raw)
|
||||
call := svc.Create(projectID, serviceAccountEmail)
|
||||
setClientHeader(call.Header())
|
||||
|
||||
var hkPb *raw.HmacKey
|
||||
var err error
|
||||
err = runWithRetry(ctx, func() error {
|
||||
hkPb, err = call.Context(ctx).Do()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pbHmacKeyToHMACKey(hkPb, true)
|
||||
}
|
||||
|
||||
// HMACKeyAttrsToUpdate defines the attributes of an HMACKey that will be updated.
|
||||
//
|
||||
// This type is EXPERIMENTAL and subject to change or removal without notice.
|
||||
type HMACKeyAttrsToUpdate struct {
|
||||
// State is required and must be either StateActive or StateInactive.
|
||||
State HMACState
|
||||
|
||||
// Etag is an optional field and it is the HTTP/1.1 Entity tag.
|
||||
Etag string
|
||||
}
|
||||
|
||||
// Update mutates the HMACKey referred to by accessID.
|
||||
//
|
||||
// This method is EXPERIMENTAL and subject to change or removal without notice.
|
||||
func (h *HMACKeyHandle) Update(ctx context.Context, au HMACKeyAttrsToUpdate) (*HMACKey, error) {
|
||||
if au.State != Active && au.State != Inactive {
|
||||
return nil, fmt.Errorf("storage: invalid state %q for update, must be either %q or %q", au.State, Active, Inactive)
|
||||
}
|
||||
|
||||
call := h.raw.Update(h.projectID, h.accessID, &raw.HmacKeyMetadata{
|
||||
Etag: au.Etag,
|
||||
State: string(au.State),
|
||||
})
|
||||
setClientHeader(call.Header())
|
||||
|
||||
var metadata *raw.HmacKeyMetadata
|
||||
var err error
|
||||
err = runWithRetry(ctx, func() error {
|
||||
metadata, err = call.Context(ctx).Do()
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hkPb := &raw.HmacKey{
|
||||
Metadata: metadata,
|
||||
}
|
||||
return pbHmacKeyToHMACKey(hkPb, false)
|
||||
}
|
||||
|
||||
// An HMACKeysIterator is an iterator over HMACKeys.
|
||||
//
|
||||
// This type is EXPERIMENTAL and subject to change or removal without notice.
|
||||
type HMACKeysIterator struct {
|
||||
ctx context.Context
|
||||
raw *raw.ProjectsHmacKeysService
|
||||
projectID string
|
||||
hmacKeys []*HMACKey
|
||||
pageInfo *iterator.PageInfo
|
||||
nextFunc func() error
|
||||
index int
|
||||
desc hmacKeyDesc
|
||||
}
|
||||
|
||||
// ListHMACKeys returns an iterator for listing HMACKeys.
|
||||
//
|
||||
// This method is EXPERIMENTAL and subject to change or removal without notice.
|
||||
func (c *Client) ListHMACKeys(ctx context.Context, projectID string, opts ...HMACKeyOption) *HMACKeysIterator {
|
||||
it := &HMACKeysIterator{
|
||||
ctx: ctx,
|
||||
raw: raw.NewProjectsHmacKeysService(c.raw),
|
||||
projectID: projectID,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt.withHMACKeyDesc(&it.desc)
|
||||
}
|
||||
|
||||
it.pageInfo, it.nextFunc = iterator.NewPageInfo(
|
||||
it.fetch,
|
||||
func() int { return len(it.hmacKeys) - it.index },
|
||||
func() interface{} {
|
||||
prev := it.hmacKeys
|
||||
it.hmacKeys = it.hmacKeys[:0]
|
||||
it.index = 0
|
||||
return prev
|
||||
})
|
||||
return it
|
||||
}
|
||||
|
||||
// Next returns the next result. Its second return value is iterator.Done if
|
||||
// there are no more results. Once Next returns iterator.Done, all subsequent
|
||||
// calls will return iterator.Done.
|
||||
//
|
||||
// This method is EXPERIMENTAL and subject to change or removal without notice.
|
||||
func (it *HMACKeysIterator) Next() (*HMACKey, error) {
|
||||
if err := it.nextFunc(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := it.hmacKeys[it.index]
|
||||
it.index++
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
|
||||
//
|
||||
// This method is EXPERIMENTAL and subject to change or removal without notice.
|
||||
func (it *HMACKeysIterator) PageInfo() *iterator.PageInfo { return it.pageInfo }
|
||||
|
||||
func (it *HMACKeysIterator) fetch(pageSize int, pageToken string) (token string, err error) {
|
||||
call := it.raw.List(it.projectID)
|
||||
setClientHeader(call.Header())
|
||||
if pageToken != "" {
|
||||
call = call.PageToken(pageToken)
|
||||
}
|
||||
if it.desc.showDeletedKeys {
|
||||
call = call.ShowDeletedKeys(true)
|
||||
}
|
||||
if it.desc.userProjectID != "" {
|
||||
call = call.UserProject(it.desc.userProjectID)
|
||||
}
|
||||
if it.desc.forServiceAccountEmail != "" {
|
||||
call = call.ServiceAccountEmail(it.desc.forServiceAccountEmail)
|
||||
}
|
||||
if pageSize > 0 {
|
||||
call = call.MaxResults(int64(pageSize))
|
||||
}
|
||||
|
||||
ctx := it.ctx
|
||||
var resp *raw.HmacKeysMetadata
|
||||
err = runWithRetry(it.ctx, func() error {
|
||||
resp, err = call.Context(ctx).Do()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, metadata := range resp.Items {
|
||||
hkPb := &raw.HmacKey{
|
||||
Metadata: metadata,
|
||||
}
|
||||
hkey, err := pbHmacKeyToHMACKey(hkPb, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
it.hmacKeys = append(it.hmacKeys, hkey)
|
||||
}
|
||||
return resp.NextPageToken, nil
|
||||
}
|
||||
|
||||
type hmacKeyDesc struct {
|
||||
forServiceAccountEmail string
|
||||
showDeletedKeys bool
|
||||
userProjectID string
|
||||
}
|
||||
|
||||
// HMACKeyOption configures the behavior of HMACKey related methods and actions.
|
||||
type HMACKeyOption interface {
|
||||
withHMACKeyDesc(*hmacKeyDesc)
|
||||
}
|
||||
|
||||
type hmacKeyDescFunc func(*hmacKeyDesc)
|
||||
|
||||
func (hkdf hmacKeyDescFunc) withHMACKeyDesc(hkd *hmacKeyDesc) {
|
||||
hkdf(hkd)
|
||||
}
|
||||
|
||||
// ForHMACKeyServiceAccountEmail returns HMAC Keys that are
|
||||
// associated with the email address of a service account in the project.
|
||||
//
|
||||
// Only one service account email can be used as a filter, so if multiple
|
||||
// of these options are applied, the last email to be set will be used.
|
||||
func ForHMACKeyServiceAccountEmail(serviceAccountEmail string) HMACKeyOption {
|
||||
return hmacKeyDescFunc(func(hkd *hmacKeyDesc) {
|
||||
hkd.forServiceAccountEmail = serviceAccountEmail
|
||||
})
|
||||
}
|
||||
|
||||
// ShowDeletedHMACKeys will also list keys whose state is "DELETED".
|
||||
func ShowDeletedHMACKeys() HMACKeyOption {
|
||||
return hmacKeyDescFunc(func(hkd *hmacKeyDesc) {
|
||||
hkd.showDeletedKeys = true
|
||||
})
|
||||
}
|
||||
|
||||
// HMACKeysForUserProject will bill the request against userProjectID.
|
||||
//
|
||||
// Note: This is a noop right now and only provided for API compatibility.
|
||||
func HMACKeysForUserProject(userProjectID string) HMACKeyOption {
|
||||
return hmacKeyDescFunc(func(hkd *hmacKeyDesc) {
|
||||
hkd.userProjectID = userProjectID
|
||||
})
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
// Copyright 2017 Google LLC
|
||||
//
|
||||
// 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 storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"cloud.google.com/go/iam"
|
||||
"cloud.google.com/go/internal/trace"
|
||||
raw "google.golang.org/api/storage/v1"
|
||||
iampb "google.golang.org/genproto/googleapis/iam/v1"
|
||||
)
|
||||
|
||||
// IAM provides access to IAM access control for the bucket.
|
||||
func (b *BucketHandle) IAM() *iam.Handle {
|
||||
return iam.InternalNewHandleClient(&iamClient{
|
||||
raw: b.c.raw,
|
||||
userProject: b.userProject,
|
||||
}, b.name)
|
||||
}
|
||||
|
||||
// iamClient implements the iam.client interface.
|
||||
type iamClient struct {
|
||||
raw *raw.Service
|
||||
userProject string
|
||||
}
|
||||
|
||||
func (c *iamClient) Get(ctx context.Context, resource string) (p *iampb.Policy, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.IAM.Get")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
call := c.raw.Buckets.GetIamPolicy(resource)
|
||||
setClientHeader(call.Header())
|
||||
if c.userProject != "" {
|
||||
call.UserProject(c.userProject)
|
||||
}
|
||||
var rp *raw.Policy
|
||||
err = runWithRetry(ctx, func() error {
|
||||
rp, err = call.Context(ctx).Do()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return iamFromStoragePolicy(rp), nil
|
||||
}
|
||||
|
||||
func (c *iamClient) Set(ctx context.Context, resource string, p *iampb.Policy) (err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.IAM.Set")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
rp := iamToStoragePolicy(p)
|
||||
call := c.raw.Buckets.SetIamPolicy(resource, rp)
|
||||
setClientHeader(call.Header())
|
||||
if c.userProject != "" {
|
||||
call.UserProject(c.userProject)
|
||||
}
|
||||
return runWithRetry(ctx, func() error {
|
||||
_, err := call.Context(ctx).Do()
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (c *iamClient) Test(ctx context.Context, resource string, perms []string) (permissions []string, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.IAM.Test")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
call := c.raw.Buckets.TestIamPermissions(resource, perms)
|
||||
setClientHeader(call.Header())
|
||||
if c.userProject != "" {
|
||||
call.UserProject(c.userProject)
|
||||
}
|
||||
var res *raw.TestIamPermissionsResponse
|
||||
err = runWithRetry(ctx, func() error {
|
||||
res, err = call.Context(ctx).Do()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.Permissions, nil
|
||||
}
|
||||
|
||||
func iamToStoragePolicy(ip *iampb.Policy) *raw.Policy {
|
||||
return &raw.Policy{
|
||||
Bindings: iamToStorageBindings(ip.Bindings),
|
||||
Etag: string(ip.Etag),
|
||||
}
|
||||
}
|
||||
|
||||
func iamToStorageBindings(ibs []*iampb.Binding) []*raw.PolicyBindings {
|
||||
var rbs []*raw.PolicyBindings
|
||||
for _, ib := range ibs {
|
||||
rbs = append(rbs, &raw.PolicyBindings{
|
||||
Role: ib.Role,
|
||||
Members: ib.Members,
|
||||
})
|
||||
}
|
||||
return rbs
|
||||
}
|
||||
|
||||
func iamFromStoragePolicy(rp *raw.Policy) *iampb.Policy {
|
||||
return &iampb.Policy{
|
||||
Bindings: iamFromStorageBindings(rp.Bindings),
|
||||
Etag: []byte(rp.Etag),
|
||||
}
|
||||
}
|
||||
|
||||
func iamFromStorageBindings(rbs []*raw.PolicyBindings) []*iampb.Binding {
|
||||
var ibs []*iampb.Binding
|
||||
for _, rb := range rbs {
|
||||
ibs = append(ibs, &iampb.Binding{
|
||||
Role: rb.Role,
|
||||
Members: rb.Members,
|
||||
})
|
||||
}
|
||||
return ibs
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright 2014 Google LLC
|
||||
//
|
||||
// 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 storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"cloud.google.com/go/internal"
|
||||
gax "github.com/googleapis/gax-go/v2"
|
||||
)
|
||||
|
||||
// runWithRetry calls the function until it returns nil or a non-retryable error, or
|
||||
// the context is done.
|
||||
func runWithRetry(ctx context.Context, call func() error) error {
|
||||
return internal.Retry(ctx, gax.Backoff{}, func() (stop bool, err error) {
|
||||
err = call()
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if shouldRetry(err) {
|
||||
return false, nil
|
||||
}
|
||||
return true, err
|
||||
})
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2017 Google LLC
|
||||
//
|
||||
// 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.
|
||||
|
||||
// +build !go1.10
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/api/googleapi"
|
||||
)
|
||||
|
||||
func shouldRetry(err error) bool {
|
||||
switch e := err.(type) {
|
||||
case *googleapi.Error:
|
||||
// Retry on 429 and 5xx, according to
|
||||
// https://cloud.google.com/storage/docs/exponential-backoff.
|
||||
return e.Code == 429 || (e.Code >= 500 && e.Code < 600)
|
||||
case *url.Error:
|
||||
// Retry on REFUSED_STREAM.
|
||||
// Unfortunately the error type is unexported, so we resort to string
|
||||
// matching.
|
||||
return strings.Contains(e.Error(), "REFUSED_STREAM")
|
||||
case interface{ Temporary() bool }:
|
||||
return e.Temporary()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
// Copyright 2017 Google LLC
|
||||
//
|
||||
// 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 storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"cloud.google.com/go/internal/trace"
|
||||
raw "google.golang.org/api/storage/v1"
|
||||
)
|
||||
|
||||
// A Notification describes how to send Cloud PubSub messages when certain
|
||||
// events occur in a bucket.
|
||||
type Notification struct {
|
||||
//The ID of the notification.
|
||||
ID string
|
||||
|
||||
// The ID of the topic to which this subscription publishes.
|
||||
TopicID string
|
||||
|
||||
// The ID of the project to which the topic belongs.
|
||||
TopicProjectID string
|
||||
|
||||
// Only send notifications about listed event types. If empty, send notifications
|
||||
// for all event types.
|
||||
// See https://cloud.google.com/storage/docs/pubsub-notifications#events.
|
||||
EventTypes []string
|
||||
|
||||
// If present, only apply this notification configuration to object names that
|
||||
// begin with this prefix.
|
||||
ObjectNamePrefix string
|
||||
|
||||
// An optional list of additional attributes to attach to each Cloud PubSub
|
||||
// message published for this notification subscription.
|
||||
CustomAttributes map[string]string
|
||||
|
||||
// The contents of the message payload.
|
||||
// See https://cloud.google.com/storage/docs/pubsub-notifications#payload.
|
||||
PayloadFormat string
|
||||
}
|
||||
|
||||
// Values for Notification.PayloadFormat.
|
||||
const (
|
||||
// Send no payload with notification messages.
|
||||
NoPayload = "NONE"
|
||||
|
||||
// Send object metadata as JSON with notification messages.
|
||||
JSONPayload = "JSON_API_V1"
|
||||
)
|
||||
|
||||
// Values for Notification.EventTypes.
|
||||
const (
|
||||
// Event that occurs when an object is successfully created.
|
||||
ObjectFinalizeEvent = "OBJECT_FINALIZE"
|
||||
|
||||
// Event that occurs when the metadata of an existing object changes.
|
||||
ObjectMetadataUpdateEvent = "OBJECT_METADATA_UPDATE"
|
||||
|
||||
// Event that occurs when an object is permanently deleted.
|
||||
ObjectDeleteEvent = "OBJECT_DELETE"
|
||||
|
||||
// Event that occurs when the live version of an object becomes an
|
||||
// archived version.
|
||||
ObjectArchiveEvent = "OBJECT_ARCHIVE"
|
||||
)
|
||||
|
||||
func toNotification(rn *raw.Notification) *Notification {
|
||||
n := &Notification{
|
||||
ID: rn.Id,
|
||||
EventTypes: rn.EventTypes,
|
||||
ObjectNamePrefix: rn.ObjectNamePrefix,
|
||||
CustomAttributes: rn.CustomAttributes,
|
||||
PayloadFormat: rn.PayloadFormat,
|
||||
}
|
||||
n.TopicProjectID, n.TopicID = parseNotificationTopic(rn.Topic)
|
||||
return n
|
||||
}
|
||||
|
||||
var topicRE = regexp.MustCompile("^//pubsub.googleapis.com/projects/([^/]+)/topics/([^/]+)")
|
||||
|
||||
// parseNotificationTopic extracts the project and topic IDs from from the full
|
||||
// resource name returned by the service. If the name is malformed, it returns
|
||||
// "?" for both IDs.
|
||||
func parseNotificationTopic(nt string) (projectID, topicID string) {
|
||||
matches := topicRE.FindStringSubmatch(nt)
|
||||
if matches == nil {
|
||||
return "?", "?"
|
||||
}
|
||||
return matches[1], matches[2]
|
||||
}
|
||||
|
||||
func toRawNotification(n *Notification) *raw.Notification {
|
||||
return &raw.Notification{
|
||||
Id: n.ID,
|
||||
Topic: fmt.Sprintf("//pubsub.googleapis.com/projects/%s/topics/%s",
|
||||
n.TopicProjectID, n.TopicID),
|
||||
EventTypes: n.EventTypes,
|
||||
ObjectNamePrefix: n.ObjectNamePrefix,
|
||||
CustomAttributes: n.CustomAttributes,
|
||||
PayloadFormat: string(n.PayloadFormat),
|
||||
}
|
||||
}
|
||||
|
||||
// AddNotification adds a notification to b. You must set n's TopicProjectID, TopicID
|
||||
// and PayloadFormat, and must not set its ID. The other fields are all optional. The
|
||||
// returned Notification's ID can be used to refer to it.
|
||||
func (b *BucketHandle) AddNotification(ctx context.Context, n *Notification) (ret *Notification, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Bucket.AddNotification")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
if n.ID != "" {
|
||||
return nil, errors.New("storage: AddNotification: ID must not be set")
|
||||
}
|
||||
if n.TopicProjectID == "" {
|
||||
return nil, errors.New("storage: AddNotification: missing TopicProjectID")
|
||||
}
|
||||
if n.TopicID == "" {
|
||||
return nil, errors.New("storage: AddNotification: missing TopicID")
|
||||
}
|
||||
call := b.c.raw.Notifications.Insert(b.name, toRawNotification(n))
|
||||
setClientHeader(call.Header())
|
||||
if b.userProject != "" {
|
||||
call.UserProject(b.userProject)
|
||||
}
|
||||
rn, err := call.Context(ctx).Do()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toNotification(rn), nil
|
||||
}
|
||||
|
||||
// Notifications returns all the Notifications configured for this bucket, as a map
|
||||
// indexed by notification ID.
|
||||
func (b *BucketHandle) Notifications(ctx context.Context) (n map[string]*Notification, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Bucket.Notifications")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
call := b.c.raw.Notifications.List(b.name)
|
||||
setClientHeader(call.Header())
|
||||
if b.userProject != "" {
|
||||
call.UserProject(b.userProject)
|
||||
}
|
||||
var res *raw.Notifications
|
||||
err = runWithRetry(ctx, func() error {
|
||||
res, err = call.Context(ctx).Do()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return notificationsToMap(res.Items), nil
|
||||
}
|
||||
|
||||
func notificationsToMap(rns []*raw.Notification) map[string]*Notification {
|
||||
m := map[string]*Notification{}
|
||||
for _, rn := range rns {
|
||||
m[rn.Id] = toNotification(rn)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// DeleteNotification deletes the notification with the given ID.
|
||||
func (b *BucketHandle) DeleteNotification(ctx context.Context, id string) (err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Bucket.DeleteNotification")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
call := b.c.raw.Notifications.Delete(b.name, id)
|
||||
setClientHeader(call.Header())
|
||||
if b.userProject != "" {
|
||||
call.UserProject(b.userProject)
|
||||
}
|
||||
return call.Context(ctx).Do()
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue