Skip to content

Commit 850f8d4

Browse files
authored
Merge pull request #75 from DrFaust92/forked-push
Add Forked repos
2 parents edb0e62 + 27f941d commit 850f8d4

File tree

5 files changed

+577
-5
lines changed

5 files changed

+577
-5
lines changed

bitbucket/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func Provider() *schema.Provider {
5353
"bitbucket_group_membership": resourceGroupMembership(),
5454
"bitbucket_default_reviewers": resourceDefaultReviewers(),
5555
"bitbucket_repository": resourceRepository(),
56+
"bitbucket_forked_repository": resourceForkedRepository(),
5657
"bitbucket_repository_variable": resourceRepositoryVariable(),
5758
"bitbucket_project": resourceProject(),
5859
"bitbucket_deploy_key": resourceDeployKey(),
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
package bitbucket
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
"strings"
9+
10+
"github.com/DrFaust92/bitbucket-go-client"
11+
"github.com/antihax/optional"
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
13+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
14+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
15+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
16+
)
17+
18+
func resourceForkedRepository() *schema.Resource {
19+
return &schema.Resource{
20+
CreateContext: resourceForkedRepositoryCreate,
21+
Update: resourceRepositoryUpdate,
22+
ReadContext: resourceForkedRepositoryRead,
23+
Delete: resourceRepositoryDelete,
24+
Importer: &schema.ResourceImporter{
25+
StateContext: schema.ImportStatePassthroughContext,
26+
},
27+
Schema: map[string]*schema.Schema{
28+
"scm": {
29+
Type: schema.TypeString,
30+
Computed: true,
31+
},
32+
"has_wiki": {
33+
Type: schema.TypeBool,
34+
Optional: true,
35+
Default: false,
36+
},
37+
"has_issues": {
38+
Type: schema.TypeBool,
39+
Optional: true,
40+
Default: false,
41+
},
42+
"website": {
43+
Type: schema.TypeString,
44+
Optional: true,
45+
},
46+
"clone_ssh": {
47+
Type: schema.TypeString,
48+
Computed: true,
49+
},
50+
"clone_https": {
51+
Type: schema.TypeString,
52+
Computed: true,
53+
},
54+
"project_key": {
55+
Type: schema.TypeString,
56+
Optional: true,
57+
Computed: true,
58+
},
59+
"is_private": {
60+
Type: schema.TypeBool,
61+
Optional: true,
62+
Default: true,
63+
},
64+
"pipelines_enabled": {
65+
Type: schema.TypeBool,
66+
Optional: true,
67+
Default: false,
68+
},
69+
"fork_policy": {
70+
Type: schema.TypeString,
71+
Optional: true,
72+
Default: "allow_forks",
73+
ValidateFunc: validation.StringInSlice([]string{"allow_forks", "no_public_forks", "no_forks"}, false),
74+
},
75+
"language": {
76+
Type: schema.TypeString,
77+
Optional: true,
78+
},
79+
"description": {
80+
Type: schema.TypeString,
81+
Optional: true,
82+
},
83+
"owner": {
84+
Type: schema.TypeString,
85+
Required: true,
86+
},
87+
"name": {
88+
Type: schema.TypeString,
89+
Required: true,
90+
},
91+
"slug": {
92+
Type: schema.TypeString,
93+
Optional: true,
94+
Computed: true,
95+
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
96+
return computeSlug(old) == computeSlug(new)
97+
},
98+
},
99+
"uuid": {
100+
Type: schema.TypeString,
101+
Computed: true,
102+
},
103+
"link": {
104+
Type: schema.TypeList,
105+
Optional: true,
106+
Computed: true,
107+
MaxItems: 1,
108+
Elem: &schema.Resource{
109+
Schema: map[string]*schema.Schema{
110+
"avatar": {
111+
Type: schema.TypeList,
112+
Optional: true,
113+
MaxItems: 1,
114+
Elem: &schema.Resource{
115+
Schema: map[string]*schema.Schema{
116+
"href": {
117+
Type: schema.TypeString,
118+
Optional: true,
119+
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
120+
return strings.HasPrefix(old, "https://bytebucket.org/ravatar/")
121+
},
122+
},
123+
},
124+
},
125+
},
126+
},
127+
},
128+
},
129+
"parent": {
130+
Type: schema.TypeMap,
131+
Elem: &schema.Schema{
132+
Type: schema.TypeString,
133+
},
134+
Required: true,
135+
ForceNew: true,
136+
ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
137+
v := val.(map[string]interface{})
138+
if _, ok := v["slug"]; !ok {
139+
errs = append(errs, fmt.Errorf("A repository 'slug' is required when specifying a parent to fork from."))
140+
}
141+
if _, ok := v["owner"]; !ok {
142+
errs = append(errs, fmt.Errorf("A repository 'owner' is required when specifying a parent to fork from."))
143+
}
144+
return warns, errs
145+
},
146+
},
147+
},
148+
}
149+
}
150+
151+
type forkWorkspace struct {
152+
Slug string `json:"slug,omitempty"`
153+
}
154+
155+
type forkedRepositoryBody struct {
156+
Name string `json:"name,omitempty"`
157+
Language string `json:"language,omitempty"`
158+
IsPrivate bool `json:"is_private,omitempty"`
159+
Description string `json:"description,omitempty"`
160+
ForkPolicy string `json:"fork_policy,omitempty"`
161+
HasWiki bool `json:"has_wiki,omitempty"`
162+
HasIssues bool `json:"has_issues,omitempty"`
163+
Links *bitbucket.RepositoryLinks `json:"links,omitempty"`
164+
Project *bitbucket.Project `json:"project,omitempty"`
165+
Workspace *forkWorkspace `json:"workspace,omitempty"`
166+
}
167+
168+
func createForkedRepositoryFromRepository(repo *bitbucket.Repository, targetWorkspaceSlug string) *forkedRepositoryBody {
169+
forkedRepo := &forkedRepositoryBody{
170+
Name: repo.Name,
171+
Language: repo.Language,
172+
IsPrivate: repo.IsPrivate,
173+
Description: repo.Description,
174+
ForkPolicy: repo.ForkPolicy,
175+
HasWiki: repo.HasWiki,
176+
HasIssues: repo.HasIssues,
177+
}
178+
179+
workspace := &forkWorkspace{
180+
Slug: targetWorkspaceSlug,
181+
}
182+
forkedRepo.Workspace = workspace
183+
if repo.Links != nil {
184+
forkedRepo.Links = repo.Links
185+
}
186+
if repo.Project != nil {
187+
forkedRepo.Project = repo.Project
188+
}
189+
return forkedRepo
190+
}
191+
192+
func resourceForkedRepositoryCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
193+
c := m.(Clients).genClient
194+
repoApi := c.ApiClient.RepositoriesApi
195+
pipeApi := c.ApiClient.PipelinesApi
196+
repo := newRepositoryFromResource(d)
197+
198+
var repoSlug string
199+
repoSlug = d.Get("slug").(string)
200+
if repoSlug == "" {
201+
repoSlug = d.Get("name").(string)
202+
}
203+
repoSlug = computeSlug(repoSlug)
204+
205+
workspace := d.Get("owner").(string)
206+
parent := d.Get("parent").(map[string]interface{})
207+
parentRepoSlug := parent["slug"].(string)
208+
parentWorkspace := parent["owner"].(string)
209+
requestRepo := createForkedRepositoryFromRepository(repo, workspace)
210+
repoBody := &bitbucket.RepositoriesApiRepositoriesWorkspaceRepoSlugForksPostOpts{
211+
Body: optional.NewInterface(requestRepo),
212+
}
213+
_, _, err := repoApi.RepositoriesWorkspaceRepoSlugForksPost(c.AuthContext, parentRepoSlug, parentWorkspace, repoBody)
214+
if err != nil {
215+
swaggerErr := err.(bitbucket.GenericSwaggerError)
216+
return diag.Errorf("error forking repository (%s) from (%s): %s", repoSlug, parentRepoSlug, string(swaggerErr.Body()))
217+
}
218+
219+
d.SetId(string(fmt.Sprintf("%s/%s", d.Get("owner").(string), repoSlug)))
220+
221+
pipelinesEnabled := d.Get("pipelines_enabled").(bool)
222+
pipelinesConfig := &bitbucket.PipelinesConfig{Enabled: pipelinesEnabled}
223+
224+
retryErr := resource.RetryContext(ctx, d.Timeout(schema.TimeoutCreate), func() *resource.RetryError {
225+
_, pipelineResponse, err := pipeApi.UpdateRepositoryPipelineConfig(c.AuthContext, *pipelinesConfig, workspace, repoSlug)
226+
if pipelineResponse.StatusCode == 403 || pipelineResponse.StatusCode == 404 {
227+
return resource.RetryableError(
228+
fmt.Errorf("Permissions error setting Pipelines config, retrying..."),
229+
)
230+
}
231+
if err != nil {
232+
return resource.NonRetryableError(fmt.Errorf("unexpected error enabling pipeline for repository (%s): %w", repoSlug, err))
233+
}
234+
return nil
235+
})
236+
if retryErr != nil {
237+
return diag.FromErr(retryErr)
238+
}
239+
240+
return diag.FromErr(resourceRepositoryRead(d, m))
241+
}
242+
243+
func resourceForkedRepositoryRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
244+
id := d.Id()
245+
if id != "" {
246+
idparts := strings.Split(id, "/")
247+
if len(idparts) == 2 {
248+
d.Set("owner", idparts[0])
249+
d.Set("slug", idparts[1])
250+
} else {
251+
return diag.Errorf("incorrect ID format, should match `owner/slug`")
252+
}
253+
}
254+
255+
var repoSlug string
256+
repoSlug = d.Get("slug").(string)
257+
if repoSlug == "" {
258+
repoSlug = d.Get("name").(string)
259+
}
260+
repoSlug = computeSlug(repoSlug)
261+
262+
workspace := d.Get("owner").(string)
263+
c := m.(Clients).genClient
264+
repoApi := c.ApiClient.RepositoriesApi
265+
pipeApi := c.ApiClient.PipelinesApi
266+
267+
repoRes, res, err := repoApi.RepositoriesWorkspaceRepoSlugGet(c.AuthContext, repoSlug, workspace)
268+
if err != nil {
269+
return diag.FromErr(fmt.Errorf("error reading repository (%s): %w", d.Id(), err))
270+
}
271+
272+
if res.StatusCode == http.StatusNotFound {
273+
log.Printf("[WARN] Repository (%s) not found, removing from state", d.Id())
274+
d.SetId("")
275+
return nil
276+
}
277+
278+
d.Set("scm", repoRes.Scm)
279+
d.Set("is_private", repoRes.IsPrivate)
280+
d.Set("has_wiki", repoRes.HasWiki)
281+
d.Set("has_issues", repoRes.HasIssues)
282+
d.Set("name", repoRes.Name)
283+
d.Set("slug", repoRes.Name)
284+
d.Set("language", repoRes.Language)
285+
d.Set("fork_policy", repoRes.ForkPolicy)
286+
// d.Set("website", repoRes.Website)
287+
d.Set("description", repoRes.Description)
288+
d.Set("project_key", repoRes.Project.Key)
289+
d.Set("uuid", repoRes.Uuid)
290+
291+
if repoRes.Parent != nil {
292+
parentMap := make(map[string]string)
293+
parentOwner, parentSlug, splitErr := splitFullName(repoRes.Parent.FullName)
294+
if splitErr != nil {
295+
return diag.Errorf("error reading forked repository (%s)", d.Get("name").(string))
296+
}
297+
parentMap["owner"] = parentOwner
298+
parentMap["slug"] = parentSlug
299+
d.Set("parent", parentMap)
300+
}
301+
302+
for _, cloneURL := range repoRes.Links.Clone {
303+
if cloneURL.Name == "https" {
304+
d.Set("clone_https", cloneURL.Href)
305+
} else {
306+
d.Set("clone_ssh", cloneURL.Href)
307+
}
308+
}
309+
310+
d.Set("link", flattenLinks(repoRes.Links))
311+
312+
pipelinesConfigReq, res, err := pipeApi.GetRepositoryPipelineConfig(c.AuthContext, workspace, repoSlug)
313+
314+
if err != nil && res.StatusCode != http.StatusNotFound {
315+
return diag.FromErr(err)
316+
}
317+
318+
if res.StatusCode == 200 {
319+
d.Set("pipelines_enabled", pipelinesConfigReq.Enabled)
320+
} else if res.StatusCode == http.StatusNotFound {
321+
d.Set("pipelines_enabled", false)
322+
}
323+
324+
return nil
325+
}

0 commit comments

Comments
 (0)