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 }