Skip to content

Commit de25448

Browse files
authored
add initial support for deployment variables (#121)
Deployment variables are almost identical to pipeline variables, except they must be associated with a particular environment. Also, apparently the current API for listing deployment variables does not (yet?) support filtering, paging or sorting. Supports listing, adding, deleting and updating deployment variables, but there is no API for getting individual deployment variables. Documented at: https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Bworkspace%7D/%7Brepo_slug%7D/deployments_config/environments/%7Benvironment_uuid%7D/variables Signed-off-by: Kent R. Spillner <[email protected]>
1 parent e36aa61 commit de25448

File tree

3 files changed

+357
-0
lines changed

3 files changed

+357
-0
lines changed

bitbucket.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ type repository interface {
4747
AddEnvironment(opt RepositoryEnvironmentOptions) (*Environment, error)
4848
DeleteEnvironment(opt RepositoryEnvironmentDeleteOptions) (interface{}, error)
4949
GetEnvironment(opt RepositoryEnvironmentOptions) (*Environment, error)
50+
ListDeploymentVariables(opt RepositoryDeploymentVariablesOptions) (*DeploymentVariables, error)
51+
AddDeploymentVariable(opt RepositoryDeploymentVariableOptions) (*DeploymentVariable, error)
52+
DeleteDeploymentVariable(opt RepositoryDeploymentVariableDeleteOptions) (interface{}, error)
53+
UpdateDeploymentVariable(opt RepositoryDeploymentVariableOptions) (*DeploymentVariable, error)
5054
}
5155

5256
type repositories interface {
@@ -356,3 +360,31 @@ type RepositoryEnvironmentDeleteOptions struct {
356360
RepoSlug string `json:"repo_slug"`
357361
Uuid string `json:"uuid"`
358362
}
363+
364+
type RepositoryDeploymentVariablesOptions struct {
365+
Owner string `json:"owner"`
366+
RepoSlug string `json:"repo_slug"`
367+
Environment *Environment `json:"environment"`
368+
Query string `json:"q"`
369+
Sort string `json:"sort"`
370+
PageNum int `json:"page"`
371+
Pagelen int `json:"pagelen"`
372+
MaxDepth int `json:"max_depth"`
373+
}
374+
375+
type RepositoryDeploymentVariableOptions struct {
376+
Owner string `json:"owner"`
377+
RepoSlug string `json:"repo_slug"`
378+
Environment *Environment `json:"environment"`
379+
Uuid string `json:"uuid"`
380+
Key string `json:"key"`
381+
Value string `json:"value"`
382+
Secured bool `json:"secured"`
383+
}
384+
385+
type RepositoryDeploymentVariableDeleteOptions struct {
386+
Owner string `json:"owner"`
387+
RepoSlug string `json:"repo_slug"`
388+
Environment *Environment `json:"environment"`
389+
Uuid string `json:"uuid"`
390+
}

repository.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,23 @@ type Environment struct {
165165
Type string
166166
}
167167

168+
type DeploymentVariables struct {
169+
Page int
170+
Pagelen int
171+
MaxDepth int
172+
Size int
173+
Next string
174+
Variables []DeploymentVariable
175+
}
176+
177+
type DeploymentVariable struct {
178+
Type string
179+
Uuid string
180+
Key string
181+
Value string
182+
Secured bool
183+
}
184+
168185
func (r *Repository) Create(ro *RepositoryOptions) (*Repository, error) {
169186
data := r.buildRepositoryBody(ro)
170187
urlStr := r.c.requestUrl("/repositories/%s/%s", ro.Owner, ro.RepoSlug)
@@ -492,6 +509,70 @@ func (r *Repository) GetEnvironment(opt *RepositoryEnvironmentOptions) (*Environ
492509
return decodeEnvironment(res)
493510
}
494511

512+
func (r *Repository) ListDeploymentVariables(opt *RepositoryDeploymentVariablesOptions) (*DeploymentVariables, error) {
513+
params := url.Values{}
514+
if opt.Query != "" {
515+
params.Add("q", opt.Query)
516+
}
517+
518+
if opt.Sort != "" {
519+
params.Add("sort", opt.Sort)
520+
}
521+
522+
if opt.PageNum > 0 {
523+
params.Add("page", strconv.Itoa(opt.PageNum))
524+
}
525+
526+
if opt.Pagelen > 0 {
527+
params.Add("pagelen", strconv.Itoa(opt.Pagelen))
528+
}
529+
530+
if opt.MaxDepth > 0 {
531+
params.Add("max_depth", strconv.Itoa(opt.MaxDepth))
532+
}
533+
534+
urlStr := r.c.requestUrl("/repositories/%s/%s/deployments_config/environments/%s/variables?%s", opt.Owner, opt.RepoSlug, opt.Environment.Uuid, params.Encode())
535+
response, err := r.c.executeRaw("GET", urlStr, "")
536+
if err != nil {
537+
return nil, err
538+
}
539+
bodyBytes, err := ioutil.ReadAll(response)
540+
if err != nil {
541+
return nil, err
542+
}
543+
bodyString := string(bodyBytes)
544+
return decodeDeploymentVariables(bodyString)
545+
}
546+
547+
func (r *Repository) AddDeploymentVariable(opt *RepositoryDeploymentVariableOptions) (*DeploymentVariable, error) {
548+
body := r.buildDeploymentVariableBody(opt)
549+
urlStr := r.c.requestUrl("/repositories/%s/%s/deployments_config/environments/%s/variables", opt.Owner, opt.RepoSlug, opt.Environment.Uuid)
550+
551+
response, err := r.c.execute("POST", urlStr, body)
552+
if err != nil {
553+
return nil, err
554+
}
555+
556+
return decodeDeploymentVariable(response)
557+
}
558+
559+
func (r *Repository) DeleteDeploymentVariable(opt *RepositoryDeploymentVariableDeleteOptions) (interface{}, error) {
560+
urlStr := r.c.requestUrl("/repositories/%s/%s/deployments_config/environments/%s/variables/%s", opt.Owner, opt.RepoSlug, opt.Environment.Uuid, opt.Uuid)
561+
return r.c.execute("DELETE", urlStr, "")
562+
}
563+
564+
func (r *Repository) UpdateDeploymentVariable(opt *RepositoryDeploymentVariableOptions) (*DeploymentVariable, error) {
565+
body := r.buildDeploymentVariableBody(opt)
566+
urlStr := r.c.requestUrl("/repositories/%s/%s/deployments_config/environments/%s/variables/%s", opt.Owner, opt.RepoSlug, opt.Environment.Uuid, opt.Uuid)
567+
568+
response, err := r.c.execute("PUT", urlStr, body)
569+
if err != nil {
570+
return nil, err
571+
}
572+
573+
return decodeDeploymentVariable(response)
574+
}
575+
495576
func (r *Repository) buildRepositoryBody(ro *RepositoryOptions) string {
496577

497578
body := map[string]interface{}{}
@@ -642,6 +723,19 @@ func (r *Repository) buildEnvironmentBody(opt *RepositoryEnvironmentOptions) str
642723
return r.buildJsonBody(body)
643724
}
644725

726+
func (r *Repository) buildDeploymentVariableBody(opt *RepositoryDeploymentVariableOptions) string {
727+
body := map[string]interface{}{}
728+
729+
if opt.Uuid != "" {
730+
body["uuid"] = opt.Uuid
731+
}
732+
body["key"] = opt.Key
733+
body["value"] = opt.Value
734+
body["secured"] = opt.Secured
735+
736+
return r.buildJsonBody(body)
737+
}
738+
645739
func (r *Repository) buildJsonBody(body map[string]interface{}) string {
646740

647741
data, err := json.Marshal(body)
@@ -1022,6 +1116,83 @@ func decodeEnvironment(response interface{}) (*Environment, error) {
10221116
return environment, nil
10231117
}
10241118

1119+
func decodeDeploymentVariables(response string) (*DeploymentVariables, error) {
1120+
var responseMap map[string]interface{}
1121+
err := json.Unmarshal([]byte(response), &responseMap)
1122+
if err != nil {
1123+
return nil, err
1124+
}
1125+
1126+
values := responseMap["values"].([]interface{})
1127+
var variablesArray []DeploymentVariable
1128+
var errs error = nil
1129+
for idx, value := range values {
1130+
var variable DeploymentVariable
1131+
err = mapstructure.Decode(value, &variable)
1132+
if err != nil {
1133+
if errs == nil {
1134+
errs = err
1135+
} else {
1136+
errs = fmt.Errorf("%w; deployment variable %d: %w", errs, idx, err)
1137+
}
1138+
} else {
1139+
variablesArray = append(variablesArray, variable)
1140+
}
1141+
}
1142+
1143+
page, ok := responseMap["page"].(float64)
1144+
if !ok {
1145+
page = 0
1146+
}
1147+
1148+
pagelen, ok := responseMap["pagelen"].(float64)
1149+
if !ok {
1150+
pagelen = 0
1151+
}
1152+
1153+
max_depth, ok := responseMap["max_depth"].(float64)
1154+
if !ok {
1155+
max_depth = 0
1156+
}
1157+
1158+
size, ok := responseMap["size"].(float64)
1159+
if !ok {
1160+
size = 0
1161+
}
1162+
1163+
next, ok := responseMap["next"].(string)
1164+
if !ok {
1165+
next = ""
1166+
}
1167+
1168+
deploymentVariables := DeploymentVariables{
1169+
Page: int(page),
1170+
Pagelen: int(pagelen),
1171+
MaxDepth: int(max_depth),
1172+
Size: int(size),
1173+
Next: next,
1174+
Variables: variablesArray,
1175+
}
1176+
1177+
return &deploymentVariables, nil
1178+
}
1179+
1180+
func decodeDeploymentVariable(response interface{}) (*DeploymentVariable, error) {
1181+
responseMap := response.(map[string]interface{})
1182+
1183+
if responseMap["type"] == "error" {
1184+
return nil, DecodeError(responseMap)
1185+
}
1186+
1187+
var variable = new(DeploymentVariable)
1188+
err := mapstructure.Decode(responseMap, &variable)
1189+
if err != nil {
1190+
return nil, err
1191+
}
1192+
1193+
return variable, nil
1194+
}
1195+
10251196
func (rf RepositoryFile) String() string {
10261197
return rf.Path
10271198
}

tests/variable_test.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package tests
2+
3+
import (
4+
"os"
5+
"fmt"
6+
"testing"
7+
"time"
8+
9+
_ "github.com/k0kubun/pp"
10+
"github.com/ktrysmt/go-bitbucket"
11+
)
12+
13+
func TestEndToEndDeploymentVariables(t *testing.T) {
14+
15+
user := os.Getenv("BITBUCKET_TEST_USERNAME")
16+
pass := os.Getenv("BITBUCKET_TEST_PASSWORD")
17+
owner := os.Getenv("BITBUCKET_TEST_OWNER")
18+
repo := os.Getenv("BITBUCKET_TEST_REPOSLUG")
19+
20+
if user == "" {
21+
t.Error("BITBUCKET_TEST_USERNAME is empty.")
22+
}
23+
if pass == "" {
24+
t.Error("BITBUCKET_TEST_PASSWORD is empty.")
25+
}
26+
if owner == "" {
27+
t.Error("BITBUCKET_TEST_OWNER is empty.")
28+
}
29+
if repo == "" {
30+
t.Error("BITBUCKET_TEST_REPOSLUG is empty.")
31+
}
32+
33+
c := bitbucket.NewBasicAuth(user, pass)
34+
35+
environmentOpt := &bitbucket.RepositoryEnvironmentsOptions{
36+
Owner: owner,
37+
RepoSlug: repo,
38+
}
39+
40+
environments, err := c.Repositories.Repository.ListEnvironments(environmentOpt)
41+
if err != nil {
42+
t.Error(err)
43+
}
44+
45+
if environments == nil {
46+
t.Error("list didn't return any environments")
47+
}
48+
49+
environment, err := findEnvironmentByName("Test", environments)
50+
if err != nil {
51+
t.Error(err)
52+
}
53+
54+
opt := &bitbucket.RepositoryDeploymentVariableOptions{
55+
Owner: owner,
56+
RepoSlug: repo,
57+
Environment: environment,
58+
Key: "foo",
59+
Value: "value",
60+
}
61+
62+
variable, err := c.Repositories.Repository.AddDeploymentVariable(opt)
63+
if err != nil {
64+
t.Error(err)
65+
}
66+
67+
opt.Key = "bar"
68+
opt.Value = "other value"
69+
opt.Uuid = variable.Uuid
70+
71+
updatedVariable, err := c.Repositories.Repository.UpdateDeploymentVariable(opt)
72+
if err != nil {
73+
t.Error(err)
74+
}
75+
76+
listOpt := &bitbucket.RepositoryDeploymentVariablesOptions{
77+
Owner: owner,
78+
RepoSlug: repo,
79+
Environment: environment,
80+
MaxDepth: 10,
81+
PageNum: 1,
82+
Pagelen: 10,
83+
}
84+
85+
err = waitForVariables(updatedVariable.Uuid, "bar", "other value", c, listOpt)
86+
if err != nil {
87+
t.Error(err)
88+
}
89+
90+
deleteOpt := &bitbucket.RepositoryDeploymentVariableDeleteOptions{
91+
Owner: owner,
92+
RepoSlug: repo,
93+
Environment: environment,
94+
Uuid: variable.Uuid,
95+
}
96+
97+
_, err = c.Repositories.Repository.DeleteDeploymentVariable(deleteOpt)
98+
if err != nil {
99+
t.Error(err)
100+
}
101+
102+
err = waitForDeletion(updatedVariable.Uuid, c, listOpt)
103+
if err == nil {
104+
t.Error("updated variable was not deleted")
105+
}
106+
}
107+
108+
func findEnvironmentByName(name string, e *bitbucket.Environments) (*bitbucket.Environment, error) {
109+
for _, environment := range e.Environments {
110+
if environment.Name == name {
111+
return &environment, nil
112+
}
113+
}
114+
115+
return nil, fmt.Errorf("no environment named %s", name)
116+
}
117+
118+
func waitForDeletion(uuid string, c *bitbucket.Client, opt *bitbucket.RepositoryDeploymentVariablesOptions) error {
119+
for i := 0; i < 3; i++ {
120+
deploymentVariables, err := c.Repositories.Repository.ListDeploymentVariables(opt)
121+
if err != nil {
122+
return err
123+
}
124+
125+
for _, deploymentVariable := range deploymentVariables.Variables {
126+
if deploymentVariable.Uuid == uuid {
127+
time.Sleep(3 * time.Second)
128+
129+
break
130+
}
131+
}
132+
}
133+
134+
return fmt.Errorf("update variable not found in list")
135+
}
136+
137+
func waitForVariables(uuid string, key string, value string, c *bitbucket.Client, opt *bitbucket.RepositoryDeploymentVariablesOptions) error {
138+
for i := 0; i < 3; i++ {
139+
deploymentVariables, err := c.Repositories.Repository.ListDeploymentVariables(opt)
140+
if err != nil {
141+
return err
142+
}
143+
144+
for _, deploymentVariable := range deploymentVariables.Variables {
145+
if deploymentVariable.Uuid == uuid && deploymentVariable.Key == key && deploymentVariable.Value == value {
146+
return nil
147+
}
148+
}
149+
150+
time.Sleep(3 * time.Second)
151+
}
152+
153+
return fmt.Errorf("update variable not found in list")
154+
}

0 commit comments

Comments
 (0)