Skip to content

[DISCUSS] feat/template versioning #5508

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,6 @@ test-results/
*.terraform*

# Finder state files
.DS_Store
.DS_Store

/temp/*
2 changes: 2 additions & 0 deletions cli/azd/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/azure/azure-dev/cli/azd/internal/cmd"
"github.com/azure/azure-dev/cli/azd/internal/cmd/add"
"github.com/azure/azure-dev/cli/azd/internal/cmd/show"
internalMiddleware "github.com/azure/azure-dev/cli/azd/internal/middleware"
"github.com/azure/azure-dev/cli/azd/internal/telemetry"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -361,6 +362,7 @@ func NewRootCmd(
root.
UseMiddleware("debug", middleware.NewDebugMiddleware).
UseMiddleware("ux", middleware.NewUxMiddleware).
UseMiddleware("template-version", internalMiddleware.NewTemplateVersionMiddleware).
UseMiddlewareWhen("telemetry", middleware.NewTelemetryMiddleware, func(descriptor *actions.ActionDescriptor) bool {
return !descriptor.Options.DisableTelemetry
}).
Expand Down
12 changes: 12 additions & 0 deletions cli/azd/cmd/template_version_registration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

// Intentionally empty as we're just adding comments

// Registers the template version manager with the IOC container
func init() {
// The template version services are registered automatically by the
// RegisterServices function in the templateversion package
}
191 changes: 191 additions & 0 deletions cli/azd/internal/middleware/template_version_middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package middleware

import (
"context"
"fmt"
"path/filepath"

"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/cmd/middleware"
"github.com/azure/azure-dev/cli/azd/internal/tracing"
"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
"github.com/azure/azure-dev/cli/azd/pkg/exec"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/azure/azure-dev/cli/azd/pkg/templateversion"
)

// NewTemplateVersionMiddleware creates middleware that ensures the template version file exists
// and updates azure.yaml with the version information
func NewTemplateVersionMiddleware(container *ioc.NestedContainer) middleware.Middleware {
return &templateVersionMiddleware{
container: container,
}
}

type templateVersionMiddleware struct {
container *ioc.NestedContainer
}

// Run implements middleware.Middleware
func (m *templateVersionMiddleware) Run(ctx context.Context, next middleware.NextFn) (*actions.ActionResult, error) {
ctx, span := tracing.Start(ctx, "template-version-middleware")
defer span.End()

// Get a console for debug messages
var console input.Console
err := m.container.Resolve(&console)
if err != nil {
// No console available, just continue with the command
span.SetStatus(1, "Failed to get console for debug messages")
}

// For now, we'll enable template versioning for all commands
// since we don't have a way to determine the specific command being run
if console != nil {
console.Message(ctx, "DEBUG: Checking template version")
}

// Get the project path from the context
projectPath := ""

// Try to create a new AzdContext to get the project path
azdContext, err := azdcontext.NewAzdContext()
if err != nil {
// No AZD context available, continue with the command
if console != nil {
console.Message(ctx, fmt.Sprintf("DEBUG: Failed to create AZD context: %v", err))
}
span.SetStatus(1, "Failed to create AZD context")
return next(ctx)
}

projectPath = azdContext.ProjectDirectory()
if console != nil {
console.Message(ctx, fmt.Sprintf("DEBUG: Got project directory: %s", projectPath))
}

if projectPath == "" {
// No project path available, continue with the command
if console != nil {
console.Message(ctx, "DEBUG: Project path is empty, skipping template version check")
}
span.SetStatus(1, "No project path found")
return next(ctx)
}

// Get template version manager
var runner exec.CommandRunner
if err = m.container.Resolve(&runner); err != nil {
// Runner not available, continue with the command
if console != nil {
console.Message(ctx, fmt.Sprintf("DEBUG: Failed to resolve command runner: %v", err))
}
span.SetStatus(1, "Command runner not available")
return next(ctx)
}

// Create version manager directly instead of using IoC container
versionManager := templateversion.NewManager(console, runner)

if console != nil {
console.Message(ctx, "DEBUG: Successfully created version manager directly")
}

// Ensure template version file exists - errors are handled within the EnsureTemplateVersion method
if console != nil {
console.Message(ctx, fmt.Sprintf("DEBUG: Calling EnsureTemplateVersion with projectPath: %s", projectPath))
}

version, err := versionManager.EnsureTemplateVersion(ctx, projectPath)

if console != nil {
console.Message(ctx, fmt.Sprintf("DEBUG: EnsureTemplateVersion returned version: %s, err: %v", version, err))
}

if err != nil {
// Log error but continue with the command
span.SetStatus(1, "Failed to ensure template version")
if console != nil {
console.Message(ctx, fmt.Sprintf("DEBUG: Failed to ensure template version: %v", err))
}
} else if version != "" {
// Only update azure.yaml if we successfully got a version
if console != nil {
console.Message(ctx, fmt.Sprintf("DEBUG: Updating azure.yaml with version: %s", version))
}

err = updateAzureYamlVersion(ctx, projectPath, version)
if err != nil {
// Log error but continue with the command
span.SetStatus(1, "Failed to update azure.yaml with template version")
if console != nil {
console.Message(ctx, fmt.Sprintf("DEBUG: Failed to update azure.yaml with template version: %v", err))
}
} else if console != nil {
console.Message(ctx, "DEBUG: Successfully updated azure.yaml with version")
}
}

// Always continue with the next middleware/command
return next(ctx)
}

// isTemplateCommand returns true if the command requires a template
func isTemplateCommand(commandName string) bool {
templateCommands := []string{
"init",
"up",
"deploy",
"provision",
"env", // Some env commands may need the template
"pipeline",
"monitor",
}

for _, cmd := range templateCommands {
if cmd == commandName {
return true
}
}

return false
}

// updateAzureYamlVersion updates the azure.yaml file with the template version
func updateAzureYamlVersion(ctx context.Context, projectPath string, version string) error {
ctx, span := tracing.Start(ctx, "update-azure-yaml-version")
defer span.End()

if projectPath == "" {
return nil
}

// Get the azure.yaml path
azureYamlPath := filepath.Join(projectPath, "azure.yaml")

// Load the project config
projectConfig, err := project.Load(ctx, azureYamlPath)
if err != nil {
return err
}

if projectConfig == nil {
return nil
}

// Check if version already matches
if projectConfig.TrackingId == version {
return nil
}

// Update the tracking ID
projectConfig.TrackingId = version

// Save the updated config
return project.Save(ctx, projectConfig, azureYamlPath)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package middleware

import (
"github.com/azure/azure-dev/cli/azd/cmd/middleware"
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
)

// RegisterTemplateVersionMiddleware registers the template version middleware with the CLI
func RegisterTemplateVersionMiddleware(runner *middleware.MiddlewareRunner) error {
// Register the middleware with a factory function that uses dependency injection
return runner.Use("template-version", func(container *ioc.NestedContainer) middleware.Middleware {
return NewTemplateVersionMiddleware(container)
})
}
58 changes: 58 additions & 0 deletions cli/azd/internal/middleware/template_version_middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package middleware

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

// Copy of the isTemplateCommand function from template_version_middleware.go for testing
func isTemplateCommandTest(commandName string) bool {
templateCommands := []string{
"init",
"up",
"deploy",
"provision",
"env", // Some env commands may need the template
"pipeline",
"monitor",
}

for _, cmd := range templateCommands {
if cmd == commandName {
return true
}
}

return false
}

// Test the isTemplateCommand function that's used by the middleware
func TestIsTemplateCommand(t *testing.T) {
testCases := []struct {
commandName string
expected bool
}{
{"init", true},
{"up", true},
{"deploy", true},
{"provision", true},
{"env", true},
{"pipeline", true},
{"monitor", true},
{"version", false},
{"login", false},
{"logout", false},
}

for _, tc := range testCases {
t.Run(fmt.Sprintf("Command: %s", tc.commandName), func(t *testing.T) {
result := isTemplateCommandTest(tc.commandName)
assert.Equal(t, tc.expected, result)
})
}
}
3 changes: 3 additions & 0 deletions cli/azd/pkg/project/project_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ type ProjectConfig struct {
Workflows workflow.WorkflowMap `yaml:"workflows,omitempty"`
Cloud *cloud.Config `yaml:"cloud,omitempty"`
Resources map[string]*ResourceConfig `yaml:"resources,omitempty"`
// TrackingId is the template version in format YYYY-MM-DD-<short-git-hash>
// This is used to track the version of the template being used
TrackingId string `yaml:"tracking_id,omitempty"`

*ext.EventDispatcher[ProjectLifecycleEventArgs] `yaml:"-"`
}
Expand Down
Loading
Loading