diff-pr.sh 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. #!/usr/bin/env bash
  2. set -Eeuo pipefail
  3. shopt -s dotglob
  4. # make sure we can GTFO
  5. trap 'echo >&2 Ctrl+C captured, exiting; exit 1' SIGINT
  6. # if bashbrew is missing, bail early with a sane error
  7. bashbrew --version > /dev/null
  8. usage() {
  9. cat <<-EOUSAGE
  10. usage: $0 [PR number] [repo[:tag]]
  11. ie: $0 1024
  12. $0 9001 debian php django
  13. EOUSAGE
  14. }
  15. # TODO flags parsing
  16. allFiles=
  17. listTarballContents=1
  18. findCopies='20%'
  19. uninterestingTarballContent=(
  20. # "config_diff_2017_01_07.log"
  21. 'var/log/YaST2/'
  22. # "ks-script-mqmz_080.log"
  23. # "ks-script-ycfq606i.log"
  24. 'var/log/anaconda/'
  25. # "2016-12-20/"
  26. 'var/lib/yum/history/'
  27. 'var/lib/dnf/history/'
  28. # "a/f8c032d2be757e1a70f00336b55c434219fee230-acl-2.2.51-12.el7-x86_64/var_uuid"
  29. 'var/lib/yum/yumdb/'
  30. 'var/lib/dnf/yumdb/'
  31. # "b42ff584.0"
  32. 'etc/pki/tls/rootcerts/'
  33. # "09/401f736622f2c9258d14388ebd47900bbab126"
  34. 'usr/lib/.build-id/'
  35. )
  36. # prints "$2$1$3$1...$N"
  37. join() {
  38. local sep="$1"; shift
  39. local out; printf -v out "${sep//%/%%}%s" "$@"
  40. echo "${out#$sep}"
  41. }
  42. uninterestingTarballGrep="^([.]?/)?($(join '|' "${uninterestingTarballContent[@]}"))"
  43. if [ "$#" -eq 0 ]; then
  44. usage >&2
  45. exit 1
  46. fi
  47. pull="$1" # PR number
  48. shift
  49. diffDir="$(readlink -f "$BASH_SOURCE")"
  50. diffDir="$(dirname "$diffDir")"
  51. tempDir="$(mktemp -d)"
  52. trap "rm -rf '$tempDir'" EXIT
  53. cd "$tempDir"
  54. git clone --quiet \
  55. https://github.com/docker-library/official-images.git \
  56. oi
  57. if [ "$pull" != '0' ]; then
  58. git -C oi fetch --quiet \
  59. origin "pull/$pull/merge":refs/heads/pull
  60. else
  61. git -C oi fetch --quiet --update-shallow \
  62. "$diffDir" HEAD:refs/heads/pull
  63. fi
  64. externalPins=
  65. if [ "$#" -eq 0 ]; then
  66. externalPins="$(git -C oi/.external-pins diff --no-renames --name-only HEAD...pull -- '*/**')"
  67. images="$(git -C oi/library diff --no-renames --name-only HEAD...pull -- .)"
  68. if [ -z "$images" ] && [ -z "$externalPins" ]; then
  69. exit 0
  70. fi
  71. images="$(xargs -rn1 basename <<<"$images")"
  72. set -- $images
  73. fi
  74. export BASHBREW_LIBRARY="$PWD/oi/library"
  75. : "${BASHBREW_ARCH:=amd64}" # TODO something smarter with arches
  76. export BASHBREW_ARCH
  77. # TODO something less hacky than "git archive" hackery, like a "bashbrew archive" or "bashbrew context" or something
  78. template='
  79. tempDir="$(mktemp -d)"
  80. {{- "\n" -}}
  81. {{- range $.Entries -}}
  82. {{- $arch := .HasArchitecture arch | ternary arch (.Architectures | first) -}}
  83. {{- $outDir := join "_" $.RepoName (.Tags | last) -}}
  84. git -C "{{ gitCache }}" archive --format=tar
  85. {{- " " -}}
  86. {{- "--prefix=" -}}
  87. {{- $outDir -}}
  88. {{- "/" -}}
  89. {{- " " -}}
  90. {{- .ArchGitCommit $arch -}}
  91. {{- ":" -}}
  92. {{- $dir := .ArchDirectory $arch -}}
  93. {{- (eq $dir ".") | ternary "" $dir -}}
  94. {{- "\n" -}}
  95. mkdir -p "$tempDir/{{- $outDir -}}" && echo "{{- .ArchBuilder $arch -}}" > "$tempDir/{{- $outDir -}}/.bashbrew-builder" && echo "{{- .ArchFile $arch -}}" > "$tempDir/{{- $outDir -}}/.bashbrew-file"
  96. {{- "\n" -}}
  97. {{- end -}}
  98. tar -cC "$tempDir" . && rm -rf "$tempDir"
  99. '
  100. _tar-t() {
  101. tar -t "$@" \
  102. | grep -vE "$uninterestingTarballGrep" \
  103. | sed -e 's!^[.]/!!' \
  104. -r \
  105. -e 's!([/.-]|^)((lib)?(c?python|py)-?)[0-9]+([.][0-9]+)?([/.-]|$)!\1\2XXX\6!g' \
  106. | sort
  107. }
  108. _jq() {
  109. if [ "$#" -eq 0 ]; then
  110. set -- '.'
  111. fi
  112. jq --tab -S "$@"
  113. }
  114. copy-tar() {
  115. local src="$1"; shift
  116. local dst="$1"; shift
  117. if [ -n "$allFiles" ]; then
  118. mkdir -p "$dst"
  119. cp -al "$src"/*/ "$dst/"
  120. return
  121. fi
  122. local d indexes=() dockerfiles=()
  123. for d in "$src"/*/.bashbrew-file; do
  124. [ -f "$d" ] || continue
  125. local bf; bf="$(< "$d")"
  126. local dDir; dDir="$(dirname "$d")"
  127. local builder; builder="$(< "$dDir/.bashbrew-builder")"
  128. if [ "$builder" = 'oci-import' ]; then
  129. indexes+=( "$dDir/$bf" )
  130. else
  131. dockerfiles+=( "$dDir/$bf" )
  132. if [ "$bf" = 'Dockerfile' ]; then
  133. # if "Dockerfile.builder" exists, let's check that too (busybox, hello-world)
  134. if [ -f "$dDir/$bf.builder" ]; then
  135. dockerfiles+=( "$dDir/$bf.builder" )
  136. fi
  137. fi
  138. fi
  139. rm "$d" "$dDir/.bashbrew-builder" # remove the ".bashbrew-*" files we created
  140. done
  141. # now that we're done with our globbing needs, let's disable globbing so it doesn't give us wrong answers
  142. local -
  143. set -o noglob
  144. for i in "${indexes[@]}"; do
  145. local iName; iName="$(basename "$i")"
  146. local iDir; iDir="$(dirname "$i")"
  147. local iDirName; iDirName="$(basename "$iDir")"
  148. local iDst="$dst/$iDirName"
  149. mkdir -p "$iDst"
  150. _jq . "$i" > "$iDst/$iName"
  151. local digest
  152. digest="$(jq -r --arg name "$iName" '
  153. if $name == "index.json" then
  154. .manifests[0].digest
  155. else
  156. .digest
  157. end
  158. ' "$i")"
  159. local blob="blobs/${digest//://}"
  160. local blobDir; blobDir="$(dirname "$blob")"
  161. local manifest="$iDir/$blob"
  162. mkdir -p "$iDst/$blobDir"
  163. _jq . "$manifest" > "$iDst/$blob"
  164. local configDigest; configDigest="$(jq -r '.config.digest' "$manifest")"
  165. local blob="blobs/${configDigest//://}"
  166. local blobDir; blobDir="$(dirname "$blob")"
  167. local config="$iDir/$blob"
  168. mkdir -p "$iDst/$blobDir"
  169. _jq . "$config" > "$iDst/$blob"
  170. local layers
  171. layers="$(jq -r '[ .layers[].digest | @sh ] | join(" ")' "$manifest")"
  172. eval "layers=( $layers )"
  173. local layerDigest
  174. for layerDigest in "${layers[@]}"; do
  175. local blob="blobs/${layerDigest//://}"
  176. local blobDir; blobDir="$(dirname "$blob")"
  177. local layer="$iDir/$blob"
  178. mkdir -p "$iDst/$blobDir"
  179. _tar-t -f "$layer" > "$iDst/$blob 'tar -t'"
  180. done
  181. done
  182. for d in "${dockerfiles[@]}"; do
  183. local dDir; dDir="$(dirname "$d")"
  184. local dDirName; dDirName="$(basename "$dDir")"
  185. # TODO choke on "syntax" parser directive
  186. # TODO handle "escape" parser directive reasonably
  187. local flatDockerfile; flatDockerfile="$(
  188. gawk '
  189. BEGIN { line = "" }
  190. /^[[:space:]]*#/ {
  191. gsub(/^[[:space:]]+/, "")
  192. print
  193. next
  194. }
  195. {
  196. if (match($0, /^(.*)(\\[[:space:]]*)$/, m)) {
  197. line = line m[1]
  198. next
  199. }
  200. print line $0
  201. line = ""
  202. }
  203. ' "$d"
  204. )"
  205. local IFS=$'\n'
  206. local copyAddContext; copyAddContext="$(awk '
  207. toupper($1) == "COPY" || toupper($1) == "ADD" {
  208. for (i = 2; i < NF; i++) {
  209. if ($i ~ /^--from=/) {
  210. next
  211. }
  212. if ($i !~ /^--chown=/) {
  213. print $i
  214. }
  215. }
  216. }
  217. ' <<<"$flatDockerfile")"
  218. local dBase; dBase="$(basename "$d")"
  219. local files=(
  220. "$dBase"
  221. $copyAddContext
  222. # some extra files which are likely interesting if they exist, but no big loss if they do not
  223. ' .dockerignore' # will be used automatically by "docker build"
  224. ' *.manifest' # debian/ubuntu "package versions" list
  225. ' *.ks' # fedora "kickstart" (rootfs build script)
  226. ' build*.txt' # ubuntu "build-info.txt", debian "build-command.txt"
  227. # usefulness yet to be proven:
  228. #' *.log'
  229. #' {MD5,SHA1,SHA256}SUMS'
  230. #' *.{md5,sha1,sha256}'
  231. # (the space prefix is removed below and is used to ignore non-matching globs so that bad "Dockerfile" entries appropriately lead to failure)
  232. )
  233. unset IFS
  234. mkdir -p "$dst/$dDirName"
  235. local f origF failureMatters
  236. for origF in "${files[@]}"; do
  237. f="${origF# }" # trim off leading space (indicates we don't care about failure)
  238. [ "$f" = "$origF" ] && failureMatters=1 || failureMatters=
  239. local globbed
  240. # "find: warning: -path ./xxx/ will not match anything because it ends with /."
  241. local findGlobbedPath="${f%/}"
  242. findGlobbedPath="${findGlobbedPath#./}"
  243. local globbedStr; globbedStr="$(cd "$dDir" && find -path "./$findGlobbedPath")"
  244. local -a globbed=( $globbedStr )
  245. if [ "${#globbed[@]}" -eq 0 ]; then
  246. globbed=( "$f" )
  247. fi
  248. local g
  249. for g in "${globbed[@]}"; do
  250. local srcG="$dDir/$g" dstG="$dst/$dDirName/$g"
  251. if [ -z "$failureMatters" ] && [ ! -e "$srcG" ]; then
  252. continue
  253. fi
  254. local gDir; gDir="$(dirname "$dstG")"
  255. mkdir -p "$gDir"
  256. cp -alT "$srcG" "$dstG"
  257. if [ -n "$listTarballContents" ]; then
  258. case "$g" in
  259. *.tar.* | *.tgz)
  260. if [ -s "$dstG" ]; then
  261. _tar-t -f "$dstG" > "$dstG 'tar -t'"
  262. fi
  263. ;;
  264. esac
  265. fi
  266. done
  267. done
  268. done
  269. }
  270. # a "bashbrew cat" template that gives us the last / "least specific" tags for the arguments
  271. # (in other words, this is "bashbrew list --uniq" but last instead of first)
  272. templateLastTags='
  273. {{- range .TagEntries -}}
  274. {{- $.RepoName -}}
  275. {{- ":" -}}
  276. {{- .Tags | last -}}
  277. {{- "\n" -}}
  278. {{- end -}}
  279. '
  280. _metadata-files() {
  281. if [ "$#" -gt 0 ]; then
  282. bashbrew list "$@" 2>>temp/_bashbrew.err | sort -uV > temp/_bashbrew-list || :
  283. bashbrew cat --format '{{ range .Entries }}{{ range .Architectures }}{{ . }}{{ "\n" }}{{ end }}{{ end }}' "$@" 2>>temp/_bashbrew.err | sort -u > temp/_bashbrew-arches || :
  284. "$diffDir/_bashbrew-cat-sorted.sh" "$@" 2>>temp/_bashbrew.err > temp/_bashbrew-cat || :
  285. bashbrew list --uniq "$@" \
  286. | sort -V \
  287. | xargs -r bashbrew list --uniq --build-order 2>>temp/_bashbrew.err \
  288. | xargs -r bashbrew cat --format "$templateLastTags" 2>>temp/_bashbrew.err \
  289. > temp/_bashbrew-list-build-order || :
  290. bashbrew fetch --arch-filter "$@"
  291. script="$(bashbrew cat --format "$template" "$@")"
  292. mkdir tar
  293. ( eval "$script" | tar -xiC tar )
  294. copy-tar tar temp
  295. rm -rf tar
  296. fi
  297. if [ -n "$externalPins" ] && command -v crane &> /dev/null; then
  298. local file
  299. for file in $externalPins; do
  300. [ -e "oi/$file" ] || continue
  301. local pin digest dir
  302. pin="$("$diffDir/.external-pins/tag.sh" "$file")"
  303. digest="$(< "oi/$file")"
  304. dir="temp/$file"
  305. mkdir -p "$dir"
  306. bashbrew remote arches --json "$pin@$digest" | _jq > "$dir/bashbrew.json"
  307. local manifests manifest
  308. manifests="$(jq -r '
  309. [ (
  310. .arches
  311. | if has(env.BASHBREW_ARCH) then
  312. .[env.BASHBREW_ARCH]
  313. else
  314. .[keys_unsorted | first]
  315. end
  316. )[].digest | @sh ]
  317. | join(" ")
  318. ' "$dir/bashbrew.json")"
  319. eval "manifests=( $manifests )"
  320. for manifest in "${manifests[@]}"; do
  321. crane manifest "$pin@$manifest" | _jq > "$dir/manifest-${manifest//:/_}.json"
  322. local config
  323. config="$(jq -r '.config.digest' "$dir/manifest-${manifest//:/_}.json")"
  324. crane blob "$pin@$config" | _jq > "$dir/manifest-${manifest//:/_}-config.json"
  325. done
  326. done
  327. fi
  328. }
  329. mkdir temp
  330. git -C temp init --quiet
  331. git -C temp config user.name 'Bogus'
  332. git -C temp config user.email 'bogus@bogus'
  333. # handle "new-image" PRs gracefully
  334. for img; do touch "$BASHBREW_LIBRARY/$img"; [ -s "$BASHBREW_LIBRARY/$img" ] || echo 'Maintainers: New Image! :D (@docker-library-bot)' > "$BASHBREW_LIBRARY/$img"; done
  335. _metadata-files "$@"
  336. git -C temp add . || :
  337. git -C temp commit --quiet --allow-empty -m 'initial' || :
  338. git -C oi clean --quiet --force
  339. git -C oi checkout --quiet pull
  340. # handle "deleted-image" PRs gracefully :(
  341. for img; do touch "$BASHBREW_LIBRARY/$img"; [ -s "$BASHBREW_LIBRARY/$img" ] || echo 'Maintainers: Deleted Image D: (@docker-library-bot)' > "$BASHBREW_LIBRARY/$img"; done
  342. git -C temp rm --quiet -rf . || :
  343. _metadata-files "$@"
  344. git -C temp add .
  345. git -C temp diff \
  346. --find-copies-harder \
  347. --find-copies="$findCopies" \
  348. --find-renames="$findCopies" \
  349. --ignore-blank-lines \
  350. --ignore-space-at-eol \
  351. --ignore-space-change \
  352. --irreversible-delete \
  353. --minimal \
  354. --staged