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