github.com/beanworks/dcm@v0.0.0-20230726194615-49d2d0417e04/src/dcm.go (about)

     1  package main
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  )
     9  
    10  type doForService func(string, yamlConfig) (int, error)
    11  
    12  type Dcm struct {
    13  	Config *Config
    14  	Args   []string
    15  	Cmd    Executable
    16  }
    17  
    18  func NewDcm(c *Config, args []string) *Dcm {
    19  	return &Dcm{c, args, NewCmd()}
    20  }
    21  
    22  func (d *Dcm) Command() (int, error) {
    23  	if len(d.Args) < 1 {
    24  		d.Usage()
    25  		return 1, nil
    26  	}
    27  
    28  	moreArgs := d.Args[1:]
    29  
    30  	switch d.Args[0] {
    31  	case "help", "h":
    32  		d.Usage()
    33  		return 0, nil
    34  	case "setup":
    35  		return d.Setup()
    36  	case "run", "r":
    37  		return d.Run(moreArgs...)
    38  	case "build", "b":
    39  		return d.Run("build")
    40  	case "dir":
    41  		return d.Dir(moreArgs...)
    42  	case "shell", "sh":
    43  		return d.Shell(moreArgs...)
    44  	case "branch", "br":
    45  		return d.Branch(moreArgs...)
    46  	case "update":
    47  		return d.Update(moreArgs...)
    48  	case "purge", "rm":
    49  		return d.Purge(moreArgs...)
    50  	case "list", "ls":
    51  		return d.List()
    52  	default:
    53  		d.Usage()
    54  		return 127, nil
    55  	}
    56  }
    57  
    58  func (d *Dcm) Setup() (int, error) {
    59  	if _, err := os.Stat(d.Config.Srv); os.IsNotExist(err) {
    60  		os.MkdirAll(d.Config.Srv, 0777)
    61  	}
    62  
    63  	return d.doForEachService(func(service string, configs yamlConfig) (int, error) {
    64  		_, ok := getMapVal(configs, "image").(string)
    65  		if ok {
    66  			// If image is defined for the service, then skip
    67  			// checking out the repository
    68  			return 0, nil
    69  		}
    70  		repo, ok := getMapVal(configs, "labels", "dcm.repository").(string)
    71  		if !ok {
    72  			return 1, fmt.Errorf(
    73  				"Error reading git repository config for service [%s]",
    74  				service,
    75  			)
    76  		}
    77  		dir := d.Config.Srv + "/" + service
    78  		if _, err := os.Stat(dir); err == nil {
    79  			fmt.Printf("Skipping git clone for %s. Service folder already exists.\n", service)
    80  			return 0, nil
    81  		}
    82  		c := d.Cmd.Exec("git", "clone", repo, dir).Setdir(d.Config.Dir)
    83  		if err := c.Run(); err != nil {
    84  			return 1, fmt.Errorf(
    85  				"Error cloning git repository for service [%s]: %v",
    86  				service, err,
    87  			)
    88  		}
    89  		branch, ok := getMapVal(configs, "labels", "dcm.branch").(string)
    90  		if ok {
    91  			c = d.Cmd.Exec("git", "checkout", branch).Setdir(dir)
    92  			if err := c.Run(); err != nil {
    93  				return 1, err
    94  			}
    95  		}
    96  		return 0, nil
    97  	})
    98  }
    99  
   100  func (d *Dcm) doForEachService(fn doForService) (int, error) {
   101  	for service, configs := range d.Config.Config {
   102  		service, _ := service.(string)
   103  		configs, ok := configs.(yamlConfig)
   104  		if !ok {
   105  			return 1, fmt.Errorf("Error reading configs for service: %s", service)
   106  		}
   107  
   108  		code, err := fn(service, configs)
   109  		if err != nil {
   110  			if code == 0 {
   111  				fmt.Println(err)
   112  			} else {
   113  				// Only when error code is not zero and error is not nil
   114  				// then break the iteration and return
   115  				return code, err
   116  			}
   117  		}
   118  	}
   119  
   120  	return 0, nil
   121  }
   122  
   123  func (d *Dcm) Run(args ...string) (int, error) {
   124  	if len(args) == 0 {
   125  		args = append(args, "default")
   126  	}
   127  
   128  	switch args[0] {
   129  	case "execute":
   130  		return d.runExecute(args[1:]...)
   131  	case "init":
   132  		fmt.Println("Initializing project:", d.Config.Project, "...")
   133  		return d.runInit()
   134  	case "pre-init":
   135  		fmt.Println("Pre-initializating project", d.Config.Project, "...")
   136  		return d.runPreInit()
   137  	case "build":
   138  		fmt.Println("Building project:", d.Config.Project, "...")
   139  		return d.Run("execute", "build")
   140  	case "start":
   141  		fmt.Println("Starting project:", d.Config.Project, "...")
   142  		return d.Run("execute", "start")
   143  	case "stop":
   144  		fmt.Println("Stopping project:", d.Config.Project, "...")
   145  		return d.Run("execute", "stop")
   146  	case "restart":
   147  		fmt.Println("Restarting project:", d.Config.Project, "...")
   148  		return d.Run("execute", "restart")
   149  	case "up":
   150  		fmt.Println("Bringing up project:", d.Config.Project, "...")
   151  		return d.runUp()
   152  	default:
   153  		return d.Run("up")
   154  	}
   155  }
   156  
   157  func (d *Dcm) runExecute(args ...string) (int, error) {
   158  	env := append(
   159  		os.Environ(),
   160  		"COMPOSE_PROJECT_NAME="+d.Config.Project,
   161  		"COMPOSE_FILE="+d.Config.File,
   162  	)
   163  	c := d.Cmd.
   164  		Exec("docker-compose", args...).
   165  		Setdir(d.Config.Dir).
   166  		Setenv(env)
   167  	if err := c.Run(); err != nil {
   168  		return 1, fmt.Errorf(
   169  			"Error executing `docker-compose %s`: %v",
   170  			strings.Join(args, " "), err,
   171  		)
   172  	}
   173  	return 0, nil
   174  }
   175  
   176  func (d *Dcm) runInit() (int, error) {
   177  	return d.doForEachService(func(service string, configs yamlConfig) (int, error) {
   178  		shell := d.getShellExecutable(configs)
   179  		init, ok := getMapVal(configs, "labels", "dcm.initscript").(string)
   180  		if !ok {
   181  			fmt.Println("Skipping init script for service:", service, "...")
   182  			return 0, nil
   183  		}
   184  
   185  		c := d.Cmd.Exec(shell, init).Setdir(d.Config.Srv + "/" + service)
   186  		if err := c.Run(); err != nil {
   187  			return 1, fmt.Errorf(
   188  				"Error executing init script [%s] for service [%s]: %v",
   189  				init, service, err,
   190  			)
   191  		}
   192  		return 0, nil
   193  	})
   194  }
   195  
   196  func (d *Dcm) runPreInit() (int, error) {
   197  	return d.doForEachService(func(service string, configs yamlConfig) (int, error) {
   198  		shell := d.getShellExecutable(configs)
   199  		preInit, ok := getMapVal(configs, "labels", "dcm.pre_initscript").(string)
   200  		if !ok {
   201  			return 0, nil
   202  		}
   203  
   204  		c := d.Cmd.Exec(shell, preInit).Setdir(d.Config.Srv + "/" + service)
   205  		if err := c.Run(); err != nil {
   206  			return 1, fmt.Errorf(
   207  				"Error executing pre-init script [%s] for service [%s]: %v",
   208  				preInit, service, err,
   209  			)
   210  		}
   211  		return 0, nil
   212  	})
   213  }
   214  
   215  func (d *Dcm) runUp() (int, error) {
   216  	code, err := d.Run("pre-init")
   217  	if err != nil {
   218  		return code, err
   219  	}
   220  
   221  	code, err = d.Run("execute", "up", "-d", "--force-recreate")
   222  	if err != nil {
   223  		return code, err
   224  	}
   225  	return d.Run("init")
   226  }
   227  
   228  func (d *Dcm) Dir(args ...string) (int, error) {
   229  	var dir string
   230  	if len(args) < 1 {
   231  		dir = d.Config.Dir
   232  	} else {
   233  		dir = d.Config.Srv + "/" + args[0]
   234  		if _, err := os.Stat(dir); os.IsNotExist(err) {
   235  			dir = d.Config.Dir
   236  		}
   237  	}
   238  	fmt.Fprint(os.Stdout, dir)
   239  	return 0, nil
   240  }
   241  
   242  func (d *Dcm) Shell(args ...string) (int, error) {
   243  	if len(args) < 1 {
   244  		return 1, errors.New("Error: no service name specified.")
   245  	}
   246  
   247  	cid, err := d.getContainerId(args[0], "-qf")
   248  	if err != nil {
   249  		return 1, err
   250  	}
   251  
   252  	if err := d.Cmd.Exec("docker", "exec", "-it", cid, "bash").Run(); err != nil {
   253  		return 1, err
   254  	}
   255  
   256  	return 0, nil
   257  }
   258  
   259  func (d *Dcm) getShellExecutable(configs yamlConfig) string {
   260  	var shell = "/bin/bash"
   261  	shellDefinition, ok := getMapVal(configs, "labels", "dcm.initscript_shell").(string)
   262  	if ok {
   263  		shell = shellDefinition
   264  	}
   265  
   266  	return shell
   267  }
   268  
   269  func (d *Dcm) getContainerId(service string, flag string) (string, error) {
   270  	var filterTemplate string
   271  	if flag == "" {
   272  		flag = "-aq"
   273  	}
   274  
   275  	// Find docker-compose version
   276  	dcVersion, err := d.Cmd.Exec("docker-compose", "--version", "--short").Out()
   277  	if err != nil {
   278  		return "", d.Cmd.FormatError(err, dcVersion)
   279  	}
   280  
   281  	// V1 filter
   282  	filterTemplate = "name=%s_%s_"
   283  	if strings.HasPrefix(string(dcVersion), "2") {
   284  		// V2 filter
   285  		filterTemplate = "name=%s-%s-"
   286  	}
   287  	filter := fmt.Sprintf(filterTemplate, d.Config.Project, service)
   288  
   289  	out, err := d.Cmd.Exec("docker", "ps", flag, filter).Out()
   290  	if err != nil {
   291  		return "", d.Cmd.FormatError(err, out)
   292  	}
   293  	cid := d.Cmd.FormatOutput(out)
   294  	return cid, nil
   295  }
   296  
   297  func (d *Dcm) getImageRepository(service string) (string, error) {
   298  	repo := d.Config.Project + "_" + service
   299  	out, err := d.Cmd.Exec("docker", "images").Out()
   300  	if err != nil {
   301  		return "", d.Cmd.FormatError(err, out)
   302  	}
   303  	if strings.Contains(string(out), repo+" ") {
   304  		return repo, nil
   305  	}
   306  	return "", nil
   307  }
   308  
   309  func (d *Dcm) Branch(args ...string) (int, error) {
   310  	if len(args) < 1 {
   311  		return d.branchForAll()
   312  	} else {
   313  		return d.branchForOne(args[0])
   314  	}
   315  }
   316  
   317  func (d *Dcm) branchForAll() (int, error) {
   318  	code, err := d.branchForOne("dcm")
   319  	if err != nil {
   320  		return code, err
   321  	}
   322  	return d.doForEachService(func(service string, configs yamlConfig) (int, error) {
   323  		return d.branchForOne(service)
   324  	})
   325  }
   326  
   327  func (d *Dcm) branchForOne(service string) (int, error) {
   328  	var dir string
   329  
   330  	fmt.Print(service + ": ")
   331  
   332  	if service == "dcm" {
   333  		fmt.Print("branch: ")
   334  		dir = d.Config.Dir
   335  	} else {
   336  		configs, ok := getMapVal(d.Config.Config, service).(yamlConfig)
   337  		if !ok {
   338  			return 0, errors.New("Service not exists.")
   339  		}
   340  		if image, ok := getMapVal(configs, "image").(string); ok {
   341  			fmt.Println("Docker hub image:", image)
   342  			return 0, nil
   343  		}
   344  		if repo, ok := getMapVal(configs, "labels", "dcm.repository").(string); ok {
   345  			fmt.Print("Git repo: ", repo, ", branch: ")
   346  		}
   347  		dir = d.Config.Srv + "/" + service
   348  	}
   349  	if err := os.Chdir(dir); err != nil {
   350  		return 0, err
   351  	}
   352  	if err := d.Cmd.Exec("git", "rev-parse", "--abbrev-ref", "HEAD").Run(); err != nil {
   353  		return 0, err
   354  	}
   355  
   356  	return 0, nil
   357  }
   358  
   359  func (d *Dcm) Update(args ...string) (int, error) {
   360  	if len(args) < 1 {
   361  		return d.updateForAll()
   362  	} else {
   363  		return d.updateForOne(args[0])
   364  	}
   365  }
   366  
   367  func (d *Dcm) updateForAll() (int, error) {
   368  	return d.doForEachService(func(service string, configs yamlConfig) (int, error) {
   369  		return d.updateForOne(service)
   370  	})
   371  }
   372  
   373  func (d *Dcm) updateForOne(service string) (int, error) {
   374  	fmt.Print(service + ": ")
   375  
   376  	configs, ok := getMapVal(d.Config.Config, service).(yamlConfig)
   377  	if !ok {
   378  		return 0, errors.New("Service not exists.")
   379  	}
   380  
   381  	updateable, ok := getMapVal(configs, "labels", "dcm.updateable").(string)
   382  	if ok && updateable == "false" {
   383  		// Service is flagged as not updateable
   384  		return 0, errors.New("Service not updateable. Skipping the update.")
   385  	}
   386  
   387  	image, ok := getMapVal(configs, "image").(string)
   388  	if ok {
   389  		// Service is using docker hub image
   390  		// Pull the latest version from docker hub
   391  		if err := d.Cmd.Exec("docker", "pull", image).Run(); err != nil {
   392  			return 0, err
   393  		}
   394  		return 0, nil
   395  	} else {
   396  		// Service is using a local build
   397  		// Pull the latest version from git
   398  		if err := os.Chdir(d.Config.Srv + "/" + service); err != nil {
   399  			return 0, err
   400  		}
   401  		branch, ok := getMapVal(configs, "labels", "dcm.branch").(string)
   402  		if !ok {
   403  			// When service > labels > dcm.branch is not defined in
   404  			// the yaml config file, use "master" as default branch
   405  			branch = "master"
   406  		}
   407  		if err := d.Cmd.Exec("git", "checkout", branch).Run(); err != nil {
   408  			return 0, err
   409  		}
   410  		if err := d.Cmd.Exec("git", "pull").Run(); err != nil {
   411  			return 0, err
   412  		}
   413  	}
   414  
   415  	return 0, nil
   416  }
   417  
   418  func (d *Dcm) Purge(args ...string) (int, error) {
   419  	if len(args) == 0 {
   420  		args = append(args, "default")
   421  	}
   422  
   423  	switch args[0] {
   424  	case "img", "images":
   425  		return d.purgeImages()
   426  	case "con", "containers":
   427  		return d.purgeContainers()
   428  	case "all":
   429  		return d.purgeAll()
   430  	default:
   431  		return d.Purge("containers")
   432  	}
   433  }
   434  
   435  func (d *Dcm) purgeImages() (int, error) {
   436  	return d.doForEachService(func(service string, configs yamlConfig) (int, error) {
   437  		repo, err := d.getImageRepository(service)
   438  		if err != nil {
   439  			return 0, err
   440  		}
   441  		if err = d.Cmd.Exec("docker", "rmi", repo).Run(); err != nil {
   442  			return 0, err
   443  		}
   444  		return 0, nil
   445  	})
   446  }
   447  
   448  func (d *Dcm) purgeContainers() (int, error) {
   449  	return d.doForEachService(func(service string, configs yamlConfig) (int, error) {
   450  		// Try to get the docker container ID from running containers list
   451  		cid, err := d.getContainerId(service, "-qf")
   452  		if err != nil {
   453  			return 0, err
   454  		}
   455  		if cid != "" {
   456  			// If the container is running then kill it first
   457  			if err := d.Cmd.Exec("docker", "kill", cid).Run(); err != nil {
   458  				return 0, err
   459  			}
   460  		} else {
   461  			// Otherwise, try to get the docker container ID from a list that
   462  			// contains all containers including not running ones
   463  			cid, err = d.getContainerId(service, "-aqf")
   464  			if err != nil {
   465  				return 0, err
   466  			}
   467  		}
   468  		if cid != "" {
   469  			// Finally if the container exists (whether running or not running),
   470  			// remove it along with all the volumes linked to it
   471  			if err := d.Cmd.Exec("docker", "rm", "-v", cid).Run(); err != nil {
   472  				return 0, err
   473  			}
   474  		}
   475  		return 0, nil
   476  	})
   477  }
   478  
   479  func (d *Dcm) purgeAll() (int, error) {
   480  	code, err := d.Purge("containers")
   481  	if err != nil {
   482  		return code, err
   483  	}
   484  	return d.Purge("images")
   485  }
   486  
   487  func (d *Dcm) List() (int, error) {
   488  	return d.doForEachService(func(service string, configs yamlConfig) (int, error) {
   489  		fmt.Fprintln(os.Stdout, service)
   490  		return 0, nil
   491  	})
   492  }
   493  
   494  func (d *Dcm) Usage() {
   495  	fmt.Println("")
   496  	fmt.Println("DCM (Docker-Compose Manager)")
   497  	fmt.Println("")
   498  	fmt.Println("Usage:")
   499  	fmt.Println("  dcm help                Show this help menu.")
   500  	fmt.Println("  dcm setup               Git checkout repositories for the services that require")
   501  	fmt.Println("                          local docker build. It skips the service when the image")
   502  	fmt.Println("                          is from docker hub, or the repo's folder already exists.")
   503  	fmt.Println("  dcm run [<args>]        Run docker-compose commands. If <args> is not given, by")
   504  	fmt.Println("                          default DCM will run `docker-compose up` command.")
   505  	fmt.Println("                          <args>: up, build, start, stop, restart, pre-init, init, execute")
   506  	fmt.Println("  dcm build               Docker (re)build service images that require local build.")
   507  	fmt.Println("                          It's the shorthand version of `dcm run build` command.")
   508  	fmt.Println("  dcm shell <service>     Log into a given service container.")
   509  	fmt.Println("  dcm purge [<type>]      Remove either all the containers or all the images. If <type>")
   510  	fmt.Println("                          is not given, by default DCM will purge everything.")
   511  	fmt.Println("                          <type>: images, containers, all")
   512  	fmt.Println("  dcm branch [<service>]  Display the current git branch for the given service that")
   513  	fmt.Println("                          was built locally.")
   514  	fmt.Println("  dcm goto [<service>]    Go to the service's folder. If <service> is not given, by")
   515  	fmt.Println("                          default DCM will go to $DCM_DIR.")
   516  	fmt.Println("  dcm update [<service>]  Update DCM and(or) the given service.")
   517  	fmt.Println("  dcm list                List all the available services.")
   518  	fmt.Println("")
   519  	fmt.Println("Example:")
   520  	fmt.Println("  Initial setup")
   521  	fmt.Println("    dcm setup")
   522  	fmt.Println("    dcm run")
   523  	fmt.Println("")
   524  	fmt.Println("  Rebuild")
   525  	fmt.Println("    dcm build")
   526  	fmt.Println("    dcm run")
   527  	fmt.Println("")
   528  	fmt.Println("  Or only Rerun")
   529  	fmt.Println("    dcm run")
   530  	fmt.Println("")
   531  	fmt.Println("  Log into a service's container")
   532  	fmt.Println("    dcm shell service_name")
   533  	fmt.Println("")
   534  }