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) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 22:55:02 -07:00
parent 60c0c50acb
commit 2ff9fe2f50
5 changed files with 78 additions and 13 deletions

View File

@@ -17,6 +17,8 @@ message ManifestEntry {
string plaintext_hash = 7; // SHA-256 of plaintext (encrypted entries only) string plaintext_hash = 7; // SHA-256 of plaintext (encrypted entries only)
bool encrypted = 8; bool encrypted = 8;
bool locked = 9; // repo-authoritative; restore always overwrites 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. // KekSlot describes a single KEK source for unwrapping the DEK.

View File

@@ -57,6 +57,8 @@ func EntryToProto(e manifest.Entry) *sgardpb.ManifestEntry {
PlaintextHash: e.PlaintextHash, PlaintextHash: e.PlaintextHash,
Encrypted: e.Encrypted, Encrypted: e.Encrypted,
Locked: e.Locked, Locked: e.Locked,
Only: e.Only,
Never: e.Never,
} }
} }
@@ -72,6 +74,8 @@ func ProtoToEntry(p *sgardpb.ManifestEntry) manifest.Entry {
PlaintextHash: p.GetPlaintextHash(), PlaintextHash: p.GetPlaintextHash(),
Encrypted: p.GetEncrypted(), Encrypted: p.GetEncrypted(),
Locked: p.GetLocked(), Locked: p.GetLocked(),
Only: p.GetOnly(),
Never: p.GetNever(),
} }
} }

View File

@@ -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) { func TestEntryEmptyOptionalFieldsRoundTrip(t *testing.T) {
now := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC) now := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)
e := manifest.Entry{ e := manifest.Entry{

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.10 // protoc-gen-go v1.36.11
// protoc v6.32.1 // protoc v7.34.0
// source: sgard/v1/sgard.proto // source: sgard/v1/sgard.proto
package sgardpb 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) 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"` 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 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -183,6 +185,20 @@ func (x *ManifestEntry) GetLocked() bool {
return false 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. // KekSlot describes a single KEK source for unwrapping the DEK.
type KekSlot struct { type KekSlot struct {
state protoimpl.MessageState `protogen:"open.v1"` 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 = "" + const file_sgard_v1_sgard_proto_rawDesc = "" +
"\n" + "\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" + "\rManifestEntry\x12\x12\n" +
"\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" +
"\x04hash\x18\x02 \x01(\tR\x04hash\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" + "\aupdated\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\aupdated\x12%\n" +
"\x0eplaintext_hash\x18\a \x01(\tR\rplaintextHash\x12\x1c\n" + "\x0eplaintext_hash\x18\a \x01(\tR\rplaintextHash\x12\x1c\n" +
"\tencrypted\x18\b \x01(\bR\tencrypted\x12\x16\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" + "\aKekSlot\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x1f\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x1f\n" +
"\vargon2_time\x18\x02 \x01(\x05R\n" + "\vargon2_time\x18\x02 \x01(\x05R\n" +

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.6.1
// - protoc v6.32.1 // - protoc v7.34.0
// source: sgard/v1/sgard.proto // source: sgard/v1/sgard.proto
package sgardpb package sgardpb
@@ -152,22 +152,22 @@ type GardenSyncServer interface {
type UnimplementedGardenSyncServer struct{} type UnimplementedGardenSyncServer struct{}
func (UnimplementedGardenSyncServer) Authenticate(context.Context, *AuthenticateRequest) (*AuthenticateResponse, error) { 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) { 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 { 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) { 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 { 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) { 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) mustEmbedUnimplementedGardenSyncServer() {}
func (UnimplementedGardenSyncServer) testEmbeddedByValue() {} func (UnimplementedGardenSyncServer) testEmbeddedByValue() {}
@@ -180,7 +180,7 @@ type UnsafeGardenSyncServer interface {
} }
func RegisterGardenSyncServer(s grpc.ServiceRegistrar, srv GardenSyncServer) { 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 // embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization // unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O. // time to prevent it from happening at runtime later due to I/O.