Skip to content

Commit d4c5ec4

Browse files
authored
feat(vcr): add public repository option (#572)
* add public repository resource * fix docs
1 parent f5abffa commit d4c5ec4

9 files changed

Lines changed: 200 additions & 11 deletions

File tree

client/vcr_repository.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type VCRRepository struct {
1515
ID string `json:"id"`
1616
Name string `json:"name"`
1717
ProjectID string `json:"projectId"`
18+
Public bool `json:"public"`
1819
URL string `json:"-"`
1920
TeamID string `json:"-"`
2021
}
@@ -133,6 +134,48 @@ func (c *Client) GetVCRRepository(ctx context.Context, request GetVCRRepositoryR
133134
return res, nil
134135
}
135136

137+
type UpdateVCRRepositoryRequest struct {
138+
TeamID string `json:"-"`
139+
ProjectID string `json:"-"`
140+
IDOrName string `json:"-"`
141+
Public bool `json:"public"`
142+
}
143+
144+
func (c *Client) UpdateVCRRepository(ctx context.Context, request UpdateVCRRepositoryRequest) (res VCRRepository, err error) {
145+
url := fmt.Sprintf("%s/v1/vcr/repository/%s?projectId=%s", c.baseURL, request.IDOrName, request.ProjectID)
146+
if c.TeamID(request.TeamID) != "" {
147+
url = fmt.Sprintf("%s&teamId=%s", url, c.TeamID(request.TeamID))
148+
}
149+
payload := string(mustMarshal(request))
150+
tflog.Info(ctx, "updating vcr repository", map[string]any{
151+
"url": url,
152+
"payload": payload,
153+
})
154+
var out vcrRepositoryResponse
155+
err = c.doRequest(clientRequest{
156+
ctx: ctx,
157+
method: "PATCH",
158+
url: url,
159+
body: payload,
160+
}, &out)
161+
if err != nil {
162+
return res, err
163+
}
164+
res = out.repository()
165+
if res.Name == "" {
166+
res.Name = request.IDOrName
167+
}
168+
if res.ProjectID == "" {
169+
res.ProjectID = request.ProjectID
170+
}
171+
res.TeamID = c.TeamID(request.TeamID)
172+
res.URL, err = c.vcrRepositoryURL(ctx, request.TeamID, res.ProjectID, res.Name)
173+
if err != nil {
174+
return res, err
175+
}
176+
return res, nil
177+
}
178+
136179
type DeleteVCRRepositoryRequest struct {
137180
TeamID string `json:"-"`
138181
ProjectID string `json:"-"`

client/vcr_repository_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
)
10+
11+
func TestUpdateVCRRepository(t *testing.T) {
12+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13+
switch r.URL.Path {
14+
case "/v1/vcr/repository/repo_123":
15+
if r.Method != http.MethodPatch {
16+
t.Fatalf("method = %s, want PATCH", r.Method)
17+
}
18+
if projectID := r.URL.Query().Get("projectId"); projectID != "prj_123" {
19+
t.Fatalf("projectId = %q, want prj_123", projectID)
20+
}
21+
if teamID := r.URL.Query().Get("teamId"); teamID != "team_123" {
22+
t.Fatalf("teamId = %q, want team_123", teamID)
23+
}
24+
var body map[string]any
25+
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
26+
t.Fatalf("Decode() error = %v", err)
27+
}
28+
if len(body) != 1 || body["public"] != true {
29+
t.Fatalf("body = %#v, want only public=true", body)
30+
}
31+
_, _ = w.Write([]byte(`{"repository":{"id":"repo_123","projectId":"prj_123","name":"example","public":true}}`))
32+
case "/v2/teams/team_123":
33+
_, _ = w.Write([]byte(`{"id":"team_123","slug":"acme"}`))
34+
case "/v10/projects/prj_123":
35+
_, _ = w.Write([]byte(`{"id":"prj_123","name":"storefront"}`))
36+
default:
37+
http.NotFound(w, r)
38+
}
39+
}))
40+
t.Cleanup(server.Close)
41+
42+
repository, err := New("TOKEN").WithBaseURL(server.URL).UpdateVCRRepository(context.Background(), UpdateVCRRepositoryRequest{
43+
TeamID: "team_123",
44+
ProjectID: "prj_123",
45+
IDOrName: "repo_123",
46+
Public: true,
47+
})
48+
if err != nil {
49+
t.Fatalf("UpdateVCRRepository() error = %v", err)
50+
}
51+
if !repository.Public {
52+
t.Fatal("repository.Public = false, want true")
53+
}
54+
if repository.URL != "vcr.vercel.com/acme/storefront/example" {
55+
t.Fatalf("repository.URL = %q, want vcr.vercel.com/acme/storefront/example", repository.URL)
56+
}
57+
if repository.TeamID != "team_123" {
58+
t.Fatalf("repository.TeamID = %q, want team_123", repository.TeamID)
59+
}
60+
}

