github.com/mheon/docker@v0.11.2-0.20150922122814-44f47903a831/builder/dispatchers.go (about)

     1  package builder
     2  
     3  // This file contains the dispatchers for each command. Note that
     4  // `nullDispatch` is not actually a command, but support for commands we parse
     5  // but do nothing with.
     6  //
     7  // See evaluator.go for a higher level discussion of the whole evaluator
     8  // package.
     9  
    10  import (
    11  	"fmt"
    12  	"io/ioutil"
    13  	"os"
    14  	"path/filepath"
    15  	"regexp"
    16  	"runtime"
    17  	"sort"
    18  	"strings"
    19  
    20  	"github.com/Sirupsen/logrus"
    21  	derr "github.com/docker/docker/errors"
    22  	flag "github.com/docker/docker/pkg/mflag"
    23  	"github.com/docker/docker/pkg/nat"
    24  	"github.com/docker/docker/pkg/signal"
    25  	"github.com/docker/docker/pkg/stringutils"
    26  	"github.com/docker/docker/pkg/system"
    27  	"github.com/docker/docker/runconfig"
    28  )
    29  
    30  const (
    31  	// NoBaseImageSpecifier is the symbol used by the FROM
    32  	// command to specify that no base image is to be used.
    33  	NoBaseImageSpecifier string = "scratch"
    34  )
    35  
    36  // dispatch with no layer / parsing. This is effectively not a command.
    37  func nullDispatch(b *builder, args []string, attributes map[string]bool, original string) error {
    38  	return nil
    39  }
    40  
    41  // ENV foo bar
    42  //
    43  // Sets the environment variable foo to bar, also makes interpolation
    44  // in the dockerfile available from the next statement on via ${foo}.
    45  //
    46  func env(b *builder, args []string, attributes map[string]bool, original string) error {
    47  	if len(args) == 0 {
    48  		return derr.ErrorCodeAtLeastOneArg.WithArgs("ENV")
    49  	}
    50  
    51  	if len(args)%2 != 0 {
    52  		// should never get here, but just in case
    53  		return derr.ErrorCodeTooManyArgs.WithArgs("ENV")
    54  	}
    55  
    56  	if err := b.BuilderFlags.Parse(); err != nil {
    57  		return err
    58  	}
    59  
    60  	// TODO/FIXME/NOT USED
    61  	// Just here to show how to use the builder flags stuff within the
    62  	// context of a builder command. Will remove once we actually add
    63  	// a builder command to something!
    64  	/*
    65  		flBool1 := b.BuilderFlags.AddBool("bool1", false)
    66  		flStr1 := b.BuilderFlags.AddString("str1", "HI")
    67  
    68  		if err := b.BuilderFlags.Parse(); err != nil {
    69  			return err
    70  		}
    71  
    72  		fmt.Printf("Bool1:%v\n", flBool1)
    73  		fmt.Printf("Str1:%v\n", flStr1)
    74  	*/
    75  
    76  	commitStr := "ENV"
    77  
    78  	for j := 0; j < len(args); j++ {
    79  		// name  ==> args[j]
    80  		// value ==> args[j+1]
    81  		newVar := args[j] + "=" + args[j+1] + ""
    82  		commitStr += " " + newVar
    83  
    84  		gotOne := false
    85  		for i, envVar := range b.Config.Env {
    86  			envParts := strings.SplitN(envVar, "=", 2)
    87  			if envParts[0] == args[j] {
    88  				b.Config.Env[i] = newVar
    89  				gotOne = true
    90  				break
    91  			}
    92  		}
    93  		if !gotOne {
    94  			b.Config.Env = append(b.Config.Env, newVar)
    95  		}
    96  		j++
    97  	}
    98  
    99  	return b.commit("", b.Config.Cmd, commitStr)
   100  }
   101  
   102  // MAINTAINER some text <maybe@an.email.address>
   103  //
   104  // Sets the maintainer metadata.
   105  func maintainer(b *builder, args []string, attributes map[string]bool, original string) error {
   106  	if len(args) != 1 {
   107  		return derr.ErrorCodeExactlyOneArg.WithArgs("MAINTAINER")
   108  	}
   109  
   110  	if err := b.BuilderFlags.Parse(); err != nil {
   111  		return err
   112  	}
   113  
   114  	b.maintainer = args[0]
   115  	return b.commit("", b.Config.Cmd, fmt.Sprintf("MAINTAINER %s", b.maintainer))
   116  }
   117  
   118  // LABEL some json data describing the image
   119  //
   120  // Sets the Label variable foo to bar,
   121  //
   122  func label(b *builder, args []string, attributes map[string]bool, original string) error {
   123  	if len(args) == 0 {
   124  		return derr.ErrorCodeAtLeastOneArg.WithArgs("LABEL")
   125  	}
   126  	if len(args)%2 != 0 {
   127  		// should never get here, but just in case
   128  		return derr.ErrorCodeTooManyArgs.WithArgs("LABEL")
   129  	}
   130  
   131  	if err := b.BuilderFlags.Parse(); err != nil {
   132  		return err
   133  	}
   134  
   135  	commitStr := "LABEL"
   136  
   137  	if b.Config.Labels == nil {
   138  		b.Config.Labels = map[string]string{}
   139  	}
   140  
   141  	for j := 0; j < len(args); j++ {
   142  		// name  ==> args[j]
   143  		// value ==> args[j+1]
   144  		newVar := args[j] + "=" + args[j+1] + ""
   145  		commitStr += " " + newVar
   146  
   147  		b.Config.Labels[args[j]] = args[j+1]
   148  		j++
   149  	}
   150  	return b.commit("", b.Config.Cmd, commitStr)
   151  }
   152  
   153  // ADD foo /path
   154  //
   155  // Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling
   156  // exist here. If you do not wish to have this automatic handling, use COPY.
   157  //
   158  func add(b *builder, args []string, attributes map[string]bool, original string) error {
   159  	if len(args) < 2 {
   160  		return derr.ErrorCodeAtLeastTwoArgs.WithArgs("ADD")
   161  	}
   162  
   163  	if err := b.BuilderFlags.Parse(); err != nil {
   164  		return err
   165  	}
   166  
   167  	return b.runContextCommand(args, true, true, "ADD")
   168  }
   169  
   170  // COPY foo /path
   171  //
   172  // Same as 'ADD' but without the tar and remote url handling.
   173  //
   174  func dispatchCopy(b *builder, args []string, attributes map[string]bool, original string) error {
   175  	if len(args) < 2 {
   176  		return derr.ErrorCodeAtLeastTwoArgs.WithArgs("COPY")
   177  	}
   178  
   179  	if err := b.BuilderFlags.Parse(); err != nil {
   180  		return err
   181  	}
   182  
   183  	return b.runContextCommand(args, false, false, "COPY")
   184  }
   185  
   186  // FROM imagename
   187  //
   188  // This sets the image the dockerfile will build on top of.
   189  //
   190  func from(b *builder, args []string, attributes map[string]bool, original string) error {
   191  	if len(args) != 1 {
   192  		return derr.ErrorCodeExactlyOneArg.WithArgs("FROM")
   193  	}
   194  
   195  	if err := b.BuilderFlags.Parse(); err != nil {
   196  		return err
   197  	}
   198  
   199  	name := args[0]
   200  
   201  	// Windows cannot support a container with no base image.
   202  	if name == NoBaseImageSpecifier {
   203  		if runtime.GOOS == "windows" {
   204  			return fmt.Errorf("Windows does not support FROM scratch")
   205  		}
   206  		b.image = ""
   207  		b.noBaseImage = true
   208  		return nil
   209  	}
   210  
   211  	image, err := b.Daemon.Repositories().LookupImage(name)
   212  	if b.Pull {
   213  		image, err = b.pullImage(name)
   214  		if err != nil {
   215  			return err
   216  		}
   217  	}
   218  	if err != nil {
   219  		if b.Daemon.Graph().IsNotExist(err, name) {
   220  			image, err = b.pullImage(name)
   221  		}
   222  
   223  		// note that the top level err will still be !nil here if IsNotExist is
   224  		// not the error. This approach just simplifies the logic a bit.
   225  		if err != nil {
   226  			return err
   227  		}
   228  	}
   229  
   230  	return b.processImageFrom(image)
   231  }
   232  
   233  // ONBUILD RUN echo yo
   234  //
   235  // ONBUILD triggers run when the image is used in a FROM statement.
   236  //
   237  // ONBUILD handling has a lot of special-case functionality, the heading in
   238  // evaluator.go and comments around dispatch() in the same file explain the
   239  // special cases. search for 'OnBuild' in internals.go for additional special
   240  // cases.
   241  //
   242  func onbuild(b *builder, args []string, attributes map[string]bool, original string) error {
   243  	if len(args) == 0 {
   244  		return derr.ErrorCodeAtLeastOneArg.WithArgs("ONBUILD")
   245  	}
   246  
   247  	if err := b.BuilderFlags.Parse(); err != nil {
   248  		return err
   249  	}
   250  
   251  	triggerInstruction := strings.ToUpper(strings.TrimSpace(args[0]))
   252  	switch triggerInstruction {
   253  	case "ONBUILD":
   254  		return derr.ErrorCodeChainOnBuild
   255  	case "MAINTAINER", "FROM":
   256  		return derr.ErrorCodeBadOnBuildCmd.WithArgs(triggerInstruction)
   257  	}
   258  
   259  	original = regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(original, "")
   260  
   261  	b.Config.OnBuild = append(b.Config.OnBuild, original)
   262  	return b.commit("", b.Config.Cmd, fmt.Sprintf("ONBUILD %s", original))
   263  }
   264  
   265  // WORKDIR /tmp
   266  //
   267  // Set the working directory for future RUN/CMD/etc statements.
   268  //
   269  func workdir(b *builder, args []string, attributes map[string]bool, original string) error {
   270  	if len(args) != 1 {
   271  		return derr.ErrorCodeExactlyOneArg.WithArgs("WORKDIR")
   272  	}
   273  
   274  	if err := b.BuilderFlags.Parse(); err != nil {
   275  		return err
   276  	}
   277  
   278  	// This is from the Dockerfile and will not necessarily be in platform
   279  	// specific semantics, hence ensure it is converted.
   280  	workdir := filepath.FromSlash(args[0])
   281  
   282  	if !system.IsAbs(workdir) {
   283  		current := filepath.FromSlash(b.Config.WorkingDir)
   284  		workdir = filepath.Join(string(os.PathSeparator), current, workdir)
   285  	}
   286  
   287  	b.Config.WorkingDir = workdir
   288  
   289  	return b.commit("", b.Config.Cmd, fmt.Sprintf("WORKDIR %v", workdir))
   290  }
   291  
   292  // RUN some command yo
   293  //
   294  // run a command and commit the image. Args are automatically prepended with
   295  // 'sh -c' under linux or 'cmd /S /C' under Windows, in the event there is
   296  // only one argument. The difference in processing:
   297  //
   298  // RUN echo hi          # sh -c echo hi       (Linux)
   299  // RUN echo hi          # cmd /S /C echo hi   (Windows)
   300  // RUN [ "echo", "hi" ] # echo hi
   301  //
   302  func run(b *builder, args []string, attributes map[string]bool, original string) error {
   303  	if b.image == "" && !b.noBaseImage {
   304  		return derr.ErrorCodeMissingFrom
   305  	}
   306  
   307  	if err := b.BuilderFlags.Parse(); err != nil {
   308  		return err
   309  	}
   310  
   311  	args = handleJSONArgs(args, attributes)
   312  
   313  	if !attributes["json"] {
   314  		if runtime.GOOS != "windows" {
   315  			args = append([]string{"/bin/sh", "-c"}, args...)
   316  		} else {
   317  			args = append([]string{"cmd", "/S /C"}, args...)
   318  		}
   319  	}
   320  
   321  	runCmd := flag.NewFlagSet("run", flag.ContinueOnError)
   322  	runCmd.SetOutput(ioutil.Discard)
   323  	runCmd.Usage = nil
   324  
   325  	config, _, _, err := runconfig.Parse(runCmd, append([]string{b.image}, args...))
   326  	if err != nil {
   327  		return err
   328  	}
   329  
   330  	// stash the cmd
   331  	cmd := b.Config.Cmd
   332  	runconfig.Merge(b.Config, config)
   333  	// stash the config environment
   334  	env := b.Config.Env
   335  
   336  	defer func(cmd *stringutils.StrSlice) { b.Config.Cmd = cmd }(cmd)
   337  	defer func(env []string) { b.Config.Env = env }(env)
   338  
   339  	// derive the net build-time environment for this run. We let config
   340  	// environment override the build time environment.
   341  	// This means that we take the b.buildArgs list of env vars and remove
   342  	// any of those variables that are defined as part of the container. In other
   343  	// words, anything in b.Config.Env. What's left is the list of build-time env
   344  	// vars that we need to add to each RUN command - note the list could be empty.
   345  	//
   346  	// We don't persist the build time environment with container's config
   347  	// environment, but just sort and prepend it to the command string at time
   348  	// of commit.
   349  	// This helps with tracing back the image's actual environment at the time
   350  	// of RUN, without leaking it to the final image. It also aids cache
   351  	// lookup for same image built with same build time environment.
   352  	cmdBuildEnv := []string{}
   353  	configEnv := runconfig.ConvertKVStringsToMap(b.Config.Env)
   354  	for key, val := range b.buildArgs {
   355  		if !b.isBuildArgAllowed(key) {
   356  			// skip build-args that are not in allowed list, meaning they have
   357  			// not been defined by an "ARG" Dockerfile command yet.
   358  			// This is an error condition but only if there is no "ARG" in the entire
   359  			// Dockerfile, so we'll generate any necessary errors after we parsed
   360  			// the entire file (see 'leftoverArgs' processing in evaluator.go )
   361  			continue
   362  		}
   363  		if _, ok := configEnv[key]; !ok {
   364  			cmdBuildEnv = append(cmdBuildEnv, fmt.Sprintf("%s=%s", key, val))
   365  		}
   366  	}
   367  
   368  	// derive the command to use for probeCache() and to commit in this container.
   369  	// Note that we only do this if there are any build-time env vars.  Also, we
   370  	// use the special argument "|#" at the start of the args array. This will
   371  	// avoid conflicts with any RUN command since commands can not
   372  	// start with | (vertical bar). The "#" (number of build envs) is there to
   373  	// help ensure proper cache matches. We don't want a RUN command
   374  	// that starts with "foo=abc" to be considered part of a build-time env var.
   375  	saveCmd := config.Cmd
   376  	if len(cmdBuildEnv) > 0 {
   377  		sort.Strings(cmdBuildEnv)
   378  		tmpEnv := append([]string{fmt.Sprintf("|%d", len(cmdBuildEnv))}, cmdBuildEnv...)
   379  		saveCmd = stringutils.NewStrSlice(append(tmpEnv, saveCmd.Slice()...)...)
   380  	}
   381  
   382  	b.Config.Cmd = saveCmd
   383  	hit, err := b.probeCache()
   384  	if err != nil {
   385  		return err
   386  	}
   387  	if hit {
   388  		return nil
   389  	}
   390  
   391  	// set Cmd manually, this is special case only for Dockerfiles
   392  	b.Config.Cmd = config.Cmd
   393  	// set build-time environment for 'run'.
   394  	b.Config.Env = append(b.Config.Env, cmdBuildEnv...)
   395  
   396  	logrus.Debugf("[BUILDER] Command to be executed: %v", b.Config.Cmd)
   397  
   398  	c, err := b.create()
   399  	if err != nil {
   400  		return err
   401  	}
   402  
   403  	// Ensure that we keep the container mounted until the commit
   404  	// to avoid unmounting and then mounting directly again
   405  	c.Mount()
   406  	defer c.Unmount()
   407  
   408  	err = b.run(c)
   409  	if err != nil {
   410  		return err
   411  	}
   412  
   413  	// revert to original config environment and set the command string to
   414  	// have the build-time env vars in it (if any) so that future cache look-ups
   415  	// properly match it.
   416  	b.Config.Env = env
   417  	b.Config.Cmd = saveCmd
   418  	if err := b.commit(c.ID, cmd, "run"); err != nil {
   419  		return err
   420  	}
   421  
   422  	return nil
   423  }
   424  
   425  // CMD foo
   426  //
   427  // Set the default command to run in the container (which may be empty).
   428  // Argument handling is the same as RUN.
   429  //
   430  func cmd(b *builder, args []string, attributes map[string]bool, original string) error {
   431  	if err := b.BuilderFlags.Parse(); err != nil {
   432  		return err
   433  	}
   434  
   435  	cmdSlice := handleJSONArgs(args, attributes)
   436  
   437  	if !attributes["json"] {
   438  		if runtime.GOOS != "windows" {
   439  			cmdSlice = append([]string{"/bin/sh", "-c"}, cmdSlice...)
   440  		} else {
   441  			cmdSlice = append([]string{"cmd", "/S /C"}, cmdSlice...)
   442  		}
   443  	}
   444  
   445  	b.Config.Cmd = stringutils.NewStrSlice(cmdSlice...)
   446  
   447  	if err := b.commit("", b.Config.Cmd, fmt.Sprintf("CMD %q", cmdSlice)); err != nil {
   448  		return err
   449  	}
   450  
   451  	if len(args) != 0 {
   452  		b.cmdSet = true
   453  	}
   454  
   455  	return nil
   456  }
   457  
   458  // ENTRYPOINT /usr/sbin/nginx
   459  //
   460  // Set the entrypoint (which defaults to sh -c on linux, or cmd /S /C on Windows) to
   461  // /usr/sbin/nginx. Will accept the CMD as the arguments to /usr/sbin/nginx.
   462  //
   463  // Handles command processing similar to CMD and RUN, only b.Config.Entrypoint
   464  // is initialized at NewBuilder time instead of through argument parsing.
   465  //
   466  func entrypoint(b *builder, args []string, attributes map[string]bool, original string) error {
   467  	if err := b.BuilderFlags.Parse(); err != nil {
   468  		return err
   469  	}
   470  
   471  	parsed := handleJSONArgs(args, attributes)
   472  
   473  	switch {
   474  	case attributes["json"]:
   475  		// ENTRYPOINT ["echo", "hi"]
   476  		b.Config.Entrypoint = stringutils.NewStrSlice(parsed...)
   477  	case len(parsed) == 0:
   478  		// ENTRYPOINT []
   479  		b.Config.Entrypoint = nil
   480  	default:
   481  		// ENTRYPOINT echo hi
   482  		if runtime.GOOS != "windows" {
   483  			b.Config.Entrypoint = stringutils.NewStrSlice("/bin/sh", "-c", parsed[0])
   484  		} else {
   485  			b.Config.Entrypoint = stringutils.NewStrSlice("cmd", "/S /C", parsed[0])
   486  		}
   487  	}
   488  
   489  	// when setting the entrypoint if a CMD was not explicitly set then
   490  	// set the command to nil
   491  	if !b.cmdSet {
   492  		b.Config.Cmd = nil
   493  	}
   494  
   495  	if err := b.commit("", b.Config.Cmd, fmt.Sprintf("ENTRYPOINT %q", b.Config.Entrypoint)); err != nil {
   496  		return err
   497  	}
   498  
   499  	return nil
   500  }
   501  
   502  // EXPOSE 6667/tcp 7000/tcp
   503  //
   504  // Expose ports for links and port mappings. This all ends up in
   505  // b.Config.ExposedPorts for runconfig.
   506  //
   507  func expose(b *builder, args []string, attributes map[string]bool, original string) error {
   508  	portsTab := args
   509  
   510  	if len(args) == 0 {
   511  		return derr.ErrorCodeAtLeastOneArg.WithArgs("EXPOSE")
   512  	}
   513  
   514  	if err := b.BuilderFlags.Parse(); err != nil {
   515  		return err
   516  	}
   517  
   518  	if b.Config.ExposedPorts == nil {
   519  		b.Config.ExposedPorts = make(nat.PortSet)
   520  	}
   521  
   522  	ports, _, err := nat.ParsePortSpecs(portsTab)
   523  	if err != nil {
   524  		return err
   525  	}
   526  
   527  	// instead of using ports directly, we build a list of ports and sort it so
   528  	// the order is consistent. This prevents cache burst where map ordering
   529  	// changes between builds
   530  	portList := make([]string, len(ports))
   531  	var i int
   532  	for port := range ports {
   533  		if _, exists := b.Config.ExposedPorts[port]; !exists {
   534  			b.Config.ExposedPorts[port] = struct{}{}
   535  		}
   536  		portList[i] = string(port)
   537  		i++
   538  	}
   539  	sort.Strings(portList)
   540  	return b.commit("", b.Config.Cmd, fmt.Sprintf("EXPOSE %s", strings.Join(portList, " ")))
   541  }
   542  
   543  // USER foo
   544  //
   545  // Set the user to 'foo' for future commands and when running the
   546  // ENTRYPOINT/CMD at container run time.
   547  //
   548  func user(b *builder, args []string, attributes map[string]bool, original string) error {
   549  	if len(args) != 1 {
   550  		return derr.ErrorCodeExactlyOneArg.WithArgs("USER")
   551  	}
   552  
   553  	if err := b.BuilderFlags.Parse(); err != nil {
   554  		return err
   555  	}
   556  
   557  	b.Config.User = args[0]
   558  	return b.commit("", b.Config.Cmd, fmt.Sprintf("USER %v", args))
   559  }
   560  
   561  // VOLUME /foo
   562  //
   563  // Expose the volume /foo for use. Will also accept the JSON array form.
   564  //
   565  func volume(b *builder, args []string, attributes map[string]bool, original string) error {
   566  	if len(args) == 0 {
   567  		return derr.ErrorCodeAtLeastOneArg.WithArgs("VOLUME")
   568  	}
   569  
   570  	if err := b.BuilderFlags.Parse(); err != nil {
   571  		return err
   572  	}
   573  
   574  	if b.Config.Volumes == nil {
   575  		b.Config.Volumes = map[string]struct{}{}
   576  	}
   577  	for _, v := range args {
   578  		v = strings.TrimSpace(v)
   579  		if v == "" {
   580  			return derr.ErrorCodeVolumeEmpty
   581  		}
   582  		b.Config.Volumes[v] = struct{}{}
   583  	}
   584  	if err := b.commit("", b.Config.Cmd, fmt.Sprintf("VOLUME %v", args)); err != nil {
   585  		return err
   586  	}
   587  	return nil
   588  }
   589  
   590  // STOPSIGNAL signal
   591  //
   592  // Set the signal that will be used to kill the container.
   593  func stopSignal(b *builder, args []string, attributes map[string]bool, original string) error {
   594  	if len(args) != 1 {
   595  		return fmt.Errorf("STOPSIGNAL requires exactly one argument")
   596  	}
   597  
   598  	sig := args[0]
   599  	_, err := signal.ParseSignal(sig)
   600  	if err != nil {
   601  		return err
   602  	}
   603  
   604  	b.Config.StopSignal = sig
   605  	return b.commit("", b.Config.Cmd, fmt.Sprintf("STOPSIGNAL %v", args))
   606  }
   607  
   608  // ARG name[=value]
   609  //
   610  // Adds the variable foo to the trusted list of variables that can be passed
   611  // to builder using the --build-arg flag for expansion/subsitution or passing to 'run'.
   612  // Dockerfile author may optionally set a default value of this variable.
   613  func arg(b *builder, args []string, attributes map[string]bool, original string) error {
   614  	if len(args) != 1 {
   615  		return fmt.Errorf("ARG requires exactly one argument definition")
   616  	}
   617  
   618  	var (
   619  		name       string
   620  		value      string
   621  		hasDefault bool
   622  	)
   623  
   624  	arg := args[0]
   625  	// 'arg' can just be a name or name-value pair. Note that this is different
   626  	// from 'env' that handles the split of name and value at the parser level.
   627  	// The reason for doing it differently for 'arg' is that we support just
   628  	// defining an arg and not assign it a value (while 'env' always expects a
   629  	// name-value pair). If possible, it will be good to harmonize the two.
   630  	if strings.Contains(arg, "=") {
   631  		parts := strings.SplitN(arg, "=", 2)
   632  		name = parts[0]
   633  		value = parts[1]
   634  		hasDefault = true
   635  	} else {
   636  		name = arg
   637  		hasDefault = false
   638  	}
   639  	// add the arg to allowed list of build-time args from this step on.
   640  	b.allowedBuildArgs[name] = true
   641  
   642  	// If there is a default value associated with this arg then add it to the
   643  	// b.buildArgs if one is not already passed to the builder. The args passed
   644  	// to builder override the defaut value of 'arg'.
   645  	if _, ok := b.buildArgs[name]; !ok && hasDefault {
   646  		b.buildArgs[name] = value
   647  	}
   648  
   649  	return b.commit("", b.Config.Cmd, fmt.Sprintf("ARG %s", arg))
   650  }