policy: implement policy support for bake
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package buildflags
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/buildx/policy"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/tonistiigi/go-csvvalue"
|
||||
)
|
||||
|
||||
type PolicyConfig struct {
|
||||
Files []policy.File
|
||||
Reset bool
|
||||
Disabled bool
|
||||
Strict *bool
|
||||
LogLevel *logrus.Level
|
||||
}
|
||||
|
||||
func ParsePolicyConfigs(in []string) ([]PolicyConfig, error) {
|
||||
if len(in) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
out := make([]PolicyConfig, 0, len(in))
|
||||
for _, s := range in {
|
||||
cfg, err := ParsePolicyConfig(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, cfg)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ParsePolicyConfig(value string) (PolicyConfig, error) {
|
||||
fields, err := csvvalue.Fields(value, nil)
|
||||
if err != nil {
|
||||
return PolicyConfig{}, err
|
||||
}
|
||||
return parsePolicyFields(fields)
|
||||
}
|
||||
|
||||
func parsePolicyFields(fields []string) (PolicyConfig, error) {
|
||||
cfg := PolicyConfig{}
|
||||
for _, field := range fields {
|
||||
key, value, ok := strings.Cut(field, "=")
|
||||
if !ok {
|
||||
return PolicyConfig{}, errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
key = strings.TrimSpace(strings.ToLower(key))
|
||||
switch key {
|
||||
case "filename":
|
||||
if value == "" {
|
||||
return PolicyConfig{}, errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
dt, err := os.ReadFile(value)
|
||||
if err != nil {
|
||||
return PolicyConfig{}, errors.Wrapf(err, "failed to read policy file %s", value)
|
||||
}
|
||||
cfg.Files = append(cfg.Files, policy.File{Filename: value, Data: dt})
|
||||
case "reset":
|
||||
b, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return PolicyConfig{}, errors.Wrapf(err, "invalid value %s", field)
|
||||
}
|
||||
cfg.Reset = b
|
||||
case "disabled":
|
||||
b, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return PolicyConfig{}, errors.Wrapf(err, "invalid value %s", field)
|
||||
}
|
||||
cfg.Disabled = b
|
||||
case "strict":
|
||||
b, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return PolicyConfig{}, errors.Wrapf(err, "invalid value %s", field)
|
||||
}
|
||||
cfg.Strict = &b
|
||||
case "log-level":
|
||||
lvl, err := logrus.ParseLevel(value)
|
||||
if err != nil {
|
||||
return PolicyConfig{}, errors.Wrapf(err, "invalid value %s", field)
|
||||
}
|
||||
cfg.LogLevel = &lvl
|
||||
default:
|
||||
return PolicyConfig{}, errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package buildflags
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
)
|
||||
|
||||
type PolicyConfigs []PolicyConfig
|
||||
|
||||
var policyConfigType = sync.OnceValue(func() cty.Type {
|
||||
return cty.Map(cty.String)
|
||||
})
|
||||
|
||||
func (p *PolicyConfigs) FromCtyValue(in cty.Value, path cty.Path) error {
|
||||
got := in.Type()
|
||||
if got.IsTupleType() || got.IsListType() {
|
||||
return p.fromCtyValue(in, path)
|
||||
}
|
||||
|
||||
want := cty.List(policyConfigType())
|
||||
return path.NewErrorf("%s", convert.MismatchMessage(got, want))
|
||||
}
|
||||
|
||||
func (p *PolicyConfigs) fromCtyValue(in cty.Value, path cty.Path) (retErr error) {
|
||||
*p = make([]PolicyConfig, 0, in.LengthInt())
|
||||
|
||||
yield := func(value cty.Value) bool {
|
||||
if value.Type() == cty.String {
|
||||
var cfg PolicyConfig
|
||||
cfg, retErr = ParsePolicyConfig(value.AsString())
|
||||
if retErr != nil {
|
||||
return false
|
||||
}
|
||||
*p = append(*p, cfg)
|
||||
return true
|
||||
}
|
||||
|
||||
if value.Type().IsObjectType() || value.Type().IsMapType() {
|
||||
var cfg PolicyConfig
|
||||
cfg, retErr = policyConfigFromMap(value)
|
||||
if retErr != nil {
|
||||
return false
|
||||
}
|
||||
*p = append(*p, cfg)
|
||||
return true
|
||||
}
|
||||
|
||||
retErr = path.NewErrorf("%s", convert.MismatchMessage(value.Type(), policyConfigType()))
|
||||
return false
|
||||
}
|
||||
eachElement(in)(yield)
|
||||
return retErr
|
||||
}
|
||||
|
||||
func (p PolicyConfigs) ToCtyValue() cty.Value {
|
||||
if len(p) == 0 {
|
||||
return cty.ListValEmpty(policyConfigType())
|
||||
}
|
||||
|
||||
vals := make([]cty.Value, len(p))
|
||||
for i, entry := range p {
|
||||
vals[i] = entry.ToCtyValue()
|
||||
}
|
||||
return cty.ListVal(vals)
|
||||
}
|
||||
|
||||
func (p *PolicyConfig) FromCtyValue(in cty.Value, path cty.Path) error {
|
||||
if in.Type() == cty.String {
|
||||
cfg, err := ParsePolicyConfig(in.AsString())
|
||||
if err != nil {
|
||||
return path.NewError(err)
|
||||
}
|
||||
*p = cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
if in.Type().IsObjectType() || in.Type().IsMapType() {
|
||||
cfg, err := policyConfigFromMap(in)
|
||||
if err != nil {
|
||||
return path.NewError(err)
|
||||
}
|
||||
*p = cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
return path.NewErrorf("%s", convert.MismatchMessage(in.Type(), policyConfigType()))
|
||||
}
|
||||
|
||||
func (p PolicyConfig) ToCtyValue() cty.Value {
|
||||
vals := map[string]cty.Value{}
|
||||
if len(p.Files) > 0 {
|
||||
vals["filename"] = cty.StringVal(p.Files[0].Filename)
|
||||
}
|
||||
if p.Reset {
|
||||
vals["reset"] = cty.StringVal(strconv.FormatBool(p.Reset))
|
||||
}
|
||||
if p.Disabled {
|
||||
vals["disabled"] = cty.StringVal(strconv.FormatBool(p.Disabled))
|
||||
}
|
||||
if p.Strict != nil {
|
||||
vals["strict"] = cty.StringVal(strconv.FormatBool(*p.Strict))
|
||||
}
|
||||
if p.LogLevel != nil {
|
||||
vals["log-level"] = cty.StringVal(p.LogLevel.String())
|
||||
}
|
||||
if len(vals) == 0 {
|
||||
return cty.MapValEmpty(cty.String)
|
||||
}
|
||||
return cty.MapVal(vals)
|
||||
}
|
||||
|
||||
func policyConfigFromMap(in cty.Value) (PolicyConfig, error) {
|
||||
fields := make([]string, 0)
|
||||
for k, v := range in.AsValueMap() {
|
||||
if v.IsNull() || !v.IsKnown() {
|
||||
continue
|
||||
}
|
||||
if v.Type() == cty.String && v.AsString() == "" {
|
||||
continue
|
||||
}
|
||||
field, err := policyField(k, v)
|
||||
if err != nil {
|
||||
return PolicyConfig{}, err
|
||||
}
|
||||
fields = append(fields, field)
|
||||
}
|
||||
return parsePolicyFields(fields)
|
||||
}
|
||||
|
||||
func policyField(key string, value cty.Value) (string, error) {
|
||||
switch value.Type() {
|
||||
case cty.String:
|
||||
return fmt.Sprintf("%s=%s", key, value.AsString()), nil
|
||||
case cty.Bool:
|
||||
return fmt.Sprintf("%s=%t", key, value.True()), nil
|
||||
case cty.Number:
|
||||
var f big.Float
|
||||
f.Set(value.AsBigFloat())
|
||||
return fmt.Sprintf("%s=%s", key, f.Text('f', -1)), nil
|
||||
default:
|
||||
return "", errors.Errorf("%s", convert.MismatchMessage(value.Type(), cty.String))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package buildflags
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/buildx/policy"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestPolicyConfigs_FromCtyValue(t *testing.T) {
|
||||
policyDir := t.TempDir()
|
||||
policyPath := filepath.Join(policyDir, "policy.rego")
|
||||
policyData := []byte("package docker\n")
|
||||
require.NoError(t, os.WriteFile(policyPath, policyData, 0o600))
|
||||
|
||||
in := cty.TupleVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"filename": cty.StringVal(policyPath),
|
||||
"reset": cty.BoolVal(true),
|
||||
"strict": cty.BoolVal(true),
|
||||
"log-level": cty.StringVal("warn"),
|
||||
}),
|
||||
cty.StringVal("filename=" + policyPath + ",disabled=true"),
|
||||
})
|
||||
|
||||
var actual PolicyConfigs
|
||||
err := actual.FromCtyValue(in, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, actual, 2)
|
||||
|
||||
require.Equal(t, policyPath, actual[0].Files[0].Filename)
|
||||
require.Equal(t, policyData, actual[0].Files[0].Data)
|
||||
require.True(t, actual[0].Reset)
|
||||
require.NotNil(t, actual[0].Strict)
|
||||
require.True(t, *actual[0].Strict)
|
||||
require.NotNil(t, actual[0].LogLevel)
|
||||
require.Equal(t, logrus.WarnLevel, *actual[0].LogLevel)
|
||||
|
||||
require.Equal(t, policyPath, actual[1].Files[0].Filename)
|
||||
require.Equal(t, policyData, actual[1].Files[0].Data)
|
||||
require.True(t, actual[1].Disabled)
|
||||
}
|
||||
|
||||
func TestPolicyConfigs_ToCtyValue(t *testing.T) {
|
||||
lvl := logrus.InfoLevel
|
||||
strict := true
|
||||
in := PolicyConfigs{
|
||||
{
|
||||
Files: []policy.File{{Filename: "a.rego"}},
|
||||
Reset: true,
|
||||
},
|
||||
{
|
||||
Files: []policy.File{{Filename: "b.rego"}},
|
||||
Disabled: true,
|
||||
Strict: &strict,
|
||||
LogLevel: &lvl,
|
||||
},
|
||||
}
|
||||
|
||||
actual := in.ToCtyValue()
|
||||
expected := cty.ListVal([]cty.Value{
|
||||
cty.MapVal(map[string]cty.Value{
|
||||
"filename": cty.StringVal("a.rego"),
|
||||
"reset": cty.StringVal("true"),
|
||||
}),
|
||||
cty.MapVal(map[string]cty.Value{
|
||||
"filename": cty.StringVal("b.rego"),
|
||||
"disabled": cty.StringVal("true"),
|
||||
"strict": cty.StringVal("true"),
|
||||
"log-level": cty.StringVal("info"),
|
||||
}),
|
||||
})
|
||||
|
||||
result := actual.Equals(expected)
|
||||
require.True(t, result.True())
|
||||
}
|
||||
|
||||
func TestPolicyConfig_FromCtyValue(t *testing.T) {
|
||||
policyDir := t.TempDir()
|
||||
policyPath := filepath.Join(policyDir, "policy.rego")
|
||||
policyData := []byte("package docker\n")
|
||||
require.NoError(t, os.WriteFile(policyPath, policyData, 0o600))
|
||||
|
||||
var actual PolicyConfig
|
||||
err := actual.FromCtyValue(cty.StringVal("filename="+policyPath+",disabled=true"), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, policyPath, actual.Files[0].Filename)
|
||||
require.Equal(t, policyData, actual.Files[0].Data)
|
||||
require.True(t, actual.Disabled)
|
||||
}
|
||||
@@ -33,6 +33,13 @@ func GitTag(c *gitutil.Git, tb testing.TB, tag string) {
|
||||
require.Empty(tb, out)
|
||||
}
|
||||
|
||||
func GitTagAnnotated(c *gitutil.Git, tb testing.TB, tag, message string) {
|
||||
tb.Helper()
|
||||
out, err := fakeGit(c, "tag", "-a", tag, "-m", message)
|
||||
require.NoError(tb, err)
|
||||
require.Empty(tb, out)
|
||||
}
|
||||
|
||||
func GitCheckoutBranch(c *gitutil.Git, tb testing.TB, name string) {
|
||||
tb.Helper()
|
||||
out, err := fakeGit(c, "checkout", "-b", name)
|
||||
|
||||
Reference in New Issue
Block a user