diff --git a/examples/Go/ConsoleApp/README.md b/examples/Go/ConsoleApp/README.md new file mode 100644 index 00000000..0283fe2a --- /dev/null +++ b/examples/Go/ConsoleApp/README.md @@ -0,0 +1,67 @@ +# Azure App Configuration Console App Example + +This example demonstrates how to use the refresh functionality of Azure App Configuration in a console/command-line application. + +## Overview + +This console application: + +1. Loads configuration values from Azure App Configuration +2. Binds them to target configuration struct +3. Automatically refreshes the configuration when changed in Azure App Configuration + +## Running the Example + +### Prerequisites + +You need [an Azure subscription](https://azure.microsoft.com/free/) and the following Azure resources to run the examples: + +- [Azure App Configuration store](https://learn.microsoft.com/en-us/azure/azure-app-configuration/quickstart-azure-app-configuration-create?tabs=azure-portal) + +The examples retrieve credentials to access your App Configuration store from environment variables. + +### Add key-values + +Add the following key-values to the App Configuration store and leave **Label** and **Content Type** with their default values: + +| Key | Value | +|------------------------|----------------| +| *Config.Message* | *Hello World!* | +| *Config.Font.Color* | *blue* | +| *Config.Font.Size* | *12* | + +### Setup + +1. Initialize a new Go module. + + ```bash + go mod init console-example-refresh + ``` +1. Add the Azure App Configuration provider as a dependency. + + ```bash + go get github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration + ``` + +1. Set the connection string as an environment variable: + + ```bash + # Windows + set AZURE_APPCONFIG_CONNECTION_STRING=your-connection-string + + # Linux/macOS + export AZURE_APPCONFIG_CONNECTION_STRING=your-connection-string + ``` + +### Run the Application + +```bash +go run main.go +``` + +### Testing the Refresh Functionality + +1. Start the application +2. While it's running, modify the values in your Azure App Configuration store +3. Within 10 seconds (the configured refresh interval), the application should detect and apply the changes +4. You don't need to restart the application to see the updated values \ No newline at end of file diff --git a/examples/Go/ConsoleApp/main.go b/examples/Go/ConsoleApp/main.go new file mode 100644 index 00000000..35f99f47 --- /dev/null +++ b/examples/Go/ConsoleApp/main.go @@ -0,0 +1,133 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "syscall" + "time" + + "github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration" +) + +type Config struct { + Font Font + Message string +} + +type Font struct { + Color string + Size int +} + +func main() { + // Load configuration from Azure App Configuration + configProvider, err := loadAzureAppConfiguration() + if err != nil { + log.Fatalf("Error loading configuration: %s", err) + } + + // Parse initial configuration into struct + var config Config + err = configProvider.Unmarshal(&config, nil) + if err != nil { + log.Fatalf("Error unmarshalling configuration: %s", err) + } + + // Display the initial configuration + displayConfig(config) + + // Register refresh callback to update and display the configuration + configProvider.OnRefreshSuccess(func() { + fmt.Println("\n Configuration changed! Updating values...") + + // Re-unmarshal the configuration + var updatedConfig Config + err := configProvider.Unmarshal(&updatedConfig, nil) + if err != nil { + log.Printf("Error unmarshalling updated configuration: %s", err) + return + } + + // Update our working config + config = updatedConfig + + // Display the updated configuration + displayConfig(config) + }) + + // Setup a channel to listen for termination signals + done := make(chan os.Signal, 1) + signal.Notify(done, syscall.SIGINT, syscall.SIGTERM) + + fmt.Println("\nWaiting for configuration changes...") + fmt.Println("(Update values in Azure App Configuration to see refresh in action)") + fmt.Println("Press Ctrl+C to exit") + + // Start a ticker to periodically trigger refresh + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + // Keep the application running until terminated + for { + select { + case <-ticker.C: + // Trigger refresh in background + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := configProvider.Refresh(ctx); err != nil { + log.Printf("Error refreshing configuration: %s", err) + } + }() + case <-done: + fmt.Println("\nExiting...") + return + } + } +} + +// loadAzureAppConfiguration loads the configuration from Azure App Configuration +func loadAzureAppConfiguration() (*azureappconfiguration.AzureAppConfiguration, error) { + // Get connection string from environment variable + connectionString := os.Getenv("AZURE_APPCONFIG_CONNECTION_STRING") + + // Options setup + options := &azureappconfiguration.Options{ + Selectors: []azureappconfiguration.Selector{ + { + KeyFilter: "Config.*", + }, + }, + // Remove the prefix when mapping to struct fields + TrimKeyPrefixes: []string{"Config."}, + // Enable refresh every 10 seconds + RefreshOptions: azureappconfiguration.KeyValueRefreshOptions{ + Enabled: true, + Interval: 10 * time.Second, + }, + } + + authOptions := azureappconfiguration.AuthenticationOptions{ + ConnectionString: connectionString, + } + + // Create configuration provider with timeout + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + return azureappconfiguration.Load(ctx, authOptions, options) +} + +// displayConfig prints the current configuration values +func displayConfig(config Config) { + fmt.Println("\nCurrent Configuration Values:") + fmt.Println("--------------------") + fmt.Printf("Font Color: %s\n", config.Font.Color) + fmt.Printf("Font Size: %d\n", config.Font.Size) + fmt.Printf("Message: %s\n", config.Message) + fmt.Println("--------------------") +} diff --git a/examples/Go/WebApp/README.md b/examples/Go/WebApp/README.md new file mode 100644 index 00000000..de2ae79e --- /dev/null +++ b/examples/Go/WebApp/README.md @@ -0,0 +1,51 @@ +# Gin Feature Flags Web App + +This is a Gin web application using a feature flag in Azure App Configuration to dynamically control the availability of a new web page without restarting or redeploying it. + +## Prerequisites + +- An Azure account with an active subscription +- An Azure App Configuration store +- A feature flag named "Beta" in your App Configuration store +- Go 1.23 or later + +## Running the Example + +1. **Create a feature flag in Azure App Configuration:** + + Add a feature flag called *Beta* to the App Configuration store and leave **Label** and **Description** with their default values. For more information about how to add feature flags to a store using the Azure portal or the CLI, go to [Create a feature flag](https://learn.microsoft.com/azure/azure-app-configuration/manage-feature-flags?tabs=azure-portal#create-a-feature-flag). + +2. **Set environment variable:** + + **Windows PowerShell:** + ```powershell + $env:AZURE_APPCONFIG_CONNECTION_STRING = "your-connection-string" + ``` + + **Windows Command Prompt:** + ```cmd + setx AZURE_APPCONFIG_CONNECTION_STRING "your-connection-string" + ``` + + **Linux/macOS:** + ```bash + export AZURE_APPCONFIG_CONNECTION_STRING="your-connection-string" + ``` + +## Running the Application + +```bash +go run main.go +``` + +Open http://localhost:8080 in your browser. + +## Testing the Feature Flag + +1. **Start the application** - Beta menu item should be hidden (feature disabled) +2. **Go to Azure portal** → App Configuration → Feature manager +3. **Enable the "Beta" feature flag** +4. **Wait up to 30 seconds** and refresh the page +5. **Observe the Beta menu item** appears in navigation +6. **Click the Beta menu** to access the Beta page +7. **Disable the flag again** and test that `/beta` returns 404 \ No newline at end of file diff --git a/examples/Go/WebApp/main.go b/examples/Go/WebApp/main.go new file mode 100644 index 00000000..60adf125 --- /dev/null +++ b/examples/Go/WebApp/main.go @@ -0,0 +1,157 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + + "github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration" + "github.com/gin-gonic/gin" + "github.com/microsoft/Featuremanagement-Go/featuremanagement" + "github.com/microsoft/Featuremanagement-Go/featuremanagement/providers/azappconfig" +) + +type WebApp struct { + featureManager *featuremanagement.FeatureManager + appConfig *azureappconfiguration.AzureAppConfiguration +} + +func main() { + ctx := context.Background() + + // Load config from Azure App Configuration + appConfig, err := loadAzureAppConfiguration(ctx) + if err != nil { + log.Fatalf("Error loading Azure App Configuration: %v", err) + } + + // Create feature flag provider + featureFlagProvider, err := azappconfig.NewFeatureFlagProvider(appConfig) + if err != nil { + log.Fatalf("Error creating feature flag provider: %v", err) + } + + // Create feature manager + featureManager, err := featuremanagement.NewFeatureManager(featureFlagProvider, nil) + if err != nil { + log.Fatalf("Error creating feature manager: %v", err) + } + + // Create web app + app := &WebApp{ + featureManager: featureManager, + appConfig: appConfig, + } + + // Setup Gin with default middleware (Logger and Recovery) + r := gin.Default() + + // Setup routes + app.setupRoutes(r) + + // Start server + fmt.Println("Starting server on http://localhost:8080") + fmt.Println("Open http://localhost:8080 in your browser") + fmt.Println("Toggle the 'Beta' feature flag in Azure portal to see changes") + fmt.Println() + + if err := r.Run(":8080"); err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} + +func loadAzureAppConfiguration(ctx context.Context) (*azureappconfiguration.AzureAppConfiguration, error) { + connectionString := os.Getenv("AZURE_APPCONFIG_CONNECTION_STRING") + if connectionString == "" { + return nil, fmt.Errorf("AZURE_APPCONFIG_CONNECTION_STRING environment variable is not set") + } + + authOptions := azureappconfiguration.AuthenticationOptions{ + ConnectionString: connectionString, + } + + options := &azureappconfiguration.Options{ + FeatureFlagOptions: azureappconfiguration.FeatureFlagOptions{ + Enabled: true, + Selectors: []azureappconfiguration.Selector{ + { + KeyFilter: "*", + LabelFilter: "", + }, + }, + RefreshOptions: azureappconfiguration.RefreshOptions{ + Enabled: true, + }, + }, + } + + appConfig, err := azureappconfiguration.Load(ctx, authOptions, options) + if err != nil { + return nil, fmt.Errorf("failed to load configuration: %w", err) + } + + return appConfig, nil +} + +func (app *WebApp) featureMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // Refresh configuration to get latest feature flags + ctx := context.Background() + if err := app.appConfig.Refresh(ctx); err != nil { + log.Printf("Error refreshing configuration: %v", err) + } + + // Check if Beta feature is enabled + betaEnabled, err := app.featureManager.IsEnabled("Beta") + if err != nil { + log.Printf("Error checking Beta feature: %v", err) + betaEnabled = false + } + + // Store feature flag status for use in templates + c.Set("betaEnabled", betaEnabled) + c.Next() + } +} + +func (app *WebApp) setupRoutes(r *gin.Engine) { + // Apply feature middleware to all routes + r.Use(app.featureMiddleware()) + + // Load HTML templates + r.LoadHTMLGlob("templates/*.html") + + // Routes + r.GET("/", app.homeHandler) + r.GET("/beta", app.betaHandler) +} + +// Home page handler +func (app *WebApp) homeHandler(c *gin.Context) { + betaEnabled := c.GetBool("betaEnabled") + + c.HTML(http.StatusOK, "index.html", gin.H{ + "title": "Feature Management Demo", + "betaEnabled": betaEnabled, + }) +} + +// Beta page handler +func (app *WebApp) betaHandler(c *gin.Context) { + betaEnabled := c.GetBool("betaEnabled") + + // Feature gate logic - return 404 if feature is not enabled + if !betaEnabled { + c.HTML(http.StatusNotFound, "404.html", gin.H{ + "title": "Page Not Found", + "message": "The page you are looking for does not exist or is not available.", + }) + return + } + + c.HTML(http.StatusOK, "beta.html", gin.H{ + "title": "Beta Page", + }) +} diff --git a/examples/Go/WebApp/templates/404.html b/examples/Go/WebApp/templates/404.html new file mode 100644 index 00000000..681c4d40 --- /dev/null +++ b/examples/Go/WebApp/templates/404.html @@ -0,0 +1,37 @@ + + +
+ + +