github.com/benchkram/bob@v0.0.0-20240314204020-b7a57f2f9be9/bob/bobfile/bobfile.go (about) 1 package bobfile 2 3 import ( 4 "bytes" 5 "fmt" 6 "net/url" 7 "os" 8 "path/filepath" 9 "strings" 10 11 "github.com/benchkram/bob/pkg/nix" 12 storeclient "github.com/benchkram/bob/pkg/store-client" 13 14 "github.com/benchkram/bob/pkg/sliceutil" 15 "github.com/benchkram/bob/pkg/store" 16 "github.com/benchkram/bob/pkg/store/remotestore" 17 "github.com/benchkram/bob/pkg/usererror" 18 19 "github.com/hashicorp/go-version" 20 "github.com/pkg/errors" 21 22 "gopkg.in/yaml.v3" 23 24 "github.com/benchkram/errz" 25 26 "github.com/benchkram/bob/bob/bobfile/project" 27 "github.com/benchkram/bob/bob/global" 28 "github.com/benchkram/bob/bobrun" 29 "github.com/benchkram/bob/bobtask" 30 "github.com/benchkram/bob/pkg/file" 31 ) 32 33 var ( 34 ErrNotImplemented = fmt.Errorf("Not implemented") 35 ErrBobfileNotFound = fmt.Errorf("Could not find a bob.yaml") 36 ErrHashesFileDoesNotExist = fmt.Errorf("Hashes file does not exist") 37 ErrTaskHashDoesNotExist = fmt.Errorf("Task hash does not exist") 38 ErrBobfileExists = fmt.Errorf("Bobfile exists") 39 ErrDuplicateTaskName = fmt.Errorf("duplicate task name") 40 ErrInvalidProjectName = fmt.Errorf("invalid project name") 41 ErrSelfReference = fmt.Errorf("self reference") 42 43 ErrInvalidRunType = fmt.Errorf("Invalid run type") 44 45 ProjectNameFormatHint = "project name should be in the form 'project' or 'registry.com/user/project'" 46 ) 47 48 type Bobfile struct { 49 // Version is optional, and can be used to 50 Version string `yaml:"version,omitempty"` 51 52 // Project uniquely identifies the current project (optional). If supplied, 53 // aggregation makes sure the project does not depend on another instance 54 // of itself. If not provided, then the project name is set after the path 55 // of its bobfile. 56 Project string `yaml:"project,omitempty"` 57 58 Imports []string `yaml:"import,omitempty"` 59 60 // Variables is a map of variables that can be used in the tasks. 61 Variables VariableMap 62 63 // BTasks build tasks 64 BTasks bobtask.Map `yaml:"build"` 65 // RTasks run tasks 66 RTasks bobrun.RunMap `yaml:"run"` 67 68 Dependencies []string `yaml:"dependencies"` 69 70 // Nixpkgs specifies an optional nixpkgs source. 71 Nixpkgs string `yaml:"nixpkgs"` 72 73 // Parent directory of the Bobfile. 74 // Populated through BobfileRead(). 75 dir string 76 77 bobfiles []*Bobfile 78 79 RemoteStoreHost string 80 remotestore store.Store 81 } 82 83 func NewBobfile() *Bobfile { 84 b := &Bobfile{ 85 Variables: make(VariableMap), 86 BTasks: make(bobtask.Map), 87 RTasks: make(bobrun.RunMap), 88 } 89 return b 90 } 91 92 func (b *Bobfile) SetBobfiles(bobs []*Bobfile) { 93 b.bobfiles = bobs 94 } 95 96 func (b *Bobfile) Bobfiles() []*Bobfile { 97 return b.bobfiles 98 } 99 100 func (b *Bobfile) SetRemotestore(remote store.Store) { 101 b.remotestore = remote 102 } 103 104 func (b *Bobfile) Remotestore() store.Store { 105 return b.remotestore 106 } 107 108 // bobfileRead reads a bobfile and initializes private fields. 109 func bobfileRead(dir string) (_ *Bobfile, err error) { 110 defer errz.Recover(&err) 111 112 bobfilePath := filepath.Join(dir, global.BobFileName) 113 114 if !file.Exists(bobfilePath) { 115 return nil, usererror.Wrap(ErrBobfileNotFound) 116 } 117 bin, err := os.ReadFile(bobfilePath) 118 errz.Fatal(err) 119 120 bobfile := &Bobfile{ 121 dir: dir, 122 } 123 124 err = yaml.Unmarshal(bin, bobfile) 125 if err != nil { 126 return nil, usererror.Wrapm(err, "YAML unmarshal failed") 127 } 128 129 if bobfile.Variables == nil { 130 bobfile.Variables = VariableMap{} 131 } 132 133 if bobfile.BTasks == nil { 134 bobfile.BTasks = bobtask.Map{} 135 } 136 137 if bobfile.RTasks == nil { 138 bobfile.RTasks = bobrun.RunMap{} 139 } 140 141 // Assure tasks are initialized with their defaults 142 for key, task := range bobfile.BTasks { 143 task.SetDir(bobfile.dir) 144 task.SetName(key) 145 task.InputAdditionalIgnores = []string{} 146 147 // Make sure a task is correctly initialised. 148 // TODO: All unitialised must be initialised or get default values. 149 // This means switching to pointer types for most members. 150 task.SetEnv([]string{}) 151 task.SetRebuildStrategy(bobtask.RebuildOnChange) 152 153 // initialize docker registry for task 154 task.SetDependencies(initializeDependencies(dir, task.DependenciesDirty, bobfile)) 155 156 bobfile.BTasks[key] = task 157 } 158 159 // Assure runs are initialized with their defaults 160 for key, run := range bobfile.RTasks { 161 run.SetDir(bobfile.dir) 162 run.SetName(key) 163 run.SetEnv([]string{}) 164 165 run.SetDependencies(initializeDependencies(dir, run.DependenciesDirty, bobfile)) 166 167 bobfile.RTasks[key] = run 168 } 169 170 // // Initialize remote store in case of a valid remote url / projectname. 171 // if bobfile.Project != "" { 172 // projectname, err := project.Parse(bobfile.Project) 173 // if err != nil { 174 // return nil, err 175 // } 176 // 177 // switch projectname.Type() { 178 // case project.Local: 179 // // Do nothing 180 // case project.Remote: 181 // // Initialize remote store 182 // url, err := projectname.Remote() 183 // if err != nil { 184 // return nil, err 185 // } 186 // 187 // boblog.Log.V(1).Info(fmt.Sprintf("Using remote store: %s", url.String())) 188 // 189 // bobfile.remotestore = NewRemotestore(url) 190 // } 191 // } else { 192 // bobfile.Project = bobfile.dir 193 // } 194 195 return bobfile, nil 196 } 197 198 // initializeDependencies gathers all dependencies for a task(task level and bobfile level) 199 // and initialize them with bobfile dir and corresponding nixpkgs used 200 func initializeDependencies(dir string, taskDependencies []string, bobfile *Bobfile) []nix.Dependency { 201 dependencies := sliceutil.Unique(append(taskDependencies, bobfile.Dependencies...)) 202 dependencies = nix.AddDir(dir, dependencies) 203 204 taskDeps := make([]nix.Dependency, 0) 205 for _, v := range dependencies { 206 taskDeps = append(taskDeps, nix.Dependency{ 207 Name: v, 208 Nixpkgs: bobfile.Nixpkgs, 209 }) 210 } 211 212 return nix.UniqueDeps(taskDeps) 213 } 214 215 func NewRemotestore(endpoint *url.URL, allowInsecure bool, token string) (s store.Store) { 216 const sep = "/" 217 218 parts := strings.Split(strings.TrimLeft(endpoint.Path, sep), sep) 219 220 username := parts[0] 221 proj := strings.Join(parts[1:], sep) 222 223 protocol := "https://" 224 if allowInsecure { 225 protocol = "http://" 226 } 227 228 s = remotestore.New( 229 username, 230 proj, 231 232 remotestore.WithClient( 233 storeclient.New(protocol+endpoint.Host, token), 234 ), 235 ) 236 return s 237 } 238 239 // BobfileRead read from a bobfile. 240 // Calls sanitize on the result. 241 func BobfileRead(dir string) (_ *Bobfile, err error) { 242 defer errz.Recover(&err) 243 244 b, err := bobfileRead(dir) 245 errz.Fatal(err) 246 247 err = b.Validate() 248 errz.Fatal(err) 249 250 err = b.BTasks.Sanitize() 251 errz.Fatal(err) 252 253 err = b.RTasks.Sanitize() 254 errz.Fatal(err) 255 256 return b, nil 257 } 258 259 // BobfileReadPlain reads a bobfile. 260 // For performance reasons sanitize is not called. 261 func BobfileReadPlain(dir string) (_ *Bobfile, err error) { 262 defer errz.Recover(&err) 263 264 b, err := bobfileRead(dir) 265 errz.Fatal(err) 266 267 err = b.Validate() 268 errz.Fatal(err) 269 270 return b, nil 271 } 272 273 // Validate makes sure no task depends on itself (self-reference) or has the same name as another task 274 func (b *Bobfile) Validate() (err error) { 275 if b.Version != "" { 276 _, err = version.NewVersion(b.Version) 277 if err != nil { 278 return fmt.Errorf("invalid version '%s' (%s)", b.Version, b.Dir()) 279 } 280 } 281 282 // validate project name if set 283 if b.Project != "" { 284 if !project.RestrictedProjectNamePattern.MatchString(b.Project) { 285 return usererror.Wrap(errors.WithMessage(ErrInvalidProjectName, ProjectNameFormatHint)) 286 } 287 288 // test for double slash (do not allow prepended schema) 289 if project.ProjectNameDoubleSlashPattern.MatchString(b.Project) { 290 return usererror.Wrap(errors.WithMessage(ErrInvalidProjectName, ProjectNameFormatHint)) 291 } 292 } 293 294 // use for duplicate names validation 295 names := map[string]bool{} 296 297 for name, task := range b.BTasks { 298 // validate no duplicate name 299 if names[name] { 300 return errors.WithMessage(ErrDuplicateTaskName, name) 301 } 302 303 names[name] = true 304 305 // validate no self-reference 306 for _, dep := range task.DependsOn { 307 if name == dep { 308 return errors.WithMessage(ErrSelfReference, name) 309 } 310 } 311 } 312 313 for name, run := range b.RTasks { 314 // validate no duplicate name 315 if names[name] { 316 return errors.WithMessage(ErrDuplicateTaskName, name) 317 } 318 319 names[name] = true 320 321 // validate no self-reference 322 for _, dep := range run.DependsOn { 323 if name == dep { 324 return errors.WithMessage(ErrSelfReference, name) 325 } 326 } 327 } 328 329 return nil 330 } 331 332 func (b *Bobfile) BobfileSave(dir, name string) (err error) { 333 defer errz.Recover(&err) 334 335 buf := bytes.NewBuffer([]byte{}) 336 337 encoder := yaml.NewEncoder(buf) 338 encoder.SetIndent(2) 339 defer encoder.Close() 340 341 err = encoder.Encode(b) 342 errz.Fatal(err) 343 344 return os.WriteFile(filepath.Join(dir, name), buf.Bytes(), 0664) 345 } 346 347 func (b *Bobfile) Dir() string { 348 return b.dir 349 } 350 351 // Vars returns the bobfile variables in the form "key=value" 352 // based on its Variables 353 func (b *Bobfile) Vars() []string { 354 var env []string 355 for key, value := range b.Variables { 356 env = append(env, strings.Join([]string{key, value}, "=")) 357 } 358 return env 359 }