Skip to content

Commit 6115924

Browse files
committed
support listing regions
1 parent 7855fb1 commit 6115924

8 files changed

Lines changed: 324 additions & 1 deletion

File tree

server/app/app.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ func (a *App) registerHandlers() {
119119
notificationRouter := authRouter.PathPrefix("/notification").Subrouter()
120120
vmRouter := authRouter.PathPrefix("/vm").Subrouter()
121121
k8sRouter := authRouter.PathPrefix("/k8s").Subrouter()
122+
regionRouter := authRouter.PathPrefix("/region").Subrouter()
122123

123124
// sub routes with no authorization
124125
unAuthUserRouter := versionRouter.PathPrefix("/user").Subrouter()
@@ -158,6 +159,8 @@ func (a *App) registerHandlers() {
158159
notificationRouter.HandleFunc("", WrapFunc(a.ListNotificationsHandler)).Methods("GET", "OPTIONS")
159160
notificationRouter.HandleFunc("/{id}", WrapFunc(a.UpdateNotificationsHandler)).Methods("PUT", "OPTIONS")
160161

162+
regionRouter.HandleFunc("", WrapFunc(a.ListRegionsHandler)).Methods("GET", "OPTIONS")
163+
161164
vmRouter.HandleFunc("", WrapFunc(a.DeployVMHandler)).Methods("POST", "OPTIONS")
162165
vmRouter.HandleFunc("/validate/{name}", WrapFunc(a.ValidateVMNameHandler)).Methods("Get", "OPTIONS")
163166
vmRouter.HandleFunc("/{id}", WrapFunc(a.GetVMHandler)).Methods("GET", "OPTIONS")

server/app/vm_handler.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010

1111
"github.com/codescalers/cloud4students/deployer"
12+
"github.com/codescalers/cloud4students/internal"
1213
"github.com/codescalers/cloud4students/middlewares"
1314
"github.com/codescalers/cloud4students/models"
1415
"github.com/codescalers/cloud4students/streams"
@@ -352,3 +353,34 @@ func (a *App) DeleteAllVMsHandler(req *http.Request) (interface{}, Response) {
352353
Data: nil,
353354
}, Ok()
354355
}
356+
357+
// ListRegionsHandler returns all supported regions
358+
// Example endpoint: List all supported regions
359+
// @Summary List all supported regions
360+
// @Description List all supported regions
361+
// @Tags Region
362+
// @Accept json
363+
// @Produce json
364+
// @Security BearerAuth
365+
// @Success 200 {object} []string
366+
// @Failure 401 {object} Response
367+
// @Failure 500 {object} Response
368+
// @Router /region [get]
369+
func (a *App) ListRegionsHandler(req *http.Request) (interface{}, Response) {
370+
graphql, err := internal.NewGraphQl(a.config.Account.Network)
371+
if err != nil {
372+
log.Error().Err(err).Send()
373+
return nil, InternalServerError(errors.New(internalServerErrorMsg))
374+
}
375+
376+
regions, err := graphql.ListRegions()
377+
if err != nil {
378+
log.Error().Err(err).Send()
379+
return nil, InternalServerError(errors.New(internalServerErrorMsg))
380+
}
381+
382+
return ResponseMsg{
383+
Message: "Regions are found",
384+
Data: regions,
385+
}, Ok()
386+
}

server/app/wrapper.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ func BadRequest(err error) Response {
137137

138138
// InternalServerError result
139139
func InternalServerError(err error) Response {
140-
return Error(err, 0)
140+
return Error(err)
141141
}
142142

143143
// NotFound response

server/deployer/k8s_deployer.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@ func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDe
226226

227227
// deploy network and cluster
228228
node, networkContractID, k8sContractID, err := d.deployK8sClusterWithNetwork(ctx, k8sDeployInput, user.SSHKey, adminSSHKey)
229+
if errors.Is(err, deployer.ErrNoNodesMatchesResources) {
230+
return http.StatusBadRequest, err, errors.New("no nodes are found, please try to change region")
231+
}
229232
if err != nil {
230233
return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg)
231234
}

server/deployer/vms_deployer.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, vm mod
120120
}
121121

122122
deployedVM, contractID, networkContractID, err := d.deployVM(ctx, vm, user.SSHKey, adminSSHKey)
123+
if errors.Is(err, deployer.ErrNoNodesMatchesResources) {
124+
return http.StatusBadRequest, err, errors.New("no nodes are found, please try to change region")
125+
}
123126
if err != nil {
124127
return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg)
125128
}

