client/validate.go (229 lines of code) (raw):
// Copyright 2020 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
//
// https://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 main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"reflect"
"github.com/GoogleCloudPlatform/functions-framework-conformance/events"
"github.com/google/go-cmp/cmp"
)
type validatorParams struct {
useBuildpacks bool
validateMapping bool
runCmd string
outputFile string
source string
target string
runtime string
runtimeVersion string
builderURL string
tag string
functionSignature string
declarativeSignature string
validateConcurrency bool
envs []string
}
type validator struct {
funcServer functionServer
validateMapping bool
validateConcurrency bool
functionSignature string
declarativeSignature string
functionOutputFile string
stdoutFile string
stderrFile string
}
func newValidator(params validatorParams) *validator {
v := validator{
validateMapping: params.validateMapping,
validateConcurrency: params.validateConcurrency,
functionSignature: params.functionSignature,
declarativeSignature: params.declarativeSignature,
functionOutputFile: params.outputFile,
stdoutFile: defaultStdoutFile,
stderrFile: defaultStderrFile,
}
if !params.useBuildpacks {
v.funcServer = &localFunctionServer{
cmd: params.runCmd,
envs: params.envs,
}
return &v
}
if params.functionSignature == "legacyevent" {
params.functionSignature = "event"
}
v.funcServer = &buildpacksFunctionServer{
source: params.source,
target: params.target,
runtime: params.runtime,
runtimeVersion: params.runtimeVersion,
tag: params.tag,
funcType: params.functionSignature,
envs: params.envs,
builderURL: params.builderURL,
}
return &v
}
func (v validator) runValidation() error {
log.Printf("Validating for %s...", *functionSignature)
shutdown, err := v.funcServer.Start(v.stdoutFile, v.stderrFile, v.functionOutputFile)
if err != nil {
return v.errorWithLogsf("unable to start server: %v", err)
}
if shutdown == nil {
shutdown = func() {}
}
if err := v.validate("http://localhost:8080"); err != nil {
// shutdown to ensure all the logs are flushed
shutdown()
return v.errorWithLogsf("validation failure: %v", err)
}
shutdown()
return nil
}
func (v validator) errorWithLogsf(errorFmt string, paramsFmts ...interface{}) error {
logs, readErr := v.readLogs()
if readErr != nil {
logs = readErr.Error()
}
return fmt.Errorf("%s\nServer logs: %s", fmt.Sprintf(errorFmt, paramsFmts...), logs)
}
func (v validator) readLogs() (string, error) {
stdout, err := ioutil.ReadFile(v.stdoutFile)
if err != nil {
return "", fmt.Errorf("could not read stdout file %q: %w", v.stdoutFile, err)
}
stderr, err := ioutil.ReadFile(v.stderrFile)
if err != nil {
return "", fmt.Errorf("could not read stderr file %q: %w", v.stdoutFile, err)
}
return fmt.Sprintf("\n[%s]: '%s'\n[%s]: '%s'", v.stdoutFile, stdout, v.stderrFile, stderr), nil
}
// The HTTP function should copy the contents of the request into the response.
func (v validator) validateHTTP(url string) error {
type test struct {
Res string `json:"res"`
}
want := test{Res: "PASS"}
req, err := json.Marshal(want)
if err != nil {
return fmt.Errorf("failed to marshal json: %v", err)
}
if _, err := sendHTTP(url, req); err != nil {
return fmt.Errorf("failed to get response from HTTP function: %v", err)
}
output, err := v.funcServer.OutputFile()
if err != nil {
return fmt.Errorf("reading output file from HTTP function: %v", err)
}
got := test{}
if err = json.Unmarshal(output, &got); err != nil {
return fmt.Errorf("failed to unmarshal function output JSON: %v, function output: %q", err, output)
}
if !cmp.Equal(got, want) {
return fmt.Errorf("unexpected HTTP output data (format does not matter), got: %s, want: %s", output, req)
}
return nil
}
// The Typed function should echo the request object in the "payload" field of the response.
func (v validator) validateTyped(url string) error {
type request struct {
Message string `json:"message"`
}
req := request{
Message: "Hello world!",
}
reqJson, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("failed to marshal json: %v", err)
}
body, err := sendHTTP(url, reqJson)
if err != nil {
return fmt.Errorf("failed to get response from HTTP function: %v", err)
}
type response struct {
Payload request `json:"payload"`
}
var resJson response
if err := json.Unmarshal(body, &resJson); err != nil {
return fmt.Errorf("failed to unmarshal function output JSON: %v, function output: %q", err, string(body))
}
if !reflect.DeepEqual(resJson.Payload, req) {
return fmt.Errorf("Got response.Payload = %v, wanted %v", resJson.Payload, req)
}
return nil
}
func (v validator) validateEvents(url string, inputType, outputType events.EventType) error {
eventNames, err := events.EventNames(inputType)
if err != nil {
return err
}
vis := []*events.ValidationInfo{}
for _, name := range eventNames {
input := events.InputData(name, inputType)
if input == nil {
return fmt.Errorf("no input data for event %q", name)
}
err = send(url, inputType, input)
if err != nil {
return fmt.Errorf("failed to get response from function for %q: %v", name, err)
}
output, err := v.funcServer.OutputFile()
if err != nil {
return fmt.Errorf("reading output file from function for %q: %v", name, err)
}
if vi := events.ValidateEvent(name, inputType, outputType, output); vi != nil {
vis = append(vis, vi)
}
}
logStr, err := events.PrintValidationInfos(vis)
log.Println(logStr)
return err
}
func (v validator) validate(url string) error {
if v.validateConcurrency {
return validateConcurrency(url, v.declarativeSignature)
}
switch v.declarativeSignature {
case "http":
// Validate HTTP signature, if provided
log.Printf("HTTP validation started...")
if err := v.validateHTTP(url); err != nil {
return err
}
log.Printf("HTTP validation passed!")
return nil
case "typed":
// Validate a typed declarartive function signature
log.Printf("Typed validation started...")
if err := v.validateTyped(url); err != nil {
return err
}
log.Printf("Typed validation passed!")
return nil
case "cloudevent":
// Validate CloudEvent signature, if provided
log.Printf("CloudEvent validation with CloudEvent requests...")
if err := v.validateEvents(url, events.CloudEvent, events.CloudEvent); err != nil {
return err
}
if v.validateMapping {
log.Printf("CloudEvent validation with legacy event requests...")
if err := v.validateEvents(url, events.LegacyEvent, events.CloudEvent); err != nil {
return err
}
}
log.Printf("CloudEvent validation passed!")
return nil
case "legacyevent":
// Validate legacy event signature, if provided
log.Printf("Legacy event validation with legacy event requests...")
if err := v.validateEvents(url, events.LegacyEvent, events.LegacyEvent); err != nil {
return err
}
if v.validateMapping {
log.Printf("Legacy event validation with CloudEvent requests...")
if err := v.validateEvents(url, events.CloudEvent, events.LegacyEvent); err != nil {
return err
}
}
log.Printf("Legacy event validation passed!")
return nil
}
return fmt.Errorf("expected --declarative-type to be one of 'http', 'cloudevent', 'legacyevent', or 'typed' got %q", v.declarativeSignature)
}