Skip to content

Commit 0022f85

Browse files
committed
Adds example of using project/service config
1 parent 173b738 commit 0022f85

File tree

2 files changed

+317
-0
lines changed

2 files changed

+317
-0
lines changed
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package cmd
5+
6+
import (
7+
"context"
8+
"encoding/json"
9+
"fmt"
10+
11+
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
12+
"github.com/fatih/color"
13+
"github.com/spf13/cobra"
14+
"google.golang.org/protobuf/types/known/structpb"
15+
)
16+
17+
// MonitoringConfig represents the project-level monitoring configuration
18+
type MonitoringConfig struct {
19+
Enabled bool `json:"enabled"`
20+
Environment string `json:"environment"`
21+
RetentionDays int `json:"retentionDays"`
22+
AlertEmail string `json:"alertEmail"`
23+
}
24+
25+
// ServiceMonitoringConfig represents service-level monitoring configuration
26+
type ServiceMonitoringConfig struct {
27+
Enabled bool `json:"enabled"`
28+
HealthCheckPath string `json:"healthCheckPath"`
29+
MetricsPort int `json:"metricsPort"`
30+
LogLevel string `json:"logLevel"`
31+
AlertThresholds struct {
32+
ErrorRate float64 `json:"errorRate"`
33+
ResponseTimeMs int `json:"responseTimeMs"`
34+
CPUPercent int `json:"cpuPercent"`
35+
} `json:"alertThresholds"`
36+
Tags []string `json:"tags"`
37+
}
38+
39+
func newConfigCommand() *cobra.Command {
40+
return &cobra.Command{
41+
Use: "config",
42+
Short: "Setup monitoring configuration for the project and services",
43+
Long: `This command demonstrates the new configuration management capabilities by setting up
44+
a realistic monitoring configuration scenario. It will:
45+
46+
1. Check if project-level monitoring config exists, create it if missing
47+
2. Find the first service in the project
48+
3. Check if service-level monitoring config exists, create it if missing
49+
4. Display the final configuration state
50+
51+
This showcases how extensions can manage both project and service-level configuration
52+
using the new AdditionalProperties gRPC API with strongly-typed Go structs.`,
53+
RunE: func(cmd *cobra.Command, args []string) error {
54+
ctx := azdext.WithAccessToken(cmd.Context())
55+
56+
azdClient, err := azdext.NewAzdClient()
57+
if err != nil {
58+
return fmt.Errorf("failed to create azd client: %w", err)
59+
}
60+
defer azdClient.Close()
61+
62+
return setupMonitoringConfig(ctx, azdClient)
63+
},
64+
}
65+
}
66+
67+
func setupMonitoringConfig(ctx context.Context, azdClient *azdext.AzdClient) error {
68+
color.HiCyan("🔧 Setting up monitoring configuration...")
69+
fmt.Println()
70+
71+
// Step 1: Check and setup project-level monitoring config
72+
projectConfigCreated, err := setupProjectMonitoringConfig(ctx, azdClient)
73+
if err != nil {
74+
return err
75+
}
76+
77+
// Step 2: Get project to find services
78+
projectResp, err := azdClient.Project().Get(ctx, &azdext.EmptyRequest{})
79+
if err != nil {
80+
return fmt.Errorf("failed to get project: %w", err)
81+
}
82+
83+
if len(projectResp.Project.Services) == 0 {
84+
color.Yellow("⚠️ No services found in project - skipping service configuration")
85+
return displayConfigurationSummary(ctx, azdClient, "", projectConfigCreated, false)
86+
}
87+
88+
// Step 3: Setup monitoring for the first service
89+
var firstServiceName string
90+
for serviceName := range projectResp.Project.Services {
91+
firstServiceName = serviceName
92+
break
93+
}
94+
95+
color.HiWhite("📦 Found service: %s", firstServiceName)
96+
serviceConfigCreated, err := setupServiceMonitoringConfig(ctx, azdClient, firstServiceName)
97+
if err != nil {
98+
return err
99+
}
100+
101+
// Step 4: Display final configuration state
102+
return displayConfigurationSummary(ctx, azdClient, firstServiceName, projectConfigCreated, serviceConfigCreated)
103+
}
104+
105+
// Helper functions to convert between type-safe structs and protobuf structs
106+
func structToProtobuf(v interface{}) (*structpb.Struct, error) {
107+
// Convert struct to JSON bytes
108+
jsonBytes, err := json.Marshal(v)
109+
if err != nil {
110+
return nil, fmt.Errorf("failed to marshal struct to JSON: %w", err)
111+
}
112+
113+
// Convert JSON bytes to map
114+
var m map[string]interface{}
115+
if err := json.Unmarshal(jsonBytes, &m); err != nil {
116+
return nil, fmt.Errorf("failed to unmarshal JSON to map: %w", err)
117+
}
118+
119+
// Convert map to protobuf struct
120+
return structpb.NewStruct(m)
121+
}
122+
123+
func protobufToStruct(pbStruct *structpb.Struct, target interface{}) error {
124+
// Convert protobuf struct to JSON bytes
125+
jsonBytes, err := json.Marshal(pbStruct.AsMap())
126+
if err != nil {
127+
return fmt.Errorf("failed to marshal protobuf struct to JSON: %w", err)
128+
}
129+
130+
// Unmarshal JSON into target struct
131+
if err := json.Unmarshal(jsonBytes, target); err != nil {
132+
return fmt.Errorf("failed to unmarshal JSON to target struct: %w", err)
133+
}
134+
135+
return nil
136+
}
137+
138+
func setupProjectMonitoringConfig(ctx context.Context, azdClient *azdext.AzdClient) (bool, error) {
139+
color.HiWhite("🏢 Checking project-level monitoring configuration...")
140+
141+
// Check if monitoring config already exists
142+
configResp, err := azdClient.Project().GetConfigSection(ctx, &azdext.GetProjectConfigSectionRequest{
143+
Path: "monitoring",
144+
})
145+
if err != nil {
146+
return false, fmt.Errorf("failed to check project monitoring config: %w", err)
147+
}
148+
149+
if configResp.Found {
150+
color.Green(" ✓ Project monitoring configuration already exists")
151+
152+
// Demonstrate reading back the configuration into our type-safe struct
153+
var existingConfig MonitoringConfig
154+
if err := protobufToStruct(configResp.Section, &existingConfig); err != nil {
155+
return false, fmt.Errorf("failed to convert existing config: %w", err)
156+
}
157+
color.Cyan(" Current config: Environment=%s, Retention=%d days",
158+
existingConfig.Environment, existingConfig.RetentionDays)
159+
return false, nil // false means it already existed (not created)
160+
}
161+
162+
// Create default monitoring configuration using type-safe struct
163+
color.Yellow(" ⚙️ Creating project monitoring configuration...")
164+
165+
monitoringConfig := MonitoringConfig{
166+
Enabled: true,
167+
Environment: "development",
168+
RetentionDays: 30,
169+
AlertEmail: "[email protected]",
170+
}
171+
172+
// Convert type-safe struct to protobuf struct
173+
configStruct, err := structToProtobuf(monitoringConfig)
174+
if err != nil {
175+
return false, fmt.Errorf("failed to convert config struct: %w", err)
176+
}
177+
178+
_, err = azdClient.Project().SetConfigSection(ctx, &azdext.SetProjectConfigSectionRequest{
179+
Path: "monitoring",
180+
Section: configStruct,
181+
})
182+
if err != nil {
183+
return false, fmt.Errorf("failed to set project monitoring config: %w", err)
184+
}
185+
186+
color.Green(" ✓ Project monitoring configuration created successfully")
187+
return true, nil // true means it was created
188+
}
189+
190+
func setupServiceMonitoringConfig(ctx context.Context, azdClient *azdext.AzdClient, serviceName string) (bool, error) {
191+
color.HiWhite("🔍 Checking service-level monitoring configuration for '%s'...", serviceName)
192+
193+
// Check if service monitoring config already exists
194+
configResp, err := azdClient.Project().GetServiceConfigSection(ctx, &azdext.GetServiceConfigSectionRequest{
195+
ServiceName: serviceName,
196+
Path: "monitoring",
197+
})
198+
if err != nil {
199+
return false, fmt.Errorf("failed to check service monitoring config: %w", err)
200+
}
201+
202+
if configResp.Found {
203+
color.Green(" ✓ Service monitoring configuration already exists")
204+
205+
// Demonstrate reading back the configuration into our type-safe struct
206+
var existingConfig ServiceMonitoringConfig
207+
if err := protobufToStruct(configResp.Section, &existingConfig); err != nil {
208+
return false, fmt.Errorf("failed to convert existing service config: %w", err)
209+
}
210+
color.Cyan(" Current config: Port=%d, LogLevel=%s, Tags=%v",
211+
existingConfig.MetricsPort, existingConfig.LogLevel, existingConfig.Tags)
212+
return false, nil // false means it already existed (not created)
213+
}
214+
215+
// Create default service monitoring configuration using type-safe struct
216+
color.Yellow(" ⚙️ Creating service monitoring configuration...")
217+
218+
serviceConfig := ServiceMonitoringConfig{
219+
Enabled: true,
220+
HealthCheckPath: "/health",
221+
MetricsPort: 9090,
222+
LogLevel: "info",
223+
Tags: []string{"web", "api", "production"},
224+
}
225+
226+
// Set alert thresholds
227+
serviceConfig.AlertThresholds.ErrorRate = 5.0
228+
serviceConfig.AlertThresholds.ResponseTimeMs = 2000
229+
serviceConfig.AlertThresholds.CPUPercent = 80
230+
231+
// Convert type-safe struct to protobuf struct
232+
configStruct, err := structToProtobuf(serviceConfig)
233+
if err != nil {
234+
return false, fmt.Errorf("failed to create service config struct: %w", err)
235+
}
236+
237+
_, err = azdClient.Project().SetServiceConfigSection(ctx, &azdext.SetServiceConfigSectionRequest{
238+
ServiceName: serviceName,
239+
Path: "monitoring",
240+
Section: configStruct,
241+
})
242+
if err != nil {
243+
return false, fmt.Errorf("failed to set service monitoring config: %w", err)
244+
}
245+
246+
color.Green(" ✓ Service monitoring configuration created successfully")
247+
return true, nil // true means it was created
248+
}
249+
250+
func displayConfigurationSummary(ctx context.Context, azdClient *azdext.AzdClient, serviceName string, projectConfigCreated, serviceConfigCreated bool) error {
251+
fmt.Println()
252+
color.HiCyan("📊 Configuration Summary")
253+
color.HiCyan("========================")
254+
fmt.Println()
255+
256+
// Display project monitoring config with status
257+
projectStatus := "📋 Already existed"
258+
if projectConfigCreated {
259+
projectStatus = "✨ Newly created"
260+
}
261+
color.HiWhite("🏢 Project Monitoring Configuration (%s):", projectStatus)
262+
projectConfigResp, err := azdClient.Project().GetConfigSection(ctx, &azdext.GetProjectConfigSectionRequest{
263+
Path: "monitoring",
264+
})
265+
if err != nil {
266+
return fmt.Errorf("failed to get project monitoring config: %w", err)
267+
}
268+
269+
if projectConfigResp.Found {
270+
if err := printConfigSection(projectConfigResp.Section.AsMap()); err != nil {
271+
return err
272+
}
273+
}
274+
275+
fmt.Println()
276+
277+
// Display service monitoring config with status (only if we have a service)
278+
if serviceName != "" {
279+
serviceStatus := "📋 Already existed"
280+
if serviceConfigCreated {
281+
serviceStatus = "✨ Newly created"
282+
}
283+
color.HiWhite("📦 Service '%s' Monitoring Configuration (%s):", serviceName, serviceStatus)
284+
serviceConfigResp, err := azdClient.Project().GetServiceConfigSection(ctx, &azdext.GetServiceConfigSectionRequest{
285+
ServiceName: serviceName,
286+
Path: "monitoring",
287+
})
288+
if err != nil {
289+
return fmt.Errorf("failed to get service monitoring config: %w", err)
290+
}
291+
292+
if serviceConfigResp.Found {
293+
if err := printConfigSection(serviceConfigResp.Section.AsMap()); err != nil {
294+
return err
295+
}
296+
}
297+
fmt.Println()
298+
}
299+
300+
color.HiGreen("✅ Monitoring configuration setup complete!")
301+
fmt.Println()
302+
color.HiBlue("💡 This demonstrates how extensions can manage both project and service-level")
303+
color.HiBlue(" configuration using the new AdditionalProperties gRPC API with type-safe")
304+
color.HiBlue(" Go structs for complex configuration scenarios.")
305+
306+
return nil
307+
}
308+
309+
func printConfigSection(section map[string]interface{}) error {
310+
jsonBytes, err := json.MarshalIndent(section, " ", " ")
311+
if err != nil {
312+
return fmt.Errorf("failed to format section: %w", err)
313+
}
314+
fmt.Printf(" %s\n", string(jsonBytes))
315+
return nil
316+
}

cli/azd/extensions/microsoft.azd.demo/internal/cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func NewRootCommand() *cobra.Command {
2727
rootCmd.AddCommand(newColorsCommand())
2828
rootCmd.AddCommand(newVersionCommand())
2929
rootCmd.AddCommand(newMcpCommand())
30+
rootCmd.AddCommand(newConfigCommand())
3031

3132
return rootCmd
3233
}

0 commit comments

Comments
 (0)