server/docs/docs.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,6 +1004,45 @@ const docTemplate = `{
10041004
}
10051005
}
10061006
},
1007+
"/region": {
1008+
"get": {
1009+
"security": [
1010+
{
1011+
"BearerAuth": []
1012+
}
1013+
],
1014+
"description": "List all supported regions",
1015+
"consumes": [
1016+
"application/json"
1017+
],
1018+
"produces": [
1019+
"application/json"
1020+
],
1021+
"tags": [
1022+
"Region"
1023+
],
1024+
"summary": "List all supported regions",
1025+
"responses": {
1026+
"200": {
1027+
"description": "OK",
1028+
"schema": {
1029+
"type": "array",
1030+
"items": {
1031+
"type": "string"
1032+
}
1033+
}
1034+
},
1035+
"401": {
1036+
"description": "Unauthorized",
1037+
"schema": {}
1038+
},
1039+
"500": {
1040+
"description": "Internal Server Error",
1041+
"schema": {}
1042+
}
1043+
}
1044+
}
1045+
},
10071046
"/set_admin": {
10081047
"put": {
10091048
"security": [

server/docs/swagger.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,6 +1282,31 @@ paths:
12821282
summary: Set user's notifications as seen
12831283
tags:
12841284
- Notification
1285+
/region:
1286+
get:
1287+
consumes:
1288+
- application/json
1289+
description: List all supported regions
1290+
produces:
1291+
- application/json
1292+
responses:
1293+
"200":
1294+
description: OK
1295+
schema:
1296+
items:
1297+
type: string
1298+
type: array
1299+
"401":
1300+
description: Unauthorized
1301+
schema: {}
1302+
"500":
1303+
description: Internal Server Error
1304+
schema: {}
1305+
security:
1306+
- BearerAuth: []
1307+
summary: List all supported regions
1308+
tags:
1309+
- Region
12851310
/set_admin:
12861311
put:
12871312
consumes:

server/internal/graphql.go

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package internal
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"slices"
10+
"time"
11+
12+
"github.com/cenkalti/backoff"
13+
"github.com/pkg/errors"
14+
"github.com/rs/zerolog/log"
15+
)
16+
17+
var (
18+
DevNetwork = "dev"
19+
QaNetwork = "qa"
20+
TestNetwork = "test"
21+
MainNetwork = "main"
22+
23+
// GraphQlURLs for graphql urls
24+
GraphQlURLs = map[string][]string{
25+
DevNetwork: {
26+
"https://graphql.dev.grid.tf/graphql",
27+
"https://graphql.02.dev.grid.tf/graphql",
28+
},
29+
TestNetwork: {
30+
"https://graphql.test.grid.tf/graphql",
31+
"https://graphql.02.test.grid.tf/graphql",
32+
},
33+
QaNetwork: {
34+
"https://graphql.qa.grid.tf/graphql",
35+
"https://graphql.02.qa.grid.tf/graphql",
36+
},
37+
MainNetwork: {
38+
"https://graphql.grid.tf/graphql",
39+
"https://graphql.02.grid.tf/graphql",
40+
},
41+
}
42+
)
43+
44+
// GraphQl for tf graphql
45+
type GraphQl struct {
46+
urls []string
47+
activeStackIdx int
48+
}
49+
50+
// NewGraphQl new tf graphql
51+
func NewGraphQl(network string) (GraphQl, error) {
52+
if len(network) == 0 {
53+
return GraphQl{}, errors.New("network is required")
54+
}
55+
56+
return GraphQl{urls: GraphQlURLs[network], activeStackIdx: 0}, nil
57+
}
58+
59+
// ListContractsByTwinID returns contracts for a twinID
60+
func (g *GraphQl) ListRegions() ([]string, error) {
61+
countriesCount, err := g.getItemTotalCount("countries", "(orderBy: region_ASC)")
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
countriesData, err := g.query(`query getRegions($countriesCount: Int!){
67+
countries(limit: $countriesCount) {
68+
region
69+
}
70+
}`,
71+
map[string]interface{}{
72+
"countriesCount": countriesCount,
73+
})
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
countriesJSONData, err := json.Marshal(countriesData)
79+
if err != nil {
80+
return nil, err
81+
}
82+
83+
var listCountries struct {
84+
Countries []struct {
85+
Region string
86+
}
87+
}
88+
err = json.Unmarshal(countriesJSONData, &listCountries)
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
var regions []string
94+
for _, c := range listCountries.Countries {
95+
if !slices.Contains(regions, c.Region) {
96+
regions = append(regions, c.Region)
97+
}
98+
}
99+
100+
return regions, nil
101+
}
102+
103+
// getItemTotalCount return count of items
104+
func (g *GraphQl) getItemTotalCount(itemName string, options string) (float64, error) {
105+
countBody := fmt.Sprintf(`query { items: %vConnection%v { count: totalCount } }`, itemName, options)
106+
requestBody := map[string]interface{}{"query": countBody}
107+
108+
jsonBody, err := json.Marshal(requestBody)
109+
if err != nil {
110+
return 0, err
111+
}
112+
113+
bodyReader := bytes.NewReader(jsonBody)
114+
115+
countResponse, err := g.httpPost(bodyReader)
116+
if err != nil {
117+
return 0, err
118+
}
119+
120+
queryData, err := parseHTTPResponse(countResponse)
121+
if err != nil {
122+
return 0, err
123+
}
124+
125+
countMap := queryData["data"].(map[string]interface{})
126+
countItems := countMap["items"].(map[string]interface{})
127+
count := countItems["count"].(float64)
128+
129+
return count, nil
130+
}
131+
132+
// query queries graphql
133+
func (g *GraphQl) query(body string, variables map[string]interface{}) (map[string]interface{}, error) {
134+
result := make(map[string]interface{})
135+
136+
requestBody := map[string]interface{}{"query": body, "variables": variables}
137+
jsonBody, err := json.Marshal(requestBody)
138+
if err != nil {
139+
return result, err
140+
}
141+
142+
bodyReader := bytes.NewReader(jsonBody)
143+
144+
resp, err := g.httpPost(bodyReader)
145+
if err != nil {
146+
return result, err
147+
}
148+
149+
queryData, err := parseHTTPResponse(resp)
150+
if err != nil {
151+
return result, err
152+
}
153+
154+
result = queryData["data"].(map[string]interface{})
155+
return result, nil
156+
}
157+
158+
func parseHTTPResponse(resp *http.Response) (map[string]interface{}, error) {
159+
resBody, err := io.ReadAll(resp.Body)
160+
if err != nil {
161+
return map[string]interface{}{}, err
162+
}
163+
164+
defer resp.Body.Close()
165+
166+
var data map[string]interface{}
167+
err = json.Unmarshal(resBody, &data)
168+
if err != nil {
169+
return map[string]interface{}{}, err
170+
}
171+
172+
if resp.StatusCode >= 400 {
173+
return map[string]interface{}{}, errors.Errorf("request failed with status code: %d with error %v", resp.StatusCode, data)
174+
}
175+
176+
return data, nil
177+
}
178+
179+
func (g *GraphQl) httpPost(body io.Reader) (*http.Response, error) {
180+
cl := &http.Client{
181+
Timeout: 10 * time.Second,
182+
}
183+
184+
var (
185+
endpoint string
186+
reqErr error
187+
resp *http.Response
188+
)
189+
190+
backoffCfg := backoff.WithMaxRetries(
191+
backoff.NewConstantBackOff(1*time.Millisecond),
192+
2,
193+
)
194+
195+
err := backoff.RetryNotify(func() error {
196+
endpoint = g.urls[g.activeStackIdx]
197+
log.Debug().Str("url", endpoint).Msg("checking")
198+
199+
resp, reqErr = cl.Post(endpoint, "application/json", body)
200+
if reqErr != nil &&
201+
(errors.Is(reqErr, http.ErrAbortHandler) ||
202+
errors.Is(reqErr, http.ErrHandlerTimeout) ||
203+
errors.Is(reqErr, http.ErrServerClosed)) {
204+
g.activeStackIdx = (g.activeStackIdx + 1) % len(g.urls)
205+
return reqErr
206+
}
207+
208+
return nil
209+
}, backoffCfg, func(err error, _ time.Duration) {
210+
log.Error().Err(err).Msg("failed to connect to endpoint, retrying")
211+
})
212+
213+
if err != nil {
214+
log.Error().Err(err).Msg("failed to connect to endpoint")
215+
}
216+
217+
return resp, reqErr
218+
}

0 commit comments

Comments
 (0)