WindmillClient.psm1
$script:WindmillConnection = $null <# .SYNOPSIS Connects to Windmill #> function Connect-Windmill { param( [string] $BaseUrl = $null, [string] $Token = $null, [string] $Workspace = $null ) $script:WindmillConnection = [Windmill]::new($BaseUrl, $Token, $Workspace) } <# .SYNOPSIS Disconnects from Windmill #> function Disconnect-Windmill { $script:WindmillConnection = $null } <# .SYNOPSIS Creates a new token #> function New-WindmillToken() { param( [TimeSpan] $Duration = (New-TimeSpan -Days 1) ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } return $script:WindmillConnection.CreateToken([DateTime]::Now.Add($Duration)) } <# .SYNOPSIS Returns OIDC token for specified audience #> function Get-WindmillIdToken { param( [string] $Audience ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } return $script:WindmillConnection.GetIdToken($Audience) } <# .SYNOPSIS Returns current sorkspace #> function Get-WindmillWorkspace { if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } return $script:WindmillConnection.Workspace } <# .SYNOPSIS Returns Windmill version #> function Get-WindmillVersion { if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } return $script:WindmillConnection.Version() } <# .SYNOPSIS Returns current user #> function Get-WindmillUser { if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } return $script:WindmillConnection.Whoami() } <# .SYNOPSIS Returns the value of specified variable #> function Get-WindmillVariable { param( [string] $Path ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } return $script:WindmillConnection.GetVariable($Path) } <# .SYNOPSIS Creates a new variable with specified value #> function New-WindmillVariable { param( [string] $Path, [string] $Value, [switch] $Secret ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } $script:WindmillConnection.CreateVariable($Path, $Value, $Secret) } <# .SYNOPSIS Sets the value of specified variable #> function Set-WindmillVariable { param( [string] $Path, [string] $Value, [switch] $Secret ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } $script:WindmillConnection.SetVariable($Path, $Value, $Secret) } <# .SYNOPSIS Deletes a variable #> function Remove-WindmillVariable { param( [string] $Path ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } $script:WindmillConnection.DeleteVariable($Path) } <# .SYNOPSIS Returns the value of specified resource #> function Get-WindmillResource { param( [string] $Path ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } return $script:WindmillConnection.GetResource($Path) } <# .SYNOPSIS Creates a new resource with specified value #> function New-WindmillResource { param( [string] $Path, [Hashtable] $Value, [string] $ResourceType = $null ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } $script:WindmillConnection.CreateResource($Path, $Value, $ResourceType) } <# .SYNOPSIS Sets the value of specified resource #> function Set-WindmillResource { param( [string] $Path = $null, [Hashtable] $Value, [string] $ResourceType = $null ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } $script:WindmillConnection.SetResource($Path, $Value, $ResourceType) } <# .SYNOPSIS Sets the value of a resource with type "state". #> function Set-WindmillState { param( [Hashtable] $Value ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } $script:WindmillConnection.SetResource($script:WindmillConnection.GetStatePath(), $Value, "state") } <# .SYNOPSIS Gets the value of a resource with type "state". #> function Get-WindmillState { Get-WindmillResource -Path $script:WindmillConnection.GetStatePath() } <# .SYNOPSIS Deletes a resource #> function Remove-WindmillResource { param( [string] $Path ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } $script:WindmillConnection.DeleteResource($Path) } <# .SYNOPSIS Synchronously runs a script #> function Invoke-WindmillScript { # Runs job and waits for it to complete param( [string] $Path = $null, [string] $Hash = $null, [Hashtable] $Arguments = @{}, [boolean] $AssertResultIsNotNull = $true, [int] $Timeout = $null ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } $jobId = Start-WindmillScript -Path $Path -Hash $Hash -Arguments $Arguments $until = if ($Timeout) { (Get-Date).AddSeconds($Timeout) } else { [DateTime]::MaxValue } return $script:WindmillConnection.WaitJob($jobId, $until, $AssertResultIsNotNull) } <# .SYNOPSIS Asynchronously runs a script #> function Start-WindmillScript { param( [string] $Path = $null, [string] $Hash = $null, [Hashtable] $Arguments = @{}, [int] $ScheduledInSecs = $null ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } return $script:WindmillConnection.RunScriptAsync($Path, $Hash, $Arguments, $ScheduledInSecs) } <# .SYNOPSIS Asynchronously runs a flow #> function Start-WindmillFlow { param( [string] $Path = $null, [Hashtable] $Arguments = @{}, [int] $ScheduledInSecs = $null ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } return $script:WindmillConnection.RunFlowAsync($Path, $Arguments, $ScheduledInSecs) } <# .SYNOPSIS Stops a job #> function Stop-WindmillJob { param( [string] $JobId, [string] $Reason = "" ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } return $script:WindmillConnection.CancelJob($JobId, $Reason) } <# .SYNOPSIS Wait for a job to complete #> function Wait-WindmillJob { param( [string] $JobId, [timespan] $Timeout = [timespan]::MaxValue, [boolean] $AssertResultIsNotNull = $true ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } return $script:WindmillConnection.WaitJob($JobId, (Get-Date).Add($Timeout), $AssertResultIsNotNull) } <# .SYNOPSIS Stops all running executions of the same script #> function Stop-WindmillExecution { if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } return $script:WindmillConnection.StopExecution() } <# .SYNOPSIS Returns a specified job #> function Get-WindmillJob { param( [string] $JobId = $null ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } if (-not $JobId) { return $script:WindmillConnection.ListJobs() } return $script:WindmillConnection.GetJob($JobId) } <# .SYNOPSIS Returns the result of a specified job #> function Get-WindmillResult { param( [string] $JobId, [switch] $AssertResultIsNotNull ) if (-not $script:WindmillConnection) { throw "Windmill connection not established. Run Connect-Windmill first." } return $script:WindmillConnection.GetResult($JobId, $AssertResultIsNotNull) } class Windmill { [string] $BaseUrl [string] $Token [string] $Workspace [Hashtable] $Headers [string] $Path Windmill( [string] $BaseUrl = $null, [string] $Token = $null, [string] $Workspace = $null ) { $this.BaseUrl = if ($BaseUrl) { $BaseUrl } else { $env:BASE_INTERNAL_URL } $this.BaseUrl = "$($this.BaseUrl)/api" $this.Token = if ($Token) { $Token } else { $env:WM_TOKEN } $this.Headers = @{ "Content-Type" = "application/json" "Authorization" = "Bearer $($this.Token)" } $this.Workspace = if ($Workspace) { $Workspace } else { $env:WM_WORKSPACE } if (-not $this.Workspace) { throw "Workspace required as an argument or WM_WORKSPACE environment variable" } $this.Path = $env:WM_JOB_PATH } [String] AddQueryParams([String] $Endpoint, [Hashtable] $QueryParams) { $url = $Endpoint if ($QueryParams.Count -gt 0) { $url += '?' $QueryParams.GetEnumerator() | ForEach-Object { $url += "$($_.Key)=$($_.Value)&" } # Remove the trailing '&' $url = $url.TrimEnd('&') } return $url } [Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject] Get([string] $Endpoint, [boolean] $RaiseForStatus) { $Url = "$($this.BaseUrl)/$($Endpoint.TrimStart('/'))" $Response = Invoke-WebRequest -Uri $Url -Method "GET" -Headers $this.Headers -SkipHttpErrorCheck if ($RaiseForStatus -and -not $Response.BaseResponse.IsSuccessStatusCode) { throw "Request failed with status code $($Response.StatusCode)" } return $Response } [Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject] Post([string] $Endpoint, [Object] $Data, [boolean] $RaiseForStatus) { $Url = "$($this.BaseUrl)/$($Endpoint.TrimStart('/'))" $Response = Invoke-WebRequest -Uri $Url -Method "POST" -Headers $this.Headers -Body ($Data | ConvertTo-Json) -SkipHttpErrorCheck -ContentType "application/json" if ($RaiseForStatus -and -not $Response.BaseResponse.IsSuccessStatusCode) { throw "Request failed with status code $($Response.StatusCode)" } return $Response } [Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject] Delete([string] $Endpoint, [boolean] $RaiseForStatus) { $Url = "$($this.BaseUrl)/$($Endpoint.TrimStart('/'))" $Response = Invoke-WebRequest -Uri $Url -Method "DELETE" -Headers $this.Headers -SkipHttpErrorCheck if ($RaiseForStatus -and -not $Response.BaseResponse.IsSuccessStatusCode) { throw "Request failed with status code $($Response.StatusCode)" } return $Response } [string] Version() { $response = $this.Get("/version", $true) return $response.Content } [PSCustomObject] Whoami() { $response = $this.Get("/users/whoami", $true) $result = $response.Content | ConvertFrom-Json return $result } [string] CreateToken([datetime] $Expiration) { $endpoint = "users/tokens/create" $refresh = Get-Date (Get-Date).ToUniversalTime() -UFormat %s $payload = @{ "label" = "refresh $refresh" "expiration" = $Expiration.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") } return $this.Post($endpoint, $payload, $true).Content } [string] GetIdToken([string] $Audience) { return $this.Post("/w/$($this.Workspace)/oidc/token/$Audience").Content } [string] GetVariable([string] $Path) { $response = $this.Get("/w/$($this.Workspace)/variables/get_value/$Path", $true) return $response.Content | ConvertFrom-Json } [void] CreateVariable([string] $Path, [string] $Value, [boolean] $Secret) { $this.Post("/w/$($this.Workspace)/variables/create", @{ "path" = $Path; "value" = $Value; "is_secret" = $Secret; "description" = "" }, $true) } [void] SetVariable([string] $Path, [string] $Value, [boolean] $Secret) { $response = $this.Get("/w/$($this.Workspace)/variables/get_value/$Path", $true) if ($response.StatusCode -eq 404) { throw "Variable $Path not found" } else { $this.Post("/w/$($this.Workspace)/variables/update/$Path", @{ "value" = $Value }, $true) } } [void] DeleteVariable([string] $Path) { $this.Delete("/w/$($this.Workspace)/variables/delete/$Path", $true) } [void] CreateResource([string] $Path, [Hashtable] $Value, [string] $ResourceType) { $this.Post("/w/$($this.Workspace)/resources/create", @{ "path" = $Path; "value" = $Value; "resource_type" = $ResourceType }, $true) } [PSCustomObject] GetResource([string] $Path) { $response = $this.Get("/w/$($this.Workspace)/resources/get/$Path", $true) return $response.Content | ConvertFrom-Json } [void] SetResource([string] $Path, [Hashtable] $Value, [string] $ResourceType) { # Resolve the effective path $resolvedPath = if ($Path) { $Path } else { $script:WindmillConnection.GetStatePath() } if ($this.Get("/w/$($this.Workspace)/resources/exists/$resolvedPath", $false).Content -eq "true") { $this.Post("/w/$($this.Workspace)/resources/update_value/$resolvedPath", @{ "value" = $Value }, $true) } elseif ($ResourceType) { $this.CreateResource($resolvedPath, $Value, $ResourceType) } else { throw "Resource at path $resolvedPath does not exist and no type was provided to initialize it" } } [void] DeleteResource([string] $Path) { $this.Delete("/w/$($this.Workspace)/resources/delete/$Path", $true) } [PSCustomObject] GetJob([string] $JobId) { $response = $this.Get("/w/$($this.Workspace)/jobs_u/get/$JobId", $true) return $response.Content | ConvertFrom-Json } [PSCustomObject] WaitJob([string] $JobId, [datetime] $Until, $AssertResultIsNotNull) { # TODO: Add cleanup while ((Get-Date) -lt $Until) { $response = $this.Get("/w/$($this.Workspace)/jobs_u/completed/get_result_maybe/$JobId", $false) $job = $response.Content | ConvertFrom-Json if ($job.completed) { if ($job.success) { if ($AssertResultIsNotNull -and -not $job.result) { throw "result is null for job $JobId" } return $job.result } else { $err = $job.result.error throw "Job $JobId failed with error: $err" } } Start-Sleep -Milliseconds 500 } throw "Job $JobId did not complete before $Until" } [PSCustomObject[]] ListJobs() { $response = $this.Get("/w/$($this.Workspace)/jobs/list", $true) return $response.Content | ConvertFrom-Json } [string] CancelJob([string] $JobId, [string] $Reason) { return $this.Post("/w/$($this.Workspace)/jobs_u/queue/cancel/$JobId", @{ "reason" = $Reason }, $true) } [PSCustomObject] GetResult([string] $JobId, [boolean] $AssertResultIsNotNull) { $response = $this.Get("/w/$($this.Workspace)/jobs_u/completed/get_result/$JobId", $true) $result = $response.Content | ConvertFrom-Json if ($AssertResultIsNotNull -and -not $result) { throw "result is null for job $JobId" } return $result } [PSCustomObject] RunScriptAsync([string] $Path, [string] $Hash, [Hashtable] $Arguments, [int] $ScheduledInSecs) { $params = @{} if ($Path -and $Hash) { throw "Path and Hash are mutually exclusive" } if ($ScheduledInSecs -ne $null) { $params["scheduled_in_secs"] = $ScheduledInSecs } if ($env:WM_JOB_ID) { $params["parent_job"] = $env:WM_JOB_ID } if ($env:WM_ROOT_FLOW_JOB_ID) { $params["root_job"] = $env:WM_ROOT_FLOW_JOB_ID } if ($Path) { $endpoint = "/w/$($this.Workspace)/jobs/run/p/$Path" } elseif ($Hash) { $endpoint = "/w/$($this.Workspace)/jobs/run/h/$Hash" } else { throw "Path or Hash must be provided" } if ($params) { $endpoint = $this.AddQueryParams($endpoint, $params) } return $this.Post($endpoint, $Arguments, $true).Content } [string] RunFlowAsync([string] $Path, [Hashtable] $Arguments, [int] $ScheduledInSecs) { $params = @{} if ($ScheduledInSecs -ne $null) { $params["scheduled_in_secs"] = $ScheduledInSecs } # TODO: Figure out why this fails when we set parent_job (at least for HN Discord Feed) if ($env:WM_JOB_ID) { $params["parent_job"] = $env:WM_JOB_ID } if ($env:WM_ROOT_FLOW_JOB_ID) { $params["root_job"] = $env:WM_ROOT_FLOW_JOB_ID } $endpoint = "/w/$($this.Workspace)/jobs/run/f/$Path" if ($params) { $endpoint = $this.AddQueryParams($endpoint, $params) } return $this.Post($endpoint, $Arguments, $true).Content } [Hashtable] StopExecution() { $params = @{ "running" = "true" "script_path_exact" = $this.Path } $endpoint = $this.AddQueryParams("/w/$($this.Workspace)/jobs/list", $params) $jobs = $this.Get($endpoint, $true).Content | ConvertFrom-Json $current_job_id = $env:WM_JOB_ID $job_ids = $jobs | Where-Object { $_.id -ne $current_job_id } | Select-Object -ExpandProperty id $result = @{} foreach ($job_id in $job_ids) { $result[$job_id] = $this.CancelJob($job_id, "Killed by Stop-WindmillExecution") } return $result } [string] GetStatePath() { $statePath = $env:WM_STATE_PATH_NEW if (-not $statePath) { $statePath = $env:WM_STATE_PATH } if (-not $statePath) { throw "State path not set" } return $statePath } } Export-ModuleMember -Function @( 'Connect-Windmill', 'Disconnect-Windmill', 'New-WindmillToken', 'Get-WindmillIdToken', 'Get-WindmillWorkspace', 'Get-WindmillVersion', 'Get-WindmillUser', 'Get-WindmillVariable', 'New-WindmillVariable', 'Set-WindmillVariable', 'Remove-WindmillVariable', 'Get-WindmillResource', 'Get-WindmillState', 'New-WindmillResource', 'Set-WindmillResource', 'Set-WindmillState', 'Remove-WindmillResource', 'Invoke-WindmillScript', 'Start-WindmillScript', 'Start-WindmillFlow', 'Stop-WindmillJob', 'Wait-WindmillJob', 'Stop-WindmillExecution', 'Get-WindmillJob', 'Get-WindmillResult' ) |