Skip to content

suport setting node as approved #62

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions node-registrar/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# Node Registrar Service

[![Go Report Card](https://goreportcard.com/badge/github.com/threefoldtech/tfgrid-sdk-go/node-registrar)](https://goreportcard.com/report/github.com/threefoldtech/tfgrid-sdk-go/node-registrar)
Expand Down Expand Up @@ -40,6 +39,7 @@ It offers operations like registring, listing, and updating farms and nodes, as
| GET | `/farms/:farm_id` | Get a specific farm by ID |
| POST | `/farms/` | Create a new farm |
| PATCH | `/farms/` | Update an existing farm |
| POST | `/farms/:farm_id/approve` | Approve multiple nodes for a specific farm |

### Nodes Endpoints

Expand All @@ -51,6 +51,27 @@ It offers operations like registring, listing, and updating farms and nodes, as
| POST | `/nodes/:node_id/uptime` | Report uptime for a specific node |
| POST | `/nodes/:node_id/consumption` | Report consumption for a specific node |

### Node Approval

The `/farms/:farm_id/approve` allows farmers to approve multiple nodes at once. This endpoint:

- Requires farm owner authentication
- Accepts a list of node IDs in the request body
- Only approves nodes that:
- Belong to the specified farm
- Are currently not approved

Example request:

```json
POST /farms/123/approve
{
"node_ids": [1, 2, 3]
}
```

The operation is atomic - either all nodes are approved or none are. If any node cannot be approved (not found, already approved, or doesn't belong to the farm), the entire operation fails.

## Setup Instructions

1. **Start PostgreSQL:**
Expand Down Expand Up @@ -94,7 +115,7 @@ Replace `<domain>` and `<port>` with the appropriate values.
docker build -t registrar:latest -f node-registrar/Dockerfile .
```

2. run the image
1. run the image

```bash
docker run -d \
Expand Down
6 changes: 3 additions & 3 deletions node-registrar/client/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestCreateAccount(t *testing.T) {
}))
defer testServer.Close()

baseURL, err := url.JoinPath(testServer.URL, "v1")
baseURL, err := url.JoinPath(testServer.URL, "api", "v1")
require.NoError(err)

request = newClientWithNoAccount
Expand Down Expand Up @@ -62,7 +62,7 @@ func TestUpdateAccount(t *testing.T) {
}))
defer testServer.Close()

baseURL, err := url.JoinPath(testServer.URL, "v1")
baseURL, err := url.JoinPath(testServer.URL, "api", "v1")
require.NoError(err)

t.Run("test update account updated successfully", func(t *testing.T) {
Expand Down Expand Up @@ -112,7 +112,7 @@ func TestGetAccount(t *testing.T) {
}))
defer testServer.Close()

baseURL, err := url.JoinPath(testServer.URL, "v1")
baseURL, err := url.JoinPath(testServer.URL, "api", "v1")
require.NoError(err)

count = 0
Expand Down
2 changes: 1 addition & 1 deletion node-registrar/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestNewRegistrarClient(t *testing.T) {
}))
defer testServer.Close()

baseURL, err := url.JoinPath(testServer.URL, "v1")
baseURL, err := url.JoinPath(testServer.URL, "api", "v1")
require.NoError(err)

t.Run("test new registrar client with no account", func(t *testing.T) {
Expand Down
56 changes: 56 additions & 0 deletions node-registrar/client/farm.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,62 @@ func (c *RegistrarClient) ListFarms(filter FarmFilter) (farms []Farm, err error)
return c.listFarms(filter)
}

// ApproveNodes approves multiple nodes for a specific farm
func (c *RegistrarClient) ApproveNodes(farmID uint64, nodeIDs []uint64) error {
return c.approveNodes(farmID, nodeIDs)
}

func (c *RegistrarClient) approveNodes(farmID uint64, nodeIDs []uint64) error {
if err := c.ensureTwinID(); err != nil {
return errors.Wrap(err, "failed to ensure twin id")
}

url, err := url.JoinPath(c.baseURL, "farms", fmt.Sprint(farmID), "approve")
if err != nil {
return errors.Wrap(err, "failed to construct registrar url")
}

data := struct {
NodeIDs []uint64 `json:"node_ids"`
}{
NodeIDs: nodeIDs,
}

var body bytes.Buffer
if err = json.NewEncoder(&body).Encode(data); err != nil {
return errors.Wrap(err, "failed to encode request body")
}

req, err := http.NewRequest("POST", url, &body)
if err != nil {
return errors.Wrap(err, "failed to construct http request to the registrar")
}

authHeader, err := c.signRequest(time.Now().Unix())
if err != nil {
return errors.Wrap(err, "failed to sign request")
}
req.Header.Set("X-Auth", authHeader)
req.Header.Set("Content-Type", "application/json")

resp, err := c.httpClient.Do(req)
if err != nil {
return errors.Wrap(err, "failed to send request to approve nodes")
}

if resp == nil {
return errors.New("failed to approve nodes, no response received")
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
err = parseResponseError(resp.Body)
return errors.Wrapf(err, "failed to approve nodes with status code %s", resp.Status)
}

return nil
}

// FarmUpdate represents the data needed to update an existing farm
type FarmUpdate struct {
FarmName *string
Expand Down
50 changes: 47 additions & 3 deletions node-registrar/client/farm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestCreateFarm(t *testing.T) {
}))
defer testServer.Close()

