blob: b9608fff715b301471a76ff98b8e7ec4d17f4046 [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 (
"sort"
"github.com/luci/luci-go/common/cli"
"github.com/luci/luci-go/common/errors"
"github.com/luci/luci-go/common/flag/flagenum"
log "github.com/luci/luci-go/common/logging"
"github.com/luci/luci-go/deploytool/managedfs"
"github.com/maruel/subcommands"
)
type deployStage int
const (
// deployStaging is the staging stage, where local filesystem and build
// artifacts are constructed. Build scripts are executed, and generated files
// and directories are created. This will leave the user with a directory that
// can be used to build from.
deployStaging deployStage = iota
// deployLocalBuild performs a local build of the components, offering both a
// sanity check prior to engaging remote services and creating build artifacts
// ready to push remotely.
deployLocalBuild deployStage = iota
// deployPush pushes local build artifacts to the remote service, but doesn't
// enable them.
deployPush deployStage = iota
// deployCommit commits the pushed artifacts to the remote service, activating
// them.
deployCommit deployStage = iota
)
var deployStageFlagEnum = flagenum.Enum{
"stage": deployStaging,
"localbuild": deployLocalBuild,
"push": deployPush,
"commit": deployCommit,
}
func (s *deployStage) Set(v string) error { return deployStageFlagEnum.FlagSet(s, v) }
func (s deployStage) String() string { return deployStageFlagEnum.FlagString(s) }
type deployParams struct {
// stage is the highest deployment stage to deploy through.
stage deployStage
// ignoreCurrentVersion, if true, instructs depoyment logic to proceed with
// deployment even if the currently-deployed version matches the current
// source version.
ignoreCurrentVersion bool
// commitTainted, if true, instructs the deploy tool to perform commit
// operations on tainted Components.
commitTainted bool
// forceVersion forces the use of the supplied version instead of the one
// generated by the source.
forceVersion cloudProjectVersion
}
var cmdDeploy = subcommands.Command{
UsageLine: "deploy [DEPLOYMENT.[.COMPONENT]]...",
ShortDesc: "Deploys some or all of a deployment.",
LongDesc: "Deploys a Deployment. If specific projects are named, only those projects will be deployed.",
CommandRun: func() subcommands.CommandRun {
var cmd cmdDeployRun
cmd.dp.params.stage = deployCommit
fs := cmd.GetFlags()
fs.BoolVar(&cmd.checkout, "checkout", false,
"Refresh the checkout prior to deployment.")
fs.Var(&cmd.dp.params.stage, "stage",
"Run the deployment sequence up through this stage ["+deployStageFlagEnum.Choices()+"].")
fs.BoolVar(&cmd.dp.params.commitTainted, "commit-tainted", false,
"Install new deployments even if their source is tained.")
fs.BoolVar(&cmd.dp.params.ignoreCurrentVersion, "ignore-current-version", false,
"Push/install new deployments even if the currently-deployed version would not change or could not be parsed.")
fs.StringVar(&cmd.manualVersion, "version", "",
"Manually specify the version to use for deployment. Implies tainted.")
return &cmd
},
}
type cmdDeployRun struct {
subcommands.CommandRunBase
// checkout, if true, refreshes the checkout prior to deployment.
checkout bool
// manualVersion is the manually-specified cloud project version.
manualVersion string
dp deploymentPlan
}
func (cmd *cmdDeployRun) Run(app subcommands.Application, args []string, env subcommands.Env) int {
a, c := app.(*application), cli.GetContext(app, cmd, env)
// If a manual version was specified, use it.
if cmd.manualVersion != "" {
cmd.dp.params.forceVersion = makeStringCloudProjectVersion(cmd.manualVersion)
}
err := a.runWork(c, func(w *work) error {
// Perform our planned checkout.
if cmd.checkout {
// Perform a full checkout.
if err := checkout(w, &a.layout, false); err != nil {
return errors.Annotate(err, "failed to checkout sources").Err()
}
}
// Load our frozen checkout.
if err := a.layout.loadFrozenLayout(w); err != nil {
return errors.Annotate(err, "failed to load frozen checkout").Err()
}
return nil
})
if err != nil {
logError(c, err, "Failed to perform checkout.")
return 1
}
// Figure out what we're going to deploy.
for _, arg := range args {
matched := false
err := a.layout.matchDeploymentComponent(arg, func(dep *layoutDeployment, comp *layoutDeploymentComponent) {
matched = true
if comp != nil {
cmd.dp.addProjectComponent(comp)
} else {
cmd.dp.addDeployment(dep)
}
})
if err != nil {
logError(c, err, "Failed during matching.")
return 1
}
if !matched {
log.Fields{
"target": arg,
}.Errorf(c, "Deploy target did not match any configured deployments or components.")
return 1
}
}
err = a.runWork(c, func(w *work) error {
if err := cmd.dp.initialize(w, &a.layout); err != nil {
return errors.Annotate(err, "failed to initialize").Err()
}
return cmd.dp.deploy(w)
})
if err != nil {
logError(c, err, "Failed to perform deployment.")
return 1
}
return 0
}
type deploymentPlan struct {
// comps maps DEPLOYMENT/PROJECT/COMPONENT to its specific deployable project
// component.
comps map[string]*layoutDeploymentComponent
// deployments tracks which deployments we've specified.
deployments map[title]struct{}
// params are the deployment parameters to forward to deployment operations.
params deployParams
// reg is the registry of deployable Components.
reg deploymentRegistry
// layout is the deployment layout. It is initialized during "initialize".
layout *deployLayout
}
func (dp *deploymentPlan) addProjectComponent(comp *layoutDeploymentComponent) {
// Mark this specific component.
if dp.comps == nil {
dp.comps = make(map[string]*layoutDeploymentComponent)
}
dp.comps[comp.String()] = comp
// Mark that we've used this deployment.
if dp.deployments == nil {
dp.deployments = make(map[title]struct{})
}
dp.deployments[comp.dep.title] = struct{}{}
}
func (dp *deploymentPlan) addDeployment(dep *layoutDeployment) {
for _, comp := range dep.components {
dp.addProjectComponent(comp)
}
}
func (dp *deploymentPlan) deployedComponents() []string {
comps := make([]string, 0, len(dp.comps))
for path := range dp.comps {
comps = append(comps, path)
}
sort.Strings(comps)
return comps
}
func (dp *deploymentPlan) initialize(w *work, l *deployLayout) error {
dp.layout = l
// Load all deployable Components from checkout.
for _, depName := range l.deploymentNames {
dep := l.deployments[depName]
if _, ok := dp.deployments[dep.title]; !ok {
continue
}
// Load the application components. We will iterate through them in the
// order in which they are defined in the Application's protobuf.
for _, compName := range dep.componentNames {
comp := dep.components[compName]
// Only pass the registrar if this component is being deployed.
var pReg componentRegistrar
if _, ok := dp.comps[comp.String()]; ok {
pReg = &dp.reg
}
if err := comp.loadSourceComponent(pReg); err != nil {
return errors.Annotate(err, "failed to load component %q", comp).Err()
}
}
}
return nil
}
func (dp *deploymentPlan) deploy(w *work) error {
// Create our working root and staging/build subdirectories.
fs, err := dp.layout.workingFilesystem()
if err != nil {
return errors.Annotate(err, "failed to create working directory").Err()
}
stagingDir, err := fs.Base().EnsureDirectory("staging")
if err != nil {
return errors.Annotate(err, "failed to create staging directory").Err()
}
// Stage: Staging
if err := dp.reg.stage(w, stagingDir, &dp.params); err != nil {
return errors.Annotate(err, "failed to stage").Err()
}
if dp.params.stage <= deployStaging {
return nil
}
// Stage: Local Build
if err := dp.reg.localBuild(w); err != nil {
return errors.Annotate(err, "failed to perform local build").Err()
}
if dp.params.stage <= deployLocalBuild {
return nil
}
// Stage: Push
if err := dp.reg.push(w); err != nil {
return errors.Annotate(err, "failed to push components").Err()
}
if dp.params.stage <= deployPush {
return nil
}
// Stage: Commit
if err := dp.reg.commit(w); err != nil {
return errors.Annotate(err, "failed to commit").Err()
}
return nil
}
// deploymentRegistry is the registry of all prepared deployments.
//
// While deployment is specified and staged at a component level, actual
// deployment is allowed to happen at a practical level (e.g., a single
// cloud project's AppEngine configuration, a single Google Container Engine
// cluster, etc.).
//
// This will track individual components, as well as integrate them into larger
// action plans.
type deploymentRegistry struct {
// components is a map of components that have been deployed.
components map[string]*layoutDeploymentComponent
// componentNames is the sorted list of component names in components.
componentNames []string
// gaeProjects maps Google AppEngine deployment state to specific GAE
// projects.
gaeProjects map[string]*gaeDeployment
// gaeProjectNames is the sorted list of registered GAE project names.
gaeProjectNames []string
// gkeProjects is the set of Google Container Engine deployments for a given
// cloud project.
gkeProjects map[string]*containerEngineDeployment
// gkeProjectNames is the sorted list of registered GKE project names.
gkeProjectNames []string
}
func (reg *deploymentRegistry) addComponent(comp *layoutDeploymentComponent) {
name := comp.comp.Name
if _, has := reg.components[name]; has {
return
}
if reg.components == nil {
reg.components = make(map[string]*layoutDeploymentComponent)
}
reg.components[name] = comp
reg.componentNames = append(reg.componentNames, name)
}
func (reg *deploymentRegistry) addGAEModule(module *layoutDeploymentGAEModule) {
// Get/create the AppEngine project for this module.
cloudProjectName := module.comp.dep.cloudProject.Name
gaeD := reg.gaeProjects[cloudProjectName]
if gaeD == nil {
gaeD = makeGAEDeployment(module.comp.dep.cloudProject)
if reg.gaeProjects == nil {
reg.gaeProjects = make(map[string]*gaeDeployment)
}
reg.gaeProjects[cloudProjectName] = gaeD
reg.gaeProjectNames = append(reg.gaeProjectNames, cloudProjectName)
}
gaeD.addModule(module)
reg.addComponent(module.comp)
}
func (reg *deploymentRegistry) addGKEPod(pb *layoutDeploymentGKEPodBinding) {
// Get/create the AppEngine project for this module.
cloudProjectName := pb.cluster.cloudProject.Name
gkeD := reg.gkeProjects[cloudProjectName]
if gkeD == nil {
gkeD = &containerEngineDeployment{
project: pb.cluster.cloudProject,
clusters: make(map[string]*containerEngineDeploymentCluster),
}
if reg.gkeProjects == nil {
reg.gkeProjects = make(map[string]*containerEngineDeployment)
}
reg.gkeProjects[cloudProjectName] = gkeD
reg.gkeProjectNames = append(reg.gkeProjectNames, cloudProjectName)
}
// Register this pod in its cluster.
gkeD.addCluster(pb.cluster).attachPod(pb)
}
// appEngineModulesOnly clears all deployment parameters that are not AppEngine
// modules.
func (reg *deploymentRegistry) appEngineModulesOnly() {
reg.gkeProjects, reg.gkeProjectNames = nil, nil
}
// stage performs staging, preparing deployment structures on the local
// filesystem.
func (reg *deploymentRegistry) stage(w *work, stageDir *managedfs.Dir, params *deployParams) error {
// Sort our name lists.
sort.Strings(reg.componentNames)
sort.Strings(reg.gaeProjectNames)
sort.Strings(reg.gkeProjectNames)
// All components can be independently staged in parallel.
//
// We will still enumerate over them in sorted order for determinism.
return w.RunMulti(func(workC chan<- func() error) {
// AppEngine Projects
for _, name := range reg.gaeProjectNames {
proj := reg.gaeProjects[name]
workC <- func() error {
depDir, err := stageDir.EnsureDirectory(string(proj.project.dep.title), "appengine")
if err != nil {
return errors.Annotate(err, "failed to create deployment directory for %q", proj.project.dep.title).Err()
}
return proj.stage(w, depDir, params)
}
}
// Container Engine Projects
for _, name := range reg.gkeProjectNames {
proj := reg.gkeProjects[name]
workC <- func() error {
depDir, err := stageDir.EnsureDirectory(string(proj.project.dep.title), "container_engine")
if err != nil {
return errors.Annotate(err, "failed to create deployment directory for %q", proj.project.dep.title).Err()
}
return proj.stage(w, depDir, params)
}
}
})
}
// build performs the build stage on staged components.
func (reg *deploymentRegistry) localBuild(w *work) error {
// Enumerate over them in sorted order for determinism.
return w.RunMulti(func(workC chan<- func() error) {
// AppEngine Projects
for _, name := range reg.gaeProjectNames {
proj := reg.gaeProjects[name]
workC <- func() error {
return proj.localBuild(w)
}
}
// Container Engine Projects
for _, name := range reg.gkeProjectNames {
proj := reg.gkeProjects[name]
workC <- func() error {
return proj.localBuild(w)
}
}
})
}
// push builds and pushes the deployments to remote services, but does not
// commit to them.
func (reg *deploymentRegistry) push(w *work) error {
// All components can be independently pushed in parallel.
//
// We will still enumerate over them in sorted order for determinism.
return w.RunMulti(func(workC chan<- func() error) {
// AppEngine Projects
for _, name := range reg.gaeProjectNames {
proj := reg.gaeProjects[name]
workC <- func() error {
return proj.push(w)
}
}
// Container Engine Projects
for _, name := range reg.gkeProjectNames {
proj := reg.gkeProjects[name]
workC <- func() error {
return proj.push(w)
}
}
})
}
// commit commits the deployment to the remote services.
func (reg *deploymentRegistry) commit(w *work) error {
// All components can be independently committed in parallel.
//
// We will still enumerate over them in sorted order for determinism.
return w.RunMulti(func(workC chan<- func() error) {
// AppEngine Projects
for _, name := range reg.gaeProjectNames {
proj := reg.gaeProjects[name]
workC <- func() error {
return proj.commit(w)
}
}
// Container Engine Projects
for _, name := range reg.gkeProjectNames {
proj := reg.gkeProjects[name]
workC <- func() error {
return proj.commit(w)
}
}
})
}