Skip to content

Commit

Permalink
feat: Add the http clients/dtos of KVS and registry for Keeper
Browse files Browse the repository at this point in the history
Signed-off-by: Jack Chen <jack@iotechsys.com>
  • Loading branch information
jackchenjc committed Mar 14, 2024
1 parent a40ef71 commit e3dd702
Show file tree
Hide file tree
Showing 22 changed files with 1,113 additions and 3 deletions.
91 changes: 91 additions & 0 deletions clients/http/kvs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// Copyright (C) 2024 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

package http

import (
"context"
"net/url"

"github.com/edgexfoundry/go-mod-core-contracts/v3/clients/http/utils"
"github.com/edgexfoundry/go-mod-core-contracts/v3/clients/interfaces"
"github.com/edgexfoundry/go-mod-core-contracts/v3/common"
"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests"
"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses"
"github.com/edgexfoundry/go-mod-core-contracts/v3/errors"
)

// KVSClient is the REST client for invoking the key-value APIs(/kvs/*) from Core Keeper
type KVSClient struct {
baseUrl string
authInjector interfaces.AuthenticationInjector
}

// NewKVSClient creates an instance of KVSClient
func NewKVSClient(baseUrl string, authInjector interfaces.AuthenticationInjector) interfaces.KVSClient {
return &KVSClient{
baseUrl: baseUrl,
authInjector: authInjector,
}
}

// UpdateValuesByKey updates values of the specified key and the child keys defined in the request payload.
// If no key exists at the given path, the key(s) will be created.
func (kc KVSClient) UpdateValuesByKey(ctx context.Context, key string, req requests.UpdateKeysRequest) (res responses.KeysResponse, err errors.EdgeX) {
path := utils.EscapeAndJoinPath(common.ApiKVSRoute, common.Key, key)
queryParams := url.Values{}
queryParams.Set(common.Flatten, common.ValueTrue)
err = utils.PutRequest(ctx, &res, kc.baseUrl, path, queryParams, req, kc.authInjector)
if err != nil {
return res, errors.NewCommonEdgeXWrapper(err)
}
return res, nil
}

// ValuesByKey returns the values of the specified key prefix.
func (kc KVSClient) ValuesByKey(ctx context.Context, key string) (res responses.MultiKeyValueResponse, err errors.EdgeX) {
path := utils.EscapeAndJoinPath(common.ApiKVSRoute, common.Key, key)
queryParams := url.Values{}
queryParams.Set(common.Plaintext, common.ValueTrue)
err = utils.GetRequest(ctx, &res, kc.baseUrl, path, queryParams, kc.authInjector)
if err != nil {
return res, errors.NewCommonEdgeXWrapper(err)
}
return res, nil
}

// ListKeys returns the list of the keys with the specified key prefix.
func (kc KVSClient) ListKeys(ctx context.Context, key string) (res responses.KeysResponse, err errors.EdgeX) {
path := utils.EscapeAndJoinPath(common.ApiKVSRoute, common.Key, key)
queryParams := url.Values{}
queryParams.Set(common.KeyOnly, common.ValueTrue)
err = utils.GetRequest(ctx, &res, kc.baseUrl, path, queryParams, kc.authInjector)
if err != nil {
return res, errors.NewCommonEdgeXWrapper(err)
}
return res, nil
}

// DeleteKey deletes the specified key.
func (kc KVSClient) DeleteKey(ctx context.Context, key string) (res responses.KeysResponse, err errors.EdgeX) {
path := utils.EscapeAndJoinPath(common.ApiKVSRoute, common.Key, key)
err = utils.DeleteRequest(ctx, &res, kc.baseUrl, path, kc.authInjector)
if err != nil {
return res, errors.NewCommonEdgeXWrapper(err)
}
return res, nil
}

