diff --git a/.gitignore b/.gitignore index 5969cccbacc..6e5477df1b6 100644 --- a/.gitignore +++ b/.gitignore @@ -74,5 +74,7 @@ cli/azd/extensions/microsoft.azd.concurx/microsoft.azd.concurx.exe cli/azd/azd-test cli/azd/azd cli/azd/extensions/microsoft.azd.concurx/concurx -cli/azd/extensions/microsoft.azd.concurx/concurx cli/azd/extensions/microsoft.azd.concurx/concurx.exe +cli/azd/extensions/azure.appservice/azure.appservice +cli/azd/extensions/azure.appservice/azure.appservice.exe + diff --git a/cli/azd/extensions/azure.appservice/.gitignore b/cli/azd/extensions/azure.appservice/.gitignore new file mode 100644 index 00000000000..6bffccdeda2 --- /dev/null +++ b/cli/azd/extensions/azure.appservice/.gitignore @@ -0,0 +1,12 @@ +# Build output +bin/ +*.exe + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Go +*.test diff --git a/cli/azd/extensions/azure.appservice/CHANGELOG.md b/cli/azd/extensions/azure.appservice/CHANGELOG.md new file mode 100644 index 00000000000..de2c8206a1c --- /dev/null +++ b/cli/azd/extensions/azure.appservice/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 + +- Initial release with `swap` command for swapping App Service deployment slots. diff --git a/cli/azd/extensions/azure.appservice/README.md b/cli/azd/extensions/azure.appservice/README.md new file mode 100644 index 00000000000..e1dfb59200b --- /dev/null +++ b/cli/azd/extensions/azure.appservice/README.md @@ -0,0 +1,32 @@ +# Azure App Service Extension + +This extension provides commands for managing Azure App Service resources. + +## Commands + +### swap + +Swap deployment slots for an Azure App Service. + +```bash +azd appservice swap --service --src --dst +``` + +**Flags:** + +- `--service` - The name of the service to swap slots for (optional if there's only one App Service) +- `--src` - The source slot name. Use `@main` for production +- `--dst` - The destination slot name. Use `@main` for production + +**Examples:** + +```bash +# Swap the staging slot to production +azd appservice swap --service myapp --src staging --dst @main + +# Interactive mode - prompts for service and slots +azd appservice swap + +# Swap with production as source +azd appservice swap --src @main --dst staging +``` diff --git a/cli/azd/extensions/azure.appservice/azure-appservice b/cli/azd/extensions/azure.appservice/azure-appservice new file mode 100755 index 00000000000..f78cf708205 Binary files /dev/null and b/cli/azd/extensions/azure.appservice/azure-appservice differ diff --git a/cli/azd/extensions/azure.appservice/build.ps1 b/cli/azd/extensions/azure.appservice/build.ps1 new file mode 100644 index 00000000000..5ceb60a8bbc --- /dev/null +++ b/cli/azd/extensions/azure.appservice/build.ps1 @@ -0,0 +1,78 @@ +# Ensure script fails on any error +$ErrorActionPreference = 'Stop' + +# Get the directory of the script +$EXTENSION_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path + +# Change to the script directory +Set-Location -Path $EXTENSION_DIR + +# Create a safe version of EXTENSION_ID replacing dots with dashes +$EXTENSION_ID_SAFE = $env:EXTENSION_ID -replace '\.', '-' + +# Define output directory +$OUTPUT_DIR = if ($env:OUTPUT_DIR) { $env:OUTPUT_DIR } else { Join-Path $EXTENSION_DIR "bin" } + +# Create output directory if it doesn't exist +if (-not (Test-Path -Path $OUTPUT_DIR)) { + New-Item -ItemType Directory -Path $OUTPUT_DIR | Out-Null +} + +# Get Git commit hash and build date +$COMMIT = git rev-parse HEAD +if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Failed to get git commit hash" + exit 1 +} +$BUILD_DATE = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ") + +# List of OS and architecture combinations +if ($env:EXTENSION_PLATFORM) { + $PLATFORMS = @($env:EXTENSION_PLATFORM) +} +else { + $PLATFORMS = @( + "windows/amd64", + "windows/arm64", + "darwin/amd64", + "darwin/arm64", + "linux/amd64", + "linux/arm64" + ) +} + +$APP_PATH = "$env:EXTENSION_ID/internal/cmd" + +# Loop through platforms and build +foreach ($PLATFORM in $PLATFORMS) { + $OS, $ARCH = $PLATFORM -split '/' + + $OUTPUT_NAME = Join-Path $OUTPUT_DIR "$EXTENSION_ID_SAFE-$OS-$ARCH" + + if ($OS -eq "windows") { + $OUTPUT_NAME += ".exe" + } + + Write-Host "Building for $OS/$ARCH..." + + # Delete the output file if it already exists + if (Test-Path -Path $OUTPUT_NAME) { + Remove-Item -Path $OUTPUT_NAME -Force + } + + # Set environment variables for Go build + $env:GOOS = $OS + $env:GOARCH = $ARCH + + go build ` + -ldflags="-X '$APP_PATH.Version=$env:EXTENSION_VERSION' -X '$APP_PATH.Commit=$COMMIT' -X '$APP_PATH.BuildDate=$BUILD_DATE'" ` + -o $OUTPUT_NAME + + if ($LASTEXITCODE -ne 0) { + Write-Host "An error occurred while building for $OS/$ARCH" + exit 1 + } +} + +Write-Host "Build completed successfully!" +Write-Host "Binaries are located in the $OUTPUT_DIR directory." diff --git a/cli/azd/extensions/azure.appservice/build.sh b/cli/azd/extensions/azure.appservice/build.sh new file mode 100755 index 00000000000..f1a995ec5e9 --- /dev/null +++ b/cli/azd/extensions/azure.appservice/build.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Get the directory of the script +EXTENSION_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Change to the script directory +cd "$EXTENSION_DIR" || exit + +# Create a safe version of EXTENSION_ID replacing dots with dashes +EXTENSION_ID_SAFE="${EXTENSION_ID//./-}" + +# Define output directory +OUTPUT_DIR="${OUTPUT_DIR:-$EXTENSION_DIR/bin}" + +# Create output and target directories if they don't exist +mkdir -p "$OUTPUT_DIR" + +# Get Git commit hash and build date +COMMIT=$(git rev-parse HEAD) +BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) + +# List of OS and architecture combinations +if [ -n "$EXTENSION_PLATFORM" ]; then + PLATFORMS=("$EXTENSION_PLATFORM") +else + PLATFORMS=( + "windows/amd64" + "windows/arm64" + "darwin/amd64" + "darwin/arm64" + "linux/amd64" + "linux/arm64" + ) +fi + +APP_PATH="$EXTENSION_ID/internal/cmd" + +# Loop through platforms and build +for PLATFORM in "${PLATFORMS[@]}"; do + OS=$(echo "$PLATFORM" | cut -d'/' -f1) + ARCH=$(echo "$PLATFORM" | cut -d'/' -f2) + + OUTPUT_NAME="$OUTPUT_DIR/$EXTENSION_ID_SAFE-$OS-$ARCH" + + if [ "$OS" = "windows" ]; then + OUTPUT_NAME+='.exe' + fi + + echo "Building for $OS/$ARCH..." + + # Delete the output file if it already exists + [ -f "$OUTPUT_NAME" ] && rm -f "$OUTPUT_NAME" + + # Set environment variables for Go build + GOOS=$OS GOARCH=$ARCH go build \ + -ldflags="-X '$APP_PATH.Version=$EXTENSION_VERSION' -X '$APP_PATH.Commit=$COMMIT' -X '$APP_PATH.BuildDate=$BUILD_DATE'" \ + -o "$OUTPUT_NAME" + + if [ $? -ne 0 ]; then + echo "An error occurred while building for $OS/$ARCH" + exit 1 + fi +done + +echo "Build completed successfully!" +echo "Binaries are located in the $OUTPUT_DIR directory." diff --git a/cli/azd/extensions/azure.appservice/extension.yaml b/cli/azd/extensions/azure.appservice/extension.yaml new file mode 100644 index 00000000000..ea3f82929b1 --- /dev/null +++ b/cli/azd/extensions/azure.appservice/extension.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=../extension.schema.json +id: azure.appservice +namespace: appservice +displayName: Azure App Service +description: Extension for managing Azure App Service resources. +usage: azd appservice [options] +# NOTE: Make sure version.txt is in sync with this version. +version: 0.1.0 +language: go +capabilities: + - custom-commands +examples: + - name: swap + description: Swap deployment slots for an App Service. + usage: azd appservice swap --service --src --dst diff --git a/cli/azd/extensions/azure.appservice/go.mod b/cli/azd/extensions/azure.appservice/go.mod new file mode 100644 index 00000000000..e127c91e405 --- /dev/null +++ b/cli/azd/extensions/azure.appservice/go.mod @@ -0,0 +1,89 @@ +module azure.appservice + +go 1.25 + +require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2 v2.3.0 + github.com/azure/azure-dev/cli/azd v0.0.0-20260122173819-89795b295491 + github.com/fatih/color v1.18.0 + github.com/spf13/cobra v1.10.1 +) + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/alecthomas/chroma/v2 v2.20.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/braydonk/yaml v0.9.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/charmbracelet/colorprofile v0.3.2 // indirect + github.com/charmbracelet/glamour v0.10.0 // indirect + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect + github.com/charmbracelet/x/ansi v0.10.2 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20251008171431-5d3777519489 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/drone/envsubst v1.0.3 // indirect + github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golobby/container/v3 v3.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect + github.com/jmespath-community/go-jmespath v1.1.1 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/microsoft/ApplicationInsights-Go v0.4.4 // indirect + github.com/microsoft/go-deviceid v1.0.0 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/nathan-fiscaletti/consolesize-go v0.0.0-20220204101620-317176b6684d // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.7.13 // indirect + github.com/yuin/goldmark-emoji v1.0.6 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/azure/azure-dev/cli/azd => ../.. diff --git a/cli/azd/extensions/azure.appservice/go.sum b/cli/azd/extensions/azure.appservice/go.sum new file mode 100644 index 00000000000..d4d39c594cf --- /dev/null +++ b/cli/azd/extensions/azure.appservice/go.sum @@ -0,0 +1,247 @@ +code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2 v2.3.0 h1:JI8PcWOImyvIUEZ0Bbmfe05FOlWkMi2KhjG+cAKaUms= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2 v2.3.0/go.mod h1:nJLFPGJkyKfDDyJiPuHIXsCi/gpJkm07EvRgiX7SGlI= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/adam-lavrik/go-imath v0.0.0-20210910152346-265a42a96f0b h1:g9SuFmxM/WucQFKTMSP+irxyf5m0RiUJreBDhGI6jSA= +github.com/adam-lavrik/go-imath v0.0.0-20210910152346-265a42a96f0b/go.mod h1:XjvqMUpGd3Xn9Jtzk/4GEBCSoBX0eB2RyriXgne0IdM= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= +github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= +github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg= +github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/braydonk/yaml v0.9.0 h1:ewGMrVmEVpsm3VwXQDR388sLg5+aQ8Yihp6/hc4m+h4= +github.com/braydonk/yaml v0.9.0/go.mod h1:hcm3h581tudlirk8XEUPDBAimBPbmnL0Y45hCRl47N4= +github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY= +github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= +github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= +github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= +github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw= +github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/slice v0.0.0-20251008171431-5d3777519489 h1:a5q2sWiet6kgqucSGjYN1jhT2cn4bMKUwprtm2IGRto= +github.com/charmbracelet/x/exp/slice v0.0.0-20251008171431-5d3777519489/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= +github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= +github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= +github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golobby/container/v3 v3.3.2 h1:7u+RgNnsdVlhGoS8gY4EXAG601vpMMzLZlYqSp77Quw= +github.com/golobby/container/v3 v3.3.2/go.mod h1:RDdKpnKpV1Of11PFBe7Dxc2C1k2KaLE4FD47FflAmj0= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/jmespath-community/go-jmespath v1.1.1 h1:bFikPhsi/FdmlZhVgSCd2jj1e7G/rw+zyQfyg5UF+L4= +github.com/jmespath-community/go-jmespath v1.1.1/go.mod h1:4gOyFJsR/Gk+05RgTKYrifT7tBPWD8Lubtb5jRrfy9I= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/microsoft/ApplicationInsights-Go v0.4.4 h1:G4+H9WNs6ygSCe6sUyxRc2U81TI5Es90b2t/MwX5KqY= +github.com/microsoft/ApplicationInsights-Go v0.4.4/go.mod h1:fKRUseBqkw6bDiXTs3ESTiU/4YTIHsQS4W3fP2ieF4U= +github.com/microsoft/go-deviceid v1.0.0 h1:i5AQ654Xk9kfvwJeKQm3w2+eT1+ImBDVEpAR0AjpP40= +github.com/microsoft/go-deviceid v1.0.0/go.mod h1:KY13FeVdHkzD8gy+6T8+kVmD/7RMpTaWW75K+T4uZWg= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/nathan-fiscaletti/consolesize-go v0.0.0-20220204101620-317176b6684d h1:NqRhLdNVlozULwM1B3VaHhcXYSgrOAv8V5BE65om+1Q= +github.com/nathan-fiscaletti/consolesize-go v0.0.0-20220204101620-317176b6684d/go.mod h1:cxIIfNMTwff8f/ZvRouvWYF6wOoO7nj99neWSx2q/Es= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= +github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4= +github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= +github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= +golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/azd/extensions/azure.appservice/internal/cmd/root.go b/cli/azd/extensions/azure.appservice/internal/cmd/root.go new file mode 100644 index 00000000000..abfaa172f1a --- /dev/null +++ b/cli/azd/extensions/azure.appservice/internal/cmd/root.go @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "github.com/spf13/cobra" +) + +type rootFlagsDefinition struct { + Debug bool + NoPrompt bool +} + +// Enable access to the global command flags +var rootFlags rootFlagsDefinition + +func NewRootCommand() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "appservice [options]", + Short: "Extension for managing Azure App Service resources.", + SilenceUsage: true, + SilenceErrors: true, + CompletionOptions: cobra.CompletionOptions{ + DisableDefaultCmd: true, + }, + } + + rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + rootCmd.PersistentFlags().BoolVar( + &rootFlags.Debug, + "debug", + false, + "Enable debug mode", + ) + + // Adds support for `--no-prompt` global flag in azd + // Without this the extension command will error when the flag is provided + rootCmd.PersistentFlags().BoolVar( + &rootFlags.NoPrompt, + "no-prompt", + false, + "Accepts the default value instead of prompting, or it fails if there is no default.", + ) + + rootCmd.AddCommand(newSwapCommand(rootFlags)) + rootCmd.AddCommand(newVersionCommand()) + + return rootCmd +} diff --git a/cli/azd/extensions/azure.appservice/internal/cmd/swap.go b/cli/azd/extensions/azure.appservice/internal/cmd/swap.go new file mode 100644 index 00000000000..b0ffa6e4ca6 --- /dev/null +++ b/cli/azd/extensions/azure.appservice/internal/cmd/swap.go @@ -0,0 +1,597 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "sync" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/fatih/color" + "github.com/spf13/cobra" +) + +type swapFlags struct { + service string + src string + dst string +} + +func newSwapCommand(rootFlags rootFlagsDefinition) *cobra.Command { + flags := &swapFlags{} + + cmd := &cobra.Command{ + Use: "swap", + Short: "Swap deployment slots for an App Service.", + Long: `Swap deployment slots for an Azure App Service. + +This command allows you to swap the content between two deployment slots, +or between a slot and the production environment. + +Use @main to refer to the production slot.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runSwap(cmd.Context(), flags, rootFlags) + }, + } + + cmd.Flags().StringVar(&flags.service, "service", "", "The name of the service to swap slots for.") + cmd.Flags().StringVar(&flags.src, "src", "", "The source slot name. Use @main for production.") + cmd.Flags().StringVar(&flags.dst, "dst", "", "The destination slot name. Use @main for production.") + + return cmd +} + +func runSwap(ctx context.Context, flags *swapFlags, rootFlags rootFlagsDefinition) error { + // Create a new context that includes the AZD access token + ctx = azdext.WithAccessToken(ctx) + + // Create a new AZD client + azdClient, err := azdext.NewAzdClient() + if err != nil { + return fmt.Errorf("failed to create azd client: %w", err) + } + defer azdClient.Close() + + // Wait for debugger if AZD_EXT_DEBUG is set + if err := azdext.WaitForDebugger(ctx, azdClient); err != nil { + if errors.Is(err, context.Canceled) || errors.Is(err, azdext.ErrDebuggerAborted) { + return nil + } + return fmt.Errorf("failed waiting for debugger: %w", err) + } + + // Get current environment + envClient := azdClient.Environment() + currentEnv, err := envClient.GetCurrent(ctx, &azdext.EmptyRequest{}) + if err != nil { + return fmt.Errorf("failed to get current environment: %w", err) + } + + // Get subscription ID from environment + subscriptionResp, err := envClient.GetValue(ctx, &azdext.GetEnvRequest{ + EnvName: currentEnv.Environment.Name, + Key: "AZURE_SUBSCRIPTION_ID", + }) + if err != nil { + return fmt.Errorf("failed to get AZURE_SUBSCRIPTION_ID: %w", err) + } + + subscriptionId := subscriptionResp.Value + if subscriptionId == "" { + return fmt.Errorf("AZURE_SUBSCRIPTION_ID environment variable is required") + } + + // Get the resolved services from azd + servicesResponse, err := azdClient.Project().GetResolvedServices(ctx, &azdext.EmptyRequest{}) + if err != nil { + return fmt.Errorf("failed to get services: %w", err) + } + + // Filter to only appservice services + var appserviceServices []*azdext.ServiceConfig + for _, svc := range servicesResponse.Services { + if svc.Host == "appservice" { + appserviceServices = append(appserviceServices, svc) + } + } + + if len(appserviceServices) == 0 { + return fmt.Errorf("no App Service services found in the project") + } + + // Determine which service to use + var selectedService *azdext.ServiceConfig + if flags.service != "" { + // Service specified via flag + for _, svc := range appserviceServices { + if svc.Name == flags.service { + selectedService = svc + break + } + } + if selectedService == nil { + return fmt.Errorf("service '%s' not found or is not an App Service", flags.service) + } + } else if len(appserviceServices) == 1 { + // Only one App Service, use it + selectedService = appserviceServices[0] + } else { + // Multiple services - prompt user to select + choices := make([]*azdext.SelectChoice, len(appserviceServices)) + for i, svc := range appserviceServices { + choices[i] = &azdext.SelectChoice{ + Value: svc.Name, + Label: svc.Name, + } + } + prompt, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{ + Options: &azdext.SelectOptions{ + Message: "Select a service:", + Choices: choices, + }, + }) + if err != nil { + return fmt.Errorf("selecting service: %w", err) + } + selectedService = appserviceServices[prompt.GetValue()] + } + + color.Cyan("Using service: %s", selectedService.Name) + + // Get the target resource (resource group and app name) using azd's discovery logic + // This handles both explicit configuration and tag-based discovery + targetResourceResp, err := azdClient.Project().GetServiceTargetResource(ctx, &azdext.GetServiceTargetResourceRequest{ + ServiceName: selectedService.Name, + }) + if err != nil { + return fmt.Errorf("failed to resolve target resource for service '%s': %w", selectedService.Name, err) + } + + resourceGroup := targetResourceResp.TargetResource.ResourceGroupName + if resourceGroup == "" { + return fmt.Errorf("resource group not found for service '%s'", selectedService.Name) + } + + appName := targetResourceResp.TargetResource.ResourceName + if appName == "" { + return fmt.Errorf("resource name not found for service '%s'. "+ + "Ensure the service has been provisioned or configure 'resourceName' in azure.yaml", selectedService.Name) + } + + // Get tenant ID for the subscription + tenantResponse, err := azdClient.Account().LookupTenant(ctx, &azdext.LookupTenantRequest{ + SubscriptionId: subscriptionId, + }) + if err != nil { + return fmt.Errorf("failed to get tenant ID: %w", err) + } + + // Create Azure credential using AzureDeveloperCLICredential + credential, err := azidentity.NewAzureDeveloperCLICredential(&azidentity.AzureDeveloperCLICredentialOptions{ + TenantID: tenantResponse.TenantId, + }) + if err != nil { + return fmt.Errorf("failed to create credential: %w", err) + } + + // Create the web apps client + client, err := armappservice.NewWebAppsClient(subscriptionId, credential, nil) + if err != nil { + return fmt.Errorf("failed to create web apps client: %w", err) + } + + // Get the list of deployment slots + slots, err := getAppServiceSlots(ctx, client, resourceGroup, appName) + if err != nil { + return fmt.Errorf("getting deployment slots: %w", err) + } + + // Check if there are any slots + if len(slots) == 0 { + return fmt.Errorf("swap operation requires a service with at least one deployment slot") + } + + // Normalize src and dst flags + srcSlot := normalizeSlotName(flags.src) + dstSlot := normalizeSlotName(flags.dst) + srcProvided := flags.src != "" + dstProvided := flags.dst != "" + + // Build the list of all slot names (including production as empty string) + slotNames := []string{""} // Production is represented as empty string + for _, slot := range slots { + slotNames = append(slotNames, slot) + } + + // If there's only one slot, auto-select based on the scenario + if len(slots) == 1 { + onlySlot := slots[0] + if !srcProvided && !dstProvided { + // No arguments provided - default behavior: swap slot to production + srcSlot = onlySlot + dstSlot = "" + } else { + // Arguments provided - validate they match the only slot and production + if !isValidSlotName(srcSlot, slotNames) || !isValidSlotName(dstSlot, slotNames) { + return fmt.Errorf("invalid slot name") + } + // Ensure at least one is the only slot + if srcSlot != onlySlot && dstSlot != onlySlot { + return fmt.Errorf("at least one slot must be '%s' when there is only one slot", onlySlot) + } + } + } else { + // Multiple slots - prompt if arguments not provided + if !srcProvided || !dstProvided { + // Prompt for source slot + if !srcProvided { + srcChoices := []*azdext.SelectChoice{{Value: "", Label: "@main (production)"}} + for _, slot := range slots { + srcChoices = append(srcChoices, &azdext.SelectChoice{Value: slot, Label: slot}) + } + + prompt, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{ + Options: &azdext.SelectOptions{ + Message: "Select the source slot:", + Choices: srcChoices, + }, + }) + if err != nil { + return fmt.Errorf("selecting source slot: %w", err) + } + + srcSlot = srcChoices[prompt.GetValue()].Value + } + + // Prompt for destination slot (excluding the selected source) + if !dstProvided { + dstChoices := []*azdext.SelectChoice{} + if srcSlot != "" { + dstChoices = append(dstChoices, &azdext.SelectChoice{Value: "", Label: "@main (production)"}) + } + for _, slot := range slots { + if slot != srcSlot { + dstChoices = append(dstChoices, &azdext.SelectChoice{Value: slot, Label: slot}) + } + } + + prompt, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{ + Options: &azdext.SelectOptions{ + Message: "Select the destination slot:", + Choices: dstChoices, + }, + }) + if err != nil { + return fmt.Errorf("selecting destination slot: %w", err) + } + + dstSlot = dstChoices[prompt.GetValue()].Value + } + } + + // Validate slot names + if !isValidSlotName(srcSlot, slotNames) { + return fmt.Errorf("invalid source slot: %s", srcSlot) + } + if !isValidSlotName(dstSlot, slotNames) { + return fmt.Errorf("invalid destination slot: %s", dstSlot) + } + } + + // Validate that source and destination are different + if srcSlot == dstSlot { + return fmt.Errorf("source and destination slots cannot be the same") + } + + // Get display names for confirmation + srcDisplay := srcSlot + if srcDisplay == "" { + srcDisplay = "@main (production)" + } + dstDisplay := dstSlot + if dstDisplay == "" { + dstDisplay = "@main (production)" + } + + // Confirm the swap unless --no-prompt is set + if !rootFlags.NoPrompt { + defaultValue := true + confirmPrompt, err := azdClient.Prompt().Confirm(ctx, &azdext.ConfirmRequest{ + Options: &azdext.ConfirmOptions{ + Message: fmt.Sprintf("Swap '%s' with '%s'?", srcDisplay, dstDisplay), + DefaultValue: &defaultValue, + }, + }) + if err != nil { + return fmt.Errorf("confirming swap: %w", err) + } + + if confirmPrompt.Value == nil || !*confirmPrompt.Value { + color.Yellow("Swap cancelled by user.") + return nil + } + } + + // Perform the swap (spinner will show progress) + err = swapSlot(ctx, client, resourceGroup, appName, srcSlot, dstSlot) + if err != nil { + return fmt.Errorf("swapping slots: %w", err) + } + + color.Green("✓ Swap completed successfully: %s ↔ %s", srcDisplay, dstDisplay) + return nil +} + +func normalizeSlotName(slot string) string { + // Normalize "@main" to empty string (internal representation for main app/production slot) + if strings.EqualFold(slot, "@main") { + return "" + } + return slot +} + +func isValidSlotName(name string, availableSlots []string) bool { + for _, slot := range availableSlots { + if slot == name { + return true + } + } + return false +} + +// spinner represents a simple terminal spinner with status updates +type spinner struct { + frames []string + interval time.Duration + message string + mu sync.Mutex + stop chan struct{} + done chan struct{} +} + +// newSpinner creates a new spinner instance +func newSpinner(message string) *spinner { + return &spinner{ + frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}, + interval: 100 * time.Millisecond, + message: message, + stop: make(chan struct{}), + done: make(chan struct{}), + } +} + +// start begins the spinner animation +func (s *spinner) start() { + go func() { + defer close(s.done) + frameIdx := 0 + for { + select { + case <-s.stop: + // Clear the line before exiting + fmt.Print("\r\033[K") + return + default: + s.mu.Lock() + msg := s.message + s.mu.Unlock() + + frame := s.frames[frameIdx%len(s.frames)] + fmt.Printf("\r%s %s", color.CyanString(frame), msg) + frameIdx++ + time.Sleep(s.interval) + } + } + }() +} + +// updateMessage updates the spinner message +func (s *spinner) updateMessage(message string) { + s.mu.Lock() + defer s.mu.Unlock() + s.message = message +} + +// stopSpinner stops the spinner and waits for it to finish +func (s *spinner) stopSpinner() { + close(s.stop) + <-s.done +} + +func getAppServiceSlots( + ctx context.Context, + client *armappservice.WebAppsClient, + resourceGroup string, + appName string, +) ([]string, error) { + var slots []string + pager := client.NewListSlotsPager(resourceGroup, appName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, fmt.Errorf("listing webapp slots: %w", err) + } + for _, slot := range page.Value { + if slot.Name != nil { + // Slot names are returned as "appName/slotName", extract just the slot name + slotName := *slot.Name + if idx := strings.LastIndex(slotName, "/"); idx != -1 { + slotName = slotName[idx+1:] + } + slots = append(slots, slotName) + } + } + } + + return slots, nil +} + +func swapSlot( + ctx context.Context, + client *armappservice.WebAppsClient, + resourceGroup string, + appName string, + sourceSlot string, + targetSlot string, +) error { + // Get display names for progress messages + srcDisplay := sourceSlot + if srcDisplay == "" { + srcDisplay = "production" + } + dstDisplay := targetSlot + if dstDisplay == "" { + dstDisplay = "production" + } + + // Start the spinner + spin := newSpinner(fmt.Sprintf("Initiating swap: %s ↔ %s", srcDisplay, dstDisplay)) + spin.start() + defer spin.stopSpinner() + + // Handle the swap based on which slots are involved + if sourceSlot == "" && targetSlot == "" { + return fmt.Errorf("cannot swap production with itself") + } + + var swapErr error + + if sourceSlot == "" { + // Swapping production with a named slot (e.g., production -> staging) + swapRequest := armappservice.CsmSlotEntity{ + TargetSlot: to.Ptr(targetSlot), + } + poller, err := client.BeginSwapSlotWithProduction(ctx, resourceGroup, appName, swapRequest, nil) + if err != nil { + return fmt.Errorf("starting slot swap: %w", err) + } + swapErr = pollWithProgress(ctx, poller, spin, srcDisplay, dstDisplay) + } else if targetSlot == "" { + // Swapping a named slot with production (e.g., staging -> production) + swapRequest := armappservice.CsmSlotEntity{ + TargetSlot: to.Ptr("production"), + } + poller, err := client.BeginSwapSlot(ctx, resourceGroup, appName, sourceSlot, swapRequest, nil) + if err != nil { + return fmt.Errorf("starting slot swap: %w", err) + } + swapErr = pollWithProgress(ctx, poller, spin, srcDisplay, dstDisplay) + } else { + // Swapping between two named slots + swapRequest := armappservice.CsmSlotEntity{ + TargetSlot: to.Ptr(targetSlot), + } + poller, err := client.BeginSwapSlot(ctx, resourceGroup, appName, sourceSlot, swapRequest, nil) + if err != nil { + return fmt.Errorf("starting slot swap: %w", err) + } + swapErr = pollWithProgress(ctx, poller, spin, srcDisplay, dstDisplay) + } + + if swapErr != nil { + return fmt.Errorf("waiting for slot swap to complete: %w", swapErr) + } + + return nil +} + +// pollWithProgress polls the operation and updates the spinner with status information +func pollWithProgress[T any]( + ctx context.Context, + poller *runtime.Poller[T], + spin *spinner, + srcSlot string, + dstSlot string, +) error { + pollCount := 0 + startTime := time.Now() + + for !poller.Done() { + pollCount++ + elapsed := time.Since(startTime).Round(time.Second) + + // Update spinner with progress information + statusMsg := getProgressMessage(pollCount, elapsed, srcSlot, dstSlot) + spin.updateMessage(statusMsg) + + // Poll the operation + resp, err := poller.Poll(ctx) + if err != nil { + return err + } + + // Try to extract status from response headers or body + if resp != nil { + status := extractStatus(resp) + if status != "" { + spin.updateMessage(fmt.Sprintf("%s [%s]", statusMsg, status)) + } + } + + // Wait before next poll (Azure typically recommends waiting between polls) + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(2 * time.Second): + // Continue polling + } + } + + return nil +} + +// getProgressMessage returns a progress message based on the poll count and elapsed time +func getProgressMessage(pollCount int, elapsed time.Duration, srcSlot, dstSlot string) string { + phases := []string{ + "Preparing swap", + "Warming up target slot", + "Applying configuration", + "Swapping virtual IPs", + "Verifying swap", + "Finalizing", + } + + // Estimate phase based on poll count (rough approximation) + phaseIdx := pollCount / 3 + if phaseIdx >= len(phases) { + phaseIdx = len(phases) - 1 + } + + return fmt.Sprintf("%s: %s ↔ %s (%v)", phases[phaseIdx], srcSlot, dstSlot, elapsed) +} + +// extractStatus tries to extract status information from the HTTP response +func extractStatus(resp *http.Response) string { + if resp == nil { + return "" + } + + // Check for Azure-AsyncOperation or Location header status + if status := resp.Header.Get("x-ms-request-status"); status != "" { + return status + } + + // Check provisioning state from response body if available + // Note: We don't want to consume the body here as it may be needed later + // Just return the HTTP status for now + switch resp.StatusCode { + case http.StatusOK: + return "Completed" + case http.StatusAccepted: + return "In Progress" + case http.StatusCreated: + return "Created" + default: + return "" + } +} diff --git a/cli/azd/extensions/azure.appservice/internal/cmd/version.go b/cli/azd/extensions/azure.appservice/internal/cmd/version.go new file mode 100644 index 00000000000..586402b32cd --- /dev/null +++ b/cli/azd/extensions/azure.appservice/internal/cmd/version.go @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "azure.appservice/internal/version" + + "github.com/fatih/color" + "github.com/spf13/cobra" +) + +func newVersionCommand() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Display the version of the extension.", + RunE: func(cmd *cobra.Command, args []string) error { + color.Cyan("Azure App Service Extension") + color.White("Version: %s", version.Version) + return nil + }, + } +} diff --git a/cli/azd/extensions/azure.appservice/internal/version/version.go b/cli/azd/extensions/azure.appservice/internal/version/version.go new file mode 100644 index 00000000000..b1d80481f56 --- /dev/null +++ b/cli/azd/extensions/azure.appservice/internal/version/version.go @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package version + +// Version is the current version of the extension. +// This is set at build time via ldflags. +var Version = "0.1.0" diff --git a/cli/azd/extensions/azure.appservice/main.go b/cli/azd/extensions/azure.appservice/main.go new file mode 100644 index 00000000000..533adf6e31b --- /dev/null +++ b/cli/azd/extensions/azure.appservice/main.go @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "os" + + "azure.appservice/internal/cmd" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/fatih/color" +) + +func init() { + forceColorVal, has := os.LookupEnv("FORCE_COLOR") + if has && forceColorVal == "1" { + color.NoColor = false + } +} + +func main() { + // Execute the root command + ctx := azdext.NewContext() + rootCmd := cmd.NewRootCommand() + + if err := rootCmd.ExecuteContext(ctx); err != nil { + color.Red("Error: %v", err) + os.Exit(1) + } +} diff --git a/cli/azd/extensions/azure.appservice/version.txt b/cli/azd/extensions/azure.appservice/version.txt new file mode 100644 index 00000000000..6e8bf73aa55 --- /dev/null +++ b/cli/azd/extensions/azure.appservice/version.txt @@ -0,0 +1 @@ +0.1.0 diff --git a/cli/azd/grpc/proto/project.proto b/cli/azd/grpc/proto/project.proto index 75a7fe4f2fe..aeb40f934c0 100644 --- a/cli/azd/grpc/proto/project.proto +++ b/cli/azd/grpc/proto/project.proto @@ -7,6 +7,7 @@ package azdext; option go_package = "github.com/azure/azure-dev/cli/azd/pkg/azdext"; import "models.proto"; +import "service_target.proto"; import "include/google/protobuf/struct.proto"; // ProjectService defines methods for managing projects and their configurations. @@ -14,6 +15,14 @@ service ProjectService { // Gets the current project. rpc Get(EmptyRequest) returns (GetProjectResponse); + // GetServiceTargetResource resolves the target Azure resource for a service. + // This uses azd's standard resource discovery logic which: + // - Looks up resources by azd-service-name tag + // - Falls back to explicit resourceName from configuration + // - Resolves the resource group name from configuration or environment + // The returned TargetResource includes subscription, resource group, resource name, and resource type. + rpc GetServiceTargetResource(GetServiceTargetResourceRequest) returns (GetServiceTargetResourceResponse); + // AddService adds a new service to the project. rpc AddService(AddServiceRequest) returns (EmptyResponse); @@ -166,3 +175,15 @@ message UnsetServiceConfigRequest { string service_name = 1; string path = 2; } + +// Request message for GetServiceTargetResource +message GetServiceTargetResourceRequest { + // The name of the service to resolve the target resource for + string service_name = 1; +} + +// Response message for GetServiceTargetResource +message GetServiceTargetResourceResponse { + // The resolved target resource including subscription, resource group, resource name, and resource type + TargetResource target_resource = 1; +} diff --git a/cli/azd/internal/grpcserver/project_service.go b/cli/azd/internal/grpcserver/project_service.go index 3ac2baef41f..5522a85eb65 100644 --- a/cli/azd/internal/grpcserver/project_service.go +++ b/cli/azd/internal/grpcserver/project_service.go @@ -24,11 +24,13 @@ import ( type projectService struct { azdext.UnimplementedProjectServiceServer - lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext] - lazyEnvManager *lazy.Lazy[environment.Manager] - importManager *project.ImportManager - lazyProjectConfig *lazy.Lazy[*project.ProjectConfig] - ghCli *github.Cli + lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext] + lazyEnvManager *lazy.Lazy[environment.Manager] + lazyResourceManager *lazy.Lazy[project.ResourceManager] + lazyEnv *lazy.Lazy[*environment.Environment] + importManager *project.ImportManager + lazyProjectConfig *lazy.Lazy[*project.ProjectConfig] + ghCli *github.Cli } // NewProjectService creates a new project service instance with lazy-loaded dependencies. @@ -38,22 +40,28 @@ type projectService struct { // Parameters: // - lazyAzdContext: Lazy-loaded Azure Developer CLI context for project directory operations // - lazyEnvManager: Lazy-loaded environment manager for handling Azure environments +// - lazyResourceManager: Lazy-loaded resource manager for resolving target resources +// - lazyEnv: Lazy-loaded environment for accessing environment variables and subscription info // - lazyProjectConfig: Lazy-loaded project configuration for accessing project settings // // Returns an implementation of azdext.ProjectServiceServer. func NewProjectService( lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext], lazyEnvManager *lazy.Lazy[environment.Manager], + lazyResourceManager *lazy.Lazy[project.ResourceManager], + lazyEnv *lazy.Lazy[*environment.Environment], lazyProjectConfig *lazy.Lazy[*project.ProjectConfig], importManager *project.ImportManager, ghCli *github.Cli, ) azdext.ProjectServiceServer { return &projectService{ - lazyAzdContext: lazyAzdContext, - lazyEnvManager: lazyEnvManager, - lazyProjectConfig: lazyProjectConfig, - importManager: importManager, - ghCli: ghCli, + lazyAzdContext: lazyAzdContext, + lazyEnvManager: lazyEnvManager, + lazyResourceManager: lazyResourceManager, + lazyEnv: lazyEnv, + lazyProjectConfig: lazyProjectConfig, + importManager: importManager, + ghCli: ghCli, } } @@ -860,3 +868,64 @@ func ( FilePath: urlInfo.FilePath, }, nil } + +// GetServiceTargetResource resolves the target Azure resource for a service. +// This uses azd's standard resource discovery logic which: +// - Looks up resources by azd-service-name tag +// - Falls back to explicit resourceName from configuration +// - Resolves the resource group name from configuration or environment +// +// The returned TargetResource includes subscription, resource group, resource name, and resource type. +func (s *projectService) GetServiceTargetResource( + ctx context.Context, + req *azdext.GetServiceTargetResourceRequest, +) (*azdext.GetServiceTargetResourceResponse, error) { + if req.ServiceName == "" { + return nil, status.Error(codes.InvalidArgument, "service_name is required") + } + + // Get the project configuration + projectConfig, err := s.lazyProjectConfig.GetValue() + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get project config: %v", err) + } + + // Validate the service exists + serviceConfig, exists := projectConfig.Services[req.ServiceName] + if !exists { + return nil, status.Errorf(codes.NotFound, "service '%s' not found in project", req.ServiceName) + } + + // Get the environment to read subscription ID + env, err := s.lazyEnv.GetValue() + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get environment: %v", err) + } + + subscriptionId := env.GetSubscriptionId() + if subscriptionId == "" { + return nil, status.Error(codes.FailedPrecondition, "AZURE_SUBSCRIPTION_ID not set in environment") + } + + // Get the resource manager + resourceManager, err := s.lazyResourceManager.GetValue() + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get resource manager: %v", err) + } + + // Resolve the target resource using azd's standard logic + targetResource, err := resourceManager.GetTargetResource(ctx, subscriptionId, serviceConfig) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to resolve target resource: %v", err) + } + + return &azdext.GetServiceTargetResourceResponse{ + TargetResource: &azdext.TargetResource{ + SubscriptionId: targetResource.SubscriptionId(), + ResourceGroupName: targetResource.ResourceGroupName(), + ResourceName: targetResource.ResourceName(), + ResourceType: targetResource.ResourceType(), + Metadata: targetResource.Metadata(), + }, + }, nil +} diff --git a/cli/azd/internal/grpcserver/project_service_test.go b/cli/azd/internal/grpcserver/project_service_test.go index 635ac479f57..076357fd82c 100644 --- a/cli/azd/internal/grpcserver/project_service_test.go +++ b/cli/azd/internal/grpcserver/project_service_test.go @@ -52,7 +52,7 @@ func Test_ProjectService_NoProject(t *testing.T) { // Create the service with ImportManager. importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, ghCli) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, ghCli) _, err := service.Get(*mockContext.Context, &azdext.EmptyRequest{}) require.Error(t, err) } @@ -108,7 +108,7 @@ func Test_ProjectService_Flow(t *testing.T) { // Create the service with ImportManager. importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, ghCli) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, ghCli) // Test: Retrieve project details. getResponse, err := service.Get(*mockContext.Context, &azdext.EmptyRequest{}) @@ -156,7 +156,7 @@ func Test_ProjectService_AddService(t *testing.T) { // Create the project service with ImportManager. importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, ghCli) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, ghCli) // Prepare a new service addition request. serviceRequest := &azdext.AddServiceRequest{ @@ -221,7 +221,7 @@ func Test_ProjectService_ConfigSection(t *testing.T) { lazyProjectConfig := lazy.From(projectConfig) importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, nil) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, nil) t.Run("GetConfigSection_Success", func(t *testing.T) { resp, err := service.GetConfigSection(*mockContext.Context, &azdext.GetProjectConfigSectionRequest{ @@ -290,7 +290,7 @@ func Test_ProjectService_ConfigValue(t *testing.T) { lazyProjectConfig := lazy.From(projectConfig) importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, nil) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, nil) t.Run("GetConfigValue_String", func(t *testing.T) { resp, err := service.GetConfigValue(*mockContext.Context, &azdext.GetProjectConfigValueRequest{ @@ -364,7 +364,7 @@ func Test_ProjectService_SetConfigSection(t *testing.T) { lazyProjectConfig := lazy.From(projectConfig) importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, nil) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, nil) t.Run("SetConfigSection_NewSection", func(t *testing.T) { // Create section data @@ -447,7 +447,7 @@ func Test_ProjectService_SetConfigValue(t *testing.T) { lazyProjectConfig := lazy.From(projectConfig) importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, nil) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, nil) t.Run("SetConfigValue_String", func(t *testing.T) { value, err := structpb.NewValue("test-string") @@ -544,7 +544,7 @@ func Test_ProjectService_UnsetConfig(t *testing.T) { lazyProjectConfig := lazy.From(projectConfig) importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, nil) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, nil) t.Run("UnsetConfig_NestedValue", func(t *testing.T) { _, err := service.UnsetConfig(*mockContext.Context, &azdext.UnsetProjectConfigRequest{ @@ -614,7 +614,7 @@ func Test_ProjectService_ConfigNilAdditionalProperties(t *testing.T) { lazyProjectConfig := lazy.From(projectConfig) importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, nil) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, nil) t.Run("GetConfigValue_NilAdditionalProperties", func(t *testing.T) { resp, err := service.GetConfigValue(*mockContext.Context, &azdext.GetProjectConfigValueRequest{ @@ -703,7 +703,7 @@ func Test_ProjectService_ServiceConfiguration(t *testing.T) { // Create the service. importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, nil) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, nil) t.Run("GetServiceConfigSection_Found", func(t *testing.T) { resp, err := service.GetServiceConfigSection(*mockContext.Context, &azdext.GetServiceConfigSectionRequest{ @@ -979,7 +979,7 @@ func Test_ProjectService_ServiceConfiguration_NilAdditionalProperties(t *testing // Create the service. importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, nil) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, nil) t.Run("GetServiceConfigSection_NilAdditionalProperties", func(t *testing.T) { resp, err := service.GetServiceConfigSection(*mockContext.Context, &azdext.GetServiceConfigSectionRequest{ @@ -1053,7 +1053,7 @@ func Test_ProjectService_ChangeServiceHost(t *testing.T) { lazyProjectConfig := lazy.From(projectConfig) importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, nil) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, nil) // Test 1: Get the current host value getResp, err := service.GetServiceConfigValue(*mockContext.Context, &azdext.GetServiceConfigValueRequest{ @@ -1136,7 +1136,7 @@ func Test_ProjectService_TypeValidation_InvalidChangesNotPersisted(t *testing.T) lazyProjectConfig := lazy.From(loadedConfig) importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, nil) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, nil) t.Run("Project_SetInfraToInt_ShouldFailAndNotPersist", func(t *testing.T) { // Try to set "infra" (which should be an object) to an integer @@ -1330,7 +1330,7 @@ func Test_ProjectService_TypeValidation_CoercedValues(t *testing.T) { lazyProjectConfig := lazy.From(loadedConfig) importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, nil) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, nil) t.Run("SetNameToInt_GetsCoercedToString", func(t *testing.T) { // Try to set "name" (which should be a string) to an integer @@ -1463,7 +1463,7 @@ func Test_ProjectService_EventDispatcherPreservation(t *testing.T) { // Create project service importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, nil) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, nil) // Step 3: Modify project configuration customValue, err := structpb.NewValue("project-custom-value") @@ -1639,7 +1639,7 @@ func Test_ProjectService_EventDispatcherPreservation_MultipleUpdates(t *testing. require.NoError(t, err) importManager := project.NewImportManager(&project.DotNetImporter{}) - service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, nil) + service := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, nil) // Perform multiple configuration updates for i := 1; i <= 3; i++ { @@ -1709,7 +1709,7 @@ func Test_ProjectService_ServiceConfigValue_EmptyPath(t *testing.T) { // Create the service importManager := project.NewImportManager(&project.DotNetImporter{}) - projectService := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, nil) + projectService := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, nil) t.Run("GetServiceConfigValue_EmptyPath", func(t *testing.T) { _, err := projectService.GetServiceConfigValue(*mockContext.Context, &azdext.GetServiceConfigValueRequest{ @@ -1774,7 +1774,7 @@ func Test_ProjectService_EmptyStringValidation(t *testing.T) { // Create the service importManager := project.NewImportManager(&project.DotNetImporter{}) - projectService := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig, importManager, nil) + projectService := NewProjectService(lazyAzdContext, lazyEnvManager, nil, nil, lazyProjectConfig, importManager, nil) // Project-level config method validations t.Run("GetConfigValue_EmptyPath", func(t *testing.T) { diff --git a/cli/azd/pkg/azapi/webapp.go b/cli/azd/pkg/azapi/webapp.go index 8c9bc6e6230..4b90ced1a9b 100644 --- a/cli/azd/pkg/azapi/webapp.go +++ b/cli/azd/pkg/azapi/webapp.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2" "github.com/azure/azure-dev/cli/azd/pkg/azsdk" @@ -338,3 +339,70 @@ func (cli *AzureClient) DeployAppServiceSlotZip( return to.Ptr(response.StatusText), nil } + +// SwapSlot swaps two deployment slots or a slot with production. +// sourceSlot: the source slot name (empty string means production) +// targetSlot: the target slot name (empty string means production) +// The swap operation swaps the content of sourceSlot into targetSlot. +func (cli *AzureClient) SwapSlot( + ctx context.Context, + subscriptionId string, + resourceGroup string, + appName string, + sourceSlot string, + targetSlot string, +) error { + client, err := cli.createWebAppsClient(ctx, subscriptionId) + if err != nil { + return err + } + + // Handle the swap based on which slots are involved + var poller interface{} + var swapErr error + + if sourceSlot == "" && targetSlot == "" { + return fmt.Errorf("cannot swap production with itself") + } else if sourceSlot == "" { + // Swapping production with a named slot (e.g., production -> staging) + // Use BeginSwapSlotWithProduction with targetSlot as the slot to swap with + swapRequest := armappservice.CsmSlotEntity{ + TargetSlot: to.Ptr(targetSlot), + } + poller, swapErr = client.BeginSwapSlotWithProduction(ctx, resourceGroup, appName, swapRequest, nil) + } else if targetSlot == "" { + // Swapping a named slot with production (e.g., staging -> production) + // Use BeginSwapSlot with sourceSlot and production as target + swapRequest := armappservice.CsmSlotEntity{ + TargetSlot: to.Ptr("production"), + } + poller, swapErr = client.BeginSwapSlot(ctx, resourceGroup, appName, sourceSlot, swapRequest, nil) + } else { + // Swapping between two named slots + swapRequest := armappservice.CsmSlotEntity{ + TargetSlot: to.Ptr(targetSlot), + } + poller, swapErr = client.BeginSwapSlot(ctx, resourceGroup, appName, sourceSlot, swapRequest, nil) + } + + if swapErr != nil { + return fmt.Errorf("starting slot swap: %w", swapErr) + } + + // Wait for completion + // Type assert to get the PollUntilDone method + switch p := poller.(type) { + case *runtime.Poller[armappservice.WebAppsClientSwapSlotWithProductionResponse]: + _, swapErr = p.PollUntilDone(ctx, nil) + case *runtime.Poller[armappservice.WebAppsClientSwapSlotResponse]: + _, swapErr = p.PollUntilDone(ctx, nil) + default: + return fmt.Errorf("unexpected poller type") + } + + if swapErr != nil { + return fmt.Errorf("waiting for slot swap to complete: %w", swapErr) + } + + return nil +} diff --git a/cli/azd/pkg/azdext/project.pb.go b/cli/azd/pkg/azdext/project.pb.go index 0970f6d714a..31a1d27db88 100644 --- a/cli/azd/pkg/azdext/project.pb.go +++ b/cli/azd/pkg/azdext/project.pb.go @@ -1009,11 +1009,103 @@ func (x *UnsetServiceConfigRequest) GetPath() string { return "" } +// Request message for GetServiceTargetResource +type GetServiceTargetResourceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of the service to resolve the target resource for + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetServiceTargetResourceRequest) Reset() { + *x = GetServiceTargetResourceRequest{} + mi := &file_project_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetServiceTargetResourceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetServiceTargetResourceRequest) ProtoMessage() {} + +func (x *GetServiceTargetResourceRequest) ProtoReflect() protoreflect.Message { + mi := &file_project_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetServiceTargetResourceRequest.ProtoReflect.Descriptor instead. +func (*GetServiceTargetResourceRequest) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{19} +} + +func (x *GetServiceTargetResourceRequest) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +// Response message for GetServiceTargetResource +type GetServiceTargetResourceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resolved target resource including subscription, resource group, resource name, and resource type + TargetResource *TargetResource `protobuf:"bytes,1,opt,name=target_resource,json=targetResource,proto3" json:"target_resource,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetServiceTargetResourceResponse) Reset() { + *x = GetServiceTargetResourceResponse{} + mi := &file_project_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetServiceTargetResourceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetServiceTargetResourceResponse) ProtoMessage() {} + +func (x *GetServiceTargetResourceResponse) ProtoReflect() protoreflect.Message { + mi := &file_project_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetServiceTargetResourceResponse.ProtoReflect.Descriptor instead. +func (*GetServiceTargetResourceResponse) Descriptor() ([]byte, []int) { + return file_project_proto_rawDescGZIP(), []int{20} +} + +func (x *GetServiceTargetResourceResponse) GetTargetResource() *TargetResource { + if x != nil { + return x.TargetResource + } + return nil +} + var File_project_proto protoreflect.FileDescriptor const file_project_proto_rawDesc = "" + "\n" + - "\rproject.proto\x12\x06azdext\x1a\fmodels.proto\x1a$include/google/protobuf/struct.proto\"E\n" + + "\rproject.proto\x12\x06azdext\x1a\fmodels.proto\x1a\x14service_target.proto\x1a$include/google/protobuf/struct.proto\"E\n" + "\x12GetProjectResponse\x12/\n" + "\aproject\x18\x01 \x01(\v2\x15.azdext.ProjectConfigR\aproject\"D\n" + "\x11AddServiceRequest\x12/\n" + @@ -1070,9 +1162,15 @@ const file_project_proto_rawDesc = "" + "\x05value\x18\x03 \x01(\v2\x16.google.protobuf.ValueR\x05value\"R\n" + "\x19UnsetServiceConfigRequest\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x12\n" + - "\x04path\x18\x02 \x01(\tR\x04path2\xad\t\n" + + "\x04path\x18\x02 \x01(\tR\x04path\"D\n" + + "\x1fGetServiceTargetResourceRequest\x12!\n" + + "\fservice_name\x18\x01 \x01(\tR\vserviceName\"c\n" + + " GetServiceTargetResourceResponse\x12?\n" + + "\x0ftarget_resource\x18\x01 \x01(\v2\x16.azdext.TargetResourceR\x0etargetResource2\x9c\n" + + "\n" + "\x0eProjectService\x127\n" + - "\x03Get\x12\x14.azdext.EmptyRequest\x1a\x1a.azdext.GetProjectResponse\x12>\n" + + "\x03Get\x12\x14.azdext.EmptyRequest\x1a\x1a.azdext.GetProjectResponse\x12m\n" + + "\x18GetServiceTargetResource\x12'.azdext.GetServiceTargetResourceRequest\x1a(.azdext.GetServiceTargetResourceResponse\x12>\n" + "\n" + "AddService\x12\x19.azdext.AddServiceRequest\x1a\x15.azdext.EmptyResponse\x12P\n" + "\x13GetResolvedServices\x12\x14.azdext.EmptyRequest\x1a#.azdext.GetResolvedServicesResponse\x12O\n" + @@ -1100,81 +1198,87 @@ func file_project_proto_rawDescGZIP() []byte { return file_project_proto_rawDescData } -var file_project_proto_msgTypes = make([]protoimpl.MessageInfo, 20) +var file_project_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_project_proto_goTypes = []any{ - (*GetProjectResponse)(nil), // 0: azdext.GetProjectResponse - (*AddServiceRequest)(nil), // 1: azdext.AddServiceRequest - (*GetResolvedServicesResponse)(nil), // 2: azdext.GetResolvedServicesResponse - (*ParseGitHubUrlRequest)(nil), // 3: azdext.ParseGitHubUrlRequest - (*GetProjectConfigSectionRequest)(nil), // 4: azdext.GetProjectConfigSectionRequest - (*GetProjectConfigSectionResponse)(nil), // 5: azdext.GetProjectConfigSectionResponse - (*ParseGitHubUrlResponse)(nil), // 6: azdext.ParseGitHubUrlResponse - (*GetProjectConfigValueRequest)(nil), // 7: azdext.GetProjectConfigValueRequest - (*GetProjectConfigValueResponse)(nil), // 8: azdext.GetProjectConfigValueResponse - (*SetProjectConfigSectionRequest)(nil), // 9: azdext.SetProjectConfigSectionRequest - (*SetProjectConfigValueRequest)(nil), // 10: azdext.SetProjectConfigValueRequest - (*UnsetProjectConfigRequest)(nil), // 11: azdext.UnsetProjectConfigRequest - (*GetServiceConfigSectionRequest)(nil), // 12: azdext.GetServiceConfigSectionRequest - (*GetServiceConfigSectionResponse)(nil), // 13: azdext.GetServiceConfigSectionResponse - (*GetServiceConfigValueRequest)(nil), // 14: azdext.GetServiceConfigValueRequest - (*GetServiceConfigValueResponse)(nil), // 15: azdext.GetServiceConfigValueResponse - (*SetServiceConfigSectionRequest)(nil), // 16: azdext.SetServiceConfigSectionRequest - (*SetServiceConfigValueRequest)(nil), // 17: azdext.SetServiceConfigValueRequest - (*UnsetServiceConfigRequest)(nil), // 18: azdext.UnsetServiceConfigRequest - nil, // 19: azdext.GetResolvedServicesResponse.ServicesEntry - (*ProjectConfig)(nil), // 20: azdext.ProjectConfig - (*ServiceConfig)(nil), // 21: azdext.ServiceConfig - (*structpb.Struct)(nil), // 22: google.protobuf.Struct - (*structpb.Value)(nil), // 23: google.protobuf.Value - (*EmptyRequest)(nil), // 24: azdext.EmptyRequest - (*EmptyResponse)(nil), // 25: azdext.EmptyResponse + (*GetProjectResponse)(nil), // 0: azdext.GetProjectResponse + (*AddServiceRequest)(nil), // 1: azdext.AddServiceRequest + (*GetResolvedServicesResponse)(nil), // 2: azdext.GetResolvedServicesResponse + (*ParseGitHubUrlRequest)(nil), // 3: azdext.ParseGitHubUrlRequest + (*GetProjectConfigSectionRequest)(nil), // 4: azdext.GetProjectConfigSectionRequest + (*GetProjectConfigSectionResponse)(nil), // 5: azdext.GetProjectConfigSectionResponse + (*ParseGitHubUrlResponse)(nil), // 6: azdext.ParseGitHubUrlResponse + (*GetProjectConfigValueRequest)(nil), // 7: azdext.GetProjectConfigValueRequest + (*GetProjectConfigValueResponse)(nil), // 8: azdext.GetProjectConfigValueResponse + (*SetProjectConfigSectionRequest)(nil), // 9: azdext.SetProjectConfigSectionRequest + (*SetProjectConfigValueRequest)(nil), // 10: azdext.SetProjectConfigValueRequest + (*UnsetProjectConfigRequest)(nil), // 11: azdext.UnsetProjectConfigRequest + (*GetServiceConfigSectionRequest)(nil), // 12: azdext.GetServiceConfigSectionRequest + (*GetServiceConfigSectionResponse)(nil), // 13: azdext.GetServiceConfigSectionResponse + (*GetServiceConfigValueRequest)(nil), // 14: azdext.GetServiceConfigValueRequest + (*GetServiceConfigValueResponse)(nil), // 15: azdext.GetServiceConfigValueResponse + (*SetServiceConfigSectionRequest)(nil), // 16: azdext.SetServiceConfigSectionRequest + (*SetServiceConfigValueRequest)(nil), // 17: azdext.SetServiceConfigValueRequest + (*UnsetServiceConfigRequest)(nil), // 18: azdext.UnsetServiceConfigRequest + (*GetServiceTargetResourceRequest)(nil), // 19: azdext.GetServiceTargetResourceRequest + (*GetServiceTargetResourceResponse)(nil), // 20: azdext.GetServiceTargetResourceResponse + nil, // 21: azdext.GetResolvedServicesResponse.ServicesEntry + (*ProjectConfig)(nil), // 22: azdext.ProjectConfig + (*ServiceConfig)(nil), // 23: azdext.ServiceConfig + (*structpb.Struct)(nil), // 24: google.protobuf.Struct + (*structpb.Value)(nil), // 25: google.protobuf.Value + (*TargetResource)(nil), // 26: azdext.TargetResource + (*EmptyRequest)(nil), // 27: azdext.EmptyRequest + (*EmptyResponse)(nil), // 28: azdext.EmptyResponse } var file_project_proto_depIdxs = []int32{ - 20, // 0: azdext.GetProjectResponse.project:type_name -> azdext.ProjectConfig - 21, // 1: azdext.AddServiceRequest.service:type_name -> azdext.ServiceConfig - 19, // 2: azdext.GetResolvedServicesResponse.services:type_name -> azdext.GetResolvedServicesResponse.ServicesEntry - 22, // 3: azdext.GetProjectConfigSectionResponse.section:type_name -> google.protobuf.Struct - 23, // 4: azdext.GetProjectConfigValueResponse.value:type_name -> google.protobuf.Value - 22, // 5: azdext.SetProjectConfigSectionRequest.section:type_name -> google.protobuf.Struct - 23, // 6: azdext.SetProjectConfigValueRequest.value:type_name -> google.protobuf.Value - 22, // 7: azdext.GetServiceConfigSectionResponse.section:type_name -> google.protobuf.Struct - 23, // 8: azdext.GetServiceConfigValueResponse.value:type_name -> google.protobuf.Value - 22, // 9: azdext.SetServiceConfigSectionRequest.section:type_name -> google.protobuf.Struct - 23, // 10: azdext.SetServiceConfigValueRequest.value:type_name -> google.protobuf.Value - 21, // 11: azdext.GetResolvedServicesResponse.ServicesEntry.value:type_name -> azdext.ServiceConfig - 24, // 12: azdext.ProjectService.Get:input_type -> azdext.EmptyRequest - 1, // 13: azdext.ProjectService.AddService:input_type -> azdext.AddServiceRequest - 24, // 14: azdext.ProjectService.GetResolvedServices:input_type -> azdext.EmptyRequest - 3, // 15: azdext.ProjectService.ParseGitHubUrl:input_type -> azdext.ParseGitHubUrlRequest - 4, // 16: azdext.ProjectService.GetConfigSection:input_type -> azdext.GetProjectConfigSectionRequest - 7, // 17: azdext.ProjectService.GetConfigValue:input_type -> azdext.GetProjectConfigValueRequest - 9, // 18: azdext.ProjectService.SetConfigSection:input_type -> azdext.SetProjectConfigSectionRequest - 10, // 19: azdext.ProjectService.SetConfigValue:input_type -> azdext.SetProjectConfigValueRequest - 11, // 20: azdext.ProjectService.UnsetConfig:input_type -> azdext.UnsetProjectConfigRequest - 12, // 21: azdext.ProjectService.GetServiceConfigSection:input_type -> azdext.GetServiceConfigSectionRequest - 14, // 22: azdext.ProjectService.GetServiceConfigValue:input_type -> azdext.GetServiceConfigValueRequest - 16, // 23: azdext.ProjectService.SetServiceConfigSection:input_type -> azdext.SetServiceConfigSectionRequest - 17, // 24: azdext.ProjectService.SetServiceConfigValue:input_type -> azdext.SetServiceConfigValueRequest - 18, // 25: azdext.ProjectService.UnsetServiceConfig:input_type -> azdext.UnsetServiceConfigRequest - 0, // 26: azdext.ProjectService.Get:output_type -> azdext.GetProjectResponse - 25, // 27: azdext.ProjectService.AddService:output_type -> azdext.EmptyResponse - 2, // 28: azdext.ProjectService.GetResolvedServices:output_type -> azdext.GetResolvedServicesResponse - 6, // 29: azdext.ProjectService.ParseGitHubUrl:output_type -> azdext.ParseGitHubUrlResponse - 5, // 30: azdext.ProjectService.GetConfigSection:output_type -> azdext.GetProjectConfigSectionResponse - 8, // 31: azdext.ProjectService.GetConfigValue:output_type -> azdext.GetProjectConfigValueResponse - 25, // 32: azdext.ProjectService.SetConfigSection:output_type -> azdext.EmptyResponse - 25, // 33: azdext.ProjectService.SetConfigValue:output_type -> azdext.EmptyResponse - 25, // 34: azdext.ProjectService.UnsetConfig:output_type -> azdext.EmptyResponse - 13, // 35: azdext.ProjectService.GetServiceConfigSection:output_type -> azdext.GetServiceConfigSectionResponse - 15, // 36: azdext.ProjectService.GetServiceConfigValue:output_type -> azdext.GetServiceConfigValueResponse - 25, // 37: azdext.ProjectService.SetServiceConfigSection:output_type -> azdext.EmptyResponse - 25, // 38: azdext.ProjectService.SetServiceConfigValue:output_type -> azdext.EmptyResponse - 25, // 39: azdext.ProjectService.UnsetServiceConfig:output_type -> azdext.EmptyResponse - 26, // [26:40] is the sub-list for method output_type - 12, // [12:26] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 22, // 0: azdext.GetProjectResponse.project:type_name -> azdext.ProjectConfig + 23, // 1: azdext.AddServiceRequest.service:type_name -> azdext.ServiceConfig + 21, // 2: azdext.GetResolvedServicesResponse.services:type_name -> azdext.GetResolvedServicesResponse.ServicesEntry + 24, // 3: azdext.GetProjectConfigSectionResponse.section:type_name -> google.protobuf.Struct + 25, // 4: azdext.GetProjectConfigValueResponse.value:type_name -> google.protobuf.Value + 24, // 5: azdext.SetProjectConfigSectionRequest.section:type_name -> google.protobuf.Struct + 25, // 6: azdext.SetProjectConfigValueRequest.value:type_name -> google.protobuf.Value + 24, // 7: azdext.GetServiceConfigSectionResponse.section:type_name -> google.protobuf.Struct + 25, // 8: azdext.GetServiceConfigValueResponse.value:type_name -> google.protobuf.Value + 24, // 9: azdext.SetServiceConfigSectionRequest.section:type_name -> google.protobuf.Struct + 25, // 10: azdext.SetServiceConfigValueRequest.value:type_name -> google.protobuf.Value + 26, // 11: azdext.GetServiceTargetResourceResponse.target_resource:type_name -> azdext.TargetResource + 23, // 12: azdext.GetResolvedServicesResponse.ServicesEntry.value:type_name -> azdext.ServiceConfig + 27, // 13: azdext.ProjectService.Get:input_type -> azdext.EmptyRequest + 19, // 14: azdext.ProjectService.GetServiceTargetResource:input_type -> azdext.GetServiceTargetResourceRequest + 1, // 15: azdext.ProjectService.AddService:input_type -> azdext.AddServiceRequest + 27, // 16: azdext.ProjectService.GetResolvedServices:input_type -> azdext.EmptyRequest + 3, // 17: azdext.ProjectService.ParseGitHubUrl:input_type -> azdext.ParseGitHubUrlRequest + 4, // 18: azdext.ProjectService.GetConfigSection:input_type -> azdext.GetProjectConfigSectionRequest + 7, // 19: azdext.ProjectService.GetConfigValue:input_type -> azdext.GetProjectConfigValueRequest + 9, // 20: azdext.ProjectService.SetConfigSection:input_type -> azdext.SetProjectConfigSectionRequest + 10, // 21: azdext.ProjectService.SetConfigValue:input_type -> azdext.SetProjectConfigValueRequest + 11, // 22: azdext.ProjectService.UnsetConfig:input_type -> azdext.UnsetProjectConfigRequest + 12, // 23: azdext.ProjectService.GetServiceConfigSection:input_type -> azdext.GetServiceConfigSectionRequest + 14, // 24: azdext.ProjectService.GetServiceConfigValue:input_type -> azdext.GetServiceConfigValueRequest + 16, // 25: azdext.ProjectService.SetServiceConfigSection:input_type -> azdext.SetServiceConfigSectionRequest + 17, // 26: azdext.ProjectService.SetServiceConfigValue:input_type -> azdext.SetServiceConfigValueRequest + 18, // 27: azdext.ProjectService.UnsetServiceConfig:input_type -> azdext.UnsetServiceConfigRequest + 0, // 28: azdext.ProjectService.Get:output_type -> azdext.GetProjectResponse + 20, // 29: azdext.ProjectService.GetServiceTargetResource:output_type -> azdext.GetServiceTargetResourceResponse + 28, // 30: azdext.ProjectService.AddService:output_type -> azdext.EmptyResponse + 2, // 31: azdext.ProjectService.GetResolvedServices:output_type -> azdext.GetResolvedServicesResponse + 6, // 32: azdext.ProjectService.ParseGitHubUrl:output_type -> azdext.ParseGitHubUrlResponse + 5, // 33: azdext.ProjectService.GetConfigSection:output_type -> azdext.GetProjectConfigSectionResponse + 8, // 34: azdext.ProjectService.GetConfigValue:output_type -> azdext.GetProjectConfigValueResponse + 28, // 35: azdext.ProjectService.SetConfigSection:output_type -> azdext.EmptyResponse + 28, // 36: azdext.ProjectService.SetConfigValue:output_type -> azdext.EmptyResponse + 28, // 37: azdext.ProjectService.UnsetConfig:output_type -> azdext.EmptyResponse + 13, // 38: azdext.ProjectService.GetServiceConfigSection:output_type -> azdext.GetServiceConfigSectionResponse + 15, // 39: azdext.ProjectService.GetServiceConfigValue:output_type -> azdext.GetServiceConfigValueResponse + 28, // 40: azdext.ProjectService.SetServiceConfigSection:output_type -> azdext.EmptyResponse + 28, // 41: azdext.ProjectService.SetServiceConfigValue:output_type -> azdext.EmptyResponse + 28, // 42: azdext.ProjectService.UnsetServiceConfig:output_type -> azdext.EmptyResponse + 28, // [28:43] is the sub-list for method output_type + 13, // [13:28] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_project_proto_init() } @@ -1183,13 +1287,14 @@ func file_project_proto_init() { return } file_models_proto_init() + file_service_target_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_project_proto_rawDesc), len(file_project_proto_rawDesc)), NumEnums: 0, - NumMessages: 20, + NumMessages: 22, NumExtensions: 0, NumServices: 1, }, diff --git a/cli/azd/pkg/azdext/project_grpc.pb.go b/cli/azd/pkg/azdext/project_grpc.pb.go index 2eb9b07475b..ae569273b71 100644 --- a/cli/azd/pkg/azdext/project_grpc.pb.go +++ b/cli/azd/pkg/azdext/project_grpc.pb.go @@ -22,20 +22,21 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - ProjectService_Get_FullMethodName = "/azdext.ProjectService/Get" - ProjectService_AddService_FullMethodName = "/azdext.ProjectService/AddService" - ProjectService_GetResolvedServices_FullMethodName = "/azdext.ProjectService/GetResolvedServices" - ProjectService_ParseGitHubUrl_FullMethodName = "/azdext.ProjectService/ParseGitHubUrl" - ProjectService_GetConfigSection_FullMethodName = "/azdext.ProjectService/GetConfigSection" - ProjectService_GetConfigValue_FullMethodName = "/azdext.ProjectService/GetConfigValue" - ProjectService_SetConfigSection_FullMethodName = "/azdext.ProjectService/SetConfigSection" - ProjectService_SetConfigValue_FullMethodName = "/azdext.ProjectService/SetConfigValue" - ProjectService_UnsetConfig_FullMethodName = "/azdext.ProjectService/UnsetConfig" - ProjectService_GetServiceConfigSection_FullMethodName = "/azdext.ProjectService/GetServiceConfigSection" - ProjectService_GetServiceConfigValue_FullMethodName = "/azdext.ProjectService/GetServiceConfigValue" - ProjectService_SetServiceConfigSection_FullMethodName = "/azdext.ProjectService/SetServiceConfigSection" - ProjectService_SetServiceConfigValue_FullMethodName = "/azdext.ProjectService/SetServiceConfigValue" - ProjectService_UnsetServiceConfig_FullMethodName = "/azdext.ProjectService/UnsetServiceConfig" + ProjectService_Get_FullMethodName = "/azdext.ProjectService/Get" + ProjectService_GetServiceTargetResource_FullMethodName = "/azdext.ProjectService/GetServiceTargetResource" + ProjectService_AddService_FullMethodName = "/azdext.ProjectService/AddService" + ProjectService_GetResolvedServices_FullMethodName = "/azdext.ProjectService/GetResolvedServices" + ProjectService_ParseGitHubUrl_FullMethodName = "/azdext.ProjectService/ParseGitHubUrl" + ProjectService_GetConfigSection_FullMethodName = "/azdext.ProjectService/GetConfigSection" + ProjectService_GetConfigValue_FullMethodName = "/azdext.ProjectService/GetConfigValue" + ProjectService_SetConfigSection_FullMethodName = "/azdext.ProjectService/SetConfigSection" + ProjectService_SetConfigValue_FullMethodName = "/azdext.ProjectService/SetConfigValue" + ProjectService_UnsetConfig_FullMethodName = "/azdext.ProjectService/UnsetConfig" + ProjectService_GetServiceConfigSection_FullMethodName = "/azdext.ProjectService/GetServiceConfigSection" + ProjectService_GetServiceConfigValue_FullMethodName = "/azdext.ProjectService/GetServiceConfigValue" + ProjectService_SetServiceConfigSection_FullMethodName = "/azdext.ProjectService/SetServiceConfigSection" + ProjectService_SetServiceConfigValue_FullMethodName = "/azdext.ProjectService/SetServiceConfigValue" + ProjectService_UnsetServiceConfig_FullMethodName = "/azdext.ProjectService/UnsetServiceConfig" ) // ProjectServiceClient is the client API for ProjectService service. @@ -46,6 +47,13 @@ const ( type ProjectServiceClient interface { // Gets the current project. Get(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*GetProjectResponse, error) + // GetServiceTargetResource resolves the target Azure resource for a service. + // This uses azd's standard resource discovery logic which: + // - Looks up resources by azd-service-name tag + // - Falls back to explicit resourceName from configuration + // - Resolves the resource group name from configuration or environment + // The returned TargetResource includes subscription, resource group, resource name, and resource type. + GetServiceTargetResource(ctx context.Context, in *GetServiceTargetResourceRequest, opts ...grpc.CallOption) (*GetServiceTargetResourceResponse, error) // AddService adds a new service to the project. AddService(ctx context.Context, in *AddServiceRequest, opts ...grpc.CallOption) (*EmptyResponse, error) // GetResolvedServices gets the resolved list of services after processing any importers (e.g., Aspire projects). @@ -93,6 +101,16 @@ func (c *projectServiceClient) Get(ctx context.Context, in *EmptyRequest, opts . return out, nil } +func (c *projectServiceClient) GetServiceTargetResource(ctx context.Context, in *GetServiceTargetResourceRequest, opts ...grpc.CallOption) (*GetServiceTargetResourceResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetServiceTargetResourceResponse) + err := c.cc.Invoke(ctx, ProjectService_GetServiceTargetResource_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *projectServiceClient) AddService(ctx context.Context, in *AddServiceRequest, opts ...grpc.CallOption) (*EmptyResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(EmptyResponse) @@ -231,6 +249,13 @@ func (c *projectServiceClient) UnsetServiceConfig(ctx context.Context, in *Unset type ProjectServiceServer interface { // Gets the current project. Get(context.Context, *EmptyRequest) (*GetProjectResponse, error) + // GetServiceTargetResource resolves the target Azure resource for a service. + // This uses azd's standard resource discovery logic which: + // - Looks up resources by azd-service-name tag + // - Falls back to explicit resourceName from configuration + // - Resolves the resource group name from configuration or environment + // The returned TargetResource includes subscription, resource group, resource name, and resource type. + GetServiceTargetResource(context.Context, *GetServiceTargetResourceRequest) (*GetServiceTargetResourceResponse, error) // AddService adds a new service to the project. AddService(context.Context, *AddServiceRequest) (*EmptyResponse, error) // GetResolvedServices gets the resolved list of services after processing any importers (e.g., Aspire projects). @@ -271,6 +296,9 @@ type UnimplementedProjectServiceServer struct{} func (UnimplementedProjectServiceServer) Get(context.Context, *EmptyRequest) (*GetProjectResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") } +func (UnimplementedProjectServiceServer) GetServiceTargetResource(context.Context, *GetServiceTargetResourceRequest) (*GetServiceTargetResourceResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetServiceTargetResource not implemented") +} func (UnimplementedProjectServiceServer) AddService(context.Context, *AddServiceRequest) (*EmptyResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AddService not implemented") } @@ -349,6 +377,24 @@ func _ProjectService_Get_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _ProjectService_GetServiceTargetResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetServiceTargetResourceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).GetServiceTargetResource(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_GetServiceTargetResource_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).GetServiceTargetResource(ctx, req.(*GetServiceTargetResourceRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _ProjectService_AddService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(AddServiceRequest) if err := dec(in); err != nil { @@ -594,6 +640,10 @@ var ProjectService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Get", Handler: _ProjectService_Get_Handler, }, + { + MethodName: "GetServiceTargetResource", + Handler: _ProjectService_GetServiceTargetResource_Handler, + }, { MethodName: "AddService", Handler: _ProjectService_AddService_Handler,