pkg/firebase/publisher/publisher.go (125 lines of code) (raw):
// Copyright 2023 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 publisher provides basic functionality to coalesce user and framework adapter defined
// variables.
package publisher
import (
"fmt"
"log"
"os"
"path/filepath"
"gopkg.in/yaml.v2"
"github.com/GoogleCloudPlatform/buildpacks/pkg/firebase/apphostingschema"
"github.com/GoogleCloudPlatform/buildpacks/pkg/firebase/bundleschema"
)
// buildSchema is the internal Publisher representation of the final build settings that will
// ultimately be converted into an updateBuildRequest.
type buildSchema struct {
RunConfig *apphostingschema.RunConfig `yaml:"runConfig,omitempty"`
Env []apphostingschema.EnvironmentVariable `yaml:"env,omitempty"`
Metadata *bundleschema.Metadata `yaml:"metadata,omitempty"`
}
var (
defaultCPU int32 = 1 // From https://cloud.google.com/run/docs/configuring/services/cpu.
defaultMemory int32 = 512 // From https://cloud.google.com/run/docs/configuring/services/memory-limits.
defaultConcurrency int32 = 80 // From https://cloud.google.com/run/docs/about-concurrency.
defaultMaxInstances int32 = 100 // From https://cloud.google.com/run/docs/configuring/max-instances.
defaultMinInstances int32 = 0 // From https://cloud.google.com/run/docs/configuring/min-instances.
)
// Write the given build schema to the specified path, used to output the final arguments to BuildStepOutputs[]
func writeToFile(buildSchema buildSchema, outputFilePath string) error {
fileData, err := yaml.Marshal(&buildSchema)
if err != nil {
return fmt.Errorf("converting struct to YAML: %w", err)
}
log.Printf("Final build schema:\n%v\n. Note that any unset runConfig fields will be set to reasonable default values.", string(fileData))
err = os.MkdirAll(filepath.Dir(outputFilePath), os.ModeDir)
if err != nil {
return fmt.Errorf("creating parent directory %q: %w", outputFilePath, err)
}
file, err := os.Create(outputFilePath)
if err != nil {
return fmt.Errorf("creating build schema file: %w", err)
}
defer file.Close()
_, err = file.Write(fileData)
if err != nil {
return fmt.Errorf("writing build schema data to file: %w", err)
}
return nil
}
func toBuildSchema(appHostingSchema apphostingschema.AppHostingSchema, bundleSchema bundleschema.BundleSchema) buildSchema {
buildSchema := buildSchema{}
// Merge RunConfig fields from apphosting.yaml and bundle.yaml, Control Plane will set defaults for any unset fields.
buildSchema.RunConfig = mergeRunConfig(appHostingSchema.RunConfig, bundleSchema.RunConfig)
// Copy Metadata fields from bundle.yaml.
buildSchema.Metadata = bundleSchema.Metadata
// Merge Env fields from bundle.yaml and apphosting.yaml together.
buildSchema.Env = mergeEnvironmentVariables(appHostingSchema.Env, bundleSchema.RunConfig.EnvironmentVariables)
return buildSchema
}
// mergeEnvironmentVariables merges the environment variables from apphosting.yaml and bundle.yaml.
// If there is a conflict between the environment variables, use the value/secret from apphosting.yaml.
func mergeEnvironmentVariables(aevs []apphostingschema.EnvironmentVariable, bevs []bundleschema.EnvironmentVariable) []apphostingschema.EnvironmentVariable {
merged := aevs
varByName := make(map[string]apphostingschema.EnvironmentVariable)
for _, apphostingEv := range aevs {
varByName[apphostingEv.Variable] = apphostingEv
}
for _, bundleEv := range bevs {
apphostingEv, found := varByName[bundleEv.Variable]
if found && isEnvAvailabilityOverlap(apphostingEv.Availability, bundleEv.Availability) {
log.Printf("Apphosting.yaml environment variable %v conflicts with bundle.yaml environment variable\n", bundleEv.Variable)
log.Printf("Using an environment variable value or secret from apphosting.yaml\n")
} else {
var ev apphostingschema.EnvironmentVariable = apphostingschema.EnvironmentVariable(bundleEv)
log.Printf("Adding environment variable %v from bundle.yaml\n", bundleEv.Variable)
// merge bundleEv in if no conflict
merged = append(merged, ev)
}
}
return merged
}
// mergeRunConfig merges the RunConfig from apphosting.yaml and bundle.yaml.
// If there is a conflict between the fields, use the value from apphosting.yaml.
func mergeRunConfig(arc apphostingschema.RunConfig, brc bundleschema.RunConfig) *apphostingschema.RunConfig {
merged := &apphostingschema.RunConfig{
CPU: brc.CPU,
MemoryMiB: brc.MemoryMiB,
Concurrency: brc.Concurrency,
MinInstances: brc.MinInstances,
MaxInstances: brc.MaxInstances,
}
if arc.CPU != nil {
merged.CPU = arc.CPU
}
if arc.MemoryMiB != nil {
merged.MemoryMiB = arc.MemoryMiB
}
if arc.Concurrency != nil {
merged.Concurrency = arc.Concurrency
}
if arc.MinInstances != nil {
merged.MinInstances = arc.MinInstances
}
if arc.MaxInstances != nil {
merged.MaxInstances = arc.MaxInstances
}
if arc.VpcAccess != nil {
merged.VpcAccess = arc.VpcAccess
}
return merged
}
func isEnvAvailabilityOverlap(appHostingAvailability, bundleAvailability []string) bool {
availabilityByName := make(map[string]bool)
for _, av := range appHostingAvailability {
availabilityByName[av] = true
}
for _, av := range bundleAvailability {
if availabilityByName[av] {
return true
}
}
return false
}
// Publish takes in the path to various required files such as apphosting.yaml, bundle.yaml, and
// other files (tbd) and merges them into one output that describes the desired Backend Service
// configuration before pushing this information to the control plane.
func Publish(appHostingYAMLPath string, bundleYAMLPath string, outputFilePath string) error {
appHostingSchema, err := apphostingschema.ReadAndValidateFromFile(appHostingYAMLPath)
if err != nil {
return err
}
bundleSchema, err := bundleschema.ReadAndValidateFromFile(bundleYAMLPath)
if err != nil {
return err
}
buildSchema := toBuildSchema(appHostingSchema, bundleSchema)
err = writeToFile(buildSchema, outputFilePath)
if err != nil {
return err
}
return nil
}