@@ -10,18 +10,19 @@ import (
1010 "github.com/project-dalec/dalec/targets"
1111)
1212
13- type BuildDistrolessContainerInput struct {
14- Config * Config
15- Client gwclient.Client // Replace with interface.
16- Worker llb.State
17- SOpt dalec.SourceOpts
18- Spec * dalec.Spec
19- Target string
20- DebSt llb.State // Why is this DebSt?
21- Opts []llb.ConstraintsOpt
13+ type BuildContainerInput struct {
14+ Config * Config
15+ DefaultOutputImage string
16+ Client gwclient.Client // Replace with interface.
17+ Worker llb.State
18+ SOpt dalec.SourceOpts
19+ Spec * dalec.Spec
20+ Target string
21+ DebSt llb.State // Why is this DebSt?
22+ Opts []llb.ConstraintsOpt
2223}
2324
24- func BuildDistrolessContainer (ctx context.Context , input BuildDistrolessContainerInput ) llb.State {
25+ func BuildDistrolessContainer (ctx context.Context , input BuildContainerInput ) llb.State {
2526 opts := append (input .Opts , frontend .IgnoreCache (input .Client ), dalec .ProgressGroup ("Build Container Image" ))
2627
2728 // Those base repos come from distro configuration.
@@ -32,23 +33,8 @@ func BuildDistrolessContainer(ctx context.Context, input BuildDistrolessContaine
3233
3334 withRepos := input .Config .RepoMounts (repos , input .SOpt , opts ... )
3435
35- // Step 1: Bootstrap base image structure from scratch
36- baseImg := llb .Scratch ().
37- File (llb .Mkdir ("/etc" , 0o755 ), opts ... ).
38- File (llb .Mkdir ("/etc/apt" , 0o755 ), opts ... ).
39- File (llb .Mkdir ("/etc/apt/apt.conf.d" , 0o755 ), opts ... ).
40- File (llb .Mkdir ("/etc/apt/preferences.d" , 0o755 ), opts ... ).
41- File (llb .Mkdir ("/etc/apt/sources.list.d" , 0o755 ), opts ... ).
42- File (llb .Mkdir ("/var" , 0o755 ), opts ... ).
43- File (llb .Mkdir ("/var/cache" , 0o755 ), opts ... ).
44- File (llb .Mkdir ("/var/cache/apt" , 0o755 ), opts ... ).
45- File (llb .Mkdir ("/var/cache/apt/archives" , 0o755 ), opts ... ).
46- File (llb .Mkdir ("/var/lib" , 0o755 ), opts ... ).
47- File (llb .Mkdir ("/var/lib/dpkg" , 0o755 ), opts ... ).
48- File (llb .Mkfile ("/var/lib/dpkg/status" , 0o644 , []byte {}), opts ... )
49-
50- // Step 2: Build base packages if configured
51- var basePkg llb.State
36+ basePkg := llb .Scratch ()
37+
5238 if len (input .Config .BasePackages ) > 0 {
5339 runtimePkgs := make (dalec.PackageDependencyList , len (input .Config .BasePackages ))
5440 for _ , pkgName := range input .Config .BasePackages {
@@ -64,225 +50,170 @@ func BuildDistrolessContainer(ctx context.Context, input BuildDistrolessContaine
6450 Runtime : runtimePkgs ,
6551 },
6652 }
53+
6754 basePkg = input .Config .BuildPkg (ctx , input .Client , input .SOpt , basePkgSpec , input .Target , opts ... )
55+ }
56+
57+ baseImg := llb .Scratch ()
58+
59+ if input .DefaultOutputImage != "" {
60+ baseImg = llb .Image (input .DefaultOutputImage , llb .WithMetaResolver (input .SOpt .Resolver ), dalec .WithConstraints (opts ... ))
61+ }
62+
63+ // Allow spec to override the base image entirely
64+ bi , err := input .Spec .GetSingleBase (input .Target )
65+ if err != nil {
66+ return dalec .ErrorState (llb .Scratch (), err )
67+ }
68+ if bi != nil {
69+ baseImg = bi .ToState (input .SOpt , opts ... )
6870 } else {
69- basePkg = llb .Scratch ()
71+ baseImg = baseImg .File (llb .Mkdir ("/etc" , 0o755 ), opts ... ).
72+ File (llb .Mkdir ("/etc/apt" , 0o755 ), opts ... ).
73+ File (llb .Mkdir ("/etc/apt/apt.conf.d" , 0o755 ), opts ... ).
74+ File (llb .Mkdir ("/etc/apt/preferences.d" , 0o755 ), opts ... ).
75+ File (llb .Mkdir ("/etc/apt/sources.list.d" , 0o755 ), opts ... ).
76+ File (llb .Mkdir ("/var" , 0o755 ), opts ... ).
77+ File (llb .Mkdir ("/var/cache" , 0o755 ), opts ... ).
78+ File (llb .Mkdir ("/var/cache/apt" , 0o755 ), opts ... ).
79+ File (llb .Mkdir ("/var/cache/apt/archives" , 0o755 ), opts ... ).
80+ File (llb .Mkdir ("/var/lib" , 0o755 ), opts ... ).
81+ File (llb .Mkdir ("/var/lib/dpkg" , 0o755 ), opts ... ).
82+ File (llb .Mkfile ("/var/lib/dpkg/status" , 0o644 , []byte {}), opts ... )
7083 }
7184
72- // Step 3: Use worker to download all packages + deps and install into baseImg
73- // Worker has apt-get, dpkg, etc. while baseImg is just empty directories
74- const installScript = `#!/bin/sh
75- set -ex
76- # cache-bust-v2
85+ installScript := `#!/bin/sh
86+ set -exu
87+
88+ rootfs=/tmp/rootfs
89+ apt_archives=/var/ cache/apt/archives
7790
7891# Ensure apt cache directory exists
79- mkdir -p /var/cache/apt/archives
80-
81- # Copy local packages (base + spec) to apt cache
82- cp /base-packages/*.deb /var/cache/apt/archives/
83- cp /spec-packages/*.deb /var/cache/apt/archives/
84-
85- apt-get update
86-
87- # Download essential packages and all dependencies for our local packages
88- # Point apt at the empty target dpkg status so it thinks nothing is installed,
89- # which makes it download the full dependency tree and resolve conflicts properly.
90- essential=$(dpkg-query -Wf '${Package} ${Essential}\n' | awk '$2 == "yes" {print $1}')
91- # Get names of local packages so we can exclude them from apt-get install
92- local_pkgs=$(for f in /var/cache/apt/archives/*.deb; do dpkg-deb -f "$f" Package 2>/dev/null; done | sort -u)
93- local_deps=$(for f in /var/cache/apt/archives/*.deb; do dpkg-deb -f "$f" Depends 2>/dev/null; done | tr ',' '\n' | sed 's/([^)]*)//g; s/|.*//; s/ //g' | grep -v '^$' | sort -u)
94- # Filter out deps that are satisfied by local packages (they aren't in apt repos)
95- echo "$local_pkgs" > /tmp/local_pkg_names
96- filtered_deps=$(echo "$local_deps" | grep -vxFf /tmp/local_pkg_names || true)
97- apt-get -o Dir::State::status=/tmp/rootfs/var/lib/dpkg/status \
98- --yes --download-only install $essential $filtered_deps
99-
100- # Extract all packages into the target rootfs
101- for f in /var/cache/apt/archives/*.deb; do
102- dpkg-deb --extract "$f" /tmp/rootfs
92+ mkdir -p "${apt_archives}"
93+
94+ apt update
95+
96+ # Upgrade worker so we pull latest package versions.
97+ apt dist-upgrade -y
98+
99+ essential_packages=$(dpkg-query -Wf '${Package} ${Essential}\n' | awk '$2 == "yes" {print $1}')
100+ local_package_files=$(ls /base-packages/*.deb /spec-packages/*.deb)
101+
102+ # Extract dependencies of local packages for downloading.
103+ local_packages_dependencies=$(for f in ${local_package_files}; do dpkg-deb -f "${f}" Depends 2>/dev/null; done | tr ',' '\n' | sed 's/([^)]*)//g; s/|.*//; s/ //g' | grep -v '^$' | sort -u)
104+
105+ # Get names of local packages so we can exclude them from apt-get install.
106+ local_package_names=$(for f in ${local_package_files}; do dpkg-deb -f "${f}" Package 2>/dev/null; done | sort -u)
107+
108+ # Spec packages may depend on base packages, so we need to filter to only download remaining packages, since downloading local packages
109+ # would fail.
110+ dependencies_to_download=$(echo "${local_packages_dependencies}" | grep -vxF "${local_package_names}")
111+
112+ # Use state file from target image so we only download necessary packages. If target image is a base image, less packages should
113+ # be downloaded.
114+ apt-get -o Dir::State::status="${rootfs}/var/lib/dpkg/status" \
115+ --yes --download-only install ${essential_packages} ${dependencies_to_download}
116+
117+ # Extract all packages into the target rootfs.
118+ #
119+ # Extract base-files first to establish merged-usr symlinks (/bin -> usr/bin, etc.)
120+ # before other packages create those paths as real directories, which would
121+ # cause tar to fail when base-files tries to create the symlinks later.
122+ base_files_package=$(ls "${apt_archives}"/base-files_*.deb)
123+ for f in ${base_files_package} ${local_package_files} $(ls "${apt_archives}"/*.deb | grep -vxF "${base_files_package}"); do
124+ dpkg-deb --extract "${f}" "${rootfs}"
125+ done
126+
127+ # Fix merged-usr: on Noble+, /bin, /sbin, /lib should be symlinks to usr/bin, usr/sbin, usr/lib
128+ # but dpkg-deb --extract may recreate them as real directories.
129+ for dir in bin sbin lib; do
130+ if [ -d "${rootfs}/usr/${dir}" ] && [ -d "${rootfs}/${dir}" ] && [ ! -L "${rootfs}/${dir}" ]; then
131+ cp -a "${rootfs}/${dir}"/* "${rootfs}/usr/${dir}/" 2>/dev/null || true
132+ rm -rf "${rootfs}/${dir}"
133+ ln -s "usr/${dir}" "${rootfs}/${dir}"
134+ fi
103135done
104136
105137# dpkg-deb --extract doesn't run postinst scripts, so the /bin/sh symlink
106138# normally created by update-alternatives is missing. Create it manually.
107- # Also fix merged-usr: on Noble+, /bin should be a symlink to usr/bin but
108- # dpkg-deb --extract may create it as a real directory if extraction order
109- # causes a directory to be created before base-files' symlink.
110- if [ -d /tmp/rootfs/usr/bin ] && [ -d /tmp/rootfs/bin ] && [ ! -L /tmp/rootfs/bin ]; then
111- # /bin is a real dir but should be a symlink on merged-usr systems
112- # Move any contents and replace with symlink
113- cp -a /tmp/rootfs/bin/* /tmp/rootfs/usr/bin/ 2>/dev/null || true
114- rm -rf /tmp/rootfs/bin
115- ln -s usr/bin /tmp/rootfs/bin
116- fi
117- if [ -d /tmp/rootfs/usr/sbin ] && [ -d /tmp/rootfs/sbin ] && [ ! -L /tmp/rootfs/sbin ]; then
118- cp -a /tmp/rootfs/sbin/* /tmp/rootfs/usr/sbin/ 2>/dev/null || true
119- rm -rf /tmp/rootfs/sbin
120- ln -s usr/sbin /tmp/rootfs/sbin
121- fi
122- if [ -d /tmp/rootfs/usr/lib ] && [ -d /tmp/rootfs/lib ] && [ ! -L /tmp/rootfs/lib ]; then
123- cp -a /tmp/rootfs/lib/* /tmp/rootfs/usr/lib/ 2>/dev/null || true
124- rm -rf /tmp/rootfs/lib
125- ln -s usr/lib /tmp/rootfs/lib
126- fi
127- if [ ! -e /tmp/rootfs/usr/bin/sh ]; then
128- if [ -x /tmp/rootfs/usr/bin/dash ]; then
129- ln -s dash /tmp/rootfs/usr/bin/sh
130- elif [ -x /tmp/rootfs/bin/dash ]; then
131- ln -s dash /tmp/rootfs/bin/sh
132- fi
139+ if [ ! -e "${rootfs}/usr/bin/sh" ] && [ ! -e "${rootfs}/bin/sh" ]; then
140+ ln -s dash "${rootfs}/usr/bin/sh"
133141fi
134142
135143# Remove usrmerge package - our merged-usr fixup above already handles this,
136144# and usrmerge's postinst fails on overlayfs (which BuildKit uses).
137145# Create a fake dpkg status entry so dpkg thinks it's installed.
138- for f in /var/cache/apt/archives/usrmerge_*.deb /var/cache/apt/archives/usr-is-merged_*.deb; do
139- if [ -f "$f" ]; then
140- pkg=$(dpkg- deb -f "$f" Package)
141- ver =$(dpkg-deb -f "$f" Version )
142- arch =$(dpkg-deb -f "$f" Architecture )
143- printf 'Package: %s\nStatus: install ok installed\nVersion: %s\nArchitecture: %s\nDescription: faked by dalec\n\n' "$pkg" "$ver" "$arch" >> /tmp/rootfs/var/lib/dpkg/status
144- rm "$f "
145- fi
146+ #
147+ # This only runs when usrmerge package is not installed in the base image, since only then the deb file will be downloaded.
148+ for f in $(ls "${apt_archives}"/usrmerge_*. deb "${apt_archives}"/usr-is-merged_*.deb 2>/dev/null); do
149+ pkg =$(dpkg-deb -f "${f}" Package )
150+ ver =$(dpkg-deb -f "${f}" Version )
151+ arch=$(dpkg-deb -f "${f}" Architecture)
152+ printf 'Package: %s\nStatus: install ok installed\nVersion: %s\nArchitecture: %s\nDescription: faked by dalec\n\n' "${pkg}" "${ver}" "${arch}" >> "${rootfs}/var/lib/dpkg/status "
153+ rm "${f}"
146154done
147155
148- cp /var/cache/apt/archives/ *.deb /tmp/ rootfs/var/cache/apt/archives /
156+ cp ${local_package_files} "${apt_archives}"/ *.deb "${ rootfs}${apt_archives}" /
149157
150- # Copy apt sources from worker into rootfs so the final container can install packages
158+ # Copy apt sources from worker into rootfs so the final container can install packages. Do we want that?
159+ # There is no guarantee that the final image will have access to the same sources worker had (e.g. with mounted repos).
151160#
152- # TODO: This is a workaround. For running tests, installing test steps should follow the same logic as here.
153- cp -a /etc/apt/sources.list /tmp/rootfs/etc/apt/sources.list 2>/dev/null || true
154- cp -a /etc/apt/sources.list.d/* /tmp/rootfs/etc/apt/sources.list.d/ 2>/dev/null || true
161+ # A the moment this is necessary so we can for example install test dependencies without using worker image.
162+ cp -ar /etc/apt/sources.list* "${rootfs}/etc/apt/"
155163`
156164
157165 script := llb .Scratch ().File (llb .Mkfile ("install.sh" , 0o755 , []byte (installScript )), opts ... )
158166
167+ // Use worker to download all packages + deps and install into baseImg.
159168 baseImg = input .Worker .Run (
160169 dalec .WithConstraints (opts ... ),
161170 llb .AddMount ("/tmp/install.sh" , script , llb .SourcePath ("install.sh" )),
162171 llb .AddMount ("/base-packages" , basePkg , llb .Readonly ),
163172 llb .AddMount ("/spec-packages" , input .DebSt , llb .Readonly ),
164173 withRepos ,
174+ dalec .WithMountedAptCache (input .Config .AptCachePrefix , opts ... ),
165175 llb .AddEnv ("DEBIAN_FRONTEND" , "noninteractive" ),
166176 dalec .ShArgs ("/tmp/install.sh" ),
167177 frontend .IgnoreCache (input .Client , targets .IgnoreCacheKeyContainer ),
168178 ).AddMount ("/tmp/rootfs" , baseImg )
169179
170- // Allow spec to override the base image entirely
171- bi , err := input .Spec .GetSingleBase (input .Target )
172- if err != nil {
173- return dalec .ErrorState (llb .Scratch (), err )
174- }
175- if bi != nil {
176- baseImg = bi .ToState (input .SOpt , opts ... )
177- }
178-
179180 debug := llb .Scratch ().File (llb .Mkfile ("debug" , 0o644 , []byte (`debug=2` )), opts ... )
180- debugOpt := llb .AddMount ("/etc/dpkg/dpkg.cfg.d/99-dalec-debug" , debug , llb .SourcePath ("debug" ), llb .Readonly )
181181
182- // Run dpkg --install to properly configure the packages
183- // Try /usr/bin/sh first (merged-usr), fall back to /bin/sh (non-merged)
182+ // Run dpkg --install to properly configure the packages.
184183 baseImg = baseImg .Run (
185184 dalec .WithConstraints (opts ... ),
186- debugOpt ,
187- llb .AddEnv ("DEBIAN_FRONTEND" , "noninteractive" ),
188- llb .Args ([]string {"/bin/sh" , "-c" , "dpkg --install --force-depends /var/cache/apt/archives/*.deb && rm -rf /var/cache/apt/archives/*.deb" }),
189- frontend .IgnoreCache (input .Client , targets .IgnoreCacheKeyContainer ),
190- ).Root ()
191-
192- return baseImg .With (dalec .InstallPostSymlinks (input .Spec .GetImagePost (input .Target ), input .Worker , opts ... ))
193- }
194-
195- func (c * Config ) BuildContainer (ctx context.Context , client gwclient.Client , sOpt dalec.SourceOpts , spec * dalec.Spec , targetKey string , debSt llb.State , opts ... llb.ConstraintsOpt ) llb.State {
196- if true {
197- return BuildDistrolessContainer (ctx , BuildDistrolessContainerInput {
198- Config : c ,
199- Client : client ,
200- Worker : c .Worker (sOpt , dalec .Platform (sOpt .TargetPlatform ), dalec .WithConstraints (opts ... )),
201- SOpt : sOpt ,
202- Spec : spec ,
203- Target : targetKey ,
204- DebSt : debSt ,
205- Opts : opts ,
206- })
207- }
208-
209- opts = append (opts , frontend .IgnoreCache (client ), dalec .ProgressGroup ("Build Container Image" ))
210-
211- baseImg := llb .Image (c .DefaultOutputImage , llb .WithMetaResolver (sOpt .Resolver ), dalec .WithConstraints (opts ... ))
212-
213- bi , err := spec .GetSingleBase (targetKey )
214- if err != nil {
215- return dalec .ErrorState (llb .Scratch (), err )
216- }
217-
218- if bi != nil {
219- baseImg = bi .ToState (sOpt , opts ... )
220- }
221-
222- // Those base repos come from distro configuration.
223- repos := dalec .GetExtraRepos (c .ExtraRepos , "install" )
224-
225- // These are user specified via spec.
226- repos = append (repos , spec .GetInstallRepos (targetKey )... )
227-
228- withRepos := c .RepoMounts (repos , sOpt , opts ... )
229-
230- debug := llb .Scratch ().File (llb .Mkfile ("debug" , 0o644 , []byte (`debug=2` )), opts ... )
231- opts = append (opts , dalec .ProgressGroup ("Install spec package" ))
232-
233- // If we have base packages to install, create a meta-package to install them.
234- if len (c .BasePackages ) > 0 {
235- runtimePkgs := make (dalec.PackageDependencyList , len (c .BasePackages ))
236- for _ , pkgName := range c .BasePackages {
237- runtimePkgs [pkgName ] = dalec.PackageConstraints {}
238- }
239- basePkgSpec := & dalec.Spec {
240- Name : "dalec-deb-base-packages" ,
241- Packager : "dalec" ,
242- Description : "Base Packages for Debian-based Distros" ,
243- Version : "0.1" ,
244- Revision : "1" ,
245- Dependencies : & dalec.PackageDependencies {
246- Runtime : runtimePkgs ,
247- },
248- }
249-
250- basePkg := c .BuildPkg (ctx , client , sOpt , basePkgSpec , targetKey , opts ... )
251-
252- // Update the base image to include the base packages.
253- // This may include things that are necessary to even install the debSt package.
254- // So this must be done separately from the debSt package.
255- opts := append (opts , dalec .ProgressGroup ("Install base image packages" ))
256- baseImg = baseImg .Run (
257- dalec .WithConstraints (opts ... ),
258- InstallLocalPkg (basePkg , true , opts ... ),
259- dalec .WithMountedAptCache (c .AptCachePrefix , opts ... ),
260- ).Root ()
261- }
262-
263- worker := c .Worker (sOpt , dalec .Platform (sOpt .TargetPlatform ), dalec .WithConstraints (opts ... ))
264-
265- return baseImg .Run (
266- dalec .WithConstraints (opts ... ),
267- withRepos ,
268- dalec .WithMountedAptCache (c .AptCachePrefix , opts ... ),
269- // This file makes dpkg give more verbose output which can be useful when things go awry.
270185 llb .AddMount ("/etc/dpkg/dpkg.cfg.d/99-dalec-debug" , debug , llb .SourcePath ("debug" ), llb .Readonly ),
271186 dalec .RunOptFunc (func (cfg * llb.ExecInfo ) {
272187 // Warning: HACK here
273188 // The base ubuntu image has this `excludes` config file which prevents
274189 // installation of a lot of things, including doc files.
275190 // This is mounting over that file with an empty file so that our test suite
276191 // passes (as it is looking at these files).
277- if ! spec . GetArtifacts (targetKey ).HasDocs () {
192+ if ! input . Spec . GetArtifacts (input . Target ).HasDocs () {
278193 return
279194 }
280195
281196 tmp := llb .Scratch ().File (llb .Mkfile ("tmp" , 0o644 , nil ), opts ... )
282197 llb .AddMount ("/etc/dpkg/dpkg.cfg.d/excludes" , tmp , llb .SourcePath ("tmp" )).SetRunOption (cfg )
283198 }),
284- InstallLocalPkg (debSt , true , opts ... ),
285- frontend .IgnoreCache (client , targets .IgnoreCacheKeyContainer ),
286- ).Root ().
287- With (dalec .InstallPostSymlinks (spec .GetImagePost (targetKey ), worker , opts ... ))
199+ llb .AddEnv ("DEBIAN_FRONTEND" , "noninteractive" ),
200+ llb .Args ([]string {"/usr/bin/sh" , "-c" , "dpkg --install --force-depends /var/cache/apt/archives/*.deb && rm -rf /var/cache/apt/archives/*.deb" }),
201+ frontend .IgnoreCache (input .Client , targets .IgnoreCacheKeyContainer ),
202+ ).Root ()
203+
204+ return baseImg .With (dalec .InstallPostSymlinks (input .Spec .GetImagePost (input .Target ), input .Worker , opts ... ))
205+ }
206+
207+ func (c * Config ) BuildContainer (ctx context.Context , client gwclient.Client , sOpt dalec.SourceOpts , spec * dalec.Spec , targetKey string , debSt llb.State , opts ... llb.ConstraintsOpt ) llb.State {
208+ return BuildDistrolessContainer (ctx , BuildContainerInput {
209+ Config : c ,
210+ DefaultOutputImage : c .DefaultOutputImage ,
211+ Client : client ,
212+ Worker : c .Worker (sOpt , dalec .Platform (sOpt .TargetPlatform ), dalec .WithConstraints (opts ... )),
213+ SOpt : sOpt ,
214+ Spec : spec ,
215+ Target : targetKey ,
216+ DebSt : debSt ,
217+ Opts : opts ,
218+ })
288219}
0 commit comments