baseURL, err := url.JoinPath(testServer.URL, "v1")
baseURL, err := url.JoinPath(testServer.URL, "api", "v1")
require.NoError(err)

request = newClientWithAccountNoNode
Expand Down Expand Up @@ -68,7 +68,7 @@ func TestUpdateFarm(t *testing.T) {
}))
defer testServer.Close()

baseURL, err := url.JoinPath(testServer.URL, "v1")
baseURL, err := url.JoinPath(testServer.URL, "api", "v1")
require.NoError(err)

t.Run("test update farm with status unauthorzed", func(t *testing.T) {
Expand Down Expand Up @@ -113,7 +113,7 @@ func TestGetFarm(t *testing.T) {
}))
defer testServer.Close()

baseURL, err := url.JoinPath(testServer.URL, "v1")
baseURL, err := url.JoinPath(testServer.URL, "api", "v1")
require.NoError(err)

count = 0
Expand All @@ -134,3 +134,47 @@ func TestGetFarm(t *testing.T) {
require.Equal(result, farm)
})
}

func TestApproveNodes(t *testing.T) {
var request int
var count int
require := require.New(t)

keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic)
require.NoError(err)
account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public())

testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
statusCode, body := serverHandler(r, request, count, require)
w.WriteHeader(statusCode)
_, err := w.Write(body)
require.NoError(err)
count++
}))
defer testServer.Close()

baseURL, err := url.JoinPath(testServer.URL, "api", "v1")
require.NoError(err)

t.Run("test approve nodes with status unauthorized", func(t *testing.T) {
count = 0
request = newClientWithAccountNoNode
c, err := NewRegistrarClient(baseURL, testMnemonic)
require.NoError(err)

request = approveNodesWithStatusUnauthorized
err = c.ApproveNodes(farmID, []uint64{1, 2, 3})
require.Error(err)
})

t.Run("test approve nodes with status ok", func(t *testing.T) {
count = 0
request = newClientWithAccountNoNode
c, err := NewRegistrarClient(baseURL, testMnemonic)
require.NoError(err)

request = approveNodesWithStatusOK
err = c.ApproveNodes(farmID, []uint64{1, 2, 3})
require.NoError(err)
})
}
19 changes: 17 additions & 2 deletions node-registrar/client/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ func (c *RegistrarClient) ListNodes(opts NodeFilter) (nodes []Node, err error) {
return c.listNodesWithFilter(opts)
}

// ListUnapprovedNodes gets a list of unapproved nodes for a specific farm
func (c *RegistrarClient) ListUnapprovedNodes(farmID uint64) ([]Node, error) {
falseVal := false
filter := NodeFilter{
FarmID: &farmID,
Approved: &falseVal,
}

return c.ListNodes(filter)
}

