Skip to content

(DSC) Idiomaticize resource schemas - #2040

Open
Mikey Lombardi (He/Him) (michaeltlombardi) wants to merge 4 commits into
PowerShell:masterfrom
michaeltlombardi:dsc/main/idiomaticize-resource-schemas
Open

Mikey Lombardi (He/Him) (michaeltlombardi) wants to merge 4 commits into
PowerShell:masterfrom
michaeltlombardi:dsc/main/idiomaticize-resource-schemas

Conversation

@michaeltlombardi

Copy link
Copy Markdown

PR Summary

This change:

  1. Removes null from the type keyword for optional resource properties that don't explicitly model null as a valid state.
  2. Updates the Microsoft.PowerShell.PSResourceGet/Repository schema to define required at the top-level to indicate that name is always required and simplifies the allOf keyword for the remaining case (when _exist is true then uri is required).
  3. Updates the serialization for the resource types to idiomatically omit optional undefined properties and consolidates standardized trace messaging for the serialization process.
  4. Updates the export functionality to gracefully handle cases where a scope has zero installed PSResources instead of emitting an unhandled (non-terminating) error and returning invalid data that fails schema validation in DSC.

PR Context

Prior to this change, there were a few issues with the DSC resource schemas regarding the idiomatic definitions and semantics of JSON schema:

  1. Optional properties should not define the type keyword with null as a valid data type unless null is an explicitly modeled state for the property.

    In JSON Schema, the absence of a property in an object is semantically distinct from a property explicitly set to null.

    Instead, we control the optionality of properties through the required or dependentRequired keywords.

  2. The schema for Microsoft.PowerShell.PSResourceGet/Repository didn't define required at the top-level. Instead, it used the allOf keyword to indicate whether the name and uri properties are required or just name.

    When a property is always required, we should hoist it to the top-level required keyword array. We can still use the allOf to conditionally extend the schema as needed.

    Parsing a JSON Schema is difficult for consumers, moreso when it requires unrolling schema composition. We should surface as much information to the consumer in the simplest form possible.

PR Checklist

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate export and schema-validation issues remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR updates DSC schemas and PSResource serialization/export behavior.

Changes:

  • Removes unnecessary nullable types.
  • Simplifies repository schema requirements.
  • Refactors serialization and empty-scope export handling.
File summaries
File Summary and final review comments
src/dsc/repository.dsc.resource.json Refines repository requirements. Moderate (1 vote): Require _exist inside the if schema so absent _exist does not trigger the then branch.
src/dsc/psresourcelist.dsc.resource.json Removes unnecessary nullable property types.
src/dsc/psresourceget.ps1 Updates serialization and export behavior. Critical (3 votes): Single-resource scopes are incorrectly treated as empty at lines 913 and 927. Moderate (3 votes): Empty repositoryName omits a required property. Moderate (3 votes): Catch-all exception handling hides failures as empty results. Nit (1 vote): Add tests for empty scopes and unexpected query failures.
Review details

Suppressed comments (3)

src/dsc/psresourceget.ps1:927

  • When a scope contains exactly one resource, .Count is 1, so this branch treats the scope as empty and omits that resource from the export. Test for a non-empty collection instead.
    if ($currentUserPSResources.count -gt 1) {

src/dsc/psresourceget.ps1:863

  • The new empty-scope and error-handling branches are not exercised by the DSC tests: the existing export test installs testmodule99 and only asserts a non-empty exported list. Add coverage for a scope with no installed resources (and for an unexpected query failure) so the new fallback cannot regress into invalid or silently incomplete export output.
    try {
        Get-PSResource -Scope $Scope -ErrorAction Stop
    } catch [Microsoft.PowerShell.PSResourceGet.UtilClasses.ResourceNotFoundException] {
        # Everything is fine, there's just no installed resources
        @()
    } catch {
        Write-Trace -level error -message "Failed to get PSResources for '$Scope' scope: $_"
        @()
    }

src/dsc/repository.dsc.resource.json:83

  • Because if only has properties, it also evaluates true when _exist is absent (properties does not require the property). The then branch therefore requires uri for inputs such as { "name": "repo" }, even though the condition is supposed to apply only when _exist is true. Add required: ["_exist"] inside the if schema.
                                "const": true
  • Files reviewed: 3/3 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/dsc/psresourceget.ps1 Outdated
Comment thread src/dsc/psresourceget.ps1
Comment thread src/dsc/psresourceget.ps1
Comment on lines +860 to +863
} catch {
Write-Trace -level error -message "Failed to get PSResources for '$Scope' scope: $_"
@()
}
@Gijsreyn

