Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion cmd/rekor-server/app/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,12 @@ var serveCmd = &cobra.Command{
os.Exit(1)
}

rekorServer := server.NewServer(tesseraStorage, readOnly, algorithmRegistry)
var rekorServer server.RekorServer
if viper.GetBool("serve-read-paths") {
rekorServer = server.NewReadServer(tesseraStorage, readOnly, algorithmRegistry)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to fix - This breaks with --read-only because tesseraStorage is nil but is still needed on the read path.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transparency-dev/tessera#625 to see if Tessera can add a function that would give us a LogReader without an appender

} else {
rekorServer = server.NewServer(tesseraStorage, readOnly, algorithmRegistry)
}

server.Serve(
ctx,
Expand Down Expand Up @@ -147,6 +152,7 @@ func init() {
serveCmd.Flags().String("grpc-address", "127.0.0.1", "GRPC address to bind to")
serveCmd.Flags().Duration("timeout", 60*time.Second, "timeout")
serveCmd.Flags().Int("max-request-body-size", 4*1024*1024, "maximum request body size in bytes")
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")

// hostname
hostname, err := os.Hostname()
Expand Down
2 changes: 2 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
services:
spanner:
image: gcr.io/cloud-spanner-emulator/emulator:1.5.33@sha256:b211c813058e95bbdabfcb5dc78de0deb2d8a51e071532b2e90485fc6ec3a877
platform: linux/amd64
gcs:
image: fsouza/fake-gcs-server:1.52.2@sha256:d47b4cf8b87006cab8fbbecfa5f06a2a3c5722e464abddc0d107729663d40ec4
volumes:
Expand Down Expand Up @@ -62,6 +63,7 @@ services:
- "--gcp-spanner=projects/rekor-tiles-e2e/instances/rekor-tiles/databases/sequencer"
- "--signer-filepath=/pki/ed25519-priv-key.pem"
- "--checkpoint-interval=2s"
- "--serve-read-paths=true"
ports:
- "3003:3000" # http port
- "3001:3001" # grpc port
Expand Down
4 changes: 2 additions & 2 deletions pkg/server/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ type grpcServer struct {
serverEndpoint string
}

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

grpcPanicRecoveryHandler := func(p any) (err error) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/server/grpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func TestServe_grpcSmoke(t *testing.T) {
server.Start(t)
defer server.Stop(t)

// check if we can hit grpc endpoints
// check if we can hit gRPC endpoints
conn, err := grpc.NewClient(
server.gc.GRPCTarget(),
grpc.WithTransportCredentials(insecure.NewCredentials()))
Expand Down
11 changes: 9 additions & 2 deletions pkg/server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,15 @@ import (
const (
httpStatusCodeHeader = "x-http-code"
httpErrorMessageHeader = "x-http-error-message"
httpCacheControlHeader = "x-cache-control"
)

type httpProxy struct {
*http.Server
serverEndpoint string
}

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

// set http status code
// set cache control
if vals := md.HeaderMD.Get(httpCacheControlHeader); len(vals) > 0 {
delete(md.HeaderMD, httpCacheControlHeader)
w.Header().Set("Cache-Control", vals[0])
}

// set HTTP status code
if vals := md.HeaderMD.Get(httpStatusCodeHeader); len(vals) > 0 {
code, err := strconv.Atoi(vals[0])
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions pkg/server/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import (
"sync"
)

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

if hc.port == 0 || gc.port == 0 {
Expand Down
5 changes: 3 additions & 2 deletions pkg/server/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ import (
"google.golang.org/protobuf/types/known/emptypb"
)

// rekorServer is the collection of methods that our grpc server must implement.
type rekorServer interface {
// RekorServer is the collection of methods that the gRPC server must implement
type RekorServer interface {
pb.RekorServer
grpc_health_v1.HealthServer
}

// Server implements the write path and default healthcheck for all storage backends
type Server struct {
pb.UnimplementedRekorServer
grpc_health_v1.UnimplementedHealthServer
Expand Down
104 changes: 104 additions & 0 deletions pkg/server/service_read.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2025 The Sigstore Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package server

import (
"context"
"errors"
"os"
"strconv"

pb "github.com/sigstore/rekor-tiles/pkg/generated/protobuf"
"github.com/sigstore/rekor-tiles/pkg/tessera"
"github.com/sigstore/sigstore/pkg/signature"
"github.com/transparency-dev/trillian-tessera/api/layout"
"google.golang.org/genproto/googleapis/api/httpbody"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)

// ReadServer implements the read APIs along with the write APIs
type ReadServer struct {
Server
}

func NewReadServer(storage tessera.Storage, readOnly bool, algorithmRegistry *signature.AlgorithmRegistryConfig) *ReadServer {
if readOnly {
return &ReadServer{
Server: Server{
readOnly: readOnly,
storage: storage,
},
}
}
return &ReadServer{
Server: Server{
storage: storage,
algorithmRegistry: algorithmRegistry,
},
}
}

func (s *ReadServer) GetTile(ctx context.Context, req *pb.TileRequest) (*httpbody.HttpBody, error) {
// Verifies and parses level, index, and optional width for partial tile
l, i, w, err := layout.ParseTileLevelIndexPartial(strconv.FormatUint(uint64(req.L), 10), req.N)
if err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid level, index and optional width")
}
tile, err := s.storage.ReadTile(ctx, l, i, w)
if err != nil {
return nil, status.Error(codes.Unknown, "failed to read tile")
}
_ = grpc.SetHeader(ctx, metadata.Pairs(httpCacheControlHeader, "max-age=31536000, immutable"))
return &httpbody.HttpBody{
ContentType: "application/octet-stream",
Data: tile,
}, nil
}

func (s *ReadServer) GetEntryBundle(ctx context.Context, req *pb.EntryBundleRequest) (*httpbody.HttpBody, error) {
// Parses index and optional width for partial tile
i, w, err := layout.ParseTileIndexPartial(req.N)
if err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid index and optional width")
}
entryBundle, err := s.storage.ReadEntryBundle(ctx, i, w)
if err != nil {
return nil, status.Error(codes.Unknown, "failed to read entry bundle")
}
_ = grpc.SetHeader(ctx, metadata.Pairs(httpCacheControlHeader, "max-age=31536000, immutable"))
return &httpbody.HttpBody{
ContentType: "application/octet-stream",
Data: entryBundle,
}, nil
}

func (s *ReadServer) GetCheckpoint(ctx context.Context, _ *emptypb.Empty) (*httpbody.HttpBody, error) {
checkpoint, err := s.storage.ReadCheckpoint(ctx)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, status.Error(codes.NotFound, "checkpoint does not exist")
}
return nil, status.Error(codes.Unknown, "failed to read checkpoint")
}
_ = grpc.SetHeader(ctx, metadata.Pairs(httpCacheControlHeader, "no-cache"))
return &httpbody.HttpBody{
ContentType: "text/plain; charset=utf-8",
Data: checkpoint,
}, nil
}
Loading