Skip to content

Commit 0ae34e2

Browse files
authored
Add a bare-bones migrate tool for static-ct compliant logs (#189)
1 parent 26caaf5 commit 0ae34e2

File tree

4 files changed

+159
-4
lines changed

4 files changed

+159
-4
lines changed

cmd/experimental/migrate/gcp/main.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright 2025 The Tessera authors. All Rights Reserved.
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+
// migrate-gcp is a command-line tool for migrating data from a static-ct
16+
// compliant log, into a TesseraCT log instance.
17+
package main
18+
19+
import (
20+
"context"
21+
"encoding/base64"
22+
"flag"
23+
"fmt"
24+
"io"
25+
"net/http"
26+
"net/url"
27+
"strconv"
28+
"strings"
29+
30+
tessera "github.com/transparency-dev/trillian-tessera"
31+
"github.com/transparency-dev/trillian-tessera/api/layout"
32+
"github.com/transparency-dev/trillian-tessera/client"
33+
"github.com/transparency-dev/trillian-tessera/storage/gcp"
34+
gcp_as "github.com/transparency-dev/trillian-tessera/storage/gcp/antispam"
35+
"k8s.io/klog/v2"
36+
)
37+
38+
var (
39+
bucket = flag.String("bucket", "", "Bucket to use for storing log")
40+
spanner = flag.String("spanner", "", "Spanner resource URI ('projects/.../...')")
41+
42+
sourceURL = flag.String("source_url", "", "Base URL for the source log.")
43+
numWorkers = flag.Int("num_workers", 30, "Number of migration worker goroutines.")
44+
persistentAntispam = flag.Bool("antispam", false, "EXPERIMENTAL: Set to true to enable GCP-based persistent antispam storage")
45+
)
46+
47+
func main() {
48+
klog.InitFlags(nil)
49+
flag.Parse()
50+
ctx := context.Background()
51+
52+
srcURL, err := url.Parse(*sourceURL)
53+
if err != nil {
54+
klog.Exitf("Invalid --source_url %q: %v", *sourceURL, err)
55+
}
56+
// TODO(phbnf): This is currently built using the Tessera client lib, with a stand-alone func below for
57+
// fetching the Static CT entry bundles as they live in an different place.
58+
// When there's a Static CT client we can probably switch over to using it in here.
59+
src, err := client.NewHTTPFetcher(srcURL, nil)
60+
if err != nil {
61+
klog.Exitf("Failed to create HTTP fetcher: %v", err)
62+
}
63+
sourceCP, err := src.ReadCheckpoint(ctx)
64+
if err != nil {
65+
klog.Exitf("fetch initial source checkpoint: %v", err)
66+
}
67+
// TODO(AlCutter): We should be properly verifying and opening the checkpoint here with the source log's
68+
// public key.
69+
bits := strings.Split(string(sourceCP), "\n")
70+
sourceSize, err := strconv.ParseUint(bits[1], 10, 64)
71+
if err != nil {
72+
klog.Exitf("invalid CP size %q: %v", bits[1], err)
73+
}
74+
sourceRoot, err := base64.StdEncoding.DecodeString(bits[2])
75+
if err != nil {
76+
klog.Exitf("invalid checkpoint roothash %q: %v", bits[2], err)
77+
}
78+
79+
// Create our Tessera storage backend:
80+
gcpCfg := storageConfigFromFlags()
81+
driver, err := gcp.New(ctx, gcpCfg)
82+
if err != nil {
83+
klog.Exitf("Failed to create new GCP storage driver: %v", err)
84+
}
85+
86+
opts := tessera.NewMigrationOptions().WithCTLayout()
87+
// Configure antispam storage, if necessary
88+
var antispam tessera.Antispam
89+
// Persistent antispam is currently experimental, so there's no terraform or documentation yet!
90+
if *persistentAntispam {
91+
antispam, err = gcp_as.NewAntispam(ctx, fmt.Sprintf("%s-antispam", *spanner))
92+
if err != nil {
93+
klog.Exitf("Failed to create new GCP antispam storage: %v", err)
94+
}
95+
opts.WithAntispam(antispam)
96+
}
97+
98+
m, err := tessera.NewMigrationTarget(ctx, driver, opts)
99+
if err != nil {
100+
klog.Exitf("Failed to create MigrationTarget: %v", err)
101+
}
102+
103+
readEntryBundle := readCTEntryBundle(*sourceURL)
104+
if err := tessera.Migrate(context.Background(), *numWorkers, sourceSize, sourceRoot, readEntryBundle, m); err != nil {
105+
klog.Exitf("Migrate failed: %v", err)
106+
}
107+
108+
// TODO(phbnf): This will need extending to identify and copy over the entries from the intermediate cert storage.
109+
110+
// TODO(Tessera #341): wait for antispam follower to complete
111+
<-make(chan bool)
112+
}
113+
114+
// storageConfigFromFlags returns a gcp.Config struct populated with values
115+
// provided via flags.
116+
func storageConfigFromFlags() gcp.Config {
117+
if *bucket == "" {
118+
klog.Exit("--bucket must be set")
119+
}
120+
if *spanner == "" {
121+
klog.Exit("--spanner must be set")
122+
}
123+
return gcp.Config{
124+
Bucket: *bucket,
125+
Spanner: *spanner,
126+
}
127+
}
128+
129+
func readCTEntryBundle(srcURL string) func(ctx context.Context, i uint64, p uint8) ([]byte, error) {
130+
return func(ctx context.Context, i uint64, p uint8) ([]byte, error) {
131+
up := strings.Replace(layout.EntriesPath(i, p), "entries", "data", 1)
132+
reqURL, err := url.JoinPath(srcURL, up)
133+
if err != nil {
134+
return nil, err
135+
}
136+
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
137+
if err != nil {
138+
return nil, err
139+
}
140+
rsp, err := http.DefaultClient.Do(req)
141+
if err != nil {
142+
return nil, err
143+
}
144+
defer rsp.Body.Close()
145+
if rsp.StatusCode != http.StatusOK {
146+
return nil, fmt.Errorf("GET %q: %v", req.URL.Path, rsp.Status)
147+
}
148+
return io.ReadAll(rsp.Body)
149+
}
150+
}

cmd/gcp/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,9 @@ func newGCPStorage(ctx context.Context, signer note.Signer) (*storage.CTStorage,
178178
WithCheckpointSigner(signer).
179179
WithCTLayout()
180180

181-
appender, _, err := tessera.NewAppender(ctx, driver, opts)
181+
// TODO(phbnf): figure out the best way to thread the `shutdown` func NewAppends returns back out to main so we can cleanly close Tessera down
182+
// when it's time to exit.
183+
appender, _, _, err := tessera.NewAppender(ctx, driver, opts)
182184
if err != nil {
183185
return nil, fmt.Errorf("Failed to initialize GCP Tessera appender: %v", err)
184186
}

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ require (
1616
github.com/rivo/tview v0.0.0-20240625185742-b0a7293b8130
1717
github.com/transparency-dev/formats v0.0.0-20250127084410-134797944be6
1818
github.com/transparency-dev/merkle v0.0.2
19-
github.com/transparency-dev/trillian-tessera v0.1.1-0.20250311134629-ecabef06b8a1
19+
github.com/transparency-dev/trillian-tessera v0.1.1-0.20250314143707-b7c8fb6d4491
2020
go.etcd.io/bbolt v1.4.0
2121
golang.org/x/crypto v0.36.0
2222
golang.org/x/mod v0.24.0
@@ -40,6 +40,7 @@ require (
4040
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect
4141
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 // indirect
4242
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 // indirect
43+
github.com/avast/retry-go/v4 v4.6.1 // indirect
4344
github.com/beorn7/perks v1.0.1 // indirect
4445
github.com/cespare/xxhash/v2 v2.3.0 // indirect
4546
github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,8 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd
646646
github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
647647
github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI=
648648
github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
649+
github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk=
650+
github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA=
649651
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
650652
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
651653
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
@@ -974,8 +976,8 @@ github.com/transparency-dev/formats v0.0.0-20250127084410-134797944be6 h1:TVUG0R
974976
github.com/transparency-dev/formats v0.0.0-20250127084410-134797944be6/go.mod h1:tSjZBSQ1ZMxgaOMppnyw48SbTDL947PD/8KYbvrx+lE=
975977
github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4=
976978
github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A=
977-
github.com/transparency-dev/trillian-tessera v0.1.1-0.20250311134629-ecabef06b8a1 h1:BS5jBBOMaaLJAR7Nmyjva4M3W2txzgGBw1DyCm1CqcM=
978-
github.com/transparency-dev/trillian-tessera v0.1.1-0.20250311134629-ecabef06b8a1/go.mod h1:uvyZ7WGpaRDPY+4Lme+s1vEUOluYevTYzrDg9j05cYU=
979+
github.com/transparency-dev/trillian-tessera v0.1.1-0.20250314143707-b7c8fb6d4491 h1:HZg3ZJqnMJ2X+t+2jMP2GY1vXrES6R1YtADIURAxb0o=
980+
github.com/transparency-dev/trillian-tessera v0.1.1-0.20250314143707-b7c8fb6d4491/go.mod h1:uvyZ7WGpaRDPY+4Lme+s1vEUOluYevTYzrDg9j05cYU=
979981
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
980982
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
981983
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

0 commit comments

Comments
 (0)