Copy link
Copy Markdown
Contributor

Mikey Lombardi (He/Him) (@michaeltlombardi) - again, well done on the work you did here! I did a quick review, and yes, I agree with the direction. I checked the branch against what the DSC engine (v3.3.0) actually enforces and found a few things worth fixing before merging. Also, a small heads-up: it might need a rebase after a couple of PRs land in master. Two points:

  1. Repository schema: name-only input becomes invalid (regression)

"if": { "properties": { "_exist": { "const": true } } } passes vacuously when _exist is absent, so then requires uri for every input that omits _exist. DSC validates the instance against the resource schema before get, set and delete, so these now fail (checked with Test-Json):

Input master this PR
{"name":"x"} (typical dsc resource get / delete) valid invalid
{"name":"PSGallery","trusted":true} valid invalid
{"name":"x","_exist":true} (no uri) invalid invalid
{"name":"x","_exist":false} valid valid

Requiring _exist inside the if keeps your simplification and restores today's behaviour:

"required": ["name"],
"allOf": [
  {
    "if": { "properties": { "_exist": { "const": true } }, "required": ["_exist"] },
    "then": { "required": ["uri"] }
  }
]

uri has to stay optional for PSGallery in particular: Set-PSResourceRepository -Name PSGallery -Uri ... is rejected by the cmdlet.

Minor:** for a repository that does not exist, Repository.ToData() still emits trusted: false, priority: 0. Since the PR is about omitting undefined state, returning only name + _exist: false . Shouldn't this be the idiomatic shape?

@adityapatwardhan

Copy link
Copy Markdown
Member

Mikey Lombardi (He/Him) (@michaeltlombardi) - Can you rebase the PR, lot of changes have been merged in the DSC resource since.

Prior to this change, there were a few issues with the DSC resource
schemas regarding the idiomatic definitions and semantics of JSON
schema:

1. Optional properties should not define the `type` keyword with `null`
   as a valid data type unless `null` is an explicitly modeled state
   for the property.

   In JSON Schema, the absence of a property in an object is
   semantically distinct from a property explicitly set to `null`.

   Instead, we control the optionality of properties through the
   `required` or `dependentRequired` keywords.
1. The schema for `Microsoft.PowerShell.PSResourceGet/Repository`
   didn't define `required` at the top-level. Instead, it used the
   `allOf` keyword to indicate whether the `name` and `uri` properties
   are required or just `name`.

   When a property is always required, we should hoist it to the
   top-level `required` keyword array. We can still use the `allOf` to
   conditionally extend the schema as needed.

   Parsing a JSON Schema is difficult for consumers, moreso when it
   requires unrolling schema composition. We should surface as much
   information to the consumer in the simplest form possible.

To address these issues, this change:

1. Removes `null` from the `type` keyword for optional properties that
   don't explicitly model `null` as a valid state.
1. Updates the `Microsoft.PowerShell.PSResourceGet/Repository` schema
   to define `required` at the top-level to indicate that `name` is
   always required and simplifies the `allOf` keyword for the remaining
   case (when `_exist` is `true` then `uri` is required).
Prior to this change, the resource script serialized the data by just
converting the instances to JSON (stripping `_inDesiredState` for
non-test operations).

This caused the resource to serialize `null` for properties that
weren't defined on the instance, causing a mismatch with the idiomatic
schema.

This change updates the serialization logic to omit undefined optional
fields from the serialized output by:

1. Defining the `ToData` method to convert instances to a data
   representation that omits undefined optional fields while preserving
   property order for predictable serialization.

   The method defines two overloads:

   - `ToData([bool$forTest)`: Converts the instance to a data
      representation, omitting undefined optional fields. If `$forTest`
      is `$false`, the method doesn't insert `_inDesiredState`.
   - `ToData()`: Convenience overload to call `ToData($false)`.
1. Standardizing the serialization trace messaging and extracting into
   reusable methods.
Prior to this change, the `PSResourceList` resource would fail during
`export` operations when a scope had no installed resources because
it would emit an empty/invalid item. The non-terminating error for
no resources in a scope was also erroneously emitted.

