Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 2 additions & 0 deletions eng/tools/azure-sdk-tools/azpysdk/samples.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
# Add your library + sample file if you do not want a particular sample to be run
IGNORED_SAMPLES = {
"azure-appconfiguration-provider": [
"async_connection_string_sample.py",
"connection_string_sample.py",
"key_vault_reference_customized_clients_sample.py",
"aad_sample.py",
"key_vault_reference_sample.py",
Expand Down
52 changes: 23 additions & 29 deletions sdk/appconfiguration/azure-appconfiguration-provider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,53 @@ Using the provider enables loading sets of configurations from an Azure App Conf

## Getting started

### Get credentials

Use the [Azure CLI][azure_cli] snippet below to get the connection string from the Configuration Store.

```Powershell
az appconfig credential list --name <config-store-name>
```

Alternatively, get the connection string from the Azure Portal.

### Creating a provider

You can create a client with a connection string:
#### Microsoft Entra ID (recommended)

<!-- SNIPPET:connection_string_sample.create_provider_connection_string -->
Microsoft Entra ID authentication is recommended for connecting to Azure App Configuration.

<!-- SNIPPET:entra_id_sample.create_provider_entra_id -->

```python
import os
from azure.appconfiguration.provider import load
from azure.identity import DefaultAzureCredential

connection_string = os.environ["APPCONFIGURATION_CONNECTION_STRING"]
endpoint = os.environ["APPCONFIGURATION_ENDPOINT_STRING"]
credential = DefaultAzureCredential()

# Connecting to Azure App Configuration using connection string
config = load(connection_string=connection_string, **kwargs)
# Connecting to Azure App Configuration using Entra ID
config = load(endpoint=endpoint, credential=credential, **kwargs)
```

<!-- END SNIPPET -->

or with Entra ID:
#### Connection string

<!-- SNIPPET:entra_id_sample.create_provider_entra_id -->
Use the [Azure CLI][azure_cli] snippet below to get the connection string from the Configuration Store:

```Powershell
az appconfig credential list --name <config-store-name>
```

You can also get the connection string from the Azure portal.

<!-- SNIPPET:connection_string_sample.create_provider_connection_string -->

```python
import os
from azure.appconfiguration.provider import load
from azure.identity import DefaultAzureCredential

endpoint = os.environ["APPCONFIGURATION_ENDPOINT_STRING"]
credential = DefaultAzureCredential()
connection_string = os.environ["APPCONFIGURATION_CONNECTION_STRING"]

# Connecting to Azure App Configuration using Entra ID
config = load(endpoint=endpoint, credential=credential, **kwargs)
# Connecting to Azure App Configuration using connection string
config = load(connection_string=connection_string, **kwargs)
```

<!-- END SNIPPET -->

these providers will by default load all configurations with `(No Label)` from your configuration store into a dictionary of key/values.
These providers will by default load all configurations with `(No Label)` from your configuration store into a dictionary of key/values.

### Features

Expand Down Expand Up @@ -155,11 +155,8 @@ The provider can be configured to refresh configurations from the store on a set
<!-- SNIPPET:refresh_sample.refresh_provider -->

```python
import os
from azure.appconfiguration.provider import load, WatchKey

connection_string = os.environ["APPCONFIGURATION_CONNECTION_STRING"]

config = load(
endpoint=endpoint,
credential=credential,
Expand Down Expand Up @@ -358,11 +355,8 @@ To enable refresh for feature flags you need to enable refresh. This will allow
<!-- SNIPPET:refresh_sample_feature_flags.refresh_feature_flags -->

```python
import os
from azure.appconfiguration.provider import load, WatchKey

connection_string = os.environ["APPCONFIGURATION_CONNECTION_STRING"]

config = load(
endpoint=endpoint,
credential=credential,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ def my_callback_on_fail(_):
import os
from azure.appconfiguration.provider import load, WatchKey

connection_string = os.environ["APPCONFIGURATION_CONNECTION_STRING"]

config = load(
endpoint=endpoint,
credential=credential,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ def my_callback_on_fail(_):
import os
from azure.appconfiguration.provider import load, WatchKey

connection_string = os.environ["APPCONFIGURATION_CONNECTION_STRING"]

config = load(
endpoint=endpoint,
credential=credential,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import logging
import os
import time

from devtools_testutils import (
add_general_regex_sanitizer,
add_general_string_sanitizer,
Expand All @@ -7,22 +10,71 @@
remove_batch_sanitizers,
add_remove_header_sanitizer,
add_uri_string_sanitizer,
get_credential,
is_live,
)
import pytest
from azure.appconfiguration import AzureAppConfigurationClient
from azure.identity import DefaultAzureCredential
from azure.core.exceptions import HttpResponseError
from testcase import setup_configs, cleanup_test_resources


_LOGGER = logging.getLogger(__name__)
_RBAC_PROPAGATION_TIMEOUT = 15 * 60 + 5
_MAX_RETRY_DELAY = 30

# autouse=True will trigger this fixture on each pytest run, even if it's not explicitly used by a test method

# Module-level storage for snapshot names created during session setup
snapshot_names = {}


def _wait_for_rbac_propagation(client, timeout=_RBAC_PROPAGATION_TIMEOUT):
deadline = time.monotonic() + timeout
retry_delay = 1

while True:
try:
next(client.list_configuration_settings(key_filter="__rbac_readiness_probe__"), None)
return
except HttpResponseError as error:
if error.status_code != 403:
raise

remaining_time = deadline - time.monotonic()
if remaining_time <= 0:
raise TimeoutError("App Configuration data-plane role assignment did not propagate in time.") from error

sleep_time = min(retry_delay, remaining_time)
_LOGGER.info(
"Waiting %.0f seconds for the App Configuration data-plane role assignment to propagate.",
sleep_time,
)
time.sleep(sleep_time)
retry_delay = min(retry_delay * 2, _MAX_RETRY_DELAY)


@pytest.fixture(scope="session", autouse=True)
def wait_for_data_plane_access():
if not is_live():
return

endpoint = os.environ.get("APPCONFIGURATION_ENDPOINT_STRING")
if not endpoint:
pytest.fail("APPCONFIGURATION_ENDPOINT_STRING must be set when running live tests.")

client = AzureAppConfigurationClient(endpoint, get_credential())
try:
_wait_for_rbac_propagation(client)
finally:
client.close()


@pytest.fixture(scope="session", autouse=True)
def setup_app_config_keys():
def setup_app_config_keys(wait_for_data_plane_access):
"""Pre-populate App Configuration with test keys and snapshots once per session (live mode only)."""
del wait_for_data_plane_access

if not is_live():
yield
return
Expand All @@ -32,7 +84,7 @@ def setup_app_config_keys():
yield
return

credential = DefaultAzureCredential()
credential = get_credential()
client = AzureAppConfigurationClient(endpoint, credential)
keyvault_secret_url = os.environ.get("APPCONFIGURATION_KEY_VAULT_REFERENCE")
keyvault_secret_url2 = os.environ.get("APPCONFIGURATION_KEY_VAULT_REFERENCE2")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------
from unittest.mock import MagicMock, patch
Comment thread
mrm9084 marked this conversation as resolved.

import pytest
from azure.core.exceptions import HttpResponseError

from conftest import _wait_for_data_plane_access
Comment thread
mrm9084 marked this conversation as resolved.
Outdated


def _http_error(status_code):
response = MagicMock()
response.status_code = status_code
response.reason = "Forbidden"
return HttpResponseError(response=response)


def test_wait_for_data_plane_access_succeeds_immediately():
client = MagicMock()
client.list_configuration_settings.return_value = iter([])

with patch("conftest.time.sleep") as sleep:
_wait_for_data_plane_access(client)

sleep.assert_not_called()


def test_wait_for_data_plane_access_retries_forbidden_response():
client = MagicMock()
client.list_configuration_settings.side_effect = [_http_error(403), iter([])]

with patch("conftest.time.sleep") as sleep:
_wait_for_data_plane_access(client)

sleep.assert_called_once_with(1)


def test_wait_for_data_plane_access_does_not_retry_other_errors():
client = MagicMock()
client.list_configuration_settings.side_effect = _http_error(500)

with patch("conftest.time.sleep") as sleep, pytest.raises(HttpResponseError):
_wait_for_data_plane_access(client)

sleep.assert_not_called()


def test_wait_for_data_plane_access_times_out():
client = MagicMock()
client.list_configuration_settings.side_effect = _http_error(403)

with patch("conftest.time.monotonic", side_effect=[0, 2]), patch(
"conftest.time.sleep"
) as sleep, pytest.raises(TimeoutError):
_wait_for_data_plane_access(client, timeout=1)

sleep.assert_not_called()
60 changes: 25 additions & 35 deletions sdk/appconfiguration/azure-appconfiguration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,40 +38,9 @@ az appconfig create --name <config-store-name> --resource-group <resource-group-
### Authenticate the client

In order to interact with the App Configuration service, you'll need to create an instance of the
[AzureAppConfigurationClient][configuration_client_class] class. To make this possible,
you can either use the connection string of the Configuration Store or use an AAD token.
[AzureAppConfigurationClient][configuration_client_class] class.

#### Use connection string

##### Get credentials

Use the [Azure CLI][azure_cli] snippet below to get the connection string from the Configuration Store.

```Powershell
az appconfig credential list --name <config-store-name>
```

Alternatively, get the connection string from the Azure Portal.

##### Create client

Once you have the value of the connection string, you can create the AzureAppConfigurationClient:

<!-- SNIPPET:hello_world_sample.create_app_config_client -->

```python
import os
from azure.appconfiguration import AzureAppConfigurationClient

CONNECTION_STRING = os.environ["APPCONFIGURATION_CONNECTION_STRING"]

# Create app config client
client = AzureAppConfigurationClient.from_connection_string(CONNECTION_STRING)
```

<!-- END SNIPPET -->

#### Use Entra ID token
#### Use Microsoft Entra ID (recommended)

Here we demonstrate using [DefaultAzureCredential][default_cred_ref]
to authenticate as a service principal. However, [AzureAppConfigurationClient][configuration_client_class]
Expand Down Expand Up @@ -141,6 +110,26 @@ credential = DefaultAzureCredential()
client = AzureAppConfigurationClient(base_url="your_endpoint_url", credential=credential)
```

#### Use a connection string

Use the [Azure CLI][azure_cli] snippet below to get the connection string from the Configuration Store:

```Powershell
az appconfig credential list --name <config-store-name>
```

You can also get the connection string from the Azure portal.

```python
import os
from azure.appconfiguration import AzureAppConfigurationClient

connection_string = os.environ["APPCONFIGURATION_CONNECTION_STRING"]

# Create an App Configuration client
client = AzureAppConfigurationClient.from_connection_string(connection_string)
```

## Key concepts

### Configuration Setting
Expand Down Expand Up @@ -389,11 +378,12 @@ To use the async client library, import the AzureAppConfigurationClient from pac
```python
import os
from azure.appconfiguration.aio import AzureAppConfigurationClient
from azure.identity.aio import DefaultAzureCredential

CONNECTION_STRING = os.environ["APPCONFIGURATION_CONNECTION_STRING"]
endpoint = os.environ["APPCONFIGURATION_ENDPOINT_STRING"]

# Create an app config client
client = AzureAppConfigurationClient.from_connection_string(CONNECTION_STRING)
client = AzureAppConfigurationClient(endpoint, DefaultAzureCredential())
```

<!-- END SNIPPET -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,21 @@
USAGE: python conditional_operation_sample.py

Set the environment variables with your own values before running the sample:
1) APPCONFIGURATION_CONNECTION_STRING: Connection String used to access the Azure App Configuration.
1) APPCONFIGURATION_ENDPOINT_STRING: Endpoint URL used to access the Azure App Configuration.
"""

import os
from azure.core import MatchConditions
from azure.core.exceptions import ResourceModifiedError
from azure.appconfiguration import AzureAppConfigurationClient, ConfigurationSetting
from azure.identity import DefaultAzureCredential


def main():
CONNECTION_STRING = os.environ["APPCONFIGURATION_CONNECTION_STRING"]
endpoint = os.environ["APPCONFIGURATION_ENDPOINT_STRING"]

# Create an app config client
client = AzureAppConfigurationClient.from_connection_string(CONNECTION_STRING)
client = AzureAppConfigurationClient(endpoint, DefaultAzureCredential())

# Unconditional set
config_setting = ConfigurationSetting(
Expand Down
Loading
Loading