diff --git a/.drone.yml b/.drone.yml index a4fc90f..c93978d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -53,11 +53,25 @@ steps: repo: ocram85/blog tags: "next" dockerfile: Dockerfile + build_args: + - NODE_BASE=lts-buster-slim + - NGINX_BASE=1.21.6-alpine - name: "Trivy (next)" image: aquasec/trivy + failure: ignore commands: - - "trivy image --exit-code 1 --no-progress ocram85/blog:next" + - | + trivy image \ + --severity UNKNOWN,LOW,MEDIUM \ + --no-progress \ + ocram85/blog:next + - | + trivy image \ + --exit-code 1 \ + --severity HIGH,CRITICAL \ + --no-progress \ + ocram85/blog:next - name: "Trigger Service Update" image: ocram85/portainer-serviceupdate @@ -100,11 +114,24 @@ steps: repo: ocram85/blog auto_tag: true dockerfile: Dockerfile + build_args: + - NODE_BASE=lts-buster-slim + - NGINX_BASE=1.21.6-alpine - name: "Trivy (latest)" image: aquasec/trivy commands: - - "trivy image --exit-code 1 --no-progress ocram85/blog:latest" + - | + trivy image \ + --severity UNKNOWN,LOW,MEDIUM \ + --no-progress \ + ocram85/blog:latest + - | + trivy image \ + --exit-code 1 \ + --severity HIGH,CRITICAL \ + --no-progress \ + ocram85/blog:latest - name: "Trigger Service Update" image: ocram85/portainer-serviceupdate diff --git a/Dockerfile b/Dockerfile index 928506b..ec0d09b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,8 @@ -FROM node:lts-buster-slim as builder +# Build ARGS for base image versions +ARG NODE_BASE=lts-buster-slim +ARG NGINX_BASE=1.21.6-alpine + +FROM node:${NODE_BASE} as builder COPY . /src #RUN ls -la WORKDIR /src @@ -6,7 +10,7 @@ WORKDIR /src RUN npm install \ && npm run build -FROM nginx:1.21.5-alpine +FROM nginx:${NGINX_BASE} as prod LABEL maintainer="marco.blessing@googlemail.com" HEALTHCHECK --interval=15s --timeout=5s \ CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1 diff --git a/archetypes/default.md b/archetypes/default.md index 96ce2e4..c0757de 100644 --- a/archetypes/default.md +++ b/archetypes/default.md @@ -3,6 +3,9 @@ title: "{{ replace .Name "-" " " | title }}" date: {{ .Date }} draft: true +categories: [''] +tags: [''] + # lastmod: {{ .Date}} # showDateUpdated: true diff --git a/config/_default/languages.en.toml b/config/_default/languages.en.toml index e8fb18c..63a353b 100644 --- a/config/_default/languages.en.toml +++ b/config/_default/languages.en.toml @@ -47,6 +47,7 @@ dateFormat = "2 January 2006" # { slack = "https://workspace.url/team/userid" }, # { snapchat = "https://snapchat.com/add/username" }, # { soundcloud = "https://soundcloud.com/username" }, + { stack-overflow = "https://stackoverflow.com/users/5222635/ocram85" }, # { steam = "https://steamcommunity.com/profiles/userid" }, # { telegram = "https://t.me/username" }, # { tiktok = "https://tiktok.com/@username" }, diff --git a/content/posts/pstypename/index.md b/content/posts/pstypename/index.md new file mode 100644 index 0000000..2e8ef33 --- /dev/null +++ b/content/posts/pstypename/index.md @@ -0,0 +1,238 @@ +--- +title: 'Parameter Validation with PSTypeName' +date: 2022-03-16T09:24:56+01:00 +#draft: true + +categories: ['PowerShell'] +tags: ['parameter', 'validation', 'PSTypeName'] +# lastmod: 2022-03-16T09:24:56+01:00 +# showDateUpdated: true + +# custom overrides for pages +# showDate: false +# showAuthor: false +# showWordCount: false +# showReadingTime: false +# showTableOfContents: false +# showTaxonomies: true +# showEdit: false +# sharingLinks: [null] +--- + +![ship](ship.jpg 'Photo by [Rod Long](https://unsplash.com/@rodlong) on [Unsplash](https://unsplash.com)') + +## 🖼️ Intro + +This post explains how to use `PSCustomObject`s as function parameters. We compare the basic usage with an +advanced one using the `[PSTypeName()]` parameter attribute. + +## 🗑️ Well-Known Workflow + +So let's start with a common object definition how it is used with a function: + +```powershell +$Rocinante = [PSCustomObject]@{ + Owner = 'Martian Congressional Republic Navy' + Type = 'Light Frigate' + Class = 'Corvette' + Registry = 'ECF-270' + HullNumber = '158' + LengthInMeter = 46 + Name = 'Rocinante' +} +``` + +As you can see, a `PSCustomObject` has still the the same class type and just differs by its note properties. + +```bash +> $Rocinante | Get-Member + + TypeName: System.Management.Automation.PSCustomObject + +Name MemberType Definition +---- ---------- ---------- +Equals Method bool Equals(System.Object obj) +GetHashCode Method int GetHashCode() +GetType Method type GetType() +ToString Method string ToString() +Class NoteProperty string Class=Corvette +HullNumber NoteProperty string HullNumber=158 +LengthInMeter NoteProperty int LengthInMeter=46 +Name NoteProperty string Name=Rocinante +Owner NoteProperty string Owner=Martian Congressional Republic Navy +Registry NoteProperty string Registry=ECF-270 +Type NoteProperty string Type=Light Frigate + +> $Rocinante.PSObject.TypeNames +System.Management.Automation.PSCustomObject +System.Object +``` + +So we can use the out object as an function parameter. + +```powershell +function Invoke-Launch { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [PSCustomObject]$Ship + ) + + begin {} + + process { + # Manual input validation for $Ship + # test if all needed properties are present. + + $DockLength = 50 + if ($Ship.LengthInMeter -gt $DockLength) { + Write-Error -Message "Ship doesn't fit in the docking station." -ErrorAction 'Stop' + } + # ... + # ... + } + + end {} +} +``` + +This common pattern could fail whenever someone changes your object properties. If the _LengthInMeter_ property is missing you ran into an error. E.g.: + +```console +> $Rocinante = [PSCustomObject]@{ foo = 'bar' } +> Invoke-Launch -Ship $Rocinante +``` + +{{< note >}} +Keep in mind - Because we are using here custom objects and not class instances, we can not use `Rocinante` as a parameter type like `[Rocinante]$Ship` which would solve this immediately. +{{< /note >}} + +To fix this we can use the `[PSTypeName()]` parameter attribute, to ensure an object with the correct type name is used. This doesn't verify your parameters but minimize the risk for using invalid parameter objects. + +## 🛡️ PSTypeName Usage + +Let's first modify the object creation and use a custom type name. + +```powershell +$Rocinante = [PSCustomObject]@{ + # You can use special property 'PSTypeName' + # to set it implicit within the creation. + PSTypeName = 'Ship.Corvette.LightFrigate' + Owner = 'Martian Congressional Republic Navy' + Type = 'Light Frigate' + Class = 'Corvette' + Registry = 'ECF-270' + HullNumber = '158' + LengthInMeter = 46 + Name = 'Rocinante' +} +# Legacy syntax for injection a custom type name +# $Rocinante.PSObject.TypeNames.insert(0,'Ship.Corvette.LightFrigate') +``` + +```bash +> $Rocinante | Get-Member + + TypeName: Ship.Corvette.LightFrigate + +Name MemberType Definition +---- ---------- ---------- +Equals Method bool Equals(System.Object obj) +GetHashCode Method int GetHashCode() +GetType Method type GetType() +ToString Method string ToString() +Class NoteProperty string Class=Corvette +HullNumber NoteProperty string HullNumber=158 +LengthInMeter NoteProperty int LengthInMeter=46 +Name NoteProperty string Name=Rocinante +Owner NoteProperty string Owner=Martian Congressional Republic Navy +Registry NoteProperty string Registry=ECF-270 +Type NoteProperty string Type=Light Frigate + +> $Rocinante.PSObject.TypeNames +Ship.Corvette.LightFrigate +System.Management.Automation.PSCustomObject +System.Object +``` + +Now we can replace the `[PSCustomObject]` parameter type by `[PSTypeName()]` + +```powershell +function Invoke-Launch { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [PSTypeName('Ship.Corvette.LightFrigate')]$Ship + ) + + begin {} + + process { + $DockLength = 50 + if ($Ship.LengthInMeter -gt $DockLength) { + Write-Error -Message "Ship doesn't fit in the docking station." -ErrorAction 'Stop' + } + # ... + # ... + } + + end {} +} +``` + +## 💭 Final Thoughts + +Over time, your PowerShell functions become more and more complex. You will reach a point where you start using +objects as parameters. This is where the PSTypeName parameter attribute shown can help you. + +In my experience, the ability to create custom classes _(introduced in PowerShell 5)_ is rarely used for this. + +Most PowerShell users I know have a SysOp or DevOps background. Few come from software development and try to use +OOP paradigms and patterns. + +Therefore I would also avoid using complex classes, especially if they use not only properties but also methods. + +Like already mentioned `PSTypeName` just tests the used type name and not your definition details. +You should consider creating a your objects within a wrapper function to mimic a class constructor: + +```powershell +function New-LightFrigate { + [CmdletBinding()] + [OutputType('Ship.Corvette.LightFrigate')] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Registry, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$HullNumber, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Name + ) + + begin {} + + process { + $Ship = [PSCustomObject]@{ + PSTypeName = 'Ship.Corvette.LightFrigate' + Owner = 'Martian Congressional Republic Navy' + Type = 'Light Frigate' + Class = 'Corvette' + Registry = $Registry + HullNumber = $HullNumber + LengthInMeter = 46 + Name = $Name + } + Write-Output $Ship + } + + end {} +} + +$Rocinante = New-LightFrigate -Name 'Rocinante' -Registry 'DE-MB2' -HullNumber '158' +``` diff --git a/content/posts/pstypename/ship.jpg b/content/posts/pstypename/ship.jpg new file mode 100644 index 0000000..47cc98d Binary files /dev/null and b/content/posts/pstypename/ship.jpg differ diff --git a/themes/congo b/themes/congo index 480492a..56fc286 160000 --- a/themes/congo +++ b/themes/congo @@ -1 +1 @@ -Subproject commit 480492a976a577d5ee563a69b57c7b45b519a23c +Subproject commit 56fc286336c136cbc5cc094769721b4393dd7863