This change:

1. Defines the `GetAllPsResourcesInScope` function as a wrapper around
   `Get-PsResource` to retrieve all resources in a given scope,
   returning an empty array if no resources are found and erroring if
   the command fails for any other reason.
1. Updates `PopulatePSResourceListObject` to handle empty resource
   arrays gracefully, emitting an info message instead of invalid data.
@michaeltlombardi

Copy link
Copy Markdown
Author

Gijs Reijn (@Gijsreyn)

... I checked the branch against what the DSC engine (v3.3.0) actually enforces and found a few things worth fixing before merging. Also, a small heads-up: it might need a rebase after a couple of PRs land in master. Two points:

  1. Repository schema: name-only input becomes invalid (regression)

    "if": { "properties": { "_exist": { "const": true } } } passes vacuously when _exist is absent, so then requires uri for every input that omits _exist. DSC validates the instance against the resource schema before get, set and delete, so these now fail (checked with Test-Json)...

    uri has to stay optional for PSGallery in particular: Set-PSResourceRepository -Name PSGallery -Uri ... is rejected by the cmdlet.

A few thoughts:

Regarding the requirement for URI, I think the more correct implementation and schema is to always require uri when _exist is true or undefined and name is not PSGallery. The failure to set PSGallery is unique to that repository, right?

While it's not convenient to require uri when the repository already exists, it is required to create the repository or update the URI.

It's worse, in my opinion, to define a resource instance that works for some operaations but not all operations. Consider the following snippet for the properties of a resource instance describing a repository that isn't currently registered:

name: MikeyModules
trusted: true
priority: 1

The following would work:

  • dsc config get|test
  • dsc resource get|test|delete

But set (and presumably what-if) operations would fail when the instance doesn't already exist.

We could leave uri optional and report in what-if that you can't create the instance when the URI isn't defined, but having it conditionally required only when the user explicitly defines _exist as true seems incorrect.

When we have to make tradeoffs for friction/clarity, I think we should prefer requiring users to define the instance in a way that functions for typical operations (get/set/test) without requiring any munging. Or we should raise warnings/errors to help the user understand how their definition will be invalid for set when they call what-if. It's preferable to require a more-explicit resource instance over surprising runtime failures where we enabled convenience for get/test at the cost of breaking set operations.

Further, given that PSGallery is an inherent special case, I think it's okay to forbid URI when name is PSGallery. The effective JSON Schema would then look like:

"allOf": [
    {
        "if": { // If name is PSGallery (case-insensitive)
            "properties": {
                "name": { "pattern": "(?i)^PSGallery$" }
            }
        },
        "then": { // Then uri must not be defined
            "not": { "required": ["uri"] }
        },
        "else": { // else if name is not PSGallery
            "if": { // And exist is `true` or undefined
                "properties": {
                    "_exist": { "const": true }
                }
            }, // Then uri must be defined
            "then": { "required": ["uri"] }
        }
    }
]

I think we need to choose between:

  1. uri is optional for resources where name is not PSGallery - the schema doesn't check that it's defined except to forbid it for PSGallery:

    "allOf": [
        {
            "if": { // If name is PSGallery (case-insensitive)
                "properties": {
                    "name": { "pattern": "(?i)^PSGallery$" }
                }
            },
            "then": { // Then uri must not be defined
                "not": { "required": ["uri"] }
            }
        }
    ]

    And we raise the appropriate warning/error in what-if/set to indicate that the instance can't be created without a URI.

  2. We do require uri when _exist is true (implicitly or explicitly) except for when name is PSGallery. We make it a clearly required component for all non-delete and non-export operations.

    This also helps to head off the case where a user is expecting the repository to be registered to a particular URI but out-of-band the repository has been created with the other expected values and poiting to an incorrect or malicious URI.

@michaeltlombardi

Copy link
Copy Markdown
Author

Aditya Patwardhan (@adityapatwardhan) is there a quick way to force the tests to run for the resource in CI? I had some issues getting the local testing to work and I would definitely prefer to have my local stuff checked by CI before merge where possible.

@adityapatwardhan

Copy link
Copy Markdown
Member

Running tests from a fork is not supported. Let me push this commit to upstream and create a branch.

@adityapatwardhan

Copy link
Copy Markdown
Member

Mikey Lombardi (He/Him) (@michaeltlombardi) - please look at - #2069

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants