Skip to content

Commit

Permalink
fix: minimize dependencies on 3P libraries (#101)
Browse files Browse the repository at this point in the history
This keeps the functions framework as lightweight as possible, and
avoids pulling in all of the Google Cloud client libraries even though
only one package is needed.

The new `internal/metadata` package is just a copy of
[google-cloud-go/functions/metadata](https://github.com/googleapis/google-cloud-go/tree/main/functions/metadata)

Minimizing what transitive dependencies the Functions Framework has is
important to make them work with buildpacks, where user function code
has to be bundled together with the functions framework. If a function's
dependency conflicts with a Functions Framework dependency and the
function is using vendor, then there can be undefined behavior at build
and runtime.
  • Loading branch information
anniefu committed Nov 17, 2021
1 parent b421ea6 commit f5c1abd
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 413 deletions.
2 changes: 1 addition & 1 deletion funcframework/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import (
"regexp"
"strings"

"cloud.google.com/go/functions/metadata"
"github.com/GoogleCloudPlatform/functions-framework-go/internal/events/pubsub"
"github.com/GoogleCloudPlatform/functions-framework-go/internal/fftypes"
"github.com/GoogleCloudPlatform/functions-framework-go/internal/metadata"
)

const (
Expand Down
2 changes: 1 addition & 1 deletion funcframework/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"testing"
"time"

"cloud.google.com/go/functions/metadata"
"github.com/GoogleCloudPlatform/functions-framework-go/internal/metadata"
cloudevents "github.com/cloudevents/sdk-go/v2"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
Expand Down
8 changes: 2 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ module github.com/GoogleCloudPlatform/functions-framework-go
go 1.11

require (
cloud.google.com/go v0.63.0
cloud.google.com/go/pubsub v1.3.1
github.com/cloudevents/sdk-go/v2 v2.2.0
github.com/google/go-cmp v0.5.1
github.com/onsi/ginkgo v1.11.0 // indirect
github.com/onsi/gomega v1.8.1 // indirect
github.com/cloudevents/sdk-go/v2 v2.6.1
github.com/google/go-cmp v0.5.6
)
406 changes: 15 additions & 391 deletions go.sum

Large diffs are not rendered by default.

30 changes: 19 additions & 11 deletions internal/events/pubsub/pubsub.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import (
"regexp"
"time"

"cloud.google.com/go/functions/metadata"
"cloud.google.com/go/pubsub"
"github.com/GoogleCloudPlatform/functions-framework-go/internal/fftypes"
"github.com/GoogleCloudPlatform/functions-framework-go/internal/metadata"
)

const (
Expand All @@ -26,15 +25,24 @@ type LegacyPushSubscriptionEvent struct {
Message `json:"message"`
}

// Message is a pubsub.Message but with the correct JSON tag for the
// message ID field that matches https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage
// Message represents a Pub/Sub message.
type Message struct {
pubsub.Message
// The pubsub libary's Message.Id field (https://pkg.go.dev/cloud.google.com/go/internal/pubsub#Message)
// doesn't have the correct JSON tag (it serializes to "id" instead of
// "messageId"), so use this field to capture the JSON field with key
// "messageId".
IdFromJSON string `json:"messageId"`
// ID identifies this message.
// This ID is assigned by the server and is populated for Messages obtained from a subscription.
// This field is read-only.
ID string `json:"messageId"`

// Data is the actual data in the message.
Data []byte `json:"data"`

// Attributes represents the key-value pairs the current message
// is labelled with.
Attributes map[string]string `json:"attributes"`

// The time at which the message was published.
// This is populated by the server for Messages obtained from a subscription.
// This field is read-only.
PublishTime time.Time `json:"publishTime"`
}

// ExtractTopicFromRequestPath extracts a Pub/Sub topic from a URL request path.
Expand All @@ -59,7 +67,7 @@ func (e *LegacyPushSubscriptionEvent) ToBackgroundEvent(topic string) *fftypes.B
}
return &fftypes.BackgroundEvent{
Metadata: &metadata.Metadata{
EventID: e.IdFromJSON,
EventID: e.ID,
Timestamp: timestamp,
EventType: pubsubEventType,
Resource: &metadata.Resource{
Expand Down
2 changes: 1 addition & 1 deletion internal/events/pubsub/pubsub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"testing"
"time"

"cloud.google.com/go/functions/metadata"
"github.com/GoogleCloudPlatform/functions-framework-go/internal/fftypes"
"github.com/GoogleCloudPlatform/functions-framework-go/internal/metadata"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
Expand Down
2 changes: 1 addition & 1 deletion internal/fftypes/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
package fftypes

import (
"cloud.google.com/go/functions/metadata"
"github.com/GoogleCloudPlatform/functions-framework-go/internal/metadata"
)

// BackgroundEvent is the incoming payload to functions framework to trigger
Expand Down
147 changes: 147 additions & 0 deletions internal/metadata/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright 2021 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

import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
)

// Metadata holds Google Cloud Functions metadata.
type Metadata struct {
// EventID is a unique ID for the event. For example: "70172329041928".
EventID string `json:"eventId"`
// Timestamp is the date/time this event was created.
Timestamp time.Time `json:"timestamp"`
// EventType is the type of the event. For example: "google.pubsub.topic.publish".
EventType string `json:"eventType"`
// Resource is the resource that triggered the event.
Resource *Resource `json:"resource"`
}

// Resource holds Google Cloud Functions resource metadata.
// Resource values are dependent on the event type they're from.
type Resource struct {
// Service is the service that triggered the event.
Service string `json:"service"`
// Name is the name associated with the event.
Name string `json:"name"`
// Type is the type of event.
Type string `json:"type"`
// Path is the path to the resource type (deprecated).
// This is the case for some deprecated GCS
// notifications, which populate the resource field as a string containing the topic
// rather than as the expected dictionary.
// See the Attributes section of https://cloud.google.com/storage/docs/pubsub-notifications
// for more details.
RawPath string `json:"-"`
}

// UnmarshalJSON specializes the Resource unmarshalling to handle the case where the
// value is a string instead of a map. See the comment above on RawPath for why this
// needs to be handled.
func (r *Resource) UnmarshalJSON(data []byte) error {
// Try to unmarshal the resource into a string.
var path string
if err := json.Unmarshal(data, &path); err == nil {
r.RawPath = path
return nil
}

// Otherwise, accept whatever the result of the normal unmarshal would be.
// Need to define a new type, otherwise it infinitely recurses and panics.
type resource Resource
var res resource
if err := json.Unmarshal(data, &res); err != nil {
return err
}

r.Service = res.Service
r.Name = res.Name
r.Type = res.Type
return nil
}

// MarshalJSON specializes the Resource marshalling to handle the case where the
// value is a string instead of a map. See the comment above on RawPath for why this
// needs to be handled.
func (r *Resource) MarshalJSON() ([]byte, error) {
// If RawPath is set, use that as the whole value.
if r.RawPath != "" {
return []byte(fmt.Sprintf("%q", r.RawPath)), nil
}

// Otherwise, accept whatever the result of the normal marshal would be.
res := *r
b, err := json.Marshal(res)
if err != nil {
return nil, err
}
return b, nil
}

type contextKey string

// GCFContextKey satisfies an interface to be able to use contextKey to read
// metadata from a Cloud Functions context.Context.
//
// Be careful making changes to this function. See FromContext.
func (k contextKey) GCFContextKey() string {
return string(k)
}

const metadataContextKey = contextKey("metadata")

// FromContext extracts the Metadata from the Context, if present.
func FromContext(ctx context.Context) (*Metadata, error) {
if ctx == nil {
return nil, errors.New("nil ctx")
}
// The original JSON is inserted by the Cloud Functions worker. So, the
// format must not change, or the message may fail to unmarshal. We use
// JSON as a common format between the worker and this package to ensure
// this package can be updated independently from the worker. The contextKey
// type and the metadataContextKey value use an interface to avoid using
// a built-in type as a context key (which is easy to have collisions with).
// If we need another value to be stored in the context, we can use a new
// key or interface and avoid needing to change this one. Similarly, if we
// need to change the format of the message, we should add an additional key
// to keep backward compatibility.
b, ok := ctx.Value(metadataContextKey).(json.RawMessage)
if !ok {
return nil, errors.New("unable to find metadata")
}
meta := &Metadata{}
if err := json.Unmarshal(b, meta); err != nil {
return nil, fmt.Errorf("json.Unmarshal: %v", err)
}
return meta, nil
}

// NewContext returns a new Context carrying m. If m is nil, NewContext returns
// ctx. NewContext is only used for writing tests which rely on Metadata.
func NewContext(ctx context.Context, m *Metadata) context.Context {
if m == nil {
return ctx
}
b, err := json.Marshal(m)
if err != nil {
return ctx
}
return context.WithValue(ctx, metadataContextKey, json.RawMessage(b))
}

0 comments on commit f5c1abd

Please sign in to comment.