Extract intermediate stages to filesystem (#266)
* WIP * save and extract stage tarballs if there are dependencies
This commit is contained in:
		
							parent
							
								
									71c83e369c
								
							
						
					
					
						commit
						954b6129d6
					
				|  | @ -5,6 +5,9 @@ FROM scratch as second | |||
| ENV foopath context/foo | ||||
| COPY --from=0 $foopath context/b* /foo/ | ||||
| 
 | ||||
| FROM second | ||||
| COPY --from=base /context/foo /new/foo | ||||
| 
 | ||||
| FROM base | ||||
| ARG file | ||||
| COPY --from=second /foo $file | ||||
| COPY --from=second /foo ${file} | ||||
|  |  | |||
|  | @ -19,16 +19,32 @@ package dockerfile | |||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"path/filepath" | ||||
| 	"io/ioutil" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/GoogleContainerTools/kaniko/pkg/constants" | ||||
| 	"github.com/GoogleContainerTools/kaniko/pkg/util" | ||||
| 	"github.com/moby/buildkit/frontend/dockerfile/instructions" | ||||
| 	"github.com/moby/buildkit/frontend/dockerfile/parser" | ||||
| ) | ||||
| 
 | ||||
| // Stages reads the Dockerfile, validates it's contents, and returns stages
 | ||||
| func Stages(dockerfilePath, target string) ([]instructions.Stage, error) { | ||||
| 	d, err := ioutil.ReadFile(dockerfilePath) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	stages, err := Parse(d) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if err := ValidateTarget(stages, target); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	ResolveStages(stages) | ||||
| 	return stages, nil | ||||
| } | ||||
| 
 | ||||
| // Parse parses the contents of a Dockerfile and returns a list of commands
 | ||||
