diff --git a/solver/llbsolver/provenance/buildconfig.go b/solver/llbsolver/provenance/buildconfig.go index 8f903585be5c..362273029832 100644 --- a/solver/llbsolver/provenance/buildconfig.go +++ b/solver/llbsolver/provenance/buildconfig.go @@ -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"` } diff --git a/solver/pb/json.go b/solver/pb/json.go new file mode 100644 index 000000000000..8460ffc10f31 --- /dev/null +++ b/solver/pb/json.go @@ -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 +} diff --git a/solver/pb/json_test.go b/solver/pb/json_test.go new file mode 100644 index 000000000000..79976641a305 --- /dev/null +++ b/solver/pb/json_test.go @@ -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) + }) + } +} diff --git a/solver/pb/ops.pb.go b/solver/pb/ops.pb.go index aadff21b6474..d40a4a0c715b 100644 --- a/solver/pb/ops.pb.go +++ b/solver/pb/ops.pb.go @@ -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 diff --git a/solver/pb/ops.proto b/solver/pb/ops.proto index 2f78628f42f9..4e934d85148f 100644 --- a/solver/pb/ops.proto +++ b/solver/pb/ops.proto @@ -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;