Skip to content

Commit a443770

Browse files
committed
feat: Add forgejo_personal_access_token resource.
1 parent 8747ea2 commit a443770

9 files changed

Lines changed: 993 additions & 8 deletions

File tree

.github/workflows/test.yml

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,17 +116,18 @@ jobs:
116116
run: sleep 30s
117117

118118
- name: Create admin user
119+
id: admin
119120
working-directory: ./docker
120121
run: >
121-
docker compose exec -u git forgejo
122+
echo "password=$(docker compose exec -u git forgejo
122123
/usr/local/bin/forgejo admin user create
123124
--username tfadmin
124125
--email tfadmin@localhost
125126
--random-password
126-
--admin
127+
--admin | grep 'generated' | cut -d ' ' -f 5 | tr -d \')" >> "$GITHUB_OUTPUT"
127128
128129
- name: Generate admin token
129-
id: admin
130+
id: token
130131
working-directory: ./docker
131132
run: >
132133
echo "token=$(docker compose exec -u git forgejo
@@ -141,7 +142,9 @@ jobs:
141142
- name: Run acceptance tests
142143
env:
143144
TF_ACC: "1"
144-
FORGEJO_API_TOKEN: ${{ steps.admin.outputs.token }}
145+
TF_VAR_FORGEJO_BASIC_AUTH_USERNAME: tfadmin
146+
TF_VAR_FORGEJO_BASIC_AUTH_PASSWORD: ${{ steps.admin.outputs.password }}
147+
FORGEJO_API_TOKEN: ${{ steps.token.outputs.token }}
145148
run: go test -v -cover ./internal/provider/
146149
timeout-minutes: 10
147150

@@ -191,17 +194,18 @@ jobs:
191194
run: sleep 30s
192195

