API PowerShell Universal avec [APIEndpoint()]

Je cherche toujours à rendre l’automatisation plus facile à faire passer à l’échelle dans de vraies équipes. Récemment, j’ai créé un petit module, synedgy.universal.helper, pour simplifier la manière dont je définis et importe les points de terminaison d’API PowerShell Universal.

L’idée clé est de garder les métadonnées de point de terminaison au plus près de la fonction PowerShell à laquelle elles appartiennent, au lieu de dupliquer les définitions dans des fichiers séparés.

Illustration personnalisée de [APIEndpoint()] pour cet article

API PowerShell Universal en pratique

PowerShell Universal peut exposer des API REST directement depuis PowerShell et générer une documentation Swagger/OpenAPI à partir de vos définitions d’endpoints.

Les modèles de points de terminaison documentés sont :

Les deux fonctionnent, mais si une API grandit, la prolifération des points de terminaison dans endpoints.ps1 devient vite difficile à maintenir.

Définition d’endpoint d’API PowerShell Universal et vue Swagger générée

Conseils de production avant les modèles personnalisés

Deux habitudes aident dès le départ :

L’isolation des environnements améliore le démarrage et le contrôle opérationnel, car les modules peuvent être préchargés et les charges API peuvent être redémarrées indépendamment.

$newPSUEnvironmentParams = @{
    Name                     = 'PSUModuleEnv'
    Variables                = @('*')
    Description              = 'Environment for PSUModule API'
    Type                     = 'PowerShell7'
    Path                     = 'pwsh'
    Arguments                = '-NoLogo'
    Modules                  = @('PSUModule')
    DisableImplicitWinCompat = $true
    PSModulePath             = (Split-Path -Path $PSScriptRoot -Parent)
    PersistentRunspace       = $true
}

New-PSUEnvironment @newPSUEnvironmentParams

Là où le modèle habituel des points de terminaison devient maladroit

J’ai rencontré trois points de friction récurrents :

  1. Décalage de casse des paramètres : PowerShell utilise PascalCase, tandis que de nombreux consommateurs d’API s’attendent à camelCase.
  2. Responsabilités séparées : la logique de fonction se trouve dans un fichier, tandis que les métadonnées de point de terminaison se trouvent ailleurs.
  3. Changements d’environnement en masse : déplacer des points de terminaison entre environnements peut devenir un travail répétitif de recherche-remplacement.

Le modèle de l’attribut [APIEndpoint()]

Le modèle consiste à annoter la fonction elle-même avec les métadonnées de point de terminaison :

function Get-Something {
    [CmdletBinding()]
    [ApiEndpoint(
        Path = '/getSomething',
        Method = @('GET'),
        Description = 'Get a string using the someThing parameter.',
        Environment = 'PowerShell 7',
        ContentType = 'text/event-stream; charset=utf-8'
    )]
    param (
        [Parameter()]
        [string]
        $SomeThing
    )

    Get-ResultFromSomewhere -SomeThing $SomeThing
}

Puis importez tous les points de terminaison annotés depuis le module :

# .universal/endpoints.ps1
# Assumes synedgy.universal.helper is available in PSModulePath.
Import-PSUEndpoint -Module PSUModule -Environment PSUModuleEnv

Cela donne un point unique pour l’intention du point de terminaison et l’implémentation de la fonction.

Ce que fait Import-PSUEndpoint

Import-PSUEndpoint découvre les fonctions exportées avec [APIEndpoint()] lorsque IsEndpoint = $true, puis génère des blocs de script de points de terminaison qui :

Exemple de forme générée :

{
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]
        $someThing
    )

    Get-Something @PSBoundParameters
}

Définition de l’attribut personnalisé

L’attribut est une classe PowerShell normale qui hérite de System.Attribute.

class APIEndpoint : System.Attribute {
    [bool]$IsEndpoint = $true
    [string]$Name
    [string]$Version = 'v1'
    [string]$Path
    [string]$Description
    [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD')]
    [string[]]$Method
    [bool]$Authentication = $false
    [string[]]$Role
    [string]$Tag
    [int]$Timeout
    [string]$Environment
    [string]$ContentType = 'application/json; charset=utf-8'
    [ValidateSet('Information', 'Warning', 'Error', 'Verbose', 'Debug')]
    [string[]]$LogLevel = @('Information')
    [scriptblock]$Parameters

    APIEndpoint () {}
}

Une passe de découverte simple ressemble à ceci :

Get-Command -Module PSUModule | Where-Object {
    $_.ScriptBlock.Attributes.Where{
        $_.TypeId.ToString() -eq 'APIEndpoint' -and
        $_.IsEndpoint -eq $true
    }
}

Dépendance et réutilisation

Pour réutiliser l’attribut entre modules, exposez-le via des accélérateurs de type comme décrit dans Exporter des classes depuis des modules PowerShell, puis déclarez synedgy.universal.helper comme module requis.

Limites et évolutions futures

Deux points restent importants :

Références