Skip to content

Commit d3c9cf4

Browse files
schuellerfochosi
authored andcommitted
handler_blueprints: add rule checking for blueprint customizations
Add rule checking for blueprint customizations (files, directories, mountpoints, name, and users) to provide direct feedback before creating or updating blueprints. These checks catch issues early that would fail later during image composition. Use "rule" terminology instead of "validation" since we're not validating against a schema, but rather checking against business rules. The error type is named blueprintRuleError to reflect this. Merges rule checking for create and update operations and deduplicates code to prepare for additional rule checks.
1 parent c631b47 commit d3c9cf4

File tree

5 files changed

+704
-45
lines changed

5 files changed

+704
-45
lines changed

internal/v1/blueprint_rules.go

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
package v1
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"regexp"
9+
"strconv"
10+
11+
"github.com/labstack/echo/v4"
12+
"github.com/osbuild/images/pkg/customizations/fsnode"
13+
)
14+
15+
var (
16+
blueprintNameRegex = regexp.MustCompile(`\S+`)
17+
blueprintInvalidNameDetail = "the blueprint name must contain at least two characters"
18+
)
19+
20+
// blueprintRuleError wraps an error with HTTP response details
21+
type blueprintRuleError struct {
22+
title string
23+
detail string
24+
}
25+
26+
func (ve blueprintRuleError) Error() string {
27+
return ve.detail
28+
}
29+
30+
func newBlueprintRuleError(title, detail string) error {
31+
return blueprintRuleError{
32+
title: title,
33+
detail: detail,
34+
}
35+
}
36+
37+
// parseOctalMode parses an octal mode string to os.FileMode
38+
func parseOctalMode(modeStr *string) *os.FileMode {
39+
if modeStr == nil {
40+
return nil
41+
}
42+
43+
if modeVal, err := strconv.ParseUint(*modeStr, 8, 32); err == nil {
44+
m := os.FileMode(modeVal)
45+
return &m
46+
}
47+
return nil
48+
}
49+
50+
// parseFileUser extracts user from File union type
51+
func parseFileUser(fileUser *File_User) any {
52+
if fileUser == nil {
53+
return nil
54+
}
55+
56+
if userStr, err := fileUser.AsFileUser0(); err == nil {
57+
return userStr
58+
}
59+
if userInt, err := fileUser.AsFileUser1(); err == nil {
60+
return userInt
61+
}
62+
return nil
63+
}
64+
65+
// parseFileGroup extracts group from File union type
66+
func parseFileGroup(fileGroup *File_Group) any {
67+
if fileGroup == nil {
68+
return nil
69+
}
70+
71+
if groupStr, err := fileGroup.AsFileGroup0(); err == nil {
72+
return groupStr
73+
}
74+
if groupInt, err := fileGroup.AsFileGroup1(); err == nil {
75+
return groupInt
76+
}
77+
return nil
78+
}
79+
80+
// parseDirectoryUser extracts user from Directory union type
81+
func parseDirectoryUser(dirUser *Directory_User) any {
82+
if dirUser == nil {
83+
return nil
84+
}
85+
86+
if userStr, err := dirUser.AsDirectoryUser0(); err == nil {
87+
return userStr
88+
}
89+
if userInt, err := dirUser.AsDirectoryUser1(); err == nil {
90+
return userInt
91+
}
92+
return nil
93+
}
94+
95+
// parseDirectoryGroup extracts group from Directory union type
96+
func parseDirectoryGroup(dirGroup *Directory_Group) any {
97+
if dirGroup == nil {
98+
return nil
99+
}
100+
101+
if groupStr, err := dirGroup.AsDirectoryGroup0(); err == nil {
102+
return groupStr
103+
}
104+
if groupInt, err := dirGroup.AsDirectoryGroup1(); err == nil {
105+
return groupInt
106+
}
107+
return nil
108+
}
109+
110+
func checkNameRule(request *CreateBlueprintRequest) error {
111+
if !blueprintNameRegex.MatchString(request.Name) {
112+
return newBlueprintRuleError("blueprint name rule violation", blueprintInvalidNameDetail)
113+
}
114+
return nil
115+
}
116+
117+
func checkUserRule(request *CreateBlueprintRequest, existingUsers []User) error {
118+
users := request.Customizations.Users
119+
if users == nil {
120+
return nil
121+
}
122+
123+
for i, user := range *users {
124+
var err error
125+
if existingUsers != nil {
126+
err = (*users)[i].MergeForUpdate(existingUsers)
127+
} else {
128+
err = user.Valid()
129+
}
130+
131+
if err != nil {
132+
return newBlueprintRuleError("user rule violation", err.Error())
133+
}
134+
}
135+
return nil
136+
}
137+
138+
func checkFileRule(request *CreateBlueprintRequest) error {
139+
files := request.Customizations.Files
140+
if files == nil {
141+
return nil
142+
}
143+
144+
var errs []error
145+
for _, file := range *files {
146+
mode := parseOctalMode(file.Mode)
147+
user := parseFileUser(file.User)
148+
group := parseFileGroup(file.Group)
149+
150+
var data []byte
151+
if file.Data != nil {
152+
data = []byte(*file.Data)
153+
}
154+
155+
_, err := fsnode.NewFile(file.Path, mode, user, group, data)
156+
if err != nil {
157+
errs = append(errs, newBlueprintRuleError(
158+
"file rule violation",
159+
fmt.Sprintf("file %q: %s", file.Path, err.Error()),
160+
))
161+
}
162+
}
163+
164+
return errors.Join(errs...)
165+
}
166+
167+
func checkDirectoryRule(request *CreateBlueprintRequest) error {
168+
directories := request.Customizations.Directories
169+
if directories == nil {
170+
return nil
171+
}
172+
173+
var errs []error
174+
for _, dir := range *directories {
175+
mode := parseOctalMode(dir.Mode)
176+
user := parseDirectoryUser(dir.User)
177+
group := parseDirectoryGroup(dir.Group)
178+
179+
ensureParents := false
180+
if dir.EnsureParents != nil {
181+
ensureParents = *dir.EnsureParents
182+
}
183+
184+
_, err := fsnode.NewDirectory(dir.Path, mode, user, group, ensureParents)
185+
if err != nil {
186+
errs = append(errs, newBlueprintRuleError(
187+
"directory rule violation",
188+
fmt.Sprintf("directory %q: %s", dir.Path, err.Error()),
189+
))
190+
}
191+
}
192+
193+
return errors.Join(errs...)
194+
}
195+
196+
func checkFilesystemRule(request *CreateBlueprintRequest) error {
197+
filesystem := request.Customizations.Filesystem
198+
if filesystem == nil {
199+
return nil
200+
}
201+
202+
var errs []error
203+
for _, fs := range *filesystem {
204+
if fs.Mountpoint == "" {
205+
errs = append(errs, newBlueprintRuleError(
206+
"filesystem rule violation",
207+
"mountpoint must not be empty",
208+
))
209+
} else if fs.Mountpoint[0] != '/' {
210+
errs = append(errs, newBlueprintRuleError(
211+
"filesystem rule violation",
212+
fmt.Sprintf("mountpoint %q must be absolute", fs.Mountpoint),
213+
))
214+
} else if fs.Mountpoint != filepath.Clean(fs.Mountpoint) {
215+
errs = append(errs, newBlueprintRuleError(
216+
"filesystem rule violation",
217+
fmt.Sprintf("mountpoint %q must be canonical", fs.Mountpoint),
218+
))
219+
}
220+
221+
if fs.MinSize > 0 && fs.MinSize < 1024*1024 {
222+
errs = append(errs, newBlueprintRuleError(
223+
"filesystem rule violation",
224+
fmt.Sprintf("mountpoint %q minimum size must be at least 1MB", fs.Mountpoint),
225+
))
226+
}
227+
}
228+
229+
return errors.Join(errs...)
230+
}
231+
232+
// CheckBlueprintRules performs common rule checking for blueprint requests
233+
func CheckBlueprintRules(ctx echo.Context, request *CreateBlueprintRequest, existingUsers []User) error {
234+
return errors.Join(
235+
checkNameRule(request),
236+
checkUserRule(request, existingUsers),
237+
checkFileRule(request),
238+
checkDirectoryRule(request),
239+
checkFilesystemRule(request),
240+
)
241+
}

0 commit comments

Comments
 (0)