| func Parse(b []byte) ([]instructions.Stage, error) { | ||||
| 	p, err := parser.Parse(bytes.NewReader(b)) | ||||
|  | @ -43,6 +59,9 @@ func Parse(b []byte) ([]instructions.Stage, error) { | |||
| } | ||||
| 
 | ||||
| func ValidateTarget(stages []instructions.Stage, target string) error { | ||||
| 	if target == "" { | ||||
| 		return nil | ||||
| 	} | ||||
| 	for _, stage := range stages { | ||||
| 		if stage.Name == target { | ||||
| 			return nil | ||||
|  | @ -91,53 +110,23 @@ func ParseCommands(cmdArray []string) ([]instructions.Command, error) { | |||
| 	return cmds, nil | ||||
| } | ||||
| 
 | ||||
| // Dependencies returns a list of files in this stage that will be needed in later stages
 | ||||
| func Dependencies(index int, stages []instructions.Stage, buildArgs *BuildArgs) ([]string, error) { | ||||
| 	dependencies := []string{} | ||||
| // SaveStage returns true if the current stage will be needed later in the Dockerfile
 | ||||
| func SaveStage(index int, stages []instructions.Stage) bool { | ||||
| 	for stageIndex, stage := range stages { | ||||
| 		if stageIndex <= index { | ||||
| 			continue | ||||
| 		} | ||||
| 		sourceImage, err := util.RetrieveSourceImage(stageIndex, buildArgs.ReplacementEnvs(nil), stages) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		imageConfig, err := sourceImage.ConfigFile() | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		if stage.Name == stages[index].BaseName { | ||||
| 			return true | ||||
| 		} | ||||
| 		for _, cmd := range stage.Commands { | ||||
| 			switch c := cmd.(type) { | ||||
| 			case *instructions.EnvCommand: | ||||
| 				replacementEnvs := buildArgs.ReplacementEnvs(imageConfig.Config.Env) | ||||
| 				if err := util.UpdateConfigEnv(c.Env, &imageConfig.Config, replacementEnvs); err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 			case *instructions.ArgCommand: | ||||
| 				buildArgs.AddArg(c.Key, c.Value) | ||||
| 			case *instructions.CopyCommand: | ||||
| 				if c.From != strconv.Itoa(index) { | ||||
| 					continue | ||||
| 				if c.From == strconv.Itoa(index) { | ||||
| 					return true | ||||
| 				} | ||||
| 				// First, resolve any environment replacement
 | ||||
| 				replacementEnvs := buildArgs.ReplacementEnvs(imageConfig.Config.Env) | ||||
| 				resolvedEnvs, err := util.ResolveEnvironmentReplacementList(c.SourcesAndDest, replacementEnvs, true) | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 				// Resolve wildcards and get a list of resolved sources
 | ||||
| 				srcs, err := util.ResolveSources(resolvedEnvs, constants.RootDir) | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 				for index, src := range srcs { | ||||
| 					if !filepath.IsAbs(src) { | ||||
| 						srcs[index] = filepath.Join(constants.RootDir, src) | ||||
| 					} | ||||
| 				} | ||||
| 				dependencies = append(dependencies, srcs...) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return dependencies, nil | ||||
| 	return false | ||||
| } | ||||
|  |  | |||
|  | @ -17,7 +17,6 @@ limitations under the License. | |||
| package dockerfile | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
|  | @ -95,82 +94,59 @@ func Test_ValidateTarget(t *testing.T) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func Test_Dependencies(t *testing.T) { | ||||
| 	testDir, err := ioutil.TempDir("", "") | ||||
| func Test_SaveStage(t *testing.T) { | ||||
| 	tempDir, err := ioutil.TempDir("", "") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 		t.Fatalf("couldn't create temp dir: %v", err) | ||||
| 	} | ||||
| 	helloPath := filepath.Join(testDir, "hello") | ||||
| 	if err := os.Mkdir(helloPath, 0755); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	dockerfile := fmt.Sprintf(` | ||||
| 	FROM scratch | ||||
| 	COPY %s %s | ||||
| 	defer os.RemoveAll(tempDir) | ||||
| 	files := map[string]string{ | ||||
| 		"Dockerfile": ` | ||||
| 		FROM scratch | ||||
| 		RUN echo hi > /hi | ||||
| 		 | ||||
| 		FROM scratch AS second | ||||
| 		COPY --from=0 /hi /hi2 | ||||
| 	 | ||||
| 	FROM scratch AS second | ||||
| 	ENV hienv %s | ||||
| 	COPY a b | ||||
| 	COPY --from=0 /$hienv %s /hi2/ | ||||
| 	`, helloPath, helloPath, helloPath, testDir) | ||||
| 
 | ||||
| 	stages, err := Parse([]byte(dockerfile)) | ||||
| 		FROM second | ||||
| 		RUN xxx | ||||
| 		 | ||||
| 		FROM scratch | ||||
| 		COPY --from=second /hi2 /hi3 | ||||
| 		`, | ||||
| 	} | ||||
| 	if err := testutil.SetupFiles(tempDir, files); err != nil { | ||||
| 		t.Fatalf("couldn't create dockerfile: %v", err) | ||||
| 	} | ||||
| 	stages, err := Stages(filepath.Join(tempDir, "Dockerfile"), "") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 		t.Fatalf("couldn't retrieve stages from Dockerfile: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	expectedDependencies := [][]string{ | ||||
| 	tests := []struct { | ||||
| 		name     string | ||||
| 		index    int | ||||
| 		expected bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			helloPath, | ||||
| 			testDir, | ||||
| 			name:     "reference stage in later copy command", | ||||
| 			index:    0, | ||||
| 			expected: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "reference stage in later from command", | ||||
| 			index:    1, | ||||
| 			expected: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "don't reference stage later", | ||||
| 			index:    2, | ||||
| 			expected: false, | ||||
| 		}, | ||||
| 		{}, | ||||
| 	} | ||||
| 
 | ||||
| 	for index := range stages { | ||||
| 		buildArgs := NewBuildArgs([]string{}) | ||||
| 		actualDeps, err := Dependencies(index, stages, buildArgs) | ||||
| 		testutil.CheckErrorAndDeepEqual(t, false, err, expectedDependencies[index], actualDeps) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func Test_DependenciesWithArg(t *testing.T) { | ||||
| 	testDir, err := ioutil.TempDir("", "") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	helloPath := filepath.Join(testDir, "hello") | ||||
| 	if err := os.Mkdir(helloPath, 0755); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	dockerfile := fmt.Sprintf(` | ||||
| 	FROM scratch | ||||
| 	COPY %s %s | ||||
| 	 | ||||
| 	FROM scratch AS second | ||||
| 	ARG hienv | ||||
| 	COPY a b | ||||
| 	COPY --from=0 /$hienv %s /hi2/ | ||||
| 	`, helloPath, helloPath, testDir) | ||||
| 
 | ||||
| 	stages, err := Parse([]byte(dockerfile)) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	expectedDependencies := [][]string{ | ||||
| 		{ | ||||
| 			helloPath, | ||||
| 			testDir, | ||||
| 		}, | ||||
| 		{}, | ||||
| 	} | ||||
| 	buildArgs := NewBuildArgs([]string{fmt.Sprintf("hienv=%s", helloPath)}) | ||||
| 
 | ||||
| 	for index := range stages { | ||||
| 		actualDeps, err := Dependencies(index, stages, buildArgs) | ||||
| 		testutil.CheckErrorAndDeepEqual(t, false, err, expectedDependencies[index], actualDeps) | ||||
| 	for _, test := range tests { | ||||
| 		t.Run(test.name, func(t *testing.T) { | ||||
| 			actual := SaveStage(test.index, stages) | ||||
| 			testutil.CheckErrorAndDeepEqual(t, false, nil, test.expected, actual) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -58,32 +58,23 @@ type KanikoBuildArgs struct { | |||
| 
 | ||||
| func DoBuild(k KanikoBuildArgs) (v1.Image, error) { | ||||
| 	// Parse dockerfile and unpack base image to root
 | ||||
| 	d, err := ioutil.ReadFile(k.DockerfilePath) | ||||
| 	stages, err := dockerfile.Stages(k.DockerfilePath, k.Target) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	stages, err := dockerfile.Parse(d) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if err := dockerfile.ValidateTarget(stages, k.Target); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	dockerfile.ResolveStages(stages) | ||||
| 
 | ||||
| 	hasher, err := getHasher(k.SnapshotMode) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	for index, stage := range stages { | ||||
| 		finalStage := (index == len(stages)-1) || (k.Target == stage.Name) | ||||
| 		finalStage := finalStage(index, k.Target, stages) | ||||
| 		// Unpack file system to root
 | ||||
| 		sourceImage, err := util.RetrieveSourceImage(index, k.Args, stages) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if err := util.GetFSFromImage(sourceImage); err != nil { | ||||
| 		if err := util.GetFSFromImage(constants.RootDir, sourceImage); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		l := snapshot.NewLayeredMap(hasher) | ||||
|  | @ -168,11 +159,13 @@ func DoBuild(k KanikoBuildArgs) (v1.Image, error) { | |||
| 			} | ||||
| 			return sourceImage, nil | ||||
| 		} | ||||
| 		if err := saveStageAsTarball(index, sourceImage); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if err := saveStageDependencies(index, stages, buildArgs.Clone()); err != nil { | ||||
| 			return nil, err | ||||
| 		if dockerfile.SaveStage(index, stages) { | ||||
| 			if err := saveStageAsTarball(index, sourceImage); err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			if err := extractImageToDependecyDir(index, sourceImage); err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 		} | ||||
| 		// Delete the filesystem
 | ||||
| 		if err := util.DeleteFilesystem(); err != nil { | ||||
|  | @ -225,44 +218,24 @@ func DoPush(image v1.Image, destinations []string, tarPath string) error { | |||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| func saveStageDependencies(index int, stages []instructions.Stage, buildArgs *dockerfile.BuildArgs) error { | ||||
| 	// First, get the files in this stage later stages will need
 | ||||
| 	dependencies, err := dockerfile.Dependencies(index, stages, buildArgs) | ||||
| 	logrus.Infof("saving dependencies %s", dependencies) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 
 | ||||
| func finalStage(index int, target string, stages []instructions.Stage) bool { | ||||
| 	if index == len(stages)-1 { | ||||
| 		return true | ||||
| 	} | ||||
| 	if len(dependencies) == 0 { | ||||
| 		return nil | ||||
| 	if target == "" { | ||||
| 		return false | ||||
| 	} | ||||
| 	// Then, create the directory they will exist in
 | ||||
| 	i := strconv.Itoa(index) | ||||
| 	dependencyDir := filepath.Join(constants.KanikoDir, i) | ||||
| 	return target == stages[index].Name | ||||
| } | ||||
| 
 | ||||
| func extractImageToDependecyDir(index int, image v1.Image) error { | ||||
| 	dependencyDir := filepath.Join(constants.KanikoDir, strconv.Itoa(index)) | ||||
| 	if err := os.MkdirAll(dependencyDir, 0755); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	// Now, copy over dependencies to this dir
 | ||||
| 	for _, d := range dependencies { | ||||
| 		fi, err := os.Lstat(d) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		dest := filepath.Join(dependencyDir, d) | ||||
| 		if fi.IsDir() { | ||||
| 			if err := util.CopyDir(d, dest); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} else if fi.Mode()&os.ModeSymlink != 0 { | ||||
| 			if err := util.CopySymlink(d, dest); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} else { | ||||
| 			if err := util.CopyFile(d, dest); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| 	logrus.Infof("trying to extract to %s", dependencyDir) | ||||
| 	return util.GetFSFromImage(dependencyDir, image) | ||||
| } | ||||
| 
 | ||||
| func saveStageAsTarball(stageIndex int, image v1.Image) error { | ||||
|  |  | |||
|  | @ -42,7 +42,7 @@ var whitelist = []string{ | |||
| } | ||||
| var volumeWhitelist = []string{} | ||||
| 
 | ||||
| func GetFSFromImage(img v1.Image) error { | ||||
| func GetFSFromImage(root string, img v1.Image) error { | ||||
| 	whitelist, err := fileSystemWhitelist(constants.WhitelistPath) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
|  | @ -72,7 +72,7 @@ func GetFSFromImage(img v1.Image) error { | |||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			path := filepath.Join("/", filepath.Clean(hdr.Name)) | ||||
| 			path := filepath.Join(root, filepath.Clean(hdr.Name)) | ||||
| 			base := filepath.Base(path) | ||||
| 			dir := filepath.Dir(path) | ||||
| 			if strings.HasPrefix(base, ".wh.") { | ||||
|  | @ -91,7 +91,7 @@ func GetFSFromImage(img v1.Image) error { | |||
| 				continue | ||||
| 			} | ||||
| 
 | ||||
| 			if CheckWhitelist(path) { | ||||
| 			if CheckWhitelist(path) && !checkWhitelistRoot(root) { | ||||
| 				logrus.Infof("Not adding %s because it is whitelisted", path) | ||||
| 				continue | ||||
| 			} | ||||
|  | @ -103,7 +103,7 @@ func GetFSFromImage(img v1.Image) error { | |||
| 			} | ||||
| 			fs[path] = struct{}{} | ||||
| 
 | ||||
| 			if err := extractFile("/", hdr, tr); err != nil { | ||||
| 			if err := extractFile(root, hdr, tr); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | @ -256,6 +256,18 @@ func CheckWhitelist(path string) bool { | |||
| 	return false | ||||
| } | ||||
| 
 | ||||
| func checkWhitelistRoot(root string) bool { | ||||
| 	if root == constants.RootDir { | ||||
| 		return false | ||||
| 	} | ||||
| 	for _, wl := range whitelist { | ||||
| 		if HasFilepathPrefix(root, wl) { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // Get whitelist from roots of mounted files
 | ||||
| // Each line of /proc/self/mountinfo is in the form:
 | ||||
| // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
 | ||||
|  |  | |||
|  | @ -83,5 +83,4 @@ func remoteImage(image string) (v1.Image, error) { | |||
| 	} | ||||
| 	kc := authn.NewMultiKeychain(authn.DefaultKeychain, k8sc) | ||||
| 	return remote.Image(ref, remote.WithAuthFromKeychain(kc)) | ||||
| 
 | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue