diff --git a/build/build.go b/build/build.go index be9e3b64f..f272cb219 100644 --- a/build/build.go +++ b/build/build.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "io" + "io/fs" "maps" "os" "slices" @@ -115,7 +116,12 @@ type Inputs struct { DockerfileMappingSrc string DockerfileMappingDst string - policy []policy.File + policy *policyOpt +} + +type policyOpt struct { + Files []policy.File + FS func() (fs.StatFS, func() error, error) } type NamedContext struct { @@ -929,7 +935,7 @@ func detectSharedMounts(ctx context.Context, reqs map[string][]*reqForNode) (_ m } fsMap := m[nodeName] for name, m := range req.so.LocalMounts { - fs, ok := m.(*fs) + fs, ok := m.(*fsMount) if !ok { continue } diff --git a/build/git.go b/build/git.go index de30c5108..ac79b0689 100644 --- a/build/git.go +++ b/build/git.go @@ -135,7 +135,7 @@ func getGitAttributes(ctx context.Context, contextPath, dockerfilePath string) ( } for key, mount := range so.LocalMounts { - fs, ok := mount.(*fs) + fs, ok := mount.(*fsMount) if !ok { continue } diff --git a/build/opt.go b/build/opt.go index 076b5f907..33652c635 100644 --- a/build/opt.go +++ b/build/opt.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "io" + "io/fs" "log" "maps" "os" @@ -326,7 +327,7 @@ func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *O } defers = append(defers, releaseLoad) - if len(opt.Inputs.policy) > 0 { + if opt.Inputs.policy != nil { env := policy.Env{} for k, v := range opt.BuildArgs { if env.Args == nil { @@ -338,11 +339,12 @@ func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *O env.Target = opt.Target env.Labels = opt.Labels p := policy.NewPolicy(policy.Opt{ - Files: opt.Inputs.policy, + Files: opt.Inputs.policy.Files, Env: env, Log: func(msg string) { log.Printf("[policy] %s", msg) }, + FS: opt.Inputs.policy.FS, }) so.SourcePolicyProvider = policysession.NewPolicyProvider(p.CheckPolicy) } @@ -438,6 +440,7 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro var ( err error dockerfileReader io.ReadCloser + contextDir string dockerfileDir string dockerfileName = inp.DockerfilePath dockerfileSrcName = inp.DockerfilePath @@ -479,12 +482,14 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro if err := setLocalMount("context", inp.ContextPath, target); err != nil { return nil, err } + contextDir = inp.ContextPath } } case osutil.IsLocalDir(inp.ContextPath): if err := setLocalMount("context", inp.ContextPath, target); err != nil { return nil, err } + contextDir = inp.ContextPath sharedKey := inp.ContextPath if p, err := filepath.Abs(sharedKey); err == nil { sharedKey = filepath.Base(p) @@ -567,10 +572,30 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro if err != nil { return nil, errors.Wrapf(err, "failed to read policy file %s.rego", dockerfileName) } - inp.policy = append(inp.policy, policy.File{ - Filename: dockerfileName + ".rego", - Data: dt, - }) + inp.policy = &policyOpt{ + Files: []policy.File{ + { + Filename: dockerfileName + ".rego", + Data: dt, + }, + }, + FS: func() (fs.StatFS, func() error, error) { + if contextDir == "" { + return nil, nil, errors.Errorf("unimplemented, cannot use policy file without a local build context") + } + root, err := os.OpenRoot(contextDir) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to open root for policy file %s.rego", dockerfileName) + } + baseFS := root.FS() + statFS, ok := baseFS.(fs.StatFS) + if !ok { + root.Close() + return nil, nil, errors.Errorf("invalid root FS type %T", baseFS) + } + return statFS, root.Close, nil + }, + } } } } @@ -687,7 +712,7 @@ func setLocalMount(name, dir string, so *client.SolveOpt) error { if so.LocalMounts == nil { so.LocalMounts = map[string]fsutil.FS{} } - so.LocalMounts[name] = &fs{FS: lm, dir: dir} + so.LocalMounts[name] = &fsMount{FS: lm, dir: dir} return nil } @@ -799,12 +824,12 @@ func handleLowercaseDockerfile(dir, p string) string { return p } -type fs struct { +type fsMount struct { fsutil.FS dir string } -var _ fsutil.FS = &fs{} +var _ fsutil.FS = &fsMount{} func CreateSSH(ssh []*buildflags.SSH) (session.Attachable, error) { configs := make([]sshprovider.AgentConfig, 0, len(ssh)) diff --git a/policy/validate.go b/policy/validate.go index 91f32096d..cf0fa4d12 100644 --- a/policy/validate.go +++ b/policy/validate.go @@ -3,6 +3,7 @@ package policy import ( "context" "encoding/json" + "io/fs" "log" "net/url" "os" @@ -51,6 +52,7 @@ type Opt struct { Files []File Env Env Log func(string) + FS func() (fs.StatFS, func() error, error) } var _ policysession.PolicyCallback = (&Policy{}).CheckPolicy @@ -320,11 +322,67 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy Features: slices.Clone(ast.Features), } - comp := ast.NewCompiler().WithCapabilities(caps) + comp := ast.NewCompiler().WithCapabilities(caps).WithKeepModules(true) if p.opt.Log != nil { comp = comp.WithEnablePrintStatements(true) } + var root fs.StatFS + var closeFS func() error + + defer func() { + if closeFS != nil { + closeFS() + } + }() + + comp = comp.WithModuleLoader(func(resolved map[string]*ast.Module) (parsed map[string]*ast.Module, err error) { + out := make(map[string]*ast.Module) + for k, v := range resolved { + for _, imp := range v.Imports { + pv := imp.Path.Value.String() + pkgPath, ok := strings.CutPrefix(pv, "data.") + if !ok { + continue + } + fn := strings.ReplaceAll(pkgPath, ".", "/") + ".rego" + if _, ok := resolved[fn]; !ok { + if root == nil { + if p.opt.FS == nil { + return nil, errors.Errorf("no policy FS defined for import %s", pv) + } + f, cf, err := p.opt.FS() + if err != nil { + return nil, errors.Wrapf(err, "failed to get policy FS for import %s", pv) + } + root = f + closeFS = cf + } + if _, err := root.Stat(fn); err != nil { + return nil, errors.Wrapf(err, "import %s not found for module %s", pv, k) + } + dt, err := fs.ReadFile(root, fn) + if err != nil { + return nil, errors.Wrapf(err, "failed to read imported policy file %s for module %s", fn, k) + } + mod, err := ast.ParseModule(fn, string(dt)) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse imported policy file %s for module %s", fn, k) + } + // rewrite package to be less strict + pkgParts := strings.Split(pkgPath, ".") + ref := ast.Ref{mod.Package.Path[0]} + for _, p := range pkgParts { + ref = append(ref, ast.StringTerm(p)) + } + mod.Package = &ast.Package{Path: ref} + out[fn] = mod + } + } + } + return out, nil + }) + opts := []func(*rego.Rego){ rego.SetRegoVersion(ast.RegoV1), rego.Query("data.docker.decision"),