Skip to content

Commit e286cb0

Browse files
authored
feat: Add new gh client implementation (#3448)
* feat: Add new gh client implementation Signed-off-by: Steve Hipwell <steve.hipwell@gmail.com> * fixup! feat: Add new gh client implementation * fixup! feat: Add new gh client implementation * fixup! feat: Add new gh client implementation * fixup! feat: Add new gh client implementation * fixup! feat: Add new gh client implementation * fixup! feat: Add new gh client implementation * fixup! feat: Add new gh client implementation * fixup! feat: Add new gh client implementation * fixup! feat: Add new gh client implementation --------- Signed-off-by: Steve Hipwell <steve.hipwell@gmail.com>
1 parent 2f73c00 commit e286cb0

36 files changed

Lines changed: 1890 additions & 636 deletions

.github/workflows/dotcom-acceptance-tests.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ jobs:
109109
GITHUB_OWNER: ${{ case(matrix.mode == 'individual', vars.GH_TEST_LOGIN, matrix.mode == 'organization', vars.GH_TEST_ORG_NAME, '') }}
110110
GITHUB_USERNAME: ${{ vars.GH_TEST_LOGIN }}
111111
GITHUB_ENTERPRISE_SLUG: ${{ vars.GH_TEST_ENTERPRISE_SLUG }}
112+
GITHUB_LEGACY_CLIENT: "false"
112113
GH_TEST_AUTH_MODE: ${{ matrix.mode }}
113114
GH_TEST_USER_REPOSITORY: ${{ vars.GH_TEST_USER_REPOSITORY }}
114115
GH_TEST_ORG_USER: ${{ vars.GH_TEST_ORG_USER }}

.github/workflows/ghes-acceptance-tests.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ jobs:
120120
GITHUB_OWNER: ""
121121
GITHUB_USERNAME: ""
122122
GITHUB_ENTERPRISE_SLUG: ""
123+
GITHUB_LEGACY_CLIENT: "false"
123124
GH_TEST_AUTH_MODE: enterprise
124125
run: |
125126
set -eou pipefail

CONTRIBUTING.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ Once you have the repository cloned, there's a couple of additional steps you'll
107107
export GITHUB_OWNER="<name of an organization>"
108108
export GITHUB_USERNAME="<username of the user who created the token>"
109109
export GITHUB_TOKEN="<token of a user with an organization account>"
110+
export GITHUB_LEGACY_CLIENT="false"
110111
```
111112

112113
- Build the project with `make build`
@@ -239,6 +240,9 @@ export TF_ACC="1"
239240
# Configure the URL override for GHES.
240241
export GITHUB_BASE_URL=
241242

243+
# Use the modern client for testing.
244+
export GITHUB_LEGACY_CLIENT="false"
245+
242246
# Configure acceptance testing mode; one of anonymous, individual, organization, team or enterprise. If not set will default to anonymous
243247
export GH_TEST_AUTH_MODE=
244248

@@ -286,6 +290,7 @@ To run acceptance tests the `TF_ACC` environment variable must be set. Below is
286290
"GITHUB_ENTERPRISE_SLUG": "",
287291
"GITHUB_OWNER": "<ORGANIZATION>",
288292
"GITHUB_USERNAME": "<USERNAME>",
293+
"GITHUB_LEGACY_CLIENT": "false",
289294
"GH_TEST_AUTH_MODE": "organization",
290295
"GH_TEST_USER_REPOSITORY": "",
291296
"GH_TEST_ORG_USER": "",

docs/index.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,19 @@ provider "github" {
121121

122122
- `app_auth` (Block List, Max: 1) Authenticate using a GitHub App. (see [below for nested schema](#nestedblock--app_auth))
123123
- `base_url` (String) The base URL for the GitHub API; this defaults to the GitHub API URL. If you are using GitHub Enterprise Server (GHES) or GitHub Enterprise Cloud with Data Residency (GHEC-DR), this is required. This can also be set by the `GITHUB_BASE_URL` environment variable.
124-
- `insecure` (Boolean) Allow insecure server connections when using SSL.
124+
- `cache_path` (String) The path to the cache directory for persisting GitHub API requests between runs; if not set there will be no caching between runs. This can also be set by the `GITHUB_CACHE_PATH` environment variable.
125+
- `insecure` (Boolean, Deprecated) Allow insecure server connections when using SSL.
126+
- `legacy_client` (Boolean) Use the legacy GitHub client implementation; if set to `false`, the new client implementation is used. This can also be set by the `GITHUB_LEGACY_CLIENT` environment variable.
125127
- `max_per_page` (Number) The maximum number of results per page for paginated API requests; this defaults to `100`. This can also be set by the `GITHUB_MAX_PER_PAGE` environment variable.
126128
- `max_retries` (Number) The maximum number of retries for failed requests; this defaults to `3`.
127129
- `organization` (String, Deprecated) GitHub organization to manage. This can also be set by the `GITHUB_ORGANIZATION` environment variable.
128130
- `owner` (String) GitHub organization or user account to manage; this is required when authenticating using a GitHub App. If the owner is not provided and a token is provided, the provider will attempt to auto-detect the owner associated with the token. This can also be set by the `GITHUB_OWNER` environment variable.
129-
- `parallel_requests` (Boolean) Allow the provider to make parallel API calls; this is experimental and may cause concurrency and rate limiting issues.
130-
- `read_delay_ms` (Number) The delay in milliseconds between read operations; this defaults to `0`. This can be used to mitigate rate limiting issues when performing a large number of read operations.
131+
- `parallel_requests` (Boolean) Allow the provider to make parallel API calls; this is experimental and may cause concurrency and rate limiting issues. This is ignored for the REST API when `legacy_client` is `false` since the new client implementation is designed to safely handle parallel requests.
132+
- `read_delay_ms` (Number) The delay in milliseconds between read operations; this defaults to `0`. This can be used to mitigate rate limiting issues when performing a large number of read operations. This is ignored for the REST API when `legacy_client` is `false` since the new client implementation is GitHub rate limit aware.
131133
- `retry_delay_ms` (Number) The delay in milliseconds between retry attempts; this defaults to `1000`. This setting only applies when `max_retries` is greater than `0`.
132-
- `retryable_errors` (List of Number) List of HTTP status codes that should be retried; if not set this uses the provider defaults. This setting only applies when `max_retries` is greater than `0`.
134+
- `retryable_errors` (List of Number) List of HTTP status codes that should be retried; if not set this uses the provider defaults. This setting only applies when `max_retries` is greater than `0`. This is ignored for the REST API when `legacy_client` is `false` since the new client implementation handles the retry logic.
133135
- `token` (String) GitHub OAuth or Personal Access Token (PAT) to use for authentication. This can also be set by the `GITHUB_TOKEN` environment variable.
134-
- `write_delay_ms` (Number) The delay in milliseconds between write operations; this defaults to `1000`. This is used to mitigate the GitHub API's abuse rate limits when writing. Note that **ALL** requests to the GraphQL API are implemented as `POST` requests under the hood, so this setting affects those calls as well.
136+
- `write_delay_ms` (Number) The delay in milliseconds between write operations; this defaults to `1000`. This is used to mitigate the GitHub API's abuse rate limits when writing. Note that **ALL** requests to the GraphQL API are implemented as `POST` requests under the hood, so this setting affects those calls as well. This is ignored for the REST API when `legacy_client` is `false` since the new client implementation is GitHub rate limit aware.
135137

136138
<a id="nestedblock--app_auth"></a>
137139
### Nested Schema for `app_auth`

github/acc_test.go

Lines changed: 86 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,17 @@ var (
3434

3535
type testAccConfig struct {
3636
// Target configuration
37-
baseURL *url.URL
37+
legacyClient bool
38+
baseURL *url.URL
3839

3940
// Auth configuration
40-
authMode testMode
41-
owner string
42-
username string
43-
token string
41+
authMode testMode
42+
owner string
43+
username string
44+
token string
45+
appID string
46+
appInstallationID string
47+
appPEM string
4448

4549
// Enterprise configuration
4650
enterpriseSlug string
@@ -112,13 +116,12 @@ func TestMain(m *testing.M) {
112116
}
113117

114118
config := testAccConfig{
115-
baseURL: baseURL,
116-
authMode: authMode,
117-
testPublicRepository: "terraform-provider-github",
118-
testPublicRepositoryOwner: "integrations",
119-
testPublicReleaseId: 186531906,
120-
// The terraform-provider-github_6.4.0_manifest.json asset ID from
121-
// https://github.com/integrations/terraform-provider-github/releases/tag/v6.4.0
119+
legacyClient: os.Getenv("GITHUB_LEGACY_CLIENT") != "false",
120+
baseURL: baseURL,
121+
authMode: authMode,
122+
testPublicRepository: "terraform-provider-github",
123+
testPublicRepositoryOwner: "integrations",
124+
testPublicReleaseId: 186531906, // The terraform-provider-github_6.4.0_manifest.json asset ID from https://github.com/integrations/terraform-provider-github/releases/tag/v6.4.0
122125
testPublicRelaseAssetId: "207956097",
123126
testPublicRelaseAssetName: "terraform-provider-github_6.4.0_manifest.json",
124127
testPublicReleaseAssetContent: "{\n \"version\": 1,\n \"metadata\": {\n \"protocol_versions\": [\n \"5.0\"\n ]\n }\n}",
@@ -135,28 +138,46 @@ func TestMain(m *testing.M) {
135138
testExternalUser2: os.Getenv("GH_TEST_EXTERNAL_USER2"),
136139
testAdvancedSecurity: os.Getenv("GH_TEST_ADVANCED_SECURITY") == "true",
137140
testRepositoryVisibility: "public",
138-
enterpriseIsEMU: authMode == enterprise && os.Getenv("GH_TEST_ENTERPRISE_IS_EMU") == "true",
139141
}
140142

141143
if config.authMode != anonymous {
142144
config.owner = os.Getenv("GITHUB_OWNER")
143145
config.username = os.Getenv("GITHUB_USERNAME")
144146
config.token = os.Getenv("GITHUB_TOKEN")
147+
config.appID = os.Getenv("GITHUB_APP_ID")
148+
config.appInstallationID = os.Getenv("GITHUB_APP_INSTALLATION_ID")
149+
config.appPEM = os.Getenv("GITHUB_APP_PEM_FILE")
145150

146151
if len(config.owner) == 0 {
147152
fmt.Println("GITHUB_OWNER environment variable not set")
148153
os.Exit(1)
149154
}
150155

151-
if len(config.username) == 0 {
156+
if config.authMode == individual && len(config.username) == 0 {
152157
fmt.Println("GITHUB_USERNAME environment variable not set")
153158
os.Exit(1)
154159
}
155160

156-
if len(config.token) == 0 {
157-
fmt.Println("GITHUB_TOKEN environment variable not set")
161+
if config.token == "" && config.appID == "" {
162+
fmt.Println("authentication not configured")
158163
os.Exit(1)
159164
}
165+
166+
if config.token != "" && config.appID != "" {
167+
fmt.Println("Both token and app auth configured")
168+
os.Exit(1)
169+
}
170+
171+
if config.appID != "" && (config.appInstallationID == "" || config.appPEM == "") {
172+
fmt.Println("App auth configured without all required parameters")
173+
os.Exit(1)
174+
}
175+
}
176+
177+
if config.authMode != anonymous && config.authMode != individual {
178+
if i, err := strconv.Atoi(os.Getenv("GH_TEST_ORG_APP_INSTALLATION_ID")); err == nil {
179+
config.testOrgAppInstallationId = i
180+
}
160181
}
161182

162183
if config.authMode == enterprise {
@@ -167,19 +188,14 @@ func TestMain(m *testing.M) {
167188
os.Exit(1)
168189
}
169190

170-
i, err := strconv.Atoi(os.Getenv("GH_TEST_ENTERPRISE_EMU_GROUP_ID"))
171-
if err == nil {
172-
config.testEnterpriseEMUGroupId = i
173-
}
174-
175-
if config.enterpriseIsEMU {
191+
if os.Getenv("GH_TEST_ENTERPRISE_IS_EMU") == "true" {
192+
config.enterpriseIsEMU = true
176193
config.testRepositoryVisibility = "private"
177-
}
178-
}
179194

180-
i, err := strconv.Atoi(os.Getenv("GH_TEST_ORG_APP_INSTALLATION_ID"))
181-
if err == nil {
182-
config.testOrgAppInstallationId = i
195+
if i, err := strconv.Atoi(os.Getenv("GH_TEST_ENTERPRISE_EMU_GROUP_ID")); err == nil {
196+
config.testEnterpriseEMUGroupId = i
197+
}
198+
}
183199
}
184200

185201
testAccConf = &config
@@ -189,19 +205,48 @@ func TestMain(m *testing.M) {
189205
resource.TestMain(m)
190206
}
191207

192-
func getTestMeta() (*Owner, error) {
193-
config := Config{
194-
Token: testAccConf.token,
195-
Owner: testAccConf.owner,
196-
BaseURL: testAccConf.baseURL,
208+
func getTestAppToken() (string, error) {
209+
if testAccConf.appID == "" || testAccConf.appInstallationID == "" || testAccConf.appPEM == "" {
210+
return "", fmt.Errorf("app auth not configured")
197211
}
198212

199-
meta, err := config.Meta()
213+
appToken, err := GenerateOAuthTokenFromApp(testAccConf.baseURL, testAccConf.appID, testAccConf.appInstallationID, testAccConf.appPEM)
200214
if err != nil {
201-
return nil, fmt.Errorf("error getting GitHub meta parameter")
215+
return "", err
202216
}
203217

204-
return meta.(*Owner), nil
218+
return appToken, nil
219+
}
220+
221+
func getTestToken() (string, error) {
222+
if testAccConf.token != "" {
223+
return testAccConf.token, nil
224+
}
225+
226+
return getTestAppToken()
227+
}
228+
229+
func getTestMeta() (*Owner, error) {
230+
config := &Config{
231+
GraphQLAPIPath: "graphql",
232+
LegacyClient: testAccConf.legacyClient,
233+
BaseURL: testAccConf.baseURL,
234+
Owner: testAccConf.owner,
235+
}
236+
237+
if config.LegacyClient || testAccConf.appID == "" {
238+
token, err := getTestToken()
239+
if err != nil {
240+
return nil, fmt.Errorf("error getting test token: %w", err)
241+
}
242+
config.Token = token
243+
} else {
244+
config.AppID = &testAccConf.appID
245+
config.AppInstallationID = &testAccConf.appInstallationID
246+
config.AppPEM = []byte(testAccConf.appPEM)
247+
}
248+
249+
return configureProviderMeta(context.Background(), config)
205250
}
206251

207252
func configureSweepers() {
@@ -292,6 +337,12 @@ func skipUnauthenticated(t *testing.T) {
292337
}
293338
}
294339

340+
func skipApp(t *testing.T) {
341+
if testAccConf.appID != "" {
342+
t.Skip("Skipping as app not configured")
343+
}
344+
}
345+
295346
func skipUnlessHasOrgs(t *testing.T) {
296347
if !slices.Contains(orgTestModes, testAccConf.authMode) {
297348
t.Skip("Skipping as test mode doesn't have orgs")

github/config.go

Lines changed: 27 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,23 @@ import (
1616
)
1717

1818
type Config struct {
19-
Token string
20-
Owner string
21-
BaseURL *url.URL
22-
IsGHES bool
23-
Insecure bool
24-
WriteDelay time.Duration
25-
ReadDelay time.Duration
26-
RetryDelay time.Duration
27-
RetryableErrors map[int]bool
28-
MaxRetries int
29-
ParallelRequests bool
19+
AppID *string
20+
AppInstallationID *string
21+
AppPEM []byte
22+
BaseURL *url.URL
23+
CachePath *string
24+
GraphQLAPIPath string
25+
Insecure bool
26+
LegacyClient bool
27+
MaxRetries int
28+
Owner string
29+
ParallelRequests bool
30+
ReadDelay time.Duration
31+
RESTAPIPath string
32+
RetryableErrors map[int]bool
33+
RetryDelay time.Duration
34+
Token string
35+
WriteDelay time.Duration
3036
}
3137

3238
type Owner struct {
@@ -47,6 +53,8 @@ const (
4753
DotComAPIHost = "api.github.com"
4854
// GHESRESTAPISuffix is the rest api suffix for GitHub Enterprise Server.
4955
GHESRESTAPIPath = "api/v3/"
56+
// GHESGraphQLAPISuffix is the GraphQL api suffix for GitHub Enterprise Server.
57+
GHESGraphQLAPIPath = "api/graphql"
5058
)
5159

5260
var (
@@ -83,7 +91,7 @@ func (c *Config) AuthenticatedHTTPClient() *http.Client {
8391
}
8492

8593
func (c *Config) Anonymous() bool {
86-
return c.Token == ""
94+
return c.AppID == nil && c.Token == ""
8795
}
8896

8997
func (c *Config) AnonymousHTTPClient() *http.Client {
@@ -92,30 +100,19 @@ func (c *Config) AnonymousHTTPClient() *http.Client {
92100
}
93101

94102
func (c *Config) NewGraphQLClient(client *http.Client) (*githubv4.Client, error) {
95-
var path string
96-
if c.IsGHES {
97-
path = "api/graphql"
98-
} else {
99-
path = "graphql"
100-
}
101-
102-
return githubv4.NewEnterpriseClient(c.BaseURL.JoinPath(path).String(), client), nil
103+
return githubv4.NewEnterpriseClient(c.BaseURL.JoinPath(c.GraphQLAPIPath).String(), client), nil
103104
}
104105

105106
func (c *Config) NewRESTClient(client *http.Client) (*github.Client, error) {
106-
path := ""
107-
if c.IsGHES {
108-
path = GHESRESTAPIPath
109-
}
110-
111-
v3client, err := github.NewClient(github.WithHTTPClient(client), github.WithURLs(new(c.BaseURL.JoinPath(path).String()), nil))
107+
v3client, err := github.NewClient(github.WithHTTPClient(client), github.WithURLs(new(c.BaseURL.JoinPath(c.RESTAPIPath).String()), nil))
112108
if err != nil {
113109
return nil, err
114110
}
115111

116112
return v3client, nil
117113
}
118114

115+
// Deprecated: This is no longer required as [configureProviderMeta] is now used to configure the provider meta parameter with the necessary clients and owner information. Use [configureProviderMeta] instead.
119116
func (c *Config) ConfigureOwner(owner *Owner) (*Owner, error) {
120117
ctx := context.Background()
121118
owner.name = c.Owner
@@ -144,34 +141,9 @@ func (c *Config) ConfigureOwner(owner *Owner) (*Owner, error) {
144141

145142
// Meta returns the meta parameter that is passed into subsequent resources
146143
// https://godoc.org/github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema#ConfigureFunc
144+
// Deprecated: Use [configureProviderMeta] instead.
147145
func (c *Config) Meta() (any, error) {
148-
var client *http.Client
149-
if c.Anonymous() {
150-
client = c.AnonymousHTTPClient()
151-
} else {
152-
client = c.AuthenticatedHTTPClient()
153-
}
154-
155-
v3client, err := c.NewRESTClient(client)
156-
if err != nil {
157-
return nil, err
158-
}
159-
160-
v4client, err := c.NewGraphQLClient(client)
161-
if err != nil {
162-
return nil, err
163-
}
164-
165-
var owner Owner
166-
owner.v4client = v4client
167-
owner.v3client = v3client
168-
owner.StopContext = context.Background()
169-
170-
_, err = c.ConfigureOwner(&owner)
171-
if err != nil {
172-
return &owner, err
173-
}
174-
return &owner, nil
146+
return configureProviderMeta(context.Background(), c)
175147
}
176148

177149
type previewHeaderInjectorTransport struct {
@@ -205,8 +177,8 @@ func (injector *previewHeaderInjectorTransport) RoundTrip(req *http.Request) (*h
205177

206178
// getBaseURL returns a correctly configured base URL and a bool as to if this is GitHub Enterprise Server.
207179
func getBaseURL(s string) (*url.URL, bool, error) {
208-
if len(s) == 0 {
209-
s = DotComAPIURL
180+
if s == "" {
181+
return nil, false, fmt.Errorf("base url must not be empty")
210182
}
211183

212184
u, err := url.Parse(s)

0 commit comments

Comments
 (0)