llbsolver: unmarshal protobuf objects into the provenance attestation correctly

This modifies how build steps are unmarshaled from JSON into the
provenance attestation. The current method doesn't correctly handle
protobuf attributes that are used with `oneof`.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
This commit is contained in:
Jonathan A. Sternberg
2023-10-26 11:59:58 -05:00
parent a9a5aaf23f
commit 40fb5ce649
5 changed files with 318 additions and 1 deletions

View File

@@ -13,7 +13,7 @@ type BuildConfig struct {
type BuildStep struct {
ID string `json:"id,omitempty"`
Op interface{} `json:"op,omitempty"`
Op pb.Op `json:"op,omitempty"`
Inputs []string `json:"inputs,omitempty"`
ResourceUsage *resourcestypes.Samples `json:"resourceUsage,omitempty"`
}

96
solver/pb/json.go Normal file
View File

@@ -0,0 +1,96 @@
package pb
import "encoding/json"
func (m *Op) UnmarshalJSON(data []byte) error {
var v struct {
Inputs []*Input `json:"inputs,omitempty"`
Op struct {
*Op_Exec
*Op_Source
*Op_File
*Op_Build
*Op_Merge
*Op_Diff
}
Platform *Platform `json:"platform,omitempty"`
Constraints *WorkerConstraints `json:"constraints,omitempty"`
}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
m.Inputs = v.Inputs
switch {
case v.Op.Op_Exec != nil:
m.Op = v.Op.Op_Exec
case v.Op.Op_Source != nil:
m.Op = v.Op.Op_Source
case v.Op.Op_File != nil:
m.Op = v.Op.Op_File
case v.Op.Op_Build != nil:
m.Op = v.Op.Op_Build
case v.Op.Op_Merge != nil:
m.Op = v.Op.Op_Merge
case v.Op.Op_Diff != nil:
m.Op = v.Op.Op_Diff
}
m.Platform = v.Platform
m.Constraints = v.Constraints
return nil
}
func (m *FileAction) UnmarshalJSON(data []byte) error {
var v struct {
Input InputIndex `json:"input"`
SecondaryInput InputIndex `json:"secondaryInput"`
Output OutputIndex `json:"output"`
Action struct {
*FileAction_Copy
*FileAction_Mkfile
*FileAction_Mkdir
*FileAction_Rm
}
}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
m.Input = v.Input
m.SecondaryInput = v.SecondaryInput
m.Output = v.Output
switch {
case v.Action.FileAction_Copy != nil:
m.Action = v.Action.FileAction_Copy
case v.Action.FileAction_Mkfile != nil:
m.Action = v.Action.FileAction_Mkfile
case v.Action.FileAction_Mkdir != nil:
m.Action = v.Action.FileAction_Mkdir
case v.Action.FileAction_Rm != nil:
m.Action = v.Action.FileAction_Rm
}
return nil
}
func (m *UserOpt) UnmarshalJSON(data []byte) error {
var v struct {
User struct {
*UserOpt_ByName
*UserOpt_ByID
}
}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
switch {
case v.User.UserOpt_ByName != nil:
m.User = v.User.UserOpt_ByName
case v.User.UserOpt_ByID != nil:
m.User = v.User.UserOpt_ByID
}
return nil
}

214
solver/pb/json_test.go Normal file
View File

@@ -0,0 +1,214 @@
package pb
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
func TestOp_UnmarshalJSON(t *testing.T) {
for _, tt := range []struct {
name string
op *Op
}{
{
name: "exec",
op: &Op{
Op: &Op_Exec{
Exec: &ExecOp{
Meta: &Meta{
Args: []string{"echo", "Hello", "World"},
},
Mounts: []*Mount{
{Input: 0, Dest: "/", Readonly: true},
},
},
},
},
},
{
name: "source",
op: &Op{
Op: &Op_Source{
Source: &SourceOp{
Identifier: "local://context",
},
},
Constraints: &WorkerConstraints{},
},
},
{
name: "file",
op: &Op{
Op: &Op_File{
File: &FileOp{
Actions: []*FileAction{
{
Input: 1,
Output: 2,
Action: &FileAction_Copy{
Copy: &FileActionCopy{
Src: "/foo",
Dest: "/bar",
},
},
},
},
},
},
},
},
{
name: "build",
op: &Op{
Op: &Op_Build{
Build: &BuildOp{
Def: &Definition{},
},
},
},
},
{
name: "merge",
op: &Op{
Op: &Op_Merge{
Merge: &MergeOp{
Inputs: []*MergeInput{
{Input: 0},
{Input: 1},
},
},
},
},
},
{
name: "diff",
op: &Op{
Op: &Op_Diff{
Diff: &DiffOp{
Lower: &LowerDiffInput{Input: 0},
Upper: &UpperDiffInput{Input: 1},
},
},
},
},
} {
t.Run(tt.name, func(t *testing.T) {
out, err := json.Marshal(tt.op)
if err != nil {
t.Fatal(err)
}
exp, got := tt.op, &Op{}
if err := json.Unmarshal(out, got); err != nil {
t.Fatal(err)
}
require.Equal(t, exp, got)
})
}
}
func TestFileAction_UnmarshalJSON(t *testing.T) {
for _, tt := range []struct {
name string
fileAction *FileAction
}{
{
name: "copy",
fileAction: &FileAction{
Action: &FileAction_Copy{
Copy: &FileActionCopy{
Src: "/foo",
Dest: "/bar",
},
},
},
},
{
name: "mkfile",
fileAction: &FileAction{
Action: &FileAction_Mkfile{
Mkfile: &FileActionMkFile{
Path: "/foo",
Data: []byte("Hello, World!"),
},
},
},
},
{
name: "mkdir",
fileAction: &FileAction{
Action: &FileAction_Mkdir{
Mkdir: &FileActionMkDir{
Path: "/foo/bar",
MakeParents: true,
},
},
},
},
{
name: "rm",
fileAction: &FileAction{
Action: &FileAction_Rm{
Rm: &FileActionRm{
Path: "/foo",
AllowNotFound: true,
},
},
},
},
} {
t.Run(tt.name, func(t *testing.T) {
out, err := json.Marshal(tt.fileAction)
if err != nil {
t.Fatal(err)
}
exp, got := tt.fileAction, &FileAction{}
if err := json.Unmarshal(out, got); err != nil {
t.Fatal(err)
}
require.Equal(t, exp, got)
})
}
}
func TestUserOpt_UnmarshalJSON(t *testing.T) {
for _, tt := range []struct {
name string
userOpt *UserOpt
}{
{
name: "byName",
userOpt: &UserOpt{
User: &UserOpt_ByName{
ByName: &NamedUserOpt{
Name: "foo",
},
},
},
},
{
name: "byId",
userOpt: &UserOpt{
User: &UserOpt_ByID{
ByID: 2,
},
},
},
} {
t.Run(tt.name, func(t *testing.T) {
out, err := json.Marshal(tt.userOpt)
if err != nil {
t.Fatal(err)
}
exp, got := tt.userOpt, &UserOpt{}
if err := json.Unmarshal(out, got); err != nil {
t.Fatal(err)
}
require.Equal(t, exp, got)
})
}
}

View File

@@ -151,6 +151,7 @@ func (CacheSharingOpt) EnumDescriptor() ([]byte, []int) {
// Op represents a vertex of the LLB DAG.
type Op struct {
// changes to this structure must be represented in json.go.
// inputs is a set of input edges.
Inputs []*Input `protobuf:"bytes,1,rep,name=inputs,proto3" json:"inputs,omitempty"`
// Types that are valid to be assigned to Op:
@@ -1961,6 +1962,7 @@ func (m *FileOp) GetActions() []*FileAction {
}
type FileAction struct {
// changes to this structure must be represented in json.go.
Input InputIndex `protobuf:"varint,1,opt,name=input,proto3,customtype=InputIndex" json:"input"`
SecondaryInput InputIndex `protobuf:"varint,2,opt,name=secondaryInput,proto3,customtype=InputIndex" json:"secondaryInput"`
Output OutputIndex `protobuf:"varint,3,opt,name=output,proto3,customtype=OutputIndex" json:"output"`
@@ -2482,6 +2484,8 @@ func (m *ChownOpt) GetGroup() *UserOpt {
}
type UserOpt struct {
// changes to this structure must be represented in json.go.
//
// Types that are valid to be assigned to User:
//
// *UserOpt_ByName

View File

@@ -10,6 +10,7 @@ option (gogoproto.stable_marshaler_all) = true;
// Op represents a vertex of the LLB DAG.
message Op {
// changes to this structure must be represented in json.go.
// inputs is a set of input edges.
repeated Input inputs = 1;
oneof op {
@@ -288,6 +289,7 @@ message FileOp {
}
message FileAction {
// changes to this structure must be represented in json.go.
int64 input = 1 [(gogoproto.customtype) = "InputIndex", (gogoproto.nullable) = false]; // could be real input or target (target index + max input index)
int64 secondaryInput = 2 [(gogoproto.customtype) = "InputIndex", (gogoproto.nullable) = false]; // --//--
int64 output = 3 [(gogoproto.customtype) = "OutputIndex", (gogoproto.nullable) = false];
@@ -373,6 +375,7 @@ message ChownOpt {
}
message UserOpt {
// changes to this structure must be represented in json.go.
oneof user {
NamedUserOpt byName = 1;
uint32 byID = 2;