Docker自动化构建实例-OpenGrok

作者 Known Rabbit 日期 2017-11-06
Docker自动化构建实例-OpenGrok

为什么我这么执着于这个OpenGrok呢,大概因为这是市面上唯一一个,像样子的代码审计工具了吧【摊手】

大概半年前看到了这个项目,就随手(其实shell恶心了我好久)写了一个构建脚本,当时在docker上调通了,就扔在那儿撒手不管了,种种原因没有派上用场。最近逆向一些东西比较多,在闲置多年的学生机上docker run起来一个,打开页面给我出来一个404??😅这我就不是很开心了。没办法重头撸了一遍当年写的Dockerfile,加上当年的想法成功实现了当年想实现的那些功能,包括但不限于:一键部署、索引自动更新、实时获取最新release、结合CI固定历史版本等等。

所以就填一下,自己写Dockerfile时掉进去过的那些坑。

环境变量 ARG vs ENV

官方文档[1]和SOF的一篇回答[2]中写道,ARG和ENV一个是编译环境变量,一个是运行环境变量。这里的“变量”一词就特别有迷惑性,其实这两个根本不是一个东西!

ARG定义的变量,更像是shell中用作parameter expansion的一个别名,也就是说,它只会在Dockerfile被读取时将这个ARG的值在Dockerfile的命令行运行前替换成对应值,而RUN、ENTRYPOINT等命令调用的其他程序是看不见这个ARG的存在的。

而ENV则正相反,使用ENV定义的变量可以被容器内其他程序所获取,是真正意义上的环境变量。除此之外,ENV定义的变量也用在RUN、ENTRYPOINT调用的程序行中,因为shell也会在parameter expansion的过程中解析形如${PARAM}的参数。我举个例子。

ARG THEARG=argg
ENV THEENV=envv
RUN echo $ARG $THEENV

这一段等效于在image中运行

THEENV=envv sh -c "echo argg $THEENV"

如何将宿主机的环境变量传递到docker中呢?我们需要将它们结合起来[2]。例如我们需要让docker构建的脚本获知自身是否在Travis中。

ARG CI=false
ENV CI=$CI
RUN /your/path/to/install

这样在.travis.yml中运行docker build --build-arg CI=$CI ,install script就能够通过读取CI变量获知自己是否处于CI过程中。

Layered FS

先放一张官方文档的图。

Docker的文件系统为分层架构,Dockerfile的每一句,都会为这个image增加一层,即使是设定ENTRYPOINT和EXPOSE也不例外。层与层之间有关联关系,这也是docker的构建缓存所依赖的根本。

这个分层结构有个巨大的问题,由于每一层之间互相关联,所以下层保存后内容是不会也不能改变的,任何改动都是新的一层进行[3]。同样,下一层固定的文件,上层也无法删除。所以在创建image时,一定要在每一层清理生成的临时文件,比如包管理器cache和下载的压缩包、释放的临时文件等等,否则镜像体积会很快膨胀。

Build Cache的合理利用

既然文件系统是分层的,docker也想到,在docker build过程中只要每一步的操作相同,那么结果也一定相同。我们可以认为,Docker在运行build的每一步都存了一个档(笑),这样有一步执行失败了,可以回档重来。因此,在不同的版本之间,结果相异的步骤尽量放在后面,这样会拥有尽可能多的相同层,在其他人pull的时候也会更节省时间和空间。例如这个build在各个版本之间只有1个step的差别,前10个layer都命中了cache,于是24个版本的构建总时间也从之前的11分钟缩短到6分钟。用户在pull的时候,也只会pull最后有差异的一层。

Hosts

当年github releases还没被墙……当年release还有个固定的CNAME……当年还有16核32G的buildbot……可在墙内最辛酸的还是外网。但是即使是build的时候,也不能设置hosts,因为它根本就不是容器里的文件[4]。根据网上说的,docker用了类似Magisk中magic mount的原理实现了动态加载hosts和resolv.conf,这些文件的内容在docker run时指定。但我build时候就要用怎么办?

那只能每一步脚本中都写一遍hosts咯~

反正最后我不改hosts了放弃

操蛋的shell语言

相信我这绝对是全宇宙功能最强但最垃圾的胶水语言!

掌握了shell的奇技淫巧对我并没有一点好处!

写完了代码你永远不会想看第二遍!

永远都不会有其他人看懂!

甚至包括你自己!

It just works!

其实这一点一直是我想说的。shell语言我是用一次恶心一天。先附两个今天刚写的热翔。

翔01

[[ "$URL" =~ .zip ]] && mv $TARBALL $TARBALL.zip && unzip -p $TARBALL.zip > $TARBALL
tar xzf $TARBALL -C / || { echo "Download failed! exiting.."; exit 1; }