// DeleteKeysByPrefix deletes all keys with the specified prefix.
func (kc KVSClient) DeleteKeysByPrefix(ctx context.Context, key string) (res responses.KeysResponse, err errors.EdgeX) {
path := utils.EscapeAndJoinPath(common.ApiKVSRoute, common.Key, key)
queryParams := url.Values{}
queryParams.Set("prefixMatch", common.ValueTrue)
err = utils.DeleteRequestWithParams(ctx, &res, kc.baseUrl, path, queryParams, kc.authInjector)
if err != nil {
return res, errors.NewCommonEdgeXWrapper(err)
}
return res, nil
}
75 changes: 75 additions & 0 deletions clients/http/kvs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// Copyright (C) 2024 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

package http

import (
"context"
"net/http"
"testing"

"github.com/edgexfoundry/go-mod-core-contracts/v3/common"
"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests"
"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses"

"github.com/stretchr/testify/require"
)

const TestKey = "TestWritable"

func TestUpdateValuesByKey(t *testing.T) {
ts := newTestServer(http.MethodPut, common.ApiKVSRoute+"/"+common.Key+"/"+TestKey, responses.KeysResponse{})
defer ts.Close()

client := NewKVSClient(ts.URL, NewNullAuthenticationInjector())
res, err := client.UpdateValuesByKey(context.Background(), TestKey, requests.UpdateKeysRequest{})

require.NoError(t, err)
require.IsType(t, responses.KeysResponse{}, res)
}

func TestValuesByKey(t *testing.T) {
ts := newTestServer(http.MethodGet, common.ApiKVSRoute+"/"+common.Key+"/"+TestKey, responses.MultiKeyValueResponse{})
defer ts.Close()

client := NewKVSClient(ts.URL, NewNullAuthenticationInjector())
res, err := client.ValuesByKey(context.Background(), TestKey)

require.NoError(t, err)
require.IsType(t, responses.MultiKeyValueResponse{}, res)
}

func TestListKeys(t *testing.T) {
ts := newTestServer(http.MethodGet, common.ApiKVSRoute+"/"+common.Key+"/"+TestKey, responses.KeysResponse{})
defer ts.Close()

client := NewKVSClient(ts.URL, NewNullAuthenticationInjector())
res, err := client.ListKeys(context.Background(), TestKey)

require.NoError(t, err)
require.IsType(t, responses.KeysResponse{}, res)
}

func TestDeleteKeys(t *testing.T) {
ts := newTestServer(http.MethodDelete, common.ApiKVSRoute+"/"+common.Key+"/"+TestKey, responses.KeysResponse{})
defer ts.Close()

client := NewKVSClient(ts.URL, NewNullAuthenticationInjector())
res, err := client.DeleteKey(context.Background(), TestKey)

require.NoError(t, err)
require.IsType(t, responses.KeysResponse{}, res)
}

func TestDeleteKeysByPrefix(t *testing.T) {
ts := newTestServer(http.MethodDelete, common.ApiKVSRoute+"/"+common.Key+"/"+TestKey, responses.KeysResponse{})
defer ts.Close()

client := NewKVSClient(ts.URL, NewNullAuthenticationInjector())
res, err := client.DeleteKeysByPrefix(context.Background(), TestKey)

require.NoError(t, err)
require.IsType(t, responses.KeysResponse{}, res)
}
91 changes: 91 additions & 0 deletions clients/http/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// Copyright (C) 2024 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

package http

import (
"context"
"net/url"
"strconv"

"github.com/edgexfoundry/go-mod-core-contracts/v3/clients/http/utils"
"github.com/edgexfoundry/go-mod-core-contracts/v3/clients/interfaces"
"github.com/edgexfoundry/go-mod-core-contracts/v3/common"
"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests"
"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses"
"github.com/edgexfoundry/go-mod-core-contracts/v3/errors"
)

var emptyResponse any

// RegistryClient is the REST client for invoking the registry APIs(/registry/*) from Core Keeper
type registryClient struct {
baseUrl string
authInjector interfaces.AuthenticationInjector
enableNameFieldEscape bool
}

