Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,8 @@ When `latest` is specified (or the version is omitted), `azd` selects the **high
| *"extension X not found"* | The extension ID is not present in any configured source. | Verify your sources with `azd extension source list`. Check the extension ID spelling. |
| *"found in multiple sources, specify exact source"* | The extension exists in two or more configured sources. | Use `azd extension install X --source <name>` to specify which source to use. |
| *"no matching version found"* | The version constraint excludes all available versions. | Check available versions with `azd extension show X`. Relax the constraint. |
| *"dependency X not found"* | A recursive dependency declared by the extension is missing from all sources. | Ensure the dependency is published to an accessible source. |
| *"dependency X not found"* | A recursive dependency is not installed and is missing from the parent extension's source. | Publish the dependency to the same source or install it explicitly before installing the parent. |
| *"no version satisfies constraint"* | The dependency exists, but none of its versions match the parent extension's constraint. | Publish a compatible dependency version or update the parent extension's constraint. |
| Stale version installed | The source cache has not expired yet, so `azd` is using an older manifest. | Set `AZD_EXTENSION_CACHE_TTL=0s` or delete files in `~/.azd/cache/extensions/`. |

### Diagnostic Steps
Expand Down
34 changes: 34 additions & 0 deletions cli/azd/pkg/extensions/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,24 @@ func (e *DependencyNotFoundError) Error() string {
return fmt.Sprintf("dependency %s required by %s was not found", e.DependencyId, e.ParentId)
}

// DependencyVersionNotFoundError indicates that a required dependency exists
// but none of its versions satisfy the constraint declared by its parent.
type DependencyVersionNotFoundError struct {
// DependencyId is the id of the dependency without a matching version.
DependencyId string
// ParentId is the id of the extension that declares the dependency.
ParentId string
// Constraint is the version constraint that could not be satisfied.
Constraint string
}

func (e *DependencyVersionNotFoundError) Error() string {
return fmt.Sprintf(
"dependency %s required by %s was found, but no version satisfies constraint %q",
e.DependencyId, e.ParentId, e.Constraint,
)
Comment on lines +76 to +80
}

// DependencyAmbiguousSourceError indicates that a required dependency of an
// extension was found in more than one configured source, so azd cannot decide
// which one to use. The caller must disambiguate by specifying an exact source.
Expand Down Expand Up @@ -630,6 +648,22 @@ func (m *Manager) installInternal(
}

if len(dependencyMatches) == 0 {
if dependency.Version != "" && !strings.EqualFold(dependency.Version, "latest") {
unconstrainedOptions := *dependencyOptions
unconstrainedOptions.Version = ""
unconstrainedMatches, err := m.FindExtensions(ctx, &unconstrainedOptions)
if err != nil {
return nil, fmt.Errorf("failed to find dependency %s: %w", dependency.Id, err)
}
if len(unconstrainedMatches) > 0 {
return nil, &DependencyVersionNotFoundError{
DependencyId: dependency.Id,
ParentId: extension.Id,
Constraint: dependency.Version,
}
}
}

return nil, &DependencyNotFoundError{DependencyId: dependency.Id, ParentId: extension.Id}
}

Expand Down
66 changes: 66 additions & 0 deletions cli/azd/pkg/extensions/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,72 @@ func Test_Install_PackDependency_SemverConstraint(t *testing.T) {
require.Equal(t, "0.1.31-preview", installed.Version)
}

func Test_Install_PackDependency_ErrorClassification(t *testing.T) {
t.Parallel()

tests := []struct {
name string
dependency *ExtensionMetadata
expectVersionConstraint bool
}{
{
name: "dependency not found",
},
{
name: "dependency version not found",
dependency: &ExtensionMetadata{
Id: "test.child",
Source: "local",
Versions: []ExtensionVersion{
{Version: "1.0.0", Artifacts: sampleArtifacts},
},
},
expectVersionConstraint: true,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
parent := &ExtensionMetadata{
Id: "test.pack",
Source: "local",
Versions: []ExtensionVersion{
{
Version: "1.0.0",
Dependencies: []ExtensionDependency{
{Id: "test.child", Version: ">=2.0.0"},
},
},
},
}
sourceExtensions := []*ExtensionMetadata{parent}
if test.dependency != nil {
sourceExtensions = append(sourceExtensions, test.dependency)
}

manager := newTestManager(t)
manager.sources = []Source{&mockSource{name: "local", extensions: sourceExtensions}}

_, err := manager.Install(t.Context(), parent, "")
require.Error(t, err)
if test.expectVersionConstraint {
versionErr, ok := errors.AsType[*DependencyVersionNotFoundError](err)
require.True(t, ok)
require.Equal(t, "test.child", versionErr.DependencyId)
require.Equal(t, "test.pack", versionErr.ParentId)
require.Equal(t, ">=2.0.0", versionErr.Constraint)
require.Contains(t, versionErr.Error(), "was found, but no version satisfies constraint")
return
}

notFoundErr, ok := errors.AsType[*DependencyNotFoundError](err)
require.True(t, ok)
require.Equal(t, "test.child", notFoundErr.DependencyId)
require.Equal(t, "test.pack", notFoundErr.ParentId)
})
}
}

func Test_Install_PackDependency_InstalledDependencyMustSatisfyConstraint(t *testing.T) {
mockContext := mocks.NewMockContext(t.Context())

Expand Down