Skip to content

Commit 5053c59

Browse files
authored
feat: gracefully handle rate-limits and ephemeral errors (#48)
* Gracefully handle rate-limits through `github.com/isometry/ghait` and its use of `github.com/gofri/go-github-ratelimit`. * Gracefully handle ephemeral 504 errors from GitHub's backend; add `.spec.retryInteval` field on both `Token` and `ClusterToken` to override the default `5m` retry interval. * Increase default `.spec.refreshInterval` from `10m` to `30m`. * Expand usage guidance in `README.md` with more complete configuration and `Token` examples. Fixes: #47
1 parent 3e381c7 commit 5053c59

File tree

12 files changed

+320
-161
lines changed

12 files changed

+320
-161
lines changed

README.md

Lines changed: 119 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,19 @@ This operator functions similarly to cert-manager, but instead of managing certi
1818

1919
* A Kubernetes cluster (v1.21+)
2020
* A [GitHub App](https://docs.github.com/en/apps/creating-github-apps) with permissions and repository assignments sufficient to meet the needs of all anticipated GitHub API interactions. Typically: `metadata: read`, `contents: read`, `statuses: write`.
21+
* Specifically: App ID, App Installation ID and a Private Key are required.
2122

22-
#### Example `gtm-config` secret
23-
<details>
24-
<summary><i>expand me! ✨</i></summary>
23+
### Installation
24+
25+
A Helm Chart is provided your for convenience: [deploy/charts/github-token-manager/](deploy/charts/github-token-manager/)
26+
27+
Alternatively, a baseline Kustomization is provided under [config/default/](config/default/)
28+
29+
### Configuration
30+
31+
The operator itself requires configuration via `ConfigMap/gtm-config` in its deployment namespace. This contains the GitHub App ID, Installation ID and Private Key provider details. In addition to embedding the private key file within the secret, AWS Key Management Service (KMS), Google Cloud Key Management, and HashiCorp Vault's Transit Secrets Engine are also supported for secure external handling of keying material.
32+
33+
#### Example `gtm-config` with embedded Private Key
2534

2635
```yaml
2736
apiVersion: v1
@@ -31,8 +40,8 @@ metadata:
3140
namespace: github-token-manager
3241
stringData:
3342
gtm.yaml: |
34-
app_id: <app-id>
35-
installation_id: <installation-id>
43+
app_id: 1234
44+
installation_id: 4567890
3645
provider: file
3746
key: /config/private.key
3847
private.key: |
@@ -41,11 +50,109 @@ stringData:
4150
-----END RSA PRIVATE KEY-----
4251
```
4352
44-
</details>
53+
#### Example `gtm-config` with AWS KMS
54+
55+
```yaml
56+
apiVersion: v1
57+
kind: Secret
58+
metadata:
59+
name: gtm-config
60+
namespace: github-token-manager
61+
stringData:
62+
gtm.yaml: |
63+
app_id: 1234
64+
installation_id: 45678890
65+
provider: aws
66+
key: alias/github-token-manager
67+
```
68+
69+
### `Token` and `ClusterToken`
70+
71+
Once the operator is installed and configured, any number of namespaced `Token` and non-namespaced `ClusterToken` may be created, resulting in matching `Secret` resoures being created, containing either `token` or `username` and `password` fields, depending on configuration.
72+
73+
The namespaced `Token` resource manages a `Secret` in the same namespace containing a fine-grained [installation access token](https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app) for the configured GitHub App, appropriate for delegated management by the namespace owner.
74+
75+
The non-namespaced `ClusterToken` resource does the same thing, but supports abstracted management where only the managed `Secret` is bound to the configured target namespace via `.spec.secret.namespace`.
76+
77+
```yaml
78+
apiVersion: github.as-code.io/v1
79+
kind: ClusterToken # or Token
80+
metadata:
81+
name: foo
82+
spec:
83+
installationID: 321 # (optional) override GitHub App Installation ID configured for the operator
84+
permissions: {} # (optional) map of token permissions, default: all permissions assigned to the GitHub App
85+
refreshInterval: 45m # (optional) token refresh interval, default 30m
86+
retryInterval: 1m # (optional) token retry interval on ephemeral failure; default: 5m
87+
repositories: [] # (optional) name-based override of repositories accessible with managed token
88+
repositoryIDs: [] # (optional) ID-based override of reposotiories accessible with managed token
89+
secret: # (optional) override default `Secret` configuration
90+
annotations: {} # (optional) map of annotations for managed `Secret`
91+
basicAuth: true # (optional) create `Secret` with `username` and `password` rather than `token`
92+
labels: {} # (optional) map of labels for managed `Secret`
93+
name: bar # (optional) override name for managed `Secret` (default: .metadata.name)
94+
namespace: default # (required, ClusterToken-only) set the target namespace for managed `Secret`
95+
```
96+
97+
#### Examples
98+
99+
Manage a `Secret/github-token` containing HTTP Basic Auth `username` and `password` fields appropriate for use with a Flux' `GitRepository` [Secret Reference](https://fluxcd.io/flux/components/source/gitrepositories/#secret-reference):
100+
101+
```yaml
102+
apiVersion: github.as-code.io/v1
103+
kind: Token
104+
metadata:
105+
name: github-token
106+
namespace: flux-system
107+
spec:
108+
permissions:
109+
metadata: read
110+
contents: read
111+
refreshInterval: 45m
112+
secret:
113+
basicAuth: true
114+
```
115+
116+
Manage a `Secret/github-status` containing a plain `token` field appropriate for use with a Flux' `Provider` [GitHub Commit Status Updates](https://fluxcd.io/flux/components/notification/providers/#github):
117+
118+
```yaml
119+
apiVersion: github.as-code.io/v1
120+
kind: Token
121+
metadata:
122+
name: github-status
123+
namespace: flux-system
124+
spec:
125+
permissions:
126+
metadata: read
127+
statuses: write
128+
refreshInterval: 45m
129+
```
130+
131+
Manage `Secret/github` in the `default` namespace containing a plain `token` field, inheriting all permissions assigned to the configured GitHub App:
132+
133+
```yaml
134+
apiVersion: github.as-code.io/v1
135+
kind: ClusterToken
136+
metadata:
137+
name: default-github
138+
spec:
139+
secret:
140+
name: github
141+
namespace: default
142+
```
143+
144+
## Contributing
145+
146+
All contributions from the community are welcome.
147+
148+
**NOTE:** Run `make help` for more information on all potential `make` targets
149+
150+
More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)
45151

46152
### To Deploy on the cluster
47153

48-
**Build and push your image to the location specified by `IMG`:**
154+
155+
#### Build and push your image to the location specified by `IMG`:
49156

50157
```sh
51158
make ko-build IMG=<some-registry>/github-token-manager:tag
@@ -55,13 +162,13 @@ make ko-build IMG=<some-registry>/github-token-manager:tag
55162
And it is required to have access to pull the image from the working environment.
56163
Make sure you have the proper permission to the registry if the above commands don’t work.
57164

58-
**Install the CRDs into the cluster:**
165+
#### Install the CRDs into the cluster:
59166

60167
```sh
61168
make install
62169
```
63170

64-
**Deploy the Manager to the cluster with the image specified by `IMG`:**
171+
#### Deploy the Manager to the cluster with the image specified by `IMG`:
65172

66173
```sh
67174
make deploy IMG=<some-registry>/github-token-manager:tag
@@ -70,43 +177,26 @@ make deploy IMG=<some-registry>/github-token-manager:tag
70177
> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin
71178
privileges or be logged in as admin.
72179

73-
**Create instances of your solution**
74-
You can apply the samples (examples) from the config/sample:
75-
76-
```sh
77-
kubectl apply -k config/samples/
78-
```
79-
80-
>**NOTE**: Ensure that the samples has default values to test it out.
81-
82180
### To Uninstall
83181

84-
**Delete the instances (CRs) from the cluster:**
182+
#### Delete the instances (CRs) from the cluster
85183

86184
```sh
87185
kubectl delete -k config/samples/
88186
```
89187

90-
**Delete the APIs(CRDs) from the cluster:**
188+
#### Delete the APIs(CRDs) from the cluster
91189

92190
```sh
93191
make uninstall
94192
```
95193

96-
**UnDeploy the controller from the cluster:**
194+
#### UnDeploy the controller from the cluster
97195

98196
```sh
99197
make undeploy
100198
```
101199

102-
## Contributing
103-
104-
// TODO(user): Add detailed information on how you would like others to contribute to this project
105-
106-
**NOTE:** Run `make help` for more information on all potential `make` targets
107-
108-
More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)
109-
110200
## License
111201