// NewRegistryClient creates an instance of RegistryClient
func NewRegistryClient(baseUrl string, authInjector interfaces.AuthenticationInjector, enableNameFieldEscape bool) interfaces.RegistryClient {
return &registryClient{
baseUrl: baseUrl,
authInjector: authInjector,
enableNameFieldEscape: enableNameFieldEscape,
}
}

// Register registers a service instance
func (rc *registryClient) Register(ctx context.Context, req requests.AddRegistrationRequest) errors.EdgeX {
err := utils.PostRequestWithRawData(ctx, &emptyResponse, rc.baseUrl, common.ApiRegisterRoute, nil, req, rc.authInjector)
if err != nil {
return errors.NewCommonEdgeXWrapper(err)
}
return nil
}

// UpdateRegister updates the registration data of the service
func (rc *registryClient) UpdateRegister(ctx context.Context, req requests.AddRegistrationRequest) errors.EdgeX {
err := utils.PutRequest(ctx, &emptyResponse, rc.baseUrl, common.ApiRegisterRoute, nil, req, rc.authInjector)
if err != nil {
return errors.NewCommonEdgeXWrapper(err)
}
return nil
}

// RegistrationByServiceId returns the registration data by service id
func (rc *registryClient) RegistrationByServiceId(ctx context.Context, serviceId string) (responses.RegistrationResponse, errors.EdgeX) {
requestPath := common.NewPathBuilder().EnableNameFieldEscape(rc.enableNameFieldEscape).
SetPath(common.ApiRegisterRoute).SetPath(common.ServiceId).SetNameFieldPath(serviceId).BuildPath()
res := responses.RegistrationResponse{}
err := utils.GetRequest(ctx, &res, rc.baseUrl, requestPath, nil, rc.authInjector)
if err != nil {
return res, errors.NewCommonEdgeXWrapper(err)
}
return res, nil
}

// AllRegistry returns the registration data of all registered service
func (rc *registryClient) AllRegistry(ctx context.Context, deregistered bool) (responses.MultiRegistrationsResponse, errors.EdgeX) {
requestParams := url.Values{}
requestParams.Set(common.Deregistered, strconv.FormatBool(deregistered))

res := responses.MultiRegistrationsResponse{}
err := utils.GetRequest(ctx, &res, rc.baseUrl, common.ApiAllRegistrationsRoute, requestParams, rc.authInjector)
if err != nil {
return res, errors.NewCommonEdgeXWrapper(err)
}
return res, nil
}

// Deregister deregisters a service by service id
func (rc *registryClient) Deregister(ctx context.Context, serviceId string) errors.EdgeX {
requestPath := common.NewPathBuilder().EnableNameFieldEscape(rc.enableNameFieldEscape).
SetPath(common.ApiRegisterRoute).SetPath(common.ServiceId).SetNameFieldPath(serviceId).BuildPath()
err := utils.DeleteRequest(ctx, &emptyResponse, rc.baseUrl, requestPath, rc.authInjector)
if err != nil {
return errors.NewCommonEdgeXWrapper(err)
}
return nil
}
72 changes: 72 additions & 0 deletions clients/http/registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// Copyright (C) 2024 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

package http

import (
"context"
"net/http"
"testing"

"github.com/edgexfoundry/go-mod-core-contracts/v3/common"
"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests"
"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses"

"github.com/stretchr/testify/require"
)

const mockServiceId = "mock-service"

func TestRegister(t *testing.T) {
ts := newTestServer(http.MethodPost, common.ApiRegisterRoute, nil)
defer ts.Close()

client := NewRegistryClient(ts.URL, NewNullAuthenticationInjector(), false)
err := client.Register(context.Background(), requests.AddRegistrationRequest{})

require.NoError(t, err)
}

func TestUpdateRegister(t *testing.T) {
ts := newTestServer(http.MethodPut, common.ApiRegisterRoute, nil)
defer ts.Close()

client := NewRegistryClient(ts.URL, NewNullAuthenticationInjector(), false)
err := client.UpdateRegister(context.Background(), requests.AddRegistrationRequest{})

require.NoError(t, err)
}

