Skip to content

Commit 410b62e

Browse files
committed
Add build context management and logging functionality
- Introduced ContextTarStreamer for creating tar streams of build contexts, including support for .dockerignore. - Implemented StyledBuildLogger for enhanced logging of build outputs with styled messages. - Developed BuildOrchestrator to manage the build process for multiple services, supporting both remote and local builds. - Added TagGenerator and ContentHasher for generating deterministic image tags and content hashes. - Included validation for image tags and tag formats to ensure compliance with Docker naming conventions.
1 parent 3b3c90e commit 410b62e

File tree

4 files changed

+1177
-0
lines changed

4 files changed

+1177
-0
lines changed

internal/build/context.go

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
package build
2+
3+
import (
4+
"archive/tar"
5+
"bufio"
6+
"fmt"
7+
"io"
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
)
12+
13+
// ContextTarStreamer handles creating tar streams of build contexts
14+
type ContextTarStreamer struct {
15+
WarnThresholdMB int
16+
}
17+
18+
// NewContextTarStreamer creates a new context tar streamer
19+
func NewContextTarStreamer(warnThresholdMB int) *ContextTarStreamer {
20+
return &ContextTarStreamer{
21+
WarnThresholdMB: warnThresholdMB,
22+
}
23+
}
24+
25+
// CreateTarStream creates a tar stream of the build context
26+
func (cts *ContextTarStreamer) CreateTarStream(contextPath string) (io.ReadCloser, error) {
27+
// Validate context path
28+
if !isDirectory(contextPath) {
29+
return nil, fmt.Errorf("context path is not a directory: %s", contextPath)
30+
}
31+
32+
// Load .dockerignore patterns
33+
ignorePatterns, err := cts.loadDockerignore(contextPath)
34+
if err != nil {
35+
return nil, fmt.Errorf("failed to load .dockerignore: %w", err)
36+
}
37+
38+
// Create pipe for streaming
39+
reader, writer := io.Pipe()
40+
41+
// Start goroutine to write tar
42+
go func() {
43+
defer writer.Close()
44+
45+
tw := tar.NewWriter(writer)
46+
defer tw.Close()
47+
48+
err := cts.writeContextToTar(contextPath, ignorePatterns, tw)
49+
if err != nil {
50+
writer.CloseWithError(err)
51+
return
52+
}
53+
}()
54+
55+
return reader, nil
56+
}
57+
58+
// loadDockerignore loads .dockerignore patterns from the context directory
59+
func (cts *ContextTarStreamer) loadDockerignore(contextPath string) ([]string, error) {
60+
dockerignorePath := filepath.Join(contextPath, ".dockerignore")
61+
62+
// Check if .dockerignore exists
63+
if _, err := os.Stat(dockerignorePath); os.IsNotExist(err) {
64+
return []string{}, nil // No .dockerignore file
65+
}
66+
67+
file, err := os.Open(dockerignorePath)
68+
if err != nil {
69+
return nil, fmt.Errorf("failed to open .dockerignore: %w", err)
70+
}
71+
defer file.Close()
72+
73+
var patterns []string
74+
scanner := bufio.NewScanner(file)
75+
76+
for scanner.Scan() {
77+
line := strings.TrimSpace(scanner.Text())
78+
79+
// Skip empty lines and comments
80+
if line == "" || strings.HasPrefix(line, "#") {
81+
continue
82+
}
83+
84+
patterns = append(patterns, line)
85+
}
86+
87+
if err := scanner.Err(); err != nil {
88+
return nil, fmt.Errorf("failed to read .dockerignore: %w", err)
89+
}
90+
91+
return patterns, nil
92+
}
93+
94+
// writeContextToTar writes the build context to a tar writer
95+
func (cts *ContextTarStreamer) writeContextToTar(contextPath string, ignorePatterns []string, tw *tar.Writer) error {
96+
var totalSize int64
97+
98+
err := filepath.Walk(contextPath, func(path string, info os.FileInfo, err error) error {
99+
if err != nil {
100+
return err
101+
}
102+
103+
// Skip the root directory itself
104+
if path == contextPath {
105+
return nil
106+
}
107+
108+
// Get relative path from context
109+
relPath, err := filepath.Rel(contextPath, path)
110+
if err != nil {
111+
return err
112+
}
113+
114+
// Normalize path separators for cross-platform compatibility
115+
relPath = filepath.ToSlash(relPath)
116+
117+
// Check if path should be ignored
118+
if cts.shouldIgnore(relPath, ignorePatterns) {
119+
if info.IsDir() {
120+
return filepath.SkipDir
121+
}
122+
return nil
123+
}
124+
125+
// Create tar header
126+
header, err := tar.FileInfoHeader(info, "")
127+
if err != nil {
128+
return err
129+
}
130+
131+
// Set the name in the tar header
132+
header.Name = relPath
133+
134+
// Write header
135+
if err := tw.WriteHeader(header); err != nil {
136+
return err
137+
}
138+
139+
// Write file content for regular files
140+
if info.Mode().IsRegular() {
141+
file, err := os.Open(path)
142+
if err != nil {
143+
return err
144+
}
145+
defer file.Close()
146+
147+
// Copy file content and track size
148+
written, err := io.Copy(tw, file)
149+
if err != nil {
150+
return err
151+
}
152+
153+
totalSize += written
154+
155+
// Check size threshold
156+
if cts.WarnThresholdMB > 0 && totalSize > int64(cts.WarnThresholdMB*1024*1024) {
157+
// Note: In a real implementation, you might want to emit a warning here
158+
// For now, we'll continue but this could be enhanced to emit warnings
159+
}
160+
}
161+
162+
return nil
163+
})
164+
165+
return err
166+
}
167+
168+
// shouldIgnore checks if a path should be ignored based on .dockerignore patterns
169+
func (cts *ContextTarStreamer) shouldIgnore(relPath string, patterns []string) bool {
170+
for _, pattern := range patterns {
171+
if cts.matchesPattern(relPath, pattern) {
172+
return true
173+
}
174+
}
175+
return false
176+
}
177+
178+
// matchesPattern checks if a path matches a .dockerignore pattern
179+
func (cts *ContextTarStreamer) matchesPattern(relPath, pattern string) bool {
180+
// Handle directory patterns (ending with /)
181+
if strings.HasSuffix(pattern, "/") {
182+
dirPattern := strings.TrimSuffix(pattern, "/")
183+
return strings.HasPrefix(relPath, dirPattern+"/") || relPath == dirPattern
184+
}
185+
186+
// Handle wildcard patterns
187+
if strings.Contains(pattern, "*") {
188+
matched, _ := filepath.Match(pattern, relPath)
189+
return matched
190+
}
191+
192+
// Handle exact matches
193+
if relPath == pattern {
194+
return true
195+
}
196+
197+
// Handle prefix matches
198+
if strings.HasPrefix(relPath, pattern+"/") {
199+
return true
200+
}
201+
202+
return false
203+
}
204+
205+
// GetContextSize estimates the size of the build context
206+
func (cts *ContextTarStreamer) GetContextSize(contextPath string) (int64, error) {
207+
ignorePatterns, err := cts.loadDockerignore(contextPath)
208+
if err != nil {
209+
return 0, err
210+
}
211+
212+
var totalSize int64
213+
214+
err = filepath.Walk(contextPath, func(path string, info os.FileInfo, err error) error {
215+
if err != nil {
216+
return err
217+
}
218+
219+
if path == contextPath {
220+
return nil
221+
}
222+
223+
relPath, err := filepath.Rel(contextPath, path)
224+
if err != nil {
225+
return err
226+
}
227+
228+
relPath = filepath.ToSlash(relPath)
229+
230+
if cts.shouldIgnore(relPath, ignorePatterns) {
231+
if info.IsDir() {
232+
return filepath.SkipDir
233+
}
234+
return nil
235+
}
236+
237+
if info.Mode().IsRegular() {
238+
totalSize += info.Size()
239+
}
240+
241+
return nil
242+
})
243+
244+
return totalSize, err
245+
}
246+
247+
// ValidateContext validates that a build context is valid
248+
func (cts *ContextTarStreamer) ValidateContext(contextPath string) error {
249+
// Check if context exists and is a directory
250+
if !isDirectory(contextPath) {
251+
return fmt.Errorf("context path is not a directory: %s", contextPath)
252+
}
253+
254+
// Check if .dockerignore is readable (if it exists)
255+
dockerignorePath := filepath.Join(contextPath, ".dockerignore")
256+
if _, err := os.Stat(dockerignorePath); err == nil {
257+
file, err := os.Open(dockerignorePath)
258+
if err != nil {
259+
return fmt.Errorf("cannot read .dockerignore: %w", err)
260+
}
261+
file.Close()
262+
}
263+
264+
// Check context size
265+
size, err := cts.GetContextSize(contextPath)
266+
if err != nil {
267+
return fmt.Errorf("failed to calculate context size: %w", err)
268+
}
269+
270+
// Warn if context is too large
271+
if cts.WarnThresholdMB > 0 && size > int64(cts.WarnThresholdMB*1024*1024) {
272+
// In a real implementation, this would emit a warning
273+
// For now, we'll just continue
274+
}
275+
276+
return nil
277+
}
278+
279+
// Helper function to check if a path is a directory
280+
func isDirectory(path string) bool {
281+
info, err := os.Stat(path)
282+
if err != nil {
283+
return false
284+
}
285+
return info.IsDir()
286+
}
287+
288+
// Helper function to check if a path is a file
289+
func isFile(path string) bool {
290+
info, err := os.Stat(path)
291+
if err != nil {
292+
return false
293+
}
294+
return info.Mode().IsRegular()
295+
}
296+

0 commit comments

Comments
 (0)