Skip to content

Commit 4d97401

Browse files
authored
Fix/verbose appconfig logging (#8)
* fix: enable verbose logging for Azure App Configuration - Add logx.Init(verbose) in CLI root initialization - Ensures Azure App Configuration debug logs are shown with --verbose flag - Fixes issue where appconfig.go debug logs were suppressed - No functional changes, only improves logging visibility * fix: use APP_CONFIG_LABEL environment variable for Azure App Config - Fix Azure App Configuration provider to read APP_CONFIG_LABEL env var - Previously was using environment name instead of actual label - This fixes the issue where service-specific keys weren't being fetched - Now properly fetches keys with the correct label (e.g., 'dev') * improve: better error handling and simplified label logic - Use --env flag directly as Azure App Config label (dev/staging/prod) - Remove dependency on APP_CONFIG_LABEL environment variable - Add user-friendly warning messages for malformed JSON in Azure App Config - Provide specific guidance on common JSON issues (missing commas, duplicate keys) - This makes the tool easier to use and provides better error messages * fix: update ACR validation test to work with Azure App Config changes - Add APP_CONFIG_SKIP=true to disable Azure App Configuration in test - Clean up environment variables properly in test - Fix linting issues (errcheck and line length) - Ensures test validates missing variables correctly without interference from global config * fix: make malformed JSON in Azure App Config fail deployment CRITICAL FIX: Malformed JSON in Azure App Configuration now fails deployment instead of just warning. - Change all JSON parsing errors from WARN to ERROR with proper error returns - Malformed JSON prevents NEXT_PUBLIC_* variables from being passed as build args - This ensures applications get required environment variables or deployment fails - Add fmt import and fix linting issues with error return value checks - Provides clear error messages about JSON issues (missing commas, duplicate keys) This prevents silent failures where apps deploy without required environment variables. * fix: add auto-detection of IMAGE_NAME and IMAGE_TAG to WebApp command - WebApp command now auto-detects IMAGE_NAME and IMAGE_TAG from CI environment like ACI and ACR commands - Uses same detectImageNameFromCI() and detectImageTagFromCI() functions - Ensures consistency across all deployment commands (ACR, ACI, WebApp) - Fixes issue where WebApp required manual IMAGE_NAME/IMAGE_TAG while ACI worked automatically - Now WebApp deployment works seamlessly in CI/CD pipelines without manual variable setup * fix: WebApp now sets environment variables from Azure App Configuration - WebApp deployment now calls setWebAppSettings() to configure app settings - Uses 'az webapp config appsettings set' to pass environment variables to container - Filters out internal azctl variables (ACR_*, RESOURCE_GROUP, etc.) - Only passes application-specific variables like NEXT_PUBLIC_* to container - Fixes issue where WebApp containers couldn't access Azure App Config variables - Now WebApp behavior matches ACI in terms of environment variable injection * fix: WebApp now correctly filters environment variables from Azure App Configuration - WebApp now only sets application-specific environment variables (like ACI does) - Added isApplicationVariable() function to filter variables by prefixes: * NEXT_PUBLIC_* (React/Next.js public variables) * SUPABASE_*, SOLANA_*, AZURE_OPENAI_*, OPENAI_*, LOGFLARE_*, FIREBASE_*, SAGEMAKER_* * PORT, NODE_ENV, ENVIRONMENT - Filters out infrastructure variables (PATH, USER, SHELL, etc.) that shouldn't go to containers - Now sets only 10 relevant application settings instead of 69+ system variables - Matches ACI behavior where only service-specific variables are passed to containers - Fixes issue where WebApp was trying to set system environment variables * fix: remove unnecessary quotes from WebApp environment variables - Modified escapeShellValue() to not wrap values in quotes - Azure CLI webapp config appsettings set handles values properly without quotes - Environment variables now set without literal quote characters - Fixes issue where URLs like 'https://example.com' had quotes in the actual value * fix: WebApp now uses proper ACR authentication like ACI - Fixed container image format: chiswarm/swarm-nextjs-fe -> chiswarm.azurecr.io/swarm-nextjs-fe - Added setWebAppRegistryCredentials() function to set Docker registry credentials - Maps ACR credentials to WebApp Docker registry settings: * DOCKER_REGISTRY_SERVER_URL=https://chiswarm.azurecr.io * DOCKER_REGISTRY_SERVER_USERNAME=chiswarm (from ACR_USERNAME) * DOCKER_REGISTRY_SERVER_PASSWORD=<ACR_PASSWORD> - Now WebApp authentication matches ACI authentication exactly - Fixes 'pull access denied' errors in WebApp deployment center logs * fix: clean up formatting in webapp.go - Remove trailing whitespace - Fix line ending formatting
1 parent 0527810 commit 4d97401

File tree

5 files changed

+250
-14
lines changed

5 files changed

+250
-14
lines changed

internal/cli/integration_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@ import (
1010
)
1111

1212
func TestACRCommandValidation(t *testing.T) {
13-
// Clean environment
13+
// Clean environment and disable Azure App Configuration
1414
defer func() {
15-
for _, v := range []string{"ACR_REGISTRY", "ACR_RESOURCE_GROUP", "IMAGE_NAME", "IMAGE_TAG"} {
15+
envVars := []string{"ACR_REGISTRY", "ACR_RESOURCE_GROUP", "IMAGE_NAME", "IMAGE_TAG", "APP_CONFIG_NAME", "APP_CONFIG"}
16+
for _, v := range envVars {
1617
//nolint:errcheck // os.Unsetenv rarely fails in test cleanup
1718
os.Unsetenv(v)
1819
}
1920
}()
2021

22+
// Disable Azure App Configuration for this test
23+
//nolint:errcheck // os.Setenv rarely fails in test setup
24+
os.Setenv("APP_CONFIG_SKIP", "true")
25+
2126
// Test missing variables
2227
err := Execute(context.Background(), []string{"acr"})
2328
if err == nil {

internal/cli/root.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/furiatona/azctl/internal/config"
88
"github.com/furiatona/azctl/internal/logging"
9+
"github.com/furiatona/azctl/internal/logx"
910

1011
"github.com/spf13/cobra"
1112
)
@@ -61,6 +62,9 @@ func Execute(ctx context.Context, args []string) error {
6162
return fmt.Errorf("failed to initialize logging: %w", err)
6263
}
6364

65+
// Initialize logx package with verbose flag for Azure App Configuration logging
66+
logx.Init(verbose)
67+
6468
envfile, _ := cmd.Flags().GetString("envfile")
6569
env, _ := cmd.Flags().GetString("env")
6670

internal/cli/webapp.go

Lines changed: 207 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,22 @@ func newWebAppCmd() *cobra.Command {
4343
return fmt.Errorf("environment required for webapp deployment (--env dev|staging|prod)")
4444
}
4545

46+
// Auto-detect IMAGE_NAME and IMAGE_TAG in CI if not set
47+
if isCIEnvironment() {
48+
if cfg.Get("IMAGE_NAME") == "" {
49+
if detectedImageName := detectImageNameFromCI(); detectedImageName != "" {
50+
cfg.Set("IMAGE_NAME", detectedImageName)
51+
logging.Debugf("Auto-detected IMAGE_NAME from CI: %s", detectedImageName)
52+
}
53+
}
54+
if cfg.Get("IMAGE_TAG") == "" {
55+
if detectedImageTag := detectImageTagFromCI(); detectedImageTag != "" {
56+
cfg.Set("IMAGE_TAG", detectedImageTag)
57+
logging.Debugf("Auto-detected IMAGE_TAG from CI: %s", detectedImageTag)
58+
}
59+
}
60+
}
61+
4662
// Apply flag overrides
4763
if resourceGroup == "" {
4864
resourceGroup = cfg.Get("RESOURCE_GROUP")
@@ -158,9 +174,10 @@ func updateWebApp(ctx context.Context, resourceGroup, webAppName string, cfg *co
158174
return fmt.Errorf("missing required variables: ACR_REGISTRY, IMAGE_NAME, IMAGE_TAG")
159175
}
160176

161-
fullImageName := fmt.Sprintf("%s/%s:%s", registry, imageName, imageTag)
162-
registryUrl := fmt.Sprintf("https://%s", registry)
177+
fullImageName := fmt.Sprintf("%s.azurecr.io/%s:%s", registry, imageName, imageTag)
178+
registryUrl := fmt.Sprintf("https://%s.azurecr.io", registry)
163179

180+
// Set container image
164181
args := []string{
165182
"webapp", "config", "container", "set",
166183
"--name", webAppName,
@@ -169,7 +186,194 @@ func updateWebApp(ctx context.Context, resourceGroup, webAppName string, cfg *co
169186
"--container-registry-url", registryUrl,
170187
}
171188
if err := runx.AZ(ctx, args...); err != nil {
172-
return fmt.Errorf("failed to update webapp: %w", err)
189+
return fmt.Errorf("failed to update webapp container: %w", err)
190+
}
191+
192+
// Set Docker registry credentials
193+
if err := setWebAppRegistryCredentials(ctx, resourceGroup, webAppName, cfg); err != nil {
194+
return fmt.Errorf("failed to set webapp registry credentials: %w", err)
195+
}
196+
197+
// Set application settings (environment variables) from config
198+
if err := setWebAppSettings(ctx, resourceGroup, webAppName, cfg); err != nil {
199+
return fmt.Errorf("failed to set webapp settings: %w", err)
200+
}
201+
202+
return nil
203+
}
204+
205+
// setWebAppSettings sets application settings (environment variables) for the WebApp
206+
func setWebAppSettings(ctx context.Context, resourceGroup, webAppName string, cfg *config.Config) error {
207+
// Collect only application-specific environment variables (like ACI does)
208+
allVars := cfg.GetAll()
209+
settings := make([]string, 0, len(allVars))
210+
for key, value := range allVars {
211+
// Skip internal azctl variables that shouldn't be passed to the container
212+
if isInternalVariable(key) {
213+
continue
214+
}
215+
216+
// Skip variables with very long values that might cause Azure CLI issues
217+
if len(value) > 4000 {
218+
logging.Debugf("Skipping variable '%s' - value too long (%d chars)", key, len(value))
219+
continue
220+
}
221+
222+
// Only include variables that are application-specific (similar to ACI environmentVariables)
223+
if !isApplicationVariable(key) {
224+
logging.Debugf("Skipping infrastructure variable '%s'", key)
225+
continue
226+
}
227+
228+
// Escape the value for shell safety (but don't add quotes)
229+
escapedValue := escapeShellValue(value)
230+
settings = append(settings, fmt.Sprintf("%s=%s", key, escapedValue))
231+
logging.Debugf("Including application setting: %s", key)
232+
}
233+
234+
if len(settings) == 0 {
235+
logging.Debugf("No application settings to configure for WebApp '%s'", webAppName)
236+
return nil
237+
}
238+
239+
// Set application settings using az CLI - do it in batches to avoid command line length limits
240+
const batchSize = 20
241+
for i := 0; i < len(settings); i += batchSize {
242+
end := i + batchSize
243+
if end > len(settings) {
244+
end = len(settings)
245+
}
246+
247+
batch := settings[i:end]
248+
args := []string{
249+
"webapp", "config", "appsettings", "set",
250+
"--name", webAppName,
251+
"--resource-group", resourceGroup,
252+
"--settings",
253+
}
254+
args = append(args, batch...)
255+
256+
logging.Debugf("Setting batch %d/%d (%d settings) for WebApp '%s'",
257+
(i/batchSize)+1, (len(settings)+batchSize-1)/batchSize, len(batch), webAppName)
258+
259+
if err := runx.AZ(ctx, args...); err != nil {
260+
return fmt.Errorf("failed to set application settings batch %d: %w", (i/batchSize)+1, err)
261+
}
262+
}
263+
264+
logging.Infof("✅ Set %d application settings for WebApp '%s'", len(settings), webAppName)
265+
return nil
266+
}
267+
268+
// setWebAppRegistryCredentials sets Docker registry credentials for the WebApp
269+
func setWebAppRegistryCredentials(ctx context.Context, resourceGroup, webAppName string, cfg *config.Config) error {
270+
acrRegistry := cfg.Get("ACR_REGISTRY")
271+
acrUsername := cfg.Get("ACR_USERNAME")
272+
acrPassword := cfg.Get("ACR_PASSWORD")
273+
274+
if acrRegistry == "" || acrUsername == "" || acrPassword == "" {
275+
return fmt.Errorf("missing required ACR credentials: ACR_REGISTRY, ACR_USERNAME, ACR_PASSWORD")
276+
}
277+
278+
// Set Docker registry server URL (should include .azurecr.io suffix)
279+
registryUrl := fmt.Sprintf("https://%s.azurecr.io", acrRegistry)
280+
registrySettings := []string{
281+
fmt.Sprintf("DOCKER_REGISTRY_SERVER_URL=%s", registryUrl),
282+
fmt.Sprintf("DOCKER_REGISTRY_SERVER_USERNAME=%s", acrUsername),
283+
fmt.Sprintf("DOCKER_REGISTRY_SERVER_PASSWORD=%s", acrPassword),
284+
}
285+
286+
// Set Docker registry credentials using az CLI
287+
args := []string{
288+
"webapp", "config", "appsettings", "set",
289+
"--name", webAppName,
290+
"--resource-group", resourceGroup,
291+
"--settings",
292+
}
293+
args = append(args, registrySettings...)
294+
295+
logging.Debugf("Setting Docker registry credentials for WebApp '%s': URL=%s, Username=%s",
296+
webAppName, registryUrl, acrUsername)
297+
298+
if err := runx.AZ(ctx, args...); err != nil {
299+
return fmt.Errorf("failed to set Docker registry credentials: %w", err)
173300
}
301+
302+
logging.Infof("✅ Set Docker registry credentials for WebApp '%s'", webAppName)
174303
return nil
175304
}
305+
306+
// escapeShellValue escapes a value for safe use in shell commands
307+
func escapeShellValue(value string) string {
308+
// Replace quotes with escaped quotes and handle special characters
309+
// Don't wrap in quotes as Azure CLI handles the values properly
310+
escaped := strings.ReplaceAll(value, `"`, `\"`)
311+
return escaped
312+
}
313+
314+
// isInternalVariable checks if a variable is internal to azctl and shouldn't be passed to containers
315+
func isInternalVariable(key string) bool {
316+
internalVars := []string{
317+
"ACR_REGISTRY",
318+
"ACR_RESOURCE_GROUP",
319+
"ACR_USERNAME",
320+
"ACR_PASSWORD",
321+
"RESOURCE_GROUP",
322+
"IMAGE_NAME",
323+
"IMAGE_TAG",
324+
"WEBAPP_NAME",
325+
"APP_SERVICE_PLAN",
326+
"LOG_STORAGE_ACCOUNT",
327+
"LOG_STORAGE_KEY",
328+
"LOG_STORAGE_NAME",
329+
"FLUENTBIT_CONFIG",
330+
"APP_CONFIG_NAME",
331+
"APP_CONFIG_LABEL",
332+
"APP_CONFIG_SKIP",
333+
}
334+
335+
for _, internal := range internalVars {
336+
if key == internal {
337+
return true
338+
}
339+
}
340+
return false
341+
}
342+
343+
// isApplicationVariable checks if a variable should be passed to the application container
344+
// This matches the environmentVariables in the ACI template
345+
func isApplicationVariable(key string) bool {
346+
// Application-specific prefixes and variables (like in ACI environmentVariables)
347+
applicationPrefixes := []string{
348+
"NEXT_PUBLIC_",
349+
"SUPABASE_",
350+
"SOLANA_",
351+
"AZURE_OPENAI_",
352+
"OPENAI_",
353+
"LOGFLARE_",
354+
"FIREBASE_",
355+
"SAGEMAKER_",
356+
}
357+
358+
// Check prefixes
359+
for _, prefix := range applicationPrefixes {
360+
if strings.HasPrefix(key, prefix) {
361+
return true
362+
}
363+
}
364+
365+
// Specific application variables (not prefixed)
366+
applicationVars := []string{
367+
"PORT",
368+
"NODE_ENV",
369+
"ENVIRONMENT",
370+
}
371+
372+
for _, appVar := range applicationVars {
373+
if key == appVar {
374+
return true
375+
}
376+
}
377+
378+
return false
379+
}

internal/config/appconfig.go

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,11 @@ func fetchAzureAppConfigWithImage(ctx context.Context, name, label, imageName st
5454
}
5555
}
5656
} else {
57-
logx.Infof("[DEBUG] Failed to parse global-configurations JSON: %v", err)
58-
logx.Infof("[DEBUG] Raw JSON value: %s", globalKV.Value)
57+
//nolint:errcheck // Error logging for debugging
58+
logx.Errorf("[ERROR] Failed to parse global-configurations JSON: %v", err)
59+
//nolint:errcheck // Error logging for debugging
60+
logx.Errorf("[ERROR] Raw JSON value: %s", globalKV.Value)
61+
return nil, fmt.Errorf("malformed JSON in Azure App Configuration global-configurations key: %w", err)
5962
}
6063
}
6164
} else {
@@ -83,8 +86,11 @@ func fetchAzureAppConfigWithImage(ctx context.Context, name, label, imageName st
8386
}
8487
}
8588
} else {
86-
logx.Infof("[DEBUG] Failed to parse global-configurations JSON (no label): %v", err)
87-
logx.Infof("[DEBUG] Raw JSON value (no label): %s", globalKVNoLabel.Value)
89+
//nolint:errcheck // Error logging for debugging
90+
logx.Errorf("[ERROR] Failed to parse global-configurations JSON (no label): %v", err)
91+
//nolint:errcheck // Error logging for debugging
92+
logx.Errorf("[ERROR] Raw JSON value (no label): %s", globalKVNoLabel.Value)
93+
return nil, fmt.Errorf("malformed JSON in Azure App Configuration global-configurations key (no label): %w", err)
8894
}
8995
}
9096
} else {
@@ -118,8 +124,15 @@ func fetchAzureAppConfigWithImage(ctx context.Context, name, label, imageName st
118124
}
119125
}
120126
} else {
121-
logx.Infof("[DEBUG] Failed to parse service-specific JSON: %v", err)
122-
logx.Infof("[DEBUG] Raw service JSON value: %s", serviceKV.Value)
127+
//nolint:errcheck // Error logging for debugging
128+
logx.Errorf("[ERROR] Failed to parse service-specific JSON for key '%s': %v", imageName, err)
129+
//nolint:errcheck // Error logging for debugging
130+
logx.Errorf("[ERROR] Please check your Azure App Configuration JSON format for key '%s'", imageName)
131+
//nolint:errcheck // Error logging for debugging
132+
logx.Errorf("[ERROR] Common issues: missing commas, duplicate keys, or invalid JSON syntax")
133+
//nolint:errcheck // Error logging for debugging
134+
logx.Errorf("[ERROR] Raw service JSON value: %s", serviceKV.Value)
135+
return nil, fmt.Errorf("malformed JSON in Azure App Configuration key '%s': %w", imageName, err)
123136
}
124137
}
125138
} else {
@@ -146,8 +159,15 @@ func fetchAzureAppConfigWithImage(ctx context.Context, name, label, imageName st
146159
}
147160
}
148161
} else {
149-
logx.Infof("[DEBUG] Failed to parse service-specific JSON (no label): %v", err)
150-
logx.Infof("[DEBUG] Raw service JSON value (no label): %s", serviceKVNoLabel.Value)
162+
//nolint:errcheck // Error logging for debugging
163+
logx.Errorf("[ERROR] Failed to parse service-specific JSON (no label) for key '%s': %v", imageName, err)
164+
//nolint:errcheck // Error logging for debugging
165+
logx.Errorf("[ERROR] Please check your Azure App Configuration JSON format for key '%s'", imageName)
166+
//nolint:errcheck // Error logging for debugging
167+
logx.Errorf("[ERROR] Common issues: missing commas, duplicate keys, or invalid JSON syntax")
168+
//nolint:errcheck // Error logging for debugging
169+
logx.Errorf("[ERROR] Raw service JSON value (no label): %s", serviceKVNoLabel.Value)
170+
return nil, fmt.Errorf("malformed JSON in Azure App Configuration key '%s': %w", imageName, err)
151171
}
152172
}
153173
} else {

internal/config/config.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,10 @@ func (p *AzureAppConfigProvider) Load(ctx context.Context) (map[string]string, e
226226
// Determine service name for Azure App Config
227227
serviceName := p.determineServiceName()
228228

229-
return fetchAzureAppConfigWithImage(ctx, name, p.env, serviceName)
229+
// Use environment name as label (dev, staging, prod)
230+
label := p.env
231+
232+
return fetchAzureAppConfigWithImage(ctx, name, label, serviceName)
230233
}
231234

232235
// determineServiceName determines the service name for Azure App Config

0 commit comments

Comments
 (0)