Add config file authentication to oci object storage state store (#1430)

* Improving testing - verify expected methods are invoked

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

* Adding support for composite keys (mapped to directories in buckets)

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

* Adding support for TTL - expired elements are not returned in Get

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

* fixing linting issues

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

* fixing linting issue

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

* adding support for TTL == -1 (never expire)

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

* fix linting issue

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

* introducing Instance Principal authentication for OCI client

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

* fix linting issue

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

* adding support for oci config file based authentication for OCI bjectStorage state store

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

* expand ~ to user home directory in config file path; fix linting findings

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

* gofumpt file because of linting issue

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

* Do not allow config file path to start with ~/

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

* introducing integration test for oci objectstorage state store

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

* Fixing linting issues in integration test

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

* Fixing linting issues in integration test

Signed-off-by: lucasjellema <lucasjellema@gmail.com>

Co-authored-by: Looong Dai <long.dai@intel.com>
Co-authored-by: Yaron Schneider <schneider.yaron@live.com>
This commit is contained in:
Lucas Jellema 2022-02-07 04:27:31 +01:00 committed by GitHub
parent 4885a835fc
commit a383697ef5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 720 additions and 70 deletions

View File

@ -1,7 +1,15 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation and Dapr Contributors.
// Licensed under the MIT License.
// ------------------------------------------------------------
/*
Copyright 2021 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package objectstorage
@ -11,11 +19,16 @@ import (
"fmt"
"io"
"io/ioutil"
"os"
"path"
"strconv"
"strings"
"time"
jsoniter "github.com/json-iterator/go"
"github.com/oracle/oci-go-sdk/v54/common"
"github.com/oracle/oci-go-sdk/v54/common/auth"
"github.com/oracle/oci-go-sdk/v54/objectstorage"
"github.com/dapr/components-contrib/state"
@ -23,14 +36,22 @@ import (
)
const (
keyDelimiter = "||"
tenancyKey = "tenancyOCID"
compartmentKey = "compartmentOCID"
regionKey = "region"
fingerPrintKey = "fingerPrint"
privateKeyKey = "privateKey"
userKey = "userOCID"
bucketNameKey = "bucketName"
keyDelimiter = "||"
instancePrincipalAuthenticationKey = "instancePrincipalAuthentication"
configFileAuthenticationKey = "configFileAuthentication"
configFilePathKey = "configFilePath"
configFileProfileKey = "configFileProfile"
tenancyKey = "tenancyOCID"
compartmentKey = "compartmentOCID"
regionKey = "region"
fingerPrintKey = "fingerPrint"
privateKeyKey = "privateKey"
userKey = "userOCID"
bucketNameKey = "bucketName"
metadataTTLKey = "ttlInSeconds"
daprStateStoreMetaLabel = "dapr-state-store"
expiryTimeMetaLabel = "expiry-time-from-ttl"
isoDateTimeFormat = "2006-01-02T15:04:05"
)
type StateStore struct {
@ -43,19 +64,24 @@ type StateStore struct {
}
type Metadata struct {
userOCID string
bucketName string
region string
tenancyOCID string
fingerPrint string
privateKey string
compartmentOCID string
namespace string
userOCID string
bucketName string
region string
tenancyOCID string
fingerPrint string
privateKey string
compartmentOCID string
namespace string
configFilePath string
configFileProfile string
instancePrincipalAuthentication bool
configFileAuthentication bool
OCIObjectStorageClient *objectstorage.ObjectStorageClient
}
type objectStoreClient interface {
getObject(ctx context.Context, objectname string, logger logger.Logger) ([]byte, *string, error)
getObject(ctx context.Context, objectname string, logger logger.Logger) (content []byte, etag *string, metadata map[string]string, err error)
deleteObject(ctx context.Context, objectname string, etag *string) (err error)
putObject(ctx context.Context, objectname string, contentLen int64, content io.ReadCloser, metadata map[string]string, etag *string, logger logger.Logger) error
initStorageBucket(logger logger.Logger) error
@ -147,38 +173,92 @@ func NewOCIObjectStorageStore(logger logger.Logger) *StateStore {
/************** private helper functions. */
func getValue(metadata map[string]string, key string, valueRequired bool) (value string, err error) {
if val, ok := metadata[key]; ok && val != "" {
return val, nil
}
if !valueRequired {
return "", nil
}
return "", fmt.Errorf("missing or empty %s field from metadata", key)
}
func getOptionalBooleanValue(metadata map[string]string, key string) (value bool, err error) {
stringValue, _ := getValue(metadata, key, false)
if stringValue == "" {
stringValue = "false"
}
value, err = strconv.ParseBool(stringValue)
if err != nil {
return false, fmt.Errorf("incorrect value %s for %s, should be 'true' or 'false'", stringValue, key)
}
return value, nil
}
func getConfigFilePath(meta map[string]string) (value string, err error) {
value, _ = getValue(meta, configFilePathKey, false)
if strings.HasPrefix(value, "~/") {
return "", fmt.Errorf("%s is set to %s which starts with ~/; this is not supported - please provide absolute path to configuration file", configFilePathKey, value)
}
if value != "" {
if _, err = os.Stat(value); err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("oci configuration file %s does not exist %w", value, err)
}
return "", fmt.Errorf("error %w with reading oci configuration file %s", err, value)
}
}
return value, nil
}
func getObjectStorageMetadata(metadata map[string]string) (*Metadata, error) {
meta := Metadata{}
var err error
if meta.bucketName, err = getValue(metadata, bucketNameKey); err != nil {
if meta.instancePrincipalAuthentication, err = getOptionalBooleanValue(metadata, instancePrincipalAuthenticationKey); err != nil {
return nil, err
}
if meta.region, err = getValue(metadata, regionKey); err != nil {
if meta.configFileAuthentication, err = getOptionalBooleanValue(metadata, configFileAuthenticationKey); err != nil {
return nil, err
}
if meta.compartmentOCID, err = getValue(metadata, compartmentKey); err != nil {
if meta.configFileAuthentication {
if meta.configFilePath, err = getConfigFilePath(metadata); err != nil {
return nil, err
}
meta.configFileProfile, _ = getValue(metadata, configFileProfileKey, false)
}
if meta.bucketName, err = getValue(metadata, bucketNameKey, true); err != nil {
return nil, err
}
if meta.userOCID, err = getValue(metadata, userKey); err != nil {
if meta.compartmentOCID, err = getValue(metadata, compartmentKey, true); err != nil {
return nil, err
}
if meta.fingerPrint, err = getValue(metadata, fingerPrintKey); err != nil {
return nil, err
}
if meta.privateKey, err = getValue(metadata, privateKeyKey); err != nil {
return nil, err
}
if meta.tenancyOCID, err = getValue(metadata, tenancyKey); err != nil {
return nil, err
externalAuthentication := meta.instancePrincipalAuthentication || meta.configFileAuthentication
if !externalAuthentication {
err = getIdentityAuthenticationDetails(metadata, &meta)
if err != nil {
return nil, err
}
}
return &meta, nil
}
func getValue(metadata map[string]string, key string) (string, error) {
if val, ok := metadata[key]; ok && val != "" {
return val, nil
func getIdentityAuthenticationDetails(metadata map[string]string, meta *Metadata) (err error) {
if meta.region, err = getValue(metadata, regionKey, true); err != nil {
return err
}
return "", fmt.Errorf("missing or empty %s field from metadata", key)
if meta.userOCID, err = getValue(metadata, userKey, true); err != nil {
return err
}
if meta.fingerPrint, err = getValue(metadata, fingerPrintKey, true); err != nil {
return err
}
if meta.privateKey, err = getValue(metadata, privateKeyKey, true); err != nil {
return err
}
if meta.tenancyOCID, err = getValue(metadata, tenancyKey, true); err != nil {
return err
}
return nil
}
// functions that bridge from the Dapr State API to the OCI ObjectStorage Client.
@ -190,6 +270,12 @@ func (r *StateStore) writeDocument(req *state.SetRequest) error {
r.logger.Debugf("when FirstWrite is to be enforced, a value must be provided for the ETag")
return fmt.Errorf("when FirstWrite is to be enforced, a value must be provided for the ETag")
}
metadata := (map[string]string{"category": daprStateStoreMetaLabel})
err := convertTTLtoExpiryTime(req, r.logger, metadata)
if err != nil {
return fmt.Errorf("failed to process ttl meta data: %w", err)
}
r.logger.Debugf("Save state in OCI Object Storage Bucket under key %s ", req.Key)
objectName := getFileName(req.Key)
@ -200,7 +286,7 @@ func (r *StateStore) writeDocument(req *state.SetRequest) error {
if req.Options.Concurrency != state.FirstWrite {
etag = nil
}
err := r.client.putObject(ctx, objectName, objectLength, ioutil.NopCloser(bytes.NewReader(content)), nil, etag, r.logger)
err = r.client.putObject(ctx, objectName, objectLength, ioutil.NopCloser(bytes.NewReader(content)), metadata, etag, r.logger)
if err != nil {
r.logger.Debugf("error in writing object to OCI object storage %s, err %s", req.Key, err)
return fmt.Errorf("failed to write object to OCI Object storage : %w", err)
@ -208,17 +294,43 @@ func (r *StateStore) writeDocument(req *state.SetRequest) error {
return nil
}
func convertTTLtoExpiryTime(req *state.SetRequest, logger logger.Logger, metadata map[string]string) error {
ttl, ttlerr := parseTTL(req.Metadata)
if ttlerr != nil {
return fmt.Errorf("error in parsing TTL %w", ttlerr)
}
if ttl != nil {
if *ttl == -1 {
logger.Debugf("TTL is set to -1; this means: never expire. ")
} else {
metadata[expiryTimeMetaLabel] = time.Now().UTC().Add(time.Second * time.Duration(*ttl)).Format(isoDateTimeFormat)
logger.Debugf("Set %s in meta properties for object to ", expiryTimeMetaLabel, metadata[expiryTimeMetaLabel])
}
}
return nil
}
func (r *StateStore) readDocument(req *state.GetRequest) ([]byte, *string, error) {
if len(req.Key) == 0 || req.Key == "" {
return nil, nil, fmt.Errorf("key for value to get was missing from request")
}
objectName := getFileName(req.Key)
ctx := context.Background()
content, etag, err := r.client.getObject(ctx, objectName, r.logger)
content, etag, meta, err := r.client.getObject(ctx, objectName, r.logger)
if err != nil {
r.logger.Debugf("download file %s, err %s", req.Key, err)
return nil, nil, fmt.Errorf("failed to read object from OCI Object storage : %w", err)
}
if expiryTimeString, ok := meta[expiryTimeMetaLabel]; ok {
expirationTime, err := time.Parse(isoDateTimeFormat, expiryTimeString)
if err != nil {
return nil, nil, fmt.Errorf("failed to get object from OCI because of invalid formatted value %s in meta property %s : %w", expiryTimeString, expiryTimeMetaLabel, err)
}
if time.Now().UTC().After(expirationTime) {
r.logger.Debug("failed to get object from OCI because it has expired; expiry time set to %s", expiryTimeString)
return nil, nil, nil
}
}
return content, etag, nil
}
@ -271,7 +383,21 @@ func getFileName(key string) string {
return pr[0]
}
return pr[1]
return path.Join(pr[0], pr[1])
}
func parseTTL(requestMetadata map[string]string) (*int, error) {
if val, found := requestMetadata[metadataTTLKey]; found && val != "" {
parsedVal, err := strconv.ParseInt(val, 10, 0)
if err != nil {
return nil, fmt.Errorf("error in parsing ttl metadata : %w", err)
}
parsedInt := int(parsedVal)
return &parsedInt, nil
}
return nil, nil
}
/**************** functions with OCI ObjectStorage Service interaction. */
@ -326,7 +452,7 @@ func createBucket(ctx context.Context, client objectstorage.ObjectStorageClient,
// ***** the functions that interact with OCI Object Storage AND constitute the objectStoreClient interface.
func (c *ociObjectStorageClient) getObject(ctx context.Context, objectname string, logger logger.Logger) ([]byte, *string, error) {
func (c *ociObjectStorageClient) getObject(ctx context.Context, objectname string, logger logger.Logger) (content []byte, etag *string, metadata map[string]string, err error) {
logger.Debugf("read file %s from OCI ObjectStorage StateStore %s ", objectname, &c.objectStorageMetadata.bucketName)
request := objectstorage.GetObjectRequest{
NamespaceName: &c.objectStorageMetadata.namespace,
@ -337,16 +463,13 @@ func (c *ociObjectStorageClient) getObject(ctx context.Context, objectname strin
if err != nil {
logger.Debugf("Issue in OCI ObjectStorage with retrieving object %s, error: %s", objectname, err)
if response.RawResponse.StatusCode == 404 {
return nil, nil, nil
return nil, nil, nil, nil
}
return nil, nil, fmt.Errorf("failed to retrieve object : %w", err)
}
if response.ETag != nil {
logger.Debugf("OCI ObjectStorage StateStore metadata: ETag %s", *response.ETag)
return nil, nil, nil, fmt.Errorf("failed to retrieve object : %w", err)
}
buf := new(bytes.Buffer)
buf.ReadFrom(response.Content)
return buf.Bytes(), response.ETag, nil
return buf.Bytes(), response.ETag, response.OpcMeta, nil
}
func (c *ociObjectStorageClient) deleteObject(ctx context.Context, objectname string, etag *string) (err error) {
@ -391,7 +514,24 @@ func (c *ociObjectStorageClient) initStorageBucket(logger logger.Logger) error {
}
func (c *ociObjectStorageClient) initOCIObjectStorageClient(logger logger.Logger) (*objectstorage.ObjectStorageClient, error) {
configurationProvider := common.NewRawConfigurationProvider(c.objectStorageMetadata.tenancyOCID, c.objectStorageMetadata.userOCID, c.objectStorageMetadata.region, c.objectStorageMetadata.fingerPrint, c.objectStorageMetadata.privateKey, nil)
var configurationProvider common.ConfigurationProvider
if c.objectStorageMetadata.instancePrincipalAuthentication {
logger.Debugf("instance principal authentication is used. ")
var err error
configurationProvider, err = auth.InstancePrincipalConfigurationProvider()
if err != nil {
return nil, fmt.Errorf("failed to get oci configurationprovider based on instance principal authentication : %w", err)
}
} else {
if c.objectStorageMetadata.configFileAuthentication {
logger.Debugf("configuration file based authentication is used with configuration file path %s and configuration profile %s. ", c.objectStorageMetadata.configFilePath, c.objectStorageMetadata.configFileProfile)
configurationProvider = common.CustomProfileConfigProvider(c.objectStorageMetadata.configFilePath, c.objectStorageMetadata.configFileProfile)
} else {
logger.Debugf("identity authentication is used with configuration provided through Dapr component configuration ")
configurationProvider = common.NewRawConfigurationProvider(c.objectStorageMetadata.tenancyOCID, c.objectStorageMetadata.userOCID, c.objectStorageMetadata.region, c.objectStorageMetadata.fingerPrint, c.objectStorageMetadata.privateKey, nil)
}
}
objectStorageClient, cerr := objectstorage.NewObjectStorageClientWithConfigurationProvider(configurationProvider)
if cerr != nil {
return nil, fmt.Errorf("failed to create ObjectStorageClient : %w", cerr)

View File

@ -0,0 +1,296 @@
package objectstorage
// run the test for example in ~/dapr-dev/components-contrib
// go test -v github.com/dapr/components-contrib/state/oci/objectstorage.
import (
"fmt"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/dapr/components-contrib/state"
"github.com/dapr/kit/logger"
)
const (
configFilePathEnvKey = "DAPR_TEST_OCI_CONFIG_FILE_PATH"
bucketNameEnvKey = "DAPR_TEST_OCI_BUCKET_NAME"
compartmentOCIDEnvKey = "DAPR_TEST_OCI_COMPARTMENT_OCID"
configFileProfileEnvKey = "DAPR_TEST_OCI_CONFIG_PROFILE"
)
func getConfigFilePathString() string {
return os.Getenv(configFilePathEnvKey)
}
func getConfigProfile() string {
return os.Getenv(configFileProfileEnvKey)
}
func getCompartmentOCID() string {
return os.Getenv(compartmentOCIDEnvKey)
}
func getBucketName() string {
return os.Getenv(bucketNameEnvKey)
}
func TestOCIObjectStorageIntegration(t *testing.T) {
ociObjectStorageConfiguration := map[string]string{
"configFileAuthentication": "true",
}
configFilePath := getConfigFilePathString()
if configFilePath == "" {
// first run export DAPR_TEST_OCI_CONFIG_FILE_PATH="/home/app/.oci/config".
t.Skipf("OCI ObjectStorage state integration tests skipped. To enable define the configuration file path string using environment variable '%s' (example 'export %s=\"/home/app/.oci/config\")", configFilePathEnvKey, configFilePathEnvKey)
}
ociObjectStorageConfiguration["configFilePath"] = configFilePath
ociObjectStorageConfiguration["configFileProfile"] = getConfigProfile()
compartmentOCID := getCompartmentOCID()
if compartmentOCID == "" {
t.Skipf("OCI ObjectStorage state integration tests skipped. To enable define the compartment OCID string using environment variable '%s' (example 'export %s=\"ocid1.compartment.oc1..aaaaaaaacsssekayq4d34nl5h3eqs5e6ak3j5s4jhlws7rr5pxmt3zrq\")", compartmentOCIDEnvKey, compartmentOCIDEnvKey)
}
ociObjectStorageConfiguration["compartmentOCID"] = compartmentOCID
bucketName := getBucketName()
if bucketName == "" {
bucketName = "DAPR_TEST_BUCKET"
}
ociObjectStorageConfiguration["bucketName"] = bucketName
t.Run("Test Get", func(t *testing.T) {
t.Parallel()
testGet(t, ociObjectStorageConfiguration)
})
t.Run("Test Set", func(t *testing.T) {
t.Parallel()
testSet(t, ociObjectStorageConfiguration)
})
t.Run("Test Delete", func(t *testing.T) {
t.Parallel()
testDelete(t, ociObjectStorageConfiguration)
})
t.Run("Test Ping", func(t *testing.T) {
t.Parallel()
testPing(t, ociObjectStorageConfiguration)
})
}
func testGet(t *testing.T, ociProperties map[string]string) {
statestore := NewOCIObjectStorageStore(logger.NewLogger("logger"))
meta := state.Metadata{}
meta.Properties = ociProperties
t.Run("Get an non-existing key", func(t *testing.T) {
err := statestore.Init(meta)
assert.Nil(t, err)
getResponse, err := statestore.Get(&state.GetRequest{Key: "xyzq"})
assert.Equal(t, &state.GetResponse{}, getResponse, "Response must be empty")
assert.NoError(t, err, "Non-existing key must not be treated as error")
})
t.Run("Get an existing key", func(t *testing.T) {
err := statestore.Init(meta)
assert.Nil(t, err)
err = statestore.Set(&state.SetRequest{Key: "test-key", Value: []byte("test-value")})
assert.Nil(t, err)
getResponse, err := statestore.Get(&state.GetRequest{Key: "test-key"})
assert.Nil(t, err)
assert.Equal(t, "test-value", string(getResponse.Data), "Value retrieved should be equal to value set")
assert.NotNil(t, *getResponse.ETag, "ETag should be set")
})
t.Run("Get an existing composed key", func(t *testing.T) {
err := statestore.Init(meta)
assert.Nil(t, err)
err = statestore.Set(&state.SetRequest{Key: "test-app||test-key", Value: []byte("test-value")})
assert.Nil(t, err)
getResponse, err := statestore.Get(&state.GetRequest{Key: "test-app||test-key"})
assert.Nil(t, err)
assert.Equal(t, "test-value", string(getResponse.Data), "Value retrieved should be equal to value set")
})
t.Run("Get an unexpired state element with TTL set", func(t *testing.T) {
testKey := "unexpired-ttl-test-key"
err := statestore.Init(meta)
assert.Nil(t, err)
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value"), Metadata: (map[string]string{
"ttlInSeconds": "100",
})})
assert.Nil(t, err)
getResponse, err := statestore.Get(&state.GetRequest{Key: testKey})
assert.Nil(t, err)
assert.Equal(t, "test-value", string(getResponse.Data), "Value retrieved should be equal to value set despite TTL setting")
})
t.Run("Get a state element with TTL set to -1 (not expire)", func(t *testing.T) {
testKey := "never-expiring-ttl-test-key"
err := statestore.Init(meta)
assert.Nil(t, err)
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value"), Metadata: (map[string]string{
"ttlInSeconds": "-1",
})})
assert.Nil(t, err)
getResponse, err := statestore.Get(&state.GetRequest{Key: testKey})
assert.Nil(t, err)
assert.Equal(t, "test-value", string(getResponse.Data), "Value retrieved should be equal (TTL setting of -1 means never expire)")
})
t.Run("Get an expired (TTL in the past) state element", func(t *testing.T) {
err := statestore.Init(meta)
assert.Nil(t, err)
err = statestore.Set(&state.SetRequest{Key: "ttl-test-key", Value: []byte("test-value"), Metadata: (map[string]string{
"ttlInSeconds": "1",
})})
assert.Nil(t, err)
time.Sleep(time.Second * 2)
getResponse, err := statestore.Get(&state.GetRequest{Key: "ttl-test-key"})
assert.Equal(t, &state.GetResponse{}, getResponse, "Response must be empty")
assert.NoError(t, err, "Expired element must not be treated as error")
})
}
func testSet(t *testing.T, ociProperties map[string]string) {
meta := state.Metadata{}
meta.Properties = ociProperties
statestore := NewOCIObjectStorageStore(logger.NewLogger("logger"))
t.Run("Set without a key", func(t *testing.T) {
err := statestore.Init(meta)
assert.Nil(t, err)
err = statestore.Set(&state.SetRequest{Value: []byte("test-value")})
assert.Equal(t, err, fmt.Errorf("key for value to set was missing from request"), "Lacking Key results in error")
})
t.Run("Regular Set Operation", func(t *testing.T) {
testKey := "local-test-key"
err := statestore.Init(meta)
assert.Nil(t, err)
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value")})
assert.Nil(t, err, "Setting a value with a proper key should be errorfree")
getResponse, err := statestore.Get(&state.GetRequest{Key: testKey})
assert.Nil(t, err)
assert.Equal(t, "test-value", string(getResponse.Data), "Value retrieved should be equal to value set")
assert.NotNil(t, *getResponse.ETag, "ETag should be set")
})
t.Run("Regular Set Operation with composite key", func(t *testing.T) {
testKey := "test-app||other-test-key"
err := statestore.Init(meta)
assert.Nil(t, err)
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value")})
assert.Nil(t, err, "Setting a value with a proper composite key should be errorfree")
getResponse, err := statestore.Get(&state.GetRequest{Key: testKey})
assert.Nil(t, err)
assert.Equal(t, "test-value", string(getResponse.Data), "Value retrieved should be equal to value set")
assert.NotNil(t, *getResponse.ETag, "ETag should be set")
})
t.Run("Regular Set Operation with TTL", func(t *testing.T) {
testKey := "test-key-with-ttl"
err := statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value"), Metadata: (map[string]string{
"ttlInSeconds": "500",
})})
assert.Nil(t, err, "Setting a value with a proper key and a correct TTL value should be errorfree")
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value"), Metadata: (map[string]string{
"ttlInSeconds": "XXX",
})})
assert.NotNil(t, err, "Setting a value with a proper key and a incorrect TTL value should be produce an error")
})
t.Run("Testing Set & Concurrency (ETags)", func(t *testing.T) {
testKey := "etag-test-key"
err := statestore.Init(meta)
assert.Nil(t, err)
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value")})
assert.Nil(t, err, "Setting a value with a proper key should be errorfree")
getResponse, _ := statestore.Get(&state.GetRequest{Key: testKey})
etag := getResponse.ETag
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("overwritten-value"), ETag: etag, Options: state.SetStateOption{
Concurrency: state.FirstWrite,
}})
assert.Nil(t, err, "Updating value with proper etag should go fine")
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("more-overwritten-value"), ETag: etag, Options: state.SetStateOption{
Concurrency: state.FirstWrite,
}})
assert.NotNil(t, err, "Updating value with the old etag should be refused")
// retrieve the latest etag - assigned by the previous set operation.
getResponse, _ = statestore.Get(&state.GetRequest{Key: testKey})
assert.NotNil(t, *getResponse.ETag, "ETag should be set")
etag = getResponse.ETag
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("more-overwritten-value"), ETag: etag, Options: state.SetStateOption{
Concurrency: state.FirstWrite,
}})
assert.Nil(t, err, "Updating value with the latest etag should be accepted")
})
}
func testDelete(t *testing.T, ociProperties map[string]string) {
m := state.Metadata{}
m.Properties = ociProperties
s := NewOCIObjectStorageStore(logger.NewLogger("logger"))
t.Run("Delete without a key", func(t *testing.T) {
err := s.Init(m)
assert.Nil(t, err)
err = s.Delete(&state.DeleteRequest{})
assert.Equal(t, err, fmt.Errorf("key for value to delete was missing from request"), "Lacking Key results in error")
})
t.Run("Regular Delete Operation", func(t *testing.T) {
testKey := "test-key"
err := s.Init(m)
assert.Nil(t, err)
err = s.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value")})
assert.Nil(t, err, "Setting a value with a proper key should be errorfree")
err = s.Delete(&state.DeleteRequest{Key: testKey})
assert.Nil(t, err, "Deleting an existing value with a proper key should be errorfree")
})
t.Run("Regular Delete Operation for composite key", func(t *testing.T) {
testKey := "test-app||some-test-key"
err := s.Init(m)
assert.Nil(t, err)
err = s.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value")})
assert.Nil(t, err, "Setting a value with a proper composite key should be errorfree")
err = s.Delete(&state.DeleteRequest{Key: testKey})
assert.Nil(t, err, "Deleting an existing value with a proper composite key should be errorfree")
})
t.Run("Delete with an unknown key", func(t *testing.T) {
err := s.Delete(&state.DeleteRequest{Key: "unknownKey"})
assert.Contains(t, err.Error(), "404", "Unknown Key results in error: http status code 404, object not found")
})
t.Run("Testing Delete & Concurrency (ETags)", func(t *testing.T) {
testKey := "etag-test-delete-key"
err := s.Init(m)
assert.Nil(t, err)
// create document.
err = s.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value")})
assert.Nil(t, err, "Setting a value with a proper key should be errorfree")
getResponse, _ := s.Get(&state.GetRequest{Key: testKey})
etag := getResponse.ETag
incorrectETag := "someRandomETag"
err = s.Delete(&state.DeleteRequest{Key: testKey, ETag: &incorrectETag, Options: state.DeleteStateOption{
Concurrency: state.FirstWrite,
}})
assert.NotNil(t, err, "Deleting value with an incorrect etag should be prevented")
err = s.Delete(&state.DeleteRequest{Key: testKey, ETag: etag, Options: state.DeleteStateOption{
Concurrency: state.FirstWrite,
}})
assert.Nil(t, err, "Deleting value with proper etag should go fine")
})
}
func testPing(t *testing.T, ociProperties map[string]string) {
m := state.Metadata{}
m.Properties = ociProperties
s := NewOCIObjectStorageStore(logger.NewLogger("logger"))
t.Run("Ping", func(t *testing.T) {
err := s.Init(m)
assert.Nil(t, err)
err = s.Ping()
assert.Nil(t, err, "Ping should be successful")
})
}

