diff --git a/src/Eryph.ComputeClient.Commands/Catlets/CatletCmdLet.cs b/src/Eryph.ComputeClient.Commands/Catlets/CatletCmdLet.cs
index 8eb9e36..1d806f9 100644
--- a/src/Eryph.ComputeClient.Commands/Catlets/CatletCmdLet.cs
+++ b/src/Eryph.ComputeClient.Commands/Catlets/CatletCmdLet.cs
@@ -1,4 +1,7 @@
-using Eryph.ComputeClient.Models;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Eryph.ComputeClient.Models;
using JetBrains.Annotations;
namespace Eryph.ComputeClient.Commands.Catlets
@@ -6,11 +9,45 @@ namespace Eryph.ComputeClient.Commands.Catlets
[PublicAPI]
public abstract class CatletCmdLet : ComputeCmdLet
{
+ /// Resource kind used in catlet not-found / ambiguity messages.
+ protected const string CatletResourceKind = "catlet";
+
+ ///
+ /// Hint shown when a catlet name is ambiguous, naming the parameter that
+ /// disambiguates it (a catlet name is unique only per project + environment).
+ ///
+ protected const string EnvironmentAmbiguityHint = "-Environment";
+
+ ///
+ /// The , but only when
+ /// was not already supplied. Once -Environment has been applied it did not resolve the
+ /// ambiguity, so suggesting it again would be misleading; return null in that case.
+ ///
+ protected static string EnvironmentHintIfUnset(string environment) =>
+ string.IsNullOrWhiteSpace(environment) ? EnvironmentAmbiguityHint : null;
+
protected Catlet GetSingleCatlet(string id)
{
return Factory.CreateCatletsClient().Get(id);
}
+ ///
+ /// Lists the catlets of a project, optionally narrowed to a single environment.
+ /// A catlet's name is unique only per project + environment, so a lookup by name
+ /// can match several catlets across environments; passing an environment restricts
+ /// the listing to that environment (case-insensitive exact match). An empty/null
+ /// environment applies no filter and lists every environment. As the server has no
+ /// environment filter, this is applied client-side, mirroring Get-VNetwork and
+ /// Get-CatletDisk. An explicit id (a GUID) bypasses this listing entirely.
+ ///
+ protected IEnumerable ListCatlets(string projectId, string environment)
+ {
+ IEnumerable catlets = Factory.CreateCatletsClient().List(projectId: projectId);
+ if (!string.IsNullOrWhiteSpace(environment))
+ catlets = catlets.Where(c => string.Equals(c.Environment, environment, StringComparison.OrdinalIgnoreCase));
+ return catlets;
+ }
+
protected void WaitForOperation(
Operation operation,
bool noWait,
diff --git a/src/Eryph.ComputeClient.Commands/Catlets/CatletGuestServiceGetCmdlet.cs b/src/Eryph.ComputeClient.Commands/Catlets/CatletGuestServiceGetCmdlet.cs
index 389f571..9a56c19 100644
--- a/src/Eryph.ComputeClient.Commands/Catlets/CatletGuestServiceGetCmdlet.cs
+++ b/src/Eryph.ComputeClient.Commands/Catlets/CatletGuestServiceGetCmdlet.cs
@@ -33,6 +33,10 @@ public abstract class CatletGuestServiceGetCmdlet : CatletCmdLet
[ValidateNotNullOrEmpty]
public string ProjectName { get; set; }
+ [Parameter(ParameterSetName = "list")]
+ [ValidateNotNullOrEmpty]
+ public string Environment { get; set; }
+
protected override void BeginProcessing()
{
base.BeginProcessing();
@@ -46,7 +50,7 @@ protected override void ProcessRecord()
foreach (var id in Id)
{
if (Stopping) break;
- if (TryGetById(id, GetSingleCatlet, "catlet", out var catlet))
+ if (TryGetById(id, GetSingleCatlet, CatletResourceKind, out var catlet))
EmitCatlet(catlet);
}
@@ -59,9 +63,9 @@ protected override void ProcessRecord()
WriteByNameOrId(
Name,
GetSingleCatlet,
- () => Factory.CreateCatletsClient().List(projectId: projectId),
+ () => ListCatlets(projectId, Environment),
catlet => catlet.Name,
- "catlet",
+ CatletResourceKind,
EmitCatlet);
}
diff --git a/src/Eryph.ComputeClient.Commands/Catlets/GetCatletCommand.cs b/src/Eryph.ComputeClient.Commands/Catlets/GetCatletCommand.cs
index 390edb4..ee87f4e 100644
--- a/src/Eryph.ComputeClient.Commands/Catlets/GetCatletCommand.cs
+++ b/src/Eryph.ComputeClient.Commands/Catlets/GetCatletCommand.cs
@@ -46,6 +46,10 @@ public class GetCatletCommand : CatletCmdLet
[ValidateNotNullOrEmpty]
public string Name { get; set; }
+ [Parameter(ParameterSetName = "list")]
+ [ValidateNotNullOrEmpty]
+ public string Environment { get; set; }
+
protected override void ProcessRecord()
{
@@ -57,12 +61,12 @@ protected override void ProcessRecord()
if (Config.IsPresent)
{
- if (TryGetById(id, i => Factory.CreateCatletsClient().GetConfig(i), "catlet", out var config))
+ if (TryGetById(id, i => Factory.CreateCatletsClient().GetConfig(i), CatletResourceKind, out var config))
WriteConfig(config);
}
else
{
- if (TryGetById(id, GetSingleCatlet, "catlet", out var catlet))
+ if (TryGetById(id, GetSingleCatlet, CatletResourceKind, out var catlet))
WriteObject(catlet);
}
}
@@ -90,9 +94,9 @@ protected override void ProcessRecord()
WriteByNameOrId(
Name,
GetSingleCatlet,
- () => Factory.CreateCatletsClient().List(projectId: projectId),
+ () => ListCatlets(projectId, Environment),
catlet => catlet.Name,
- "catlet");
+ CatletResourceKind);
}
private void WriteConfig(CatletConfiguration config)
diff --git a/src/Eryph.ComputeClient.Commands/Catlets/GetCatletIpCommand.cs b/src/Eryph.ComputeClient.Commands/Catlets/GetCatletIpCommand.cs
index 8c427c9..ee8b481 100644
--- a/src/Eryph.ComputeClient.Commands/Catlets/GetCatletIpCommand.cs
+++ b/src/Eryph.ComputeClient.Commands/Catlets/GetCatletIpCommand.cs
@@ -29,6 +29,10 @@ public class GetCatletIpCommand : CatletCmdLet
[ValidateNotNullOrEmpty]
public string ProjectName { get; set; }
+ [Parameter(ParameterSetName = "list")]
+ [ValidateNotNullOrEmpty]
+ public string Environment { get; set; }
+
[Parameter]
public SwitchParameter InternalIp { get; set; }
@@ -42,7 +46,7 @@ protected override void ProcessRecord()
foreach (var id in Id)
{
if (Stopping) break;
- if (TryGetById(id, GetSingleCatlet, "catlet", out var catlet))
+ if (TryGetById(id, GetSingleCatlet, CatletResourceKind, out var catlet))
WriteIp(catlet);
}
@@ -55,9 +59,9 @@ protected override void ProcessRecord()
WriteByNameOrId(
Name,
GetSingleCatlet,
- () => Factory.CreateCatletsClient().List(projectId: projectId),
+ () => ListCatlets(projectId, Environment),
catlet => catlet.Name,
- "catlet",
+ CatletResourceKind,
WriteIp);
}
diff --git a/src/Eryph.ComputeClient.Commands/Catlets/GetCatletProvisioningLogCommand.cs b/src/Eryph.ComputeClient.Commands/Catlets/GetCatletProvisioningLogCommand.cs
index 2156d16..edf259a 100644
--- a/src/Eryph.ComputeClient.Commands/Catlets/GetCatletProvisioningLogCommand.cs
+++ b/src/Eryph.ComputeClient.Commands/Catlets/GetCatletProvisioningLogCommand.cs
@@ -38,6 +38,10 @@ public class GetCatletProvisioningLogCommand : CatletCmdLet
[ValidateNotNullOrEmpty]
public string ProjectName { get; set; }
+ [Parameter(ParameterSetName = "list")]
+ [ValidateNotNullOrEmpty]
+ public string Environment { get; set; }
+
///
/// Emit the rendered, human-readable text log instead of the structured events.
///
@@ -57,7 +61,7 @@ protected override void ProcessRecord()
foreach (var id in Id)
{
if (Stopping) break;
- if (TryGetById(id, GetSingleCatlet, "catlet", out var catlet))
+ if (TryGetById(id, GetSingleCatlet, CatletResourceKind, out var catlet))
EmitLog(catlet);
}
@@ -70,9 +74,9 @@ protected override void ProcessRecord()
WriteByNameOrId(
Name,
GetSingleCatlet,
- () => Factory.CreateCatletsClient().List(projectId: projectId),
+ () => ListCatlets(projectId, Environment),
catlet => catlet.Name,
- "catlet",
+ CatletResourceKind,
EmitLog);
}
@@ -92,7 +96,7 @@ private void EmitLog(Catlet catlet)
// Prefix a header so the text blocks stay attributable to their catlet
// when several catlets are targeted in one invocation.
var header = $"# Catlet {catlet.Name} ({catlet.Id})";
- WriteObject(header + Environment.NewLine + result.RenderedLog);
+ WriteObject(header + System.Environment.NewLine + result.RenderedLog);
return;
}
diff --git a/src/Eryph.ComputeClient.Commands/Catlets/GetCatletProvisioningStatusCommand.cs b/src/Eryph.ComputeClient.Commands/Catlets/GetCatletProvisioningStatusCommand.cs
index 482b21a..c8b14a2 100644
--- a/src/Eryph.ComputeClient.Commands/Catlets/GetCatletProvisioningStatusCommand.cs
+++ b/src/Eryph.ComputeClient.Commands/Catlets/GetCatletProvisioningStatusCommand.cs
@@ -35,6 +35,10 @@ public class GetCatletProvisioningStatusCommand : CatletCmdLet
[ValidateNotNullOrEmpty]
public string ProjectName { get; set; }
+ [Parameter(ParameterSetName = "list")]
+ [ValidateNotNullOrEmpty]
+ public string Environment { get; set; }
+
protected override void BeginProcessing()
{
base.BeginProcessing();
@@ -48,7 +52,7 @@ protected override void ProcessRecord()
foreach (var id in Id)
{
if (Stopping) break;
- if (TryGetById(id, GetSingleCatlet, "catlet", out var catlet))
+ if (TryGetById(id, GetSingleCatlet, CatletResourceKind, out var catlet))
WriteStatus(catlet);
}
@@ -61,9 +65,9 @@ protected override void ProcessRecord()
WriteByNameOrId(
Name,
GetSingleCatlet,
- () => Factory.CreateCatletsClient().List(projectId: projectId),
+ () => ListCatlets(projectId, Environment),
catlet => catlet.Name,
- "catlet",
+ CatletResourceKind,
WriteStatus);
}
diff --git a/src/Eryph.ComputeClient.Commands/Catlets/RemoveCatletCommand.cs b/src/Eryph.ComputeClient.Commands/Catlets/RemoveCatletCommand.cs
index 5c36433..9b2d307 100644
--- a/src/Eryph.ComputeClient.Commands/Catlets/RemoveCatletCommand.cs
+++ b/src/Eryph.ComputeClient.Commands/Catlets/RemoveCatletCommand.cs
@@ -55,6 +55,10 @@ public SwitchParameter NoWait
[ValidateNotNullOrEmpty]
public string ProjectName { get; set; }
+ [Parameter]
+ [ValidateNotNullOrEmpty]
+ public string Environment { get; set; }
+
private bool _force;
private bool _nowait;
private bool _passThru;
@@ -68,7 +72,7 @@ protected override void ProcessRecord()
foreach (var nameOrId in Id)
{
foreach (var catlet in ResolveActionTargets(nameOrId, ProjectName, GetSingleCatlet,
- projectId => Factory.CreateCatletsClient().List(projectId: projectId), c => c.Name, "catlet"))
+ projectId => ListCatlets(projectId, Environment), c => c.Name, CatletResourceKind, EnvironmentHintIfUnset(Environment)))
{
if (Stopping) break;
diff --git a/src/Eryph.ComputeClient.Commands/Catlets/RemoveCatletGuestServiceAccessKeyCommand.cs b/src/Eryph.ComputeClient.Commands/Catlets/RemoveCatletGuestServiceAccessKeyCommand.cs
index 628acc5..900de8f 100644
--- a/src/Eryph.ComputeClient.Commands/Catlets/RemoveCatletGuestServiceAccessKeyCommand.cs
+++ b/src/Eryph.ComputeClient.Commands/Catlets/RemoveCatletGuestServiceAccessKeyCommand.cs
@@ -23,6 +23,10 @@ public class RemoveCatletGuestServiceAccessKeyCommand : CatletCmdLet
[ValidateNotNullOrEmpty]
public string ProjectName { get; set; }
+ [Parameter]
+ [ValidateNotNullOrEmpty]
+ public string Environment { get; set; }
+
[Parameter]
public SwitchParameter Force
{
@@ -64,7 +68,7 @@ protected override void ProcessRecord()
foreach (var nameOrId in Id)
{
foreach (var catlet in ResolveActionTargets(nameOrId, ProjectName, GetSingleCatlet,
- projectId => Factory.CreateCatletsClient().List(projectId: projectId), c => c.Name, "catlet"))
+ projectId => ListCatlets(projectId, Environment), c => c.Name, CatletResourceKind, EnvironmentHintIfUnset(Environment)))
{
if (Stopping) break;
diff --git a/src/Eryph.ComputeClient.Commands/Catlets/SetCatletGuestServiceConfigCommand.cs b/src/Eryph.ComputeClient.Commands/Catlets/SetCatletGuestServiceConfigCommand.cs
index 0e9201d..3dc76e2 100644
--- a/src/Eryph.ComputeClient.Commands/Catlets/SetCatletGuestServiceConfigCommand.cs
+++ b/src/Eryph.ComputeClient.Commands/Catlets/SetCatletGuestServiceConfigCommand.cs
@@ -23,6 +23,10 @@ public class SetCatletGuestServiceConfigCommand : CatletCmdLet
[ValidateNotNullOrEmpty]
public string ProjectName { get; set; }
+ [Parameter]
+ [ValidateNotNullOrEmpty]
+ public string Environment { get; set; }
+
///
/// Shell command for interactive SSH sessions. An empty string clears the
/// override; omitting the parameter leaves the current value unchanged.
@@ -84,7 +88,7 @@ protected override void ProcessRecord()
foreach (var nameOrId in Id)
{
foreach (var catlet in ResolveActionTargets(nameOrId, ProjectName, GetSingleCatlet,
- projectId => Factory.CreateCatletsClient().List(projectId: projectId), c => c.Name, "catlet"))
+ projectId => ListCatlets(projectId, Environment), c => c.Name, CatletResourceKind, EnvironmentHintIfUnset(Environment)))
{
if (Stopping) break;
diff --git a/src/Eryph.ComputeClient.Commands/Catlets/StartCatletCommand.cs b/src/Eryph.ComputeClient.Commands/Catlets/StartCatletCommand.cs
index 579392b..1d2180a 100644
--- a/src/Eryph.ComputeClient.Commands/Catlets/StartCatletCommand.cs
+++ b/src/Eryph.ComputeClient.Commands/Catlets/StartCatletCommand.cs
@@ -44,6 +44,10 @@ public SwitchParameter NoWait
[ValidateNotNullOrEmpty]
public string ProjectName { get; set; }
+ [Parameter]
+ [ValidateNotNullOrEmpty]
+ public string Environment { get; set; }
+
private bool _force;
private bool _nowait;
private bool _yesToAll, _noToAll;
@@ -54,7 +58,7 @@ protected override void ProcessRecord()
foreach (var nameOrId in Id)
{
foreach (var catlet in ResolveActionTargets(nameOrId, ProjectName, GetSingleCatlet,
- projectId => Factory.CreateCatletsClient().List(projectId: projectId), c => c.Name, "catlet"))
+ projectId => ListCatlets(projectId, Environment), c => c.Name, CatletResourceKind, EnvironmentHintIfUnset(Environment)))
{
if (Stopping) break;
diff --git a/src/Eryph.ComputeClient.Commands/Catlets/StopCatletCommand.cs b/src/Eryph.ComputeClient.Commands/Catlets/StopCatletCommand.cs
index 434735c..5a4e7c9 100644
--- a/src/Eryph.ComputeClient.Commands/Catlets/StopCatletCommand.cs
+++ b/src/Eryph.ComputeClient.Commands/Catlets/StopCatletCommand.cs
@@ -37,6 +37,10 @@ public class StopCatletCommand : CatletCmdLet
[ValidateNotNullOrEmpty]
public string ProjectName { get; set; }
+ [Parameter]
+ [ValidateNotNullOrEmpty]
+ public string Environment { get; set; }
+
private bool _yesToAll;
private bool _noToAll;
private bool _yesToKillAll;
@@ -55,7 +59,7 @@ protected override void ProcessRecord()
foreach (var nameOrId in Id)
{
foreach (var catlet in ResolveActionTargets(nameOrId, ProjectName, GetSingleCatlet,
- projectId => Factory.CreateCatletsClient().List(projectId: projectId), c => c.Name, "catlet"))
+ projectId => ListCatlets(projectId, Environment), c => c.Name, CatletResourceKind, EnvironmentHintIfUnset(Environment)))
{
if (Stopping) break;
diff --git a/src/Eryph.ComputeClient.Commands/Catlets/UpdateCatletCommand.cs b/src/Eryph.ComputeClient.Commands/Catlets/UpdateCatletCommand.cs
index 481d433..6fb1d28 100644
--- a/src/Eryph.ComputeClient.Commands/Catlets/UpdateCatletCommand.cs
+++ b/src/Eryph.ComputeClient.Commands/Catlets/UpdateCatletCommand.cs
@@ -31,6 +31,10 @@ public class UpdateCatletCommand : CatletConfigCmdlet
[ValidateNotNullOrEmpty]
public string ProjectName { get; set; }
+ [Parameter]
+ [ValidateNotNullOrEmpty]
+ public string Environment { get; set; }
+
protected override void ProcessRecord()
{
var client = Factory.CreateCatletsClient();
@@ -38,7 +42,7 @@ protected override void ProcessRecord()
foreach (var nameOrId in Id)
{
foreach (var catlet in ResolveActionTargets(nameOrId, ProjectName, GetSingleCatlet,
- projectId => client.List(projectId: projectId), c => c.Name, "catlet"))
+ projectId => ListCatlets(projectId, Environment), c => c.Name, CatletResourceKind, EnvironmentHintIfUnset(Environment)))
{
if (Stopping) break;
diff --git a/src/Eryph.ComputeClient.Commands/ComputeCmdLet.cs b/src/Eryph.ComputeClient.Commands/ComputeCmdLet.cs
index 9cba64b..efbc084 100644
--- a/src/Eryph.ComputeClient.Commands/ComputeCmdLet.cs
+++ b/src/Eryph.ComputeClient.Commands/ComputeCmdLet.cs
@@ -236,11 +236,13 @@ protected IEnumerable ResolveByNameOrId(
///
/// Resolves the target(s) of a mutating cmdlet from a value that may be a resource
- /// id (a GUID) or a name. An id is used directly. A name must be scoped to a
- /// project ( is required) so that it cannot fan out
- /// across projects; within the project a wildcard may still match several
- /// resources. This keeps destructive operations from acting on same-named
- /// resources in other projects.
+ /// id (a GUID) or a name. An id is used directly. An exact name is resolved across
+ /// every project the caller can access; (and filters
+ /// such as -Environment) only narrow the search. Because some names are unique only
+ /// per project + environment, an exact name that resolves to more than one resource
+ /// is rejected as ambiguous rather than mutated, so a mutation never silently fans
+ /// out. A wildcard, which matches many by design, still requires
+ /// so it cannot fan out across projects.
///
///
/// Like this is a deferred iterator that calls
@@ -255,51 +257,77 @@ protected IEnumerable ResolveActionTargets(
string resourceKind,
string ambiguityHint = null)
{
- if (IsResourceId(nameOrId))
- {
- if (TryGetById(nameOrId, getById, resourceKind, out var item))
- yield return item;
- yield break;
- }
-
- if (string.IsNullOrWhiteSpace(projectName))
+ // An empty/whitespace target must never be treated as a name: as an empty
+ // filter it would match every resource, and since an exact name now resolves
+ // across all accessible projects, a stray empty array element (e.g. piped from
+ // an object with a blank name) could silently act on an unrelated resource.
+ if (string.IsNullOrWhiteSpace(nameOrId))
{
WriteError(new ErrorRecord(
- new PSArgumentException(
- $"When a {resourceKind} is identified by name, -ProjectName is required "
- + "so the name can be resolved within a single project. Specify -ProjectName, "
- + "or pass the id instead."),
- "ProjectNameRequiredForNameLookup",
+ new PSArgumentException($"A {resourceKind} name or id must be specified; it must not be empty."),
+ "EmptyTarget",
ErrorCategory.InvalidArgument,
nameOrId));
yield break;
}
- if (!TryGetProjectId(projectName, out var projectId))
+ if (IsResourceId(nameOrId))
+ {
+ if (TryGetById(nameOrId, getById, resourceKind, out var item))
+ yield return item;
yield break;
+ }
- // A wildcard may intentionally match several resources. An exact name, however,
- // must resolve to a single target before a mutation: some resource names are
- // unique only per project + environment, so an exact name could otherwise match
- // several resources and the command would act on all of them.
+ var hasProject = !string.IsNullOrWhiteSpace(projectName);
+
+ // A wildcard matches many resources by design, so the ambiguity guard below
+ // cannot protect it. Without a project to scope it, it could fan out across
+ // every project the caller can access, so a wildcard still requires -ProjectName.
if (WildcardPattern.ContainsWildcardCharacters(nameOrId))
{
- foreach (var item in FilterByName(listInProject(projectId), nameOrId, nameSelector, resourceKind))
+ if (!hasProject)
+ {
+ WriteError(new ErrorRecord(
+ new PSArgumentException(
+ $"A wildcard {resourceKind} name requires -ProjectName so it cannot fan out "
+ + "across projects. Specify -ProjectName, or use an exact name or the id."),
+ "ProjectNameRequiredForWildcard",
+ ErrorCategory.InvalidArgument,
+ nameOrId));
+ yield break;
+ }
+
+ if (!TryGetProjectId(projectName, out var wildcardProjectId))
+ yield break;
+
+ foreach (var item in FilterByName(listInProject(wildcardProjectId), nameOrId, nameSelector, resourceKind))
yield return item;
yield break;
}
+ // An exact name needs no project: a null projectId lists across all projects.
+ // -ProjectName only narrows the search when supplied.
+ string projectId = null;
+ if (hasProject && !TryGetProjectId(projectName, out projectId))
+ yield break;
+
var matches = FilterByName(listInProject(projectId), nameOrId, nameSelector, resourceKind).ToList();
if (matches.Count > 1)
{
- var hint = string.IsNullOrWhiteSpace(ambiguityHint)
- ? "Specify the id to select one."
- : $"Narrow the selection with {ambiguityHint} or specify the id.";
+ var narrowers = new List();
+ if (!hasProject)
+ narrowers.Add("-ProjectName");
+ if (!string.IsNullOrWhiteSpace(ambiguityHint))
+ narrowers.Add(ambiguityHint);
+ var hint = narrowers.Count > 0
+ ? $"Narrow the selection with {string.Join(" / ", narrowers)} or specify the id."
+ : "Specify the id to select one.";
+ var scope = hasProject ? $" in project '{projectName}'" : "";
WriteError(new ErrorRecord(
new PSArgumentException(
- $"The {resourceKind} name '{nameOrId}' is ambiguous in project '{projectName}': "
+ $"The {resourceKind} name '{nameOrId}' is ambiguous{scope}: "
+ $"it matches {matches.Count} resources. {hint}"),
- "AmbiguousNameInProject",
+ "AmbiguousName",
ErrorCategory.InvalidArgument,
nameOrId));
yield break;
diff --git a/test/pester/ResourceNameLookup.Tests.ps1 b/test/pester/ResourceNameLookup.Tests.ps1
index 2cc996c..7f67923 100644
--- a/test/pester/ResourceNameLookup.Tests.ps1
+++ b/test/pester/ResourceNameLookup.Tests.ps1
@@ -39,6 +39,10 @@ Describe 'Get-* -Name parameter surface (no server required)' {
@{ Cmd = 'Get-CatletIp' }
@{ Cmd = 'Get-VNetwork' }
@{ Cmd = 'Get-CatletDisk' }
+ @{ Cmd = 'Get-CatletGuestServiceStatus' }
+ @{ Cmd = 'Get-CatletGuestServiceConfig' }
+ @{ Cmd = 'Get-CatletProvisioningStatus' }
+ @{ Cmd = 'Get-CatletProvisioningLog' }
) {
$p = (Get-Command $Cmd).Parameters['Name']
$p | Should -Not -BeNullOrEmpty
@@ -53,6 +57,10 @@ Describe 'Get-* -Name parameter surface (no server required)' {
@{ Cmd = 'Get-CatletIp' }
@{ Cmd = 'Get-VNetwork' }
@{ Cmd = 'Get-CatletDisk' }
+ @{ Cmd = 'Get-CatletGuestServiceStatus' }
+ @{ Cmd = 'Get-CatletGuestServiceConfig' }
+ @{ Cmd = 'Get-CatletProvisioningStatus' }
+ @{ Cmd = 'Get-CatletProvisioningLog' }
) {
(Get-Command $Cmd).Parameters['Name'].ParameterSets['list'].Position | Should -Be 0
}
@@ -74,6 +82,12 @@ Describe 'Get-* -Name parameter surface (no server required)' {
It " exposes an -Environment filter in the 'list' set" -ForEach @(
@{ Cmd = 'Get-VNetwork' }
@{ Cmd = 'Get-CatletDisk' }
+ @{ Cmd = 'Get-Catlet' }
+ @{ Cmd = 'Get-CatletIp' }
+ @{ Cmd = 'Get-CatletGuestServiceStatus' }
+ @{ Cmd = 'Get-CatletGuestServiceConfig' }
+ @{ Cmd = 'Get-CatletProvisioningStatus' }
+ @{ Cmd = 'Get-CatletProvisioningLog' }
) {
$p = (Get-Command $Cmd).Parameters['Environment']
$p | Should -Not -BeNullOrEmpty
@@ -95,6 +109,10 @@ Describe 'Get-* -Name parameter surface (no server required)' {
@{ Cmd = 'Get-CatletIp' }
@{ Cmd = 'Get-VNetwork' }
@{ Cmd = 'Get-CatletSpecification' }
+ @{ Cmd = 'Get-CatletGuestServiceStatus' }
+ @{ Cmd = 'Get-CatletGuestServiceConfig' }
+ @{ Cmd = 'Get-CatletProvisioningStatus' }
+ @{ Cmd = 'Get-CatletProvisioningLog' }
) {
$p = (Get-Command $Cmd).Parameters['ProjectName']
$p | Should -Not -BeNullOrEmpty
@@ -129,15 +147,32 @@ Describe 'Action cmdlets accept name-or-id (parameter surface)' {
$positions | Should -Contain 0
}
- It " exposes a -ProjectName parameter to scope name resolution" -ForEach @(
+ It " exposes an optional -ProjectName parameter to narrow name resolution" -ForEach @(
@{ Cmd = 'Start-Catlet' }
@{ Cmd = 'Stop-Catlet' }
@{ Cmd = 'Remove-Catlet' }
@{ Cmd = 'Update-Catlet' }
+ @{ Cmd = 'Set-CatletGuestServiceConfig' }
+ @{ Cmd = 'Remove-CatletGuestServiceAccessKey' }
@{ Cmd = 'Remove-CatletSpecification' }
@{ Cmd = 'Update-CatletSpecification' }
) {
- (Get-Command $Cmd).Parameters['ProjectName'] | Should -Not -BeNullOrEmpty
+ $p = (Get-Command $Cmd).Parameters['ProjectName']
+ $p | Should -Not -BeNullOrEmpty
+ # -ProjectName is a narrowing filter, not required: an exact name resolves across
+ # projects, so it must not be mandatory in any parameter set.
+ $p.ParameterSets.Values.IsMandatory | Should -Not -Contain $true
+ }
+
+ It " exposes an -Environment parameter to narrow name resolution" -ForEach @(
+ @{ Cmd = 'Start-Catlet' }
+ @{ Cmd = 'Stop-Catlet' }
+ @{ Cmd = 'Remove-Catlet' }
+ @{ Cmd = 'Update-Catlet' }
+ @{ Cmd = 'Set-CatletGuestServiceConfig' }
+ @{ Cmd = 'Remove-CatletGuestServiceAccessKey' }
+ ) {
+ (Get-Command $Cmd).Parameters['Environment'] | Should -Not -BeNullOrEmpty
}
It 'Remove-CatletDisk is Id-only (Name+Location+DataStore is required to identify a disk; use Get-CatletDisk to resolve)' {
@@ -228,6 +263,15 @@ Describe 'Get-Catlet name-or-id (integration, read-only)' -Skip:(-not $eryphAvai
Get-Catlet -Name "zzzz-no-such-catlet-*" | Should -BeNullOrEmpty
}
+ It 'filters catlets by environment (and excludes other environments)' {
+ if ($existing.Count -eq 0) { Set-ItResult -Skipped -Because 'no catlets present'; return }
+ $environment = $existing[0].Environment
+ $filtered = @(Get-Catlet -Environment $environment)
+ $filtered | ForEach-Object { $_.Environment | Should -Be $environment }
+ $expected = @($existing | Where-Object Environment -EQ $environment).Count
+ $filtered.Count | Should -Be $expected
+ }
+
It 'treats a whitespace-only name as a name (not "list all")' {
# ' ' must not be interpreted as an omitted filter; it is an exact,
# non-matching name and so produces a not-found error, not every catlet.
@@ -273,9 +317,19 @@ Describe 'Get-CatletIp name-or-id (integration, read-only)' -Skip:(-not $eryphAv
Describe 'Action cmdlets name resolution (integration, non-destructive)' -Skip:(-not $eryphAvailable) {
- It 'requires -ProjectName when the target is given by name (no cross-project fan-out)' {
- { Start-Catlet -Name 'some-catlet' -ErrorAction Stop } |
- Should -Throw -ExpectedMessage '*ProjectName is required*'
+ BeforeAll { $existing = @(Get-Catlet) }
+
+ It 'resolves an exact name without -ProjectName (searches across projects)' {
+ # An exact name no longer requires -ProjectName: it is resolved across every project
+ # the caller can access. A random, non-existent name must therefore fail with a
+ # not-found error, NOT a "ProjectName is required" error.
+ { Start-Catlet -Name "zzz-$([guid]::NewGuid().ToString('N'))" -ErrorAction Stop } |
+ Should -Throw -ExpectedMessage '*Cannot find*'
+ }
+
+ It 'requires -ProjectName for a WILDCARD name (no cross-project fan-out)' {
+ { Start-Catlet -Name 'zzz-no-such-*' -ErrorAction Stop } |
+ Should -Throw -ExpectedMessage '*-ProjectName*'
}
It 'does not require -ProjectName when the target is given by id' {
@@ -299,6 +353,22 @@ Describe 'Action cmdlets name resolution (integration, non-destructive)' -Skip:(
{ Remove-Catlet -Name 'zzzz-no-such-catlet-*' -ProjectName 'default' -Force -ErrorAction Stop } |
Should -Not -Throw
}
+
+ It 'rejects an empty target instead of matching every catlet' {
+ # An empty name/id element must error, not fall through to an empty filter that
+ # would match (and act on) every catlet the caller can see.
+ { Start-Catlet -Id '' -ErrorAction Stop } | Should -Throw
+ }
+
+ It 'errors as ambiguous (with a narrowing hint) when an exact name matches several catlets' {
+ # A catlet name is unique only per project + environment, so the same name can exist
+ # in several environments/projects. Use existing data if it contains a duplicate
+ # name; the suite does not create catlets, so skip when none is present.
+ $dup = $existing | Group-Object Name | Where-Object Count -GT 1 | Select-Object -First 1
+ if (-not $dup) { Set-ItResult -Skipped -Because 'no duplicate catlet name present'; return }
+ { Start-Catlet -Name $dup.Name -ErrorAction Stop } |
+ Should -Throw -ExpectedMessage '*ambiguous*'
+ }
}
Describe 'Get-VNetwork name-or-id / -Environment (integration, read-only)' -Skip:(-not $eryphAvailable) {