func (c *RegistrarClient) registerNode(node Node) (nodeID uint64, err error) {
err = c.ensureTwinID()
if err != nil {
Expand Down Expand Up @@ -149,7 +160,7 @@ func (c *RegistrarClient) updateNode(opts NodeUpdate) (err error) {
return errors.Wrap(err, "failed to construct registrar url")
}

node = c.parseUpdateNodeOpts(node, opts)
node = parseUpdateNodeOpts(node, opts)

handler := func(body bytes.Buffer) error {
req, err := http.NewRequest("PATCH", url, &body)
Expand Down Expand Up @@ -331,6 +342,7 @@ type NodeFilter struct {
LastSeen *int64
Page *uint32
Size *uint32
Approved *bool
}

func (c *RegistrarClient) listNodesWithFilter(filter NodeFilter) (nodes []Node, err error) {
Expand Down Expand Up @@ -425,7 +437,7 @@ func (c *RegistrarClient) ensureNodeID() error {
return nil
}

func (c *RegistrarClient) parseUpdateNodeOpts(node Node, update NodeUpdate) Node {
func parseUpdateNodeOpts(node Node, update NodeUpdate) Node {
if update.FarmID != nil {
node.FarmID = *update.FarmID
}
Expand Down Expand Up @@ -484,6 +496,9 @@ func parseListNodeOpts(filter NodeFilter) map[string]any {
if filter.LastSeen != nil {
data["last_seen"] = *filter.LastSeen
}
if filter.Approved != nil {
data["approved"] = *filter.Approved
}

page := uint32(1)
if filter.Page != nil {
Expand Down
58 changes: 48 additions & 10 deletions node-registrar/client/node_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package client

import (
"encoding/base64"
"net/http"
"net/http/httptest"
"net/url"
Expand All @@ -20,6 +21,10 @@ func TestRegistarNode(t *testing.T) {
FarmID: farmID,
}

keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic)
require.NoError(err)
account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public())

testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
statusCode, body := serverHandler(r, request, count, require)
w.WriteHeader(statusCode)
Expand All @@ -29,7 +34,7 @@ func TestRegistarNode(t *testing.T) {
}))
defer testServer.Close()

baseURL, err := url.JoinPath(testServer.URL, "v1")
baseURL, err := url.JoinPath(testServer.URL, "api", "v1")
require.NoError(err)

t.Run("test registar node no account", func(t *testing.T) {
Expand Down Expand Up @@ -73,10 +78,10 @@ func TestUpdateNode(t *testing.T) {
var count int
require := require.New(t)

// publicKey, privateKey, err := aliceKeys()
// require.NoError(err)
// account.PublicKey = base64.StdEncoding.EncodeToString(publicKey)
//
keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic)
require.NoError(err)
account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public())

testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
statusCode, body := serverHandler(r, request, count, require)
w.WriteHeader(statusCode)
Expand All @@ -86,7 +91,7 @@ func TestUpdateNode(t *testing.T) {
}))
defer testServer.Close()

baseURL, err := url.JoinPath(testServer.URL, "v1")
baseURL, err := url.JoinPath(testServer.URL, "api", "v1")
require.NoError(err)

request = newClientWithAccountAndNode
Expand Down Expand Up @@ -118,9 +123,9 @@ func TestGetNode(t *testing.T) {
var count int
require := require.New(t)

// publicKey, privateKey, err := aliceKeys()
// require.NoError(err)
// account.PublicKey = base64.StdEncoding.EncodeToString(publicKey)
keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic)
require.NoError(err)
account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public())

testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
statusCode, body := serverHandler(r, request, count, require)
Expand All @@ -131,7 +136,7 @@ func TestGetNode(t *testing.T) {
}))
defer testServer.Close()

baseURL, err := url.JoinPath(testServer.URL, "v1")
baseURL, err := url.JoinPath(testServer.URL, "api", "v1")
require.NoError(err)

request = newClientWithAccountAndNode
Expand Down Expand Up @@ -166,3 +171,36 @@ func TestGetNode(t *testing.T) {
require.Equal([]Node{node}, result)
})
}

func TestListUnapprovedNodes(t *testing.T) {
var request int
var count int
require := require.New(t)

keyPair, err := parseKeysFromMnemonicOrSeed(testMnemonic)
require.NoError(err)
account.PublicKey = base64.StdEncoding.EncodeToString(keyPair.Public())

testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
statusCode, body := serverHandler(r, request, count, require)
w.WriteHeader(statusCode)
_, err := w.Write(body)
require.NoError(err)
count++
}))
defer testServer.Close()

baseURL, err := url.JoinPath(testServer.URL, "api", "v1")
require.NoError(err)

t.Run("test list unapproved nodes in farm", func(t *testing.T) {
request = newClientWithAccountNoNode
c, err := NewRegistrarClient(baseURL, testMnemonic)
require.NoError(err)

request = listUnapprovedNodesInFarm
nodes, err := c.ListUnapprovedNodes(farmID)
require.NoError(err)
require.Equal([]Node{node}, nodes)
})
}
Loading
Loading