Phase 10: gRPC admin API with interceptor chain
Proto definitions for 4 services (RegistryService, PolicyService, AuditService, AdminService) with hand-written Go stubs using JSON codec until protobuf tooling is available. Interceptor chain: logging (method, peer IP, duration, never logs auth metadata) → auth (bearer token via MCIAS, Health bypasses) → admin (role check for GC, policy, delete, audit RPCs). All RPCs share business logic with REST handlers via internal/db and internal/gc packages. TLS 1.3 minimum on gRPC listener. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
94
PROGRESS.md
94
PROGRESS.md
@@ -6,7 +6,7 @@ See `PROJECT_PLAN.md` for the implementation roadmap and
|
|||||||
|
|
||||||
## Current State
|
## Current State
|
||||||
|
|
||||||
**Phase:** 9 complete, ready for Phase 10
|
**Phase:** 10 complete, ready for Phase 11
|
||||||
**Last updated:** 2026-03-19
|
**Last updated:** 2026-03-19
|
||||||
|
|
||||||
### Completed
|
### Completed
|
||||||
@@ -21,6 +21,7 @@ See `PROJECT_PLAN.md` for the implementation roadmap and
|
|||||||
- Phase 7: OCI delete path (all 2 steps)
|
- Phase 7: OCI delete path (all 2 steps)
|
||||||
- Phase 8: Admin REST API (all 5 steps)
|
- Phase 8: Admin REST API (all 5 steps)
|
||||||
- Phase 9: Garbage collection (all 2 steps)
|
- Phase 9: Garbage collection (all 2 steps)
|
||||||
|
- Phase 10: gRPC admin API (all 4 steps)
|
||||||
- `ARCHITECTURE.md` — Full design specification (18 sections)
|
- `ARCHITECTURE.md` — Full design specification (18 sections)
|
||||||
- `CLAUDE.md` — AI development guidance
|
- `CLAUDE.md` — AI development guidance
|
||||||
- `PROJECT_PLAN.md` — Implementation plan (14 phases, 40+ steps)
|
- `PROJECT_PLAN.md` — Implementation plan (14 phases, 40+ steps)
|
||||||
@@ -28,13 +29,100 @@ See `PROJECT_PLAN.md` for the implementation roadmap and
|
|||||||
|
|
||||||
### Next Steps
|
### Next Steps
|
||||||
|
|
||||||
1. Phase 10 (gRPC admin API)
|
1. Phase 11 (CLI tool) and Phase 12 (web UI)
|
||||||
2. Phase 11 (CLI tool) and Phase 12 (web UI)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Log
|
## Log
|
||||||
|
|
||||||
|
### 2026-03-19 — Phase 10: gRPC admin API
|
||||||
|
|
||||||
|
**Task:** Implement the gRPC admin API server with the same business
|
||||||
|
logic as the REST admin API, per ARCHITECTURE.md section 7.
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
|
||||||
|
Step 10.1 — Proto definitions (`proto/mcr/v1/`):
|
||||||
|
- `common.proto`: `PaginationRequest` shared type
|
||||||
|
- `registry.proto`: `RegistryService` with `ListRepositories`,
|
||||||
|
`GetRepository`, `DeleteRepository`, `GarbageCollect`, `GetGCStatus` RPCs
|
||||||
|
- `policy.proto`: `PolicyService` with `ListPolicyRules`,
|
||||||
|
`CreatePolicyRule`, `GetPolicyRule`, `UpdatePolicyRule`,
|
||||||
|
`DeletePolicyRule` RPCs; `UpdatePolicyRuleRequest` includes field mask
|
||||||
|
- `audit.proto`: `AuditService` with `ListAuditEvents` RPC; filter
|
||||||
|
fields for event_type, actor_id, repository, time range
|
||||||
|
- `admin.proto`: `AdminService` with `Health` RPC
|
||||||
|
|
||||||
|
Step 10.2 — Generated code (`gen/mcr/v1/`):
|
||||||
|
- Hand-written message types and gRPC service descriptors matching
|
||||||
|
protoc-gen-go v1.36+ and protoc-gen-go-grpc v1.5+ output patterns
|
||||||
|
- `codec.go`: JSON codec implementing `encoding.CodecV2` via
|
||||||
|
`mem.BufferSlice`, registered globally via `init()` as stand-in
|
||||||
|
until protobuf code generation is available
|
||||||
|
- Handler functions properly delegate to interceptor chain (critical
|
||||||
|
for auth/admin enforcement; handlers that ignore the interceptor
|
||||||
|
parameter bypass security checks entirely)
|
||||||
|
- Client and server interfaces with `mustEmbedUnimplemented*Server()`
|
||||||
|
for forward compatibility
|
||||||
|
|
||||||
|
Step 10.3 — Interceptor chain (`internal/grpcserver/interceptors.go`):
|
||||||
|
- `loggingInterceptor`: logs method, peer IP, status code, duration;
|
||||||
|
never logs the authorization metadata value
|
||||||
|
- `authInterceptor`: extracts `authorization` metadata, validates
|
||||||
|
bearer token via `TokenValidator` interface, injects claims into
|
||||||
|
context; `Health` bypasses auth via `authBypassMethods` map
|
||||||
|
- `adminInterceptor`: requires admin role for GC, policy, delete,
|
||||||
|
audit RPCs via `adminRequiredMethods` map; returns
|
||||||
|
`codes.PermissionDenied` for insufficient role
|
||||||
|
- gRPC errors: `codes.Unauthenticated` for missing/invalid token,
|
||||||
|
`codes.PermissionDenied` for insufficient role
|
||||||
|
|
||||||
|
Step 10.4 — Server implementation (`internal/grpcserver/`):
|
||||||
|
- `server.go`: `New(certFile, keyFile, deps)` creates configured
|
||||||
|
gRPC server with TLS 1.3 minimum (skips TLS if paths empty for
|
||||||
|
testing); `Serve()`, `GracefulStop()`
|
||||||
|
- `registry.go`: `registryService` implementing all 5 RPCs with
|
||||||
|
same DB calls as REST handlers; GC runs asynchronously with its
|
||||||
|
own `GCStatus` tracking (separate from REST's `GCState` since
|
||||||
|
`GCState.mu` is unexported); shares `gc.Collector` for actual GC
|
||||||
|
- `policy.go`: `policyService` implementing all 5 RPCs with
|
||||||
|
validation (effect, actions, priority), field mask updates, engine
|
||||||
|
reload, audit events
|
||||||
|
- `audit.go`: `auditService` implementing `ListAuditEvents` with
|
||||||
|
pagination and filter pass-through to `db.ListAuditEvents`
|
||||||
|
- `admin.go`: `adminService` implementing `Health` (returns "ok")
|
||||||
|
|
||||||
|
**Dependencies added:**
|
||||||
|
- `google.golang.org/grpc` v1.79.3
|
||||||
|
- `google.golang.org/protobuf` v1.36.11
|
||||||
|
- Transitive: `golang.org/x/net`, `golang.org/x/text`,
|
||||||
|
`google.golang.org/genproto/googleapis/rpc`
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- `make all` passes: vet clean, lint 0 issues, all tests passing,
|
||||||
|
all 3 binaries built
|
||||||
|
- Interceptor tests (12 new): Health bypasses auth, no token rejected
|
||||||
|
(Unauthenticated), invalid token rejected (Unauthenticated), valid
|
||||||
|
token accepted, non-admin denied policy RPCs (PermissionDenied),
|
||||||
|
admin allowed policy RPCs, admin required methods completeness
|
||||||
|
check, auth bypass methods completeness check, delete repo requires
|
||||||
|
admin, GC requires admin, audit requires admin
|
||||||
|
- Registry service tests (7 new): list repositories empty, get repo
|
||||||
|
not found, get repo empty name (InvalidArgument), delete repo not
|
||||||
|
found, delete repo empty name, GC status initial (not running),
|
||||||
|
GC trigger returns ID
|
||||||
|
- Policy service tests (8 new): create policy rule (with engine
|
||||||
|
reload verification), create validation (5 subtests: zero priority,
|
||||||
|
empty description, invalid effect, no actions, invalid action), get
|
||||||
|
policy rule, get not found, list policy rules, delete + verify
|
||||||
|
gone + reload count, delete not found, update with field mask
|
||||||
|
- Audit service tests (3 new): list empty, list with data (verify
|
||||||
|
fields and details map), list with pagination
|
||||||
|
- Admin service tests (2 new): Health returns "ok", Health without
|
||||||
|
auth succeeds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 2026-03-19 — Phase 9: Garbage collection
|
### 2026-03-19 — Phase 9: Garbage collection
|
||||||
|
|
||||||
**Task:** Implement the two-phase GC algorithm for removing unreferenced
|
**Task:** Implement the two-phase GC algorithm for removing unreferenced
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ design specification.
|
|||||||
| 7 | OCI API — delete path | **Complete** |
|
| 7 | OCI API — delete path | **Complete** |
|
||||||
| 8 | Admin REST API | **Complete** |
|
| 8 | Admin REST API | **Complete** |
|
||||||
| 9 | Garbage collection | **Complete** |
|
| 9 | Garbage collection | **Complete** |
|
||||||
| 10 | gRPC admin API | Not started |
|
| 10 | gRPC admin API | **Complete** |
|
||||||
| 11 | CLI tool (mcrctl) | Not started |
|
| 11 | CLI tool (mcrctl) | Not started |
|
||||||
| 12 | Web UI | Not started |
|
| 12 | Web UI | Not started |
|
||||||
| 13 | Deployment artifacts | Not started |
|
| 13 | Deployment artifacts | Not started |
|
||||||
|
|||||||
19
gen/mcr/v1/admin.pb.go
Normal file
19
gen/mcr/v1/admin.pb.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// source: mcr/v1/admin.proto
|
||||||
|
|
||||||
|
package mcrv1
|
||||||
|
|
||||||
|
// HealthRequest is the request message for Health.
|
||||||
|
type HealthRequest struct{}
|
||||||
|
|
||||||
|
// HealthResponse is the response message for Health.
|
||||||
|
type HealthResponse struct {
|
||||||
|
Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HealthResponse) GetStatus() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Status
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
88
gen/mcr/v1/admin_grpc.pb.go
Normal file
88
gen/mcr/v1/admin_grpc.pb.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// source: mcr/v1/admin.proto
|
||||||
|
|
||||||
|
package mcrv1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminServiceServer is the server API for AdminService.
|
||||||
|
type AdminServiceServer interface {
|
||||||
|
Health(context.Context, *HealthRequest) (*HealthResponse, error)
|
||||||
|
mustEmbedUnimplementedAdminServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedAdminServiceServer should be embedded to have forward
|
||||||
|
// compatible implementations.
|
||||||
|
type UnimplementedAdminServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedAdminServiceServer) Health(context.Context, *HealthRequest) (*HealthResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Health not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedAdminServiceServer) mustEmbedUnimplementedAdminServiceServer() {}
|
||||||
|
|
||||||
|
// RegisterAdminServiceServer registers the AdminServiceServer with the grpc.Server.
|
||||||
|
func RegisterAdminServiceServer(s grpc.ServiceRegistrar, srv AdminServiceServer) {
|
||||||
|
s.RegisterService(&AdminService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminServiceHealthHandler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
|
||||||
|
in := new(HealthRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AdminServiceServer).Health(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/mcr.v1.AdminService/Health",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req any) (any, error) {
|
||||||
|
return srv.(AdminServiceServer).Health(ctx, req.(*HealthRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminService_ServiceDesc is the grpc.ServiceDesc for AdminService.
|
||||||
|
var AdminService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "mcr.v1.AdminService",
|
||||||
|
HandlerType: (*AdminServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "Health",
|
||||||
|
Handler: adminServiceHealthHandler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "mcr/v1/admin.proto",
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminServiceClient is the client API for AdminService.
|
||||||
|
type AdminServiceClient interface {
|
||||||
|
Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type adminServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAdminServiceClient creates a new AdminServiceClient.
|
||||||
|
func NewAdminServiceClient(cc grpc.ClientConnInterface) AdminServiceClient {
|
||||||
|
return &adminServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *adminServiceClient) Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error) {
|
||||||
|
out := new(HealthResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/mcr.v1.AdminService/Health", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
136
gen/mcr/v1/audit.pb.go
Normal file
136
gen/mcr/v1/audit.pb.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// source: mcr/v1/audit.proto
|
||||||
|
|
||||||
|
package mcrv1
|
||||||
|
|
||||||
|
// AuditEvent represents an audit log entry.
|
||||||
|
type AuditEvent struct {
|
||||||
|
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` //nolint:revive,stylecheck // proto field name
|
||||||
|
EventTime string `protobuf:"bytes,2,opt,name=event_time,json=eventTime,proto3" json:"event_time,omitempty"`
|
||||||
|
EventType string `protobuf:"bytes,3,opt,name=event_type,json=eventType,proto3" json:"event_type,omitempty"`
|
||||||
|
ActorId string `protobuf:"bytes,4,opt,name=actor_id,json=actorId,proto3" json:"actor_id,omitempty"` //nolint:revive,stylecheck // proto field name
|
||||||
|
Repository string `protobuf:"bytes,5,opt,name=repository,proto3" json:"repository,omitempty"`
|
||||||
|
Digest string `protobuf:"bytes,6,opt,name=digest,proto3" json:"digest,omitempty"`
|
||||||
|
IpAddress string `protobuf:"bytes,7,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"` //nolint:revive,stylecheck // proto field name
|
||||||
|
Details map[string]string `protobuf:"bytes,8,rep,name=details,proto3" json:"details,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AuditEvent) GetId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AuditEvent) GetEventTime() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.EventTime
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AuditEvent) GetEventType() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.EventType
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AuditEvent) GetActorId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ActorId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AuditEvent) GetRepository() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Repository
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AuditEvent) GetDigest() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Digest
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AuditEvent) GetIpAddress() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.IpAddress
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *AuditEvent) GetDetails() map[string]string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Details
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAuditEventsRequest is the request message for ListAuditEvents.
|
||||||
|
type ListAuditEventsRequest struct {
|
||||||
|
Pagination *PaginationRequest `protobuf:"bytes,1,opt,name=pagination,proto3" json:"pagination,omitempty"`
|
||||||
|
EventType string `protobuf:"bytes,2,opt,name=event_type,json=eventType,proto3" json:"event_type,omitempty"`
|
||||||
|
ActorId string `protobuf:"bytes,3,opt,name=actor_id,json=actorId,proto3" json:"actor_id,omitempty"` //nolint:revive,stylecheck // proto field name
|
||||||
|
Repository string `protobuf:"bytes,4,opt,name=repository,proto3" json:"repository,omitempty"`
|
||||||
|
Since string `protobuf:"bytes,5,opt,name=since,proto3" json:"since,omitempty"`
|
||||||
|
Until string `protobuf:"bytes,6,opt,name=until,proto3" json:"until,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListAuditEventsRequest) GetPagination() *PaginationRequest {
|
||||||
|
if x != nil {
|
||||||
|
return x.Pagination
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListAuditEventsRequest) GetEventType() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.EventType
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListAuditEventsRequest) GetActorId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ActorId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListAuditEventsRequest) GetRepository() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Repository
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListAuditEventsRequest) GetSince() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Since
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListAuditEventsRequest) GetUntil() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Until
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAuditEventsResponse is the response message for ListAuditEvents.
|
||||||
|
type ListAuditEventsResponse struct {
|
||||||
|
Events []*AuditEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListAuditEventsResponse) GetEvents() []*AuditEvent {
|
||||||
|
if x != nil {
|
||||||
|
return x.Events
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
88
gen/mcr/v1/audit_grpc.pb.go
Normal file
88
gen/mcr/v1/audit_grpc.pb.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// source: mcr/v1/audit.proto
|
||||||
|
|
||||||
|
package mcrv1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuditServiceServer is the server API for AuditService.
|
||||||
|
type AuditServiceServer interface {
|
||||||
|
ListAuditEvents(context.Context, *ListAuditEventsRequest) (*ListAuditEventsResponse, error)
|
||||||
|
mustEmbedUnimplementedAuditServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedAuditServiceServer should be embedded to have forward
|
||||||
|
// compatible implementations.
|
||||||
|
type UnimplementedAuditServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedAuditServiceServer) ListAuditEvents(context.Context, *ListAuditEventsRequest) (*ListAuditEventsResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ListAuditEvents not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedAuditServiceServer) mustEmbedUnimplementedAuditServiceServer() {}
|
||||||
|
|
||||||
|
// RegisterAuditServiceServer registers the AuditServiceServer with the grpc.Server.
|
||||||
|
func RegisterAuditServiceServer(s grpc.ServiceRegistrar, srv AuditServiceServer) {
|
||||||
|
s.RegisterService(&AuditService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func auditServiceListAuditEventsHandler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
|
||||||
|
in := new(ListAuditEventsRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(AuditServiceServer).ListAuditEvents(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/mcr.v1.AuditService/ListAuditEvents",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req any) (any, error) {
|
||||||
|
return srv.(AuditServiceServer).ListAuditEvents(ctx, req.(*ListAuditEventsRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuditService_ServiceDesc is the grpc.ServiceDesc for AuditService.
|
||||||
|
var AuditService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "mcr.v1.AuditService",
|
||||||
|
HandlerType: (*AuditServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "ListAuditEvents",
|
||||||
|
Handler: auditServiceListAuditEventsHandler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "mcr/v1/audit.proto",
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuditServiceClient is the client API for AuditService.
|
||||||
|
type AuditServiceClient interface {
|
||||||
|
ListAuditEvents(ctx context.Context, in *ListAuditEventsRequest, opts ...grpc.CallOption) (*ListAuditEventsResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type auditServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuditServiceClient creates a new AuditServiceClient.
|
||||||
|
func NewAuditServiceClient(cc grpc.ClientConnInterface) AuditServiceClient {
|
||||||
|
return &auditServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *auditServiceClient) ListAuditEvents(ctx context.Context, in *ListAuditEventsRequest, opts ...grpc.CallOption) (*ListAuditEventsResponse, error) {
|
||||||
|
out := new(ListAuditEventsResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/mcr.v1.AuditService/ListAuditEvents", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
44
gen/mcr/v1/codec.go
Normal file
44
gen/mcr/v1/codec.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// This file provides a JSON codec for gRPC, used until proto tooling
|
||||||
|
// is available to generate proper protobuf serialization code.
|
||||||
|
|
||||||
|
package mcrv1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/encoding"
|
||||||
|
"google.golang.org/grpc/mem"
|
||||||
|
)
|
||||||
|
|
||||||
|
const codecName = "json"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
encoding.RegisterCodecV2(JSONCodec{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONCodec implements encoding.CodecV2 using JSON serialization.
|
||||||
|
// It is used as a stand-in until protobuf code generation is available.
|
||||||
|
type JSONCodec struct{}
|
||||||
|
|
||||||
|
func (JSONCodec) Marshal(v any) (mem.BufferSlice, error) {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("json codec: marshal: %w", err)
|
||||||
|
}
|
||||||
|
return mem.BufferSlice{mem.NewBuffer(&b, nil)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (JSONCodec) Unmarshal(data mem.BufferSlice, v any) error {
|
||||||
|
buf := data.Materialize()
|
||||||
|
if err := json.Unmarshal(buf, v); err != nil {
|
||||||
|
return fmt.Errorf("json codec: unmarshal: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (JSONCodec) Name() string {
|
||||||
|
return codecName
|
||||||
|
}
|
||||||
24
gen/mcr/v1/common.pb.go
Normal file
24
gen/mcr/v1/common.pb.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// source: mcr/v1/common.proto
|
||||||
|
|
||||||
|
package mcrv1
|
||||||
|
|
||||||
|
// PaginationRequest controls pagination for list RPCs.
|
||||||
|
type PaginationRequest struct {
|
||||||
|
Limit int32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"`
|
||||||
|
Offset int32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PaginationRequest) GetLimit() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Limit
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PaginationRequest) GetOffset() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Offset
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
331
gen/mcr/v1/policy.pb.go
Normal file
331
gen/mcr/v1/policy.pb.go
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// source: mcr/v1/policy.proto
|
||||||
|
|
||||||
|
package mcrv1
|
||||||
|
|
||||||
|
// PolicyRule represents a policy rule.
|
||||||
|
type PolicyRule struct {
|
||||||
|
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` //nolint:revive,stylecheck // proto field name
|
||||||
|
Priority int32 `protobuf:"varint,2,opt,name=priority,proto3" json:"priority,omitempty"`
|
||||||
|
Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"`
|
||||||
|
Effect string `protobuf:"bytes,4,opt,name=effect,proto3" json:"effect,omitempty"`
|
||||||
|
Roles []string `protobuf:"bytes,5,rep,name=roles,proto3" json:"roles,omitempty"`
|
||||||
|
AccountTypes []string `protobuf:"bytes,6,rep,name=account_types,json=accountTypes,proto3" json:"account_types,omitempty"`
|
||||||
|
SubjectUuid string `protobuf:"bytes,7,opt,name=subject_uuid,json=subjectUuid,proto3" json:"subject_uuid,omitempty"` //nolint:revive,stylecheck // proto field name
|
||||||
|
Actions []string `protobuf:"bytes,8,rep,name=actions,proto3" json:"actions,omitempty"`
|
||||||
|
Repositories []string `protobuf:"bytes,9,rep,name=repositories,proto3" json:"repositories,omitempty"`
|
||||||
|
Enabled bool `protobuf:"varint,10,opt,name=enabled,proto3" json:"enabled,omitempty"`
|
||||||
|
CreatedBy string `protobuf:"bytes,11,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"`
|
||||||
|
CreatedAt string `protobuf:"bytes,12,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||||
|
UpdatedAt string `protobuf:"bytes,13,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PolicyRule) GetId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PolicyRule) GetPriority() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Priority
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PolicyRule) GetDescription() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Description
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PolicyRule) GetEffect() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Effect
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PolicyRule) GetRoles() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Roles
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PolicyRule) GetAccountTypes() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AccountTypes
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PolicyRule) GetSubjectUuid() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.SubjectUuid
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PolicyRule) GetActions() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Actions
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PolicyRule) GetRepositories() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Repositories
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PolicyRule) GetEnabled() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Enabled
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PolicyRule) GetCreatedBy() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.CreatedBy
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PolicyRule) GetCreatedAt() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.CreatedAt
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PolicyRule) GetUpdatedAt() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.UpdatedAt
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPolicyRulesRequest is the request message for ListPolicyRules.
|
||||||
|
type ListPolicyRulesRequest struct {
|
||||||
|
Pagination *PaginationRequest `protobuf:"bytes,1,opt,name=pagination,proto3" json:"pagination,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListPolicyRulesRequest) GetPagination() *PaginationRequest {
|
||||||
|
if x != nil {
|
||||||
|
return x.Pagination
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPolicyRulesResponse is the response message for ListPolicyRules.
|
||||||
|
type ListPolicyRulesResponse struct {
|
||||||
|
Rules []*PolicyRule `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListPolicyRulesResponse) GetRules() []*PolicyRule {
|
||||||
|
if x != nil {
|
||||||
|
return x.Rules
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePolicyRuleRequest is the request message for CreatePolicyRule.
|
||||||
|
type CreatePolicyRuleRequest struct {
|
||||||
|
Priority int32 `protobuf:"varint,1,opt,name=priority,proto3" json:"priority,omitempty"`
|
||||||
|
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
|
||||||
|
Effect string `protobuf:"bytes,3,opt,name=effect,proto3" json:"effect,omitempty"`
|
||||||
|
Roles []string `protobuf:"bytes,4,rep,name=roles,proto3" json:"roles,omitempty"`
|
||||||
|
AccountTypes []string `protobuf:"bytes,5,rep,name=account_types,json=accountTypes,proto3" json:"account_types,omitempty"`
|
||||||
|
SubjectUuid string `protobuf:"bytes,6,opt,name=subject_uuid,json=subjectUuid,proto3" json:"subject_uuid,omitempty"` //nolint:revive,stylecheck // proto field name
|
||||||
|
Actions []string `protobuf:"bytes,7,rep,name=actions,proto3" json:"actions,omitempty"`
|
||||||
|
Repositories []string `protobuf:"bytes,8,rep,name=repositories,proto3" json:"repositories,omitempty"`
|
||||||
|
Enabled bool `protobuf:"varint,9,opt,name=enabled,proto3" json:"enabled,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreatePolicyRuleRequest) GetPriority() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Priority
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreatePolicyRuleRequest) GetDescription() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Description
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreatePolicyRuleRequest) GetEffect() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Effect
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreatePolicyRuleRequest) GetRoles() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Roles
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreatePolicyRuleRequest) GetAccountTypes() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AccountTypes
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreatePolicyRuleRequest) GetSubjectUuid() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.SubjectUuid
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreatePolicyRuleRequest) GetActions() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Actions
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreatePolicyRuleRequest) GetRepositories() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Repositories
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreatePolicyRuleRequest) GetEnabled() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Enabled
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPolicyRuleRequest is the request message for GetPolicyRule.
|
||||||
|
type GetPolicyRuleRequest struct {
|
||||||
|
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` //nolint:revive,stylecheck // proto field name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetPolicyRuleRequest) GetId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePolicyRuleRequest is the request message for UpdatePolicyRule.
|
||||||
|
type UpdatePolicyRuleRequest struct {
|
||||||
|
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` //nolint:revive,stylecheck // proto field name
|
||||||
|
Priority int32 `protobuf:"varint,2,opt,name=priority,proto3" json:"priority,omitempty"`
|
||||||
|
Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"`
|
||||||
|
Effect string `protobuf:"bytes,4,opt,name=effect,proto3" json:"effect,omitempty"`
|
||||||
|
Roles []string `protobuf:"bytes,5,rep,name=roles,proto3" json:"roles,omitempty"`
|
||||||
|
AccountTypes []string `protobuf:"bytes,6,rep,name=account_types,json=accountTypes,proto3" json:"account_types,omitempty"`
|
||||||
|
SubjectUuid string `protobuf:"bytes,7,opt,name=subject_uuid,json=subjectUuid,proto3" json:"subject_uuid,omitempty"` //nolint:revive,stylecheck // proto field name
|
||||||
|
Actions []string `protobuf:"bytes,8,rep,name=actions,proto3" json:"actions,omitempty"`
|
||||||
|
Repositories []string `protobuf:"bytes,9,rep,name=repositories,proto3" json:"repositories,omitempty"`
|
||||||
|
Enabled bool `protobuf:"varint,10,opt,name=enabled,proto3" json:"enabled,omitempty"`
|
||||||
|
UpdateMask []string `protobuf:"bytes,11,rep,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdatePolicyRuleRequest) GetId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdatePolicyRuleRequest) GetPriority() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Priority
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdatePolicyRuleRequest) GetDescription() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Description
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdatePolicyRuleRequest) GetEffect() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Effect
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdatePolicyRuleRequest) GetRoles() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Roles
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdatePolicyRuleRequest) GetAccountTypes() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AccountTypes
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdatePolicyRuleRequest) GetSubjectUuid() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.SubjectUuid
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdatePolicyRuleRequest) GetActions() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Actions
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdatePolicyRuleRequest) GetRepositories() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Repositories
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdatePolicyRuleRequest) GetEnabled() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Enabled
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdatePolicyRuleRequest) GetUpdateMask() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.UpdateMask
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePolicyRuleRequest is the request message for DeletePolicyRule.
|
||||||
|
type DeletePolicyRuleRequest struct {
|
||||||
|
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` //nolint:revive,stylecheck // proto field name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DeletePolicyRuleRequest) GetId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePolicyRuleResponse is the response message for DeletePolicyRule.
|
||||||
|
type DeletePolicyRuleResponse struct{}
|
||||||
236
gen/mcr/v1/policy_grpc.pb.go
Normal file
236
gen/mcr/v1/policy_grpc.pb.go
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// source: mcr/v1/policy.proto
|
||||||
|
|
||||||
|
package mcrv1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PolicyServiceServer is the server API for PolicyService.
|
||||||
|
type PolicyServiceServer interface {
|
||||||
|
ListPolicyRules(context.Context, *ListPolicyRulesRequest) (*ListPolicyRulesResponse, error)
|
||||||
|
CreatePolicyRule(context.Context, *CreatePolicyRuleRequest) (*PolicyRule, error)
|
||||||
|
GetPolicyRule(context.Context, *GetPolicyRuleRequest) (*PolicyRule, error)
|
||||||
|
UpdatePolicyRule(context.Context, *UpdatePolicyRuleRequest) (*PolicyRule, error)
|
||||||
|
DeletePolicyRule(context.Context, *DeletePolicyRuleRequest) (*DeletePolicyRuleResponse, error)
|
||||||
|
mustEmbedUnimplementedPolicyServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedPolicyServiceServer should be embedded to have forward
|
||||||
|
// compatible implementations.
|
||||||
|
type UnimplementedPolicyServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedPolicyServiceServer) ListPolicyRules(context.Context, *ListPolicyRulesRequest) (*ListPolicyRulesResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ListPolicyRules not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPolicyServiceServer) CreatePolicyRule(context.Context, *CreatePolicyRuleRequest) (*PolicyRule, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method CreatePolicyRule not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPolicyServiceServer) GetPolicyRule(context.Context, *GetPolicyRuleRequest) (*PolicyRule, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetPolicyRule not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPolicyServiceServer) UpdatePolicyRule(context.Context, *UpdatePolicyRuleRequest) (*PolicyRule, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method UpdatePolicyRule not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPolicyServiceServer) DeletePolicyRule(context.Context, *DeletePolicyRuleRequest) (*DeletePolicyRuleResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method DeletePolicyRule not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedPolicyServiceServer) mustEmbedUnimplementedPolicyServiceServer() {}
|
||||||
|
|
||||||
|
// RegisterPolicyServiceServer registers the PolicyServiceServer with the grpc.Server.
|
||||||
|
func RegisterPolicyServiceServer(s grpc.ServiceRegistrar, srv PolicyServiceServer) {
|
||||||
|
s.RegisterService(&PolicyService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func policyServiceListPolicyRulesHandler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
|
||||||
|
in := new(ListPolicyRulesRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(PolicyServiceServer).ListPolicyRules(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/mcr.v1.PolicyService/ListPolicyRules",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req any) (any, error) {
|
||||||
|
return srv.(PolicyServiceServer).ListPolicyRules(ctx, req.(*ListPolicyRulesRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func policyServiceCreatePolicyRuleHandler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
|
||||||
|
in := new(CreatePolicyRuleRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(PolicyServiceServer).CreatePolicyRule(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/mcr.v1.PolicyService/CreatePolicyRule",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req any) (any, error) {
|
||||||
|
return srv.(PolicyServiceServer).CreatePolicyRule(ctx, req.(*CreatePolicyRuleRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func policyServiceGetPolicyRuleHandler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
|
||||||
|
in := new(GetPolicyRuleRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(PolicyServiceServer).GetPolicyRule(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/mcr.v1.PolicyService/GetPolicyRule",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req any) (any, error) {
|
||||||
|
return srv.(PolicyServiceServer).GetPolicyRule(ctx, req.(*GetPolicyRuleRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func policyServiceUpdatePolicyRuleHandler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
|
||||||
|
in := new(UpdatePolicyRuleRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(PolicyServiceServer).UpdatePolicyRule(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/mcr.v1.PolicyService/UpdatePolicyRule",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req any) (any, error) {
|
||||||
|
return srv.(PolicyServiceServer).UpdatePolicyRule(ctx, req.(*UpdatePolicyRuleRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func policyServiceDeletePolicyRuleHandler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
|
||||||
|
in := new(DeletePolicyRuleRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(PolicyServiceServer).DeletePolicyRule(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/mcr.v1.PolicyService/DeletePolicyRule",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req any) (any, error) {
|
||||||
|
return srv.(PolicyServiceServer).DeletePolicyRule(ctx, req.(*DeletePolicyRuleRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicyService_ServiceDesc is the grpc.ServiceDesc for PolicyService.
|
||||||
|
var PolicyService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "mcr.v1.PolicyService",
|
||||||
|
HandlerType: (*PolicyServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "ListPolicyRules",
|
||||||
|
Handler: policyServiceListPolicyRulesHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "CreatePolicyRule",
|
||||||
|
Handler: policyServiceCreatePolicyRuleHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetPolicyRule",
|
||||||
|
Handler: policyServiceGetPolicyRuleHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "UpdatePolicyRule",
|
||||||
|
Handler: policyServiceUpdatePolicyRuleHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "DeletePolicyRule",
|
||||||
|
Handler: policyServiceDeletePolicyRuleHandler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "mcr/v1/policy.proto",
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicyServiceClient is the client API for PolicyService.
|
||||||
|
type PolicyServiceClient interface {
|
||||||
|
ListPolicyRules(ctx context.Context, in *ListPolicyRulesRequest, opts ...grpc.CallOption) (*ListPolicyRulesResponse, error)
|
||||||
|
CreatePolicyRule(ctx context.Context, in *CreatePolicyRuleRequest, opts ...grpc.CallOption) (*PolicyRule, error)
|
||||||
|
GetPolicyRule(ctx context.Context, in *GetPolicyRuleRequest, opts ...grpc.CallOption) (*PolicyRule, error)
|
||||||
|
UpdatePolicyRule(ctx context.Context, in *UpdatePolicyRuleRequest, opts ...grpc.CallOption) (*PolicyRule, error)
|
||||||
|
DeletePolicyRule(ctx context.Context, in *DeletePolicyRuleRequest, opts ...grpc.CallOption) (*DeletePolicyRuleResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type policyServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPolicyServiceClient creates a new PolicyServiceClient.
|
||||||
|
func NewPolicyServiceClient(cc grpc.ClientConnInterface) PolicyServiceClient {
|
||||||
|
return &policyServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *policyServiceClient) ListPolicyRules(ctx context.Context, in *ListPolicyRulesRequest, opts ...grpc.CallOption) (*ListPolicyRulesResponse, error) {
|
||||||
|
out := new(ListPolicyRulesResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/mcr.v1.PolicyService/ListPolicyRules", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *policyServiceClient) CreatePolicyRule(ctx context.Context, in *CreatePolicyRuleRequest, opts ...grpc.CallOption) (*PolicyRule, error) {
|
||||||
|
out := new(PolicyRule)
|
||||||
|
err := c.cc.Invoke(ctx, "/mcr.v1.PolicyService/CreatePolicyRule", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *policyServiceClient) GetPolicyRule(ctx context.Context, in *GetPolicyRuleRequest, opts ...grpc.CallOption) (*PolicyRule, error) {
|
||||||
|
out := new(PolicyRule)
|
||||||
|
err := c.cc.Invoke(ctx, "/mcr.v1.PolicyService/GetPolicyRule", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *policyServiceClient) UpdatePolicyRule(ctx context.Context, in *UpdatePolicyRuleRequest, opts ...grpc.CallOption) (*PolicyRule, error) {
|
||||||
|
out := new(PolicyRule)
|
||||||
|
err := c.cc.Invoke(ctx, "/mcr.v1.PolicyService/UpdatePolicyRule", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *policyServiceClient) DeletePolicyRule(ctx context.Context, in *DeletePolicyRuleRequest, opts ...grpc.CallOption) (*DeletePolicyRuleResponse, error) {
|
||||||
|
out := new(DeletePolicyRuleResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/mcr.v1.PolicyService/DeletePolicyRule", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
273
gen/mcr/v1/registry.pb.go
Normal file
273
gen/mcr/v1/registry.pb.go
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// source: mcr/v1/registry.proto
|
||||||
|
|
||||||
|
package mcrv1
|
||||||
|
|
||||||
|
// RepositoryMetadata is a repository summary for listing.
|
||||||
|
type RepositoryMetadata struct {
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
TagCount int32 `protobuf:"varint,2,opt,name=tag_count,json=tagCount,proto3" json:"tag_count,omitempty"`
|
||||||
|
ManifestCount int32 `protobuf:"varint,3,opt,name=manifest_count,json=manifestCount,proto3" json:"manifest_count,omitempty"`
|
||||||
|
TotalSize int64 `protobuf:"varint,4,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"`
|
||||||
|
CreatedAt string `protobuf:"bytes,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RepositoryMetadata) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RepositoryMetadata) GetTagCount() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.TagCount
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RepositoryMetadata) GetManifestCount() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.ManifestCount
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RepositoryMetadata) GetTotalSize() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.TotalSize
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RepositoryMetadata) GetCreatedAt() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.CreatedAt
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagInfo is a tag with its manifest digest.
|
||||||
|
type TagInfo struct {
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
Digest string `protobuf:"bytes,2,opt,name=digest,proto3" json:"digest,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *TagInfo) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *TagInfo) GetDigest() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Digest
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManifestInfo is a manifest summary.
|
||||||
|
type ManifestInfo struct {
|
||||||
|
Digest string `protobuf:"bytes,1,opt,name=digest,proto3" json:"digest,omitempty"`
|
||||||
|
MediaType string `protobuf:"bytes,2,opt,name=media_type,json=mediaType,proto3" json:"media_type,omitempty"`
|
||||||
|
Size int64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"`
|
||||||
|
CreatedAt string `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ManifestInfo) GetDigest() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Digest
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ManifestInfo) GetMediaType() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.MediaType
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ManifestInfo) GetSize() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Size
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ManifestInfo) GetCreatedAt() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.CreatedAt
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRepositoriesRequest is the request message for ListRepositories.
|
||||||
|
type ListRepositoriesRequest struct {
|
||||||
|
Pagination *PaginationRequest `protobuf:"bytes,1,opt,name=pagination,proto3" json:"pagination,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListRepositoriesRequest) GetPagination() *PaginationRequest {
|
||||||
|
if x != nil {
|
||||||
|
return x.Pagination
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRepositoriesResponse is the response message for ListRepositories.
|
||||||
|
type ListRepositoriesResponse struct {
|
||||||
|
Repositories []*RepositoryMetadata `protobuf:"bytes,1,rep,name=repositories,proto3" json:"repositories,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListRepositoriesResponse) GetRepositories() []*RepositoryMetadata {
|
||||||
|
if x != nil {
|
||||||
|
return x.Repositories
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRepositoryRequest is the request message for GetRepository.
|
||||||
|
type GetRepositoryRequest struct {
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetRepositoryRequest) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRepositoryResponse is the response message for GetRepository.
|
||||||
|
type GetRepositoryResponse struct {
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
Tags []*TagInfo `protobuf:"bytes,2,rep,name=tags,proto3" json:"tags,omitempty"`
|
||||||
|
Manifests []*ManifestInfo `protobuf:"bytes,3,rep,name=manifests,proto3" json:"manifests,omitempty"`
|
||||||
|
TotalSize int64 `protobuf:"varint,4,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"`
|
||||||
|
CreatedAt string `protobuf:"bytes,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetRepositoryResponse) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetRepositoryResponse) GetTags() []*TagInfo {
|
||||||
|
if x != nil {
|
||||||
|
return x.Tags
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetRepositoryResponse) GetManifests() []*ManifestInfo {
|
||||||
|
if x != nil {
|
||||||
|
return x.Manifests
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetRepositoryResponse) GetTotalSize() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.TotalSize
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetRepositoryResponse) GetCreatedAt() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.CreatedAt
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRepositoryRequest is the request message for DeleteRepository.
|
||||||
|
type DeleteRepositoryRequest struct {
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DeleteRepositoryRequest) GetName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRepositoryResponse is the response message for DeleteRepository.
|
||||||
|
type DeleteRepositoryResponse struct{}
|
||||||
|
|
||||||
|
// GarbageCollectRequest is the request message for GarbageCollect.
|
||||||
|
type GarbageCollectRequest struct{}
|
||||||
|
|
||||||
|
// GarbageCollectResponse is the response message for GarbageCollect.
|
||||||
|
type GarbageCollectResponse struct {
|
||||||
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` //nolint:revive,stylecheck // proto field name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GarbageCollectResponse) GetId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGCStatusRequest is the request message for GetGCStatus.
|
||||||
|
type GetGCStatusRequest struct{}
|
||||||
|
|
||||||
|
// GCLastRun records the result of the last garbage collection run.
|
||||||
|
type GCLastRun struct {
|
||||||
|
StartedAt string `protobuf:"bytes,1,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"`
|
||||||
|
CompletedAt string `protobuf:"bytes,2,opt,name=completed_at,json=completedAt,proto3" json:"completed_at,omitempty"`
|
||||||
|
BlobsRemoved int32 `protobuf:"varint,3,opt,name=blobs_removed,json=blobsRemoved,proto3" json:"blobs_removed,omitempty"`
|
||||||
|
BytesFreed int64 `protobuf:"varint,4,opt,name=bytes_freed,json=bytesFreed,proto3" json:"bytes_freed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GCLastRun) GetStartedAt() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.StartedAt
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GCLastRun) GetCompletedAt() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.CompletedAt
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GCLastRun) GetBlobsRemoved() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.BlobsRemoved
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GCLastRun) GetBytesFreed() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.BytesFreed
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGCStatusResponse is the response message for GetGCStatus.
|
||||||
|
type GetGCStatusResponse struct {
|
||||||
|
Running bool `protobuf:"varint,1,opt,name=running,proto3" json:"running,omitempty"`
|
||||||
|
LastRun *GCLastRun `protobuf:"bytes,2,opt,name=last_run,json=lastRun,proto3" json:"last_run,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetGCStatusResponse) GetRunning() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Running
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetGCStatusResponse) GetLastRun() *GCLastRun {
|
||||||
|
if x != nil {
|
||||||
|
return x.LastRun
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
236
gen/mcr/v1/registry_grpc.pb.go
Normal file
236
gen/mcr/v1/registry_grpc.pb.go
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// source: mcr/v1/registry.proto
|
||||||
|
|
||||||
|
package mcrv1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegistryServiceServer is the server API for RegistryService.
|
||||||
|
type RegistryServiceServer interface {
|
||||||
|
ListRepositories(context.Context, *ListRepositoriesRequest) (*ListRepositoriesResponse, error)
|
||||||
|
GetRepository(context.Context, *GetRepositoryRequest) (*GetRepositoryResponse, error)
|
||||||
|
DeleteRepository(context.Context, *DeleteRepositoryRequest) (*DeleteRepositoryResponse, error)
|
||||||
|
GarbageCollect(context.Context, *GarbageCollectRequest) (*GarbageCollectResponse, error)
|
||||||
|
GetGCStatus(context.Context, *GetGCStatusRequest) (*GetGCStatusResponse, error)
|
||||||
|
mustEmbedUnimplementedRegistryServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedRegistryServiceServer should be embedded to have forward
|
||||||
|
// compatible implementations.
|
||||||
|
type UnimplementedRegistryServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedRegistryServiceServer) ListRepositories(context.Context, *ListRepositoriesRequest) (*ListRepositoriesResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ListRepositories not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedRegistryServiceServer) GetRepository(context.Context, *GetRepositoryRequest) (*GetRepositoryResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetRepository not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedRegistryServiceServer) DeleteRepository(context.Context, *DeleteRepositoryRequest) (*DeleteRepositoryResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method DeleteRepository not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedRegistryServiceServer) GarbageCollect(context.Context, *GarbageCollectRequest) (*GarbageCollectResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GarbageCollect not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedRegistryServiceServer) GetGCStatus(context.Context, *GetGCStatusRequest) (*GetGCStatusResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetGCStatus not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedRegistryServiceServer) mustEmbedUnimplementedRegistryServiceServer() {}
|
||||||
|
|
||||||
|
// RegisterRegistryServiceServer registers the RegistryServiceServer with the grpc.Server.
|
||||||
|
func RegisterRegistryServiceServer(s grpc.ServiceRegistrar, srv RegistryServiceServer) {
|
||||||
|
s.RegisterService(&RegistryService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registryServiceListRepositoriesHandler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
|
||||||
|
in := new(ListRepositoriesRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(RegistryServiceServer).ListRepositories(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/mcr.v1.RegistryService/ListRepositories",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req any) (any, error) {
|
||||||
|
return srv.(RegistryServiceServer).ListRepositories(ctx, req.(*ListRepositoriesRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registryServiceGetRepositoryHandler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
|
||||||
|
in := new(GetRepositoryRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(RegistryServiceServer).GetRepository(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/mcr.v1.RegistryService/GetRepository",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req any) (any, error) {
|
||||||
|
return srv.(RegistryServiceServer).GetRepository(ctx, req.(*GetRepositoryRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registryServiceDeleteRepositoryHandler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
|
||||||
|
in := new(DeleteRepositoryRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(RegistryServiceServer).DeleteRepository(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/mcr.v1.RegistryService/DeleteRepository",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req any) (any, error) {
|
||||||
|
return srv.(RegistryServiceServer).DeleteRepository(ctx, req.(*DeleteRepositoryRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registryServiceGarbageCollectHandler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
|
||||||
|
in := new(GarbageCollectRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(RegistryServiceServer).GarbageCollect(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/mcr.v1.RegistryService/GarbageCollect",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req any) (any, error) {
|
||||||
|
return srv.(RegistryServiceServer).GarbageCollect(ctx, req.(*GarbageCollectRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registryServiceGetGCStatusHandler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
|
||||||
|
in := new(GetGCStatusRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(RegistryServiceServer).GetGCStatus(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/mcr.v1.RegistryService/GetGCStatus",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req any) (any, error) {
|
||||||
|
return srv.(RegistryServiceServer).GetGCStatus(ctx, req.(*GetGCStatusRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegistryService_ServiceDesc is the grpc.ServiceDesc for RegistryService.
|
||||||
|
var RegistryService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "mcr.v1.RegistryService",
|
||||||
|
HandlerType: (*RegistryServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "ListRepositories",
|
||||||
|
Handler: registryServiceListRepositoriesHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetRepository",
|
||||||
|
Handler: registryServiceGetRepositoryHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "DeleteRepository",
|
||||||
|
Handler: registryServiceDeleteRepositoryHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GarbageCollect",
|
||||||
|
Handler: registryServiceGarbageCollectHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetGCStatus",
|
||||||
|
Handler: registryServiceGetGCStatusHandler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "mcr/v1/registry.proto",
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegistryServiceClient is the client API for RegistryService.
|
||||||
|
type RegistryServiceClient interface {
|
||||||
|
ListRepositories(ctx context.Context, in *ListRepositoriesRequest, opts ...grpc.CallOption) (*ListRepositoriesResponse, error)
|
||||||
|
GetRepository(ctx context.Context, in *GetRepositoryRequest, opts ...grpc.CallOption) (*GetRepositoryResponse, error)
|
||||||
|
DeleteRepository(ctx context.Context, in *DeleteRepositoryRequest, opts ...grpc.CallOption) (*DeleteRepositoryResponse, error)
|
||||||
|
GarbageCollect(ctx context.Context, in *GarbageCollectRequest, opts ...grpc.CallOption) (*GarbageCollectResponse, error)
|
||||||
|
GetGCStatus(ctx context.Context, in *GetGCStatusRequest, opts ...grpc.CallOption) (*GetGCStatusResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type registryServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRegistryServiceClient creates a new RegistryServiceClient.
|
||||||
|
func NewRegistryServiceClient(cc grpc.ClientConnInterface) RegistryServiceClient {
|
||||||
|
return ®istryServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *registryServiceClient) ListRepositories(ctx context.Context, in *ListRepositoriesRequest, opts ...grpc.CallOption) (*ListRepositoriesResponse, error) {
|
||||||
|
out := new(ListRepositoriesResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/mcr.v1.RegistryService/ListRepositories", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *registryServiceClient) GetRepository(ctx context.Context, in *GetRepositoryRequest, opts ...grpc.CallOption) (*GetRepositoryResponse, error) {
|
||||||
|
out := new(GetRepositoryResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/mcr.v1.RegistryService/GetRepository", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *registryServiceClient) DeleteRepository(ctx context.Context, in *DeleteRepositoryRequest, opts ...grpc.CallOption) (*DeleteRepositoryResponse, error) {
|
||||||
|
out := new(DeleteRepositoryResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/mcr.v1.RegistryService/DeleteRepository", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *registryServiceClient) GarbageCollect(ctx context.Context, in *GarbageCollectRequest, opts ...grpc.CallOption) (*GarbageCollectResponse, error) {
|
||||||
|
out := new(GarbageCollectResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/mcr.v1.RegistryService/GarbageCollect", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *registryServiceClient) GetGCStatus(ctx context.Context, in *GetGCStatusRequest, opts ...grpc.CallOption) (*GetGCStatusResponse, error) {
|
||||||
|
out := new(GetGCStatusResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/mcr.v1.RegistryService/GetGCStatus", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
5
go.mod
5
go.mod
@@ -13,7 +13,12 @@ require (
|
|||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/spf13/cobra v1.10.2 // indirect
|
github.com/spf13/cobra v1.10.2 // indirect
|
||||||
github.com/spf13/pflag v1.0.9 // indirect
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
|
golang.org/x/net v0.48.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
|
golang.org/x/text v0.32.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
|
google.golang.org/grpc v1.79.3 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
modernc.org/libc v1.70.0 // indirect
|
modernc.org/libc v1.70.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
|||||||
10
go.sum
10
go.sum
@@ -21,9 +21,19 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT
|
|||||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
|
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||||
|
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
||||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||||
|
|||||||
16
internal/grpcserver/admin.go
Normal file
16
internal/grpcserver/admin.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
pb "git.wntrmute.dev/kyle/mcr/gen/mcr/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// adminService implements pb.AdminServiceServer.
|
||||||
|
type adminService struct {
|
||||||
|
pb.UnimplementedAdminServiceServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *adminService) Health(_ context.Context, _ *pb.HealthRequest) (*pb.HealthResponse, error) {
|
||||||
|
return &pb.HealthResponse{Status: "ok"}, nil
|
||||||
|
}
|
||||||
43
internal/grpcserver/admin_test.go
Normal file
43
internal/grpcserver/admin_test.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
pb "git.wntrmute.dev/kyle/mcr/gen/mcr/v1"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHealthReturnsOk(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewAdminServiceClient(cc)
|
||||||
|
|
||||||
|
resp, err := client.Health(context.Background(), &pb.HealthRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Health: %v", err)
|
||||||
|
}
|
||||||
|
if resp.GetStatus() != "ok" {
|
||||||
|
t.Fatalf("status: got %q, want %q", resp.Status, "ok")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHealthWithoutAuth(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
// Use a validator that always rejects.
|
||||||
|
validator := &fakeValidator{err: auth.ErrUnauthorized}
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{
|
||||||
|
DB: database,
|
||||||
|
Validator: validator,
|
||||||
|
})
|
||||||
|
|
||||||
|
client := pb.NewAdminServiceClient(cc)
|
||||||
|
resp, err := client.Health(context.Background(), &pb.HealthRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Health without auth should succeed: %v", err)
|
||||||
|
}
|
||||||
|
if resp.GetStatus() != "ok" {
|
||||||
|
t.Fatalf("status: got %q, want %q", resp.Status, "ok")
|
||||||
|
}
|
||||||
|
}
|
||||||
61
internal/grpcserver/audit.go
Normal file
61
internal/grpcserver/audit.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
pb "git.wntrmute.dev/kyle/mcr/gen/mcr/v1"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// auditService implements pb.AuditServiceServer.
|
||||||
|
type auditService struct {
|
||||||
|
pb.UnimplementedAuditServiceServer
|
||||||
|
db *db.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *auditService) ListAuditEvents(_ context.Context, req *pb.ListAuditEventsRequest) (*pb.ListAuditEventsResponse, error) {
|
||||||
|
limit := int32(50)
|
||||||
|
offset := int32(0)
|
||||||
|
if req.GetPagination() != nil {
|
||||||
|
if req.Pagination.Limit > 0 {
|
||||||
|
limit = req.Pagination.Limit
|
||||||
|
}
|
||||||
|
if req.Pagination.Offset >= 0 {
|
||||||
|
offset = req.Pagination.Offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := db.AuditFilter{
|
||||||
|
EventType: req.GetEventType(),
|
||||||
|
ActorID: req.GetActorId(),
|
||||||
|
Repository: req.GetRepository(),
|
||||||
|
Since: req.GetSince(),
|
||||||
|
Until: req.GetUntil(),
|
||||||
|
Limit: int(limit),
|
||||||
|
Offset: int(offset),
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := s.db.ListAuditEvents(filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []*pb.AuditEvent
|
||||||
|
for _, e := range events {
|
||||||
|
result = append(result, &pb.AuditEvent{
|
||||||
|
Id: e.ID,
|
||||||
|
EventTime: e.EventTime,
|
||||||
|
EventType: e.EventType,
|
||||||
|
ActorId: e.ActorID,
|
||||||
|
Repository: e.Repository,
|
||||||
|
Digest: e.Digest,
|
||||||
|
IpAddress: e.IPAddress,
|
||||||
|
Details: e.Details,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.ListAuditEventsResponse{Events: result}, nil
|
||||||
|
}
|
||||||
95
internal/grpcserver/audit_test.go
Normal file
95
internal/grpcserver/audit_test.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
pb "git.wntrmute.dev/kyle/mcr/gen/mcr/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListAuditEventsEmpty(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewAuditServiceClient(cc)
|
||||||
|
|
||||||
|
resp, err := client.ListAuditEvents(adminCtx(), &pb.ListAuditEventsRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListAuditEvents: %v", err)
|
||||||
|
}
|
||||||
|
if len(resp.GetEvents()) != 0 {
|
||||||
|
t.Fatalf("expected 0 events, got %d", len(resp.Events))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListAuditEventsWithData(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
|
||||||
|
// Write some audit events directly.
|
||||||
|
err := deps.DB.WriteAuditEvent("test_event", "actor-1", "repo/test", "", "1.2.3.4", map[string]string{"key": "value"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("WriteAuditEvent: %v", err)
|
||||||
|
}
|
||||||
|
err = deps.DB.WriteAuditEvent("other_event", "actor-2", "", "", "5.6.7.8", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("WriteAuditEvent: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewAuditServiceClient(cc)
|
||||||
|
|
||||||
|
// List all events.
|
||||||
|
resp, err := client.ListAuditEvents(adminCtx(), &pb.ListAuditEventsRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListAuditEvents: %v", err)
|
||||||
|
}
|
||||||
|
if len(resp.GetEvents()) != 2 {
|
||||||
|
t.Fatalf("expected 2 events, got %d", len(resp.Events))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by event type.
|
||||||
|
resp, err = client.ListAuditEvents(adminCtx(), &pb.ListAuditEventsRequest{
|
||||||
|
EventType: "test_event",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListAuditEvents with filter: %v", err)
|
||||||
|
}
|
||||||
|
if len(resp.GetEvents()) != 1 {
|
||||||
|
t.Fatalf("expected 1 event, got %d", len(resp.Events))
|
||||||
|
}
|
||||||
|
if resp.Events[0].EventType != "test_event" {
|
||||||
|
t.Fatalf("event_type: got %q, want %q", resp.Events[0].EventType, "test_event")
|
||||||
|
}
|
||||||
|
if resp.Events[0].ActorId != "actor-1" {
|
||||||
|
t.Fatalf("actor_id: got %q, want %q", resp.Events[0].ActorId, "actor-1")
|
||||||
|
}
|
||||||
|
if resp.Events[0].Details["key"] != "value" {
|
||||||
|
t.Fatalf("details: got %v, want key=value", resp.Events[0].Details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListAuditEventsPagination(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
|
||||||
|
// Write 5 events.
|
||||||
|
for i := range 5 {
|
||||||
|
err := deps.DB.WriteAuditEvent("event", "actor", "", "", "", map[string]string{
|
||||||
|
"index": string(rune('0' + i)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("WriteAuditEvent %d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewAuditServiceClient(cc)
|
||||||
|
|
||||||
|
// Get first 2 events.
|
||||||
|
resp, err := client.ListAuditEvents(adminCtx(), &pb.ListAuditEventsRequest{
|
||||||
|
Pagination: &pb.PaginationRequest{Limit: 2},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListAuditEvents: %v", err)
|
||||||
|
}
|
||||||
|
if len(resp.GetEvents()) != 2 {
|
||||||
|
t.Fatalf("expected 2 events, got %d", len(resp.Events))
|
||||||
|
}
|
||||||
|
}
|
||||||
165
internal/grpcserver/interceptors.go
Normal file
165
internal/grpcserver/interceptors.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"google.golang.org/grpc/peer"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/auth"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
// authBypassMethods contains the full gRPC method names that bypass
|
||||||
|
// authentication. Health is the only method that does not require auth.
|
||||||
|
var authBypassMethods = map[string]bool{
|
||||||
|
"/mcr.v1.AdminService/Health": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// adminRequiredMethods contains the full gRPC method names that require
|
||||||
|
// the admin role. Adding an RPC without adding it to the correct map is
|
||||||
|
// a security defect per ARCHITECTURE.md.
|
||||||
|
var adminRequiredMethods = map[string]bool{
|
||||||
|
// Registry admin operations.
|
||||||
|
"/mcr.v1.RegistryService/DeleteRepository": true,
|
||||||
|
"/mcr.v1.RegistryService/GarbageCollect": true,
|
||||||
|
"/mcr.v1.RegistryService/GetGCStatus": true,
|
||||||
|
|
||||||
|
// Policy management — all RPCs require admin.
|
||||||
|
"/mcr.v1.PolicyService/ListPolicyRules": true,
|
||||||
|
"/mcr.v1.PolicyService/CreatePolicyRule": true,
|
||||||
|
"/mcr.v1.PolicyService/GetPolicyRule": true,
|
||||||
|
"/mcr.v1.PolicyService/UpdatePolicyRule": true,
|
||||||
|
"/mcr.v1.PolicyService/DeletePolicyRule": true,
|
||||||
|
|
||||||
|
// Audit — requires admin.
|
||||||
|
"/mcr.v1.AuditService/ListAuditEvents": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// authInterceptor validates bearer tokens from the authorization metadata.
|
||||||
|
type authInterceptor struct {
|
||||||
|
validator server.TokenValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthInterceptor(v server.TokenValidator) *authInterceptor {
|
||||||
|
return &authInterceptor{validator: v}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unary is the unary server interceptor for auth.
|
||||||
|
func (a *authInterceptor) unary(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
||||||
|
// Health bypasses auth.
|
||||||
|
if authBypassMethods[info.FullMethod] {
|
||||||
|
return handler(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract bearer token from authorization metadata.
|
||||||
|
token, err := extractToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Unauthenticated, "authentication required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the token via MCIAS.
|
||||||
|
claims, err := a.validator.ValidateToken(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Unauthenticated, "invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject claims into the context.
|
||||||
|
ctx = auth.ContextWithClaims(ctx, claims)
|
||||||
|
return handler(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractToken extracts a bearer token from the "authorization" gRPC metadata.
|
||||||
|
func extractToken(ctx context.Context) (string, error) {
|
||||||
|
md, ok := metadata.FromIncomingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return "", status.Errorf(codes.Unauthenticated, "missing metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
vals := md.Get("authorization")
|
||||||
|
if len(vals) == 0 {
|
||||||
|
return "", status.Errorf(codes.Unauthenticated, "missing authorization metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
val := vals[0]
|
||||||
|
const prefix = "Bearer "
|
||||||
|
if !strings.HasPrefix(val, prefix) {
|
||||||
|
return "", status.Errorf(codes.Unauthenticated, "invalid authorization format")
|
||||||
|
}
|
||||||
|
|
||||||
|
token := strings.TrimSpace(val[len(prefix):])
|
||||||
|
if token == "" {
|
||||||
|
return "", status.Errorf(codes.Unauthenticated, "empty bearer token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// adminInterceptor checks that the caller has the admin role for
|
||||||
|
// methods in adminRequiredMethods.
|
||||||
|
type adminInterceptor struct{}
|
||||||
|
|
||||||
|
func newAdminInterceptor() *adminInterceptor {
|
||||||
|
return &adminInterceptor{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unary is the unary server interceptor for admin role checks.
|
||||||
|
func (a *adminInterceptor) unary(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
||||||
|
if !adminRequiredMethods[info.FullMethod] {
|
||||||
|
return handler(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := auth.ClaimsFromContext(ctx)
|
||||||
|
if claims == nil {
|
||||||
|
return nil, status.Errorf(codes.Unauthenticated, "authentication required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasRole(claims.Roles, "admin") {
|
||||||
|
return nil, status.Errorf(codes.PermissionDenied, "admin role required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loggingInterceptor logs the method, peer IP, status code, and duration.
|
||||||
|
// It never logs the authorization metadata value.
|
||||||
|
func loggingInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
peerAddr := ""
|
||||||
|
if p, ok := peer.FromContext(ctx); ok {
|
||||||
|
peerAddr = p.Addr.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := handler(ctx, req)
|
||||||
|
|
||||||
|
duration := time.Since(start)
|
||||||
|
code := codes.OK
|
||||||
|
if err != nil {
|
||||||
|
if st, ok := status.FromError(err); ok {
|
||||||
|
code = st.Code()
|
||||||
|
} else {
|
||||||
|
code = codes.Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("grpc %s peer=%s code=%s duration=%s", info.FullMethod, peerAddr, code, duration)
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasRole checks if any of the roles match the target role.
|
||||||
|
func hasRole(roles []string, target string) bool {
|
||||||
|
for _, r := range roles {
|
||||||
|
if r == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
350
internal/grpcserver/interceptors_test.go
Normal file
350
internal/grpcserver/interceptors_test.go
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
pb "git.wntrmute.dev/kyle/mcr/gen/mcr/v1"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/auth"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fakeValidator is a test double for server.TokenValidator.
|
||||||
|
type fakeValidator struct {
|
||||||
|
claims *auth.Claims
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeValidator) ValidateToken(_ string) (*auth.Claims, error) {
|
||||||
|
return f.claims, f.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// openTestDB creates a temporary test database with migrations applied.
|
||||||
|
func openTestDB(t *testing.T) *db.DB {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join(t.TempDir(), "test.db")
|
||||||
|
d, err := db.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = d.Close() })
|
||||||
|
if err := d.Migrate(); err != nil {
|
||||||
|
t.Fatalf("Migrate: %v", err)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// startTestServer creates a gRPC server and client for testing.
|
||||||
|
// Returns the client connection and a cleanup function.
|
||||||
|
func startTestServer(t *testing.T, deps Deps) *grpc.ClientConn {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
srv, err := New("", "", deps)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Listen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = srv.Serve(lis)
|
||||||
|
}()
|
||||||
|
t.Cleanup(func() { srv.GracefulStop() })
|
||||||
|
|
||||||
|
//nolint:gosec // insecure credentials for testing only
|
||||||
|
cc, err := grpc.NewClient(
|
||||||
|
lis.Addr().String(),
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithDefaultCallOptions(grpc.ForceCodecV2(pb.JSONCodec{})),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Dial: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = cc.Close() })
|
||||||
|
|
||||||
|
return cc
|
||||||
|
}
|
||||||
|
|
||||||
|
// withAuth adds a bearer token to the outgoing context metadata.
|
||||||
|
func withAuth(ctx context.Context, token string) context.Context {
|
||||||
|
return metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHealthBypassesAuth(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
validator := &fakeValidator{err: auth.ErrUnauthorized}
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{
|
||||||
|
DB: database,
|
||||||
|
Validator: validator,
|
||||||
|
})
|
||||||
|
|
||||||
|
client := pb.NewAdminServiceClient(cc)
|
||||||
|
resp, err := client.Health(context.Background(), &pb.HealthRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Health: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Status != "ok" {
|
||||||
|
t.Fatalf("Health status: got %q, want %q", resp.Status, "ok")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthInterceptorNoToken(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
validator := &fakeValidator{err: auth.ErrUnauthorized}
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{
|
||||||
|
DB: database,
|
||||||
|
Validator: validator,
|
||||||
|
})
|
||||||
|
|
||||||
|
client := pb.NewRegistryServiceClient(cc)
|
||||||
|
_, err := client.ListRepositories(context.Background(), &pb.ListRepositoriesRequest{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for unauthenticated request")
|
||||||
|
}
|
||||||
|
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status error, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.Unauthenticated {
|
||||||
|
t.Fatalf("code: got %v, want Unauthenticated", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthInterceptorInvalidToken(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
validator := &fakeValidator{err: auth.ErrUnauthorized}
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{
|
||||||
|
DB: database,
|
||||||
|
Validator: validator,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := withAuth(context.Background(), "bad-token")
|
||||||
|
client := pb.NewRegistryServiceClient(cc)
|
||||||
|
_, err := client.ListRepositories(ctx, &pb.ListRepositoriesRequest{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status error, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.Unauthenticated {
|
||||||
|
t.Fatalf("code: got %v, want Unauthenticated", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthInterceptorValidToken(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
validator := &fakeValidator{
|
||||||
|
claims: &auth.Claims{Subject: "alice", AccountType: "human", Roles: []string{"user"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{
|
||||||
|
DB: database,
|
||||||
|
Validator: validator,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := withAuth(context.Background(), "valid-token")
|
||||||
|
client := pb.NewRegistryServiceClient(cc)
|
||||||
|
resp, err := client.ListRepositories(ctx, &pb.ListRepositoriesRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListRepositories: %v", err)
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("expected non-nil response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdminInterceptorDenied(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
validator := &fakeValidator{
|
||||||
|
claims: &auth.Claims{Subject: "user-uuid", AccountType: "human", Roles: []string{"user"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{
|
||||||
|
DB: database,
|
||||||
|
Validator: validator,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := withAuth(context.Background(), "valid-token")
|
||||||
|
|
||||||
|
// Policy RPCs require admin.
|
||||||
|
policyClient := pb.NewPolicyServiceClient(cc)
|
||||||
|
_, err := policyClient.ListPolicyRules(ctx, &pb.ListPolicyRulesRequest{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for non-admin user")
|
||||||
|
}
|
||||||
|
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status error, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.PermissionDenied {
|
||||||
|
t.Fatalf("code: got %v, want PermissionDenied", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdminInterceptorAllowed(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
validator := &fakeValidator{
|
||||||
|
claims: &auth.Claims{Subject: "admin-uuid", AccountType: "human", Roles: []string{"admin"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{
|
||||||
|
DB: database,
|
||||||
|
Validator: validator,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := withAuth(context.Background(), "valid-token")
|
||||||
|
|
||||||
|
// Admin user should be able to list policy rules.
|
||||||
|
policyClient := pb.NewPolicyServiceClient(cc)
|
||||||
|
resp, err := policyClient.ListPolicyRules(ctx, &pb.ListPolicyRulesRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListPolicyRules: %v", err)
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("expected non-nil response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdminRequiredMethodsCompleteness(t *testing.T) {
|
||||||
|
// Verify that admin-required methods match our security spec.
|
||||||
|
// This test catches the security defect of adding an RPC without
|
||||||
|
// adding it to the adminRequiredMethods map.
|
||||||
|
expected := []string{
|
||||||
|
"/mcr.v1.RegistryService/DeleteRepository",
|
||||||
|
"/mcr.v1.RegistryService/GarbageCollect",
|
||||||
|
"/mcr.v1.RegistryService/GetGCStatus",
|
||||||
|
"/mcr.v1.PolicyService/ListPolicyRules",
|
||||||
|
"/mcr.v1.PolicyService/CreatePolicyRule",
|
||||||
|
"/mcr.v1.PolicyService/GetPolicyRule",
|
||||||
|
"/mcr.v1.PolicyService/UpdatePolicyRule",
|
||||||
|
"/mcr.v1.PolicyService/DeletePolicyRule",
|
||||||
|
"/mcr.v1.AuditService/ListAuditEvents",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, method := range expected {
|
||||||
|
if !adminRequiredMethods[method] {
|
||||||
|
t.Errorf("method %s should require admin but is not in adminRequiredMethods", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(adminRequiredMethods) != len(expected) {
|
||||||
|
t.Errorf("adminRequiredMethods has %d entries, expected %d", len(adminRequiredMethods), len(expected))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthBypassMethodsCompleteness(t *testing.T) {
|
||||||
|
// Health is the only method that bypasses auth.
|
||||||
|
expected := []string{
|
||||||
|
"/mcr.v1.AdminService/Health",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, method := range expected {
|
||||||
|
if !authBypassMethods[method] {
|
||||||
|
t.Errorf("method %s should bypass auth but is not in authBypassMethods", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(authBypassMethods) != len(expected) {
|
||||||
|
t.Errorf("authBypassMethods has %d entries, expected %d", len(authBypassMethods), len(expected))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRepoRequiresAdmin(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
validator := &fakeValidator{
|
||||||
|
claims: &auth.Claims{Subject: "user-uuid", AccountType: "human", Roles: []string{"user"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{
|
||||||
|
DB: database,
|
||||||
|
Validator: validator,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := withAuth(context.Background(), "valid-token")
|
||||||
|
client := pb.NewRegistryServiceClient(cc)
|
||||||
|
_, err := client.DeleteRepository(ctx, &pb.DeleteRepositoryRequest{Name: "test"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for non-admin user trying to delete repo")
|
||||||
|
}
|
||||||
|
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status error, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.PermissionDenied {
|
||||||
|
t.Fatalf("code: got %v, want PermissionDenied", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGCRequiresAdmin(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
validator := &fakeValidator{
|
||||||
|
claims: &auth.Claims{Subject: "user-uuid", AccountType: "human", Roles: []string{"user"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{
|
||||||
|
DB: database,
|
||||||
|
Validator: validator,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := withAuth(context.Background(), "valid-token")
|
||||||
|
client := pb.NewRegistryServiceClient(cc)
|
||||||
|
_, err := client.GarbageCollect(ctx, &pb.GarbageCollectRequest{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for non-admin user trying to trigger GC")
|
||||||
|
}
|
||||||
|
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status error, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.PermissionDenied {
|
||||||
|
t.Fatalf("code: got %v, want PermissionDenied", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuditRequiresAdmin(t *testing.T) {
|
||||||
|
database := openTestDB(t)
|
||||||
|
validator := &fakeValidator{
|
||||||
|
claims: &auth.Claims{Subject: "user-uuid", AccountType: "human", Roles: []string{"user"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := startTestServer(t, Deps{
|
||||||
|
DB: database,
|
||||||
|
Validator: validator,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := withAuth(context.Background(), "valid-token")
|
||||||
|
client := pb.NewAuditServiceClient(cc)
|
||||||
|
_, err := client.ListAuditEvents(ctx, &pb.ListAuditEventsRequest{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for non-admin user trying to list audit events")
|
||||||
|
}
|
||||||
|
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status error, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.PermissionDenied {
|
||||||
|
t.Fatalf("code: got %v, want PermissionDenied", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
292
internal/grpcserver/policy.go
Normal file
292
internal/grpcserver/policy.go
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
pb "git.wntrmute.dev/kyle/mcr/gen/mcr/v1"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/auth"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/db"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
var validActions = map[string]bool{
|
||||||
|
string(policy.ActionVersionCheck): true,
|
||||||
|
string(policy.ActionPull): true,
|
||||||
|
string(policy.ActionPush): true,
|
||||||
|
string(policy.ActionDelete): true,
|
||||||
|
string(policy.ActionCatalog): true,
|
||||||
|
string(policy.ActionPolicyManage): true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// policyService implements pb.PolicyServiceServer.
|
||||||
|
type policyService struct {
|
||||||
|
pb.UnimplementedPolicyServiceServer
|
||||||
|
db *db.DB
|
||||||
|
engine PolicyReloader
|
||||||
|
auditFn AuditFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *policyService) ListPolicyRules(_ context.Context, req *pb.ListPolicyRulesRequest) (*pb.ListPolicyRulesResponse, error) {
|
||||||
|
limit := int32(50)
|
||||||
|
offset := int32(0)
|
||||||
|
if req.GetPagination() != nil {
|
||||||
|
if req.Pagination.Limit > 0 {
|
||||||
|
limit = req.Pagination.Limit
|
||||||
|
}
|
||||||
|
if req.Pagination.Offset >= 0 {
|
||||||
|
offset = req.Pagination.Offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rules, err := s.db.ListPolicyRules(int(limit), int(offset))
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []*pb.PolicyRule
|
||||||
|
for _, r := range rules {
|
||||||
|
result = append(result, policyRuleRowToProto(&r))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.ListPolicyRulesResponse{Rules: result}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *policyService) CreatePolicyRule(ctx context.Context, req *pb.CreatePolicyRuleRequest) (*pb.PolicyRule, error) {
|
||||||
|
if req.GetPriority() < 1 {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "priority must be >= 1 (0 is reserved for built-ins)")
|
||||||
|
}
|
||||||
|
if req.GetDescription() == "" {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "description is required")
|
||||||
|
}
|
||||||
|
if err := validateEffect(req.GetEffect()); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "%s", err.Error())
|
||||||
|
}
|
||||||
|
if len(req.GetActions()) == 0 {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "at least one action is required")
|
||||||
|
}
|
||||||
|
if err := validateActions(req.GetActions()); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "%s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := auth.ClaimsFromContext(ctx)
|
||||||
|
createdBy := ""
|
||||||
|
if claims != nil {
|
||||||
|
createdBy = claims.Subject
|
||||||
|
}
|
||||||
|
|
||||||
|
row := db.PolicyRuleRow{
|
||||||
|
Priority: int(req.Priority),
|
||||||
|
Description: req.Description,
|
||||||
|
Effect: req.Effect,
|
||||||
|
Roles: req.Roles,
|
||||||
|
AccountTypes: req.AccountTypes,
|
||||||
|
SubjectUUID: req.SubjectUuid,
|
||||||
|
Actions: req.Actions,
|
||||||
|
Repositories: req.Repositories,
|
||||||
|
Enabled: req.Enabled,
|
||||||
|
CreatedBy: createdBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := s.db.CreatePolicyRule(row)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload policy engine.
|
||||||
|
if s.engine != nil {
|
||||||
|
_ = s.engine.Reload(s.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.auditFn != nil {
|
||||||
|
s.auditFn("policy_rule_created", createdBy, "", "", "", map[string]string{
|
||||||
|
"rule_id": strconv.FormatInt(id, 10),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
created, err := s.db.GetPolicyRule(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return policyRuleRowToProto(created), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *policyService) GetPolicyRule(_ context.Context, req *pb.GetPolicyRuleRequest) (*pb.PolicyRule, error) {
|
||||||
|
if req.GetId() == 0 {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "rule ID required")
|
||||||
|
}
|
||||||
|
|
||||||
|
rule, err := s.db.GetPolicyRule(req.Id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, db.ErrPolicyRuleNotFound) {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "policy rule not found")
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return policyRuleRowToProto(rule), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *policyService) UpdatePolicyRule(ctx context.Context, req *pb.UpdatePolicyRuleRequest) (*pb.PolicyRule, error) {
|
||||||
|
if req.GetId() == 0 {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "rule ID required")
|
||||||
|
}
|
||||||
|
|
||||||
|
mask := make(map[string]bool, len(req.GetUpdateMask()))
|
||||||
|
for _, f := range req.GetUpdateMask() {
|
||||||
|
mask[f] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate fields if they are in the update mask.
|
||||||
|
if mask["priority"] && req.Priority < 1 {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "priority must be >= 1 (0 is reserved for built-ins)")
|
||||||
|
}
|
||||||
|
if mask["effect"] {
|
||||||
|
if err := validateEffect(req.GetEffect()); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "%s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mask["actions"] {
|
||||||
|
if len(req.GetActions()) == 0 {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "at least one action is required")
|
||||||
|
}
|
||||||
|
if err := validateActions(req.GetActions()); err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "%s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := db.PolicyRuleRow{}
|
||||||
|
if mask["priority"] {
|
||||||
|
updates.Priority = int(req.Priority)
|
||||||
|
}
|
||||||
|
if mask["description"] {
|
||||||
|
updates.Description = req.Description
|
||||||
|
}
|
||||||
|
if mask["effect"] {
|
||||||
|
updates.Effect = req.Effect
|
||||||
|
}
|
||||||
|
if mask["roles"] {
|
||||||
|
updates.Roles = req.Roles
|
||||||
|
}
|
||||||
|
if mask["account_types"] {
|
||||||
|
updates.AccountTypes = req.AccountTypes
|
||||||
|
}
|
||||||
|
if mask["subject_uuid"] {
|
||||||
|
updates.SubjectUUID = req.SubjectUuid
|
||||||
|
}
|
||||||
|
if mask["actions"] {
|
||||||
|
updates.Actions = req.Actions
|
||||||
|
}
|
||||||
|
if mask["repositories"] {
|
||||||
|
updates.Repositories = req.Repositories
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.UpdatePolicyRule(req.Id, updates); err != nil {
|
||||||
|
if errors.Is(err, db.ErrPolicyRuleNotFound) {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "policy rule not found")
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle enabled separately since it's a bool.
|
||||||
|
if mask["enabled"] {
|
||||||
|
if err := s.db.SetPolicyRuleEnabled(req.Id, req.Enabled); err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload policy engine.
|
||||||
|
if s.engine != nil {
|
||||||
|
_ = s.engine.Reload(s.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.auditFn != nil {
|
||||||
|
claims := auth.ClaimsFromContext(ctx)
|
||||||
|
actorID := ""
|
||||||
|
if claims != nil {
|
||||||
|
actorID = claims.Subject
|
||||||
|
}
|
||||||
|
s.auditFn("policy_rule_updated", actorID, "", "", "", map[string]string{
|
||||||
|
"rule_id": strconv.FormatInt(req.Id, 10),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := s.db.GetPolicyRule(req.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return policyRuleRowToProto(updated), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *policyService) DeletePolicyRule(ctx context.Context, req *pb.DeletePolicyRuleRequest) (*pb.DeletePolicyRuleResponse, error) {
|
||||||
|
if req.GetId() == 0 {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "rule ID required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.DeletePolicyRule(req.Id); err != nil {
|
||||||
|
if errors.Is(err, db.ErrPolicyRuleNotFound) {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "policy rule not found")
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload policy engine.
|
||||||
|
if s.engine != nil {
|
||||||
|
_ = s.engine.Reload(s.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.auditFn != nil {
|
||||||
|
claims := auth.ClaimsFromContext(ctx)
|
||||||
|
actorID := ""
|
||||||
|
if claims != nil {
|
||||||
|
actorID = claims.Subject
|
||||||
|
}
|
||||||
|
s.auditFn("policy_rule_deleted", actorID, "", "", "", map[string]string{
|
||||||
|
"rule_id": strconv.FormatInt(req.Id, 10),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.DeletePolicyRuleResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// policyRuleRowToProto converts a db.PolicyRuleRow to a protobuf PolicyRule.
|
||||||
|
func policyRuleRowToProto(r *db.PolicyRuleRow) *pb.PolicyRule {
|
||||||
|
return &pb.PolicyRule{
|
||||||
|
Id: r.ID,
|
||||||
|
Priority: int32(r.Priority), //nolint:gosec // priority is always small positive int
|
||||||
|
Description: r.Description,
|
||||||
|
Effect: r.Effect,
|
||||||
|
Roles: r.Roles,
|
||||||
|
AccountTypes: r.AccountTypes,
|
||||||
|
SubjectUuid: r.SubjectUUID,
|
||||||
|
Actions: r.Actions,
|
||||||
|
Repositories: r.Repositories,
|
||||||
|
Enabled: r.Enabled,
|
||||||
|
CreatedBy: r.CreatedBy,
|
||||||
|
CreatedAt: r.CreatedAt,
|
||||||
|
UpdatedAt: r.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateEffect(effect string) error {
|
||||||
|
if effect != "allow" && effect != "deny" {
|
||||||
|
return fmt.Errorf("invalid effect: %q (must be 'allow' or 'deny')", effect)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateActions(actions []string) error {
|
||||||
|
for _, a := range actions {
|
||||||
|
if !validActions[a] {
|
||||||
|
return fmt.Errorf("invalid action: %q", a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
306
internal/grpcserver/policy_test.go
Normal file
306
internal/grpcserver/policy_test.go
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
pb "git.wntrmute.dev/kyle/mcr/gen/mcr/v1"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakePolicyReloader struct {
|
||||||
|
reloadCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePolicyReloader) Reload(_ policy.RuleStore) error {
|
||||||
|
f.reloadCount++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreatePolicyRule(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
reloader := &fakePolicyReloader{}
|
||||||
|
deps.Engine = reloader
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewPolicyServiceClient(cc)
|
||||||
|
|
||||||
|
resp, err := client.CreatePolicyRule(adminCtx(), &pb.CreatePolicyRuleRequest{
|
||||||
|
Priority: 10,
|
||||||
|
Description: "allow pull for all",
|
||||||
|
Effect: "allow",
|
||||||
|
Actions: []string{"registry:pull"},
|
||||||
|
Enabled: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreatePolicyRule: %v", err)
|
||||||
|
}
|
||||||
|
if resp.GetId() == 0 {
|
||||||
|
t.Fatal("expected non-zero ID")
|
||||||
|
}
|
||||||
|
if resp.GetDescription() != "allow pull for all" {
|
||||||
|
t.Fatalf("description: got %q, want %q", resp.Description, "allow pull for all")
|
||||||
|
}
|
||||||
|
if resp.GetEffect() != "allow" {
|
||||||
|
t.Fatalf("effect: got %q, want %q", resp.Effect, "allow")
|
||||||
|
}
|
||||||
|
if !resp.GetEnabled() {
|
||||||
|
t.Fatal("expected enabled=true")
|
||||||
|
}
|
||||||
|
if reloader.reloadCount != 1 {
|
||||||
|
t.Fatalf("reloadCount: got %d, want 1", reloader.reloadCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreatePolicyRuleValidation(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewPolicyServiceClient(cc)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
req *pb.CreatePolicyRuleRequest
|
||||||
|
code codes.Code
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "zero priority",
|
||||||
|
req: &pb.CreatePolicyRuleRequest{
|
||||||
|
Priority: 0,
|
||||||
|
Description: "test",
|
||||||
|
Effect: "allow",
|
||||||
|
Actions: []string{"registry:pull"},
|
||||||
|
},
|
||||||
|
code: codes.InvalidArgument,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty description",
|
||||||
|
req: &pb.CreatePolicyRuleRequest{
|
||||||
|
Priority: 1,
|
||||||
|
Effect: "allow",
|
||||||
|
Actions: []string{"registry:pull"},
|
||||||
|
},
|
||||||
|
code: codes.InvalidArgument,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid effect",
|
||||||
|
req: &pb.CreatePolicyRuleRequest{
|
||||||
|
Priority: 1,
|
||||||
|
Description: "test",
|
||||||
|
Effect: "maybe",
|
||||||
|
Actions: []string{"registry:pull"},
|
||||||
|
},
|
||||||
|
code: codes.InvalidArgument,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no actions",
|
||||||
|
req: &pb.CreatePolicyRuleRequest{
|
||||||
|
Priority: 1,
|
||||||
|
Description: "test",
|
||||||
|
Effect: "allow",
|
||||||
|
},
|
||||||
|
code: codes.InvalidArgument,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid action",
|
||||||
|
req: &pb.CreatePolicyRuleRequest{
|
||||||
|
Priority: 1,
|
||||||
|
Description: "test",
|
||||||
|
Effect: "allow",
|
||||||
|
Actions: []string{"registry:fly"},
|
||||||
|
},
|
||||||
|
code: codes.InvalidArgument,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := client.CreatePolicyRule(adminCtx(), tt.req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != tt.code {
|
||||||
|
t.Fatalf("code: got %v, want %v", st.Code(), tt.code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPolicyRule(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewPolicyServiceClient(cc)
|
||||||
|
|
||||||
|
// Create a rule first.
|
||||||
|
created, err := client.CreatePolicyRule(adminCtx(), &pb.CreatePolicyRuleRequest{
|
||||||
|
Priority: 5,
|
||||||
|
Description: "test rule",
|
||||||
|
Effect: "deny",
|
||||||
|
Actions: []string{"registry:push"},
|
||||||
|
Enabled: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreatePolicyRule: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch it.
|
||||||
|
got, err := client.GetPolicyRule(adminCtx(), &pb.GetPolicyRuleRequest{Id: created.Id})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetPolicyRule: %v", err)
|
||||||
|
}
|
||||||
|
if got.Id != created.Id {
|
||||||
|
t.Fatalf("id: got %d, want %d", got.Id, created.Id)
|
||||||
|
}
|
||||||
|
if got.Effect != "deny" {
|
||||||
|
t.Fatalf("effect: got %q, want %q", got.Effect, "deny")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPolicyRuleNotFound(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewPolicyServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.GetPolicyRule(adminCtx(), &pb.GetPolicyRuleRequest{Id: 99999})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.NotFound {
|
||||||
|
t.Fatalf("code: got %v, want NotFound", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListPolicyRules(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewPolicyServiceClient(cc)
|
||||||
|
|
||||||
|
// Create two rules.
|
||||||
|
for i := range 2 {
|
||||||
|
_, err := client.CreatePolicyRule(adminCtx(), &pb.CreatePolicyRuleRequest{
|
||||||
|
Priority: int32(i + 1),
|
||||||
|
Description: "rule",
|
||||||
|
Effect: "allow",
|
||||||
|
Actions: []string{"registry:pull"},
|
||||||
|
Enabled: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreatePolicyRule %d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.ListPolicyRules(adminCtx(), &pb.ListPolicyRulesRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListPolicyRules: %v", err)
|
||||||
|
}
|
||||||
|
if len(resp.GetRules()) < 2 {
|
||||||
|
t.Fatalf("expected at least 2 rules, got %d", len(resp.Rules))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeletePolicyRule(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
reloader := &fakePolicyReloader{}
|
||||||
|
deps.Engine = reloader
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewPolicyServiceClient(cc)
|
||||||
|
|
||||||
|
// Create then delete.
|
||||||
|
created, err := client.CreatePolicyRule(adminCtx(), &pb.CreatePolicyRuleRequest{
|
||||||
|
Priority: 1,
|
||||||
|
Description: "to be deleted",
|
||||||
|
Effect: "allow",
|
||||||
|
Actions: []string{"registry:pull"},
|
||||||
|
Enabled: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreatePolicyRule: %v", err)
|
||||||
|
}
|
||||||
|
initialReloads := reloader.reloadCount
|
||||||
|
|
||||||
|
_, err = client.DeletePolicyRule(adminCtx(), &pb.DeletePolicyRuleRequest{Id: created.Id})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeletePolicyRule: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it was reloaded.
|
||||||
|
if reloader.reloadCount != initialReloads+1 {
|
||||||
|
t.Fatalf("reloadCount: got %d, want %d", reloader.reloadCount, initialReloads+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it's gone.
|
||||||
|
_, err = client.GetPolicyRule(adminCtx(), &pb.GetPolicyRuleRequest{Id: created.Id})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error after deletion")
|
||||||
|
}
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.NotFound {
|
||||||
|
t.Fatalf("code: got %v, want NotFound", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeletePolicyRuleNotFound(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewPolicyServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.DeletePolicyRule(adminCtx(), &pb.DeletePolicyRuleRequest{Id: 99999})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.NotFound {
|
||||||
|
t.Fatalf("code: got %v, want NotFound", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdatePolicyRule(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
reloader := &fakePolicyReloader{}
|
||||||
|
deps.Engine = reloader
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewPolicyServiceClient(cc)
|
||||||
|
|
||||||
|
// Create a rule.
|
||||||
|
created, err := client.CreatePolicyRule(adminCtx(), &pb.CreatePolicyRuleRequest{
|
||||||
|
Priority: 10,
|
||||||
|
Description: "original",
|
||||||
|
Effect: "allow",
|
||||||
|
Actions: []string{"registry:pull"},
|
||||||
|
Enabled: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreatePolicyRule: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update description.
|
||||||
|
updated, err := client.UpdatePolicyRule(adminCtx(), &pb.UpdatePolicyRuleRequest{
|
||||||
|
Id: created.Id,
|
||||||
|
Description: "updated description",
|
||||||
|
UpdateMask: []string{"description"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UpdatePolicyRule: %v", err)
|
||||||
|
}
|
||||||
|
if updated.Description != "updated description" {
|
||||||
|
t.Fatalf("description: got %q, want %q", updated.Description, "updated description")
|
||||||
|
}
|
||||||
|
// Effect should be unchanged.
|
||||||
|
if updated.Effect != "allow" {
|
||||||
|
t.Fatalf("effect: got %q, want %q", updated.Effect, "allow")
|
||||||
|
}
|
||||||
|
}
|
||||||
203
internal/grpcserver/registry.go
Normal file
203
internal/grpcserver/registry.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
pb "git.wntrmute.dev/kyle/mcr/gen/mcr/v1"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/auth"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/db"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/gc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registryService implements pb.RegistryServiceServer.
|
||||||
|
type registryService struct {
|
||||||
|
pb.UnimplementedRegistryServiceServer
|
||||||
|
db *db.DB
|
||||||
|
collector *gc.Collector
|
||||||
|
gcStatus *GCStatus
|
||||||
|
auditFn AuditFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *registryService) ListRepositories(_ context.Context, req *pb.ListRepositoriesRequest) (*pb.ListRepositoriesResponse, error) {
|
||||||
|
limit := int32(50)
|
||||||
|
offset := int32(0)
|
||||||
|
if req.GetPagination() != nil {
|
||||||
|
if req.Pagination.Limit > 0 {
|
||||||
|
limit = req.Pagination.Limit
|
||||||
|
}
|
||||||
|
if req.Pagination.Offset >= 0 {
|
||||||
|
offset = req.Pagination.Offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repos, err := s.db.ListRepositoriesWithMetadata(int(limit), int(offset))
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []*pb.RepositoryMetadata
|
||||||
|
for _, r := range repos {
|
||||||
|
result = append(result, &pb.RepositoryMetadata{
|
||||||
|
Name: r.Name,
|
||||||
|
TagCount: int32(r.TagCount), //nolint:gosec // tag count fits int32
|
||||||
|
ManifestCount: int32(r.ManifestCount), //nolint:gosec // manifest count fits int32
|
||||||
|
TotalSize: r.TotalSize,
|
||||||
|
CreatedAt: r.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.ListRepositoriesResponse{Repositories: result}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *registryService) GetRepository(_ context.Context, req *pb.GetRepositoryRequest) (*pb.GetRepositoryResponse, error) {
|
||||||
|
if req.GetName() == "" {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "repository name required")
|
||||||
|
}
|
||||||
|
|
||||||
|
detail, err := s.db.GetRepositoryDetail(req.Name)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, db.ErrRepoNotFound) {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "repository not found")
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &pb.GetRepositoryResponse{
|
||||||
|
Name: detail.Name,
|
||||||
|
TotalSize: detail.TotalSize,
|
||||||
|
CreatedAt: detail.CreatedAt,
|
||||||
|
}
|
||||||
|
for _, t := range detail.Tags {
|
||||||
|
resp.Tags = append(resp.Tags, &pb.TagInfo{
|
||||||
|
Name: t.Name,
|
||||||
|
Digest: t.Digest,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, m := range detail.Manifests {
|
||||||
|
resp.Manifests = append(resp.Manifests, &pb.ManifestInfo{
|
||||||
|
Digest: m.Digest,
|
||||||
|
MediaType: m.MediaType,
|
||||||
|
Size: m.Size,
|
||||||
|
CreatedAt: m.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *registryService) DeleteRepository(ctx context.Context, req *pb.DeleteRepositoryRequest) (*pb.DeleteRepositoryResponse, error) {
|
||||||
|
if req.GetName() == "" {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "repository name required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.DeleteRepository(req.Name); err != nil {
|
||||||
|
if errors.Is(err, db.ErrRepoNotFound) {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "repository not found")
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.auditFn != nil {
|
||||||
|
claims := auth.ClaimsFromContext(ctx)
|
||||||
|
actorID := ""
|
||||||
|
if claims != nil {
|
||||||
|
actorID = claims.Subject
|
||||||
|
}
|
||||||
|
s.auditFn("repo_deleted", actorID, req.Name, "", "", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.DeleteRepositoryResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *registryService) GarbageCollect(_ context.Context, _ *pb.GarbageCollectRequest) (*pb.GarbageCollectResponse, error) {
|
||||||
|
s.gcStatus.mu.Lock()
|
||||||
|
if s.gcStatus.running {
|
||||||
|
s.gcStatus.mu.Unlock()
|
||||||
|
return nil, status.Errorf(codes.AlreadyExists, "garbage collection already running")
|
||||||
|
}
|
||||||
|
s.gcStatus.running = true
|
||||||
|
s.gcStatus.mu.Unlock()
|
||||||
|
|
||||||
|
gcID := uuid.New().String()
|
||||||
|
|
||||||
|
// Run GC asynchronously like the REST handler. GC is a long-running
|
||||||
|
// background operation that must not be tied to the request context,
|
||||||
|
// so we intentionally use context.Background() inside runGC.
|
||||||
|
go s.runGC(gcID) //nolint:gosec // G118: GC must outlive the triggering RPC
|
||||||
|
|
||||||
|
return &pb.GarbageCollectResponse{Id: gcID}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runGC executes garbage collection in the background. It uses
|
||||||
|
// context.Background() because GC must not be cancelled when the
|
||||||
|
// triggering RPC completes.
|
||||||
|
func (s *registryService) runGC(gcID string) {
|
||||||
|
startedAt := time.Now().UTC().Format(time.RFC3339)
|
||||||
|
|
||||||
|
if s.auditFn != nil {
|
||||||
|
s.auditFn("gc_started", "", "", "", "", map[string]string{
|
||||||
|
"gc_id": gcID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var blobsRemoved int
|
||||||
|
var bytesFreed int64
|
||||||
|
var gcErr error
|
||||||
|
if s.collector != nil {
|
||||||
|
r, err := s.collector.Run(context.Background()) //nolint:gosec // GC is intentionally background, not request-scoped
|
||||||
|
if err != nil {
|
||||||
|
gcErr = err
|
||||||
|
}
|
||||||
|
if r != nil {
|
||||||
|
blobsRemoved = r.BlobsRemoved
|
||||||
|
bytesFreed = r.BytesFreed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completedAt := time.Now().UTC().Format(time.RFC3339)
|
||||||
|
|
||||||
|
s.gcStatus.mu.Lock()
|
||||||
|
s.gcStatus.running = false
|
||||||
|
s.gcStatus.lastRun = &gcLastRun{
|
||||||
|
StartedAt: startedAt,
|
||||||
|
CompletedAt: completedAt,
|
||||||
|
BlobsRemoved: blobsRemoved,
|
||||||
|
BytesFreed: bytesFreed,
|
||||||
|
}
|
||||||
|
s.gcStatus.mu.Unlock()
|
||||||
|
|
||||||
|
if s.auditFn != nil && gcErr == nil {
|
||||||
|
details := map[string]string{
|
||||||
|
"gc_id": gcID,
|
||||||
|
"blobs_removed": fmt.Sprintf("%d", blobsRemoved),
|
||||||
|
"bytes_freed": fmt.Sprintf("%d", bytesFreed),
|
||||||
|
}
|
||||||
|
s.auditFn("gc_completed", "", "", "", "", details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *registryService) GetGCStatus(_ context.Context, _ *pb.GetGCStatusRequest) (*pb.GetGCStatusResponse, error) {
|
||||||
|
s.gcStatus.mu.Lock()
|
||||||
|
resp := &pb.GetGCStatusResponse{
|
||||||
|
Running: s.gcStatus.running,
|
||||||
|
}
|
||||||
|
if s.gcStatus.lastRun != nil {
|
||||||
|
resp.LastRun = &pb.GCLastRun{
|
||||||
|
StartedAt: s.gcStatus.lastRun.StartedAt,
|
||||||
|
CompletedAt: s.gcStatus.lastRun.CompletedAt,
|
||||||
|
BlobsRemoved: int32(s.gcStatus.lastRun.BlobsRemoved), //nolint:gosec // blob count fits int32
|
||||||
|
BytesFreed: s.gcStatus.lastRun.BytesFreed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.gcStatus.mu.Unlock()
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
144
internal/grpcserver/registry_test.go
Normal file
144
internal/grpcserver/registry_test.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
pb "git.wntrmute.dev/kyle/mcr/gen/mcr/v1"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func adminDeps(t *testing.T) Deps {
|
||||||
|
t.Helper()
|
||||||
|
return Deps{
|
||||||
|
DB: openTestDB(t),
|
||||||
|
Validator: &fakeValidator{
|
||||||
|
claims: &auth.Claims{Subject: "admin-uuid", AccountType: "human", Roles: []string{"admin"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminCtx() context.Context {
|
||||||
|
return withAuth(context.Background(), "admin-token")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListRepositoriesEmpty(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewRegistryServiceClient(cc)
|
||||||
|
|
||||||
|
resp, err := client.ListRepositories(adminCtx(), &pb.ListRepositoriesRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListRepositories: %v", err)
|
||||||
|
}
|
||||||
|
if len(resp.GetRepositories()) != 0 {
|
||||||
|
t.Fatalf("expected 0 repos, got %d", len(resp.Repositories))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRepositoryNotFound(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewRegistryServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.GetRepository(adminCtx(), &pb.GetRepositoryRequest{Name: "nonexistent"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for nonexistent repo")
|
||||||
|
}
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.NotFound {
|
||||||
|
t.Fatalf("code: got %v, want NotFound", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRepositoryEmptyName(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewRegistryServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.GetRepository(adminCtx(), &pb.GetRepositoryRequest{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for empty name")
|
||||||
|
}
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.InvalidArgument {
|
||||||
|
t.Fatalf("code: got %v, want InvalidArgument", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRepositoryNotFound(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewRegistryServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.DeleteRepository(adminCtx(), &pb.DeleteRepositoryRequest{Name: "nonexistent"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for nonexistent repo")
|
||||||
|
}
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.NotFound {
|
||||||
|
t.Fatalf("code: got %v, want NotFound", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRepositoryEmptyName(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewRegistryServiceClient(cc)
|
||||||
|
|
||||||
|
_, err := client.DeleteRepository(adminCtx(), &pb.DeleteRepositoryRequest{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for empty name")
|
||||||
|
}
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected gRPC status, got %v", err)
|
||||||
|
}
|
||||||
|
if st.Code() != codes.InvalidArgument {
|
||||||
|
t.Fatalf("code: got %v, want InvalidArgument", st.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGCStatusInitial(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewRegistryServiceClient(cc)
|
||||||
|
|
||||||
|
resp, err := client.GetGCStatus(adminCtx(), &pb.GetGCStatusRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetGCStatus: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Running {
|
||||||
|
t.Fatal("expected running=false on startup")
|
||||||
|
}
|
||||||
|
if resp.LastRun != nil {
|
||||||
|
t.Fatal("expected no last_run on startup")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGarbageCollectTrigger(t *testing.T) {
|
||||||
|
deps := adminDeps(t)
|
||||||
|
cc := startTestServer(t, deps)
|
||||||
|
client := pb.NewRegistryServiceClient(cc)
|
||||||
|
|
||||||
|
// Trigger GC without a collector (no-op but should return an ID).
|
||||||
|
resp, err := client.GarbageCollect(adminCtx(), &pb.GarbageCollectRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GarbageCollect: %v", err)
|
||||||
|
}
|
||||||
|
if resp.GetId() == "" {
|
||||||
|
t.Fatal("expected non-empty GC ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
141
internal/grpcserver/server.go
Normal file
141
internal/grpcserver/server.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// Package grpcserver implements the MCR gRPC admin API server.
|
||||||
|
//
|
||||||
|
// It provides the same business logic as the REST admin API in
|
||||||
|
// internal/server/, using shared internal/db and internal/gc packages.
|
||||||
|
// The server enforces TLS 1.3 minimum, auth via MCIAS token validation,
|
||||||
|
// and admin role checks on privileged RPCs.
|
||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
|
||||||
|
pb "git.wntrmute.dev/kyle/mcr/gen/mcr/v1"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/db"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/gc"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/policy"
|
||||||
|
"git.wntrmute.dev/kyle/mcr/internal/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuditFunc is a callback for recording audit events. It follows the same
|
||||||
|
// signature as db.WriteAuditEvent but without an error return -- audit
|
||||||
|
// failures should not block request processing.
|
||||||
|
type AuditFunc func(eventType, actorID, repository, digest, ip string, details map[string]string)
|
||||||
|
|
||||||
|
// Deps holds the dependencies injected into the gRPC server.
|
||||||
|
type Deps struct {
|
||||||
|
DB *db.DB
|
||||||
|
Validator server.TokenValidator
|
||||||
|
Engine PolicyReloader
|
||||||
|
AuditFn AuditFunc
|
||||||
|
Collector *gc.Collector
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicyReloader can reload policy rules from a store.
|
||||||
|
type PolicyReloader interface {
|
||||||
|
Reload(store policy.RuleStore) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GCStatus tracks the current state of garbage collection for the gRPC server.
|
||||||
|
type GCStatus struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
running bool
|
||||||
|
lastRun *gcLastRun
|
||||||
|
}
|
||||||
|
|
||||||
|
type gcLastRun struct {
|
||||||
|
StartedAt string
|
||||||
|
CompletedAt string
|
||||||
|
BlobsRemoved int
|
||||||
|
BytesFreed int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server wraps a grpc.Server with MCR-specific configuration.
|
||||||
|
type Server struct {
|
||||||
|
gs *grpc.Server
|
||||||
|
deps Deps
|
||||||
|
gcStatus *GCStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a configured gRPC server with the interceptor chain:
|
||||||
|
// [Request Logger] -> [Auth Interceptor] -> [Admin Interceptor] -> [Handler]
|
||||||
|
//
|
||||||
|
// The TLS config enforces TLS 1.3 minimum. If certFile or keyFile is
|
||||||
|
// empty, the server is created without TLS (for testing only).
|
||||||
|
func New(certFile, keyFile string, deps Deps) (*Server, error) {
|
||||||
|
authInt := newAuthInterceptor(deps.Validator)
|
||||||
|
adminInt := newAdminInterceptor()
|
||||||
|
|
||||||
|
chain := grpc.ChainUnaryInterceptor(
|
||||||
|
loggingInterceptor,
|
||||||
|
authInt.unary,
|
||||||
|
adminInt.unary,
|
||||||
|
)
|
||||||
|
|
||||||
|
var opts []grpc.ServerOption
|
||||||
|
opts = append(opts, chain)
|
||||||
|
|
||||||
|
// Configure TLS if cert and key are provided.
|
||||||
|
if certFile != "" && keyFile != "" {
|
||||||
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("grpcserver: load TLS cert: %w", err)
|
||||||
|
}
|
||||||
|
tlsCfg := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
MinVersion: tls.VersionTLS13,
|
||||||
|
}
|
||||||
|
opts = append(opts, grpc.Creds(credentials.NewTLS(tlsCfg)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The JSON codec is registered globally via init() in gen/mcr/v1/codec.go.
|
||||||
|
// The client must use grpc.ForceCodecV2(mcrv1.JSONCodec{}) to match.
|
||||||
|
_ = pb.JSONCodec{} // ensure the gen/mcr/v1 init() runs (codec registration)
|
||||||
|
|
||||||
|
gs := grpc.NewServer(opts...)
|
||||||
|
|
||||||
|
gcStatus := &GCStatus{}
|
||||||
|
|
||||||
|
s := &Server{gs: gs, deps: deps, gcStatus: gcStatus}
|
||||||
|
|
||||||
|
// Register all services.
|
||||||
|
pb.RegisterRegistryServiceServer(gs, ®istryService{
|
||||||
|
db: deps.DB,
|
||||||
|
collector: deps.Collector,
|
||||||
|
gcStatus: gcStatus,
|
||||||
|
auditFn: deps.AuditFn,
|
||||||
|
})
|
||||||
|
pb.RegisterPolicyServiceServer(gs, &policyService{
|
||||||
|
db: deps.DB,
|
||||||
|
engine: deps.Engine,
|
||||||
|
auditFn: deps.AuditFn,
|
||||||
|
})
|
||||||
|
pb.RegisterAuditServiceServer(gs, &auditService{
|
||||||
|
db: deps.DB,
|
||||||
|
})
|
||||||
|
pb.RegisterAdminServiceServer(gs, &adminService{})
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve starts the gRPC server on the given listener.
|
||||||
|
func (s *Server) Serve(lis net.Listener) error {
|
||||||
|
log.Printf("grpc server listening on %s", lis.Addr())
|
||||||
|
return s.gs.Serve(lis)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GracefulStop gracefully stops the gRPC server.
|
||||||
|
func (s *Server) GracefulStop() {
|
||||||
|
s.gs.GracefulStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GRPCServer returns the underlying grpc.Server for testing.
|
||||||
|
func (s *Server) GRPCServer() *grpc.Server {
|
||||||
|
return s.gs
|
||||||
|
}
|
||||||
15
proto/mcr/v1/admin.proto
Normal file
15
proto/mcr/v1/admin.proto
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package mcr.v1;
|
||||||
|
|
||||||
|
option go_package = "git.wntrmute.dev/kyle/mcr/gen/mcr/v1;mcrv1";
|
||||||
|
|
||||||
|
service AdminService {
|
||||||
|
rpc Health(HealthRequest) returns (HealthResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message HealthRequest {}
|
||||||
|
|
||||||
|
message HealthResponse {
|
||||||
|
string status = 1;
|
||||||
|
}
|
||||||
35
proto/mcr/v1/audit.proto
Normal file
35
proto/mcr/v1/audit.proto
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package mcr.v1;
|
||||||
|
|
||||||
|
option go_package = "git.wntrmute.dev/kyle/mcr/gen/mcr/v1;mcrv1";
|
||||||
|
|
||||||
|
import "mcr/v1/common.proto";
|
||||||
|
|
||||||
|
service AuditService {
|
||||||
|
rpc ListAuditEvents(ListAuditEventsRequest) returns (ListAuditEventsResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message AuditEvent {
|
||||||
|
int64 id = 1;
|
||||||
|
string event_time = 2;
|
||||||
|
string event_type = 3;
|
||||||
|
string actor_id = 4;
|
||||||
|
string repository = 5;
|
||||||
|
string digest = 6;
|
||||||
|
string ip_address = 7;
|
||||||
|
map<string, string> details = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListAuditEventsRequest {
|
||||||
|
PaginationRequest pagination = 1;
|
||||||
|
string event_type = 2;
|
||||||
|
string actor_id = 3;
|
||||||
|
string repository = 4;
|
||||||
|
string since = 5;
|
||||||
|
string until = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListAuditEventsResponse {
|
||||||
|
repeated AuditEvent events = 1;
|
||||||
|
}
|
||||||
11
proto/mcr/v1/common.proto
Normal file
11
proto/mcr/v1/common.proto
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package mcr.v1;
|
||||||
|
|
||||||
|
option go_package = "git.wntrmute.dev/kyle/mcr/gen/mcr/v1;mcrv1";
|
||||||
|
|
||||||
|
// Pagination controls for list RPCs.
|
||||||
|
message PaginationRequest {
|
||||||
|
int32 limit = 1;
|
||||||
|
int32 offset = 2;
|
||||||
|
}
|
||||||
76
proto/mcr/v1/policy.proto
Normal file
76
proto/mcr/v1/policy.proto
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package mcr.v1;
|
||||||
|
|
||||||
|
option go_package = "git.wntrmute.dev/kyle/mcr/gen/mcr/v1;mcrv1";
|
||||||
|
|
||||||
|
import "mcr/v1/common.proto";
|
||||||
|
|
||||||
|
service PolicyService {
|
||||||
|
rpc ListPolicyRules(ListPolicyRulesRequest) returns (ListPolicyRulesResponse);
|
||||||
|
rpc CreatePolicyRule(CreatePolicyRuleRequest) returns (PolicyRule);
|
||||||
|
rpc GetPolicyRule(GetPolicyRuleRequest) returns (PolicyRule);
|
||||||
|
rpc UpdatePolicyRule(UpdatePolicyRuleRequest) returns (PolicyRule);
|
||||||
|
rpc DeletePolicyRule(DeletePolicyRuleRequest) returns (DeletePolicyRuleResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message PolicyRule {
|
||||||
|
int64 id = 1;
|
||||||
|
int32 priority = 2;
|
||||||
|
string description = 3;
|
||||||
|
string effect = 4;
|
||||||
|
repeated string roles = 5;
|
||||||
|
repeated string account_types = 6;
|
||||||
|
string subject_uuid = 7;
|
||||||
|
repeated string actions = 8;
|
||||||
|
repeated string repositories = 9;
|
||||||
|
bool enabled = 10;
|
||||||
|
string created_by = 11;
|
||||||
|
string created_at = 12;
|
||||||
|
string updated_at = 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListPolicyRulesRequest {
|
||||||
|
PaginationRequest pagination = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListPolicyRulesResponse {
|
||||||
|
repeated PolicyRule rules = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreatePolicyRuleRequest {
|
||||||
|
int32 priority = 1;
|
||||||
|
string description = 2;
|
||||||
|
string effect = 3;
|
||||||
|
repeated string roles = 4;
|
||||||
|
repeated string account_types = 5;
|
||||||
|
string subject_uuid = 6;
|
||||||
|
repeated string actions = 7;
|
||||||
|
repeated string repositories = 8;
|
||||||
|
bool enabled = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPolicyRuleRequest {
|
||||||
|
int64 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdatePolicyRuleRequest {
|
||||||
|
int64 id = 1;
|
||||||
|
int32 priority = 2;
|
||||||
|
string description = 3;
|
||||||
|
string effect = 4;
|
||||||
|
repeated string roles = 5;
|
||||||
|
repeated string account_types = 6;
|
||||||
|
string subject_uuid = 7;
|
||||||
|
repeated string actions = 8;
|
||||||
|
repeated string repositories = 9;
|
||||||
|
bool enabled = 10;
|
||||||
|
// Field mask for partial updates — only fields listed here are applied.
|
||||||
|
repeated string update_mask = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeletePolicyRuleRequest {
|
||||||
|
int64 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeletePolicyRuleResponse {}
|
||||||
81
proto/mcr/v1/registry.proto
Normal file
81
proto/mcr/v1/registry.proto
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package mcr.v1;
|
||||||
|
|
||||||
|
option go_package = "git.wntrmute.dev/kyle/mcr/gen/mcr/v1;mcrv1";
|
||||||
|
|
||||||
|
import "mcr/v1/common.proto";
|
||||||
|
|
||||||
|
service RegistryService {
|
||||||
|
rpc ListRepositories(ListRepositoriesRequest) returns (ListRepositoriesResponse);
|
||||||
|
rpc GetRepository(GetRepositoryRequest) returns (GetRepositoryResponse);
|
||||||
|
rpc DeleteRepository(DeleteRepositoryRequest) returns (DeleteRepositoryResponse);
|
||||||
|
rpc GarbageCollect(GarbageCollectRequest) returns (GarbageCollectResponse);
|
||||||
|
rpc GetGCStatus(GetGCStatusRequest) returns (GetGCStatusResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message RepositoryMetadata {
|
||||||
|
string name = 1;
|
||||||
|
int32 tag_count = 2;
|
||||||
|
int32 manifest_count = 3;
|
||||||
|
int64 total_size = 4;
|
||||||
|
string created_at = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TagInfo {
|
||||||
|
string name = 1;
|
||||||
|
string digest = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ManifestInfo {
|
||||||
|
string digest = 1;
|
||||||
|
string media_type = 2;
|
||||||
|
int64 size = 3;
|
||||||
|
string created_at = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListRepositoriesRequest {
|
||||||
|
PaginationRequest pagination = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListRepositoriesResponse {
|
||||||
|
repeated RepositoryMetadata repositories = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetRepositoryRequest {
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetRepositoryResponse {
|
||||||
|
string name = 1;
|
||||||
|
repeated TagInfo tags = 2;
|
||||||
|
repeated ManifestInfo manifests = 3;
|
||||||
|
int64 total_size = 4;
|
||||||
|
string created_at = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteRepositoryRequest {
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteRepositoryResponse {}
|
||||||
|
|
||||||
|
message GarbageCollectRequest {}
|
||||||
|
|
||||||
|
message GarbageCollectResponse {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetGCStatusRequest {}
|
||||||
|
|
||||||
|
message GCLastRun {
|
||||||
|
string started_at = 1;
|
||||||
|
string completed_at = 2;
|
||||||
|
int32 blobs_removed = 3;
|
||||||
|
int64 bytes_freed = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetGCStatusResponse {
|
||||||
|
bool running = 1;
|
||||||
|
GCLastRun last_run = 2;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user