这两句包含的坑点:

  • =~操作符右边的字符串不被双引号包围,才会被认为是正则表达式,否则所有字符都会被自动转义。
  • $TARBALL.zip字串中的TARBALL有一百种方法替换失败
    • 如果存在$TAR 变量,想输出TAR变量的值加BALL.zip字串的话,需要写成${TAR}BALL.zip。意即,若不用大括号包围,shell只会识别它能找到的最长变量名并用其值替换,即使是空值它也不管。
    • 如果$TARBALL变量含有空格或特殊字符如回车,会被如实替换到命令里造成语法错误。比如mv poxn hub.avi poxn hub.avi.zip 这里的语义就发生了改变。变为将 poxn、hub.avi、poxn三个文件移动到hub.avi.zip。目标不是文件夹,必然会出错。解决方法就是在变量周围用引号扩上。这样里面有空格也会被转义。
  • unzip -p文件内容输出到stdout!而我用管道符将它重定向到文件。如果压缩包里文件数大于1,里面的所有文件都会被拼接到一个文件里,绝大部分时候这都不是我们想要的。
  • 傻逼bash!傻逼bash(zsh没有这个问题)对大括号括起来的复合表达式有特殊要求
    • 左大括号右边必须要有一个空格,否则会报错
    • 最后一个语句必须要分号结束
    • 你可以用(echo "fuck" && echo "you") (不管用空格不用管分号)来替换{ echo "fuck" && echo "you";}因为前者是逻辑表达式。而整个bash文档都没说小括号在语言中的定义是啥。

翔02

URLS_PATTERN='https?:(?:(?!p5p|pkg|src).)*(.tar.gz|\.zip|.tar.gz.zip)'

这是一个用来识别后缀为tar.gz或zip或两者皆有,并排除文件名中包含p5p、pkg、src的网址,的正则表达式。

手写。能用就这样吧反正俩月过去了我也看不懂了。🙄

FILE=/tmp/releases.txt
curl 'https://api.github.com/repos/OpenGrok/OpenGrok/releases' -o $FILE
tags=($(grep 'tag_name' $FILE | cut -f4 -d\"))
urls=($(grep -Po $URLS_PATTERN $FILE))

这两行查询Github API并将其中所有tag名保存到tag数组里,将所有符合上一个正则的网址塞到urls数组里。

看到数组声明和tag识别在哪儿了么?🤓

  • 用括号把不带引号的字符串括起来,就把它按空格切割成了字符串数组(更准确的说,按$IFS切割)
  • 因为$IFS包含空格和回车,所以如果tag和url的任意一个元素包含空格,空格就会被当作token切割符把这个url切成两半。解决方法:在这两句前赋值IFS=$'\n'
    • 哦对了,如果想在字符串里包含一个真真正正的换行符,比如上面的IFS赋值,你得用单引号括起来(不能用双引号不能没引号),再搁前面加个$符号。缺一不可!至于为啥?这是feature!zsh都没有的feature!
  • 如果需要遍历一个字符数组,需要使用for tag in "${tags[@]}"的傻逼形式,而且最奇怪的是这里的引号不会导致字符串被连接到一起而是直接忽略。下面是一个demo,其中set -x表示显示出参数变换后shell实际执行的语句,以加号开头。
bash-3.2$ s=(one two)
bash-3.2$ set -x
bash-3.2$ echo ${s[@]}
+ echo one two
one two
bash-3.2$ echo "${s[@]}"
+ echo one two
one two
--> 双引号没了
bash-3.2$ echo \"${s[@]}\"
+ echo '"one' 'two"'
"one two"
--> 多了两个奇怪的单引号
bash-3.2$ echo "aa${s[@]}aa"
+ echo aaone twoaa
aaone twoaa
-> 双引号被忽略,aa在展开后数组的边上

bash-3.2$ for u in ${s[@]}; do echo $u; done
+ for u in '${s[@]}'
+ echo one
one
+ for u in '${s[@]}'
+ echo two
two
bash-3.2$ for u in "${s[@]}"; do echo $u; done
+ for u in '"${s[@]}"'
+ echo one
one
+ for u in '"${s[@]}"'
+ echo two
two
--> 又来了俩单引号,且双引号毫无卵用

bash-3.2$ for u in one two; do echo $u; done
+ for u in one two
+ echo one
one
+ for u in one two
+ echo two
two
bash-3.2$ for u in "one two"; do echo $u; done
+ for u in '"one two"'
+ echo one two
one two
--> 若不使用数组,双引号转义效果正常

我干啥要踩这种坑?这种坑爹的语言特性和“回”的四种写法,有什么区别?像命名管道、参数替换、后台任务与协程我还能写一篇文章。可这又啥意义?一点也没有!

不说了。

再看这一顿操作后,大小有变化没?

有,大了250兆……(逃

一顿操作猛如虎,build一算六分五。

# before
REPOSITORY TAG IMAGE ID CREATED SIZE
opengrok latest dd08abaa35aa 5 months ago 446.4 MB
# after
REPOSITORY TAG IMAGE ID CREATED SIZE
ttimasdf/opengrok latest 19a31e242f56 17 minutes ago 695.1 MB

最后,抄起shell就是干,一把梭!

docker pull ttimasdf/opengrok
docker run -d -v ~/src:/src -p 8080:8080 --name grok opengrok

参考资料


  1. Dockerfile reference

  2. ARG or ENV, which one to use in this case?

  3. The copy-on-write (CoW) strategy

  4. Docker容器修改hosts文件重启不变 - DockOne.io