Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions butane/base/util/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package util

import (
"fmt"
"os"
"path/filepath"
"strings"
Expand All @@ -37,17 +38,29 @@ func EnsurePathWithinFilesDir(path, filesDir string) error {
return nil
}

func ReadLocalFile(configPath, filesDir string) ([]byte, error) {
if filesDir == "" {
func ReadLocalFile(configPath string, options common.TranslateOptions) ([]byte, error) {
if options.FilesDir == "" {
// a files dir isn't configured; refuse to read anything
return nil, common.ErrNoFilesDir
}
// calculate file path within FilesDir and check for path traversal
filePath := filepath.Join(filesDir, filepath.FromSlash(configPath))
if err := EnsurePathWithinFilesDir(filePath, filesDir); err != nil {
filePath := filepath.Join(options.FilesDir, filepath.FromSlash(configPath))
if err := EnsurePathWithinFilesDir(filePath, options.FilesDir); err != nil {
return nil, err
}
return os.ReadFile(filePath)
return ReadFile(filePath, options.LocalFileReader)
}

// ReadFile reads an already-validated path with the optional reader.
func ReadFile(path string, reader func(string) ([]byte, error)) ([]byte, error) {
if reader == nil {
return os.ReadFile(path)
}
contents, err := reader(path)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", path, err)
}
return contents, nil
}

// CheckForDecimalMode fails if the specified mode appears to have been
Expand Down
4 changes: 2 additions & 2 deletions butane/base/v0_2/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func translateResource(from Resource, options common.TranslateOptions) (to types

if from.Local != nil {
c := path.New("yaml", "local")
contents, err := baseutil.ReadLocalFile(*from.Local, options.FilesDir)
contents, err := baseutil.ReadLocalFile(*from.Local, options)
if err != nil {
r.AddOnError(c, err)
return
Expand Down Expand Up @@ -265,7 +265,7 @@ func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report
ts.AddTranslation(yamlPath, path.New("json", "storage", "files"))
}
}
contents, err := os.ReadFile(srcPath)
contents, err := baseutil.ReadFile(srcPath, options.LocalFileReader)
if err != nil {
r.AddOnError(yamlPath, err)
return nil
Expand Down
4 changes: 2 additions & 2 deletions butane/base/v0_3/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func translateResource(from Resource, options common.TranslateOptions) (to types

if from.Local != nil {
c := path.New("yaml", "local")
contents, err := baseutil.ReadLocalFile(*from.Local, options.FilesDir)
contents, err := baseutil.ReadLocalFile(*from.Local, options)
if err != nil {
r.AddOnError(c, err)
return
Expand Down Expand Up @@ -272,7 +272,7 @@ func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report
ts.AddTranslation(yamlPath, path.New("json", "storage", "files"))
}
}
contents, err := os.ReadFile(srcPath)
contents, err := baseutil.ReadFile(srcPath, options.LocalFileReader)
if err != nil {
r.AddOnError(yamlPath, err)
return nil
Expand Down
4 changes: 2 additions & 2 deletions butane/base/v0_4/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func translateResource(from Resource, options common.TranslateOptions) (to types

if from.Local != nil {
c := path.New("yaml", "local")
contents, err := baseutil.ReadLocalFile(*from.Local, options.FilesDir)
contents, err := baseutil.ReadLocalFile(*from.Local, options)
if err != nil {
r.AddOnError(c, err)
return
Expand Down Expand Up @@ -287,7 +287,7 @@ func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report
ts.AddTranslation(yamlPath, path.New("json", "storage", "files"))
}
}
contents, err := os.ReadFile(srcPath)
contents, err := baseutil.ReadFile(srcPath, options.LocalFileReader)
if err != nil {
r.AddOnError(yamlPath, err)
return nil
Expand Down
10 changes: 5 additions & 5 deletions butane/base/v0_5/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func translateResource(from Resource, options common.TranslateOptions) (to types

if from.Local != nil {
c := path.New("yaml", "local")
contents, err := baseutil.ReadLocalFile(*from.Local, options.FilesDir)
contents, err := baseutil.ReadLocalFile(*from.Local, options)
if err != nil {
r.AddOnError(c, err)
return
Expand Down Expand Up @@ -236,7 +236,7 @@ func translatePasswdUser(from PasswdUser, options common.TranslateOptions) (to t
}

for keyFileIndex, sshKeyFile := range from.SSHAuthorizedKeysLocal {
sshKeys, err := baseutil.ReadLocalFile(sshKeyFile, options.FilesDir)
sshKeys, err := baseutil.ReadLocalFile(sshKeyFile, options)
if err != nil {
r.AddOnError(c.Append(keyFileIndex), err)
continue
Expand Down Expand Up @@ -265,7 +265,7 @@ func translateUnit(from Unit, options common.TranslateOptions) (to types.Unit, t

if util.NotEmpty(from.ContentsLocal) {
c := path.New("yaml", "contents_local")
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir)
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options)
if err != nil {
r.AddOnError(c, err)
return
Expand All @@ -284,7 +284,7 @@ func translateDropin(from Dropin, options common.TranslateOptions) (to types.Dro

if util.NotEmpty(from.ContentsLocal) {
c := path.New("yaml", "contents_local")
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir)
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options)
if err != nil {
r.AddOnError(c, err)
return
Expand Down Expand Up @@ -377,7 +377,7 @@ func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report
ts.AddTranslation(yamlPath, path.New("json", "storage", "files"))
}
}
contents, err := os.ReadFile(srcPath)
contents, err := baseutil.ReadFile(srcPath, options.LocalFileReader)
if err != nil {
r.AddOnError(yamlPath, err)
return nil
Expand Down
10 changes: 5 additions & 5 deletions butane/base/v0_6/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func translateResource(from Resource, options common.TranslateOptions) (to types

if from.Local != nil {
c := path.New("yaml", "local")
contents, err := baseutil.ReadLocalFile(*from.Local, options.FilesDir)
contents, err := baseutil.ReadLocalFile(*from.Local, options)
if err != nil {
r.AddOnError(c, err)
return
Expand Down Expand Up @@ -236,7 +236,7 @@ func translatePasswdUser(from PasswdUser, options common.TranslateOptions) (to t
}

for keyFileIndex, sshKeyFile := range from.SSHAuthorizedKeysLocal {
sshKeys, err := baseutil.ReadLocalFile(sshKeyFile, options.FilesDir)
sshKeys, err := baseutil.ReadLocalFile(sshKeyFile, options)
if err != nil {
r.AddOnError(c.Append(keyFileIndex), err)
continue
Expand Down Expand Up @@ -265,7 +265,7 @@ func translateUnit(from Unit, options common.TranslateOptions) (to types.Unit, t

if util.NotEmpty(from.ContentsLocal) {
c := path.New("yaml", "contents_local")
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir)
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options)
if err != nil {
r.AddOnError(c, err)
return
Expand All @@ -284,7 +284,7 @@ func translateDropin(from Dropin, options common.TranslateOptions) (to types.Dro

if util.NotEmpty(from.ContentsLocal) {
c := path.New("yaml", "contents_local")
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir)
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options)
if err != nil {
r.AddOnError(c, err)
return
Expand Down Expand Up @@ -377,7 +377,7 @@ func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report
ts.AddTranslation(yamlPath, path.New("json", "storage", "files"))
}
}
contents, err := os.ReadFile(srcPath)
contents, err := baseutil.ReadFile(srcPath, options.LocalFileReader)
if err != nil {
r.AddOnError(yamlPath, err)
return nil
Expand Down
10 changes: 5 additions & 5 deletions butane/base/v0_7/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func translateResource(from Resource, options common.TranslateOptions) (to types

if from.Local != nil {
c := path.New("yaml", "local")
contents, err := baseutil.ReadLocalFile(*from.Local, options.FilesDir)
contents, err := baseutil.ReadLocalFile(*from.Local, options)
if err != nil {
r.AddOnError(c, err)
return
Expand Down Expand Up @@ -237,7 +237,7 @@ func translatePasswdUser(from PasswdUser, options common.TranslateOptions) (to t
}

for keyFileIndex, sshKeyFile := range from.SSHAuthorizedKeysLocal {
sshKeys, err := baseutil.ReadLocalFile(sshKeyFile, options.FilesDir)
sshKeys, err := baseutil.ReadLocalFile(sshKeyFile, options)
if err != nil {
r.AddOnError(c.Append(keyFileIndex), err)
continue
Expand Down Expand Up @@ -266,7 +266,7 @@ func translateUnit(from Unit, options common.TranslateOptions) (to types.Unit, t

if util.NotEmpty(from.ContentsLocal) {
c := path.New("yaml", "contents_local")
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir)
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options)
if err != nil {
r.AddOnError(c, err)
return
Expand All @@ -285,7 +285,7 @@ func translateDropin(from Dropin, options common.TranslateOptions) (to types.Dro

if util.NotEmpty(from.ContentsLocal) {
c := path.New("yaml", "contents_local")
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir)
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options)
if err != nil {
r.AddOnError(c, err)
return
Expand Down Expand Up @@ -416,7 +416,7 @@ func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report
ts.AddTranslation(yamlPath, path.New("json", "storage", "files"))
}
}
contents, err := os.ReadFile(srcPath)
contents, err := baseutil.ReadFile(srcPath, options.LocalFileReader)
if err != nil {
r.AddOnError(yamlPath, err)
return nil
Expand Down
12 changes: 6 additions & 6 deletions butane/base/v0_8_exp/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func translateResource(from Resource, options common.TranslateOptions) (to types

if from.Local != nil {
c := path.New("yaml", "local")
contents, err := baseutil.ReadLocalFile(*from.Local, options.FilesDir)
contents, err := baseutil.ReadLocalFile(*from.Local, options)
if err != nil {
r.AddOnError(c, err)
return
Expand Down Expand Up @@ -242,7 +242,7 @@ func translatePasswdUser(from PasswdUser, options common.TranslateOptions) (to t
}

for keyFileIndex, sshKeyFile := range from.SSHAuthorizedKeysLocal {
sshKeys, err := baseutil.ReadLocalFile(sshKeyFile, options.FilesDir)
sshKeys, err := baseutil.ReadLocalFile(sshKeyFile, options)
if err != nil {
r.AddOnError(c.Append(keyFileIndex), err)
continue
Expand Down Expand Up @@ -271,7 +271,7 @@ func translateUnit(from Unit, options common.TranslateOptions) (to types.Unit, t

if util.NotEmpty(from.ContentsLocal) {
c := path.New("yaml", "contents_local")
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir)
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options)
if err != nil {
r.AddOnError(c, err)
return
Expand All @@ -290,7 +290,7 @@ func translateDropin(from Dropin, options common.TranslateOptions) (to types.Dro

if util.NotEmpty(from.ContentsLocal) {
c := path.New("yaml", "contents_local")
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir)
contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options)
if err != nil {
r.AddOnError(c, err)
return
Expand Down Expand Up @@ -340,7 +340,7 @@ func isTemplateInstance(name string) (bool, string) {
func readLocalOrInlineContents(contentsLocal, contentsInline *string, ctxPath path.ContextPath, options common.TranslateOptions) (content []byte, contentPath path.ContextPath, err error) {
if util.NotEmpty(contentsLocal) {
contentPath = ctxPath.Append("contents_local")
localContents, err := baseutil.ReadLocalFile(*contentsLocal, options.FilesDir)
localContents, err := baseutil.ReadLocalFile(*contentsLocal, options)
if err != nil {
return content, contentPath, err
}
Expand Down Expand Up @@ -587,7 +587,7 @@ func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report
ts.AddTranslation(yamlPath, path.New("json", "storage", "files"))
}
}
contents, err := os.ReadFile(srcPath)
contents, err := baseutil.ReadFile(srcPath, options.LocalFileReader)
if err != nil {
r.AddOnError(yamlPath, err)
return nil
Expand Down
7 changes: 4 additions & 3 deletions butane/config/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
package common

type TranslateOptions struct {
FilesDir string // allow embedding local files relative to this directory
NoResourceAutoCompression bool // skip automatic compression of inline/local resources
DebugPrintTranslations bool // report translations to stderr
FilesDir string // allow embedding local files relative to this directory
LocalFileReader func(string) ([]byte, error) // read local files; nil uses os.ReadFile
NoResourceAutoCompression bool // skip automatic compression of inline/local resources
DebugPrintTranslations bool // report translations to stderr
}

type TranslateBytesOptions struct {
Expand Down
58 changes: 58 additions & 0 deletions butane/internal/file_processor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2026 Red Hat, Inc
//
// 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 main

import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
)

func matchesExtension(path string, extensions []string) bool {
for _, ext := range extensions {
if ext == "*" || strings.HasSuffix(filepath.Base(path), "."+ext) {
return true
}
}
return false
}

func processFile(reader io.Reader, processor string) ([]byte, error) {
if processor == "" {
return io.ReadAll(reader)
}

cmd := exec.Command(processor)
cmd.Stdin = reader
cmd.Stderr = os.Stderr
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("file processor %q failed: %w", processor, err)
}
return output, nil
}

func processLocalFile(path, processor string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
output, err := processFile(file, processor)
return output, errors.Join(err, file.Close())
}
Loading
Loading