Skip to content

Commit e36aa61

Browse files
authored
add initial support for environments (#120)
Currently only supports listing, creating, getting, and deleting environments. Updating existing environments is also possible but unfortunately the existing API documentation for the update endpoint is incomplete, so I'm skipping it for now. Documented at: https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Bworkspace%7D/%7Brepo_slug%7D/environments Signed-off-by: Kent R. Spillner <[email protected]>
1 parent 9af2f29 commit e36aa61

File tree

3 files changed

+310
-5
lines changed

3 files changed

+310
-5
lines changed

bitbucket.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ type repository interface {
4343
GetFileBlob(opt RepositoryBlobOptions) (*RepositoryBlob, error)
4444
ListBranches(opt RepositoryBranchOptions) (*RepositoryBranches, error)
4545
BranchingModel(opt RepositoryBranchingModelOptions) (*BranchingModel, error)
46+
ListEnvironments(opt RepositoryEnvironmentsOptions) (*Environments, error)
47+
AddEnvironment(opt RepositoryEnvironmentOptions) (*Environment, error)
48+
DeleteEnvironment(opt RepositoryEnvironmentDeleteOptions) (interface{}, error)
49+
GetEnvironment(opt RepositoryEnvironmentOptions) (*Environment, error)
4650
}
4751

4852
type repositories interface {
@@ -320,3 +324,35 @@ type PipelinesOptions struct {
320324
IDOrUuid string `json:"ID"`
321325
StepUuid string `json:"StepUUID"`
322326
}
327+
328+
type RepositoryEnvironmentsOptions struct {
329+
Owner string `json:"owner"`
330+
RepoSlug string `json:"repo_slug"`
331+
}
332+
333+
type RepositoryEnvironmentTypeOption int
334+
335+
const (
336+
Test RepositoryEnvironmentTypeOption = iota
337+
Staging
338+
Production
339+
)
340+
341+
func (e RepositoryEnvironmentTypeOption) String() string {
342+
return [...]string{"Test", "Staging", "Production"}[e]
343+
}
344+
345+
type RepositoryEnvironmentOptions struct {
346+
Owner string `json:"owner"`
347+
RepoSlug string `json:"repo_slug"`
348+
Uuid string `json:"uuid"`
349+
Name string `json:"name"`
350+
EnvironmentType RepositoryEnvironmentTypeOption `json:"environment_type"`
351+
Rank int `json:"rank"`
352+
}
353+
354+
type RepositoryEnvironmentDeleteOptions struct {
355+
Owner string `json:"owner"`
356+
RepoSlug string `json:"repo_slug"`
357+
Uuid string `json:"uuid"`
358+
}

repository.go

Lines changed: 165 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package bitbucket
33
import (
44
"encoding/json"
55
"errors"
6+
"fmt"
67
"io/ioutil"
78
"net/url"
89
"os"
@@ -141,6 +142,29 @@ type BranchModel struct {
141142
Use_Mainbranch bool
142143
}
143144

145+
type Environments struct {
146+
Page int
147+
Pagelen int
148+
MaxDepth int
149+
Size int
150+
Next string
151+
Environments []Environment
152+
}
153+
154+
type EnvironmentType struct {
155+
Name string
156+
Rank int
157+
Type string
158+
}
159+
160+
type Environment struct {
161+
Uuid string
162+
Name string
163+
EnvironmentType EnvironmentType
164+
Rank int
165+
Type string
166+
}
167+
144168
func (r *Repository) Create(ro *RepositoryOptions) (*Repository, error) {
145169
data := r.buildRepositoryBody(ro)
146170
urlStr := r.c.requestUrl("/repositories/%s/%s", ro.Owner, ro.RepoSlug)
@@ -426,15 +450,46 @@ func (r *Repository) BranchingModel(rbmo *RepositoryBranchingModelOptions) (*Bra
426450
return decodeBranchingModel(response)
427451
}
428452

429-
func (r *Repository) buildJsonBody(body map[string]interface{}) string {
453+
func (r *Repository) ListEnvironments(opt *RepositoryEnvironmentsOptions) (*Environments, error) {
454+
urlStr := r.c.requestUrl("/repositories/%s/%s/environments/", opt.Owner, opt.RepoSlug)
455+
res, err := r.c.executeRaw("GET", urlStr, "")
456+
if err != nil {
457+
return nil, err
458+
}
430459

431-
data, err := json.Marshal(body)
460+
bodyBytes, err := ioutil.ReadAll(res)
432461
if err != nil {
433-
pp.Println(err)
434-
os.Exit(9)
462+
return nil, err
435463
}
436464

437-
return string(data)
465+
bodyString := string(bodyBytes)
466+
return decodeEnvironments(bodyString)
467+
}
468+
469+
func (r *Repository) AddEnvironment(opt *RepositoryEnvironmentOptions) (*Environment, error) {
470+
body := r.buildEnvironmentBody(opt)
471+
urlStr := r.c.requestUrl("/repositories/%s/%s/environments/", opt.Owner, opt.RepoSlug)
472+
res, err := r.c.execute("POST", urlStr, body)
473+
if err != nil {
474+
return nil, err
475+
}
476+
477+
return decodeEnvironment(res)
478+
}
479+
480+
func (r *Repository) DeleteEnvironment(opt *RepositoryEnvironmentDeleteOptions) (interface{}, error) {
481+
urlStr := r.c.requestUrl("/repositories/%s/%s/environments/%s", opt.Owner, opt.RepoSlug, opt.Uuid)
482+
return r.c.execute("DELETE", urlStr, "")
483+
}
484+
485+
func (r *Repository) GetEnvironment(opt *RepositoryEnvironmentOptions) (*Environment, error) {
486+
urlStr := r.c.requestUrl("/repositories/%s/%s/environments/%s", opt.Owner, opt.RepoSlug, opt.Uuid)
487+
res, err := r.c.execute("GET", urlStr, "")
488+
if err != nil {
489+
return nil, err
490+
}
491+
492+
return decodeEnvironment(res)
438493
}
439494

440495
func (r *Repository) buildRepositoryBody(ro *RepositoryOptions) string {
@@ -570,6 +625,34 @@ func (r *Repository) buildTagBody(rbo *RepositoryTagCreationOptions) string {
570625
return r.buildJsonBody(body)
571626
}
572627

628+
func (r *Repository) buildEnvironmentBody(opt *RepositoryEnvironmentOptions) string {
629+
body := map[string]interface{}{}
630+
631+
body["environment_type"] = map[string]interface{}{
632+
"name": opt.EnvironmentType.String(),
633+
"rank": opt.Rank,
634+
"type": "deployment_environment_type",
635+
}
636+
if opt.Uuid != "" {
637+
body["uuid"] = opt.Uuid
638+
}
639+
body["name"] = opt.Name
640+
body["rank"] = opt.Rank
641+
642+
return r.buildJsonBody(body)
643+
}
644+
645+
func (r *Repository) buildJsonBody(body map[string]interface{}) string {
646+
647+
data, err := json.Marshal(body)
648+
if err != nil {
649+
pp.Println(err)
650+
os.Exit(9)
651+
}
652+
653+
return string(data)
654+
}
655+
573656
func decodeRepository(repoResponse interface{}) (*Repository, error) {
574657
repoMap := repoResponse.(map[string]interface{})
575658

@@ -862,6 +945,83 @@ func decodeBranchingModel(branchingModelResponse interface{}) (*BranchingModel,
862945
return branchingModel, nil
863946
}
864947

948+
func decodeEnvironments(response string) (*Environments, error) {
949+
var responseMap map[string]interface{}
950+
err := json.Unmarshal([]byte(response), &responseMap)
951+
if err != nil {
952+
return nil, err
953+
}
954+
955+
values := responseMap["values"].([]interface{})
956+
var environmentsArray []Environment
957+
var errs error = nil
958+
for idx, value := range values {
959+
var environment Environment
960+
err = mapstructure.Decode(value, &environment)
961+
if err != nil {
962+
if errs == nil {
963+
errs = err
964+
} else {
965+
errs = fmt.Errorf("%w; environment %d: %w", errs, idx, err)
966+
}
967+
} else {
968+
environmentsArray = append(environmentsArray, environment)
969+
}
970+
}
971+
972+
page, ok := responseMap["page"].(float64)
973+
if !ok {
974+
page = 0
975+
}
976+
977+
pagelen, ok := responseMap["pagelen"].(float64)
978+
if !ok {
979+
pagelen = 0
980+
}
981+
982+
max_depth, ok := responseMap["max_depth"].(float64)
983+
if !ok {
984+
max_depth = 0
985+
}
986+
987+
size, ok := responseMap["size"].(float64)
988+
if !ok {
989+
size = 0
990+
}
991+
992+
next, ok := responseMap["next"].(string)
993+
if !ok {
994+
next = ""
995+
}
996+
997+
environments := Environments{
998+
Page: int(page),
999+
Pagelen: int(pagelen),
1000+
MaxDepth: int(max_depth),
1001+
Size: int(size),
1002+
Next: next,
1003+
Environments: environmentsArray,
1004+
}
1005+
1006+
return &environments, nil
1007+
}
1008+
1009+
func decodeEnvironment(response interface{}) (*Environment, error) {
1010+
responseMap := response.(map[string]interface{})
1011+
1012+
if responseMap["type"] == "error" {
1013+
return nil, DecodeError(responseMap)
1014+
}
1015+
1016+
var environment = new(Environment)
1017+
err := mapstructure.Decode(responseMap, &environment)
1018+
if err != nil {
1019+
return nil, err
1020+
}
1021+
1022+
return environment, nil
1023+
}
1024+
8651025
func (rf RepositoryFile) String() string {
8661026
return rf.Path
8671027
}

tests/environment_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package tests
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
_ "github.com/k0kubun/pp"
8+
"github.com/ktrysmt/go-bitbucket"
9+
)
10+
11+
func TestListEnvironments(t *testing.T) {
12+
13+
user := os.Getenv("BITBUCKET_TEST_USERNAME")
14+
pass := os.Getenv("BITBUCKET_TEST_PASSWORD")
15+
owner := os.Getenv("BITBUCKET_TEST_OWNER")
16+
repo := os.Getenv("BITBUCKET_TEST_REPOSLUG")
17+
18+
if user == "" {
19+
t.Error("BITBUCKET_TEST_USERNAME is empty.")
20+
}
21+
if pass == "" {
22+
t.Error("BITBUCKET_TEST_PASSWORD is empty.")
23+
}
24+
if owner == "" {
25+
t.Error("BITBUCKET_TEST_OWNER is empty.")
26+
}
27+
if repo == "" {
28+
t.Error("BITBUCKET_TEST_REPOSLUG is empty.")
29+
}
30+
31+
c := bitbucket.NewBasicAuth(user, pass)
32+
33+
opt := &bitbucket.RepositoryEnvironmentsOptions{
34+
Owner: owner,
35+
RepoSlug: repo,
36+
}
37+
38+
res, err := c.Repositories.Repository.ListEnvironments(opt)
39+
if err != nil {
40+
t.Error(err)
41+
}
42+
43+
if res == nil {
44+
t.Error("list didn't return any environments")
45+
}
46+
}
47+
48+
func TestEndToEndEnvironments(t *testing.T) {
49+
50+
user := os.Getenv("BITBUCKET_TEST_USERNAME")
51+
pass := os.Getenv("BITBUCKET_TEST_PASSWORD")
52+
owner := os.Getenv("BITBUCKET_TEST_OWNER")
53+
repo := os.Getenv("BITBUCKET_TEST_REPOSLUG")
54+
55+
if user == "" {
56+
t.Error("BITBUCKET_TEST_USERNAME is empty.")
57+
}
58+
if pass == "" {
59+
t.Error("BITBUCKET_TEST_PASSWORD is empty.")
60+
}
61+
if owner == "" {
62+
t.Error("BITBUCKET_TEST_OWNER is empty.")
63+
}
64+
if repo == "" {
65+
t.Error("BITBUCKET_TEST_REPOSLUG is empty.")
66+
}
67+
68+
c := bitbucket.NewBasicAuth(user, pass)
69+
70+
opt := &bitbucket.RepositoryEnvironmentOptions{
71+
Owner: owner,
72+
RepoSlug: repo,
73+
Name: "foo",
74+
EnvironmentType: bitbucket.Test,
75+
}
76+
77+
environment, err := c.Repositories.Repository.AddEnvironment(opt)
78+
if err != nil {
79+
t.Error(err)
80+
}
81+
82+
if environment.Uuid == "" {
83+
t.Error("new environment does not have a UUID")
84+
}
85+
86+
opt.Name = ""
87+
opt.Uuid = environment.Uuid
88+
89+
foundEnvironment, err := c.Repositories.Repository.GetEnvironment(opt)
90+
if err != nil {
91+
t.Error(err)
92+
}
93+
94+
if foundEnvironment.Name != "foo" {
95+
t.Errorf("got environment with wrong name: %s", foundEnvironment.Name)
96+
}
97+
98+
deleteOpt := &bitbucket.RepositoryEnvironmentDeleteOptions{
99+
Owner: owner,
100+
RepoSlug: repo,
101+
Uuid: environment.Uuid,
102+
}
103+
104+
// On success the delete API doesn't return any content (HTTP status 204)
105+
_, err = c.Repositories.Repository.DeleteEnvironment(deleteOpt)
106+
if err != nil {
107+
t.Error(err)
108+
}
109+
}

0 commit comments

Comments
 (0)