blob: 6bb707d7eb2001f78bf52a57ec182d534e11e042 [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"
"os"
"path/filepath"
"sort"
"strings"
"github.com/luci/luci-go/common/errors"
log "github.com/luci/luci-go/common/logging"
"github.com/luci/luci-go/deploytool/api/deploy"
"github.com/luci/luci-go/deploytool/managedfs"
"golang.org/x/net/context"
)
const defaultLayoutFilename = "layout.cfg"
type layoutSource struct {
*deploy.FrozenLayout_Source
sg *layoutSourceGroup
title title
}
func (s *layoutSource) String() string { return joinPath(s.sg.title, s.title) }
func (s *layoutSource) checkoutPath() string {
return s.sg.layout.workingPathTo(s.Relpath)
}
// pathTo resolves a path, p, specified in a directory with source-root-relative
// path relpath.
//
// - If the path begins with "/", it is taken relative to the source root.
// - If the path does not begin with "/", it is taken relative to "relpath"
// within the source root (e.g., "relpath" is prepended to it).
func (s *layoutSource) pathTo(p, relpath string) string {
if s.Relpath == "" {
panic(errors.Reason("source %q is not checked out", s).Err())
}
// If this is absolute, take it relative to source root.
if strings.HasPrefix(p, "/") {
relpath = ""
}
// Convert "p" to a source-relative absolute path.
p = strings.Trim(p, "/")
if relpath != "" {
relpath = strings.Trim(relpath, "/")
p = relpath + "/" + p
}
return deployToNative(s.checkoutPath(), p)
}
// layoutSourceGroup is a group of named, associated Source entries.
type layoutSourceGroup struct {
*deploy.FrozenLayout_SourceGroup
// layout is the owning layout.
layout *deployLayout
title title
sources map[title]*layoutSource
}
func (sg *layoutSourceGroup) String() string { return string(sg.title) }
func (sg *layoutSourceGroup) allSources() []*layoutSource {
keys := make([]string, 0, len(sg.sources))
for key := range sg.sources {
keys = append(keys, string(key))
}
sort.Strings(keys)
srcs := make([]*layoutSource, len(keys))
for i, k := range keys {
srcs[i] = sg.sources[title(k)]
}
return srcs
}
type layoutApp struct {
*deploy.Application
title title
components map[title]*layoutAppComponent
}
type layoutAppComponent struct {
*deploy.Application_Component
proj *layoutApp
title title
}
type layoutDeployment struct {
*deploy.Deployment
l *deployLayout
title title
// app is the application that this deployment is bound to.
app *layoutApp
// sg is the source group that this deployment is bound to.
sg *layoutSourceGroup
// components is the set of deployed application components.
components map[title]*layoutDeploymentComponent
// componentNames is a list of keys in the components map, ordered by the
// order in which these components appeared in the protobuf.
componentNames []title
// cloudProject is the deployment cloud project, if one is specified.
cloudProject *layoutDeploymentCloudProject
}
func (d *layoutDeployment) String() string { return string(d.title) }
// substituteParams applies parameter substitution to the supplied string in a
// left-to-right manner.
func (d *layoutDeployment) substituteParams(vp *string) error {
if err := substitute(vp, d.Parameter); err != nil {
return errors.Annotate(err, "").Err()
}
return nil
}
type layoutDeploymentComponent struct {
deploy.Component
// reldir is the source-relative directory that relative paths in this
// component will reference. This will be the parent directory of the
// component's configuration file.
reldir string
// dep is the deployment that this component belongs to.
dep *layoutDeployment
// comp is the Component definition.
comp *layoutAppComponent
// sources is the set of sources required to build this Component. This will
// always have at least one entry: the source where this Component is located.
sources []*layoutSource
// buildDirs is a map of build directory keys to their on-disk paths generated
// for them. This is populated during `buildComponent()`.
buildDirs map[string]string
// buildPathMap is a map of the resolved on-disk paths for a given BuildPath.
buildPathMap map[*deploy.BuildPath]string
// gkePod is the set of cluster-bound GKE pods that this component is deployed
// to.
gkePod *layoutDeploymentGKEPod
// gkePods is the set of pod/cluster bindings for this Component's pod.
gkePods []*layoutDeploymentGKEPodBinding
}
func (comp *layoutDeploymentComponent) String() string {
return joinPath(comp.dep.title, comp.comp.title)
}
func (comp *layoutDeploymentComponent) source() *layoutSource {
return comp.sources[0]
}
func (comp *layoutDeploymentComponent) pathTo(relpath string) string {
return comp.source().pathTo(relpath, comp.reldir)
}
func (comp *layoutDeploymentComponent) buildPath(bp *deploy.BuildPath) (string, error) {
if path, ok := comp.buildPathMap[bp]; ok {
return path, nil
}
return "", errors.Reason("no build path resolved for: %+v", bp).Err()
}
func (comp *layoutDeploymentComponent) loadSourceComponent(reg componentRegistrar) error {
if err := unmarshalTextProtobuf(comp.source().pathTo(comp.comp.Path, ""), &comp.Component); err != nil {
return errors.Annotate(err, "failed to load source component %q", comp).Err()
}
// Referenced build paths.
for i, p := range comp.BuildPath {
var msg deploy.Component_Build
if err := unmarshalTextProtobuf(comp.pathTo(p), &msg); err != nil {
return errors.Annotate(err, "failed to load component Build #%d from [%s]", i, p).Err()
}
comp.Build = append(comp.Build, &msg)
}
// Load any referenced data and normalize it for internal usage.
dep := comp.dep
switch t := comp.GetComponent().(type) {
case *deploy.Component_AppengineModule:
// Normalize AppEngine module:
// - Modules explicitly named "default" will have their ModuleName changed
// to the empty string.
// - All referenced path parameters will be loaded and appended onto their
// non-path members.
if dep.cloudProject == nil {
return errors.Reason("AppEngine module %q requires a cloud project", comp).Err()
}
aem := t.AppengineModule
if aem.ModuleName == "default" {
aem.ModuleName = ""
}
module := layoutDeploymentGAEModule{
AppEngineModule: aem,
comp: comp,
}
// Referenced handler paths.
for i, p := range aem.HandlerPath {
var msg deploy.AppEngineModule_HandlerSet
if err := unmarshalTextProtobuf(comp.pathTo(p), &msg); err != nil {
return errors.Annotate(err, "failed to load HandlerSet #%d for %q", i, comp).Err()
}
module.Handlers.Handler = append(module.Handlers.Handler, msg.Handler...)
}
// If the module specifies a direct "index.yaml" path, load index entries
// from there and translate them to resources.
if p := module.IndexYamlPath; p != "" {
path := module.comp.pathTo(p)
res, err := loadIndexYAMLResource(path)
if err != nil {
return errors.Annotate(err, "failed to load 'index.yaml' from [%s]", path).Err()
}
dep.cloudProject.appendResources(res, &module)
}
// Append GAE Resources.
if r := module.Resources; r != nil {
dep.cloudProject.appendResources(r, &module)
}
for i, p := range module.ResourcePath {
if err := comp.dep.substituteParams(&p); err != nil {
return errors.Annotate(err, "failed to substitute parameters for resource path").
InternalReason("path(%s)", p).Err()
}
var res deploy.AppEngineResources
if err := unmarshalTextProtobuf(comp.pathTo(p), &res); err != nil {
return errors.Annotate(err, "failed to load Resources #%d for %q", i, comp).
InternalReason("path(%s)", p).Err()
}
dep.cloudProject.appendResources(&res, &module)
}
// Add this module to our cloud project's AppEngine modules list.
dep.cloudProject.appEngineModules = append(dep.cloudProject.appEngineModules, &module)
if reg != nil {
reg.addGAEModule(&module)
}
case *deploy.Component_GkePod:
if len(comp.gkePods) == 0 {
return errors.Reason("GKE Container %q is not bound to a GKE cluster", comp).Err()
}
comp.gkePod = &layoutDeploymentGKEPod{
ContainerEnginePod: t.GkePod,
comp: comp,
}
// None of the labels may use our "deploytool" prefix.
var invalidLabels []string
for k := range comp.gkePod.KubePod.Labels {
if isKubeDeployToolKey(k) {
invalidLabels = append(invalidLabels, k)
}
}
if len(invalidLabels) > 0 {
sort.Strings(invalidLabels)
return errors.Reason("user-supplied labels may not use deploytool prefix").
InternalReason("labels(%v)", invalidLabels).Err()
}
for _, bp := range comp.gkePods {
bp.pod = comp.gkePod
if reg != nil {
reg.addGKEPod(bp)
}
}
}
return nil
}
// expandPaths iterates through the Component-defined directory fields and
// expands their paths into actual filesystem paths.
//
// This must be performed after the build instructions have been executed so
// that the "build_dir" map will be available if needed by a Component.
func (comp *layoutDeploymentComponent) expandPaths() error {
resolveBuildPath := func(bp *deploy.BuildPath) error {
if _, ok := comp.buildPathMap[bp]; ok {
// Already resolved.
return nil
}
var resolved string
if bp.DirKey != "" {
dir, ok := comp.buildDirs[bp.DirKey]
if !ok {
return errors.Reason("Invalid `dir_key` value: %q", bp.DirKey).Err()
}
resolved = deployToNative(dir, bp.Path)
} else {
resolved = comp.pathTo(bp.Path)
}
if comp.buildPathMap == nil {
comp.buildPathMap = make(map[*deploy.BuildPath]string)
}
comp.buildPathMap[bp] = resolved
return nil
}
switch t := comp.Component.Component.(type) {
case *deploy.Component_AppengineModule:
aem := t.AppengineModule
if aem.Handlers != nil {
for _, handler := range aem.Handlers.Handler {
switch c := handler.Content.(type) {
case *deploy.AppEngineModule_Handler_StaticFiles_:
sf := c.StaticFiles
switch bd := sf.BaseDir.(type) {
case *deploy.AppEngineModule_Handler_StaticFiles_Path:
// Normalize our static files directory to a BuildPath rooted at our
// source.
sf.BaseDir = &deploy.AppEngineModule_Handler_StaticFiles_Build{
Build: &deploy.BuildPath{Path: bd.Path},
}
if err := resolveBuildPath(sf.GetBuild()); err != nil {
return errors.Annotate(err, "").Err()
}
case *deploy.AppEngineModule_Handler_StaticFiles_Build:
if err := resolveBuildPath(bd.Build); err != nil {
return errors.Annotate(err, "").Err()
}
default:
return errors.Reason("unknown `base_dir` type %T", bd).Err()
}
case *deploy.AppEngineModule_Handler_StaticDir:
// Normalize our static directory (source-relative) to a BuildPath
// rooted at our source.
handler.Content = &deploy.AppEngineModule_Handler_StaticBuildDir{
StaticBuildDir: &deploy.BuildPath{Path: c.StaticDir},
}
if err := resolveBuildPath(handler.GetStaticBuildDir()); err != nil {
return errors.Annotate(err, "").Err()
}
case *deploy.AppEngineModule_Handler_StaticBuildDir:
if err := resolveBuildPath(c.StaticBuildDir); err != nil {
return errors.Annotate(err, "").Err()
}
}
}
}
case *deploy.Component_GkePod:
for _, container := range t.GkePod.KubePod.Container {
switch df := container.Dockerfile.(type) {
case *deploy.KubernetesPod_Container_Path:
// Convert to BuildPath.
container.Dockerfile = &deploy.KubernetesPod_Container_Build{
Build: &deploy.BuildPath{Path: df.Path},
}
if err := resolveBuildPath(container.GetBuild()); err != nil {
return errors.Annotate(err, "").Err()
}
case *deploy.KubernetesPod_Container_Build:
if err := resolveBuildPath(df.Build); err != nil {
return errors.Annotate(err, "").Err()
}
default:
return errors.Reason("unknown `dockerfile` type %T", df).Err()
}
}
}
return nil
}
// layoutDeploymentCloudProject tracks a cloud project element, as well as
// any cloud project configurations referenced by this Deployment's Components.
type layoutDeploymentCloudProject struct {
*deploy.Deployment_CloudProject
// dep is the Deployment that owns this cloud project.
dep *layoutDeployment
// gkeCluster is a map of GKE cluster names to their definitions.
gkeClusters map[string]*layoutDeploymentGKECluster
// appEngineModules is the set of AppEngine modules in this Deployment that
// reference this cloud project.
appEngineModules []*layoutDeploymentGAEModule
// resources is the accumulated set of AppEngine Resources, loaded from
// component and deployment source configs.
resources deploy.AppEngineResources
}
func (cp *layoutDeploymentCloudProject) String() string {
return joinPath(title(cp.dep.String()), title(cp.Name))
}
func (cp *layoutDeploymentCloudProject) appendResources(res *deploy.AppEngineResources,
module *layoutDeploymentGAEModule) {
// Accumulate global (non-module-bound) resources in our cloud project's
// resources protobuf.
cp.resources.Index = append(cp.resources.Index, res.Index...)
// Accumulate module-specific resources in our module's resources protobuf.
module.resources.Dispatch = append(module.resources.Dispatch, res.Dispatch...)
module.resources.TaskQueue = append(module.resources.TaskQueue, res.TaskQueue...)
module.resources.Cron = append(module.resources.Cron, res.Cron...)
}
// layoutDeploymentGAEModule is a single configured AppEngine module.
type layoutDeploymentGAEModule struct {
*deploy.AppEngineModule
// comp is the component that describes this module.
comp *layoutDeploymentComponent
// resources is the set of module-bound resources.
resources deploy.AppEngineResources
}
// layoutDeploymentGKECluster tracks a GKE cluster element.
type layoutDeploymentGKECluster struct {
*deploy.Deployment_CloudProject_GKECluster
// cloudProject is the cloud project that this GKE cluster belongs to.
cloudProject *layoutDeploymentCloudProject
// pods is the set of GKE pods in this Deployment.
pods []*layoutDeploymentGKEPodBinding
}
func (c *layoutDeploymentGKECluster) String() string {
return joinPath(title(c.cloudProject.String()), title(c.Name))
}
// layoutDeploymentGKEPodBinding tracks a GKE pod bound to a GKE cluster.
type layoutDeploymentGKEPodBinding struct {
*deploy.Deployment_CloudProject_GKECluster_PodBinding
// cluster is the cluster that the pod is deployed to.
cluster *layoutDeploymentGKECluster
// pod is the pod that is deployed. This is filled in when project contents
// are loaded.
pod *layoutDeploymentGKEPod
}
func (pb *layoutDeploymentGKEPodBinding) String() string {
return joinPath(title(pb.pod.String()), title(pb.cluster.String()))
}
// layoutDeploymentGKEPod tracks a deployed pod component bound to a GKE
// cluster.
type layoutDeploymentGKEPod struct {
// The container engine pod definition. This won't be filled in until the
// deployment's source configuration is loaded in loadSourceComponent.
*deploy.ContainerEnginePod
// comp is the component that this pod was described by.
comp *layoutDeploymentComponent
}
func (pb *layoutDeploymentGKEPod) String() string { return pb.comp.String() }
// deployLayout is the loaded and configured layout, populated with any state
// that has been loaded during operation.
type deployLayout struct {
deploy.Layout
// base is the base path of the layout file. By default, all other sources
// will be relative to this path.
basePath string
// user is the user configuration protobuf.
user deploy.UserConfig
// userSourceOverrides is a map of user checkout URL overrides.
userSourceOverrides map[string]*deploy.Source
// sourceGroups is the set of source group files, mapped to their source group
// name.
sourceGroups map[title]*layoutSourceGroup
// apps is the set of applications, mapped to their application name.
apps map[title]*layoutApp
// deployments is the set of deployments, mapped to their deployment name.
deployments map[title]*layoutDeployment
// deploymentNames is a list of keys in the deployments map, ordered by the
// order in which the deployments were loaded.
deploymentNames []title
// map of cloud project names to their deployments.
cloudProjects map[string]*layoutDeploymentCloudProject
}
// workingFilesystem creates a new managed filesystem at our working directory
// root.
//
// This adds layout-defined components to the filesystem.
func (l *deployLayout) workingFilesystem() (*managedfs.Filesystem, error) {
fs, err := managedfs.New(l.WorkingPath)
if err != nil {
return nil, errors.Annotate(err, "").Err()
}
return fs, nil
}
func (l *deployLayout) workingPathTo(relpath string) string {
return filepath.Join(l.WorkingPath, relpath)
}
func (l *deployLayout) getDeploymentComponent(v string) (*layoutDeployment, *layoutDeploymentComponent, error) {
deployment, component := splitComponentPath(v)
dep := l.deployments[deployment]
if dep == nil {
return nil, nil, errors.Reason("unknown Deployment %q", deployment).
InternalReason("value(%s)", v).Err()
}
// If a component was specified, only add that component.
if component != "" {
comp := dep.components[component]
if comp == nil {
return nil, nil, errors.Reason("unknown Deployment Component %q", v).
InternalReason("dep(%s)/comp(%s)", deployment, component).Err()
}
return dep, comp, nil
}
return dep, nil, nil
}
func (l *deployLayout) matchDeploymentComponent(m string, cb func(*layoutDeployment, *layoutDeploymentComponent)) error {
for _, depName := range l.deploymentNames {
dep := l.deployments[depName]
matched, err := filepath.Match(m, dep.String())
if err != nil {
return errors.Annotate(err, "failed to match %q", m).Err()
}
if matched {
// Matches entire deployment.
cb(dep, nil)
continue
}
// Try each of the deployment's components.
for _, compName := range dep.componentNames {
comp := dep.components[compName]
matched, err := filepath.Match(m, comp.String())
if err != nil {
return errors.Annotate(err, "failed to match %q", m).Err()
}
if matched {
cb(dep, comp)
}
}
}
return nil
}
func (l *deployLayout) load(c context.Context, path string) error {
if path == "" {
wd, err := os.Getwd()
if err != nil {
return err
}
path, err = findLayout(defaultLayoutFilename, wd)
if err != nil {
return err
}
}
if err := unmarshalTextProtobuf(path, &l.Layout); err != nil {
return err
}
// Load the user config, if available.
if err := loadUserConfig(c, &l.user); err != nil {
return errors.Annotate(err, "failed to load user config").Err()
}
if len(l.user.SourceOverride) > 0 {
l.userSourceOverrides = make(map[string]*deploy.Source, len(l.user.SourceOverride))
for k, v := range l.user.SourceOverride {
l.userSourceOverrides[k] = v
}
}
// Populate with defaults.
l.basePath = filepath.Dir(path)
if l.SourcesPath == "" {
l.SourcesPath = filepath.Join(l.basePath, "sources")
}
if l.ApplicationsPath == "" {
l.ApplicationsPath = filepath.Join(l.basePath, "applications")
}
if l.DeploymentsPath == "" {
l.DeploymentsPath = filepath.Join(l.basePath, "deployments")
}
if l.WorkingPath == "" {
l.WorkingPath = filepath.Join(l.basePath, ".working")
} else {
if !filepath.IsAbs(l.WorkingPath) {
l.WorkingPath = filepath.Join(l.basePath, l.WorkingPath)
}
}
absWorkingPath, err := filepath.Abs(l.WorkingPath)
if err != nil {
return errors.Annotate(err, "failed to resolve absolute path for %q", l.WorkingPath).Err()
}
l.WorkingPath = absWorkingPath
return nil
}
func (l *deployLayout) initFrozenCheckout(c context.Context) (*deploy.FrozenLayout, error) {
fis, err := ioutil.ReadDir(l.SourcesPath)
if err != nil {
return nil, errors.Annotate(err, "failed to read directory").Err()
}
// Build internal and frozen layout in parallel.
var frozen deploy.FrozenLayout
frozen.SourceGroup = make(map[string]*deploy.FrozenLayout_SourceGroup, len(fis))
for _, fi := range fis {
name := title(fi.Name())
path := filepath.Join(l.SourcesPath, string(name))
if !fi.IsDir() {
log.Fields{
"path": path,
}.Warningf(c, "Skipping non-directory in source group directory.")
continue
}
if err := name.validate(); err != nil {
log.Fields{
log.ErrorKey: err,
"title": name,
"path": path,
}.Warningf(c, "Skipping invalid source group title.")
continue
}
// Load the Sources.
srcInfos, err := ioutil.ReadDir(path)
if err != nil {
log.Fields{
log.ErrorKey: err,
"sourceGroup": name,
"path": path,
}.Warningf(c, "Could not read source group directory.")
continue
}
sg := deploy.FrozenLayout_SourceGroup{
Source: make(map[string]*deploy.FrozenLayout_Source, len(srcInfos)),
}
var srcBase deploy.Source
err = unmarshalTextProtobufDir(path, srcInfos, &srcBase, func(name string) error {
cpy := srcBase
t, err := titleFromConfigPath(name)
if err != nil {
return errors.Annotate(err, "invalid source title").Err()
}
src := deploy.FrozenLayout_Source{
Source: &cpy,
}
sg.Source[string(t)] = &src
return nil
})
if err != nil {
return nil, errors.Annotate(err, "failed to load source group %q from [%s]", name, path).Err()
}
frozen.SourceGroup[string(name)] = &sg
}
return &frozen, nil
}
func (l *deployLayout) loadFrozenLayout(c context.Context) error {
// Load the frozen configuration file from disk.
frozen, err := checkoutFrozen(l)
if err != nil {
return errors.Annotate(err, "failed to load frozen checkout").Err()
}
// Load our source groups and sources.
l.sourceGroups = make(map[title]*layoutSourceGroup, len(frozen.SourceGroup))
for sgName, fsg := range frozen.SourceGroup {
sg := layoutSourceGroup{
FrozenLayout_SourceGroup: fsg,
layout: l,
title: title(sgName),
sources: make(map[title]*layoutSource, len(fsg.Source)),
}
for srcName, fs := range fsg.Source {
src := layoutSource{
FrozenLayout_Source: fs,
sg: &sg,
title: title(srcName),
}
sg.sources[src.title] = &src
}
l.sourceGroups[sg.title] = &sg
}
// Build our internally-connected structures from the frozen layout.
if err := l.loadApps(c); err != nil {
return errors.Annotate(err, "failed to load applications from [%s]", l.ApplicationsPath).Err()
}
if err := l.loadDeployments(c); err != nil {
return errors.Annotate(err, "failed to load deployments from [%s]", l.DeploymentsPath).Err()
}
return nil
}
func (l *deployLayout) loadApps(c context.Context) error {
fis, err := ioutil.ReadDir(l.ApplicationsPath)
if err != nil {
return errors.Annotate(err, "failed to read directory").Err()
}
apps := make(map[title]*deploy.Application, len(fis))
var appBase deploy.Application
err = unmarshalTextProtobufDir(l.ApplicationsPath, fis, &appBase, func(name string) error {
cpy := appBase
t, err := titleFromConfigPath(name)
if err != nil {
return errors.Annotate(err, "invalid application title").Err()
}
apps[t] = &cpy
return nil
})
if err != nil {
return err
}
l.apps = make(map[title]*layoutApp, len(apps))
for t, app := range apps {
proj := layoutApp{
Application: app,
title: t,
components: make(map[title]*layoutAppComponent, len(app.Component)),
}
// Initialize components. These can't actually be populated until we have
// loaded our sources.
for _, comp := range proj.Component {
compT := title(comp.Name)
if err := compT.validate(); err != nil {
return errors.Annotate(err, "application %q component %q is not a valid component title", t, comp.Name).Err()
}
proj.components[compT] = &layoutAppComponent{
Application_Component: comp,
proj: &proj,
title: compT,
}
}
l.apps[t] = &proj
}
return nil
}
func (l *deployLayout) loadDeployments(c context.Context) error {
fis, err := ioutil.ReadDir(l.DeploymentsPath)
if err != nil {
return errors.Annotate(err, "failed to read directory").Err()
}
deployments := make(map[title]*deploy.Deployment, len(fis))
var deploymentBase deploy.Deployment
err = unmarshalTextProtobufDir(l.DeploymentsPath, fis, &deploymentBase, func(name string) error {
cpy := deploymentBase
t, err := titleFromConfigPath(name)
if err != nil {
return errors.Annotate(err, "invalid deployment title").Err()
}
deployments[t] = &cpy
return nil
})
if err != nil {
return err
}
l.deployments = make(map[title]*layoutDeployment, len(deployments))
for t, d := range deployments {
dep, err := l.loadDeployment(t, d)
if err != nil {
return errors.Annotate(err, "failed to load deployment (%q)", t).Err()
}
l.deployments[t] = dep
l.deploymentNames = append(l.deploymentNames, dep.title)
}
return nil
}
func (l *deployLayout) loadDeployment(t title, d *deploy.Deployment) (*layoutDeployment, error) {
dep := layoutDeployment{
Deployment: d,
l: l,
title: t,
sg: l.sourceGroups[title(d.SourceGroup)],
}
if dep.sg == nil {
return nil, errors.Reason("unknown source group %q", d.SourceGroup).Err()
}
// Resolve our application.
dep.app = l.apps[title(dep.Application)]
if dep.app == nil {
return nil, errors.Reason("unknown application %q", dep.Application).Err()
}
// Initialize our Components. Their protobufs cannot not be loaded until
// we have a checkout.
dep.components = make(map[title]*layoutDeploymentComponent, len(dep.app.components))
for compTitle, projComp := range dep.app.components {
comp := layoutDeploymentComponent{
reldir: deployDirname(projComp.Path),
dep: &dep,
comp: projComp,
sources: []*layoutSource{dep.sg.sources[title(projComp.Source)]},
}
if comp.sources[0] == nil {
return nil, errors.Reason("application references non-existent source %q", projComp.Source).Err()
}
for _, os := range projComp.OtherSource {
src := dep.sg.sources[title(os)]
if src == nil {
return nil, errors.Reason("application references non-existent other source %q", os).Err()
}
comp.sources = append(comp.sources, src)
}
dep.components[compTitle] = &comp
dep.componentNames = append(dep.componentNames, compTitle)
}
// Build a map of cloud project names.
if dep.CloudProject != nil {
cp := layoutDeploymentCloudProject{
Deployment_CloudProject: dep.CloudProject,
dep: &dep,
}
if l.cloudProjects == nil {
l.cloudProjects = make(map[string]*layoutDeploymentCloudProject)
}
if cur, ok := l.cloudProjects[cp.Name]; ok {
return nil, errors.Reason("cloud project %q defined by both %q and %q", cp.Name, cur.dep.title, dep.title).Err()
}
l.cloudProjects[cp.Name] = &cp
if len(dep.CloudProject.GkeCluster) > 0 {
cp.gkeClusters = make(map[string]*layoutDeploymentGKECluster, len(dep.CloudProject.GkeCluster))
for _, gke := range dep.CloudProject.GkeCluster {
gkeCluster := layoutDeploymentGKECluster{
Deployment_CloudProject_GKECluster: gke,
cloudProject: &cp,
}
// Bind Components to their GKE cluster.
for _, b := range gke.Pod {
comp := dep.components[title(b.Name)]
switch {
case comp == nil:
return nil, errors.Reason("unknown component %q for cluster %q", b.Name, gke.Name).Err()
case b.Replicas <= 0:
return nil, errors.Reason("GKE component %q must have at least 1 replica", b.Name).Err()
}
bp := &layoutDeploymentGKEPodBinding{
Deployment_CloudProject_GKECluster_PodBinding: b,
cluster: &gkeCluster,
}
comp.gkePods = append(comp.gkePods, bp)
gkeCluster.pods = append(gkeCluster.pods, bp)
}
cp.gkeClusters[gke.Name] = &gkeCluster
}
}
dep.cloudProject = &cp
}
return &dep, nil
}
// allSourceGroups returns a sorted list of all of the source group names
// in the layout.
func (l *deployLayout) allSourceGroups() []*layoutSourceGroup {
titles := make([]string, 0, len(l.sourceGroups))
for t := range l.sourceGroups {
titles = append(titles, string(t))
}
sort.Strings(titles)
result := make([]*layoutSourceGroup, len(titles))
for i, t := range titles {
result[i] = l.sourceGroups[title(t)]
}
return result
}
// findLayout looks in the current working directory and ascends towards the
// root filesystem looking for a "layout.cfg" file.
func findLayout(filename, dir string) (string, error) {
for {
path := filepath.Join(dir, filename)
switch st, err := os.Stat(path); {
case err == nil:
if !st.IsDir() {
return path, nil
}
case isNotExist(err):
break
default:
return "", errors.Annotate(err, "failed to state %q", path).Err()
}
// Walk up one directory.
oldDir := dir
dir, _ = filepath.Split(dir)
if oldDir == dir {
return "", errors.Reason("could not find %q starting from %q", filename, dir).Err()
}
}
}
type componentRegistrar interface {
addGAEModule(*layoutDeploymentGAEModule)
addGKEPod(*layoutDeploymentGKEPodBinding)
}