diff --git a/README.md b/README.md index ba12466e..c0844132 100644 --- a/README.md +++ b/README.md @@ -332,6 +332,22 @@ func loadConfig(ctx context.Context, client *github.Client, owner, repo, ref str } ``` +To load a specific remote file without discovery or remote-reference parsing, use +`LoadRemoteRef`. Missing files are errors; existing empty files retain their +source metadata. + +```go +c, err := loader.LoadRemoteRef(ctx, client, clientOwner, appconfig.RemoteRef{ + Remote: "example/shared-config", + Path: "config/app.yml", + Ref: "main", +}) +``` + +`clientOwner` must be the supplied client's original installation owner, even if +an earlier load returned a file owned by another organization. Using another +owner's installation requires `appconfig.WithPrivateRemotes`. + ## OAuth2 The `oauth2` package provides an `http.Handler` implementation that simplifies diff --git a/appconfig/appconfig.go b/appconfig/appconfig.go index f9e9f1e0..9947ab98 100644 --- a/appconfig/appconfig.go +++ b/appconfig/appconfig.go @@ -163,6 +163,16 @@ func (ld *Loader) LoadConfig(ctx context.Context, client *github.Client, owner, return Config{}, nil } +// LoadRemoteRef loads the file identified by remote. Unlike LoadConfig, it +// does not follow remote references, try other paths, or fall back to owner +// defaults. A missing file is an error. +// +// clientOwner is the owner of the installation for that client. +// It determines whether WithPrivateRemotes selects a different installation. +func (ld *Loader) LoadRemoteRef(ctx context.Context, client *github.Client, clientOwner string, remote RemoteRef) (Config, error) { + return ld.loadRemoteConfig(ctx, client, clientOwner, remote, Config{}) +} + func (ld *Loader) loadRemoteConfig(ctx context.Context, client *github.Client, sourceOwner string, remote RemoteRef, c Config) (Config, error) { logger := zerolog.Ctx(ctx) notFoundErr := fmt.Errorf("invalid remote reference: file does not exist") diff --git a/appconfig/appconfig_test.go b/appconfig/appconfig_test.go index 2fa9c20a..031fa379 100644 --- a/appconfig/appconfig_test.go +++ b/appconfig/appconfig_test.go @@ -179,6 +179,72 @@ func TestLoadConfig(t *testing.T) { } } +func TestLoadRemoteRef(t *testing.T) { + tests := map[string]struct { + Remote RemoteRef + Expected Config + Error string + }{ + "literalContent": { + Remote: RemoteRef{Remote: "test/remote-ref", Path: ".github/test-app.yml", Ref: TestRef}, + Expected: Config{ + Content: []byte("remote: remote/config\npath: config/test-app.yml\nref: develop\n"), + Source: "test/remote-ref@develop", + Path: ".github/test-app.yml", + IsRemote: true, + }, + }, + "missingFile": { + Remote: RemoteRef{Remote: "test/local-file", Path: ".github/test-app.v2.yml", Ref: TestRef}, + Error: "invalid remote reference: file does not exist", + }, + "defaultBranch": { + Remote: RemoteRef{Remote: "remote/config", Path: "config/test-app.yml"}, + Expected: Config{ + Content: []byte("message: hello\n"), + Source: "remote/config@main", + Path: "config/test-app.yml", + IsRemote: true, + }, + }, + } + + ctx := context.Background() + client := makeTestClient() + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ld := NewLoader([]string{".github/test-app.v2.yml", ".github/test-app.yml"}) + cfg, err := ld.LoadRemoteRef(ctx, client, TestOwner, test.Remote) + if test.Error != "" { + if err == nil { + t.Fatal("expected error loading config, but got nil") + } + if err.Error() != test.Error { + t.Errorf("incorrect error: expected: %q, actual: %q", test.Error, err) + } + return + } + if err != nil { + t.Fatalf("unexpected error loading config: %v", err) + } + + if test.Expected.Source != cfg.Source { + t.Errorf("incorrect source: expected: %q, actual: %q", test.Expected.Source, cfg.Source) + } + if test.Expected.Path != cfg.Path { + t.Errorf("incorrect path: expected: %q, actual: %q", test.Expected.Path, cfg.Path) + } + if test.Expected.IsRemote != cfg.IsRemote { + t.Errorf("incorrect remote flag: expected: %t, actual: %t", test.Expected.IsRemote, cfg.IsRemote) + } + if !bytes.Equal(test.Expected.Content, cfg.Content) { + t.Errorf("incorrect content\nexpected: %s\n actual: %s", test.Expected.Content, cfg.Content) + } + }) + } +} + func TestLoadConfigWithPrivateRemotes(t *testing.T) { newClient := func(rp *ResponsePlayer) *github.Client { client, _ := github.NewClient(github.WithHTTPClient(&http.Client{Transport: rp})) diff --git a/appconfig/options.go b/appconfig/options.go index 7285d443..508d43a0 100644 --- a/appconfig/options.go +++ b/appconfig/options.go @@ -21,7 +21,8 @@ import ( type Option func(*Loader) // WithRemoteRefParser sets the parser for encoded RemoteRefs. The default -// parser uses YAML. Set a nil parser to disable remote references. +// parser uses YAML. Set a nil parser to disable automatic remote references in +// LoadConfig. func WithRemoteRefParser(parser RemoteRefParser) Option { return func(ld *Loader) { ld.parser = parser