blob: dc09e0a9374aa2984da8237fab49ac159fe74353 [file] [log] [blame]
// Copyright 2016 The LUCI Authors.
//
// 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 main
import (
"io/ioutil"
"path/filepath"
"strings"
"github.com/luci/luci-go/common/errors"
"github.com/luci/luci-go/deploytool/api/deploy"
"gopkg.in/yaml.v2"
)
// gaeAppYAML is a YAML struct for an AppEngine "app.yaml".
type gaeAppYAML struct {
Module string `yaml:"module,omitempty"`
Runtime string `yaml:"runtime,omitempty"`
ThreadSafe *bool `yaml:"threadsafe,omitempty"`
APIVersion interface{} `yaml:"api_version,omitempty"`
VM bool `yaml:"vm,omitempty"`
InstanceClass string `yaml:"instance_class,omitempty"`
AutomaticScaling *gaeAppAutomaticScalingParams `yaml:"automatic_scaling,omitempty"`
BasicScaling *gaeAppBasicScalingParams `yaml:"basic_scaling,omitempty"`
ManualScaling *gaeAppManualScalingParams `yaml:"manual_scaling,omitempty"`
BetaSettings *gaeAppYAMLBetaSettings `yaml:"beta_settings,omitempty"`
Handlers []*gaeAppYAMLHandler `yaml:"handlers,omitempty"`
}
// gaeAppAutomaticScalingParams is the combined set of scaling parameters for
// automatic scaling.
type gaeAppAutomaticScalingParams struct {
MinIdleInstances *int `yaml:"min_idle_instances,omitempty"`
MaxIdleInstances *int `yaml:"max_idle_instances,omitempty"`
MinPendingLatency string `yaml:"min_pending_latency,omitempty"`
MaxPendingLatency string `yaml:"max_pending_latency,omitempty"`
MaxConcurrentRequests *int `yaml:"max_concurrent_requests,omitempty"`
}
// gaeAppBasicScalingParams is the combined set of scaling parameters for
// basic scaling.
type gaeAppBasicScalingParams struct {
IdleTimeout string `yaml:"idle_timeout,omitempty"`
MaxInstances int `yaml:"max_instances"`
}
// gaeAppManualScalingParams is the combined set of scaling parameters for
// manual scaling.
type gaeAppManualScalingParams struct {
Instances int `yaml:"instances"`
}
// gaeAppYAMLBuiltIns is the set of builtin options that can be enabled. Any
// enabled option will have the value, "on".
type gaeAppYAMLBuiltIns struct {
AppStats string `yaml:"appstats,omitempty"`
RemoteAPI string `yaml:"remote_api,omitempty"`
Deferred string `yaml:"deferred,omitempty"`
}
type gaeLibrary struct {
Name string `yaml:"name"`
Version string `yaml:"version,omitempty"`
}
// gaeAppYAMLBetaSettings is a YAML struct for an AppEngine "app.yaml"
// beta settings.
type gaeAppYAMLBetaSettings struct {
ServiceAccountScopes string `yaml:"service_account_scopes,omitempty"`
}
// gaeAppYAMLHAndler is a YAML struct for an AppEngine "app.yaml"
// handler entry.
type gaeAppYAMLHandler struct {
URL string `yaml:"url"`
Script string `yaml:"script,omitempty"`
Secure string `yaml:"secure,omitempty"`
Login string `yaml:"login,omitempty"`
StaticDir string `yaml:"static_dir,omitempty"`
StaticFiles string `yaml:"static_files,omitempty"`
Upload string `yaml:"upload,omitempty"`
Expiration string `yaml:"expiration,omitempty"`
HTTPHeaders map[string]string `yaml:"http_headers,omitempty"`
}
// gaeCronYAML is a YAML struct for an AppEngine "cron.yaml".
type gaeCronYAML struct {
Cron []*gaeCronYAMLEntry `yaml:"cron,omitempty"`
}
// gaeCronYAMLEntry is a YAML struct for an AppEngine "cron.yaml" entry.
type gaeCronYAMLEntry struct {
Description string `yaml:"description,omitempty"`
URL string `yaml:"url"`
Schedule string `yaml:"schedule"`
Target string `yaml:"target,omitempty"`
}
// gaeIndexYAML is a YAML struct for an AppEngine "index.yaml".
type gaeIndexYAML struct {
Indexes []*gaeIndexYAMLEntry `yaml:"indexes,omitempty"`
}
// gaeIndexYAMLEntry is a YAML struct for an AppEngine "index.yaml" entry.
type gaeIndexYAMLEntry struct {
Kind string `yaml:"kind"`
Ancestor interface{} `yaml:"ancestor,omitempty"`
Properties []*gaeIndexYAMLEntryProperty `yaml:"properties"`
}
// gaeIndexYAMLEntryProperty is a YAML struct for an AppEngine "index.yaml"
// entry's property.
type gaeIndexYAMLEntryProperty struct {
Name string `yaml:"name"`
Direction string `yaml:"direction,omitempty"`
}
// gaeDispatchYAML is a YAML struct for an AppEngine "dispatch.yaml".
type gaeDispatchYAML struct {
Dispatch []*gaeDispatchYAMLEntry `yaml:"dispatch,omitempty"`
}
// gaeDispatchYAMLEntry is a YAML struct for an AppEngine "dispatch.yaml" entry.
type gaeDispatchYAMLEntry struct {
Module string `yaml:"module"`
URL string `yaml:"url"`
}
// gaeQueueYAML is a YAML struct for an AppEngine "queue.yaml".
type gaeQueueYAML struct {
Queue []*gaeQueueYAMLEntry `yaml:"queue,omitempty"`
}
// gaeQueueYAMLEntry is a YAML struct for an AppEngine "queue.yaml" entry.
type gaeQueueYAMLEntry struct {
Name string `yaml:"name"`
Mode string `yaml:"mode"`
Rate string `yaml:"rate,omitempty"`
BucketSize int `yaml:"bucket_size,omitempty"`
MaxConcurrentRequests int `yaml:"max_concurrent_requests,omitempty"`
Target string `yaml:"target,omitempty"`
RetryParameters *gaeQueueYAMLEntryRetryParameters `yaml:"retry_parameters,omitempty"`
}
// gaeQueueYAMLEntryRetryParameters is a YAML struct for an AppEngine
// "queue.yaml" entry push queue retry parameters.
type gaeQueueYAMLEntryRetryParameters struct {
TaskAgeLimit string `yaml:"task_age_limit,omitempty"`
MinBackoffSeconds int `yaml:"min_backoff_seconds,omitempty"`
MaxBackoffSeconds int `yaml:"max_backoff_seconds,omitempty"`
MaxDoublings int `yaml:"max_doublings,omitempty"`
}
func gaeBuildAppYAML(aem *deploy.AppEngineModule, staticMap map[*deploy.BuildPath]string) (*gaeAppYAML, error) {
appYAML := gaeAppYAML{
Module: aem.ModuleName,
}
var (
defaultScript = ""
isStub = false
)
switch aem.GetRuntime().(type) {
case *deploy.AppEngineModule_GoModule_:
appYAML.Runtime = "go"
if aem.GetManagedVm() != nil {
appYAML.APIVersion = 1
appYAML.VM = true
} else {
appYAML.APIVersion = "go1"
}
defaultScript = "_go_app"
case *deploy.AppEngineModule_StaticModule_:
// A static module presents itself as an empty Python AppEngine module. This
// doesn't actually need any Python code to support it.
appYAML.Runtime = "python27"
appYAML.APIVersion = "1"
threadSafe := true
appYAML.ThreadSafe = &threadSafe
isStub = true
default:
return nil, errors.Reason("unsupported runtime %q", aem.Runtime).Err()
}
// Classic / Managed VM Properties
switch p := aem.GetParams().(type) {
case *deploy.AppEngineModule_Classic:
var icPrefix string
switch sc := p.Classic.GetScaling().(type) {
case nil:
// (Automatic, default).
icPrefix = "F"
case *deploy.AppEngineModule_ClassicParams_AutomaticScaling_:
as := sc.AutomaticScaling
appYAML.AutomaticScaling = &gaeAppAutomaticScalingParams{
MinIdleInstances: intPtrOrNilIfZero(as.MinIdleInstances),
MaxIdleInstances: intPtrOrNilIfZero(as.MaxIdleInstances),
MinPendingLatency: as.MinPendingLatency.AppYAMLString(),
MaxPendingLatency: as.MaxPendingLatency.AppYAMLString(),
MaxConcurrentRequests: intPtrOrNilIfZero(as.MaxConcurrentRequests),
}
icPrefix = "F"
case *deploy.AppEngineModule_ClassicParams_BasicScaling_:
bs := sc.BasicScaling
appYAML.BasicScaling = &gaeAppBasicScalingParams{
IdleTimeout: bs.IdleTimeout.AppYAMLString(),
MaxInstances: int(bs.MaxInstances),
}
icPrefix = "B"
case *deploy.AppEngineModule_ClassicParams_ManualScaling_:
ms := sc.ManualScaling
appYAML.ManualScaling = &gaeAppManualScalingParams{
Instances: int(ms.Instances),
}
icPrefix = "B"
default:
return nil, errors.Reason("unknown scaling type %T", sc).Err()
}
appYAML.InstanceClass = p.Classic.InstanceClass.AppYAMLString(icPrefix)
case *deploy.AppEngineModule_ManagedVm:
if scopes := p.ManagedVm.Scopes; len(scopes) > 0 {
appYAML.BetaSettings = &gaeAppYAMLBetaSettings{
ServiceAccountScopes: strings.Join(scopes, ","),
}
}
}
if handlerSet := aem.Handlers; handlerSet != nil {
appYAML.Handlers = make([]*gaeAppYAMLHandler, len(handlerSet.Handler))
for i, handler := range handlerSet.Handler {
entry := gaeAppYAMLHandler{
URL: handler.Url,
Secure: handler.Secure.AppYAMLString(),
Login: handler.Login.AppYAMLString(),
}
// Fill in static dir/file paths.
switch t := handler.GetContent().(type) {
case nil:
entry.Script = defaultScript
case *deploy.AppEngineModule_Handler_Script:
entry.Script = t.Script
if entry.Script == "" {
entry.Script = defaultScript
}
case *deploy.AppEngineModule_Handler_StaticBuildDir:
entry.StaticDir = staticMap[t.StaticBuildDir]
case *deploy.AppEngineModule_Handler_StaticFiles_:
sf := t.StaticFiles
relDir := staticMap[sf.GetBuild()]
entry.Upload = filepath.Join(relDir, sf.Upload)
entry.StaticFiles = filepath.Join(relDir, sf.UrlMap)
default:
return nil, errors.Reason("don't know how to handle content %T", t).
InternalReason("url(%q)", handler.Url).Err()
}
if entry.Script != "" && isStub {
return nil, errors.Reason("stub module cannot have entry script").Err()
}
appYAML.Handlers[i] = &entry
}
}
return &appYAML, nil
}
func gaeBuildCronYAML(cp *layoutDeploymentCloudProject) *gaeCronYAML {
var cronYAML gaeCronYAML
for _, m := range cp.appEngineModules {
for _, cron := range m.resources.Cron {
cronYAML.Cron = append(cronYAML.Cron, &gaeCronYAMLEntry{
Description: cron.Description,
URL: cron.Url,
Schedule: cron.Schedule,
Target: m.ModuleName,
})
}
}
return &cronYAML
}
func gaeBuildIndexYAML(cp *layoutDeploymentCloudProject) *gaeIndexYAML {
var indexYAML gaeIndexYAML
for _, index := range cp.resources.Index {
entry := gaeIndexYAMLEntry{
Kind: index.Kind,
Properties: make([]*gaeIndexYAMLEntryProperty, len(index.Property)),
Ancestor: index.Ancestor,
}
for i, prop := range index.Property {
entry.Properties[i] = &gaeIndexYAMLEntryProperty{
Name: prop.Name,
Direction: prop.Direction.AppYAMLString(),
}
}
indexYAML.Indexes = append(indexYAML.Indexes, &entry)
}
return &indexYAML
}
func gaeBuildDispatchYAML(cp *layoutDeploymentCloudProject) (*gaeDispatchYAML, error) {
var dispatchYAML gaeDispatchYAML
for _, m := range cp.appEngineModules {
for _, url := range m.resources.Dispatch {
dispatchYAML.Dispatch = append(dispatchYAML.Dispatch, &gaeDispatchYAMLEntry{
Module: m.ModuleName,
URL: url,
})
}
}
if count := len(dispatchYAML.Dispatch); count > 10 {
return nil, errors.Reason("dispatch file has more than 10 routing rules (%d)", count).Err()
}
return &dispatchYAML, nil
}
func gaeBuildQueueYAML(cp *layoutDeploymentCloudProject) (*gaeQueueYAML, error) {
var queueYAML gaeQueueYAML
for _, m := range cp.appEngineModules {
for _, queue := range m.resources.TaskQueue {
entry := gaeQueueYAMLEntry{
Name: queue.Name,
}
switch t := queue.GetType().(type) {
case *deploy.AppEngineResources_TaskQueue_Push_:
push := t.Push
entry.Target = m.ModuleName
entry.Mode = "push"
entry.Rate = push.Rate
entry.BucketSize = int(push.BucketSize)
entry.MaxConcurrentRequests = int(push.MaxConcurrentRequests)
entry.RetryParameters = &gaeQueueYAMLEntryRetryParameters{
TaskAgeLimit: push.RetryTaskAgeLimit,
MinBackoffSeconds: int(push.RetryMinBackoffSeconds),
MaxBackoffSeconds: int(push.RetryMaxBackoffSeconds),
MaxDoublings: int(push.RetryMaxDoublings),
}
default:
return nil, errors.Reason("unknown task queue type %T", t).Err()
}
queueYAML.Queue = append(queueYAML.Queue, &entry)
}
}
return &queueYAML, nil
}
// loadIndexYAMLResource loads an AppEngine "index.yaml" file from the specified
// filesystem path and translates it into AppEngineResources Index entries.
func loadIndexYAMLResource(path string) (*deploy.AppEngineResources, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, errors.Annotate(err, "failed to load [%s]", path).Err()
}
var indexYAML gaeIndexYAML
if err := yaml.Unmarshal(data, &indexYAML); err != nil {
return nil, errors.Annotate(err, "could not load 'index.yaml' from [%s]", path).Err()
}
var res deploy.AppEngineResources
if len(indexYAML.Indexes) > 0 {
res.Index = make([]*deploy.AppEngineResources_Index, len(indexYAML.Indexes))
for i, idx := range indexYAML.Indexes {
entry := deploy.AppEngineResources_Index{
Kind: idx.Kind,
Ancestor: yesOrBoolToBool(idx.Ancestor),
Property: make([]*deploy.AppEngineResources_Index_Property, len(idx.Properties)),
}
for pidx, idxProp := range idx.Properties {
dir, err := deploy.IndexDirectionFromAppYAMLString(idxProp.Direction)
if err != nil {
return nil, errors.Annotate(err, "could not identify direction for entry").
InternalReason("index(%d)/propertyIndex(%d)", i, pidx).Err()
}
entry.Property[pidx] = &deploy.AppEngineResources_Index_Property{
Name: idxProp.Name,
Direction: dir,
}
}
res.Index[i] = &entry
}
}
return &res, nil
}
func intPtrOrNilIfZero(v uint32) *int {
if v == 0 {
return nil
}
value := int(v)
return &value
}
func yesOrBoolToBool(v interface{}) bool {
switch t := v.(type) {
case string:
return (t == "yes")
case bool:
return t
default:
return false
}
}