github.com/yourbase/yb@v0.7.1/package.go (about)

     1  // Copyright 2020 YourBase Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // SPDX-License-Identifier: Apache-2.0
    16  
    17  package yb
    18  
    19  import (
    20  	"fmt"
    21  	"io/ioutil"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/yourbase/narwhal"
    28  )
    29  
    30  const docsURL = "https://docs.yourbase.io"
    31  
    32  // DefaultTarget is the name of the target that should be built when no
    33  // arguments are given to yb build.
    34  const DefaultTarget = "default"
    35  
    36  // DefaultExecEnvironment is the name of the execution environment variable set
    37  // that should be used when no options are given to yb exec.
    38  const DefaultExecEnvironment = "default"
    39  
    40  // PackageConfigFilename is the name of the file at the base of a package
    41  // directory containing the package's configuration.
    42  const PackageConfigFilename = ".yourbase.yml"
    43  
    44  // Package is a parsed build configuration (from .yourbase.yml).
    45  type Package struct {
    46  	// Name is the name of the package directory.
    47  	Name string
    48  	// Path is the absolute path to the package directory.
    49  	Path string
    50  	// Targets is the set of targets in the package, keyed by target name.
    51  	Targets map[string]*Target
    52  	// ExecEnvironments is the set of targets representing the exec phase
    53  	// in the configuration, keyed by environment name.
    54  	ExecEnvironments map[string]*Target
    55  }
    56  
    57  // LoadPackage loads the package for the given .yourbase.yml file.
    58  func LoadPackage(configPath string) (*Package, error) {
    59  	configPath, err := filepath.Abs(configPath)
    60  	if err != nil {
    61  		return nil, fmt.Errorf("load package %s: %w", configPath, err)
    62  	}
    63  	configYAML, err := ioutil.ReadFile(configPath)
    64  	if os.IsNotExist(err) {
    65  		return nil, fmt.Errorf("load package %s: %w\nTry running in the package directory or creating %s if it is missing. See %s", configPath, err, filepath.Base(configPath), docsURL)
    66  	}
    67  	if err != nil {
    68  		return nil, fmt.Errorf("load package %s: %w", configPath, err)
    69  	}
    70  	pkg, err := parse(filepath.Dir(configPath), configYAML)
    71  	if err != nil {
    72  		return nil, fmt.Errorf("load package %s: %w", configPath, err)
    73  	}
    74  	targets := make([]*Target, 0, len(pkg.Targets))
    75  	for _, target := range pkg.Targets {
    76  		targets = append(targets, target)
    77  	}
    78  	if _, err := buildOrder(targets); err != nil {
    79  		return nil, fmt.Errorf("load package %s: %w", configPath, err)
    80  	}
    81  	return pkg, nil
    82  }
    83  
    84  // A Target is a buildable unit.
    85  type Target struct {
    86  	Name    string
    87  	Package *Package
    88  	Deps    map[*Target]struct{}
    89  	Tags    map[string]string
    90  
    91  	// Container specifies the container environment that should be used to run
    92  	// the commands in if container execution is requested. It will never be nil.
    93  	Container *narwhal.ContainerDefinition
    94  	// UseContainer indicates whether this target requires executing the commands
    95  	// inside a container.
    96  	UseContainer bool
    97  
    98  	Commands   []string
    99  	RunDir     string
   100  	Env        map[string]EnvTemplate
   101  	Buildpacks map[string]BuildpackSpec
   102  	Resources  map[string]*ResourceDefinition
   103  }
   104  
   105  type ResourceDefinition struct {
   106  	narwhal.ContainerDefinition
   107  
   108  	HealthCheckTimeout time.Duration
   109  }
   110  
   111  // BuildOrder returns a topological sort of the targets needed to build the
   112  // given target(s). If a single argument is passed, then the last element in the
   113  // returned slice is always the argument.
   114  func BuildOrder(desired ...*Target) []*Target {
   115  	targetList, err := buildOrder(desired)
   116  	if err != nil {
   117  		panic(err)
   118  	}
   119  	return targetList
   120  }
   121  
   122  func buildOrder(desired []*Target) ([]*Target, error) {
   123  	type stackFrame struct {
   124  		target *Target
   125  		done   bool
   126  	}
   127  	stk := make([]stackFrame, 0, len(desired))
   128  	for i := len(desired) - 1; i >= 0; i-- {
   129  		stk = append(stk, stackFrame{target: desired[i]})
   130  	}
   131  
   132  	var targetList []*Target
   133  	marks := make(map[*Target]int)
   134  	for len(stk) > 0 {
   135  		curr := stk[len(stk)-1]
   136  		stk = stk[:len(stk)-1]
   137  
   138  		if curr.done {
   139  			marks[curr.target] = 2
   140  			targetList = append(targetList, curr.target)
   141  			continue
   142  		}
   143  		switch marks[curr.target] {
   144  		case 0:
   145  			// First visit. Revisit once all dependencies have been added to the list.
   146  			marks[curr.target] = 1
   147  			stk = append(stk, stackFrame{target: curr.target, done: true})
   148  			for dep := range curr.target.Deps {
   149  				stk = append(stk, stackFrame{target: dep})
   150  			}
   151  		case 1:
   152  			// Cycle.
   153  			intermediaries := findCycle(curr.target)
   154  			formatted := new(strings.Builder)
   155  			for _, target := range intermediaries {
   156  				formatted.WriteString(target.Name)
   157  				formatted.WriteString(" -> ")
   158  			}
   159  			formatted.WriteString(curr.target.Name)
   160  			return nil, fmt.Errorf("target %s has a cycle: %s", curr.target.Name, formatted)
   161  		}
   162  	}
   163  	return targetList, nil
   164  }
   165  
   166  func findCycle(target *Target) []*Target {
   167  	var paths [][]*Target
   168  	for dep := range target.Deps {
   169  		paths = append(paths, []*Target{dep})
   170  	}
   171  	for {
   172  		// Dequeue.
   173  		curr := paths[0]
   174  		copy(paths, paths[1:])
   175  		paths[len(paths)-1] = nil
   176  		paths = paths[:len(paths)-1]
   177  
   178  		// Check if the path leads back to the original target.
   179  		deps := curr[len(curr)-1].Deps
   180  		if _, done := deps[target]; done {
   181  			return curr
   182  		}
   183  
   184  		// Advance paths.
   185  		for dep := range deps {
   186  			paths = append(paths, append(curr[:len(curr):len(curr)], dep))
   187  		}
   188  	}
   189  }
   190  
   191  // BuildpackSpec is a buildpack specifier, consisting of a name and a version.
   192  type BuildpackSpec string
   193  
   194  // ParseBuildpackSpec validates a buildpack specifier string.
   195  func ParseBuildpackSpec(s string) (BuildpackSpec, error) {
   196  	i := strings.IndexByte(s, ':')
   197  	if i == -1 {
   198  		return "", fmt.Errorf("parse buildpack %q: no version specified (missing ':')", s)
   199  	}
   200  	return BuildpackSpec(s), nil
   201  }
   202  
   203  func (spec BuildpackSpec) Name() string {
   204  	i := strings.IndexByte(string(spec), ':')
   205  	if i == -1 {
   206  		panic("Name() called on invalid spec: " + string(spec))
   207  	}
   208  	return string(spec[:i])
   209  }
   210  
   211  func (spec BuildpackSpec) Version() string {
   212  	i := strings.IndexByte(string(spec), ':')
   213  	if i == -1 {
   214  		panic("Version() called on invalid spec: " + string(spec))
   215  	}
   216  	return string(spec[i+1:])
   217  }
   218  
   219  // EnvTemplate is an expression for an environment variable value. It's mostly a
   220  // literal string, but may include substitutions for container IP addresses in
   221  // the form `{{ .Containers.IP "mycontainer" }}`.
   222  type EnvTemplate string