Skip to content

Commit c78d8b8

Browse files
committed
save proto files in local cache
1 parent 2ffe949 commit c78d8b8

File tree

3 files changed

+94
-30
lines changed

3 files changed

+94
-30
lines changed

book/src/framework/components/chipingresset/chip_ingress.md

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,10 @@ if outErr != nil {
6060
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
6161
defer cancel()
6262

63-
// we recommend to use GITHUB_TOKEN with read access to repositories with protos to avoid heavy rate limiting
64-
var client *github.Client
65-
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
66-
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
67-
tc := oauth2.NewClient(ctx, ts)
68-
client = github.NewClient(tc)
69-
} else {
70-
client = github.NewClient(nil)
71-
}
72-
73-
protoErr := chipingressset.DefaultRegisterAndFetchProtos(ctx, client, []chipingressset.RepoConfiguration{
63+
protoErr := chipingressset.DefaultRegisterAndFetchProtos(
64+
ctx,
65+
nil, // GH client will be created dynamically, if needed
66+
[]chipingressset.RepoConfiguration{
7467
{
7568
URI: "https://github.com/smartcontractkit/chainlink-protostractkit",
7669
Ref: "626c42d55bdcb36dffe0077fff58abba40acc3e5",
@@ -123,4 +116,8 @@ for _, schemaSet := range configFiles {
123116

124117
Registration logic is very simple and should handle cases of protos that import other protos as long they are all available in the `ProtoSchemaSet`s provided to the registration function. That function uses an algorithm called "topological sorting by trail", which will try to register all protos in a loop until it cannot register any more protos or it has registered all of them. That allows us to skip dependency parsing completely.
125118

126-
Kafka doesn't have any automatic discoverability mechanism for subject - schema relationship (it has to be provided out-of-band). Currenly, we create the subject in the following way: <subject_prefix>.<package>.<1st-message-name>. Subject prefix is optional and if it's not present, then subject is equal to: <package>.<1st-message-name>. Only the first message in the `.proto` file is ever registered.
119+
Kafka doesn't have any automatic discoverability mechanism for subject - schema relationship (it has to be provided out-of-band). Currenly, we create the subject in the following way: <subject_prefix>.<package>.<1st-message-name>. Subject prefix is optional and if it's not present, then subject is equal to: <package>.<1st-message-name>. Only the first message in the `.proto` file is ever registered.
120+
121+
## Protobuf caching
122+
123+
Once fetched from `https://github.com` protobuf files will be saved in `.local/share/beholder/protobufs/<OWNER>/<REPOSTIORY>/<SHA>` folder and subsequently used. If saving to cache or reading from it fails, we will load files from the original source.

framework/components/dockercompose/chip_ingress_set/protos.go

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/google/go-github/v72/github"
1616
"github.com/pkg/errors"
1717
"github.com/smartcontractkit/chainlink-testing-framework/framework"
18+
"golang.org/x/oauth2"
1819
)
1920

2021
type protoFile struct {
@@ -83,8 +84,27 @@ func RegisterAndFetchProtos(ctx context.Context, client *github.Client, protoSch
8384
}
8485
}
8586

87+
ghClientFn := func() *github.Client {
88+
if client != nil {
89+
return client
90+
}
91+
92+
var client *github.Client
93+
94+
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
95+
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
96+
tc := oauth2.NewClient(ctx, ts)
97+
client = github.NewClient(tc)
98+
} else {
99+
framework.L.Warn().Msg("GITHUB_TOKEN is not set, using unauthenticated GitHub client. This may cause rate limiting issues when downloading proto files")
100+
client = github.NewClient(nil)
101+
}
102+
103+
return client
104+
}
105+
86106
for _, protoSchemaSet := range protoSchemaSets {
87-
protos, protosErr := fetchProtoFilesInFolders(ctx, client, protoSchemaSet.URI, protoSchemaSet.Ref, protoSchemaSet.Folders)
107+
protos, protosErr := fetchProtoFilesInFolders(ctx, ghClientFn, protoSchemaSet.URI, protoSchemaSet.Ref, protoSchemaSet.Folders)
88108
if protosErr != nil {
89109
return errors.Wrapf(protosErr, "failed to fetch protos from %s", protoSchemaSet.URI)
90110
}
@@ -167,7 +187,7 @@ func extractTopLevelMessageNamesWithRegex(protoSrc string) ([]string, error) {
167187
}
168188

169189
// Fetches .proto files from a GitHub repo optionally scoped to specific folders. It is recommended to use `*github.Client` with auth token to avoid rate limiting.
170-
func fetchProtoFilesInFolders(ctx context.Context, client *github.Client, uri, ref string, folders []string) ([]protoFile, error) {
190+
func fetchProtoFilesInFolders(ctx context.Context, clientFn func() *github.Client, uri, ref string, folders []string) ([]protoFile, error) {
171191
framework.L.Debug().Msgf("Fetching proto files from %s in folders: %s", uri, strings.Join(folders, ", "))
172192

173193
if strings.HasPrefix(uri, "file://") {
@@ -176,14 +196,20 @@ func fetchProtoFilesInFolders(ctx context.Context, client *github.Client, uri, r
176196

177197
parts := strings.Split(strings.TrimPrefix(uri, "https://"), "/")
178198

179-
return fetchProtosFromGithub(ctx, client, parts[1], parts[2], ref, folders)
199+
return fetchProtosFromGithub(ctx, clientFn, parts[1], parts[2], ref, folders)
180200
}
181201

182-
func fetchProtosFromGithub(ctx context.Context, client *github.Client, owner, repository, ref string, folders []string) ([]protoFile, error) {
183-
if client == nil {
184-
return nil, errors.New("github client cannot be nil")
202+
func fetchProtosFromGithub(ctx context.Context, clientFn func() *github.Client, owner, repository, ref string, folders []string) ([]protoFile, error) {
203+
cachedFiles, found, cacheErr := loadCachedProtoFiles(owner, repository, ref, folders)
204+
if cacheErr != nil {
205+
framework.L.Warn().Msgf("Failed to load cached proto files for %s/%s at ref %s: %v", owner, repository, ref, cacheErr)
206+
}
207+
if cacheErr == nil && found {
208+
framework.L.Debug().Msgf("Using cached proto files for %s/%s at ref %s", owner, repository, ref)
209+
return cachedFiles, nil
185210
}
186211

212+
client := clientFn()
187213
var files []protoFile
188214

189215
sha, shaErr := resolveRefSHA(ctx, client, owner, repository, ref)
@@ -255,9 +281,61 @@ searchLoop:
255281
return nil, fmt.Errorf("no proto files found in %s/%s in folders %s", owner, repository, strings.Join(folders, ", "))
256282
}
257283

284+
saveErr := saveProtoFilesToCache(owner, repository, ref, files)
285+
if saveErr != nil {
286+
framework.L.Warn().Msgf("Failed to save proto files to cache for %s/%s at ref %s: %v", owner, repository, ref, saveErr)
287+
}
288+
258289
return files, nil
259290
}
260291

292+
func loadCachedProtoFiles(owner, repository, ref string, _ []string) ([]protoFile, bool, error) {
293+
cachePath, cacheErr := cacheFilePath(owner, repository, ref)
294+
if cacheErr != nil {
295+
return nil, false, errors.Wrapf(cacheErr, "failed to get cache file path for %s/%s at ref %s", owner, repository, ref)
296+
}
297+
298+
if _, err := os.Stat(cachePath); os.IsNotExist(err) {
299+
return nil, false, nil // cache not found
300+
}
301+
302+
cachedFiles, cachedErr := fetchProtosFromFilesystem("file://"+cachePath, []string{}) // ignore folders since, we already filtered them when fetching from GitHub
303+
if cachedErr != nil {
304+
return nil, false, errors.Wrapf(cachedErr, "failed to load cached proto files from %s", cachePath)
305+
}
306+
307+
return cachedFiles, true, nil
308+
}
309+
310+
func saveProtoFilesToCache(owner, repository, ref string, files []protoFile) error {
311+
cachePath, cacheErr := cacheFilePath(owner, repository, ref)
312+
if cacheErr != nil {
313+
return errors.Wrapf(cacheErr, "failed to get cache file path for %s/%s at ref %s", owner, repository, ref)
314+
}
315+
316+
for _, file := range files {
317+
path := filepath.Join(cachePath, file.Path)
318+
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
319+
return errors.Wrapf(err, "failed to create directory for cache file %s", cachePath)
320+
}
321+
if writeErr := os.WriteFile(path, []byte(file.Content), 0755); writeErr != nil {
322+
return errors.Wrapf(writeErr, "failed to write cached proto files to %s", cachePath)
323+
}
324+
}
325+
326+
framework.L.Debug().Msgf("Saved %d proto files to cache at %s", len(files), cachePath)
327+
328+
return nil
329+
}
330+
331+
func cacheFilePath(owner, repository, ref string) (string, error) {
332+
homeDir, homeErr := os.UserHomeDir()
333+
if homeErr != nil {
334+
return "", errors.Wrap(homeErr, "failed to get user home directory")
335+
}
336+
return filepath.Join(homeDir, ".local", "share", "beholder", "protobufs", owner, repository, ref), nil
337+
}
338+
261339
func fetchProtosFromFilesystem(uri string, folders []string) ([]protoFile, error) {
262340
var files []protoFile
263341

framework/examples/myproject/smoke_chip_ingress_test.go

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@ import (
66
"testing"
77
"time"
88

9-
"github.com/google/go-github/v72/github"
109
"github.com/smartcontractkit/chainlink-testing-framework/framework"
1110
chipingressset "github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose/chip_ingress_set"
1211
"github.com/stretchr/testify/require"
13-
"golang.org/x/oauth2"
1412
)
1513

1614
type ChipConfig struct {
@@ -33,19 +31,10 @@ func TestChipIngressSmoke(t *testing.T) {
3331
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
3432
defer cancel()
3533

36-
var client *github.Client
37-
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
38-
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
39-
tc := oauth2.NewClient(ctx, ts)
40-
client = github.NewClient(tc)
41-
} else {
42-
client = github.NewClient(nil)
43-
}
44-
4534
createTopicsErr := chipingressset.CreateTopics(ctx, out.RedPanda.KafkaExternalURL, []string{"cre"})
4635
require.NoError(t, createTopicsErr, "failed to create topics")
4736

48-
err := chipingressset.DefaultRegisterAndFetchProtos(ctx, client, []chipingressset.ProtoSchemaSet{
37+
err := chipingressset.DefaultRegisterAndFetchProtos(ctx, nil, []chipingressset.ProtoSchemaSet{
4938
{
5039
URI: "https://github.com/smartcontractkit/chainlink-protos",
5140
Ref: "95decc005a91a1fd2621af9d9f00cb36d8061067",

0 commit comments

Comments
 (0)