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 }