func TestRegistrationByServiceId(t *testing.T) {
ts := newTestServer(http.MethodGet, common.ApiRegisterRoute+"/"+common.ServiceId+"/"+mockServiceId, responses.RegistrationResponse{})
defer ts.Close()

client := NewRegistryClient(ts.URL, NewNullAuthenticationInjector(), false)
res, err := client.RegistrationByServiceId(context.Background(), mockServiceId)

require.NoError(t, err)
require.IsType(t, responses.RegistrationResponse{}, res)
}

func TestAllRegistry(t *testing.T) {
ts := newTestServer(http.MethodGet, common.ApiAllRegistrationsRoute, responses.MultiRegistrationsResponse{})
defer ts.Close()

client := NewRegistryClient(ts.URL, NewNullAuthenticationInjector(), false)
res, err := client.AllRegistry(context.Background(), false)

require.NoError(t, err)
require.IsType(t, responses.MultiRegistrationsResponse{}, res)
}

func TestDeregister(t *testing.T) {
ts := newTestServer(http.MethodDelete, common.ApiRegisterRoute+"/"+common.ServiceId+"/"+mockServiceId, nil)
defer ts.Close()

client := NewRegistryClient(ts.URL, NewNullAuthenticationInjector(), false)
err := client.Deregister(context.Background(), mockServiceId)

require.NoError(t, err)
}
19 changes: 18 additions & 1 deletion clients/http/utils/request.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (C) 2020-2021 IOTech Ltd
// Copyright (C) 2020-2024 IOTech Ltd
// Copyright (C) 2023 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
Expand Down Expand Up @@ -238,3 +238,20 @@ func DeleteRequest(ctx context.Context, returnValuePointer interface{}, baseUrl
}
return nil
}

// DeleteRequestWithParams makes the delete request with URL query params and return the body
func DeleteRequestWithParams(ctx context.Context, returnValuePointer interface{}, baseUrl string, requestPath string, requestParams url.Values, authInjector interfaces.AuthenticationInjector) errors.EdgeX {
req, err := createRequest(ctx, http.MethodDelete, baseUrl, requestPath, requestParams)
if err != nil {
return errors.NewCommonEdgeXWrapper(err)
}

res, err := sendRequest(ctx, req, authInjector)
if err != nil {
return errors.NewCommonEdgeXWrapper(err)
}
if err := json.Unmarshal(res, returnValuePointer); err != nil {
return errors.NewCommonEdgeX(errors.KindContractInvalid, "failed to parse the response body", err)
}
return nil
}
29 changes: 29 additions & 0 deletions clients/interfaces/kvs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Copyright (C) 2024 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

package interfaces

import (
"context"

"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests"
"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses"
"github.com/edgexfoundry/go-mod-core-contracts/v3/errors"
)

// KVSClient defines the interface for interactions with the kvs endpoint on the EdgeX core-keeper service.
type KVSClient interface {
// UpdateValuesByKey updates values of the specified key and the child keys defined in the request payload.
// If no key exists at the given path, the key(s) will be created.
UpdateValuesByKey(ctx context.Context, key string, reqs requests.UpdateKeysRequest) (responses.KeysResponse, errors.EdgeX)
// ValuesByKey returns the values of the specified key prefix.
ValuesByKey(ctx context.Context, key string) (responses.MultiKeyValueResponse, errors.EdgeX)
// ListKeys returns the list of the keys with the specified key prefix.
ListKeys(ctx context.Context, key string) (responses.KeysResponse, errors.EdgeX)
// DeleteKey deletes the specified key.
DeleteKey(ctx context.Context, key string) (responses.KeysResponse, errors.EdgeX)
// DeleteKeysByPrefix deletes all keys with the specified prefix.
DeleteKeysByPrefix(ctx context.Context, key string) (responses.KeysResponse, errors.EdgeX)
}
Loading

0 comments on commit e3dd702

Please sign in to comment.