Skip to content

Commit dcdfa09

Browse files
Implement APIs for read server
This allows for deployers to serve read traffic without a separate CDN/proxy. This is discouraged for GCP, since it's more costly to serve read traffic in terms of egress costs, but can be used for other backends and for local testing. Signed-off-by: Hayden B <[email protected]>
1 parent ec16bc7 commit dcdfa09

File tree

12 files changed

+416
-27
lines changed

12 files changed

+416
-27
lines changed

cmd/rekor-server/app/serve.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,12 @@ var serveCmd = &cobra.Command{
112112
os.Exit(1)
113113
}
114114

115-
rekorServer := server.NewServer(tesseraStorage, readOnly, algorithmRegistry)
115+
var rekorServer server.RekorServer
116+
if viper.GetBool("serve-read-paths") {
117+
rekorServer = server.NewReadServer(tesseraStorage, readOnly, algorithmRegistry)
118+
} else {
119+
rekorServer = server.NewServer(tesseraStorage, readOnly, algorithmRegistry)
120+
}
116121

117122
server.Serve(
118123
ctx,
@@ -147,6 +152,7 @@ func init() {
147152
serveCmd.Flags().String("grpc-address", "127.0.0.1", "GRPC address to bind to")
148153
serveCmd.Flags().Duration("timeout", 60*time.Second, "timeout")
149154
serveCmd.Flags().Int("max-request-body-size", 4*1024*1024, "maximum request body size in bytes")
155+
serveCmd.Flags().Bool("serve-read-paths", false, "whether to serve the read paths /checkpoint and /tile from the filesystem, as an alternative to a standalone read traffic server")
150156

151157
// hostname
152158
hostname, err := os.Hostname()

compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
services:
1717
spanner:
1818
image: gcr.io/cloud-spanner-emulator/emulator:1.5.33@sha256:b211c813058e95bbdabfcb5dc78de0deb2d8a51e071532b2e90485fc6ec3a877
19+
platform: linux/amd64
1920
gcs:
2021
image: fsouza/fake-gcs-server:1.52.2@sha256:d47b4cf8b87006cab8fbbecfa5f06a2a3c5722e464abddc0d107729663d40ec4
2122
volumes:
@@ -62,6 +63,7 @@ services:
6263
- "--gcp-spanner=projects/rekor-tiles-e2e/instances/rekor-tiles/databases/sequencer"
6364
- "--signer-filepath=/pki/ed25519-priv-key.pem"
6465
- "--checkpoint-interval=2s"
66+
- "--serve-read-paths=true"
6567
ports:
6668
- "3003:3000" # http port
6769
- "3001:3001" # grpc port

pkg/server/grpc.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ type grpcServer struct {
5252
serverEndpoint string
5353
}
5454

55-
// newGRPCServer starts a new grpc server and registers the services.
56-
func newGRPCServer(config *GRPCConfig, server rekorServer) *grpcServer {
55+
// newGRPCServer starts a new gRPC server and registers the services
56+
func newGRPCServer(config *GRPCConfig, server RekorServer) *grpcServer {
5757
var opts []grpc.ServerOption
5858

5959
grpcPanicRecoveryHandler := func(p any) (err error) {

pkg/server/grpc_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func TestServe_grpcSmoke(t *testing.T) {
3838
server.Start(t)
3939
defer server.Stop(t)
4040

41-
// check if we can hit grpc endpoints
41+
// check if we can hit gRPC endpoints
4242
conn, err := grpc.NewClient(
4343
server.gc.GRPCTarget(),
4444
grpc.WithTransportCredentials(insecure.NewCredentials()))

pkg/server/http.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,15 @@ import (
4646
const (
4747
httpStatusCodeHeader = "x-http-code"
4848
httpErrorMessageHeader = "x-http-error-message"
49+
httpCacheControlHeader = "x-cache-control"
4950
)
5051

5152
type httpProxy struct {
5253
*http.Server
5354
serverEndpoint string
5455
}
5556

56-
// newHTTProxy creates a mux for each of the service grpc methods, including the grpc heatlhcheck.
57+
// newHTTProxy creates a mux for each of the service grpc methods, including the gRPC heatlhcheck.
5758
func newHTTPProxy(ctx context.Context, config *HTTPConfig, grpcServer *grpcServer) *httpProxy {
5859
// configure a custom marshaler to fail on unknown fields
5960
strictMarshaler := runtime.HTTPBodyMarshaler{
@@ -197,7 +198,13 @@ func httpResponseModifier(ctx context.Context, w http.ResponseWriter, _ proto.Me
197198
}
198199
}
199200

200-
// set http status code
201+
// set cache control
202+
if vals := md.HeaderMD.Get(httpCacheControlHeader); len(vals) > 0 {
203+
delete(md.HeaderMD, httpCacheControlHeader)
204+
w.Header().Set("Cache-Control", vals[0])
205+
}
206+
207+
// set HTTP status code
201208
if vals := md.HeaderMD.Get(httpStatusCodeHeader); len(vals) > 0 {
202209
code, err := strconv.Atoi(vals[0])
203210
if err != nil {

pkg/server/serve.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import (
2121
"sync"
2222
)
2323

24-
// Serve starts the grpc server and its http proxy.
25-
func Serve(ctx context.Context, hc *HTTPConfig, gc *GRPCConfig, s rekorServer, tesseraShutdownFn func(context.Context) error) {
24+
// Serve starts the gRPC server and HTTP proxy
25+
func Serve(ctx context.Context, hc *HTTPConfig, gc *GRPCConfig, s RekorServer, tesseraShutdownFn func(context.Context) error) {
2626
var wg sync.WaitGroup
2727

2828
if hc.port == 0 || gc.port == 0 {

pkg/server/service.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ import (
3939
"google.golang.org/protobuf/types/known/emptypb"
4040
)
4141

42-
// rekorServer is the collection of methods that our grpc server must implement.
43-
type rekorServer interface {
42+
// RekorServer is the collection of methods that the gRPC server must implement
43+
type RekorServer interface {
4444
pb.RekorServer
4545
grpc_health_v1.HealthServer
4646
}
4747

48+
// Server implements the write path and default healthcheck for all storage backends
4849
type Server struct {
4950
pb.UnimplementedRekorServer
5051
grpc_health_v1.UnimplementedHealthServer

pkg/server/service_read.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2025 The Sigstore Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package server
16+
17+
import (
18+
"context"
19+
"errors"
20+
"os"
21+
"strconv"
22+
23+
pb "github.com/sigstore/rekor-tiles/pkg/generated/protobuf"
24+
"github.com/sigstore/rekor-tiles/pkg/tessera"
25+
"github.com/sigstore/sigstore/pkg/signature"
26+
"github.com/transparency-dev/trillian-tessera/api/layout"
27+
"google.golang.org/genproto/googleapis/api/httpbody"
28+
"google.golang.org/grpc"
29+
"google.golang.org/grpc/codes"
30+
"google.golang.org/grpc/metadata"
31+
"google.golang.org/grpc/status"
32+
"google.golang.org/protobuf/types/known/emptypb"
33+
)
34+
35+
// ReadServer implements the read APIs along with the write APIs
36+
type ReadServer struct {
37+
Server
38+
}
39+
40+
func NewReadServer(storage tessera.Storage, readOnly bool, algorithmRegistry *signature.AlgorithmRegistryConfig) *ReadServer {
41+
if readOnly {
42+
return &ReadServer{
43+
Server: Server{
44+
readOnly: readOnly,
45+
storage: storage,
46+
},
47+
}
48+
}
49+
return &ReadServer{
50+
Server: Server{
51+
storage: storage,
52+
algorithmRegistry: algorithmRegistry,
53+
},
54+
}
55+
}
56+
57+
func (s *ReadServer) GetTile(ctx context.Context, req *pb.TileRequest) (*httpbody.HttpBody, error) {
58+
// Verifies and parses level, index, and optional width for partial tile
59+
l, i, w, err := layout.ParseTileLevelIndexPartial(strconv.FormatUint(uint64(req.L), 10), req.N)
60+
if err != nil {
61+
return nil, status.Error(codes.InvalidArgument, "invalid level, index and optional width")
62+
}
63+
tile, err := s.storage.ReadTile(ctx, l, i, w)
64+
if err != nil {
65+
return nil, status.Error(codes.Unknown, "failed to read tile")
66+
}
67+
_ = grpc.SetHeader(ctx, metadata.Pairs(httpCacheControlHeader, "max-age=31536000, immutable"))
68+
return &httpbody.HttpBody{
69+
ContentType: "application/octet-stream",
70+
Data: tile,
71+
}, nil
72+
}
73+
74+
func (s *ReadServer) GetEntryBundle(ctx context.Context, req *pb.EntryBundleRequest) (*httpbody.HttpBody, error) {
75+
// Parses index and optional width for partial tile
76+
i, w, err := layout.ParseTileIndexPartial(req.N)
77+
if err != nil {
78+
return nil, status.Error(codes.InvalidArgument, "invalid index and optional width")
79+
}
80+
entryBundle, err := s.storage.ReadEntryBundle(ctx, i, w)
81+
if err != nil {
82+
return nil, status.Error(codes.Unknown, "failed to read entry bundle")
83+
}
84+
_ = grpc.SetHeader(ctx, metadata.Pairs(httpCacheControlHeader, "max-age=31536000, immutable"))
85+
return &httpbody.HttpBody{
86+
ContentType: "application/octet-stream",
87+
Data: entryBundle,
88+
}, nil
89+
}
90+
91+
func (s *ReadServer) GetCheckpoint(ctx context.Context, _ *emptypb.Empty) (*httpbody.HttpBody, error) {
92+
checkpoint, err := s.storage.ReadCheckpoint(ctx)
93+
if err != nil {
94+
if errors.Is(err, os.ErrNotExist) {
95+
return nil, status.Error(codes.NotFound, "checkpoint does not exist")
96+
}
97+
return nil, status.Error(codes.Unknown, "failed to read checkpoint")
98+
}
99+
_ = grpc.SetHeader(ctx, metadata.Pairs(httpCacheControlHeader, "no-cache"))
100+
return &httpbody.HttpBody{
101+
ContentType: "text/plain; charset=utf-8",
102+
Data: checkpoint,
103+
}, nil
104+
}

0 commit comments

Comments
 (0)