From 2ff9fe2f5064403afa9c38791df40f237855cbc0 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 24 Mar 2026 22:55:02 -0700 Subject: [PATCH] Step 31: Proto + sync update for targeting. Added only/never repeated string fields to ManifestEntry proto. Updated convert.go for round-trip. Targeting test in convert_test.go. Co-Authored-By: Claude Opus 4.6 (1M context) --- proto/sgard/v1/sgard.proto | 2 ++ server/convert.go | 4 ++++ server/convert_test.go | 40 ++++++++++++++++++++++++++++++++++++++ sgardpb/sgard.pb.go | 27 +++++++++++++++++++++---- sgardpb/sgard_grpc.pb.go | 18 ++++++++--------- 5 files changed, 78 insertions(+), 13 deletions(-) diff --git a/proto/sgard/v1/sgard.proto b/proto/sgard/v1/sgard.proto index c8a9093..3f6e277 100644 --- a/proto/sgard/v1/sgard.proto +++ b/proto/sgard/v1/sgard.proto @@ -17,6 +17,8 @@ message ManifestEntry { string plaintext_hash = 7; // SHA-256 of plaintext (encrypted entries only) bool encrypted = 8; bool locked = 9; // repo-authoritative; restore always overwrites + repeated string only = 10; // per-machine targeting: only apply on matching + repeated string never = 11; // per-machine targeting: never apply on matching } // KekSlot describes a single KEK source for unwrapping the DEK. diff --git a/server/convert.go b/server/convert.go index 187ae17..eecbe5c 100644 --- a/server/convert.go +++ b/server/convert.go @@ -57,6 +57,8 @@ func EntryToProto(e manifest.Entry) *sgardpb.ManifestEntry { PlaintextHash: e.PlaintextHash, Encrypted: e.Encrypted, Locked: e.Locked, + Only: e.Only, + Never: e.Never, } } @@ -72,6 +74,8 @@ func ProtoToEntry(p *sgardpb.ManifestEntry) manifest.Entry { PlaintextHash: p.GetPlaintextHash(), Encrypted: p.GetEncrypted(), Locked: p.GetLocked(), + Only: p.GetOnly(), + Never: p.GetNever(), } } diff --git a/server/convert_test.go b/server/convert_test.go index c846c4f..e584de2 100644 --- a/server/convert_test.go +++ b/server/convert_test.go @@ -91,6 +91,46 @@ func TestEmptyManifestRoundTrip(t *testing.T) { } } +func TestTargetingRoundTrip(t *testing.T) { + now := time.Date(2026, 3, 24, 0, 0, 0, 0, time.UTC) + + onlyEntry := manifest.Entry{ + Path: "~/.bashrc.linux", + Type: "file", + Hash: "abcd", + Only: []string{"os:linux", "tag:work"}, + Updated: now, + } + + proto := EntryToProto(onlyEntry) + back := ProtoToEntry(proto) + + if len(back.Only) != 2 || back.Only[0] != "os:linux" || back.Only[1] != "tag:work" { + t.Errorf("Only round-trip: got %v, want [os:linux tag:work]", back.Only) + } + if len(back.Never) != 0 { + t.Errorf("Never should be empty, got %v", back.Never) + } + + neverEntry := manifest.Entry{ + Path: "~/.config/heavy", + Type: "file", + Hash: "efgh", + Never: []string{"arch:arm64"}, + Updated: now, + } + + proto2 := EntryToProto(neverEntry) + back2 := ProtoToEntry(proto2) + + if len(back2.Never) != 1 || back2.Never[0] != "arch:arm64" { + t.Errorf("Never round-trip: got %v, want [arch:arm64]", back2.Never) + } + if len(back2.Only) != 0 { + t.Errorf("Only should be empty, got %v", back2.Only) + } +} + func TestEntryEmptyOptionalFieldsRoundTrip(t *testing.T) { now := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC) e := manifest.Entry{ diff --git a/sgardpb/sgard.pb.go b/sgardpb/sgard.pb.go index 5f42af6..eadc0b8 100644 --- a/sgardpb/sgard.pb.go +++ b/sgardpb/sgard.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v6.32.1 +// protoc-gen-go v1.36.11 +// protoc v7.34.0 // source: sgard/v1/sgard.proto package sgardpb @@ -86,6 +86,8 @@ type ManifestEntry struct { PlaintextHash string `protobuf:"bytes,7,opt,name=plaintext_hash,json=plaintextHash,proto3" json:"plaintext_hash,omitempty"` // SHA-256 of plaintext (encrypted entries only) Encrypted bool `protobuf:"varint,8,opt,name=encrypted,proto3" json:"encrypted,omitempty"` Locked bool `protobuf:"varint,9,opt,name=locked,proto3" json:"locked,omitempty"` // repo-authoritative; restore always overwrites + Only []string `protobuf:"bytes,10,rep,name=only,proto3" json:"only,omitempty"` // per-machine targeting: only apply on matching + Never []string `protobuf:"bytes,11,rep,name=never,proto3" json:"never,omitempty"` // per-machine targeting: never apply on matching unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -183,6 +185,20 @@ func (x *ManifestEntry) GetLocked() bool { return false } +func (x *ManifestEntry) GetOnly() []string { + if x != nil { + return x.Only + } + return nil +} + +func (x *ManifestEntry) GetNever() []string { + if x != nil { + return x.Never + } + return nil +} + // KekSlot describes a single KEK source for unwrapping the DEK. type KekSlot struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1079,7 +1095,7 @@ var File_sgard_v1_sgard_proto protoreflect.FileDescriptor const file_sgard_v1_sgard_proto_rawDesc = "" + "\n" + - "\x14sgard/v1/sgard.proto\x12\bsgard.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x8a\x02\n" + + "\x14sgard/v1/sgard.proto\x12\bsgard.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xb4\x02\n" + "\rManifestEntry\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + "\x04hash\x18\x02 \x01(\tR\x04hash\x12\x12\n" + @@ -1089,7 +1105,10 @@ const file_sgard_v1_sgard_proto_rawDesc = "" + "\aupdated\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\aupdated\x12%\n" + "\x0eplaintext_hash\x18\a \x01(\tR\rplaintextHash\x12\x1c\n" + "\tencrypted\x18\b \x01(\bR\tencrypted\x12\x16\n" + - "\x06locked\x18\t \x01(\bR\x06locked\"\xe4\x01\n" + + "\x06locked\x18\t \x01(\bR\x06locked\x12\x12\n" + + "\x04only\x18\n" + + " \x03(\tR\x04only\x12\x14\n" + + "\x05never\x18\v \x03(\tR\x05never\"\xe4\x01\n" + "\aKekSlot\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x1f\n" + "\vargon2_time\x18\x02 \x01(\x05R\n" + diff --git a/sgardpb/sgard_grpc.pb.go b/sgardpb/sgard_grpc.pb.go index 3dd5976..65f8904 100644 --- a/sgardpb/sgard_grpc.pb.go +++ b/sgardpb/sgard_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 -// - protoc v6.32.1 +// - protoc-gen-go-grpc v1.6.1 +// - protoc v7.34.0 // source: sgard/v1/sgard.proto package sgardpb @@ -152,22 +152,22 @@ type GardenSyncServer interface { type UnimplementedGardenSyncServer struct{} func (UnimplementedGardenSyncServer) Authenticate(context.Context, *AuthenticateRequest) (*AuthenticateResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented") + return nil, status.Error(codes.Unimplemented, "method Authenticate not implemented") } func (UnimplementedGardenSyncServer) PushManifest(context.Context, *PushManifestRequest) (*PushManifestResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method PushManifest not implemented") + return nil, status.Error(codes.Unimplemented, "method PushManifest not implemented") } func (UnimplementedGardenSyncServer) PushBlobs(grpc.ClientStreamingServer[PushBlobsRequest, PushBlobsResponse]) error { - return status.Errorf(codes.Unimplemented, "method PushBlobs not implemented") + return status.Error(codes.Unimplemented, "method PushBlobs not implemented") } func (UnimplementedGardenSyncServer) PullManifest(context.Context, *PullManifestRequest) (*PullManifestResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method PullManifest not implemented") + return nil, status.Error(codes.Unimplemented, "method PullManifest not implemented") } func (UnimplementedGardenSyncServer) PullBlobs(*PullBlobsRequest, grpc.ServerStreamingServer[PullBlobsResponse]) error { - return status.Errorf(codes.Unimplemented, "method PullBlobs not implemented") + return status.Error(codes.Unimplemented, "method PullBlobs not implemented") } func (UnimplementedGardenSyncServer) Prune(context.Context, *PruneRequest) (*PruneResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Prune not implemented") + return nil, status.Error(codes.Unimplemented, "method Prune not implemented") } func (UnimplementedGardenSyncServer) mustEmbedUnimplementedGardenSyncServer() {} func (UnimplementedGardenSyncServer) testEmbeddedByValue() {} @@ -180,7 +180,7 @@ type UnsafeGardenSyncServer interface { } func RegisterGardenSyncServer(s grpc.ServiceRegistrar, srv GardenSyncServer) { - // If the following call pancis, it indicates UnimplementedGardenSyncServer was + // If the following call panics, it indicates UnimplementedGardenSyncServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O.