Skip to content

Commit

Permalink
azure appconfig configuration store (dapr#1874)
Browse files Browse the repository at this point in the history
* azure appconfig configuration store

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

* added retry options

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

* removed unused property

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

* fixed error msgs, typos and test names

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

* resource documenation

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

* return err from not implemented functions

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

Co-authored-by: Bernd Verst <4535280+berndverst@users.noreply.github.com>
Co-authored-by: Loong Dai <long.dai@intel.com>
Signed-off-by: Andrew Duss <andy.duss@storable.com>
  • Loading branch information
3 people authored and Andrew Duss committed Aug 18, 2022
1 parent 1093e29 commit 1ccb4e4
Show file tree
Hide file tree
Showing 6 changed files with 405 additions and 0 deletions.
195 changes: 195 additions & 0 deletions configuration/azure/appconfig/appconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/*
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 appconfig

import (
"context"
"fmt"
"strconv"
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig"

"github.com/dapr/components-contrib/configuration"
azauth "github.com/dapr/components-contrib/internal/authentication/azure"

"github.com/dapr/kit/logger"
)

const (
host = "appConfigHost"
connectionString = "appConfigConnectionString"
maxRetries = "maxRetries"
retryDelay = "retryDelay"
maxRetryDelay = "maxRetryDelay"
defaultMaxRetries = 3
defaultRetryDelay = time.Second * 4
defaultMaxRetryDelay = time.Second * 120
)

// ConfigurationStore is a Azure App Configuration store.
type ConfigurationStore struct {
client *azappconfig.Client
metadata metadata

logger logger.Logger
}

// NewAzureAppConfigurationStore returns a new Azure App Configuration store.
func NewAzureAppConfigurationStore(logger logger.Logger) configuration.Store {
s := &ConfigurationStore{
logger: logger,
}

return s
}

// Init does metadata and connection parsing.
func (r *ConfigurationStore) Init(metadata configuration.Metadata) error {
m, err := parseMetadata(metadata)
if err != nil {
return err
}
r.metadata = m

coreClientOpts := azcore.ClientOptions{
Telemetry: policy.TelemetryOptions{
ApplicationID: "dapr-" + logger.DaprVersion,
},
Retry: policy.RetryOptions{
MaxRetries: int32(m.maxRetries),
RetryDelay: m.maxRetryDelay,
MaxRetryDelay: m.maxRetryDelay,
},
}

options := azappconfig.ClientOptions{
ClientOptions: coreClientOpts,
}

if r.metadata.connectionString != "" {
r.client, err = azappconfig.NewClientFromConnectionString(r.metadata.connectionString, &options)
if err != nil {
return err
}
} else {
var settings azauth.EnvironmentSettings
settings, err = azauth.NewEnvironmentSettings("appconfig", metadata.Properties)
if err != nil {
return err
}

var cred azcore.TokenCredential
cred, err = settings.GetTokenCredential()
if err != nil {
return err
}

r.client, err = azappconfig.NewClient(r.metadata.host, cred, &options)
if err != nil {
return err
}
}

return nil
}

func parseMetadata(meta configuration.Metadata) (metadata, error) {
m := metadata{}

if val, ok := meta.Properties[host]; ok && val != "" {
m.host = val
}

if val, ok := meta.Properties[connectionString]; ok && val != "" {
m.connectionString = val
}

if m.connectionString != "" && m.host != "" {
return m, fmt.Errorf("azure appconfig error: can't set both %s and %s fields in metadata", host, connectionString)
}

if m.connectionString == "" && m.host == "" {
return m, fmt.Errorf("azure appconfig error: specify %s or %s field in metadata", host, connectionString)
}

m.maxRetries = defaultMaxRetries
if val, ok := meta.Properties[maxRetries]; ok && val != "" {
parsedVal, err := strconv.Atoi(val)
if err != nil {
return m, fmt.Errorf("azure appconfig error: can't parse maxRetries field: %s", err)
}
m.maxRetries = parsedVal
}

m.maxRetryDelay = defaultMaxRetryDelay
if val, ok := meta.Properties[maxRetryDelay]; ok && val != "" {
parsedVal, err := strconv.Atoi(val)
if err != nil {
return m, fmt.Errorf("azure appconfig error: can't parse maxRetryDelay field: %s", err)
}
m.maxRetryDelay = time.Duration(parsedVal)
}

m.retryDelay = defaultRetryDelay
if val, ok := meta.Properties[retryDelay]; ok && val != "" {
parsedVal, err := strconv.Atoi(val)
if err != nil {
return m, fmt.Errorf("azure appconfig error: can't parse retryDelay field: %s", err)
}
m.retryDelay = time.Duration(parsedVal)
}

return m, nil
}

func (r *ConfigurationStore) Get(ctx context.Context, req *configuration.GetRequest) (*configuration.GetResponse, error) {
keys := req.Keys
items := make([]*configuration.Item, 0, len(keys))
for _, key := range keys {
resp, err := r.client.GetSetting(
ctx,
key,
&azappconfig.GetSettingOptions{
Label: to.Ptr("label"),
})
if err != nil {
return nil, err
}

item := &configuration.Item{
Metadata: map[string]string{},
}
item.Key = key
item.Value = *resp.Value
item.Metadata["label"] = *resp.Label

items = append(items, item)
}

return &configuration.GetResponse{
Items: items,
}, nil
}

func (r *ConfigurationStore) Subscribe(ctx context.Context, req *configuration.SubscribeRequest, handler configuration.UpdateHandler) (string, error) {
return "", fmt.Errorf("Subscribe is not implemented by this configuration store")
}

func (r *ConfigurationStore) Unsubscribe(ctx context.Context, req *configuration.UnsubscribeRequest) error {
return fmt.Errorf("Unsubscribe is not implemented by this configuration store")
}
178 changes: 178 additions & 0 deletions configuration/azure/appconfig/appconfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
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 appconfig

import (
"fmt"
"reflect"
"testing"
"time"

"github.com/stretchr/testify/assert"

"github.com/dapr/components-contrib/configuration"
"github.com/dapr/kit/logger"
)

func TestNewAzureAppConfigurationStore(t *testing.T) {
type args struct {
logger logger.Logger
}
tests := []struct {
name string
args args
want configuration.Store
}{
{
args: args{
logger: logger.NewLogger("test"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewAzureAppConfigurationStore(tt.args.logger)
assert.NotNil(t, got)
})
}
}

func TestInit(t *testing.T) {
s := NewAzureAppConfigurationStore(logger.NewLogger("test"))
t.Run("Init with valid appConfigHost metadata", func(t *testing.T) {
testProperties := make(map[string]string)
testProperties[host] = "testHost"
testProperties[maxRetries] = "3"
testProperties[retryDelay] = "4000000000"
testProperties[maxRetryDelay] = "120000000000"

m := configuration.Metadata{
Properties: testProperties,
}

err := s.Init(m)
assert.Nil(t, err)
cs, ok := s.(*ConfigurationStore)
assert.True(t, ok)
assert.Equal(t, testProperties[host], cs.metadata.host)
assert.Equal(t, 3, cs.metadata.maxRetries)
assert.Equal(t, time.Second*4, cs.metadata.retryDelay)
assert.Equal(t, time.Second*120, cs.metadata.maxRetryDelay)
})

t.Run("Init with valid appConfigConnectionString metadata", func(t *testing.T) {
testProperties := make(map[string]string)
testProperties[connectionString] = "Endpoint=https://foo.azconfig.io;Id=osOX-l9-s0:sig;Secret=00000000000000000000000000000000000000000000"
testProperties[maxRetries] = "3"
testProperties[retryDelay] = "4000000000"
testProperties[maxRetryDelay] = "120000000000"

m := configuration.Metadata{
Properties: testProperties,
}

err := s.Init(m)
assert.Nil(t, err)
cs, ok := s.(*ConfigurationStore)
assert.True(t, ok)
assert.Equal(t, testProperties[connectionString], cs.metadata.connectionString)
assert.Equal(t, 3, cs.metadata.maxRetries)
assert.Equal(t, time.Second*4, cs.metadata.retryDelay)
assert.Equal(t, time.Second*120, cs.metadata.maxRetryDelay)
})
}

func Test_parseMetadata(t *testing.T) {
t.Run(fmt.Sprintf("parse metadata with %s", host), func(t *testing.T) {
testProperties := make(map[string]string)
testProperties[host] = "testHost"
testProperties[maxRetries] = "3"
testProperties[retryDelay] = "4000000000"
testProperties[maxRetryDelay] = "120000000000"

meta := configuration.Metadata{
Properties: testProperties,
}

want := metadata{
host: "testHost",
maxRetries: 3,
retryDelay: time.Second * 4,
maxRetryDelay: time.Second * 120,
}

m, _ := parseMetadata(meta)
assert.NotNil(t, m)
if !reflect.DeepEqual(m, want) {
t.Errorf("parseMetadata() got = %v, want %v", m, want)
}
})

t.Run(fmt.Sprintf("parse metadata with %s", connectionString), func(t *testing.T) {
testProperties := make(map[string]string)
testProperties[connectionString] = "testConnectionString"
testProperties[maxRetries] = "3"
testProperties[retryDelay] = "4000000000"
testProperties[maxRetryDelay] = "120000000000"

meta := configuration.Metadata{
Properties: testProperties,
}

want := metadata{
connectionString: "testConnectionString",
maxRetries: 3,
retryDelay: time.Second * 4,
maxRetryDelay: time.Second * 120,
}

m, _ := parseMetadata(meta)
assert.NotNil(t, m)
if !reflect.DeepEqual(m, want) {
t.Errorf("parseMetadata() got = %v, want %v", m, want)
}
})

t.Run(fmt.Sprintf("both %s and %s fields set in metadata", host, connectionString), func(t *testing.T) {
testProperties := make(map[string]string)
testProperties[host] = "testHost"
testProperties[connectionString] = "testConnectionString"
testProperties[maxRetries] = "3"
testProperties[retryDelay] = "4000000000"
testProperties[maxRetryDelay] = "120000000000"

meta := configuration.Metadata{
Properties: testProperties,
}

_, err := parseMetadata(meta)
assert.Error(t, err)
})

t.Run(fmt.Sprintf("both %s and %s fields not set in metadata", host, connectionString), func(t *testing.T) {
testProperties := make(map[string]string)
testProperties[host] = ""
testProperties[connectionString] = ""
testProperties[maxRetries] = "3"
testProperties[retryDelay] = "4000000000"
testProperties[maxRetryDelay] = "120000000000"

meta := configuration.Metadata{
Properties: testProperties,
}

_, err := parseMetadata(meta)
assert.Error(t, err)
})
}
24 changes: 24 additions & 0 deletions configuration/azure/appconfig/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
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 appconfig

import "time"

type metadata struct {
host string
connectionString string
maxRetries int
maxRetryDelay time.Duration
retryDelay time.Duration
}
Loading

0 comments on commit 1ccb4e4

Please sign in to comment.