This adds a --resolve-symlinks flag to the flux build artifact and flux push artifact commands. When enabled, symlinks in the source directory are resolved (copied as regular files/directories) before building the artifact. This includes: - Recursive symlink resolution with cycle detection - File permission preservation - Proper handling of both single-file and directory symlink targets - Comprehensive test coverage Fixes #5055 Signed-off-by: Rohan Sood <56945243+rohansood10@users.noreply.github.com>
182 lines
5.5 KiB
Go
182 lines
5.5 KiB
Go
/*
|
|
Copyright 2022 The Flux authors
|
|
|
|
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 (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
func Test_saveReaderToFile(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
testString := `apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: myapp
|
|
data:
|
|
foo: bar`
|
|
|
|
tests := []struct {
|
|
name string
|
|
string string
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "yaml",
|
|
string: testString,
|
|
},
|
|
{
|
|
name: "yaml with carriage return",
|
|
string: testString + "\r\n",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpFile, err := saveReaderToFile(strings.NewReader(tt.string))
|
|
g.Expect(err).To(BeNil())
|
|
|
|
t.Cleanup(func() { _ = os.Remove(tmpFile) })
|
|
|
|
b, err := os.ReadFile(tmpFile)
|
|
if tt.expectErr {
|
|
g.Expect(err).To(Not(BeNil()))
|
|
return
|
|
}
|
|
|
|
g.Expect(err).To(BeNil())
|
|
g.Expect(string(b)).To(BeEquivalentTo(testString))
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
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, cleanupDir, err := resolveSymlinks(symlinkDir)
|
|
g.Expect(err).To(BeNil())
|
|
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
|
|
|
|
// 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())
|
|
|
|
// Verify that the symlinked directory was resolved and its contents were copied
|
|
content, err = os.ReadFile(filepath.Join(resolved, "linkeddir", "nested.yaml"))
|
|
g.Expect(err).To(BeNil())
|
|
g.Expect(string(content)).To(Equal("nested"))
|
|
|
|
// Verify that the file inside the symlinked directory is a regular file
|
|
info, err = os.Lstat(filepath.Join(resolved, "linkeddir", "nested.yaml"))
|
|
g.Expect(err).To(BeNil())
|
|
g.Expect(info.Mode().IsRegular()).To(BeTrue())
|
|
}
|
|
|
|
func Test_resolveSymlinks_singleFile(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
// Create a real file
|
|
srcDir := t.TempDir()
|
|
realFile := filepath.Join(srcDir, "manifest.yaml")
|
|
g.Expect(os.WriteFile(realFile, []byte("kind: ConfigMap"), 0o644)).To(Succeed())
|
|
|
|
// Create a symlink to the real file
|
|
linkDir := t.TempDir()
|
|
linkFile := filepath.Join(linkDir, "link.yaml")
|
|
g.Expect(os.Symlink(realFile, linkFile)).To(Succeed())
|
|
|
|
// Resolve the single symlinked file
|
|
resolved, cleanupDir, err := resolveSymlinks(linkFile)
|
|
g.Expect(err).To(BeNil())
|
|
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
|
|
|
|
// The returned path should be a file, not a directory
|
|
info, err := os.Stat(resolved)
|
|
g.Expect(err).To(BeNil())
|
|
g.Expect(info.IsDir()).To(BeFalse())
|
|
|
|
// Verify contents
|
|
content, err := os.ReadFile(resolved)
|
|
g.Expect(err).To(BeNil())
|
|
g.Expect(string(content)).To(Equal("kind: ConfigMap"))
|
|
}
|
|
|
|
func Test_resolveSymlinks_cycle(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
// Create a directory with a symlink cycle: dir/link -> dir
|
|
dir := t.TempDir()
|
|
g.Expect(os.WriteFile(filepath.Join(dir, "file.yaml"), []byte("data"), 0o644)).To(Succeed())
|
|
g.Expect(os.Symlink(dir, filepath.Join(dir, "cycle"))).To(Succeed())
|
|
|
|
// resolveSymlinks should not infinite-loop
|
|
resolved, cleanupDir, err := resolveSymlinks(dir)
|
|
g.Expect(err).To(BeNil())
|
|
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
|
|
|
|
// The file should be copied
|
|
content, err := os.ReadFile(filepath.Join(resolved, "file.yaml"))
|
|
g.Expect(err).To(BeNil())
|
|
g.Expect(string(content)).To(Equal("data"))
|
|
|
|
// The cycle directory should exist but not cause infinite nesting
|
|
_, err = os.Stat(filepath.Join(resolved, "cycle"))
|
|
g.Expect(err).To(BeNil())
|
|
|
|
// There should NOT be deeply nested cycle/cycle/cycle/... paths
|
|
_, err = os.Stat(filepath.Join(resolved, "cycle", "cycle", "cycle"))
|
|
g.Expect(os.IsNotExist(err)).To(BeTrue())
|
|
}
|