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  }