View File

@ -1,14 +1,25 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation and Dapr Contributors.
// Licensed under the MIT License.
// ------------------------------------------------------------
/*
Copyright 2021 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package objectstorage
import (
"context"
"fmt"
"io"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
@ -19,19 +30,20 @@ import (
func getDummyOCIObjectStorageConfiguration() map[string]string {
return map[string]string{
"bucketName": "myBuck",
"tenancyOCID": "ocid1.tenancy.oc1..aaaaaaaag7c7sq",
"userOCID": "ocid1.user.oc1..aaaaaaaaby",
"compartmentOCID": "ocid1.compartment.oc1..aaaaaaaq",
"fingerPrint": "02:91:6c",
"privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEogI=\n-----END RSA PRIVATE KEY-----",
"region": "us-ashburn-1",
"tenancyOCID": "xxxocid1.tenancy.oc1..aaaaaaaag7c7sq",
"userOCID": "xxxxocid1.user.oc1..aaaaaaaaby",
"compartmentOCID": "xxxocid1.compartment.oc1..aaaaaaaq",
"fingerPrint": "xxx02:91:6c",
"privateKey": "xxx-----BEGIN RSA PRIVATE KEY-----\nMIIEogI=\n-----END RSA PRIVATE KEY-----",
"region": "xxxus-ashburn-1",
}
}
func TestInit(t *testing.T) {
meta := state.Metadata{}
statestore := NewOCIObjectStorageStore(logger.NewLogger("logger"))
t.Run("Init with complete yet incorrect metadata", func(t *testing.T) {
t.Parallel()
t.Run("Init with beautifully complete yet incorrect metadata", func(t *testing.T) {
meta.Properties = getDummyOCIObjectStorageConfiguration()
err := statestore.Init(meta)
assert.NotNil(t, err)
@ -80,9 +92,62 @@ func TestInit(t *testing.T) {
assert.NotNil(t, err)
assert.Equal(t, fmt.Errorf("missing or empty privateKey field from metadata"), err, "Lacking configuration property should be spotted")
})
t.Run("Init with incorrect value for instancePrincipalAuthentication", func(t *testing.T) {
meta.Properties = getDummyOCIObjectStorageConfiguration()
meta.Properties[instancePrincipalAuthenticationKey] = "ZQWE"
err := statestore.Init(meta)
assert.NotNil(t, err, "if instancePrincipalAuthentication is defined, it should be true or false; if not: error should be raised ")
})
t.Run("Init with missing fingerprint with instancePrincipalAuthentication", func(t *testing.T) {
meta.Properties = getDummyOCIObjectStorageConfiguration()
meta.Properties[fingerPrintKey] = ""
meta.Properties[instancePrincipalAuthenticationKey] = "true"
err := statestore.Init(meta)
if err != nil {
assert.Contains(t, err.Error(), "failed to initialize client", "unit tests not run on OCI will not be able to correctly create an OCI client based on instance principal authentication")
}
})
t.Run("Init with configFileAuthentication and incorrect configFilePath", func(t *testing.T) {
meta.Properties = getDummyOCIObjectStorageConfiguration()
meta.Properties[configFileAuthenticationKey] = "true"
meta.Properties[configFilePathKey] = "file_does_not_exist"
err := statestore.Init(meta)
assert.NotNil(t, err, "if configFileAuthentication is true and configFilePath does not indicate an existing file, then an error should be produced")
if err != nil {
assert.Contains(t, err.Error(), "does not exist", "if configFileAuthentication is true and configFilePath does not indicate an existing file, then an error should be produced that indicates this")
}
})
t.Run("Init with configFileAuthentication and configFilePath starting with ~/", func(t *testing.T) {
meta.Properties = getDummyOCIObjectStorageConfiguration()
meta.Properties[configFileAuthenticationKey] = "true"
meta.Properties[configFilePathKey] = "~/some-file"
err := statestore.Init(meta)
assert.NotNil(t, err, "if configFileAuthentication is true and configFilePath contains a value that starts with ~/ , then an error should be produced")
if err != nil {
assert.Contains(t, err.Error(), "~", "if configFileAuthentication is true and configFilePath starts with ~/, then an error should be produced that indicates this")
}
})
t.Run("Init with missing fingerprint with false instancePrincipalAuthentication and false configFileAuthentication", func(t *testing.T) {
meta.Properties = getDummyOCIObjectStorageConfiguration()
meta.Properties[fingerPrintKey] = ""
meta.Properties[instancePrincipalAuthenticationKey] = "false"
err := statestore.Init(meta)
assert.NotNil(t, err, "if instancePrincipalAuthentication and configFileAuthentication are both false, then fingerprint is required and an error should be raised when it is missing")
})
t.Run("Init with missing fingerprint with configFileAuthentication", func(t *testing.T) {
meta.Properties = getDummyOCIObjectStorageConfiguration()
meta.Properties[fingerPrintKey] = ""
meta.Properties[instancePrincipalAuthenticationKey] = "false"
meta.Properties[configFileAuthenticationKey] = "true"
err := statestore.Init(meta)
if err != nil {
assert.Contains(t, err.Error(), "failed to initialize client", "if configFileAuthentication is true, then fingerprint is not required and error should be raised for failed to initialize client, not for missing fingerprint")
}
})
}
func TestFeatures(t *testing.T) {
t.Parallel()
s := NewOCIObjectStorageStore(logger.NewLogger("logger"))
t.Run("Test contents of Features", func(t *testing.T) {
features := s.Features()
@ -90,16 +155,52 @@ func TestFeatures(t *testing.T) {
})
}
type mockedObjectStoreClient struct {
objectStoreClient
func TestGetObjectStorageMetadata(t *testing.T) {
t.Parallel()
t.Run("Test getObjectStorageMetadata with full properties map", func(t *testing.T) {
meta, err := getObjectStorageMetadata(getDummyOCIObjectStorageConfiguration())
assert.Nil(t, err, "No error expected in clean property set")
assert.Equal(t, getDummyOCIObjectStorageConfiguration()["region"], meta.region, "Region in object storage metadata should match region in properties")
})
t.Run("Test getObjectStorageMetadata with incomplete property set", func(t *testing.T) {
properties := map[string]string{
"region": "xxxus-ashburn-1",
}
_, err := getObjectStorageMetadata(properties)
assert.NotNil(t, err, "Error expected with incomplete property set")
})
}
func (c *mockedObjectStoreClient) getObject(ctx context.Context, objectname string, logger logger.Logger) ([]byte, *string, error) {
etag := "etag"
return []byte("Hello World"), &etag, nil
type mockedObjectStoreClient struct {
ociObjectStorageClient
getIsCalled bool
putIsCalled bool
deleteIsCalled bool
pingBucketIsCalled bool
}
func (c *mockedObjectStoreClient) getObject(ctx context.Context, objectname string, logger logger.Logger) (content []byte, etag *string, metadata map[string]string, err error) {
c.getIsCalled = true
etagString := "etag"
contentString := "Hello World"
metadata = map[string]string{}
if objectname == "unknownKey" {
return nil, nil, nil, nil
}
if objectname == "test-expired-ttl-key" {
metadata[expiryTimeMetaLabel] = time.Now().UTC().Add(time.Second * -10).Format(isoDateTimeFormat)
}
if objectname == "test-app/test-key" {
contentString = "Hello Continent"
}
return []byte(contentString), &etagString, metadata, nil
}
func (c *mockedObjectStoreClient) deleteObject(ctx context.Context, objectname string, etag *string) (err error) {
c.deleteIsCalled = true
if objectname == "unknownKey" {
return fmt.Errorf("failed to delete object that does not exist - HTTP status code 404")
}
@ -110,6 +211,7 @@ func (c *mockedObjectStoreClient) deleteObject(ctx context.Context, objectname s
}
func (c *mockedObjectStoreClient) putObject(ctx context.Context, objectname string, contentLen int64, content io.ReadCloser, metadata map[string]string, etag *string, logger logger.Logger) error {
c.putIsCalled = true
if etag != nil && *etag == "notTheCorrectETag" {
return fmt.Errorf("failed to delete object because of incorrect etag-value ")
}
@ -124,22 +226,41 @@ func (c *mockedObjectStoreClient) initStorageBucket(logger logger.Logger) error
}
func (c *mockedObjectStoreClient) pingBucket(logger logger.Logger) error {
c.pingBucketIsCalled = true
return nil
}
func TestGetWithMockClient(t *testing.T) {
s := NewOCIObjectStorageStore(logger.NewLogger("logger"))
s.client = &mockedObjectStoreClient{}
t.Run("Test contents of Features", func(t *testing.T) {
mockClient := &mockedObjectStoreClient{}
s.client = mockClient
t.Parallel()
t.Run("Test regular Get", func(t *testing.T) {
getResponse, err := s.Get(&state.GetRequest{Key: "test-key"})
assert.True(t, mockClient.getIsCalled, "function Get should be invoked on the mockClient")
assert.Equal(t, "Hello World", string(getResponse.Data), "Value retrieved should be equal to value set")
assert.NotNil(t, *getResponse.ETag, "ETag should be set")
assert.Nil(t, err)
})
t.Run("Test Get with composite key", func(t *testing.T) {
getResponse, err := s.Get(&state.GetRequest{Key: "test-app||test-key"})
assert.Equal(t, "Hello Continent", string(getResponse.Data), "Value retrieved should be equal to value set")
assert.Nil(t, err)
})
t.Run("Test Get with an unknown key", func(t *testing.T) {
getResponse, err := s.Get(&state.GetRequest{Key: "unknownKey"})
assert.Nil(t, getResponse.Data, "No value should be retrieved for an unknown key")
assert.Nil(t, err, "404", "Not finding an object because of unknown key should not result in an error")
})
t.Run("Test expired element (because of TTL) ", func(t *testing.T) {
getResponse, err := s.Get(&state.GetRequest{Key: "test-expired-ttl-key"})
assert.Nil(t, getResponse.Data, "No value should be retrieved for an expired state element")
assert.Nil(t, err, "Not returning an object because of expiration should not result in an error")
})
}
func TestInitWithMockClient(t *testing.T) {
t.Parallel()
s := NewOCIObjectStorageStore(logger.NewLogger("logger"))
s.client = &mockedObjectStoreClient{}
meta := state.Metadata{}
@ -150,18 +271,23 @@ func TestInitWithMockClient(t *testing.T) {
}
func TestPingWithMockClient(t *testing.T) {
t.Parallel()
s := NewOCIObjectStorageStore(logger.NewLogger("logger"))
s.client = &mockedObjectStoreClient{}
mockClient := &mockedObjectStoreClient{}
s.client = mockClient
t.Run("Test Ping", func(t *testing.T) {
err := s.Ping()
assert.Nil(t, err)
assert.True(t, mockClient.pingBucketIsCalled, "function pingBucket should be invoked on the mockClient")
})
}
func TestSetWithMockClient(t *testing.T) {
t.Parallel()
statestore := NewOCIObjectStorageStore(logger.NewLogger("logger"))
statestore.client = &mockedObjectStoreClient{}
mockClient := &mockedObjectStoreClient{}
statestore.client = mockClient
t.Run("Set without a key", func(t *testing.T) {
err := statestore.Set(&state.SetRequest{Value: []byte("test-value")})
assert.Equal(t, err, fmt.Errorf("key for value to set was missing from request"), "Lacking Key results in error")
@ -170,6 +296,24 @@ func TestSetWithMockClient(t *testing.T) {
testKey := "test-key"
err := statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value")})
assert.Nil(t, err, "Setting a value with a proper key should be errorfree")
assert.True(t, mockClient.putIsCalled, "function put should be invoked on the mockClient")
})
t.Run("Regular Set Operation with TTL", func(t *testing.T) {
testKey := "test-key"
err := statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value"), Metadata: (map[string]string{
"ttlInSeconds": "5",
})})
assert.Nil(t, err, "Setting a value with a proper key and a correct TTL value should be errorfree")
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value"), Metadata: (map[string]string{
"ttlInSeconds": "XXX",
})})
assert.NotNil(t, err, "Setting a value with a proper key and a incorrect TTL value should be produce an error")
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value"), Metadata: (map[string]string{
"ttlInSeconds": "1",
})})
assert.Nil(t, err, "Setting a value with a proper key and a correct TTL value should be errorfree")
})
t.Run("Testing Set & Concurrency (ETags)", func(t *testing.T) {
testKey := "etag-test-key"
@ -199,8 +343,10 @@ func TestSetWithMockClient(t *testing.T) {
}
func TestDeleteWithMockClient(t *testing.T) {
t.Parallel()
s := NewOCIObjectStorageStore(logger.NewLogger("logger"))
s.client = &mockedObjectStoreClient{}
mockClient := &mockedObjectStoreClient{}
s.client = mockClient
t.Run("Delete without a key", func(t *testing.T) {
err := s.Delete(&state.DeleteRequest{})
assert.Equal(t, err, fmt.Errorf("key for value to delete was missing from request"), "Lacking Key results in error")
@ -213,6 +359,7 @@ func TestDeleteWithMockClient(t *testing.T) {
testKey := "test-key"
err := s.Delete(&state.DeleteRequest{Key: testKey})
assert.Nil(t, err, "Deleting an existing value with a proper key should be errorfree")
assert.True(t, mockClient.deleteIsCalled, "function delete should be invoked on the mockClient")
})
t.Run("Testing Delete & Concurrency (ETags)", func(t *testing.T) {
testKey := "etag-test-delete-key"
@ -234,3 +381,70 @@ func TestDeleteWithMockClient(t *testing.T) {
assert.NotNil(t, err, "Asking for FirstWrite concurrency policy without ETag should fail")
})
}
func TestGetValue(t *testing.T) {
meta := map[string]string{
"testKey": "theValue",
}
t.Parallel()
t.Run("Existing value", func(t *testing.T) {
value, _ := getValue(meta, "testKey", true)
assert.Equal(t, "theValue", value)
})
t.Run("Non-existing, required value", func(t *testing.T) {
value, err := getValue(meta, "noKey", true)
assert.NotNil(t, err, "Missing required value should result in error")
assert.Equal(t, "", value, "Empty string should be returned for non-existing value")
})
t.Run("Non-existing, optional value", func(t *testing.T) {
value, err := getValue(meta, "noKey", false)
assert.Nil(t, err, "Missing optional value should not result in error")
assert.Equal(t, "", value, "Empty string should be returned for non-existing value")
})
}
func TestGetFilename(t *testing.T) {
t.Parallel()
t.Run("Valid composite key", func(t *testing.T) {
filename := getFileName("app-id||key")
assert.Equal(t, "app-id/key", filename)
})
t.Run("Normal (not-composite) key", func(t *testing.T) {
filename := getFileName("app-id-key")
assert.Equal(t, "app-id-key", filename)
})
}
func TestParseTTL(t *testing.T) {
t.Parallel()
t.Run("TTL Not an integer", func(t *testing.T) {
ttlInSeconds := "not an integer"
ttl, err := parseTTL(map[string]string{
"ttlInSeconds": ttlInSeconds,
})
assert.Error(t, err)
assert.Nil(t, ttl)
})
t.Run("TTL specified with wrong key", func(t *testing.T) {
ttlInSeconds := 12345
ttl, err := parseTTL(map[string]string{
"expirationTime": strconv.Itoa(ttlInSeconds),
})
assert.NoError(t, err)
assert.Nil(t, ttl)
})
t.Run("TTL is a number", func(t *testing.T) {
ttlInSeconds := 12345
ttl, err := parseTTL(map[string]string{
"ttlInSeconds": strconv.Itoa(ttlInSeconds),
})
assert.NoError(t, err)
assert.Equal(t, *ttl, ttlInSeconds)
})
t.Run("TTL not set", func(t *testing.T) {
ttl, err := parseTTL(map[string]string{})
assert.NoError(t, err)
assert.Nil(t, ttl)
})
}