docs/data-sources/vcr_repository.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ data "vercel_vcr_repository" "example" {
3939
### Read-Only
4040

4141
- `id` (String) The ID of the VCR Repository.
42+
- `public` (Boolean) Whether the repository is pullable by any Vercel team.
4243
- `url` (String) The URL of the repository, composed of the owner slug, the project slug and the repository name (e.g. `vcr.vercel.com/team-slug/project-slug/repository-name`). Use it to push and pull images with Docker-compatible tooling.

docs/resources/vcr_repository.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ resource "vercel_project" "example" {
2727
resource "vercel_vcr_repository" "example" {
2828
project_id = vercel_project.example.id
2929
name = "my-repository"
30+
public = true
3031
}
3132
```
3233

@@ -40,6 +41,7 @@ resource "vercel_vcr_repository" "example" {
4041

4142
### Optional
4243

44+
- `public` (Boolean) Whether the repository is pullable by any Vercel team. Private repositories are only accessible within the same project. Defaults to `false`.
4345
- `team_id` (String) The ID of the team the repository should be created under. Required when configuring a team resource if a default team has not been set in the provider.
4446

4547
### Read-Only

examples/resources/vercel_vcr_repository/resource.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ resource "vercel_project" "example" {
55
resource "vercel_vcr_repository" "example" {
66
project_id = vercel_project.example.id
77
name = "my-repository"
8+
public = true
89
}

vercel/data_source_vcr_repository.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ used by Vercel Functions and Vercel Sandbox.
7171
Description: "The name of the repository.",
7272
Required: true,
7373
},
74+
"public": schema.BoolAttribute{
75+
Computed: true,
76+
Description: "Whether the repository is pullable by any Vercel team.",
77+
},
7478
"url": schema.StringAttribute{
7579
Computed: true,
7680
Description: "The URL of the repository, composed of the owner slug, the project slug and the repository name (e.g. `vcr.vercel.com/team-slug/project-slug/repository-name`). Use it to push and pull images with Docker-compatible tooling.",
@@ -109,6 +113,7 @@ func (d *vcrRepositoryDataSource) Read(ctx context.Context, req datasource.ReadR
109113
"team_id": res.TeamID,
110114
"project_id": res.ProjectID,
111115
"name": res.Name,
116+
"public": res.Public,
112117
})
113118

114119
diags = resp.State.Set(ctx, convertResponseToVCRRepository(res))

vercel/data_source_vcr_repository_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func TestAcc_VCRRepositoryDataSource(t *testing.T) {
2020
resource.TestCheckResourceAttrSet("data.vercel_vcr_repository.test", "id"),
2121
resource.TestCheckResourceAttrSet("data.vercel_vcr_repository.test", "project_id"),
2222
resource.TestCheckResourceAttr("data.vercel_vcr_repository.test", "name", fmt.Sprintf("test-acc-%s", projectSuffix)),
23+
resource.TestCheckResourceAttr("data.vercel_vcr_repository.test", "public", "false"),
2324
resource.TestCheckResourceAttrSet("data.vercel_vcr_repository.test", "url"),
2425
),
2526
},

vercel/resource_vcr_repository.go

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
99
"github.com/hashicorp/terraform-plugin-framework/resource"
1010
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
11+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
1112
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1213
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
1314
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
@@ -88,6 +89,12 @@ used by Vercel Functions and Vercel Sandbox. Images are pushed to and pulled fro
8889
),
8990
},
9091
},
92+
"public": schema.BoolAttribute{
93+
Optional: true,
94+
Computed: true,
95+
Default: booldefault.StaticBool(false),
96+
Description: "Whether the repository is pullable by any Vercel team. Private repositories are only accessible within the same project. Defaults to `false`.",
97+
},
9198
"url": schema.StringAttribute{
9299
Computed: true,
93100
Description: "The URL of the repository, composed of the owner slug, the project slug and the repository name (e.g. `vcr.vercel.com/team-slug/project-slug/repository-name`). Use it to push and pull images with Docker-compatible tooling.",
@@ -102,6 +109,7 @@ type VCRRepository struct {
102109
TeamID types.String `tfsdk:"team_id"`
103110
ProjectID types.String `tfsdk:"project_id"`
104111
Name types.String `tfsdk:"name"`
112+
Public types.Bool `tfsdk:"public"`
105113
URL types.String `tfsdk:"url"`
106114
}
107115

@@ -117,6 +125,7 @@ func convertResponseToVCRRepository(res client.VCRRepository) VCRRepository {
117125
TeamID: types.StringValue(res.TeamID),
118126
ProjectID: types.StringValue(res.ProjectID),
119127
Name: types.StringValue(res.Name),
128+
Public: types.BoolValue(res.Public),
120129
URL: types.StringValue(res.URL),
121130
}
122131
}
@@ -141,11 +150,31 @@ func (r *vcrRepositoryResource) Create(ctx context.Context, req resource.CreateR
141150
)
142151
return
143152
}
153+
if res.Public != plan.Public.ValueBool() {
154+
idOrName := res.ID
155+
if idOrName == "" {
156+
idOrName = res.Name
157+
}
158+
res, err = r.client.UpdateVCRRepository(ctx, client.UpdateVCRRepositoryRequest{
159+
TeamID: res.TeamID,
160+
ProjectID: res.ProjectID,
161+
IDOrName: idOrName,
162+
Public: plan.Public.ValueBool(),
163+
})
164+
if err != nil {
165+
resp.Diagnostics.AddError(
166+
"Error updating newly created VCR Repository",
167+
fmt.Sprintf("Could not set visibility for newly created VCR Repository, unexpected error: %s", err),
168+
)
169+
return
170+
}
171+
}
144172

145173
tflog.Info(ctx, "created vcr repository", map[string]any{
146174
"team_id": res.TeamID,
147175
"project_id": res.ProjectID,
148176
"name": res.Name,
177+
"public": res.Public,
149178
})
150179

151180
diags = resp.State.Set(ctx, convertResponseToVCRRepository(res))
@@ -186,18 +215,53 @@ func (r *vcrRepositoryResource) Read(ctx context.Context, req resource.ReadReque
186215
"team_id": res.TeamID,
187216
"project_id": res.ProjectID,
188217
"name": res.Name,
218+
"public": res.Public,
189219
})
190220

191221
diags = resp.State.Set(ctx, convertResponseToVCRRepository(res))
192222
resp.Diagnostics.Append(diags...)
193223
}
194224

195-
// Update is never called as all attributes force a replacement.
196225
func (r *vcrRepositoryResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
197-
resp.Diagnostics.AddError(
198-
"Error updating VCR Repository",
199-
"VCR Repositories cannot be updated. Any change requires the repository to be replaced.",
200-
)
226+
var plan VCRRepository
227+
diags := req.Plan.Get(ctx, &plan)
228+
resp.Diagnostics.Append(diags...)
229+
if resp.Diagnostics.HasError() {
230+
return
231+
}
232+
233+
res, err := r.client.UpdateVCRRepository(ctx, client.UpdateVCRRepositoryRequest{
234+
TeamID: plan.TeamID.ValueString(),
235+
ProjectID: plan.ProjectID.ValueString(),
236+
IDOrName: plan.ID.ValueString(),
237+
Public: plan.Public.ValueBool(),
238+
})
239+
if client.NotFound(err) {
240+
resp.State.RemoveResource(ctx)
241+
return
242+
}
243+
if err != nil {
244+
resp.Diagnostics.AddError(
245+
"Error updating VCR Repository",
246+
fmt.Sprintf("Could not update VCR Repository %s %s %s, unexpected error: %s",
247+
plan.TeamID.ValueString(),
248+
plan.ProjectID.ValueString(),
249+
plan.Name.ValueString(),
250+
err,
251+
),
252+
)
253+
return
254+
}
255+
256+
tflog.Info(ctx, "updated vcr repository", map[string]any{
257+
"team_id": res.TeamID,
258+
"project_id": res.ProjectID,
259+
"name": res.Name,
260+
"public": res.Public,
261+
})
262+
263+
diags = resp.State.Set(ctx, convertResponseToVCRRepository(res))
264+
resp.Diagnostics.Append(diags...)
201265
}
202266

203267
func (r *vcrRepositoryResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {

vercel/resource_vcr_repository_test.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"github.com/vercel/terraform-provider-vercel/v5/client"
1212
)
1313

14-
func testCheckVCRRepositoryExists(testClient *client.Client, teamID string, n string) resource.TestCheckFunc {
14+
func testCheckVCRRepositoryExists(testClient *client.Client, teamID string, expectedPublic bool, n string) resource.TestCheckFunc {
1515
return func(s *terraform.State) error {
1616
rs, ok := s.RootModule().Resources[n]
1717
if !ok {
@@ -25,14 +25,17 @@ func testCheckVCRRepositoryExists(testClient *client.Client, teamID string, n st
2525
projectID := rs.Primary.Attributes["project_id"]
2626
name := rs.Primary.Attributes["name"]
2727

28-
_, err := testClient.GetVCRRepository(context.TODO(), client.GetVCRRepositoryRequest{
28+
repository, err := testClient.GetVCRRepository(context.TODO(), client.GetVCRRepositoryRequest{
2929
TeamID: teamID,
3030
ProjectID: projectID,
3131
IDOrName: name,
3232
})
3333
if client.NotFound(err) {
3434
return fmt.Errorf("test failed because the vcr repository %s %s %s - %s could not be found", teamID, projectID, name, rs.Primary.ID)
3535
}
36+
if err == nil && repository.Public != expectedPublic {
37+
return fmt.Errorf("vcr repository public = %t, want %t", repository.Public, expectedPublic)
38+
}
3639
return err
3740
}
3841
}
@@ -44,15 +47,23 @@ func TestAcc_VCRRepositoryResource(t *testing.T) {
4447
CheckDestroy: testAccProjectDestroy(testClient(t), "vercel_project.test", testTeam(t)),
4548
Steps: []resource.TestStep{
4649
{
47-
Config: cfg(testAccVCRRepository(projectSuffix)),
50+
Config: cfg(testAccVCRRepository(projectSuffix, false)),
4851
Check: resource.ComposeAggregateTestCheckFunc(
49-
testCheckVCRRepositoryExists(testClient(t), testTeam(t), "vercel_vcr_repository.test"),
52+
testCheckVCRRepositoryExists(testClient(t), testTeam(t), false, "vercel_vcr_repository.test"),
5053
resource.TestCheckResourceAttrSet("vercel_vcr_repository.test", "id"),
5154
resource.TestCheckResourceAttrSet("vercel_vcr_repository.test", "project_id"),
5255
resource.TestCheckResourceAttr("vercel_vcr_repository.test", "name", fmt.Sprintf("test-acc-%s", projectSuffix)),
56+
resource.TestCheckResourceAttr("vercel_vcr_repository.test", "public", "false"),
5357
resource.TestCheckResourceAttrSet("vercel_vcr_repository.test", "url"),
5458
),
5559
},
60+
{
61+
Config: cfg(testAccVCRRepository(projectSuffix, true)),
62+
Check: resource.ComposeAggregateTestCheckFunc(
63+
testCheckVCRRepositoryExists(testClient(t), testTeam(t), true, "vercel_vcr_repository.test"),
64+
resource.TestCheckResourceAttr("vercel_vcr_repository.test", "public", "true"),
65+
),
66+
},
5667
{
5768
ResourceName: "vercel_vcr_repository.test",
5869
ImportState: true,
@@ -78,7 +89,7 @@ func getVCRRepositoryImportID(n string) resource.ImportStateIdFunc {
7889
}
7990
}
8091

81-
func testAccVCRRepository(projectSuffix string) string {
92+
func testAccVCRRepository(projectSuffix string, public bool) string {
8293
return fmt.Sprintf(`
8394
resource "vercel_project" "test" {
8495
name = "test-acc-vcr-repo-%[1]s"
@@ -87,6 +98,7 @@ resource "vercel_project" "test" {
8798
resource "vercel_vcr_repository" "test" {
8899
project_id = vercel_project.test.id
89100
name = "test-acc-%[1]s"
101+
public = %[2]t
90102
}
91-
`, projectSuffix)
103+
`, projectSuffix, public)
92104
}

0 commit comments

Comments
 (0)