mirror of
https://github.com/GoogleContainerTools/kaniko
synced 2026-10-03 21:40:51 +02:00
* fix(ci): Bump golangci-lint to 1.51.1 * chore(lint): fix gofmt and goimport issues * chore(lint): fix linter issues - Adapted error comparison according to linter recommendation - Disabled noctx linting for http request where canceling makes no sense - Disabled nilerror linting where nil error is returned on purpose - Disabled makezero linter where slice is explicitly deepcopied * chore(ci): Update go version in tests workflows * fix(ci): Allow boilerplate years from 2000-2099 Previously the regex only allowed the copyright notice to contain the years 2018,2019,2020,2021, or 2022. This commit widens to regex to 20\d\d allowing any year in the range [2000-2099] * feat(ci): Replace minikube with k3s for intregration tests The existing setup for minikube is very complicated, replicating most of the setup steps for a full kubernetes cluster in an only partially supported minikube configuration (driver=none). Furthermore the existing setup has been broken for sometime, likely, at least in part due to the changes to CNI and CRI in recent kubernetes versions. Since what we actually need is only a running Kubernetes cluster on the node and access to a registry on localhost:5000, we can switch the extremely complicated minikube setup for a lightweight cluster using k3s. Minikube came with a default addon for running a registry on every node, but the same is not the case for k3s, instead we make use of the package helm controller and its HelmChart CR to deploy twuni/docker-registry.helm and expose it on localhost using the integrated LoadBalancer controller. * fix(test-684): pin base container version The dockerfile for the regression test connected to issue 684 used a rolling tag as base image, making it flaky and fail since it was introduced. This commit pins the base image to the digest of bionic-20200219, which, based on the date of the commit that introduced to the dockerfile would be the most newest ubuntu build and likely what the "rolling" tag resolved to back then. Since this also an image from the pre-oci days of ubuntu, this circumvents a bug in container-diff as well (https://github.com/GoogleContainerTools/container-diff/issues/389)
479 lines
14 KiB
Go
479 lines
14 KiB
Go
/*
|
|
Copyright 2018 Google LLC
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package util
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
|
"github.com/moby/buildkit/frontend/dockerfile/instructions"
|
|
"github.com/moby/buildkit/frontend/dockerfile/parser"
|
|
"github.com/moby/buildkit/frontend/dockerfile/shell"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/GoogleContainerTools/kaniko/pkg/config"
|
|
)
|
|
|
|
// for testing
|
|
var (
|
|
getUIDAndGIDFunc = getUIDAndGID
|
|
)
|
|
|
|
const (
|
|
pathSeparator = "/"
|
|
)
|
|
|
|
// ResolveEnvironmentReplacementList resolves a list of values by calling resolveEnvironmentReplacement
|
|
func ResolveEnvironmentReplacementList(values, envs []string, isFilepath bool) ([]string, error) {
|
|
var resolvedValues []string
|
|
for _, value := range values {
|
|
resolved, err := ResolveEnvironmentReplacement(value, envs, isFilepath)
|
|
logrus.Debugf("Resolved %s to %s", value, resolved)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resolvedValues = append(resolvedValues, resolved)
|
|
}
|
|
return resolvedValues, nil
|
|
}
|
|
|
|
// ResolveEnvironmentReplacement resolves replacing env variables in some text from envs
|
|
// It takes in a string representation of the command, the value to be resolved, and a list of envs (config.Env)
|
|
// Ex: value = $foo/newdir, envs = [foo=/foodir], then this should return /foodir/newdir
|
|
// The dockerfile/shell package handles processing env values
|
|
// It handles escape characters and supports expansion from the config.Env array
|
|
// Shlex handles some of the following use cases (these and more are tested in integration tests)
|
|
// ""a'b'c"" -> "a'b'c"
|
|
// "Rex\ The\ Dog \" -> "Rex The Dog"
|
|
// "a\"b" -> "a"b"
|
|
func ResolveEnvironmentReplacement(value string, envs []string, isFilepath bool) (string, error) {
|
|
shlex := shell.NewLex(parser.DefaultEscapeToken)
|
|
fp, err := shlex.ProcessWord(value, envs)
|
|
// Check after replacement if value is a remote URL
|
|
if !isFilepath || IsSrcRemoteFileURL(fp) {
|
|
return fp, err
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
isDir := strings.HasSuffix(fp, pathSeparator)
|
|
fp = filepath.Clean(fp)
|
|
if isDir && !strings.HasSuffix(fp, pathSeparator) {
|
|
fp = fp + pathSeparator
|
|
}
|
|
return fp, nil
|
|
}
|
|
|
|
func ResolveEnvAndWildcards(sd instructions.SourcesAndDest, fileContext FileContext, envs []string) ([]string, string, error) {
|
|
// First, resolve any environment replacement
|
|
resolvedEnvs, err := ResolveEnvironmentReplacementList(sd, envs, true)
|
|
if err != nil {
|
|
return nil, "", errors.Wrap(err, "failed to resolve environment")
|
|
}
|
|
if len(resolvedEnvs) == 0 {
|
|
return nil, "", errors.New("resolved envs is empty")
|
|
}
|
|
dest := resolvedEnvs[len(resolvedEnvs)-1]
|
|
// Resolve wildcards and get a list of resolved sources
|
|
srcs, err := ResolveSources(resolvedEnvs[0:len(resolvedEnvs)-1], fileContext.Root)
|
|
if err != nil {
|
|
return nil, "", errors.Wrap(err, "failed to resolve sources")
|
|
}
|
|
err = IsSrcsValid(sd, srcs, fileContext)
|
|
return srcs, dest, err
|
|
}
|
|
|
|
// ContainsWildcards returns true if any entry in paths contains wildcards
|
|
func ContainsWildcards(paths []string) bool {
|
|
for _, path := range paths {
|
|
if strings.ContainsAny(path, "*?[") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ResolveSources resolves the given sources if the sources contains wildcards
|
|
// It returns a list of resolved sources
|
|
func ResolveSources(srcs []string, root string) ([]string, error) {
|
|
// If sources contain wildcards, we first need to resolve them to actual paths
|
|
if !ContainsWildcards(srcs) {
|
|
return srcs, nil
|
|
}
|
|
logrus.Infof("Resolving srcs %v...", srcs)
|
|
files, err := RelativeFiles("", root)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "resolving sources")
|
|
}
|
|
resolved, err := matchSources(srcs, files)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "matching sources")
|
|
}
|
|
logrus.Debugf("Resolved sources to %v", resolved)
|
|
return resolved, nil
|
|
}
|
|
|
|
// matchSources returns a list of sources that match wildcards
|
|
func matchSources(srcs, files []string) ([]string, error) {
|
|
var matchedSources []string
|
|
for _, src := range srcs {
|
|
if IsSrcRemoteFileURL(src) {
|
|
matchedSources = append(matchedSources, src)
|
|
continue
|
|
}
|
|
src = filepath.Clean(src)
|
|
for _, file := range files {
|
|
if filepath.IsAbs(src) {
|
|
file = filepath.Join(config.RootDir, file)
|
|
}
|
|
matched, err := filepath.Match(src, file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if matched || src == file {
|
|
matchedSources = append(matchedSources, file)
|
|
}
|
|
}
|
|
}
|
|
return matchedSources, nil
|
|
}
|
|
|
|
func IsDestDir(path string) bool {
|
|
// try to stat the path
|
|
fileInfo, err := os.Stat(path)
|
|
if err != nil {
|
|
// fall back to string-based determination
|
|
return strings.HasSuffix(path, pathSeparator) || path == "."
|
|
}
|
|
// if it's a real path, check the fs response
|
|
return fileInfo.IsDir()
|
|
}
|
|
|
|
// DestinationFilepath returns the destination filepath from the build context to the image filesystem
|
|
// If source is a file:
|
|
//
|
|
// If dest is a dir, copy it to /dest/relpath
|
|
// If dest is a file, copy directly to dest
|
|
//
|
|
// If source is a dir:
|
|
//
|
|
// Assume dest is also a dir, and copy to dest/
|
|
//
|
|
// If dest is not an absolute filepath, add /cwd to the beginning
|
|
func DestinationFilepath(src, dest, cwd string) (string, error) {
|
|
_, srcFileName := filepath.Split(src)
|
|
newDest := dest
|
|
|
|
if !filepath.IsAbs(newDest) {
|
|
newDest = filepath.Join(cwd, newDest)
|
|
// join call clean on all results.
|
|
if strings.HasSuffix(dest, pathSeparator) || strings.HasSuffix(dest, ".") {
|
|
newDest += pathSeparator
|
|
}
|
|
}
|
|
if IsDestDir(newDest) {
|
|
newDest = filepath.Join(newDest, srcFileName)
|
|
}
|
|
|
|
if len(srcFileName) <= 0 && !strings.HasSuffix(newDest, pathSeparator) {
|
|
newDest += pathSeparator
|
|
}
|
|
|
|
return newDest, nil
|
|
}
|
|
|
|
// URLDestinationFilepath gives the destination a file from a remote URL should be saved to
|
|
func URLDestinationFilepath(rawurl, dest, cwd string, envs []string) (string, error) {
|
|
if !IsDestDir(dest) {
|
|
if !filepath.IsAbs(dest) {
|
|
return filepath.Join(cwd, dest), nil
|
|
}
|
|
return dest, nil
|
|
}
|
|
urlBase := filepath.Base(rawurl)
|
|
urlBase, err := ResolveEnvironmentReplacement(urlBase, envs, true)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
destPath := filepath.Join(dest, urlBase)
|
|
|
|
if !filepath.IsAbs(dest) {
|
|
destPath = filepath.Join(cwd, destPath)
|
|
}
|
|
return destPath, nil
|
|
}
|
|
|
|
func IsSrcsValid(srcsAndDest instructions.SourcesAndDest, resolvedSources []string, fileContext FileContext) error {
|
|
srcs := srcsAndDest[:len(srcsAndDest)-1]
|
|
dest := srcsAndDest[len(srcsAndDest)-1]
|
|
|
|
if !ContainsWildcards(srcs) {
|
|
totalSrcs := 0
|
|
for _, src := range srcs {
|
|
if fileContext.ExcludesFile(src) {
|
|
continue
|
|
}
|
|
totalSrcs++
|
|
}
|
|
if totalSrcs > 1 && !IsDestDir(dest) {
|
|
return errors.New("when specifying multiple sources in a COPY command, destination must be a directory and end in '/'")
|
|
}
|
|
}
|
|
|
|
// If there is only one source and it's a directory, docker assumes the dest is a directory
|
|
if len(resolvedSources) == 1 {
|
|
if IsSrcRemoteFileURL(resolvedSources[0]) {
|
|
return nil
|
|
}
|
|
path := filepath.Join(fileContext.Root, resolvedSources[0])
|
|
fi, err := os.Lstat(path)
|
|
if err != nil {
|
|
return errors.Wrap(err, fmt.Sprintf("failed to get fileinfo for %v", path))
|
|
}
|
|
if fi.IsDir() {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
totalFiles := 0
|
|
for _, src := range resolvedSources {
|
|
if IsSrcRemoteFileURL(src) {
|
|
totalFiles++
|
|
continue
|
|
}
|
|
src = filepath.Clean(src)
|
|
files, err := RelativeFiles(src, fileContext.Root)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to get relative files")
|
|
}
|
|
for _, file := range files {
|
|
if fileContext.ExcludesFile(file) {
|
|
continue
|
|
}
|
|
totalFiles++
|
|
}
|
|
}
|
|
if totalFiles == 0 {
|
|
return errors.New("copy failed: no source files specified")
|
|
}
|
|
// If there are wildcards, and the destination is a file, there must be exactly one file to copy over,
|
|
// Otherwise, return an error
|
|
if !IsDestDir(dest) && totalFiles > 1 {
|
|
return errors.New("when specifying multiple sources in a COPY command, destination must be a directory and end in '/'")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func IsSrcRemoteFileURL(rawurl string) bool {
|
|
_, err := url.ParseRequestURI(rawurl)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_, err = http.Get(rawurl) //nolint:noctx
|
|
return err == nil
|
|
}
|
|
|
|
func UpdateConfigEnv(envVars []instructions.KeyValuePair, config *v1.Config, replacementEnvs []string) error {
|
|
newEnvs := make([]instructions.KeyValuePair, len(envVars))
|
|
for index, pair := range envVars {
|
|
expandedKey, err := ResolveEnvironmentReplacement(pair.Key, replacementEnvs, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
expandedValue, err := ResolveEnvironmentReplacement(pair.Value, replacementEnvs, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newEnvs[index] = instructions.KeyValuePair{
|
|
Key: expandedKey,
|
|
Value: expandedValue,
|
|
}
|
|
}
|
|
|
|
// First, convert config.Env array to []instruction.KeyValuePair
|
|
var kvps []instructions.KeyValuePair
|
|
for _, env := range config.Env {
|
|
entry := strings.SplitN(env, "=", 2)
|
|
kvps = append(kvps, instructions.KeyValuePair{
|
|
Key: entry[0],
|
|
Value: entry[1],
|
|
})
|
|
}
|
|
// Iterate through new environment variables, and replace existing keys
|
|
// We can't use a map because we need to preserve the order of the environment variables
|
|
Loop:
|
|
for _, newEnv := range newEnvs {
|
|
for index, kvp := range kvps {
|
|
// If key exists, replace the KeyValuePair...
|
|
if kvp.Key == newEnv.Key {
|
|
logrus.Debugf("Replacing environment variable %v with %v in config", kvp, newEnv)
|
|
kvps[index] = newEnv
|
|
continue Loop
|
|
}
|
|
}
|
|
// ... Else, append it as a new env variable
|
|
kvps = append(kvps, newEnv)
|
|
}
|
|
// Convert back to array and set in config
|
|
envArray := []string{}
|
|
for _, kvp := range kvps {
|
|
entry := kvp.Key + "=" + kvp.Value
|
|
envArray = append(envArray, entry)
|
|
}
|
|
config.Env = envArray
|
|
return nil
|
|
}
|
|
|
|
func GetUserGroup(chownStr string, env []string) (int64, int64, error) {
|
|
if chownStr == "" {
|
|
return DoNotChangeUID, DoNotChangeGID, nil
|
|
}
|
|
|
|
chown, err := ResolveEnvironmentReplacement(chownStr, env, false)
|
|
if err != nil {
|
|
return -1, -1, err
|
|
}
|
|
|
|
uid32, gid32, err := getUIDAndGIDFromString(chown, true)
|
|
if err != nil {
|
|
return -1, -1, err
|
|
}
|
|
|
|
return int64(uid32), int64(gid32), nil
|
|
}
|
|
|
|
// Extract user and group id from a string formatted 'user:group'.
|
|
// If fallbackToUID is set, the gid is equal to uid if the group is not specified
|
|
// otherwise gid is set to zero.
|
|
// UserID and GroupID don't need to be present on the system.
|
|
func getUIDAndGIDFromString(userGroupString string, fallbackToUID bool) (uint32, uint32, error) {
|
|
userAndGroup := strings.Split(userGroupString, ":")
|
|
userStr := userAndGroup[0]
|
|
var groupStr string
|
|
if len(userAndGroup) > 1 {
|
|
groupStr = userAndGroup[1]
|
|
}
|
|
return getUIDAndGIDFunc(userStr, groupStr, fallbackToUID)
|
|
}
|
|
|
|
func getUIDAndGID(userStr string, groupStr string, fallbackToUID bool) (uint32, uint32, error) {
|
|
user, err := LookupUser(userStr)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
uid32, err := getUID(user.Uid)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
gid, err := getGIDFromName(groupStr, fallbackToUID)
|
|
if err != nil {
|
|
if errors.Is(err, fallbackToUIDError) {
|
|
return uid32, uid32, nil
|
|
}
|
|
return 0, 0, err
|
|
}
|
|
return uid32, gid, nil
|
|
}
|
|
|
|
// getGID tries to parse the gid or falls back to getGroupFromName if it's not an id
|
|
func getGID(groupStr string, fallbackToUID bool) (uint32, error) {
|
|
gid, err := strconv.ParseUint(groupStr, 10, 32)
|
|
if err != nil {
|
|
return 0, fallbackToUIDOrError(err, fallbackToUID)
|
|
}
|
|
return uint32(gid), nil
|
|
}
|
|
|
|
// getGIDFromName tries to parse the groupStr into an existing group.
|
|
// if the group doesn't exist, fallback to getGID to parse non-existing valid GIDs.
|
|
func getGIDFromName(groupStr string, fallbackToUID bool) (uint32, error) {
|
|
group, err := user.LookupGroup(groupStr)
|
|
if err != nil {
|
|
// unknown group error could relate to a non existing group
|
|
var groupErr *user.UnknownGroupError
|
|
if errors.Is(err, groupErr) {
|
|
return getGID(groupStr, fallbackToUID)
|
|
}
|
|
group, err = user.LookupGroupId(groupStr)
|
|
if err != nil {
|
|
return getGID(groupStr, fallbackToUID)
|
|
}
|
|
}
|
|
return getGID(group.Gid, fallbackToUID)
|
|
}
|
|
|
|
var fallbackToUIDError = new(fallbackToUIDErrorType)
|
|
|
|
type fallbackToUIDErrorType struct{}
|
|
|
|
func (e fallbackToUIDErrorType) Error() string {
|
|
return "fallback to uid"
|
|
}
|
|
|
|
func fallbackToUIDOrError(err error, fallbackToUID bool) error {
|
|
if fallbackToUID {
|
|
return fallbackToUIDError
|
|
}
|
|
return err
|
|
}
|
|
|
|
// LookupUser will try to lookup the userStr inside the passwd file.
|
|
// If the user does not exists, the function will fallback to parsing the userStr as an uid.
|
|
func LookupUser(userStr string) (*user.User, error) {
|
|
userObj, err := user.Lookup(userStr)
|
|
if err != nil {
|
|
unknownUserErr := new(user.UnknownUserError)
|
|
// only return if it's not an unknown user error or the passwd file does not exist
|
|
if !errors.As(err, unknownUserErr) && !os.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
|
|
// Lookup by id
|
|
userObj, err = user.LookupId(userStr)
|
|
if err != nil {
|
|
uid, err := getUID(userStr)
|
|
if err != nil {
|
|
// at this point, the user does not exist and the userStr is not a valid number.
|
|
return nil, fmt.Errorf("user %v is not a uid and does not exist on the system", userStr)
|
|
}
|
|
userObj = &user.User{
|
|
Uid: fmt.Sprint(uid),
|
|
HomeDir: "/",
|
|
}
|
|
}
|
|
}
|
|
return userObj, nil
|
|
}
|
|
|
|
func getUID(userStr string) (uint32, error) {
|
|
// checkif userStr is a valid id
|
|
uid, err := strconv.ParseUint(userStr, 10, 32)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return uint32(uid), nil
|
|
}
|