This commit is contained in:
Rohan Sood 2026-02-19 09:29:21 -08:00 committed by GitHub
commit 3657a85382
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 186 additions and 14 deletions

View file

@ -22,6 +22,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
@ -48,9 +49,10 @@ from the given directory or a single manifest file.`,
}
type buildArtifactFlags struct {
output string
path string
ignorePaths []string
output string
path string
ignorePaths []string
resolveSymlinks bool
}
var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...)
@ -61,6 +63,7 @@ func init() {
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.path, "path", "p", "", "Path to the directory where the Kubernetes manifests are located.")
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.output, "output", "o", "artifact.tgz", "Path to where the artifact tgz file should be written.")
buildArtifactCmd.Flags().StringSliceVar(&buildArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format")
buildArtifactCmd.Flags().BoolVar(&buildArtifactArgs.resolveSymlinks, "resolve-symlinks", false, "resolve symlinks by copying their targets into the artifact")
buildCmd.AddCommand(buildArtifactCmd)
}
@ -85,6 +88,15 @@ func buildArtifactCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid path '%s', must point to an existing directory or file", path)
}
if buildArtifactArgs.resolveSymlinks {
resolved, err := resolveSymlinks(path)
if err != nil {
return fmt.Errorf("resolving symlinks failed: %w", err)
}
defer os.RemoveAll(resolved)
path = resolved
}
logger.Actionf("building artifact from %s", path)
ociClient := oci.NewClient(oci.DefaultOptions())
@ -96,6 +108,110 @@ func buildArtifactCmdRun(cmd *cobra.Command, args []string) error {
return nil
}
// resolveSymlinks creates a temporary directory with symlinks resolved to their
// real file contents. This allows building artifacts from symlink trees (e.g.,
// those created by Nix) where the actual files live outside the source directory.
func resolveSymlinks(srcPath string) (string, error) {
absPath, err := filepath.Abs(srcPath)
if err != nil {
return "", err
}
info, err := os.Stat(absPath)
if err != nil {
return "", err
}
// For a single file, resolve the symlink and return a temp dir containing it
if !info.IsDir() {
resolved, err := filepath.EvalSymlinks(absPath)
if err != nil {
return "", fmt.Errorf("resolving symlink for %s: %w", absPath, err)
}
tmpDir, err := os.MkdirTemp("", "flux-artifact-*")
if err != nil {
return "", err
}
dst := filepath.Join(tmpDir, filepath.Base(absPath))
if err := copyFile(resolved, dst); err != nil {
os.RemoveAll(tmpDir)
return "", err
}
return tmpDir, nil
}
tmpDir, err := os.MkdirTemp("", "flux-artifact-*")
if err != nil {
return "", err
}
err = filepath.Walk(absPath, func(p string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(absPath, p)
if err != nil {
return err
}
dstPath := filepath.Join(tmpDir, relPath)
// Resolve symlinks to get the real file info
realPath := p
realInfo := fi
if fi.Mode()&os.ModeSymlink != 0 {
realPath, err = filepath.EvalSymlinks(p)
if err != nil {
return fmt.Errorf("resolving symlink %s: %w", p, err)
}
realInfo, err = os.Stat(realPath)
if err != nil {
return fmt.Errorf("stat resolved path %s: %w", realPath, err)
}
}
if realInfo.IsDir() {
return os.MkdirAll(dstPath, realInfo.Mode())
}
if !realInfo.Mode().IsRegular() {
return nil
}
return copyFile(realPath, dstPath)
})
if err != nil {
os.RemoveAll(tmpDir)
return "", err
}
return tmpDir, nil
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
return out.Close()
}
func saveReaderToFile(reader io.Reader) (string, error) {
b, err := io.ReadAll(bufio.NewReader(reader))
if err != nil {

View file

@ -18,6 +18,7 @@ package main
import (
"os"
"path/filepath"
"strings"
"testing"
@ -68,3 +69,47 @@ data:
}
}
func Test_resolveSymlinks(t *testing.T) {
g := NewWithT(t)
// Create source directory with a real file
srcDir := t.TempDir()
realFile := filepath.Join(srcDir, "real.yaml")
g.Expect(os.WriteFile(realFile, []byte("apiVersion: v1\nkind: Namespace\nmetadata:\n name: test\n"), 0o644)).To(Succeed())
// Create a directory with symlinks pointing to files outside it
symlinkDir := t.TempDir()
symlinkFile := filepath.Join(symlinkDir, "linked.yaml")
g.Expect(os.Symlink(realFile, symlinkFile)).To(Succeed())
// Also add a regular file in the symlink dir
regularFile := filepath.Join(symlinkDir, "regular.yaml")
g.Expect(os.WriteFile(regularFile, []byte("apiVersion: v1\nkind: ConfigMap\n"), 0o644)).To(Succeed())
// Create a symlinked subdirectory
subDir := filepath.Join(srcDir, "subdir")
g.Expect(os.MkdirAll(subDir, 0o755)).To(Succeed())
g.Expect(os.WriteFile(filepath.Join(subDir, "nested.yaml"), []byte("nested"), 0o644)).To(Succeed())
g.Expect(os.Symlink(subDir, filepath.Join(symlinkDir, "linkeddir"))).To(Succeed())
// Resolve symlinks
resolved, err := resolveSymlinks(symlinkDir)
g.Expect(err).To(BeNil())
t.Cleanup(func() { os.RemoveAll(resolved) })
// Verify the regular file was copied
content, err := os.ReadFile(filepath.Join(resolved, "regular.yaml"))
g.Expect(err).To(BeNil())
g.Expect(string(content)).To(Equal("apiVersion: v1\nkind: ConfigMap\n"))
// Verify the symlinked file was resolved and copied
content, err = os.ReadFile(filepath.Join(resolved, "linked.yaml"))
g.Expect(err).To(BeNil())
g.Expect(string(content)).To(ContainSubstring("kind: Namespace"))
// Verify that the resolved file is a regular file, not a symlink
info, err := os.Lstat(filepath.Join(resolved, "linked.yaml"))
g.Expect(err).To(BeNil())
g.Expect(info.Mode().IsRegular()).To(BeTrue())
}

View file

@ -103,17 +103,18 @@ The command can read the credentials from '~/.docker/config.json' but they can a
}
type pushArtifactFlags struct {
path string
source string
revision string
creds string
provider flags.SourceOCIProvider
ignorePaths []string
annotations []string
output string
debug bool
reproducible bool
insecure bool
path string
source string
revision string
creds string
provider flags.SourceOCIProvider
ignorePaths []string
annotations []string
output string
debug bool
reproducible bool
insecure bool
resolveSymlinks bool
}
var pushArtifactArgs = newPushArtifactFlags()
@ -137,6 +138,7 @@ func init() {
pushArtifactCmd.Flags().BoolVarP(&pushArtifactArgs.debug, "debug", "", false, "display logs from underlying library")
pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.reproducible, "reproducible", false, "ensure reproducible image digests by setting the created timestamp to '1970-01-01T00:00:00Z'")
pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.insecure, "insecure-registry", false, "allows artifacts to be pushed without TLS")
pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.resolveSymlinks, "resolve-symlinks", false, "resolve symlinks by copying their targets into the artifact")
pushCmd.AddCommand(pushArtifactCmd)
}
@ -183,6 +185,15 @@ func pushArtifactCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid path '%s', must point to an existing directory or file: %w", path, err)
}
if pushArtifactArgs.resolveSymlinks {
resolved, err := resolveSymlinks(path)
if err != nil {
return fmt.Errorf("resolving symlinks failed: %w", err)
}
defer os.RemoveAll(resolved)
path = resolved
}
annotations := map[string]string{}
for _, annotation := range pushArtifactArgs.annotations {
kv := strings.Split(annotation, "=")