github.com/dnephin/dobi@v0.15.0/config/job.go (about)

     1  package config
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"reflect"
     7  
     8  	"github.com/dnephin/configtf"
     9  	pth "github.com/dnephin/configtf/path"
    10  	shlex "github.com/kballard/go-shellquote"
    11  	"golang.org/x/crypto/ssh/terminal"
    12  )
    13  
    14  // JobConfig A **job** resource uses an `image`_ to run a job in a container.
    15  //
    16  // A **job** resource that doesn't have an ``artifact`` is never considered
    17  // up-to-date and will always run.  If a job resource has an ``artifact``
    18  // the job will be skipped if the artifact is newer than the source.
    19  // The last modified time of the ``artifact`` files is compared against the
    20  // last modified time of the files in ``sources``, or if ``sources`` is left
    21  // unset, the last modified time of the ``use`` image and all the files in
    22  // the ``mounts``.
    23  //
    24  // ``mounts`` are provided to the container as bind mounts. If the ``DOBI_NO_BIND_MOUNT``
    25  // environment variable, or `--no-bind-mount` flag is set, then ``mounts``
    26  // will be copied into the container, and all artifacts will be copied out of the
    27  // container to the host after the job is complete.
    28  //
    29  // The `image`_ specified in ``use`` and any `mount`_ resources listed in
    30  // ``mounts`` are automatically added as dependencies and will always be
    31  // created first.
    32  //
    33  // name: job
    34  // example: Run a container using the ``builder`` image to compile some source
    35  // code to ``./dist/app-binary``.
    36  //
    37  // .. code-block:: yaml
    38  //
    39  //     job=compile:
    40  //         use: builder
    41  //         mounts: [source, dist]
    42  //         artifact: dist/app-binary
    43  //
    44  type JobConfig struct {
    45  	// Use The name of an `image`_ resource. The referenced image is used
    46  	// to created the container for the **job**.
    47  	Use string `config:"required"`
    48  	// Artifact File paths or globs identifying the files created by the **job**.
    49  	// Paths to directories must end with a path separator (``/``).
    50  	// Paths are relative to the ``dobi.yaml``
    51  	// type: list of file paths or glob patterns
    52  	Artifact PathGlobs
    53  	// Command The command to run in the container.
    54  	// type: shell quoted string
    55  	// example: ``"bash -c 'echo something'"``
    56  	Command ShlexSlice
    57  	// Entrypoint Override the image entrypoint
    58  	// type: shell quoted string
    59  	Entrypoint ShlexSlice
    60  	// Sources File paths or globs of the files used to create the
    61  	// artifact. The modified time of these files are compared to the modified time
    62  	// of the artifact to determine if the **job** is stale. If the **sources**
    63  	// list is defined the modified time of **mounts** and the **use** image are
    64  	// ignored.
    65  	// type: list of file paths or glob patterns
    66  	Sources PathGlobs
    67  	// Mounts A list of `mount`_ resources to use when creating the container.
    68  	// type: list of mount resources
    69  	Mounts []string
    70  	// Privileged Gives extended privileges to the container
    71  	Privileged bool
    72  	// Interactive Makes the container interative and enables a tty.
    73  	Interactive bool
    74  	// Env Environment variables to pass to the container. This field
    75  	// supports :doc:`variables`.
    76  	// type: list of ``key=value`` strings
    77  	Env []string
    78  	// ProvideDocker Exposes the docker engine to the container by either
    79  	// mounting the unix socket or setting the ``DOCKER_HOST`` environment
    80  	// variable. All environment variables with a  ``DOCKER_`` prefix in the
    81  	// environment are set on the container.
    82  	ProvideDocker bool
    83  	// NetMode The network mode to use. This field supports :doc:`variables`.
    84  	NetMode string
    85  	// WorkingDir The directory to set as the active working directory in the
    86  	// container. This field supports :doc:`variables`.
    87  	WorkingDir string
    88  	// User Username or UID to use in the container. Format ``user[:group]``.
    89  	User string
    90  	// Ports Publish ports to the host
    91  	// type: list of 'host_port:container_port'
    92  	Ports []string
    93  	// Devices Maps the host devices you want to connect to a container
    94  	// type: list of device specs
    95  	// example: ``{Host: /dev/fb0, Container: /dev/fb0, Permissions: rwm}``
    96  	Devices []Device
    97  	// Labels sets the labels of the running job container
    98  	// type: map of string keys to string values
    99  	Labels map[string]string
   100  	Dependent
   101  	Annotations
   102  }
   103  
   104  // Device is the defined structure to attach host devices to containers
   105  type Device struct {
   106  	Host        string
   107  	Container   string
   108  	Permissions string
   109  }
   110  
   111  // Dependencies returns the list of implicit and explicit dependencies
   112  func (c *JobConfig) Dependencies() []string {
   113  	return append([]string{c.Use}, append(c.Depends, c.Mounts...)...)
   114  }
   115  
   116  // Validate checks that all fields have acceptable values
   117  func (c *JobConfig) Validate(path pth.Path, config *Config) *pth.Error {
   118  	validators := []validator{
   119  		newValidator("use", func() error { return c.validateUse(config) }),
   120  		newValidator("mounts", func() error { return c.validateMounts(config) }),
   121  		newValidator("artifact", c.Artifact.Validate),
   122  		newValidator("sources", c.Sources.Validate),
   123  	}
   124  	for _, validator := range validators {
   125  		if err := validator.validate(); err != nil {
   126  			return pth.Errorf(path.Add(validator.name), err.Error())
   127  		}
   128  	}
   129  	return nil
   130  }
   131  
   132  func (c *JobConfig) validateUse(config *Config) error {
   133  	err := fmt.Errorf("%s is not an image resource", c.Use)
   134  
   135  	res, ok := config.Resources[c.Use]
   136  	if !ok {
   137  		return err
   138  	}
   139  
   140  	switch res.(type) {
   141  	case *ImageConfig:
   142  	default:
   143  		return err
   144  	}
   145  
   146  	return nil
   147  }
   148  
   149  func (c *JobConfig) validateMounts(config *Config) error {
   150  	for _, mount := range c.Mounts {
   151  		err := fmt.Errorf("%s is not a mount resource", mount)
   152  
   153  		res, ok := config.Resources[mount]
   154  		if !ok {
   155  			return err
   156  		}
   157  
   158  		switch res.(type) {
   159  		case *MountConfig:
   160  		default:
   161  			return err
   162  		}
   163  	}
   164  	return nil
   165  }
   166  
   167  func (c *JobConfig) String() string {
   168  	artifact, command := "", ""
   169  	if !c.Artifact.Empty() {
   170  		artifact = fmt.Sprintf(" to create '%s'", &c.Artifact)
   171  	}
   172  	// TODO: look for entrypoint as well as command
   173  	if !c.Command.Empty() {
   174  		command = fmt.Sprintf("'%s' using ", c.Command.String())
   175  	}
   176  	return fmt.Sprintf("Run %sthe '%s' image%s", command, c.Use, artifact)
   177  }
   178  
   179  // Resolve resolves variables in the resource
   180  func (c *JobConfig) Resolve(resolver Resolver) (Resource, error) {
   181  	conf := *c
   182  	var err error
   183  	conf.Env, err = resolver.ResolveSlice(c.Env)
   184  	if err != nil {
   185  		return &conf, err
   186  	}
   187  	conf.WorkingDir, err = resolver.Resolve(c.WorkingDir)
   188  	if err != nil {
   189  		return &conf, err
   190  	}
   191  	conf.User, err = resolver.Resolve(c.User)
   192  	if err != nil {
   193  		return &conf, err
   194  	}
   195  	conf.NetMode, err = resolver.Resolve(c.NetMode)
   196  	return &conf, err
   197  }
   198  
   199  // ShlexSlice is a type used for config transforming a string into a []string
   200  // using shelx.
   201  type ShlexSlice struct {
   202  	original string
   203  	parsed   []string
   204  }
   205  
   206  func (s *ShlexSlice) String() string {
   207  	return s.original
   208  }
   209  
   210  // Value returns the slice value
   211  func (s *ShlexSlice) Value() []string {
   212  	return s.parsed
   213  }
   214  
   215  // Empty returns true if the instance contains the zero value
   216  func (s *ShlexSlice) Empty() bool {
   217  	return s.original == ""
   218  }
   219  
   220  // TransformConfig is used to transform a string from a config file into a
   221  // sliced value, using shlex.
   222  func (s *ShlexSlice) TransformConfig(raw reflect.Value) error {
   223  	if !raw.IsValid() {
   224  		return fmt.Errorf("must be a string, was undefined")
   225  	}
   226  
   227  	var err error
   228  	switch value := raw.Interface().(type) {
   229  	case string:
   230  		s.original = value
   231  		s.parsed, err = shlex.Split(value)
   232  		if err != nil {
   233  			return fmt.Errorf("failed to parse command %q: %s", value, err)
   234  		}
   235  	default:
   236  		return fmt.Errorf("must be a string, not %T", value)
   237  	}
   238  	return nil
   239  }
   240  
   241  func jobFromConfig(name string, values map[string]interface{}) (Resource, error) {
   242  	isTerminal := terminal.IsTerminal(int(os.Stdin.Fd()))
   243  	cmd := &JobConfig{}
   244  	if isTerminal {
   245  		if _, ok := values["interactive"]; !ok {
   246  			values["interactive"] = true
   247  		}
   248  	}
   249  	return cmd, configtf.Transform(name, values, cmd)
   250  }
   251  
   252  func init() {
   253  	RegisterResource("job", jobFromConfig)
   254  	// Backwards compatibility for v0.4, remove in v1.0
   255  	RegisterResource("run", jobFromConfig)
   256  }