diff --git a/pkg/bindings/images/build.go b/pkg/bindings/images/build.go index cff4ed3f05..3bd1c737ff 100644 --- a/pkg/bindings/images/build.go +++ b/pkg/bindings/images/build.go @@ -53,6 +53,21 @@ type BuildResponse struct { Aux json.RawMessage `json:"aux,omitempty"` } +// BuildFilePaths contains the file paths and exclusion patterns for the build context. +type BuildFilePaths struct { + tarContent []string + newContainerFiles []string // dockerfile paths, relative to context dir, ToSlash()ed + dontexcludes []string + excludes []string +} + +// RequestParts contains the components of an HTTP request for the build API. +type RequestParts struct { + Headers http.Header + Params url.Values + Body io.ReadCloser +} + // Modify the build contexts that uses a local windows path. The windows path is // converted into the corresping guest path in the default Windows machine // (e.g. C:\test ==> /mnt/c/test). @@ -89,12 +104,32 @@ func convertVolumeSrcPath(volume string) string { } } -// Build creates an image using a containerfile reference -func Build(ctx context.Context, containerFiles []string, options types.BuildOptions) (*types.BuildReport, error) { - if options.CommonBuildOpts == nil { - options.CommonBuildOpts = new(define.CommonBuildOptions) +// isSupportedVersion checks if the server version is greater than or equal to the specified minimum version. +// It extracts version numbers from the server version string, removing any suffixes like -dev or -rc, +// and compares them using semantic versioning. +func isSupportedVersion(ctx context.Context, minVersion string) (bool, error) { + serverVersion := bindings.ServiceVersion(ctx) + + // Extract just the version numbers (remove -dev, -rc, etc) + versionStr := serverVersion.String() + if idx := strings.Index(versionStr, "-"); idx > 0 { + versionStr = versionStr[:idx] + } + + serverVer, err := semver.ParseTolerant(versionStr) + if err != nil { + return false, fmt.Errorf("parsing server version %q: %w", serverVersion, err) } + minMultipartVersion, _ := semver.ParseTolerant(minVersion) + + return serverVer.GTE(minMultipartVersion), nil +} + +// prepareParams converts BuildOptions into URL parameters for the build API request. +// It handles various build options including capabilities, annotations, CPU settings, +// devices, labels, platforms, volumes, and other build configuration parameters. +func prepareParams(options types.BuildOptions) (url.Values, error) { params := url.Values{} if caps := options.AddCapabilities; len(caps) > 0 { @@ -399,12 +434,6 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti params.Add("groupadd", group) } - var err error - var contextDir string - if contextDir, err = filepath.EvalSymlinks(options.ContextDirectory); err == nil { - options.ContextDirectory = contextDir - } - params.Set("pullpolicy", options.PullPolicy.String()) switch options.CommonBuildOpts.IdentityLabel { @@ -486,59 +515,54 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti params.Add("unsetannotation", uannotation) } - var ( - headers http.Header - ) - if options.SystemContext != nil { - if options.SystemContext.DockerAuthConfig != nil { - headers, err = auth.MakeXRegistryAuthHeader(options.SystemContext, options.SystemContext.DockerAuthConfig.Username, options.SystemContext.DockerAuthConfig.Password) - } else { - headers, err = auth.MakeXRegistryConfigHeader(options.SystemContext, "", "") - } - if options.SystemContext.DockerInsecureSkipTLSVerify == imageTypes.OptionalBoolTrue { - params.Set("tlsVerify", "false") - } - } - if err != nil { - return nil, err - } + return params, nil +} - stdout := io.Writer(os.Stdout) - if options.Out != nil { - stdout = options.Out +// prepareAuthHeaders sets up authentication headers for the build request. +// It handles Docker authentication configuration and TLS verification settings +// from the system context. +func prepareAuthHeaders(options types.BuildOptions, requestParts *RequestParts) (*RequestParts, error) { + var err error + + if options.SystemContext == nil { + return requestParts, err } - contextDir, err = filepath.Abs(options.ContextDirectory) - if err != nil { - logrus.Errorf("Cannot find absolute path of %v: %v", options.ContextDirectory, err) - return nil, err + if options.SystemContext.DockerAuthConfig != nil { + requestParts.Headers, err = auth.MakeXRegistryAuthHeader(options.SystemContext, options.SystemContext.DockerAuthConfig.Username, options.SystemContext.DockerAuthConfig.Password) + } else { + requestParts.Headers, err = auth.MakeXRegistryConfigHeader(options.SystemContext, "", "") } + if options.SystemContext.DockerInsecureSkipTLSVerify == imageTypes.OptionalBoolTrue { + requestParts.Params.Set("tlsVerify", "false") + } + + return requestParts, err +} - tarContent := []string{options.ContextDirectory} - newContainerFiles := []string{} // dockerfile paths, relative to context dir, ToSlash()ed +// prepareContainerFiles processes container files (Dockerfiles/Containerfiles) for the build. +// It handles URLs, stdin input, symlinks, and determines which files need to be included +// in the tar archive versus which are already in the context directory. +func prepareContainerFiles(containerFiles []string, contextDir string, options *BuildOptions, tempManager *TempFileManager) (*BuildFilePaths, error) { + out := BuildFilePaths{ + tarContent: []string{options.ContextDirectory}, + newContainerFiles: []string{}, // dockerfile paths, relative to context dir, ToSlash()ed + dontexcludes: []string{"!Dockerfile", "!Containerfile", "!.dockerignore", "!.containerignore"}, + excludes: []string{}, + } - dontexcludes := []string{"!Dockerfile", "!Containerfile", "!.dockerignore", "!.containerignore"} for _, c := range containerFiles { // Don not add path to containerfile if it is a URL if strings.HasPrefix(c, "http://") || strings.HasPrefix(c, "https://") { - newContainerFiles = append(newContainerFiles, c) + out.newContainerFiles = append(out.newContainerFiles, c) continue } if c == "/dev/stdin" { - content, err := io.ReadAll(os.Stdin) - if err != nil { - return nil, err - } - tmpFile, err := os.CreateTemp("", "build") + stdinFile, err := tempManager.CreateTempFileFromStdin("") if err != nil { - return nil, err - } - defer os.Remove(tmpFile.Name()) // clean up - defer tmpFile.Close() - if _, err := tmpFile.Write(content); err != nil { - return nil, err + return nil, fmt.Errorf("processing stdin: %w", err) } - c = tmpFile.Name() + c = stdinFile } c = filepath.Clean(c) cfDir := filepath.Dir(c) @@ -557,9 +581,9 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti // Do NOT add to tarfile if strings.HasPrefix(containerfile, contextDir+string(filepath.Separator)) { containerfile = strings.TrimPrefix(containerfile, contextDir+string(filepath.Separator)) - dontexcludes = append(dontexcludes, "!"+containerfile) - dontexcludes = append(dontexcludes, "!"+containerfile+".dockerignore") - dontexcludes = append(dontexcludes, "!"+containerfile+".containerignore") + out.dontexcludes = append(out.dontexcludes, "!"+containerfile) + out.dontexcludes = append(out.dontexcludes, "!"+containerfile+".dockerignore") + out.dontexcludes = append(out.dontexcludes, "!"+containerfile+".containerignore") } else { // If Containerfile does not exist, assume it is in context directory and do Not add to tarfile if err := fileutils.Lexists(containerfile); err != nil { @@ -567,251 +591,233 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti return nil, err } containerfile = c - dontexcludes = append(dontexcludes, "!"+containerfile) - dontexcludes = append(dontexcludes, "!"+containerfile+".dockerignore") - dontexcludes = append(dontexcludes, "!"+containerfile+".containerignore") + out.dontexcludes = append(out.dontexcludes, "!"+containerfile) + out.dontexcludes = append(out.dontexcludes, "!"+containerfile+".dockerignore") + out.dontexcludes = append(out.dontexcludes, "!"+containerfile+".containerignore") } else { // If Containerfile does exist and not in the context directory, add it to the tarfile - tarContent = append(tarContent, containerfile) + out.tarContent = append(out.tarContent, containerfile) } } - newContainerFiles = append(newContainerFiles, filepath.ToSlash(containerfile)) + out.newContainerFiles = append(out.newContainerFiles, filepath.ToSlash(containerfile)) } - if len(newContainerFiles) > 0 { - cFileJSON, err := json.Marshal(newContainerFiles) - if err != nil { - return nil, err - } - params.Set("dockerfile", string(cFileJSON)) - } + return &out, nil +} - excludes := options.Excludes - if len(excludes) == 0 { - excludes, _, err = util.ParseDockerignore(newContainerFiles, options.ContextDirectory) - if err != nil { - return nil, err - } - } +// prepareSecrets processes build secrets by creating temporary files for them. +// It moves secrets to the context directory and modifies the secret configuration +// to use relative paths suitable for remote builds. +func prepareSecrets(secrets []string, contextDir string, tempManager *TempFileManager) ([]string, []string, error) { + if len(secrets) == 0 { + return nil, nil, nil + } + + secretsForRemote := []string{} + tarContent := []string{} + + for _, secret := range secrets { + secretOpt := strings.Split(secret, ",") + modifiedOpt := []string{} + for _, token := range secretOpt { + opt, val, hasVal := strings.Cut(token, "=") + if hasVal { + if opt == "src" { + // read specified secret into a tmp file + // move tmp file to tar and change secret source to relative tmp file + tmpSecretFilePath, err := tempManager.CreateTempSecret(val, contextDir) + if err != nil { + return nil, nil, err + } - saveFormat := ldefine.OCIArchive - if options.OutputFormat == define.Dockerv2ImageManifest { - saveFormat = ldefine.V2s2Archive - } + // add tmp file to context dir + tarContent = append(tarContent, tmpSecretFilePath) - // build secrets are usually absolute host path or relative to context dir on host - // in any case move secret to current context and ship the tar. - if secrets := options.CommonBuildOpts.Secrets; len(secrets) > 0 { - secretsForRemote := []string{} - - for _, secret := range secrets { - secretOpt := strings.Split(secret, ",") - if len(secretOpt) > 0 { - modifiedOpt := []string{} - for _, token := range secretOpt { - opt, val, hasVal := strings.Cut(token, "=") - if hasVal { - if opt == "src" { - // read specified secret into a tmp file - // move tmp file to tar and change secret source to relative tmp file - tmpSecretFile, err := os.CreateTemp(options.ContextDirectory, "podman-build-secret") - if err != nil { - return nil, err - } - defer os.Remove(tmpSecretFile.Name()) // clean up - defer tmpSecretFile.Close() - srcSecretFile, err := os.Open(val) - if err != nil { - return nil, err - } - defer srcSecretFile.Close() - _, err = io.Copy(tmpSecretFile, srcSecretFile) - if err != nil { - return nil, err - } - - // add tmp file to context dir - tarContent = append(tarContent, tmpSecretFile.Name()) - - modifiedSrc := fmt.Sprintf("src=%s", filepath.Base(tmpSecretFile.Name())) - modifiedOpt = append(modifiedOpt, modifiedSrc) - } else { - modifiedOpt = append(modifiedOpt, token) - } - } + modifiedSrc := fmt.Sprintf("src=%s", filepath.Base(tmpSecretFilePath)) + modifiedOpt = append(modifiedOpt, modifiedSrc) + } else { + modifiedOpt = append(modifiedOpt, token) } - secretsForRemote = append(secretsForRemote, strings.Join(modifiedOpt, ",")) } } - - c, err := jsoniter.MarshalToString(secretsForRemote) - if err != nil { - return nil, err - } - params.Add("secrets", c) + secretsForRemote = append(secretsForRemote, strings.Join(modifiedOpt, ",")) } - tarfile, err := nTar(append(excludes, dontexcludes...), tarContent...) + return secretsForRemote, tarContent, nil +} + +// prepareRequestBody creates the request body for the build API call. +// It handles both simple tar archives and multipart form data for builds with +// additional build contexts, supporting URLs, images, and local directories. +// WARNING: Caller must close request body. +func prepareRequestBody(ctx context.Context, requestParts *RequestParts, buildFilePaths *BuildFilePaths, options types.BuildOptions) (*RequestParts, error) { + tarfile, err := nTar(append(buildFilePaths.excludes, buildFilePaths.dontexcludes...), buildFilePaths.tarContent...) if err != nil { - logrus.Errorf("Cannot tar container entries %v error: %v", tarContent, err) + logrus.Errorf("Cannot tar container entries %v error: %v", buildFilePaths.tarContent, err) return nil, err } - defer func() { - if err := tarfile.Close(); err != nil { - logrus.Errorf("%v\n", err) - } - }() - var requestBody io.Reader var contentType string // If there are additional build contexts, we need to handle them based on the server version // podman version >= 5.6.0 supports multipart/form-data for additional build contexts that // are local directories or archives. URLs and images are still sent as query parameters. - if len(options.AdditionalBuildContexts) > 0 { - serverVersion := bindings.ServiceVersion(ctx) + isSupported, err := isSupportedVersion(ctx, "5.6.0") + if err != nil { + return nil, err + } - // Extract just the version numbers (remove -dev, -rc, etc) - versionStr := serverVersion.String() - if idx := strings.Index(versionStr, "-"); idx > 0 { - versionStr = versionStr[:idx] - } + if len(options.AdditionalBuildContexts) == 0 { + requestParts.Body = tarfile + logrus.Debugf("Using main build context: %q", options.ContextDirectory) + return requestParts, nil + } - serverVer, err := semver.ParseTolerant(versionStr) + if !isSupported { + convertAdditionalBuildContexts(options.AdditionalBuildContexts) + additionalBuildContextMap, err := jsoniter.Marshal(options.AdditionalBuildContexts) if err != nil { - return nil, fmt.Errorf("parsing server version %q: %w", serverVersion, err) + return nil, err } + requestParts.Params.Set("additionalbuildcontexts", string(additionalBuildContextMap)) - minMultipartVersion, _ := semver.ParseTolerant("5.6.0") + requestParts.Body = tarfile + logrus.Debugf("Using main build context: %q", options.ContextDirectory) + return requestParts, nil + } - if serverVer.GTE(minMultipartVersion) { - imageContexts := make(map[string]string) - urlContexts := make(map[string]string) - localContexts := make(map[string]*define.AdditionalBuildContext) + imageContexts := make(map[string]string) + urlContexts := make(map[string]string) + localContexts := make(map[string]*define.AdditionalBuildContext) - for name, context := range options.AdditionalBuildContexts { - switch { - case context.IsImage: - imageContexts[name] = context.Value - case context.IsURL: - urlContexts[name] = context.Value - default: - localContexts[name] = context - } - } + for name, context := range options.AdditionalBuildContexts { + switch { + case context.IsImage: + imageContexts[name] = context.Value + case context.IsURL: + urlContexts[name] = context.Value + default: + localContexts[name] = context + } + } - logrus.Debugf("URL Contexts: %v", urlContexts) - for name, url := range urlContexts { - params.Add("additionalbuildcontexts", fmt.Sprintf("%s=url:%s", name, url)) - } + logrus.Debugf("URL Contexts: %v", urlContexts) + for name, url := range urlContexts { + requestParts.Params.Add("additionalbuildcontexts", fmt.Sprintf("%s=url:%s", name, url)) + } - logrus.Debugf("Image Contexts: %v", imageContexts) - for name, imageRef := range imageContexts { - params.Add("additionalbuildcontexts", fmt.Sprintf("%s=image:%s", name, imageRef)) - } + logrus.Debugf("Image Contexts: %v", imageContexts) + for name, imageRef := range imageContexts { + requestParts.Params.Add("additionalbuildcontexts", fmt.Sprintf("%s=image:%s", name, imageRef)) + } - if len(localContexts) > 0 { - // Multipart request structure: - // - "MainContext": The main build context as a tar file - // - "build-context-": Each additional local context as a tar file - logrus.Debugf("Using additional local build contexts: %v", localContexts) - pr, pw := io.Pipe() - writer := multipart.NewWriter(pw) - contentType = writer.FormDataContentType() - requestBody = pr - - if headers == nil { - headers = make(http.Header) - } - headers.Set("Content-Type", contentType) + if len(localContexts) == 0 { + requestParts.Body = tarfile + logrus.Debugf("Using main build context: %q", options.ContextDirectory) + return requestParts, nil + } + // Multipart request structure: + // - "MainContext": The main build context as a tar file + // - "build-context-": Each additional local context as a tar file + logrus.Debugf("Using additional local build contexts: %v", localContexts) + pr, pw := io.Pipe() + writer := multipart.NewWriter(pw) + contentType = writer.FormDataContentType() + requestParts.Body = pr - go func() { - defer pw.Close() - defer writer.Close() + if requestParts.Headers == nil { + requestParts.Headers = make(http.Header) + } + requestParts.Headers.Set("Content-Type", contentType) - mainContext, err := writer.CreateFormFile("MainContext", "MainContext.tar") - if err != nil { - pw.CloseWithError(fmt.Errorf("creating form file for main context: %w", err)) - return - } + go func() { + defer pw.Close() + defer writer.Close() - if _, err := io.Copy(mainContext, tarfile); err != nil { - pw.CloseWithError(fmt.Errorf("copying main context: %w", err)) - return - } + mainContext, err := writer.CreateFormFile("MainContext", "MainContext.tar") + if err != nil { + pw.CloseWithError(fmt.Errorf("creating form file for main context: %w", err)) + return + } - for name, context := range localContexts { - logrus.Debugf("Processing additional local context: %s", name) - part, err := writer.CreateFormFile(fmt.Sprintf("build-context-%s", name), name) - if err != nil { - pw.CloseWithError(fmt.Errorf("creating form file for context %q: %w", name, err)) - return - } + if _, err := io.Copy(mainContext, tarfile); err != nil { + pw.CloseWithError(fmt.Errorf("copying main context: %w", err)) + return + } - // Context is already a tar - if archive.IsArchivePath(context.Value) { - file, err := os.Open(context.Value) - if err != nil { - pw.CloseWithError(fmt.Errorf("opening archive %q: %w", name, err)) - return - } - if _, err := io.Copy(part, file); err != nil { - file.Close() - pw.CloseWithError(fmt.Errorf("copying context %q: %w", name, err)) - return - } - file.Close() - } else { - tarContent, err := nTar(nil, context.Value) - if err != nil { - pw.CloseWithError(fmt.Errorf("creating tar content %q: %w", name, err)) - return - } - if _, err = io.Copy(part, tarContent); err != nil { - pw.CloseWithError(fmt.Errorf("copying tar content %q: %w", name, err)) - return - } - if err := tarContent.Close(); err != nil { - logrus.Errorf("Error closing tar content for context %q: %v\n", name, err) - } - } - } - }() - logrus.Debugf("Multipart body is created with content type: %s", contentType) - } else { - requestBody = tarfile - logrus.Debugf("Using main build context: %q", options.ContextDirectory) + defer func() { + if err := tarfile.Close(); err != nil { + logrus.Errorf("%v\n", err) } - } else { - convertAdditionalBuildContexts(options.AdditionalBuildContexts) - additionalBuildContextMap, err := jsoniter.Marshal(options.AdditionalBuildContexts) + }() + + for name, context := range localContexts { + logrus.Debugf("Processing additional local context: %s", name) + part, err := writer.CreateFormFile(fmt.Sprintf("build-context-%s", name), name) if err != nil { - return nil, err + pw.CloseWithError(fmt.Errorf("creating form file for context %q: %w", name, err)) + return } - params.Set("additionalbuildcontexts", string(additionalBuildContextMap)) - requestBody = tarfile - logrus.Debugf("Using main build context: %q", options.ContextDirectory) + // Context is already a tar + if archive.IsArchivePath(context.Value) { + file, err := os.Open(context.Value) + if err != nil { + pw.CloseWithError(fmt.Errorf("opening archive %q: %w", name, err)) + return + } + if _, err := io.Copy(part, file); err != nil { + file.Close() + pw.CloseWithError(fmt.Errorf("copying context %q: %w", name, err)) + return + } + file.Close() + } else { + tarContent, err := nTar(nil, context.Value) + if err != nil { + pw.CloseWithError(fmt.Errorf("creating tar content %q: %w", name, err)) + return + } + if _, err = io.Copy(part, tarContent); err != nil { + pw.CloseWithError(fmt.Errorf("copying tar content %q: %w", name, err)) + return + } + if err := tarContent.Close(); err != nil { + logrus.Errorf("Error closing tar content for context %q: %v\n", name, err) + } + } } - } else { - requestBody = tarfile - logrus.Debugf("Using main build context: %q", options.ContextDirectory) - } + }() + logrus.Debugf("Multipart body is created with content type: %s", contentType) + + return requestParts, nil +} +// executeBuildRequest sends the build request to the API endpoint and returns the response. +// It handles the HTTP request creation and error checking for the build operation. +// WARNING: Caller must close the response body. +func executeBuildRequest(ctx context.Context, endpoint string, requestParts *RequestParts) (*bindings.APIResponse, error) { conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } - response, err := conn.DoRequest(ctx, requestBody, http.MethodPost, "/build", params, headers) + + response, err := conn.DoRequest(ctx, requestParts.Body, http.MethodPost, endpoint, requestParts.Params, requestParts.Headers) if err != nil { return nil, err } - defer response.Body.Close() if !response.IsSuccess() { return nil, response.Process(err) } + return response, nil +} + +// processBuildResponse processes the streaming build response from the API. +// It reads the JSON stream, extracts build output and errors, writes to stdout, +// and returns a build report with the final image ID. +func processBuildResponse(response *bindings.APIResponse, stdout io.Writer, saveFormat string) (*types.BuildReport, error) { body := response.Body.(io.Reader) if logrus.IsLevelEnabled(logrus.DebugLevel) { if v, found := os.LookupEnv("PODMAN_RETAIN_BUILD_ARTIFACT"); found { @@ -868,6 +874,109 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti return &types.BuildReport{ID: id, SaveFormat: saveFormat}, nil } +func Build(ctx context.Context, containerFiles []string, options types.BuildOptions) (*types.BuildReport, error) { + if options.CommonBuildOpts == nil { + options.CommonBuildOpts = new(define.CommonBuildOptions) + } + + tempManager := NewTempFileManager() + defer tempManager.Cleanup() + + params_, err := prepareParams(options) + if err != nil { + return nil, err + } + + var headers http.Header + var requestBody io.ReadCloser + requestParts := &RequestParts{ + Params: params_, + Headers: headers, + Body: requestBody, + } + + var contextDir string + if contextDir, err = filepath.EvalSymlinks(options.ContextDirectory); err == nil { + options.ContextDirectory = contextDir + } + + requestParts, err = prepareAuthHeaders(options, requestParts) + if err != nil { + return nil, err + } + + contextDirAbs, err := filepath.Abs(options.ContextDirectory) + if err != nil { + logrus.Errorf("Cannot find absolute path of %v: %v", options.ContextDirectory, err) + return nil, err + } + + buildFilePaths, err := prepareContainerFiles(containerFiles, contextDirAbs, &options, tempManager) + if err != nil { + return nil, err + } + + if len(buildFilePaths.newContainerFiles) > 0 { + cFileJSON, err := json.Marshal(buildFilePaths.newContainerFiles) + if err != nil { + return nil, err + } + requestParts.Params.Set("dockerfile", string(cFileJSON)) + } + + buildFilePaths.excludes = options.Excludes + if len(buildFilePaths.excludes) == 0 { + buildFilePaths.excludes, _, err = util.ParseDockerignore(buildFilePaths.newContainerFiles, options.ContextDirectory) + if err != nil { + return nil, err + } + } + + // build secrets are usually absolute host path or relative to context dir on host + // in any case move secret to current context and ship the tar. + secretsForRemote, secretsTarContent, err := prepareSecrets(options.CommonBuildOpts.Secrets, options.ContextDirectory, tempManager) + if err != nil { + return nil, err + } + + if len(secretsForRemote) > 0 { + c, err := jsoniter.MarshalToString(secretsForRemote) + if err != nil { + return nil, err + } + requestParts.Params.Add("secrets", c) + buildFilePaths.tarContent = append(buildFilePaths.tarContent, secretsTarContent...) + } + + requestParts, err = prepareRequestBody(ctx, requestParts, buildFilePaths, options) + if err != nil { + return nil, fmt.Errorf("building tar file: %w", err) + } + defer func() { + if err := requestParts.Body.Close(); err != nil { + logrus.Errorf("%v\n", err) + } + }() + + response, err := executeBuildRequest(ctx, "/build", requestParts) + if err != nil { + return nil, err + } + defer response.Body.Close() + + saveFormat := ldefine.OCIArchive + if options.OutputFormat == define.Dockerv2ImageManifest { + saveFormat = ldefine.V2s2Archive + } + + stdout := io.Writer(os.Stdout) + if options.Out != nil { + stdout = options.Out + } + + return processBuildResponse(response, stdout, saveFormat) +} + func nTar(excludes []string, sources ...string) (io.ReadCloser, error) { pm, err := fileutils.NewPatternMatcher(excludes) if err != nil { diff --git a/pkg/bindings/images/utils.go b/pkg/bindings/images/utils.go new file mode 100644 index 0000000000..e9c9cc6e06 --- /dev/null +++ b/pkg/bindings/images/utils.go @@ -0,0 +1,103 @@ +package images + +import ( + "errors" + "fmt" + "io" + "os" + + "github.com/sirupsen/logrus" +) + +// TempFileManager manages temporary files created during image build. +// It maintains a list of created temporary files and provides cleanup functionality +// to ensure proper resource management. +type TempFileManager struct { + files []string +} + +func NewTempFileManager() *TempFileManager { + return &TempFileManager{ + files: make([]string, 0), + } +} + +func (t *TempFileManager) AddFile(filename string) { + t.files = append(t.files, filename) +} + +func (t *TempFileManager) Cleanup() { + for _, file := range t.files { + if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) { + logrus.Errorf("Failed to remove temp file %s: %v", file, err) + } + } + t.files = t.files[:0] // Reset slice +} + +// CreateTempFileFromStdin reads content from stdin and creates a temporary file +// in the specified destination directory. The temporary file is automatically +// added to the manager's cleanup list. +// +// Parameters: +// - dest: The directory where the temporary file should be created +// +// Returns: +// - string: The path to the created temporary file +// - error: Any error encountered during the operation +func (t *TempFileManager) CreateTempFileFromStdin(dest string) (string, error) { + content, err := io.ReadAll(os.Stdin) + if err != nil { + return "", fmt.Errorf("reading from stdin: %w", err) + } + + tmpFile, err := os.CreateTemp(dest, "build-stdin-*") + if err != nil { + return "", fmt.Errorf("creating temp file: %w", err) + } + defer tmpFile.Close() + + filename := tmpFile.Name() + t.AddFile(filename) + + if _, err := tmpFile.Write(content); err != nil { + return "", fmt.Errorf("writing to temp file: %w", err) + } + + return filename, nil +} + +// CreateTempSecret creates a temporary copy of a secret file in the specified +// context directory. The original secret file is copied to a new temporary file +// which is automatically added to the manager's cleanup list. +// +// Parameters: +// - secretPath: The path to the source secret file to copy +// - contextDir: The directory where the temporary secret file should be created +// +// Returns: +// - string: The path to the created temporary secret file +// - error: Any error encountered during the operation +func (t *TempFileManager) CreateTempSecret(secretPath, contextDir string) (string, error) { + tmpSecretFile, err := os.CreateTemp(contextDir, "podman-build-secret-*") + if err != nil { + return "", fmt.Errorf("creating temp secret file: %w", err) + } + defer tmpSecretFile.Close() + + filename := tmpSecretFile.Name() + t.AddFile(filename) + + srcSecretFile, err := os.Open(secretPath) + if err != nil { + tmpSecretFile.Close() + return "", fmt.Errorf("opening secret file %s: %w", secretPath, err) + } + defer srcSecretFile.Close() + + if _, err := io.Copy(tmpSecretFile, srcSecretFile); err != nil { + return "", fmt.Errorf("copying secret content: %w", err) + } + + return filename, nil +} diff --git a/pkg/bindings/images/utils_test.go b/pkg/bindings/images/utils_test.go new file mode 100644 index 0000000000..a73c8d50cd --- /dev/null +++ b/pkg/bindings/images/utils_test.go @@ -0,0 +1,42 @@ +package images + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTempFileManager(t *testing.T) { + manager := NewTempFileManager() + + t.Run("CreateTempFileFromStdin", func(t *testing.T) { + oldStdin := os.Stdin + defer func() { os.Stdin = oldStdin }() + + r, w, err := os.Pipe() + assert.NoError(t, err) + os.Stdin = r + + _, err = w.WriteString("test content") + assert.NoError(t, err) + w.Close() + + filename, err := manager.CreateTempFileFromStdin("") + assert.NoError(t, err) + assert.NotEmpty(t, filename) + + manager.Cleanup() + }) + + t.Run("CreateTempSecret", func(t *testing.T) { + secretPath := "testdata/secret.txt" + contextDir := "testdata" + + filename, err := manager.CreateTempSecret(secretPath, contextDir) + assert.NoError(t, err) + assert.NotEmpty(t, filename) + + manager.Cleanup() + }) +}