112202
Copyright 2024 Robin Breathe.

api/v1/clustertoken_types.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package v1
1919
import (
2020
"time"
2121

22-
"github.com/google/go-github/v66/github"
22+
"github.com/google/go-github/v69/github"
2323
"github.com/isometry/github-token-manager/internal/ghapp"
2424
"k8s.io/apimachinery/pkg/api/meta"
2525
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -42,11 +42,18 @@ type ClusterTokenSpec struct {
4242

4343
// +optional
4444
// +kubebuilder:validation:Format:=duration
45-
// +kubebuilder:default:="10m"
45+
// +kubebuilder:default:="30m"
4646
// +kubebuilder:example:="45m"
4747
// Specify how often to refresh the token (maximum: 1h)
4848
RefreshInterval metav1.Duration `json:"refreshInterval"`
4949

50+
// +optional
51+
// +kubebuilder:validation:Format:=duration
52+
// +kubebuilder:default:="5m"
53+
// +kubebuilder:example:="1m"
54+
// Specify how long to wait before retrying on transient token retrieval error
55+
RetryInterval metav1.Duration `json:"retryInterval"`
56+
5057
// +optional
5158
// +kubebuilder:example:={"metadata": "read", "contents": "read"}
5259
// Specify the permissions for the token as a subset of those of the GitHub App
@@ -128,6 +135,10 @@ func (t *ClusterToken) GetRefreshInterval() time.Duration {
128135
return t.Spec.RefreshInterval.Duration
129136
}
130137

138+
func (t *ClusterToken) GetRetryInterval() time.Duration {
139+
return t.Spec.RetryInterval.Duration
140+
}
141+
131142
func (t *ClusterToken) GetSecretNamespace() string {
132143
return t.Spec.Secret.Namespace
133144
}

api/v1/permissions.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package v1
22

33
import (
4-
"github.com/google/go-github/v66/github"
4+
"github.com/google/go-github/v69/github"
55
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
66
)
77

api/v1/token_types.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package v1
1919
import (
2020
"time"
2121

22-
"github.com/google/go-github/v66/github"
22+
"github.com/google/go-github/v69/github"
2323
"github.com/isometry/github-token-manager/internal/ghapp"
2424
"k8s.io/apimachinery/pkg/api/meta"
2525
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -43,11 +43,18 @@ type TokenSpec struct {
4343

4444
// +optional
4545
// +kubebuilder:validation:Format:=duration
46-
// +kubebuilder:default:="10m"
46+
// +kubebuilder:default:="30m"
4747
// +kubebuilder:example:="45m"
4848
// Specify how often to refresh the token (maximum: 1h)
4949
RefreshInterval metav1.Duration `json:"refreshInterval"`
5050

51+
// +optional
52+
// +kubebuilder:validation:Format:=duration
53+
// +kubebuilder:default:="5m"
54+
// +kubebuilder:example:="1m"
55+
// Specify how long to wait before retrying on transient token retrieval error
56+
RetryInterval metav1.Duration `json:"retryInterval"`
57+
5158
// +optional
5259
// +kubebuilder:example:={"metadata": "read", "contents": "read"}
5360
// Specify the permissions for the token as a subset of those of the GitHub App
@@ -121,6 +128,10 @@ func (t *Token) GetRefreshInterval() time.Duration {
121128
return t.Spec.RefreshInterval.Duration
122129
}
123130

131+
func (t *Token) GetRetryInterval() time.Duration {
132+
return t.Spec.RetryInterval.Duration
133+
}
134+
124135
func (t *Token) GetSecretNamespace() string {
125136
return t.Namespace
126137
}

api/v1/zz_generated.deepcopy.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/github.as-code.io_clustertokens.yaml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ spec:
236236
type: string
237237
type: object
238238
refreshInterval:
239-
default: 10m
239+
default: 30m
240240
description: "Specify how often to refresh the token (maximum: 1h)"
241241
example: 45m
242242
format: duration
@@ -258,6 +258,14 @@ spec:
258258
type: integer
259259
maxItems: 500
260260
type: array
261+
retryInterval:
262+
default: 5m
263+
description:
264+
Specify how long to wait before retrying on transient
265+
token retrieval error
266+
example: 1m
267+
format: duration
268+
type: string
261269
secret:
262270
properties:
263271
annotations:

config/crd/bases/github.as-code.io_tokens.yaml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ spec:
236236
type: string
237237
type: object
238238
refreshInterval:
239-
default: 10m
239+
default: 30m
240240
description: "Specify how often to refresh the token (maximum: 1h)"
241241
example: 45m
242242
format: duration
@@ -258,6 +258,14 @@ spec:
258258
type: integer
259259
maxItems: 500
260260
type: array
261+
retryInterval:
262+
default: 5m
263+
description:
264+
Specify how long to wait before retrying on transient
265+
token retrieval error
266+
example: 1m
267+
format: duration
268+
type: string
261269
secret:
262270
description: Override the default token secret name and type
263271
properties:

0 commit comments

Comments
 (0)