├── AUTHORS ├── bin └── docktitude ├── .jshintrc ├── docs └── docktitude-logo.png ├── NOTICE ├── demo └── docker-contexts │ ├── demo_site │ └── Dockerfile │ ├── idea │ └── Dockerfile │ ├── transmission │ └── Dockerfile │ ├── vscode │ └── Dockerfile │ ├── apache │ ├── apache │ │ └── Dockerfile │ └── activemq │ │ └── Dockerfile │ ├── debian-jdk8-ui │ └── Dockerfile │ ├── debian-jdk8 │ └── Dockerfile │ ├── debian-local │ └── Dockerfile │ ├── debian-jdk8-scm │ └── Dockerfile │ ├── gitlab │ └── gitlab_runner-local │ │ └── Dockerfile │ └── libreoffice │ └── Dockerfile ├── tsconfig.json ├── tsd.json ├── Dockerfile ├── src ├── lib │ ├── tree │ │ ├── constant.ts │ │ ├── builder.ts │ │ └── dataholder.ts │ ├── common.ts │ ├── board.ts │ ├── constant.ts │ ├── command.ts │ ├── util.ts │ └── tree.ts ├── test │ └── TreeBuilderTest.ts └── main.ts ├── completion ├── bash └── fish ├── package.json ├── README.md └── LICENSE /AUTHORS: -------------------------------------------------------------------------------- 1 | Johann Duton-Morgand 2 | -------------------------------------------------------------------------------- /bin/docktitude: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../main.js') 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "eqnull": true, 3 | "sub": true, 4 | "node": true 5 | } 6 | -------------------------------------------------------------------------------- /docs/docktitude-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/docktitude-zz/docktitude/HEAD/docs/docktitude-logo.png -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Docktitude 2 | Copyright 2015-2016 Docktitude 3 | 4 | This product includes software developed at Docktitude (http://docktitude.com). 5 | -------------------------------------------------------------------------------- /demo/docker-contexts/demo_site/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | MAINTAINER demo@docktitude.com 3 | 4 | # Hierarchy demo (content omitted for brevity) 5 | -------------------------------------------------------------------------------- /demo/docker-contexts/idea/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:jdk8-scm 2 | MAINTAINER demo@docktitude.com 3 | 4 | # Hierarchy demo (content omitted for brevity) 5 | -------------------------------------------------------------------------------- /demo/docker-contexts/transmission/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | MAINTAINER demo@docktitude.com 3 | 4 | # Hierarchy demo (content omitted for brevity) 5 | -------------------------------------------------------------------------------- /demo/docker-contexts/vscode/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:local 2 | MAINTAINER demo@docktitude.com 3 | 4 | # Hierarchy demo (content omitted for brevity) 5 | -------------------------------------------------------------------------------- /demo/docker-contexts/apache/apache/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:local 2 | MAINTAINER demo@docktitude.com 3 | 4 | # Hierarchy demo (content omitted for brevity) 5 | -------------------------------------------------------------------------------- /demo/docker-contexts/debian-jdk8-ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:jdk8 2 | MAINTAINER demo@docktitude.com 3 | 4 | # Hierarchy demo (content omitted for brevity) 5 | -------------------------------------------------------------------------------- /demo/docker-contexts/debian-jdk8/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:local 2 | MAINTAINER demo@docktitude.com 3 | 4 | # Hierarchy demo (content omitted for brevity) 5 | -------------------------------------------------------------------------------- /demo/docker-contexts/debian-local/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | MAINTAINER demo@docktitude.com 3 | 4 | # Hierarchy demo (content omitted for brevity) 5 | -------------------------------------------------------------------------------- /demo/docker-contexts/apache/activemq/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:jdk8 2 | MAINTAINER demo@docktitude.com 3 | 4 | # Hierarchy demo (content omitted for brevity) 5 | -------------------------------------------------------------------------------- /demo/docker-contexts/debian-jdk8-scm/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:jdk8-ui 2 | MAINTAINER demo@docktitude.com 3 | 4 | # Hierarchy demo (content omitted for brevity) 5 | -------------------------------------------------------------------------------- /demo/docker-contexts/gitlab/gitlab_runner-local/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | MAINTAINER demo@docktitude.com 3 | 4 | # Hierarchy demo (content omitted for brevity) 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noImplicitAny": true, 6 | "suppressImplicitAnyIndexErrors": true, 7 | "sourceMap": false, 8 | "noImplicitReturns": true, 9 | "removeComments": true, 10 | "preserveConstEnums": true, 11 | "outDir": "build" 12 | }, 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /demo/docker-contexts/libreoffice/Dockerfile: -------------------------------------------------------------------------------- 1 | #@[--DOCKTITUDE-SCRIPT 2 | #@ #!/bin/sh - 3 | #@ 4 | #@ docker run -it --rm --name libreoffice \ 5 | #@ -e DISPLAY=unix$DISPLAY \ 6 | #@ -e GDK_SCALE \ 7 | #@ -e GDK_DPI_SCALE \ 8 | #@ -v /tmp/.X11-unix:/tmp/.X11-unix \ 9 | #@ -v /etc/localtime:/etc/localtime:ro \ 10 | #@ libreoffice 11 | #@DOCKTITUDE-SCRIPT--] 12 | # 13 | FROM debian:jdk8-ui 14 | MAINTAINER demo@docktitude.com 15 | 16 | # Hierarchy demo (content omitted for brevity) 17 | -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "typings", 6 | "bundle": "typings/tsd.d.ts", 7 | "installed": { 8 | "node/node.d.ts": { 9 | "commit": "e8f8e7471b8ca92ca417f8c878f64ae4d1067972" 10 | }, 11 | "mocha/mocha.d.ts": { 12 | "commit": "94d8775a947f626269b06ccadd6f5e61839389d0" 13 | }, 14 | "chai/chai.d.ts": { 15 | "commit": "08ed4e9f1869e37e29514d862e0158b40e550232" 16 | }, 17 | "assertion-error/assertion-error.d.ts": { 18 | "commit": "08ed4e9f1869e37e29514d862e0158b40e550232" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #@[--DOCKTITUDE-SCRIPT 2 | #@ #!/bin/sh - 3 | #@ 4 | #@ sudo docker run -it --rm --name docktitude \ 5 | #@ -v /var/run/docker.sock:/var/run/docker.sock \ 6 | #@ -v $(which docker):/bin/docker \ 7 | #@ -v your-docker-contexts-dir:/docker-contexts \ 8 | #@ docktitude/docktitude 9 | #@DOCKTITUDE-SCRIPT--] 10 | # 11 | # 12 | # 13 | FROM node:4.5.0 14 | MAINTAINER support@docktitude.com 15 | 16 | ENV DEBIAN_FRONTEND noninteractive 17 | 18 | RUN apt-get update -qq \ 19 | && apt-get install -qqy libapparmor1 locales bash-completion --no-install-recommends \ 20 | && locale-gen en_US.UTF-8 \ 21 | && localedef -c -f UTF-8 -i en_US en_US.UTF-8 \ 22 | && npm install -g docktitude \ 23 | && echo ". /usr/local/lib/node_modules/docktitude/completion/bash" >> /etc/bash.bashrc \ 24 | && mkdir /docker-contexts \ 25 | && rm -rf /var/lib/apt/lists/* 26 | 27 | ENV LANG en_US.UTF-8 28 | 29 | VOLUME /docker-contexts 30 | WORKDIR /docker-contexts 31 | 32 | CMD ["/bin/bash"] 33 | -------------------------------------------------------------------------------- /src/lib/tree/constant.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Docktitude 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // ***************************************************************************** 18 | 19 | export const SEED: string = "."; 20 | export const EMPTY: string = ""; 21 | export const SPACE: string = " "; 22 | export const STAR: string = "*"; 23 | 24 | export const BRANCH0: string = "\u2502"; // | 25 | export const BRANCH1: string = "\u2514\u2500\u2500"; // L 26 | export const BRANCH2: string = "\u251C\u2500\u2500"; // T 27 | 28 | export const TAB_SIZE: number = 4; -------------------------------------------------------------------------------- /src/lib/common.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Docktitude 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // ***************************************************************************** 18 | 19 | export interface Callback { 20 | (t?: T): void; 21 | } 22 | 23 | export interface BiConsumer { 24 | (x: X, y: Y): void; 25 | } 26 | 27 | export interface StringKeyMap { 28 | [key: string]: V; 29 | } 30 | 31 | export interface NumericKeyMap { 32 | [key: number]: V; 33 | } 34 | 35 | export interface Indexed { 36 | index: string; 37 | } 38 | 39 | export type Map = StringKeyMap | NumericKeyMap -------------------------------------------------------------------------------- /completion/bash: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2016 Docktitude 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | _docktitude() 17 | { 18 | local cur prev 19 | COMPREPLY=() 20 | cur="${COMP_WORDS[COMP_CWORD]}" 21 | prev="${COMP_WORDS[COMP_CWORD-1]}" 22 | case $prev in 23 | 'docktitude') 24 | COMPREPLY=( $(compgen -W "build clean config export help info op play print script snapshot status tree update upgrade version" -- $cur) ) 25 | return 0 26 | ;; 27 | 'build'|'print'|'script') 28 | COMPREPLY=( $(compgen -f -o plusdirs -- $cur) ) 29 | return 0 30 | ;; 31 | esac 32 | return 0 33 | } 34 | complete -F _docktitude docktitude 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docktitude", 3 | "version": "2.3.0", 4 | "description": "A mastering Docker tool to enhance your docker experience", 5 | "main": "main.js", 6 | "bin": "./bin/docktitude", 7 | "scripts": { 8 | "clean": "rm -rf build *.js", 9 | "compile": "tslint src/**/*.ts src/*.ts && tsc", 10 | "lint": "jshint build/lib/**/*.js build/*.js", 11 | "browserify": "browserify build/main.js --node -t [ envify --NODE_ENV production ] -o main.js && browserify build/test/*.js -o tests.js", 12 | "uglify": "uglifyjs main.js -c -m -o main.js", 13 | "build": "npm run clean && npm run compile && npm run lint && npm run browserify && npm run uglify", 14 | "pretest": "npm run build", 15 | "test": "mocha tests.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/docktitude/docktitude" 20 | }, 21 | "engines": { 22 | "node": ">= 4.4.3" 23 | }, 24 | "os": [ 25 | "darwin", 26 | "linux" 27 | ], 28 | "keywords": [ 29 | "docker", 30 | "dockerfile", 31 | "container", 32 | "hierarchy", 33 | "dependencies", 34 | "build", 35 | "gitlab-ci", 36 | "jenkins", 37 | "bamboo", 38 | "tool" 39 | ], 40 | "author": { 41 | "name": "Docktitude", 42 | "email": "contact@docktitude.com", 43 | "url": "https://docktitude.com" 44 | }, 45 | "license": "Apache-2.0", 46 | "bugs": { 47 | "url": "https://github.com/docktitude/docktitude/issues" 48 | }, 49 | "devDependencies": { 50 | "browserify": "^13.0.1", 51 | "chai": "^3.5.0", 52 | "envify": "^3.4.0", 53 | "jshint": "^2.9.2", 54 | "mocha": "^2.4.5", 55 | "tslint": "^3.8.1", 56 | "typescript": "^1.8.10", 57 | "uglify-js": "^2.6.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /completion/fish: -------------------------------------------------------------------------------- 1 | complete -c docktitude -n '__fish_use_subcommand' -xa build --description 'Build context Docker image' 2 | complete -c docktitude -n '__fish_use_subcommand' -xa clean --description 'Remove exited Docker containers and useless images. Use -v to remove the associated volumes' 3 | complete -c docktitude -n '__fish_use_subcommand' -xa config --description 'List auto-configured Docker images building tags' 4 | complete -c docktitude -n '__fish_use_subcommand' -xa export --description 'Export all contexts except binaries to a tar archive' 5 | complete -c docktitude -n '__fish_use_subcommand' -xa help --description 'Print usage' 6 | complete -c docktitude -n '__fish_use_subcommand' -xa info --description 'Show information relating to the Dockerfile files' 7 | complete -c docktitude -n '__fish_use_subcommand' -xa op --description 'Change maintainer information in the Dockerfile files' 8 | complete -c docktitude -n '__fish_use_subcommand' -xa play --description 'Run shell script for defined docktitude script tags' 9 | complete -c docktitude -n '__fish_use_subcommand' -xa print --description 'Show context Dockerfile' 10 | complete -c docktitude -n '__fish_use_subcommand' -xa script --description 'Show shell script for defined docktitude script tags' 11 | complete -c docktitude -n '__fish_use_subcommand' -xa snapshot --description 'Display Docker images and save the selected one (.tar)' 12 | complete -c docktitude -n '__fish_use_subcommand' -xa status --description 'Show local Docker images update status' 13 | complete -c docktitude -n '__fish_use_subcommand' -xa tree --description 'List Docker images in a tree-like format' 14 | complete -c docktitude -n '__fish_use_subcommand' -xa update --description 'Update external Docker images' 15 | complete -c docktitude -n '__fish_use_subcommand' -xa upgrade --description 'Build cascade local Docker images' 16 | complete -c docktitude -n '__fish_use_subcommand' -xa version --description 'Show version information' 17 | -------------------------------------------------------------------------------- /src/lib/board.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Docktitude 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // ***************************************************************************** 18 | 19 | import c = require("./constant"); 20 | import util = require("./util"); 21 | import { BiConsumer, Callback, Indexed, Map } from "./common"; 22 | 23 | // ############################################################################# 24 | 25 | interface MetaData { 26 | kMax: number; 27 | vMax: number; 28 | detailed: boolean; 29 | keys: string[]; 30 | } 31 | 32 | // ############################################################################# 33 | 34 | export function print1(map: Map, legend?: string, printer: Callback = util.println): void { 35 | const md: MetaData = computeColsWidth(map, legend); 36 | const border: string = `+${util.repeat(c.DASH, md.kMax + c.DEFAULT_COL_PADDING)}+`; 37 | 38 | const filler: Callback = (k: string) => { 39 | printer(`| ${k + util.repeat(c.SPACE, md.kMax - k.length)} |`); 40 | }; 41 | 42 | printer(border); 43 | for (let key of md.keys) { 44 | filler(key); 45 | } 46 | printer(border); 47 | if (md.detailed) { 48 | filler(legend); 49 | printer(border); 50 | } 51 | } 52 | 53 | export function print2(map: Map, kTitle?: string, vTitle?: string, printer: Callback = util.println): void { 54 | const md: MetaData = computeColsWidth(map, kTitle, vTitle); 55 | const border: string = `+${util.repeat(c.DASH, md.kMax + c.DEFAULT_COL_PADDING)}+${util.repeat(c.DASH, md.vMax + c.DEFAULT_COL_PADDING)}+`; 56 | 57 | const filler: BiConsumer = (k: string, v: string) => { 58 | printer(`| ${k + util.repeat(c.SPACE, md.kMax - k.length)} | ${v + util.repeat(c.SPACE, md.vMax - v.length)} |`); 59 | }; 60 | 61 | if (md.detailed) { 62 | printer(border); 63 | filler(kTitle, vTitle); 64 | } 65 | printer(border); 66 | for (let key of md.keys) { 67 | filler(key, map[key].index); 68 | } 69 | printer(border); 70 | } 71 | 72 | function computeColsWidth(map: Map, kTitle?: string, vTitle?: string): MetaData { 73 | const keys: string[] = []; 74 | let kMax: number = 0; 75 | let vMax: number = 0; 76 | 77 | for (let key of Object.keys(map)) { 78 | keys.push(key); 79 | 80 | if (key.length > kMax) { 81 | kMax = key.length; 82 | } 83 | if ((map[key].index != null) && (map[key].index.length > vMax)) { 84 | vMax = map[key].index.length; 85 | } 86 | } 87 | 88 | if ((kTitle != null) && (kTitle.length > kMax)) { 89 | kMax = kTitle.length; 90 | } 91 | if ((vTitle != null) && (vTitle.length > vMax)) { 92 | vMax = vTitle.length; 93 | } 94 | 95 | return { 96 | kMax: kMax, 97 | vMax: vMax, 98 | detailed: (kTitle != null) || (vTitle != null), 99 | keys: util.isNumeric(keys[0]) ? keys : keys.sort() 100 | }; 101 | } -------------------------------------------------------------------------------- /src/lib/constant.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Docktitude 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // ***************************************************************************** 18 | 19 | export const EMPTY: string = ""; 20 | export const SPACE: string = " "; 21 | export const DASH: string = "-"; 22 | export const DOT: string = "."; 23 | export const COLON: string = ":"; 24 | export const UNDERSCORE: string = "_"; 25 | export const QMK: string = "?"; 26 | export const SLASH: string = "/"; 27 | export const NL: string = "\n"; 28 | 29 | export const PLUS: string = "+"; 30 | export const PERCENT: string = "%"; 31 | export const ARROW: string = "=>"; 32 | export const COLLAPSE: string = "[-]"; 33 | export const ZERO: string = "0"; 34 | 35 | export const STRING_FMT: string = "%s"; 36 | export const ENCODING_UTF8: string = "utf8"; 37 | export const EOF: string = "EOF"; 38 | 39 | export const DOCKTITUDE: string = "docktitude"; 40 | export const VERSION: string = "version"; 41 | 42 | export const SEE_HELP: string = `${DOCKTITUDE}: %s is not a ${DOCKTITUDE} command. See '${DOCKTITUDE} help'.`; 43 | export const HELP_OPTS: string[] = ["help", "--help", "-h"]; 44 | export const VERSION_OPTS: string[] = ["--version", "-v"]; 45 | 46 | export const ALINEA: string = "+++"; 47 | export const CTX: string = "CONTEXT"; 48 | export const CTX_IMG_TAG: string = "IMAGE TAG"; 49 | export const INFO_COL1: string = "BASE IMAGE"; 50 | export const INFO_COL2: string = "DISTRIBUTION"; 51 | 52 | export const PROMPT_IMG: string = "Image to save (choose a number)> "; 53 | 54 | export const DOCKERFILE: string = "Dockerfile"; 55 | export const DEFAULT_TAG: string = "latest"; 56 | export const PARENT_PATTERN: string = "FROM "; 57 | export const GHOST_IMAGE: string = ":"; 58 | export const IMG_HISTORY_ERROR: string = "Err"; 59 | 60 | export const SCRIPT_NAME_SUFFIX: string = "[ SHELL SCRIPT ]"; 61 | export const SCRIPT_TAG: string = "#@"; 62 | export const BEGIN_TEMPLATE_SCRIPT: string = "[--DOCKTITUDE-SCRIPT"; 63 | export const END_TEMPLATE_SCRIPT: string = "DOCKTITUDE-SCRIPT--]"; 64 | 65 | export const SECTION: string = "------------------------------"; 66 | 67 | export const UPGRADE_REQUIRED: string = "parent image updated [%s] | Upgrade required"; 68 | export const PARENT_NOT_FOUND: string = "<> not found !"; 69 | export const IMAGE_NOT_FOUND: string = "image not available | Build OK ?"; 70 | export const NOTHING_TO_REPORT: string = "nothing to upgrade"; 71 | 72 | export const NODE_UNSUPPORTED_VERSION: string = "Warning ! Installed Node.js version (%s) not supported !"; 73 | export const MISSING_ARGUMENT: string = "Missing argument !"; 74 | export const CTX_NOT_FOUND: string = `Docker context not found ! See '${DOCKTITUDE} config'.`; 75 | export const DUPLICATED_CTX: string = "Duplicated docker context found !"; 76 | export const NO_CTX: string = "No Docker context found in the current directory !"; 77 | export const NO_ENTRY_FOUND: string = "No entry found for '%s' !"; 78 | export const NO_SCRIPT_DEFINED: string = `No ${DOCKTITUDE} script tags defined !`; 79 | export const NO_PERM: string = `${DOCKTITUDE}: Are you root?`; 80 | 81 | // ############################################################################# 82 | 83 | export const NODE_SUPPORTED_INI_VERSION: number = 4.4; 84 | export const ARGS_PROCESS_START_INDEX: number = 2; 85 | export const DEFAULT_COL_PADDING: number = 2; 86 | 87 | // ############################################################################# -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docktitude - A mastering Docker tool to enhance your docker experience 2 | 3 | ![](https://raw.githubusercontent.com/docktitude/docktitude/master/docs/docktitude-logo.png "https://docktitude.com") 4 | 5 | Display Tree | Update | Snapshot your Docker images hierarchy and much more... 6 | 7 | ## Requirements 8 | 9 | ### Node.js (v4.4+) 10 | - Download: [Node.js](https://nodejs.org) 11 | 12 | ### Docker 13 | - Obviously you need it :) 14 | - Download: [Docker](https://www.docker.com) 15 | 16 | 17 | ## Installation 18 | 19 | - Node.js package 20 | [![NPM Version](http://img.shields.io/npm/v/docktitude.svg?style=flat)](https://www.npmjs.org/package/docktitude) 21 | ``` 22 | $> npm install -g docktitude 23 | ``` 24 | 25 | - Docker image 26 | ``` 27 | $> docker run -it --rm --name docktitude \ 28 | -v /var/run/docker.sock:/var/run/docker.sock \ 29 | -v $(which docker):/bin/docker \ 30 | -v your-docker-contexts-dir:/docker-contexts \ 31 | docktitude/docktitude 32 | ``` 33 | 34 | 35 | ## Getting started 36 | 37 | 1. **cd docker-contexts** *(where you store your Docker contexts)* 38 | 2. **docktitude config** *(check the computed tags for the build according to the contexts naming convention)* 39 | 3. **docktitude upgrade** *(cascade build: from the root to the leafs)* 40 | 41 | 42 | ## Usage examples 43 | 44 | ```bash 45 | $> docktitude tree 46 | . 47 | +-- alpine* 48 | | +-- transmission 49 | +-- debian:latest* 50 | | +-- debian:local 51 | | +-- clamav 52 | | +-- debian:jdk8 53 | | | +-- apache/activemq 54 | | | +-- debian:jdk8-ui 55 | | | +-- debian:jdk8-scm 56 | | | | +-- idea 57 | | | +-- libreoffice 58 | | +-- vscode 59 | +-- nginx* 60 | | +-- demo-site 61 | +-- ubuntu:14.04* 62 | +-- gitlab/gitlab-runner:local 63 | ``` 64 | 65 | 66 | ```bash 67 | $> docktitude info 68 | +---------------+-------------------+ 69 | | BASE IMAGE | DISTRIBUTION (12) | 70 | +---------------+-------------------+ 71 | | alpine | 8.333 % (1) | 72 | | debian:latest | 75.00 % (9) | 73 | | nginx | 8.333 % (1) | 74 | | ubuntu:14.04 | 8.333 % (1) | 75 | +---------------+-------------------+ 76 | ``` 77 | 78 | 79 | ```bash 80 | $> docktitude script libreoffice 81 | -------------------------------- 82 | +++ libreoffice [ SHELL SCRIPT ] 83 | -------------------------------- 84 | #!/bin/sh - 85 | 86 | docker run -it --rm --name libreoffice \ 87 | -e DISPLAY=unix$DISPLAY \ 88 | -e GDK_SCALE \ 89 | -e GDK_DPI_SCALE \ 90 | -v /tmp/.X11-unix:/tmp/.X11-unix \ 91 | -v /etc/localtime:/etc/localtime:ro \ 92 | libreoffice 93 | -------------------------------- 94 | ``` 95 | 96 | 97 | ## Documentation 98 | 99 | ```java 100 | $> docktitude -h 101 | usage: docktitude [-h | --help | help] [] 102 | 103 | Commands: 104 | build Build context Docker image. Use -f to force build 105 | clean Remove exited Docker containers and useless images 106 | Use -v to remove the associated volumes 107 | config List auto-configured Docker images building tags 108 | export Export all contexts except binaries to a tar archive 109 | info Show information relating to the Dockerfile files 110 | op Change maintainer information in the Dockerfile files 111 | play Run shell script for defined docktitude script tags 112 | print Show context Dockerfile 113 | script Show shell script for defined docktitude script tags 114 | snapshot Display Docker images and save the selected one (.tar) 115 | status Show local Docker images update status 116 | tree List Docker images in a tree-like format 117 | update Update external Docker images 118 | upgrade Build cascade local Docker images 119 | version Show version information 120 | ``` 121 | 122 | 123 | ## Licensing 124 | 125 | Docktitude is licensed under Apache License, Version 2.0. 126 | -------------------------------------------------------------------------------- /src/lib/command.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Docktitude 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // ***************************************************************************** 18 | 19 | import constant = require("./constant"); 20 | import util = require("./util"); 21 | 22 | // ############################################################################# 23 | 24 | interface Option { 25 | command: Command; 26 | desc: string; 27 | argName?: string; 28 | } 29 | 30 | // ############################################################################# 31 | 32 | export enum Command { 33 | build, clean, config, export, info, op, play, print, script, snapshot, status, tree, update, upgrade, version 34 | } 35 | 36 | export class Usage { 37 | 38 | private options: Option[] = []; 39 | private maxNameLength: number = 0; 40 | private maxArgNameLength: number = 0; 41 | 42 | constructor(private usage: string) { 43 | } 44 | 45 | add(command: Command, desc: string, argName?: string): void { 46 | this.options.push({ 47 | command: command, 48 | desc: desc, 49 | argName: argName 50 | }); 51 | 52 | if (len(command) > this.maxNameLength) { 53 | this.maxNameLength = len(command); 54 | } 55 | if ((argName != null) && (argName.length > this.maxArgNameLength)) { 56 | this.maxArgNameLength = argName.length; 57 | } 58 | } 59 | 60 | display(): void { 61 | util.println(this.usage); 62 | 63 | let argName: string = constant.EMPTY; 64 | let padding: number = 0; 65 | let multiLinesDesc: string[]; 66 | 67 | this.options.forEach(o => { 68 | argName = tidyArg(o.argName); 69 | padding = this.computePadding(o.command, argName); 70 | multiLinesDesc = null; 71 | if (o.desc.indexOf(constant.NL) > 0) { 72 | multiLinesDesc = o.desc.split(constant.NL); 73 | } 74 | if (multiLinesDesc == null) { 75 | util.println(`${util.padLeft(stringify(o.command), 3)} ${argName}${util.padLeft(o.desc, padding)}`); 76 | } 77 | else { 78 | const line: string = `${util.padLeft(stringify(o.command), 3)} ${util.padRight(argName, padding)}`; 79 | util.println(line, multiLinesDesc[0]); 80 | for (let n: number = 1; n < multiLinesDesc.length; n++) { 81 | util.printPadLeft(multiLinesDesc[n], line.length); 82 | } 83 | } 84 | }); 85 | } 86 | 87 | contains(command: string): boolean { 88 | let b: boolean = false; 89 | this.options.some(o => { 90 | if (stringify(o.command) === command) { 91 | b = true; 92 | } 93 | return b; 94 | }); 95 | return b; 96 | } 97 | 98 | private computePadding(command: Command, argName: string): number { 99 | return this.maxNameLength + this.maxArgNameLength + 3 - (len(command) + argName.length); 100 | } 101 | } 102 | 103 | // ############################################################################# 104 | 105 | function stringify(c: Command): string { 106 | return Command[c]; 107 | } 108 | 109 | function len(c: Command): number { 110 | return stringify(c).length; 111 | } 112 | 113 | function tidyArg(s: string): string { 114 | return (s != null) ? `<${s}>` : constant.EMPTY; 115 | } 116 | 117 | // ############################################################################# -------------------------------------------------------------------------------- /src/lib/tree/builder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Docktitude 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // ***************************************************************************** 18 | 19 | import constant = require("./constant"); 20 | import util = require("../util"); 21 | import { Callback, Indexed, StringKeyMap } from "../common"; 22 | import { TreeDataHolder } from "./dataholder"; 23 | 24 | // ############################################################################# 25 | 26 | export class TreeBuilder { 27 | 28 | private lineConsumer: Callback; 29 | private decorationsByNode: StringKeyMap; 30 | 31 | private leftPadding: string; 32 | private rightPadding: string; 33 | 34 | constructor(private treeDataHolder: TreeDataHolder, callback?: Callback) { 35 | if (callback != null) { 36 | this.lineConsumer = callback; 37 | } 38 | else { 39 | this.lineConsumer = (line: string[]) => { 40 | util.println(line.join(constant.EMPTY)); 41 | }; 42 | } 43 | this.decorationsByNode = {}; 44 | this.leftPadding = util.repeat(constant.SPACE, constant.TAB_SIZE); 45 | this.rightPadding = `${constant.BRANCH0}${util.repeat(constant.SPACE, constant.TAB_SIZE - constant.BRANCH0.length)}`; 46 | } 47 | 48 | build(): void { 49 | if (!this.treeDataHolder.isEmpty()) { 50 | const line: string[] = [constant.SEED]; 51 | this.lineConsumer(line); 52 | for (let node of this.treeDataHolder.roots) { 53 | this.decorationsByNode[node.index] = constant.STAR; 54 | } 55 | this.appendTreeNodes(this.treeDataHolder.roots, 0, line); 56 | } 57 | } 58 | 59 | private appendTreeNodes( 60 | nodes: T[], 61 | level: number, 62 | prevLine: string[], 63 | index: number[] = [1]): void { 64 | 65 | const nbNodes: number = nodes.length; 66 | let line: string[]; 67 | 68 | for (let node of nodes) { 69 | line = []; 70 | if (level > 0) { 71 | for (let i: number = 0; i < prevLine.length - 1; i++) { 72 | if (this.containsAny([constant.BRANCH0, constant.BRANCH2], prevLine[i].trim())) { 73 | line.push(this.rightPadding); 74 | } 75 | else { 76 | line.push(this.leftPadding); 77 | } 78 | } 79 | } 80 | line.push(((nbNodes > 1) && (index[0] !== nbNodes)) ? constant.BRANCH2 : constant.BRANCH1); 81 | line.push(` ${node.index}${this.getDecoration(node)}`); 82 | 83 | this.lineConsumer(line); 84 | index[0]++; 85 | 86 | const subNodes: T[] = this.treeDataHolder.getNodes(node); 87 | if (subNodes != null) { 88 | this.appendTreeNodes(subNodes, level + 1, line); 89 | } 90 | } 91 | } 92 | 93 | private containsAny(tokens: string[], lineElement: string): boolean { 94 | for (let token of tokens) { 95 | if (util.containsString(token, lineElement)) { 96 | return true; 97 | } 98 | } 99 | return false; 100 | } 101 | 102 | private getDecoration(node: T): string { 103 | if (this.decorationsByNode[node.index] != null) { 104 | return constant.STAR; 105 | } 106 | return constant.EMPTY; 107 | } 108 | } 109 | 110 | // ############################################################################# -------------------------------------------------------------------------------- /src/lib/tree/dataholder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Docktitude 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // ***************************************************************************** 18 | 19 | import util = require("../util"); 20 | import { Callback, Indexed, StringKeyMap } from "../common"; 21 | 22 | // ############################################################################# 23 | 24 | export class TreeDataHolder { 25 | public roots: T[] = []; 26 | 27 | constructor(public nodesByParentId: StringKeyMap) { 28 | this.computeRoots(nodesByParentId); 29 | TreeDataHolder.sort(this.roots, nodesByParentId); 30 | } 31 | 32 | isEmpty(): boolean { 33 | return ((this.roots != null) ? (this.roots.length === 0) : true); 34 | } 35 | 36 | getNodes(parent: T): T[] { 37 | if (this.nodesByParentId[parent.index] != null) { 38 | return this.nodesByParentId[parent.index]; 39 | } 40 | return null; 41 | } 42 | 43 | getParent(node: T): Indexed { 44 | for (let id of Object.keys(this.nodesByParentId)) { 45 | if (TreeDataHolder.contains(node, this.nodesByParentId[id])) { 46 | return { index: id }; 47 | } 48 | } 49 | return null; 50 | } 51 | 52 | getRoot(nodeIndex: Indexed): Indexed { 53 | const parent: Indexed = this.getParent(nodeIndex); 54 | if (parent != null) { 55 | if (TreeDataHolder.contains(parent, this.roots)) { 56 | return parent; 57 | } 58 | return this.getRoot(parent); 59 | } 60 | else if (TreeDataHolder.contains(nodeIndex, this.roots)) { 61 | return nodeIndex; 62 | } 63 | return null; 64 | } 65 | 66 | getNbNodes(): number { 67 | let n: number = 0; 68 | for (let id of Object.keys(this.nodesByParentId)) { 69 | n += this.nodesByParentId[id].length; 70 | } 71 | return n; 72 | } 73 | 74 | walk(func: Callback): void { 75 | TreeDataHolder.walk(this.roots, this.nodesByParentId, func); 76 | } 77 | 78 | private computeRoots(nodesByParentId: StringKeyMap): void { 79 | const nodesTags: string[] = util.stringValues(nodesByParentId); 80 | 81 | for (let id of Object.keys(nodesByParentId)) { 82 | if (!util.contains(id, nodesTags)) { 83 | this.roots.push({ index: id }); 84 | } 85 | } 86 | } 87 | 88 | private static walk(rootElements: T[], nodesByParent: StringKeyMap, func: Callback): void { 89 | rootElements.forEach(e => { 90 | func(e); 91 | const children: T[] = nodesByParent[e.index]; 92 | if (children != null) { 93 | TreeDataHolder.walk(children, nodesByParent, func); 94 | } 95 | }); 96 | } 97 | 98 | private static sort(array: T[], map: StringKeyMap): void { 99 | array.sort(TreeDataHolder.compare); 100 | for (let k of Object.keys(map)) { 101 | map[k].sort(TreeDataHolder.compare); 102 | } 103 | } 104 | 105 | private static compare(t1: T, t2: T): number { 106 | if (t1.index > t2.index) { 107 | return 1; 108 | } 109 | if (t1.index < t2.index) { 110 | return -1; 111 | } 112 | return 0; 113 | } 114 | 115 | private static contains(t: T, array: T[]): boolean { 116 | for (let e of array) { 117 | if (e.index === t.index) { 118 | return true; 119 | } 120 | } 121 | return false; 122 | } 123 | } 124 | 125 | // ############################################################################# -------------------------------------------------------------------------------- /src/lib/util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Docktitude 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // ***************************************************************************** 18 | 19 | import constant = require("./constant"); 20 | import { Callback, Indexed, Map } from "./common"; 21 | 22 | // ############################################################################# 23 | 24 | const spawn = require("child_process").spawn; 25 | const execSync = require("child_process").execSync; 26 | 27 | // ############################################################################# 28 | 29 | export function print(t: any): void { 30 | process.stdout.write(t); 31 | } 32 | 33 | export function println(...params: any[]): void { 34 | console.log(params.join(constant.EMPTY)); 35 | } 36 | 37 | export function printArray(array: string[], prefix: string = constant.EMPTY): void { 38 | for (let s of array) { 39 | console.log(`${prefix}${s}`); 40 | } 41 | } 42 | 43 | export function printPadLeft(s: string, n: number): void { 44 | println(repeat(constant.SPACE, n), s); 45 | } 46 | 47 | export function printPadRight(s: string, n: number): void { 48 | println(s, repeat(constant.SPACE, n)); 49 | } 50 | 51 | export function fmt(s: string, ...params: string[]): void { 52 | console.log(s, params); 53 | } 54 | 55 | export function fmtString(s: string, param: string): string { 56 | return s.replace(constant.STRING_FMT, param); 57 | } 58 | 59 | export function padLeft(s: string, n: number): string { 60 | return repeat(constant.SPACE, n) + s; 61 | } 62 | 63 | export function padRight(s: string, n: number): string { 64 | return s + repeat(constant.SPACE, n); 65 | } 66 | 67 | export function repeat(s: string, n: number): string { 68 | if (n <= 0) { 69 | return constant.EMPTY; 70 | } 71 | return Array(n + 1).join(s); 72 | } 73 | 74 | export function isEmpty(s: string): boolean { 75 | return ((s != null) ? s.trim().length === 0 : true); 76 | } 77 | 78 | export function containsString(searchStr: string, str: string): boolean { 79 | return (str.indexOf(searchStr) >= 0); 80 | } 81 | 82 | export function contains(t: T, array: T[]): boolean { 83 | return containsAny(t, [array]); 84 | } 85 | 86 | export function containsAny(t: T, arrays: T[][]): boolean { 87 | let b: boolean = false; 88 | arrays.some(array => { 89 | if (array.indexOf(t) >= 0) { 90 | b = true; 91 | } 92 | return b; 93 | }); 94 | return b; 95 | } 96 | 97 | export function quote(s: string): string { 98 | return `'${s}'`; 99 | } 100 | 101 | export function isNumeric(s: string): boolean { 102 | if ((s != null) && (s.trim().length > 0)) { 103 | return !isNaN(parseFloat(s)); 104 | } 105 | return false; 106 | } 107 | 108 | export function stringValues(map: Map): string[] { 109 | const array: string[] = []; 110 | let curNodes: T[]; 111 | for (let k of Object.keys(map)) { 112 | curNodes = map[k]; 113 | for (let e of curNodes) { 114 | array.push(e.index); 115 | } 116 | } 117 | return array; 118 | } 119 | 120 | export function getCurrentWorkingDir(): string { 121 | return runSyncString("pwd"); 122 | } 123 | 124 | export function getNodeVersion(): number { 125 | return Number(process.version.match(/^v(\d+\.\d+)/)[1]); 126 | } 127 | 128 | export function runSync(command: string, echo: boolean = false, exitOnError: boolean = true): void { 129 | if (echo) { 130 | println(`${constant.NL}$> ${command}`); 131 | } 132 | try { 133 | execSync(command, { stdio: [0, 1, 2] }); 134 | } 135 | catch (e) { 136 | if (exitOnError) { 137 | process.exit(1); 138 | } 139 | } 140 | } 141 | 142 | export function runSyncString(command: string, exitOnError: boolean = true): string { 143 | let output: string; 144 | try { 145 | output = execSync(command).toString().trim(); 146 | } 147 | catch (e) { 148 | if (exitOnError) { 149 | process.exit(1); 150 | } 151 | } 152 | return output; 153 | } 154 | 155 | export function runAsync(command: string, ...callbacks: Callback[]): void { 156 | const p = spawn("bash", ["-c", command]); 157 | 158 | const lines: string[] = []; 159 | p.stdout.on("data", (data: any) => { 160 | const frags: string[] = data.toString().split(constant.NL); 161 | for (let s of frags) { 162 | if (s.trim().length > 0) { 163 | lines.push(s); 164 | } 165 | } 166 | }); 167 | 168 | handleError(p); 169 | 170 | p.on("close", (code: any) => { 171 | callbacks.forEach(f => f(lines)); 172 | }); 173 | } 174 | 175 | export function apply(...callbacks: Callback[]): void { 176 | callbacks.forEach(c => c.apply(null)); 177 | } 178 | 179 | function handleError(t: any): void { 180 | t.stderr.on("data", (data: any) => { 181 | println(`${constant.DOCKTITUDE}: ${data}`); 182 | process.exit(1); 183 | }); 184 | } 185 | 186 | // ############################################################################# -------------------------------------------------------------------------------- /src/lib/tree.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Docktitude 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // ***************************************************************************** 18 | 19 | import constant = require("./constant"); 20 | import util = require("./util"); 21 | import { Callback, Indexed, StringKeyMap } from "./common"; 22 | import { TreeDataHolder } from "./tree/dataholder"; 23 | import { TreeBuilder } from "./tree/builder"; 24 | 25 | import fs = require("fs"); 26 | import path = require("path"); 27 | 28 | // ############################################################################# 29 | 30 | export interface Context extends Indexed { 31 | name: string; 32 | paths: string[]; 33 | tag?: string; 34 | parent?: string; 35 | } 36 | 37 | // ############################################################################# 38 | 39 | export function print(): void { 40 | const f = (t: TreeDataHolder) => { 41 | const builder: TreeBuilder = new TreeBuilder(t); 42 | builder.build(); 43 | }; 44 | generateTreeData(f); 45 | } 46 | 47 | export function findContexts(callback: Callback>): void { 48 | checkSearchDepth(); 49 | 50 | const currentWorkingDir: string = util.getCurrentWorkingDir(); 51 | const contextsByName: StringKeyMap = {}; 52 | 53 | const read = (paths: string[]): void => { 54 | let curCtx: string; 55 | let fullpath: string; 56 | 57 | for (let p of paths) { 58 | curCtx = path.basename(p); 59 | fullpath = path.join(currentWorkingDir, p); 60 | 61 | if (contextsByName[curCtx] != null) { 62 | contextsByName[curCtx].paths.push(fullpath); 63 | } 64 | else { 65 | const curTag: string = computeTag(currentWorkingDir, fullpath); 66 | contextsByName[curCtx] = { 67 | name: curCtx, 68 | paths: [fullpath], 69 | tag: curTag, 70 | index: curTag 71 | }; 72 | } 73 | } 74 | 75 | const names: string[] = Object.keys(contextsByName); 76 | if (names.length === 0) { 77 | util.println(constant.NO_CTX); 78 | process.exit(0); 79 | } 80 | for (let ctx of names) { 81 | if (contextsByName[ctx].paths.length > 1) { 82 | util.println(`${constant.DUPLICATED_CTX}${constant.NL}${constant.ARROW} ${ctx}`); 83 | util.printArray(contextsByName[ctx].paths, constant.COLLAPSE + constant.SPACE); 84 | process.exit(1); 85 | } 86 | } 87 | return callback(contextsByName); 88 | }; 89 | 90 | util.runAsync("find . -type f -name 'Dockerfile' | awk -F '.' '{idx=index($0,\".\"); print substr($0,idx+1)}' | awk -F 'Dockerfile' '{print $1}'", read); 91 | } 92 | 93 | export function generateTreeData(callback: Callback>): void { 94 | const nodesByParentId: StringKeyMap = {}; 95 | const roots: Context[] = []; 96 | 97 | const computeHierarchy = (contextsByName: StringKeyMap) => { 98 | for (let name of Object.keys(contextsByName)) { 99 | contextsByName[name].parent = getParentImage(name, contextsByName[name].paths[0]); 100 | } 101 | return computeNodes(contextsByName); 102 | }; 103 | 104 | const computeNodes = (contextsByName: StringKeyMap) => { 105 | let curParentId: string; 106 | for (let name of Object.keys(contextsByName)) { 107 | curParentId = contextsByName[name].parent; 108 | if (nodesByParentId[curParentId] != null) { 109 | nodesByParentId[curParentId].push(contextsByName[name]); 110 | } 111 | else { 112 | nodesByParentId[curParentId] = [contextsByName[name]]; 113 | } 114 | } 115 | return callback(new TreeDataHolder(nodesByParentId)); 116 | }; 117 | 118 | findContexts(computeHierarchy); 119 | } 120 | 121 | export function checkSearchDepth(): void { 122 | const output: string = util.runSyncString("find . -mindepth 2 -maxdepth 2 -type f -name 'Dockerfile' | wc -l"); 123 | if (output === constant.ZERO) { 124 | util.println(constant.NO_CTX); 125 | process.exit(0); 126 | } 127 | } 128 | 129 | function computeTag(currentWorkingDir: string, dirpath: string): string { 130 | const f = (s: string): string => { 131 | return s.replace(constant.DASH, constant.COLON).replace(constant.UNDERSCORE, constant.DASH); 132 | }; 133 | const parentDir: string = path.dirname(dirpath); 134 | if (currentWorkingDir === parentDir) { 135 | return f(path.basename(dirpath)); 136 | } 137 | return f(path.join(parentDir.replace(currentWorkingDir + path.sep, constant.EMPTY), path.basename(dirpath))); 138 | } 139 | 140 | function getParentImage(ctx: string, dirpath: string): string { 141 | let parent: string = constant.QMK; 142 | fs.readFileSync(path.join(dirpath, constant.DOCKERFILE), constant.ENCODING_UTF8).split(constant.NL).some((line: string) => { 143 | if (line.indexOf(constant.PARENT_PATTERN) === 0) { 144 | const index: number = line.indexOf(constant.PARENT_PATTERN) + constant.PARENT_PATTERN.length; 145 | parent = line.substr(index); 146 | return true; 147 | } 148 | return false; 149 | }); 150 | return parent; 151 | } 152 | 153 | // ############################################################################# -------------------------------------------------------------------------------- /src/test/TreeBuilderTest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Docktitude 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // ***************************************************************************** 18 | 19 | /// 20 | /// 21 | 22 | import constant = require("../lib/constant"); 23 | import util = require("../lib/util"); 24 | 25 | import { Callback, StringKeyMap } from "../lib/common"; 26 | import { TreeDataHolder } from "../lib/tree/dataholder"; 27 | import { TreeBuilder } from "../lib/tree/builder"; 28 | 29 | // ############################################################################# 30 | 31 | interface Indexed { 32 | index: string; 33 | parent?: string; 34 | } 35 | 36 | // ############################################################################# 37 | 38 | const assert = require("chai").assert; 39 | 40 | const EXPECTED_TREE1: string = 41 | `. 42 | ├── alpine* 43 | │ └── transmission 44 | └── debian:latest* 45 | └── base/debian:build 46 | └── base/debian 47 | ├── base/debian:jdk7 48 | │ └── base/debian:jdk7-ui 49 | │ └── gatling 50 | ├── base/debian:jdk8 51 | │ ├── activemq 52 | │ └── base/debian:jdk8-ui 53 | │ ├── base/debian:jdk8-scm 54 | │ │ ├── idea 55 | │ │ └── netbeans 56 | │ └── libreoffice 57 | ├── chrome 58 | ├── clamav 59 | ├── grafana 60 | ├── influxdb 61 | └── telegraf`; 62 | 63 | // ############################################################################# 64 | 65 | const nodesByParentId: StringKeyMap = {}; 66 | nodesByParentId["base/debian:jdk7"] = ["base/debian:jdk7-ui"]; 67 | nodesByParentId["base/debian:jdk8"] = ["activemq", "base/debian:jdk8-ui"]; 68 | nodesByParentId["base/debian:jdk8-scm"] = ["idea", "netbeans"]; 69 | nodesByParentId["alpine"] = ["transmission"]; 70 | nodesByParentId["base/debian"] = ["base/debian:jdk7", "base/debian:jdk8", "chrome", "clamav", "grafana", "influxdb", "telegraf"]; 71 | nodesByParentId["base/debian:jdk7-ui"] = ["gatling"]; 72 | nodesByParentId["base/debian:build"] = ["base/debian"]; 73 | nodesByParentId["debian:latest"] = ["base/debian:build"]; 74 | nodesByParentId["base/debian:jdk8-ui"] = ["base/debian:jdk8-scm", "libreoffice"]; 75 | 76 | const treeDataHolder: TreeDataHolder = new TreeDataHolder(indexMap(nodesByParentId)); 77 | 78 | const content: string[] = []; 79 | const lineConsumer: Callback = (line: string[]) => { 80 | content.push(line.join(constant.EMPTY)); 81 | }; 82 | const treeBuilder: TreeBuilder = new TreeBuilder(treeDataHolder, lineConsumer); 83 | 84 | describe("TreeDataHolder", () => { 85 | describe("constructor", () => { 86 | 87 | it("should have 2 roots", () => { 88 | assert.equal(treeDataHolder.roots.length, 2); 89 | }); 90 | 91 | it("should return the first root", () => { 92 | assert.equal(treeDataHolder.roots[0].index, "alpine"); 93 | }); 94 | 95 | it("should return the second root", () => { 96 | assert.equal(treeDataHolder.roots[1].index, "debian:latest"); 97 | }); 98 | }); 99 | 100 | describe("#isEmpty()", () => { 101 | it("should not be empty", () => { 102 | assert.isFalse(treeDataHolder.isEmpty()); 103 | }); 104 | }); 105 | 106 | describe("#getNodes(..)", () => { 107 | it("should be null", () => { 108 | assert.isNull(treeDataHolder.getNodes(index("test"))); 109 | }); 110 | 111 | it("should return a node", () => { 112 | assert.deepEqual(treeDataHolder.getNodes(index("alpine")), [index("transmission", "alpine")]); 113 | }); 114 | }); 115 | 116 | describe("#getParent(..)", () => { 117 | it("should be null", () => { 118 | assert.isNull(treeDataHolder.getParent(index("test"))); 119 | }); 120 | 121 | it("should return a parent", () => { 122 | assert.deepEqual(treeDataHolder.getParent(index("clamav")), { index: "base/debian" }); 123 | }); 124 | }); 125 | 126 | describe("#getRoot(..)", () => { 127 | it("should be null", () => { 128 | assert.isNull(treeDataHolder.getRoot(index("test"))); 129 | }); 130 | 131 | it("should return a root", () => { 132 | assert.equal(treeDataHolder.getRoot(index("libreoffice")).index, "debian:latest"); 133 | }); 134 | }); 135 | 136 | describe("#getNbNodes()", () => { 137 | it("should be 18", () => { 138 | assert.equal(treeDataHolder.getNbNodes(), 18); 139 | }); 140 | }); 141 | }); 142 | 143 | describe("TreeBuilder", () => { 144 | describe("#build()", () => { 145 | treeBuilder.build(); 146 | it("should build a tree", () => { 147 | assert.equal(content.join(constant.NL), EXPECTED_TREE1); 148 | }); 149 | }); 150 | }); 151 | 152 | // ############################################################################# 153 | 154 | function indexMap(map: StringKeyMap): StringKeyMap { 155 | const newMap: StringKeyMap = {}; 156 | for (let k of Object.keys(map)) { 157 | newMap[k] = indexArray(k, map[k]); 158 | } 159 | return newMap; 160 | } 161 | 162 | function indexArray(key: string, array: string[]): Indexed[] { 163 | const newArray: Indexed[] = []; 164 | for (let i: number = 0; i < array.length; i++) { 165 | newArray[i] = index(array[i], key); 166 | } 167 | return newArray; 168 | } 169 | 170 | function index(name: string, parent?: string): Indexed { 171 | return { 172 | index: name, 173 | parent: parent, 174 | }; 175 | } 176 | 177 | // ############################################################################# -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2015-2016 Docktitude 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Docktitude 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // ***************************************************************************** 18 | 19 | import constant = require("./lib/constant"); 20 | import util = require("./lib/util"); 21 | import tree = require("./lib/tree"); 22 | import board = require("./lib/board"); 23 | import { TreeDataHolder } from "./lib/tree/dataholder"; 24 | import { Command, Usage } from "./lib/command"; 25 | import { BiConsumer, Callback, Indexed, NumericKeyMap, StringKeyMap } from "./lib/common"; 26 | 27 | import fs = require("fs"); 28 | import path = require("path"); 29 | import readline = require("readline"); 30 | 31 | // ############################################################################# 32 | 33 | function parseArgs(): void { 34 | const args: string[] = process.argv.slice(constant.ARGS_PROCESS_START_INDEX); 35 | const usage: Usage = buildUsage(); 36 | 37 | if (args.length === 0 || usage.contains(args[0]) || util.containsAny(args[0], [constant.HELP_OPTS, constant.VERSION_OPTS])) { 38 | const index: number = util.contains(args[0], constant.VERSION_OPTS) ? -1 : Command[args[0]]; 39 | switch (index) { 40 | case Command.build: { build(checkArgs(args)); break; } 41 | case Command.clean: { clean(args.length > 1 && args[1] === "-v"); break; } 42 | case Command.config: { printConfig(); break; } 43 | case Command.export: { exportContexts(); break; } 44 | case Command.info: { printInfo(); break; } 45 | case Command.op: { changeMaintainer(checkArg(args, true)); break; } 46 | case Command.play: { runScript(checkArg(args)); break; } 47 | case Command.print: { printContext(checkArg(args)); break; } 48 | case Command.script: { printScript(checkArg(args)); break; } 49 | case Command.snapshot: { snapshot(); break; } 50 | case Command.status: { printStatus(); break; } 51 | case Command.tree: { tree.print(); break; } 52 | case Command.update: { update(); break; } 53 | case Command.upgrade: { upgrade(); break; } 54 | case Command.version: { printVersion(); break; } 55 | case -1: { printVersion(); break; } 56 | default: usage.display(); 57 | } 58 | } 59 | else { 60 | util.fmt(constant.SEE_HELP, util.quote(args[0])); 61 | } 62 | } 63 | 64 | function printStatus(): void { 65 | const output: string = util.runSyncString("docker images | awk -F ' ' '{print $1\":\"$2\"+\"$3}' | awk 'NR != 1' | sort | uniq -u | grep -v ':'"); 66 | 67 | const idsByImageTag: StringKeyMap = {}; 68 | output.split(constant.NL).forEach((value, index, array) => { 69 | const [tag, id]: string[] = value.split(constant.PLUS); 70 | idsByImageTag[tag] = id; 71 | }); 72 | 73 | const f = (t: TreeDataHolder) => { 74 | const statusByImageTag: StringKeyMap = {}; 75 | 76 | t.walk((ctx: tree.Context) => { 77 | if (!util.contains(ctx, t.roots)) { 78 | let layersIds: string[] = []; 79 | if (idsByImageTag[normalize(ctx.index)] != null) { 80 | layersIds = util.runSyncString(`docker history -q ${ctx.index}`).split(constant.NL); 81 | } 82 | if ((layersIds.length > 1) && (!util.containsString(constant.IMG_HISTORY_ERROR, layersIds[0]))) { 83 | const parent: Indexed = t.getParent(ctx); 84 | if (parent != null) { 85 | const parentId: string = idsByImageTag[normalize(parent.index)]; 86 | if (!util.contains(parentId, layersIds)) { 87 | statusByImageTag[ctx.index] = { index: util.fmtString(constant.UPGRADE_REQUIRED, parent.index) }; 88 | } 89 | } 90 | else { 91 | statusByImageTag[ctx.index] = { index: constant.PARENT_NOT_FOUND }; 92 | } 93 | } 94 | else { 95 | statusByImageTag[ctx.index] = { index: constant.IMAGE_NOT_FOUND }; 96 | } 97 | } 98 | }); 99 | if (Object.keys(statusByImageTag).length > 0) { 100 | board.print2(statusByImageTag); 101 | } 102 | else { 103 | util.println(constant.NOTHING_TO_REPORT); 104 | } 105 | }; 106 | tree.generateTreeData(f); 107 | } 108 | 109 | function printScript(ctx: string): void { 110 | const f = (line: string): void => { 111 | if (line.indexOf(constant.EOF) !== 0) { 112 | util.println(line); 113 | } 114 | }; 115 | useContext(ctx, useScript(ctx, f)); 116 | } 117 | 118 | function runScript(ctx: string): void { 119 | const lines: string[] = []; 120 | 121 | const f = (line: string): void => { 122 | if (!util.isEmpty(line)) { 123 | const endOfScript: boolean = (line.indexOf(constant.EOF) === 0); 124 | if ((line.indexOf(constant.SECTION) !== 0) && (line.indexOf(constant.ALINEA) !== 0) && !endOfScript && (line.indexOf("#!/") !== 0)) { 125 | lines.push(line); 126 | } 127 | if (endOfScript) { 128 | util.runSync(lines.join(constant.NL), true); 129 | } 130 | } 131 | }; 132 | useContext(ctx, useScript(ctx, f)); 133 | } 134 | 135 | function useScript(ctx: string, callback: Callback): BiConsumer> { 136 | return (ctx: string, contextsByName: StringKeyMap): void => { 137 | const filepath: string = path.join(contextsByName[ctx].paths[0], constant.DOCKERFILE); 138 | const fileContent: string[] = fs.readFileSync(filepath, constant.ENCODING_UTF8).split(constant.NL).filter((value, index, array) => { 139 | return (value.indexOf(constant.SCRIPT_TAG) === 0); 140 | }); 141 | 142 | if ((fileContent.length > 2) && 143 | (util.containsString(constant.BEGIN_TEMPLATE_SCRIPT, fileContent[0])) && 144 | (util.containsString(constant.END_TEMPLATE_SCRIPT, fileContent[fileContent.length - 1]))) { 145 | 146 | callback(constant.SECTION); 147 | callback(`${constant.ALINEA} ${ctx} ${constant.SCRIPT_NAME_SUFFIX}`); 148 | callback(constant.SECTION); 149 | 150 | for (let i: number = 1; i < fileContent.length - 1; i++) { 151 | if (fileContent[i].indexOf(constant.SCRIPT_TAG + constant.SPACE) === 0) { 152 | callback(fileContent[i].replace(constant.SCRIPT_TAG + constant.SPACE, constant.EMPTY)); 153 | } 154 | else { 155 | callback(fileContent[i].replace(constant.SCRIPT_TAG, constant.EMPTY)); 156 | } 157 | } 158 | callback(constant.SECTION); 159 | callback(constant.EOF); 160 | } 161 | else { 162 | callback(constant.NO_SCRIPT_DEFINED); 163 | } 164 | }; 165 | } 166 | 167 | function snapshot(): void { 168 | const output: string = util.runSyncString("docker images | awk -F ' ' '{print $1\":\"$2}' | awk 'NR != 1' | sort | uniq -u | grep -v ':'"); 169 | const imagesByIndex: NumericKeyMap = {}; 170 | output.split(constant.NL).forEach((value, index, array) => { 171 | imagesByIndex[index + 1] = { index: value }; 172 | }); 173 | board.print2(imagesByIndex); 174 | 175 | const rl: readline.ReadLine = readline.createInterface(process.stdin, process.stdout); 176 | rl.question(constant.PROMPT_IMG, (answer: string) => { 177 | if (imagesByIndex[Number(answer)] != null) { 178 | let img: string = imagesByIndex[Number(answer)].index; 179 | util.runSync(`docker save ${img} > ${img.replace(constant.SLASH, constant.DASH).replace(constant.COLON, constant.DASH)}.tar`); 180 | } 181 | else { 182 | util.fmt(constant.NO_ENTRY_FOUND, answer); 183 | } 184 | rl.close(); 185 | }); 186 | } 187 | 188 | function changeMaintainer(maintainer: string): void { 189 | if (maintainer != null) { 190 | tree.checkSearchDepth(); 191 | util.runSync(`find . -type f -name 'Dockerfile' -exec sed -i 's/MAINTAINER.*/MAINTAINER ${maintainer}/' {} \\;`); 192 | } 193 | else { 194 | util.println(constant.MISSING_ARGUMENT); 195 | } 196 | } 197 | 198 | function exportContexts(): void { 199 | tree.findContexts((contextsByName: StringKeyMap) => { 200 | // TODO Consider yml files as non binary 201 | util.runSync("find . -size -3000k -exec file {} \\; | grep text | cut -d: -f1 | tar -cJ -f export.tar.xz -T -"); 202 | }); 203 | } 204 | 205 | function printInfo(): void { 206 | const f = (t: TreeDataHolder) => { 207 | const total: number = t.getNbNodes(); 208 | 209 | const stats: StringKeyMap = {}; 210 | const statsAsString: StringKeyMap = {}; 211 | 212 | for (let e of t.roots) { 213 | stats[e.index] = 0; 214 | } 215 | for (let id of Object.keys(t.nodesByParentId)) { 216 | stats[t.getRoot({ index: id }).index] += t.nodesByParentId[id].length; 217 | } 218 | for (let n of Object.keys(stats)) { 219 | statsAsString[n] = { 220 | index: util.fmtString(`%s ${constant.PERCENT} (${stats[n]})`, ((stats[n] * 100) / total).toPrecision(4)) 221 | }; 222 | } 223 | board.print2(statsAsString, constant.INFO_COL1, `${constant.INFO_COL2} (${total})`); 224 | }; 225 | tree.generateTreeData(f); 226 | } 227 | 228 | function printConfig(): void { 229 | tree.findContexts((contextsByName: StringKeyMap) => { 230 | board.print2(contextsByName, constant.CTX, constant.CTX_IMG_TAG); 231 | }); 232 | } 233 | 234 | function build(args: string[]): void { 235 | if (args == null) { 236 | util.println(constant.MISSING_ARGUMENT); 237 | process.exit(1); 238 | } 239 | 240 | const filteredArgs: string[] = args.filter((value, index, array) => { 241 | return ((value !== "-f") && (value !== "--f")); 242 | }); 243 | const noCacheOption: string = (args.length !== filteredArgs.length) ? "--no-cache " : constant.EMPTY; 244 | 245 | const f = (ctx: string, contextsByName: StringKeyMap): void => { 246 | util.runSync(`docker build ${noCacheOption}-t "${contextsByName[ctx].tag}" ${contextsByName[ctx].paths[0]}`, true); 247 | }; 248 | useContext((filteredArgs.length > 0) ? filteredArgs[0] : null, f); 249 | } 250 | 251 | function readDockerfile(ctx: string, dirpath: string): void { 252 | const filepath: string = path.join(dirpath, constant.DOCKERFILE); 253 | 254 | const stream: fs.ReadStream = fs.createReadStream(filepath, { 255 | "encoding": constant.ENCODING_UTF8, 256 | "autoClose": true 257 | }); 258 | 259 | util.println(constant.SECTION); 260 | util.println(`${constant.ALINEA} ${ctx}`); 261 | util.println(`[${filepath}]`); 262 | util.println(constant.SECTION); 263 | 264 | stream.on("data", (chunck: string): void => { 265 | util.print(chunck); 266 | }); 267 | stream.on("end", () => { 268 | util.println(constant.SECTION); 269 | }); 270 | } 271 | 272 | function printContext(ctx: string): void { 273 | const f = (ctx: string, contextsByName: StringKeyMap): void => { 274 | readDockerfile(ctx, contextsByName[ctx].paths[0]); 275 | }; 276 | useContext(ctx, f); 277 | } 278 | 279 | function useContext(ctx: string, func: BiConsumer>): void { 280 | if (ctx != null) { 281 | tree.findContexts((contextsByName: StringKeyMap) => { 282 | if (contextsByName[ctx] != null) { 283 | func(ctx, contextsByName); 284 | } 285 | else { 286 | util.println(constant.CTX_NOT_FOUND); 287 | } 288 | }); 289 | } 290 | else { 291 | util.println(constant.MISSING_ARGUMENT); 292 | } 293 | } 294 | 295 | function upgrade(): void { 296 | const f = (t: TreeDataHolder) => { 297 | t.walk((ctx: tree.Context) => { 298 | if ((ctx.paths != null) && (ctx.paths[0] != null)) { 299 | util.runSync(`docker build -t "${ctx.tag}" ${ctx.paths[0]}`, true); 300 | } 301 | }); 302 | }; 303 | tree.generateTreeData(f); 304 | } 305 | 306 | function update(): void { 307 | let registryImages: string[]; 308 | 309 | const dumpImages = (images: string[]) => { 310 | registryImages = images.slice(); 311 | }; 312 | 313 | const updateImages = (contextsByName: StringKeyMap) => { 314 | const localTags: string[] = []; 315 | 316 | for (let name of Object.keys(contextsByName)) { 317 | localTags.push(normalize(contextsByName[name].tag)); 318 | } 319 | 320 | const filteredImages: string[] = registryImages.filter((value, index, array) => { 321 | return !util.contains(value, localTags); 322 | }); 323 | 324 | for (let img of filteredImages) { 325 | util.runSync(`docker pull ${img}`, false, false); 326 | } 327 | }; 328 | 329 | const findContexts = () => { 330 | tree.findContexts(updateImages); 331 | }; 332 | 333 | util.runAsync("docker images | awk -F ' ' '{print $1\":\"$2}' | awk 'NR != 1' | sort | uniq -u | grep -v \":\"", 334 | dumpImages, 335 | findContexts 336 | ); 337 | } 338 | 339 | function normalize(imageTag: string): string { 340 | return (util.containsString(constant.COLON, imageTag) ? imageTag : `${imageTag}${constant.COLON}${constant.DEFAULT_TAG}`); 341 | } 342 | 343 | function clean(removeVolumes: boolean): void { 344 | const rm = (exitedContainers: string[]) => { 345 | if (exitedContainers.length > 0) { 346 | if (removeVolumes) { 347 | util.runSync("docker rm -v $(docker ps -a | grep 'Exited' | awk -F ' ' '{print $1}')"); 348 | } 349 | else { 350 | util.runSync("docker rm $(docker ps -a | grep 'Exited' | awk -F ' ' '{print $1}')"); 351 | } 352 | } 353 | util.apply(findGhostImages); 354 | }; 355 | 356 | const findGhostImages = () => { 357 | util.runAsync("echo $(docker images | grep '' | awk -F ' ' '{print $3}') | awk /./", rmi); 358 | }; 359 | 360 | const rmi = (ghostImages: string[]) => { 361 | if (ghostImages.length > 0) { 362 | util.runSync("docker rmi $(docker images | grep '' | awk -F ' ' '{print $3}')"); 363 | } 364 | }; 365 | 366 | util.runAsync("echo $(docker ps -a | grep 'Exited' | awk -F ' ' '{print $1}') | awk /./", rm); 367 | } 368 | 369 | function printVersion(): void { 370 | util.println(`${constant.DOCKTITUDE} ${constant.VERSION} ${process.env.npm_package_version} (installed node version: ${process.version.substring(1)})`); 371 | } 372 | 373 | function checkNodeVersion(): void { 374 | if (util.getNodeVersion() < constant.NODE_SUPPORTED_INI_VERSION) { 375 | util.fmt(constant.NODE_UNSUPPORTED_VERSION, process.version.substring(1)); 376 | } 377 | } 378 | 379 | function checkArg(args: string[], joined: boolean = false): string { 380 | if (args.length > 1) { 381 | if (joined) { 382 | return args.slice(1).join(constant.SPACE); 383 | } 384 | return args[1]; 385 | } 386 | return null; 387 | } 388 | 389 | function checkArgs(args: string[]): string[] { 390 | if (args.length > 2) { 391 | return args.slice(1); 392 | } 393 | const arg: string = checkArg(args); 394 | if (arg != null) { 395 | return [arg]; 396 | } 397 | return null; 398 | } 399 | 400 | function buildUsage(): Usage { 401 | const u = new Usage("usage: docktitude [-h | --help | help] []\n\nCommands:"); 402 | u.add(Command.build, "Build context Docker image. Use -f to force build", "context"); 403 | u.add(Command.clean, "Remove exited Docker containers and useless images\nUse -v to remove the associated volumes"); 404 | u.add(Command.config, "List auto-configured Docker images building tags"); 405 | u.add(Command.export, "Export all contexts except binaries to a tar archive"); 406 | u.add(Command.info, "Show information relating to the Dockerfile files"); 407 | u.add(Command.op, "Change maintainer information in the Dockerfile files", "name"); 408 | u.add(Command.play, "Run shell script for defined docktitude script tags", "context"); 409 | u.add(Command.print, "Show context Dockerfile", "context"); 410 | u.add(Command.script, "Show shell script for defined docktitude script tags", "context"); 411 | u.add(Command.snapshot, "Display Docker images and save the selected one (.tar)"); 412 | u.add(Command.status, "Show local Docker images update status"); 413 | u.add(Command.tree, "List Docker images in a tree-like format"); 414 | u.add(Command.update, "Update external Docker images"); 415 | u.add(Command.upgrade, "Build cascade local Docker images"); 416 | u.add(Command.version, "Show version information"); 417 | return u; 418 | } 419 | 420 | // ############################################################################# 421 | 422 | checkNodeVersion(); 423 | parseArgs(); 424 | 425 | // ############################################################################# --------------------------------------------------------------------------------