193196
- name: Create admin user
197+
id: admin
194198
working-directory: ./docker
195199
run: >
196-
docker compose exec -u git forgejo
200+
echo "password=$(docker compose exec -u git forgejo
197201
/usr/local/bin/forgejo admin user create
198202
--username tfadmin
199203
--email tfadmin@localhost
200204
--random-password
201-
--admin
205+
--admin | grep 'generated' | cut -d ' ' -f 5 | tr -d \')" >> "$GITHUB_OUTPUT"
202206
203207
- name: Generate admin token
204-
id: admin
208+
id: token
205209
working-directory: ./docker
206210
run: >
207211
echo "token=$(docker compose exec -u git forgejo
@@ -216,6 +220,8 @@ jobs:
216220
- name: Run acceptance tests
217221
env:
218222
TF_ACC: "1"
219-
FORGEJO_API_TOKEN: ${{ steps.admin.outputs.token }}
223+
TF_VAR_FORGEJO_BASIC_AUTH_USERNAME: tfadmin
224+
TF_VAR_FORGEJO_BASIC_AUTH_PASSWORD: ${{ steps.admin.outputs.password }}
225+
FORGEJO_API_TOKEN: ${{ steps.token.outputs.token }}
220226
run: go test -v -cover ./internal/provider/
221227
timeout-minutes: 10
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
terraform {
2+
required_providers {
3+
forgejo = {
4+
source = "svalabs/forgejo"
5+
}
6+
}
7+
}
8+
9+
provider "forgejo" {
10+
host = "http://localhost:3000"
11+
}
12+
13+
# Existing user
14+
data "forgejo_user" "test_user" {
15+
login = "test_user"
16+
}
17+
18+
# Existing personal access token
19+
data "forgejo_personal_access_token" "test_token" {
20+
user_id = data.forgejo_user.test_user.id
21+
name = "test token"
22+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
terraform {
2+
required_providers {
3+
forgejo = {
4+
source = "svalabs/forgejo"
5+
}
6+
}
7+
}
8+
9+
variable "test_password" { sensitive = true }
10+
11+
provider "forgejo" {
12+
host = "http://localhost:3000"
13+
14+
username = "admin"
15+
password = var.forgejo_password
16+
# ...or use the FORGEJO_USERNAME / FORGEJO_PASSWORD environment variables
17+
}
18+
19+
resource "forgejo_user" "test_user" {
20+
login = "test_user"
21+
email = "test_user@localhost.localdomain"
22+
password = var.test_password
23+
}
24+
25+
resource "forgejo_personal_access_token" "test_token" {
26+
user_id = forgejo_user.test_user.id
27+
name = "test token"
28+
scopes = [
29+
"all",
30+
"read:repository"
31+
]
32+
}

internal/provider/env.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/sh
2+
3+
# Change the contents of this output to get the environment variables
4+
# of interest. The output must be valid JSON, with strings for both
5+
# keys and values.
6+
cat <<EOF
7+
{
8+
"FORGEJO_ADMIN_USERNAME": "$FORGEJO_ADMIN_USERNAME",
9+
"FORGEJO_ADMIN_PASSWORD": "$FORGEJO_ADMIN_PASSWORD"
10+
}
11+
EOF
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"slices"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/datasource"
9+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
10+
"github.com/hashicorp/terraform-plugin-framework/diag"
11+
"github.com/hashicorp/terraform-plugin-framework/types"
12+
"github.com/hashicorp/terraform-plugin-log/tflog"
13+
14+
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3"
15+
)
16+
17+
// Ensure the implementation satisfies the expected interfaces.
18+
var (
19+
_ datasource.DataSource = &personalAccessTokenDataSource{}
20+
_ datasource.DataSourceWithConfigure = &personalAccessTokenDataSource{}
21+
)
22+
23+
// personalAccessTokenDataSource is the data source implementation.
24+
type personalAccessTokenDataSource struct {
25+
client *forgejo.Client
26+
}
27+
28+
// personalAccessTokenDataSourceModel maps the data source schema data.
29+
// https://pkg.go.dev/codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3#CreateAccessTokenOption
30+
type personalAccessTokenDataSourceModel struct {
31+
UserID types.Int64 `tfsdk:"user_id"`
32+
ID types.Int64 `tfsdk:"id"`
33+
Name types.String `tfsdk:"name"`
34+
TokenLastEight types.String `tfsdk:"token_last_eight"`
35+
Scopes types.Set `tfsdk:"scopes"`
36+
}
37+
38+
// Metadata returns the data source type name.
39+
func (d *personalAccessTokenDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
40+
resp.TypeName = req.ProviderTypeName + "_personal_access_token"
41+
}
42+
43+
// Schema defines the schema for the data source.
44+
func (d *personalAccessTokenDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
45+
resp.Schema = schema.Schema{
46+
Description: "Forgejo personal access token data source.",
47+
48+
Attributes: map[string]schema.Attribute{
49+
"user_id": schema.Int64Attribute{
50+
Description: "ID of the user.",
51+
Required: true,
52+
},
53+
"name": schema.StringAttribute{
54+
Description: "Name of the personal access token.",
55+
Required: true,
56+
},
57+
"id": schema.Int64Attribute{
58+
Description: "ID of the personal access token.",
59+
Computed: true,
60+
},
61+
"token_last_eight": schema.StringAttribute{
62+
Description: "Last eight characters of the personal access token.",
63+
Computed: true,
64+
},
65+
"scopes": schema.SetAttribute{
66+
Description: "Scopes of the personal access token.",
67+
Computed: true,
68+
ElementType: types.StringType,
69+
},
70+
},
71+
}
72+
}
73+
74+
// Configure adds the provider configured client to the data source.
75+
func (d *personalAccessTokenDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
76+
// Prevent panic if the provider has not been configured.
77+
if req.ProviderData == nil {
78+
return
79+
}
80+
81+
client, ok := req.ProviderData.(*forgejo.Client)
82+
if !ok {
83+
resp.Diagnostics.AddError(
84+
"Unexpected Data Source Configure Type",
85+
fmt.Sprintf(
86+
"Expected *forgejo.Client, got: %T. Please report this issue to the provider developers.",
87+
req.ProviderData,
88+
),
89+
)
90+
91+
return
92+
}
93+
94+
d.client = client
95+
}
96+
97+
// Read refreshes the Terraform state with the latest data.
98+
func (d *personalAccessTokenDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
99+
defer un(trace(ctx, "Read personal access token data source"))
100+
101+
var (
102+
data personalAccessTokenDataSourceModel
103+
)
104+
105+
// Read Terraform configuration data into model
106+
diags := req.Config.Get(ctx, &data)
107+
resp.Diagnostics.Append(diags...)
108+
if resp.Diagnostics.HasError() {
109+
return
110+
}
111+
112+
// Use Forgejo client to get personal access token
113+
token, diags := getPersonalAccessToken(ctx, d.client, data.UserID.ValueInt64(), data.Name.ValueString())
114+
resp.Diagnostics.Append(diags...)
115+
if resp.Diagnostics.HasError() {
116+
return
117+
}
118+
119+
// Map response body to model
120+
data.ID = types.Int64Value(token.ID)
121+
data.Name = types.StringValue(token.Name)
122+
data.TokenLastEight = types.StringValue(token.TokenLastEight)
123+
data.Scopes, diags = types.SetValueFrom(ctx, types.StringType, token.Scopes)
124+
resp.Diagnostics.Append(diags...)
125+
126+
// Save data into Terraform state
127+
diags = resp.State.Set(ctx, &data)
128+
resp.Diagnostics.Append(diags...)
129+
}
130+
131+
func getPersonalAccessToken(
132+
ctx context.Context,
133+
client *forgejo.Client,
134+
userID int64,
135+
tokenName string) (*forgejo.AccessToken, diag.Diagnostics) {
136+
137+
var (
138+
diags diag.Diagnostics
139+
user userResourceModel
140+
)
141+
142+
// Use Forgejo client to get user
143+
usr, diags := getUserByID(
144+
ctx,
145+
client,
146+
userID,
147+
)
148+
if diags.HasError() {
149+
return nil, diags
150+
}
151+
152+
// Map response body to model
153+
user.from(usr)
154+
155+
tflog.Info(ctx, "List personal access tokens", map[string]any{
156+
"user": user.Name.ValueString(),
157+
})
158+
159+
// Use Forgejo client to list personal access tokens
160+
tokens, res, err := client.ListAccessTokens(
161+
user.Name.ValueString(),
162+
forgejo.ListAccessTokensOptions{
163+
ListOptions: forgejo.ListOptions{
164+
Page: -1,
165+
},
166+
},
167+
)
168+
if err != nil {
169+
var msg string
170+
if res == nil {
171+
msg = fmt.Sprintf("Unknown error with nil response: %s", err)
172+
} else {
173+
tflog.Error(ctx, "Error", map[string]any{
174+
"status": res.Status,
175+
})
176+
177+
switch res.StatusCode {
178+
case 403:
179+
msg = fmt.Sprintf(
180+
"Personal access tokens from user %s forbidden: %s",
181+
user.Name.String(),
182+
err,
183+
)
184+
case 404:
185+
msg = fmt.Sprintf(
186+
"Personal access tokens from user %s not found: %s",
187+
user.Name.String(),
188+
err,
189+
)
190+
default:
191+
msg = fmt.Sprintf(
192+
"Unknown error (status %d): %s",
193+
res.StatusCode,
194+
err,
195+
)
196+
}
197+
}
198+
diags.AddError("Unable to list personal access tokens", msg)
199+
return nil, diags
200+
}
201+
202+
// Search for personal access token with given name
203+
idx := slices.IndexFunc(tokens, func(t *forgejo.AccessToken) bool {
204+
return t.Name == tokenName
205+
})
206+
if idx == -1 {
207+
diags.AddError(
208+
"Unable to find personal access token by name",
209+
fmt.Sprintf(
210+
"Personal access token from user %s and name %s not found",
211+
user.Name.String(),
212+
tokenName,
213+
),
214+
)
215+
216+
return nil, diags
217+
}
218+
return tokens[idx], diags
219+
}
220+
221+
// NewPersonalAccessTokenDataSource is a helper function to simplify the provider implementation.
222+
func NewPersonalAccessTokenDataSource() datasource.DataSource {
223+
return &personalAccessTokenDataSource{}
224+
}

0 commit comments

Comments
 (0)