wstl1/mapping_engine/cloudfunction/cloudfunction.go (90 lines of code) (raw):
// 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 cloudfunction contains methods for creating and calling projectors containing Google cloud functions.
package cloudfunction
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strings"
"github.com/GoogleCloudPlatform/healthcare-data-harmonization/mapping_engine/types" /* copybara-comment: types */
"github.com/GoogleCloudPlatform/healthcare-data-harmonization/mapping_engine/util/jsonutil" /* copybara-comment: jsonutil */
errs "github.com/GoogleCloudPlatform/healthcare-data-harmonization/mapping_engine/errors" /* copybara-comment: errors */
httppb "github.com/GoogleCloudPlatform/healthcare-data-harmonization/mapping_engine/proto" /* copybara-comment: http_go_proto */
)
const (
cloudFunctionURLPattern = `^http(s)?://.*$`
)
// LoadCloudFunctionProjectors registers all defined Google cloud functions.
func LoadCloudFunctionProjectors(r *types.Registry, cloudfuncs []*httppb.CloudFunction) error {
for _, cf := range cloudfuncs {
proj, err := FromCloudFunction(cf)
if err != nil {
return fmt.Errorf("error loading cloud function %s: %v", cf.Name, err)
}
if err = r.RegisterProjector(cf.Name, proj); err != nil {
return fmt.Errorf("error registering projector %s: %v", cf.Name, err)
}
}
return nil
}
// FromCloudFunction creates a projector from a Google cloud function.
// Currently only supports HTTP trigger to call cloud function.
func FromCloudFunction(cf *httppb.CloudFunction) (types.Projector, error) {
if err := validateCloudFunction(cf); err != nil {
return nil, fmt.Errorf("invalid Google cloud function definition: %v", err)
}
return func(metaArgs []jsonutil.JSONMetaNode, pctx *types.Context) (jsonutil.JSONToken, error) {
errLocation := errs.FnLocationf("Cloud Function Preamble %q", cf.Name)
args := make([]jsonutil.JSONToken, len(metaArgs))
for i, metaArg := range metaArgs {
node, err := jsonutil.NodeToToken(metaArg)
if err != nil {
return nil, errs.Wrap(errLocation, fmt.Errorf("error converting args: %v", err))
}
args[i] = node
}
var body []byte
var err error
if len(args) == 1 {
body, err = json.Marshal(args[0])
} else {
body, err = json.Marshal(args)
}
if err != nil {
return nil, errs.Wrap(errLocation, fmt.Errorf("error marshaling arguments into a JSON object: %v", err))
}
errLocation = errs.FnLocationf("Cloud Function %q", cf.Name)
resp, err := http.Post(cf.RequestUrl, "application/json", bytes.NewBuffer(body))
if err != nil {
return nil, errs.Wrap(errLocation, fmt.Errorf("cloud function request failed due to: %v", err))
}
if resp.StatusCode != http.StatusOK {
message, _ := ioutil.ReadAll(resp.Body)
return nil, errs.Wrap(errLocation, fmt.Errorf("error http response status code: %v, error message: %s", resp.Status, message))
}
defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errs.Wrap(errLocation, fmt.Errorf("error reading http response body: %v", err))
}
if len(bytes) == 0 {
return nil, errs.Wrap(errLocation, fmt.Errorf("error empty http response body"))
}
val, err := jsonutil.UnmarshalJSON(json.RawMessage(bytes))
if err != nil {
return nil, errs.Wrap(errLocation, fmt.Errorf("error unmarshal http response body into JSONToken: %v", err))
}
return val, nil
}, nil
}
func validateCloudFunction(cf *httppb.CloudFunction) error {
if !strings.HasPrefix(cf.Name, "@") {
return fmt.Errorf("invalid cloud function name, should start with '@'")
}
m, err := regexp.Compile(cloudFunctionURLPattern)
if err != nil {
return fmt.Errorf("invalid cloud function url pattern: %v", err)
}
if !m.MatchString(cf.RequestUrl) {
return fmt.Errorf("error cloud function request url: %s", cf.RequestUrl)
}
return nil
}