From 813efbc72ff49472aa9a841b8ab5cb3f6cfff655 Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Mon, 13 Apr 2026 16:22:54 +0200 Subject: [PATCH 1/3] add chip router client --- .../chiprouter/.changeset/v1.0.2.md | 1 + .../components/chiprouter/client/client.go | 147 ++++++++++++++++++ framework/components/chiprouter/go.mod | 2 +- 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 framework/components/chiprouter/.changeset/v1.0.2.md create mode 100644 framework/components/chiprouter/client/client.go diff --git a/framework/components/chiprouter/.changeset/v1.0.2.md b/framework/components/chiprouter/.changeset/v1.0.2.md new file mode 100644 index 000000000..09ac07126 --- /dev/null +++ b/framework/components/chiprouter/.changeset/v1.0.2.md @@ -0,0 +1 @@ +- Add ChIP Router client \ No newline at end of file diff --git a/framework/components/chiprouter/client/client.go b/framework/components/chiprouter/client/client.go new file mode 100644 index 000000000..924282a09 --- /dev/null +++ b/framework/components/chiprouter/client/client.go @@ -0,0 +1,147 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "strings" + "time" + + pkgerrors "github.com/pkg/errors" +) + +const ( + adminRequestTimeout = 5 * time.Second +) + +type RegisterSubscriberRequest struct { + Name string `json:"name"` + Endpoint string `json:"endpoint"` +} + +type RegisterSubscriberResponse struct { + ID string `json:"id"` +} + +type RealthResponse struct { + AdminURL string `json:"admin_url"` + GRPCURL string `json:"grpc_url"` +} + +type Client struct { + httpClient *http.Client + adminURL string + grpcURL string +} + +func New(ctx context.Context, adminURL, grpcURL string) (*Client, error) { + c := &Client{ + httpClient: &http.Client{Timeout: adminRequestTimeout}, + adminURL: adminURL, + grpcURL: grpcURL, + } + + if !isHTTPReady(ctx, c.adminURL) { + return nil, fmt.Errorf("chip ingress router admin endpoint is not reachable: %s", c.adminURL) + } + if !isTCPReady(c.grpcURL) { + return nil, fmt.Errorf("chip ingress router grpc endpoint is not reachable: %s", c.grpcURL) + } + return c, nil +} + +func (c *Client) RegisterSubscriber(ctx context.Context, name, endpoint string) (string, error) { + body, err := json.Marshal(RegisterSubscriberRequest{Name: name, Endpoint: endpoint}) + if err != nil { + return "", pkgerrors.Wrap(err, "marshal chip router register request") + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.adminURL, "/")+"/subscribers", bytes.NewReader(body)) + if err != nil { + return "", pkgerrors.Wrap(err, "create chip router register request") + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return "", pkgerrors.Wrap(err, "perform chip router register request") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("chip router register request failed with status %s", resp.Status) + } + + var out RegisterSubscriberResponse + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + return "", pkgerrors.Wrap(err, "decode chip router register response") + } + if out.ID == "" { + return "", pkgerrors.New("chip router register response missing subscriber id") + } + + return out.ID, nil +} + +func (c *Client) UnregisterSubscriber(ctx context.Context, id string) error { + if strings.TrimSpace(id) == "" { + return nil + } + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, strings.TrimRight(c.adminURL, "/")+"/subscribers/"+id, nil) + if err != nil { + return pkgerrors.Wrap(err, "create chip router unregister request") + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return pkgerrors.Wrap(err, "perform chip router unregister request") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("chip router unregister request failed with status %s", resp.Status) + } + return nil +} + +func isHTTPReady(ctx context.Context, adminURL string) bool { + _, err := fetchHealth(ctx, adminURL) + return err == nil +} + +func fetchHealth(ctx context.Context, adminURL string) (*RealthResponse, error) { + if strings.TrimSpace(adminURL) == "" { + return nil, pkgerrors.New("admin url is empty") + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(adminURL, "/")+"/health", nil) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected health status %s", resp.Status) + } + var health RealthResponse + if err := json.NewDecoder(resp.Body).Decode(&health); err != nil { + return nil, err + } + return &health, nil +} + +func isTCPReady(addr string) bool { + dialer := &net.Dialer{Timeout: time.Second} + conn, err := dialer.Dial("tcp", addr) + if err != nil { + return false + } + _ = conn.Close() + return true +} diff --git a/framework/components/chiprouter/go.mod b/framework/components/chiprouter/go.mod index dca3eca19..d53d280e0 100644 --- a/framework/components/chiprouter/go.mod +++ b/framework/components/chiprouter/go.mod @@ -7,6 +7,7 @@ require ( github.com/docker/docker v28.5.2+incompatible github.com/docker/go-connections v0.6.0 github.com/google/uuid v1.6.0 + github.com/pkg/errors v0.9.1 github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20251211140724-319861e514c4 github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8 github.com/testcontainers/testcontainers-go v0.41.0 @@ -58,7 +59,6 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/rs/zerolog v1.33.0 // indirect From eb5f31ce9c1ace605a3b398a395eb60fd01c71cb Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Tue, 14 Apr 2026 09:32:54 +0200 Subject: [PATCH 2/3] simplify --- .../components/chiprouter/client/client.go | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/framework/components/chiprouter/client/client.go b/framework/components/chiprouter/client/client.go index 924282a09..5c06c13bc 100644 --- a/framework/components/chiprouter/client/client.go +++ b/framework/components/chiprouter/client/client.go @@ -26,11 +26,6 @@ type RegisterSubscriberResponse struct { ID string `json:"id"` } -type RealthResponse struct { - AdminURL string `json:"admin_url"` - GRPCURL string `json:"grpc_url"` -} - type Client struct { httpClient *http.Client adminURL string @@ -44,10 +39,10 @@ func New(ctx context.Context, adminURL, grpcURL string) (*Client, error) { grpcURL: grpcURL, } - if !isHTTPReady(ctx, c.adminURL) { + if !c.isHTTPReady(ctx) { return nil, fmt.Errorf("chip ingress router admin endpoint is not reachable: %s", c.adminURL) } - if !isTCPReady(c.grpcURL) { + if !c.isTCPReady() { return nil, fmt.Errorf("chip ingress router grpc endpoint is not reachable: %s", c.grpcURL) } return c, nil @@ -108,37 +103,29 @@ func (c *Client) UnregisterSubscriber(ctx context.Context, id string) error { return nil } -func isHTTPReady(ctx context.Context, adminURL string) bool { - _, err := fetchHealth(ctx, adminURL) - return err == nil -} - -func fetchHealth(ctx context.Context, adminURL string) (*RealthResponse, error) { - if strings.TrimSpace(adminURL) == "" { - return nil, pkgerrors.New("admin url is empty") +func (c *Client) isHTTPReady(ctx context.Context) bool { + if strings.TrimSpace(c.adminURL) == "" { + return false } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(adminURL, "/")+"/health", nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(c.adminURL, "/")+"/health", nil) if err != nil { - return nil, err + return false } resp, err := http.DefaultClient.Do(req) if err != nil { - return nil, err + return false } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected health status %s", resp.Status) - } - var health RealthResponse - if err := json.NewDecoder(resp.Body).Decode(&health); err != nil { - return nil, err + return false } - return &health, nil + + return true } -func isTCPReady(addr string) bool { +func (c *Client) isTCPReady() bool { dialer := &net.Dialer{Timeout: time.Second} - conn, err := dialer.Dial("tcp", addr) + conn, err := dialer.Dial("tcp", c.grpcURL) if err != nil { return false } From e51b6d4cff561b0d6c9043c91768121f0c8e6dfc Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Tue, 14 Apr 2026 10:25:21 +0200 Subject: [PATCH 3/3] CR changes --- framework/components/chiprouter/client/client.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/framework/components/chiprouter/client/client.go b/framework/components/chiprouter/client/client.go index 5c06c13bc..25fa90031 100644 --- a/framework/components/chiprouter/client/client.go +++ b/framework/components/chiprouter/client/client.go @@ -40,10 +40,10 @@ func New(ctx context.Context, adminURL, grpcURL string) (*Client, error) { } if !c.isHTTPReady(ctx) { - return nil, fmt.Errorf("chip ingress router admin endpoint is not reachable: %s", c.adminURL) + return nil, fmt.Errorf("chip router admin endpoint is not reachable: %s", c.adminURL) } if !c.isTCPReady() { - return nil, fmt.Errorf("chip ingress router grpc endpoint is not reachable: %s", c.grpcURL) + return nil, fmt.Errorf("chip router grpc endpoint is not reachable: %s", c.grpcURL) } return c, nil } @@ -111,7 +111,7 @@ func (c *Client) isHTTPReady(ctx context.Context) bool { if err != nil { return false } - resp, err := http.DefaultClient.Do(req) + resp, err := c.httpClient.Do(req) if err != nil { return false } @@ -124,7 +124,7 @@ func (c *Client) isHTTPReady(ctx context.Context) bool { } func (c *Client) isTCPReady() bool { - dialer := &net.Dialer{Timeout: time.Second} + dialer := &net.Dialer{Timeout: adminRequestTimeout} conn, err := dialer.Dial("tcp", c.grpcURL) if err != nil { return false