<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:series="https://publishpress.com/"
	>

<channel>
	<title>Damian Karlson</title>
	<atom:link href="https://damiankarlson.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://damiankarlson.com</link>
	<description>cloud, code, and random things</description>
	<lastBuildDate>Tue, 01 Oct 2024 23:24:00 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</generator>

<image>
	<url>https://damiankarlson.com/wp-content/uploads/2024/04/cropped-favicon-32x32.png</url>
	<title>Damian Karlson</title>
	<link>https://damiankarlson.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">232174215</site>	<item>
		<title>PowerShell on AWS Lambda</title>
		<link>https://damiankarlson.com/2024/10/01/powershell-on-aws-lambda/</link>
		
		<dc:creator><![CDATA[Damian Karlson]]></dc:creator>
		<pubDate>Tue, 01 Oct 2024 23:23:57 +0000</pubDate>
				<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[#vBrownBag]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Lambda]]></category>
		<guid isPermaLink="false">https://damiankarlson.com/?p=1694</guid>

					<description><![CDATA[Here&#8217;s another short episode I did on the vBrownBag YouTube channel talking about what I&#8217;ve learned about writing PowerShell for AWS Lambda. 00:28 A quick overview of PowerShell &#38; AWS Lambda 💪 01:07 Anatomy of a PowerShell Lambda function 01:35 Setting up a development environment 03:00 AWSLambdaPSCore modules 03:42 Packaging a PowerShell module 04:19 Handling [&#8230;]]]></description>
										<content:encoded><![CDATA[<p><iframe title="Deep Dive: PowerShell on AWS Lambda" width="500" height="281" src="https://www.youtube.com/embed/2auB7DGhD24?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><br />
Here&#8217;s another short episode I did on the vBrownBag YouTube channel talking about what I&#8217;ve learned about writing PowerShell for AWS Lambda.</p>
<ul>
<li>00:28 A quick overview of PowerShell &amp; AWS Lambda <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4aa.png" alt="💪" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>
<li>01:07 Anatomy of a PowerShell Lambda function</li>
<li>01:35 Setting up a development environment</li>
<li>03:00 AWSLambdaPSCore modules</li>
<li>03:42 Packaging a PowerShell module</li>
<li>04:19 Handling the handler</li>
<li>05:14 Publishing your PowerShell Lambda function <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>
<li>05:42 Understanding the AWS Lambda filesystem</li>
</ul>
<p>Resources:</p>
<ul>
<li><a href="https://www.powershellgallery.com/packages/AWSLambdaPSCore">https://www.powershellgallery.com/packages/AWSLambdaPSCore</a></li>
<li><a href="https://dotnet.microsoft.com/en-us/download/dotnet/8.0">https://dotnet.microsoft.com/en-us/download/dotnet/8.0</a></li>
<li><a href="https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.4">https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.4</a></li>
<li><a href="https://code.visualstudio.com/download">https://code.visualstudio.com/download</a></li>
<li><a href="https://github.com/PowerShell/PowerShell">https://github.com/PowerShell/PowerShell</a></li>
</ul>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1694</post-id>	</item>
		<item>
		<title>Automating the vBrownBag with AWS Serverless</title>
		<link>https://damiankarlson.com/2024/09/19/automating-the-vbrownbag-with-aws-serverless/</link>
		
		<dc:creator><![CDATA[Damian Karlson]]></dc:creator>
		<pubDate>Thu, 19 Sep 2024 23:15:00 +0000</pubDate>
				<category><![CDATA[#vBrownBag]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Lambda]]></category>
		<guid isPermaLink="false">https://damiankarlson.com/?p=1692</guid>

					<description><![CDATA[I did a short episode for the vBrownBag with a deeper dive into the Meatgrinder, showing how the different AWS services interact, how the process logs to CloudWatch, and more! 00:00 Intro 1:20 The AWS Services that power the Meatgrinder 💪 1:51 AWS S3 2:18 AWS EventBridge 2:50 AWS Step Functions 4:10 PowerShell on Lambda [&#8230;]]]></description>
										<content:encoded><![CDATA[<p><iframe title="Deep Dive: Automating the vBrownBag with AWS Serverless" width="500" height="281" src="https://www.youtube.com/embed/e2lZ_tRZwKU?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><br />
I did a short episode for the vBrownBag with a deeper dive into the Meatgrinder, showing how the different AWS services interact, how the process logs to CloudWatch, and more!</p>
<ul>
<li>00:00 Intro</li>
<li>1:20 The AWS Services that power the Meatgrinder <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4aa.png" alt="💪" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>
<li>1:51 AWS S3</li>
<li>2:18 AWS EventBridge</li>
<li>2:50 AWS Step Functions</li>
<li>4:10 PowerShell on Lambda &amp; Step Functions interaction <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f57a.png" alt="🕺" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f483.png" alt="💃" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>
<li>7:05 Python Lamba function</li>
<li>7:33 Logging success in AWS CloudWatch <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>
</ul>
<p>Resources:</p>
<ul>
<li><a href="https://vbrownbag.com">https://vbrownbag.com</a></li>
<li><a href="https://aws.amazon.com/serverless/">https://aws.amazon.com/serverless/</a></li>
<li><a href="https://damiankarlson.com/2024/04/21/whats-in-the-bag-behind-the-scenes-at-vbrownbag-com/">https://damiankarlson.com/2024/04/21/whats-in-the-bag-behind-the-scenes-at-vbrownbag-com/</a></li>
</ul>
]]></content:encoded>
					
		
		
		
		<series:name><![CDATA[What's in the bag? Behind the scenes at vBrownBag.com]]></series:name>
<post-id xmlns="com-wordpress:feed-additions:1">1692</post-id>	</item>
		<item>
		<title>Part 6 of &#8220;What’s in the bag?&#8221; Behind the scenes at vBrownBag.com</title>
		<link>https://damiankarlson.com/2024/05/31/part-6-of-whats-in-the-bag-behind-the-scenes-at-vbrownbag-com/</link>
		
		<dc:creator><![CDATA[Damian Karlson]]></dc:creator>
		<pubDate>Fri, 31 May 2024 17:49:05 +0000</pubDate>
				<category><![CDATA[AWS]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[#vBrownBag]]></category>
		<category><![CDATA[Lambda]]></category>
		<guid isPermaLink="false">https://damiankarlson.com/?p=1667</guid>

					<description><![CDATA[In Part 5 of this series, I covered Google OAuth with PSAuthClient and decrypting AWS Lambda environment variables. In this post, I&#8217;d like to cover some code for the WordPress REST API and RSS XML updates. In the previous iteration of the Meatgrinder process, new posts were added by email. However, I didn&#8217;t like the [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In Part 5 of this series, I covered Google OAuth with PSAuthClient and decrypting AWS Lambda environment variables. In this post, I&#8217;d like to cover some code for the WordPress REST API and RSS XML updates.</p>



<p class="wp-block-paragraph">In the previous iteration of the Meatgrinder process, <a href="https://wordpress.com/support/post-by-email/">new posts were added by email</a>. However, I didn&#8217;t like the idea of using an email in the process because I wanted the new Meatgrinder to be as self-contained as possible, and using PowerShell&#8217;s <code>Invoke-WebRequest</code> with the REST API meets that requirement.</p>



<p class="wp-block-paragraph">Some notes on the process: A WordPress <a href="https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/">application password</a> is recommended as it simplifies authentication. Author, tags, and categories are their respective unique IDs in each WordPress database. Featured images can&#8217;t be uploaded along with posts; they have to be uploaded and then their unique image ID associated with the post&#8217;s unique ID. The thumbnail is read from S3 into the <code>/tmp</code> directory of the Lambda function at run time using <code>Read-S3Object</code>. The <code>decrypt</code> cmdlet in this sample is the helper function I wrote to <a href="https://damiankarlson.com/2024/05/22/part-5-of-whats-in-the-bag-behind-the-scenes-at-vbrownbag-com/#variables">decrypt AWS Lambda environment variables</a>.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:0.9em;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25em;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#f2f2f2;color:#2f363c">PowerShell</span><span role="button" tabindex="0" data-code="# Creating the credential object
$app_pwd = decrypt -ciphertext $env:app_pwd

$securepass = ConvertTo-SecureString $app_pwd -AsPlainText -Force

$credential = New-Object System.Management.Automation.PSCredential($env:app_user, $securepass)

# Begin building the post
$title = &quot;New post title!&quot;

$content = &quot;Here's a brand new vBrownBag.com post!&quot;

# Fun one-liner that creates an excerpt if the content is longer than 124 chars
$excerpt = $content.Length -gt 124 ? $content.Substring(0, 124) + &quot; ...&quot; : $content

# Retrieving the featured image from S3 as $thumbnail, and reading in the bytes
$thumbnail = Read-S3Object -BucketName $env:bucket -Key &quot;$thumbnail.jpg&quot; -File (Join-Path $env:TEMP &quot;$thumbnail.jpg&quot;)

$thumbnailBytes = [System.IO.File]::ReadAllBytes($thumbnail)

# Uploading the featured image and retrieving the $imageId
$response = Invoke-WebRequest -Uri &quot;$env:url/wp-json/wp/v2/media&quot; -Method Post -Authentication Basic -Credential $credential -Headers @{
  &quot;Content-Type&quot;        = &quot;image/jpeg&quot;
  &quot;Content-Disposition&quot; = &quot;attachment; filename=`&quot;$thumbnail.jpg`&quot;&quot;
} -Body $thumbnailBytes

$imageId = ($response.Content | ConvertFrom-Json).id

# Updating the featured image title &amp; alt text for $imageId
$imageJson = @{
  &quot;date&quot;     = Get-Date -Format &quot;yyyy-MM-dd HH:mm:ss&quot;
  &quot;title&quot;    = $title
  &quot;alt_text&quot; = &quot;Featured image for &quot; + $title
} | ConvertTo-Json -Depth 10

Invoke-WebRequest -Uri &quot;$env:url/wp-json/wp/v2/media/$imageId&quot; -Method Post -Authentication Basic -Credential $credential -Headers @{&quot;Content-Type&quot; = &quot;application/json&quot; } -Body $imageJson

$postJson = @{
  &quot;date&quot;           = Get-Date -Format &quot;yyyy-MM-dd HH:mm:ss&quot;
  &quot;author&quot;         = $authorId
  &quot;title&quot;          = $title
  &quot;excerpt&quot;        = $excerpt
  &quot;content&quot;        = $content
  &quot;comment_status&quot; = &quot;closed&quot;
  &quot;ping_status&quot;    = &quot;closed&quot;
  &quot;featured_media&quot; = $imageId
  &quot;status&quot;         = $status # &quot;publish&quot; or &quot;pending&quot;
  &quot;categories&quot;     = @(&quot;1&quot;, &quot;2&quot;)
} | ConvertTo-Json -Depth 10

$response = Invoke-WebRequest -Uri &quot;$env:url/wp-json/wp/v2/posts&quot; -Method Post -Authentication Basic -Credential $credential -Headers @{&quot;Content-Type&quot; = &quot;application/json&quot; } -Body $postJson" style="color:#24292eff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki min-light" style="background-color: #ffffff" tabindex="0"><code><span class="line"><span style="color: #C2C3C5"># Creating the credential object</span></span>
<span class="line"><span style="color: #24292EFF">$app_pwd </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> decrypt </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">ciphertext $</span><span style="color: #1976D2">env:</span><span style="color: #24292EFF">app_pwd</span></span>
<span class="line"></span>
<span class="line"><span style="color: #24292EFF">$securepass </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">ConvertTo-SecureString</span><span style="color: #24292EFF"> $app_pwd </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">AsPlainText </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Force</span></span>
<span class="line"></span>
<span class="line"><span style="color: #24292EFF">$credential </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">New-Object</span><span style="color: #24292EFF"> System.Management.Automation.PSCredential($</span><span style="color: #1976D2">env:</span><span style="color: #24292EFF">app_user</span><span style="color: #D32F2F">,</span><span style="color: #24292EFF"> $securepass)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #C2C3C5"># Begin building the post</span></span>
<span class="line"><span style="color: #24292EFF">$title </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;New post title!&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #24292EFF">$content </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;Here&#39;s a brand new vBrownBag.com post!&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #C2C3C5"># Fun one-liner that creates an excerpt if the content is longer than 124 chars</span></span>
<span class="line"><span style="color: #24292EFF">$excerpt </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $content.Length </span><span style="color: #D32F2F">-gt</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">124</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> $content.Substring(</span><span style="color: #1976D2">0</span><span style="color: #D32F2F">,</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">124</span><span style="color: #24292EFF">) </span><span style="color: #D32F2F">+</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot; ...&quot;</span><span style="color: #24292EFF"> : $content</span></span>
<span class="line"></span>
<span class="line"><span style="color: #C2C3C5"># Retrieving the featured image from S3 as $thumbnail, and reading in the bytes</span></span>
<span class="line"><span style="color: #24292EFF">$thumbnail </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Read-S3Object</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">BucketName $</span><span style="color: #1976D2">env:</span><span style="color: #24292EFF">bucket </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Key </span><span style="color: #22863A">&quot;$thumbnail.jpg&quot;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">File (</span><span style="color: #6F42C1">Join-Path</span><span style="color: #24292EFF"> $</span><span style="color: #1976D2">env:</span><span style="color: #24292EFF">TEMP </span><span style="color: #22863A">&quot;$thumbnail.jpg&quot;</span><span style="color: #24292EFF">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #24292EFF">$thumbnailBytes </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">System.IO.File</span><span style="color: #24292EFF">]::ReadAllBytes($thumbnail)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #C2C3C5"># Uploading the featured image and retrieving the $imageId</span></span>
<span class="line"><span style="color: #24292EFF">$response </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Invoke-WebRequest</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Uri </span><span style="color: #22863A">&quot;$</span><span style="color: #1976D2">env:</span><span style="color: #22863A">url/wp-json/wp/v2/media&quot;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Method Post </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Authentication Basic </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Credential $credential </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Headers </span><span style="color: #D32F2F">@</span><span style="color: #24292EFF">{</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #22863A">&quot;Content-Type&quot;</span><span style="color: #24292EFF">        </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;image/jpeg&quot;</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #22863A">&quot;Content-Disposition&quot;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;attachment; filename=`&quot;$thumbnail.jpg`&quot;&quot;</span></span>
<span class="line"><span style="color: #24292EFF">} </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Body $thumbnailBytes</span></span>
<span class="line"></span>
<span class="line"><span style="color: #24292EFF">$imageId </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> ($response.Content </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">ConvertFrom-Json</span><span style="color: #24292EFF">).id</span></span>
<span class="line"></span>
<span class="line"><span style="color: #C2C3C5"># Updating the featured image title &amp; alt text for $imageId</span></span>
<span class="line"><span style="color: #24292EFF">$imageJson </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">@</span><span style="color: #24292EFF">{</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #22863A">&quot;date&quot;</span><span style="color: #24292EFF">     </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Get-Date</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Format </span><span style="color: #22863A">&quot;yyyy-MM-dd HH:mm:ss&quot;</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #22863A">&quot;title&quot;</span><span style="color: #24292EFF">    </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $title</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #22863A">&quot;alt_text&quot;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;Featured image for &quot;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">+</span><span style="color: #24292EFF"> $title</span></span>
<span class="line"><span style="color: #24292EFF">} </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">ConvertTo-Json</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Depth </span><span style="color: #1976D2">10</span></span>
<span class="line"></span>
<span class="line"><span style="color: #6F42C1">Invoke-WebRequest</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Uri </span><span style="color: #22863A">&quot;$</span><span style="color: #1976D2">env:</span><span style="color: #22863A">url/wp-json/wp/v2/media/$imageId&quot;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Method Post </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Authentication Basic </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Credential $credential </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Headers </span><span style="color: #D32F2F">@</span><span style="color: #24292EFF">{</span><span style="color: #22863A">&quot;Content-Type&quot;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;application/json&quot;</span><span style="color: #24292EFF"> } </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Body $imageJson</span></span>
<span class="line"></span>
<span class="line"><span style="color: #24292EFF">$postJson </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">@</span><span style="color: #24292EFF">{</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #22863A">&quot;date&quot;</span><span style="color: #24292EFF">           </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Get-Date</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Format </span><span style="color: #22863A">&quot;yyyy-MM-dd HH:mm:ss&quot;</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #22863A">&quot;author&quot;</span><span style="color: #24292EFF">         </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $authorId</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #22863A">&quot;title&quot;</span><span style="color: #24292EFF">          </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $title</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #22863A">&quot;excerpt&quot;</span><span style="color: #24292EFF">        </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $excerpt</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #22863A">&quot;content&quot;</span><span style="color: #24292EFF">        </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $content</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #22863A">&quot;comment_status&quot;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;closed&quot;</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #22863A">&quot;ping_status&quot;</span><span style="color: #24292EFF">    </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;closed&quot;</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #22863A">&quot;featured_media&quot;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $imageId</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #22863A">&quot;status&quot;</span><span style="color: #24292EFF">         </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $status </span><span style="color: #C2C3C5"># &quot;publish&quot; or &quot;pending&quot;</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #22863A">&quot;categories&quot;</span><span style="color: #24292EFF">     </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">@</span><span style="color: #24292EFF">(</span><span style="color: #22863A">&quot;1&quot;</span><span style="color: #D32F2F">,</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;2&quot;</span><span style="color: #24292EFF">)</span></span>
<span class="line"><span style="color: #24292EFF">} </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">ConvertTo-Json</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Depth </span><span style="color: #1976D2">10</span></span>
<span class="line"></span>
<span class="line"><span style="color: #24292EFF">$response </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Invoke-WebRequest</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Uri </span><span style="color: #22863A">&quot;$</span><span style="color: #1976D2">env:</span><span style="color: #22863A">url/wp-json/wp/v2/posts&quot;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Method Post </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Authentication Basic </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Credential $credential </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Headers </span><span style="color: #D32F2F">@</span><span style="color: #24292EFF">{</span><span style="color: #22863A">&quot;Content-Type&quot;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;application/json&quot;</span><span style="color: #24292EFF"> } </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Body $postJson</span></span></code></pre></div>



<p class="wp-block-paragraph">Next, let&#8217;s cover some XML code that I&#8217;m using to update our RSS file. The process is rather simple: download the latest version of the XML file to <code>/tmp</code> using <code>Invoke-WebRequest</code>, save a time-stamped backup copy to S3, read in the contents of the XML file, create a new entry, remove any entries that are than 2 years old, make sure there&#8217;s no more than 300 entries in the file (just in case!), write the updated XML to disk, then upload it to the website. The <code>createRssElement</code> helper function was from the original Meatgrinder script, and its job is to simplify XML element creation. Lastly, the <code>Send-SFTPFile</code> cmdlet is from the <a href="https://github.com/EvotecIT/Transferetto" data-type="link" data-id="https://github.com/EvotecIT/Transferetto">Transferetto</a> module.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:0.9em;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25em;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#f2f2f2;color:#2f363c">PowerShell</span><span role="button" tabindex="0" data-code="function createRssElement {
  param([string]$elementName, [string]$value, $parent, [string]$namespaceURI = $null)
  if ($namespaceURI) {
    $thisNode = $xml.CreateElement(&quot;itunes&quot;, $elementName, &quot;http://www.itunes.com/dtds/podcast-1.0.dtd&quot;)
  }
  else {
    $thisNode = $xml.CreateElement($elementName)
  }
  $thisNode.InnerText = $value
  $null = $parent.AppendChild($thisNode)
    return $thisNode
}

$xmlUrl = $env:rssxml_url + $env:rssxml_filename
$xmlbackupFileName = (Get-Date -Format &quot;yyyyMMddHHmmss&quot;) + &quot; $($env:rssxml_filename)&quot;
$xmlbackupPath = Join-Path $env:TEMP $env:rssxml_filename
# Download the file to /tmp
Invoke-WebRequest -Uri $xmlUrl -OutFile $xmlbackupPath | Out-Null
# Backup to S3
Write-S3Object -BucketName $bucket -File $xmlbackupPath -Key $xmlbackupFileName

# Read in the contents of the file
$xml = [xml] (Get-Content $xmlbackupPath)

# Remove anything with a bad date or more than 2 years old
$xml.rss.channel.Item | Where-Object {
  $pubDate = [DateTime]::MinValue
  $parseSuccessful = [DateTime]::TryParse($_.pubDate, [ref]$pubDate)
  if (-not $parseSuccessful -or $pubDate -lt (Get-Date).AddYears(-2)) {
    $xml.rss.channel.RemoveChild($_) | Out-Null
  }
}

# Create the new entry
$rssChannel = $xml.rss.channel
$thisItem = createRssElement -elementName 'item' -value '' -parent $rssChannel
$null = createRssElement -elementName 'title' -value $title -parent $thisItem
$null = createRssElement -elementName 'link' -value 'http://vbrownbag.com' -parent $thisItem
$null = createRssElement -elementName 'description' -value $description -parent $thisItem
$null = createRssElement -elementName 'guid' -value $url -parent $thisItem
$enclosure = createRssElement -elementName 'enclosure' -parent $thisItem
$null = $enclosure.SetAttribute('url', $url)
$null = $enclosure.SetAttribute('length', $mp4file.Size)
$null = $enclosure.SetAttribute('type', 'video/mp4')
$null = createRssElement -elementName 'category' -value &quot;Podcasts&quot; -parent $thisItem
$pubDate = Get-Date -Format 'r'
$null = createRssElement -elementName 'pubDate' $pubDate -parent $thisItem

# Sort by date and remove any duplicates
$orderedItems = $rssChannel.Item | Group-Object -Property pubdate | ForEach-Object { 
  if ($_.Group.Count -gt 1) {
    for ($i = 1; $i -lt $_.Group.Count; $i++) {
        Write-Output &quot;Duplicate item: $($_.Group[$i].title)&quot;
    }
  }
  $duplicatesCount += ($_.Group.Count - 1)
  $_.Group[0]
} |  Sort-Object { [DateTime]::Parse($_.pubDate) } -Descending

# Only use the first 300 because Apple doesn't like more than that
if ($orderedItems.Count -gt 300) {
  $orderedItems = $orderedItems | Select-Object -First 300
}

# Remove all items from the channel
$itemsToRemove = $xml.rss.channel.item.Clone()
foreach ($item in $itemsToRemove) {
  $xml.rss.channel.RemoveChild($item) | Out-Null
}
        
# Add the ordered items back to the channel
foreach ($item in $orderedItems) {
  $xml.rss.channel.AppendChild($item) | Out-Null
}

# Timestamp this newest version of the file and write to disk
$xml.rss.channel.lastBuildDate = (Get-Date).ToUniversalTime().ToString(&quot;r&quot;)
$xmlWriterSettings = new-object System.Xml.XmlWriterSettings
$xmlWriterSettings.CloseOutput = $true
$xmlWriterSettings.IndentChars = &quot;    &quot;
$xmlWriterSettings.Indent = $true
$xmlWriterSettings.NewLineOnAttributes = $true
$xmlWriterSettings.NewLineHandling = [System.Xml.NewLineHandling]::Replace
$xmlWriter = [System.Xml.XmlWriter]::Create($xmlbackupPath, $xmlWriterSettings)
$null = $xml.WriteTo($xmlWriter)
$xmlWriter.Close()

# SFTP the file to where it lives
$sftp_password = decrypt -ciphertext $env:sftp_password
$sftp_credential = New-Object System.Management.Automation.PSCredential($env:sftp_username, (ConvertTo-SecureString $sftp_password -AsPlainText -Force))
$sftpClient = Connect-SFTP -Server $env:sftp_host -Credential $sftp_credential -Port $env:sftp_port
Send-SFTPFile -SftpClient $SftpClient -LocalPath $filepath -RemotePath (Join-Path $env:sftp_destination $env:rssxml_filename) -AllowOverride" style="color:#24292eff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki min-light" style="background-color: #ffffff" tabindex="0"><code><span class="line"><span style="color: #D32F2F">function</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createRssElement</span><span style="color: #24292EFF"> {</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">param</span><span style="color: #24292EFF">([</span><span style="color: #D32F2F">string</span><span style="color: #24292EFF">]$elementName</span><span style="color: #D32F2F">,</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">string</span><span style="color: #24292EFF">]$value</span><span style="color: #D32F2F">,</span><span style="color: #24292EFF"> $parent</span><span style="color: #D32F2F">,</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">string</span><span style="color: #24292EFF">]$namespaceURI </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">$null</span><span style="color: #24292EFF">)</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> ($namespaceURI) {</span></span>
<span class="line"><span style="color: #24292EFF">    $thisNode </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $xml.CreateElement(</span><span style="color: #22863A">&quot;itunes&quot;</span><span style="color: #D32F2F">,</span><span style="color: #24292EFF"> $elementName</span><span style="color: #D32F2F">,</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;http://www.itunes.com/dtds/podcast-1.0.dtd&quot;</span><span style="color: #24292EFF">)</span></span>
<span class="line"><span style="color: #24292EFF">  }</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">else</span><span style="color: #24292EFF"> {</span></span>
<span class="line"><span style="color: #24292EFF">    $thisNode </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $xml.CreateElement($elementName)</span></span>
<span class="line"><span style="color: #24292EFF">  }</span></span>
<span class="line"><span style="color: #24292EFF">  $thisNode.InnerText </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $value</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #1976D2">$null</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $parent.AppendChild($thisNode)</span></span>
<span class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> $thisNode</span></span>
<span class="line"><span style="color: #24292EFF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #24292EFF">$xmlUrl </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $</span><span style="color: #1976D2">env:</span><span style="color: #24292EFF">rssxml_url </span><span style="color: #D32F2F">+</span><span style="color: #24292EFF"> $</span><span style="color: #1976D2">env:</span><span style="color: #24292EFF">rssxml_filename</span></span>
<span class="line"><span style="color: #24292EFF">$xmlbackupFileName </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> (</span><span style="color: #6F42C1">Get-Date</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Format </span><span style="color: #22863A">&quot;yyyyMMddHHmmss&quot;</span><span style="color: #24292EFF">) </span><span style="color: #D32F2F">+</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot; </span><span style="color: #D32F2F">$</span><span style="color: #22863A">($</span><span style="color: #1976D2">env:</span><span style="color: #22863A">rssxml_filename)</span><span style="color: #22863A">&quot;</span></span>
<span class="line"><span style="color: #24292EFF">$xmlbackupPath </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Join-Path</span><span style="color: #24292EFF"> $</span><span style="color: #1976D2">env:</span><span style="color: #24292EFF">TEMP $</span><span style="color: #1976D2">env:</span><span style="color: #24292EFF">rssxml_filename</span></span>
<span class="line"><span style="color: #C2C3C5"># Download the file to /tmp</span></span>
<span class="line"><span style="color: #6F42C1">Invoke-WebRequest</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Uri $xmlUrl </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">OutFile $xmlbackupPath </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Out-Null</span></span>
<span class="line"><span style="color: #C2C3C5"># Backup to S3</span></span>
<span class="line"><span style="color: #6F42C1">Write-S3Object</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">BucketName $bucket </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">File $xmlbackupPath </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Key $xmlbackupFileName</span></span>
<span class="line"></span>
<span class="line"><span style="color: #C2C3C5"># Read in the contents of the file</span></span>
<span class="line"><span style="color: #24292EFF">$xml </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">xml</span><span style="color: #24292EFF">] (</span><span style="color: #6F42C1">Get-Content</span><span style="color: #24292EFF"> $xmlbackupPath)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #C2C3C5"># Remove anything with a bad date or more than 2 years old</span></span>
<span class="line"><span style="color: #24292EFF">$xml.rss.channel.Item </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Where-Object</span><span style="color: #24292EFF"> {</span></span>
<span class="line"><span style="color: #24292EFF">  $pubDate </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">DateTime</span><span style="color: #24292EFF">]::MinValue</span></span>
<span class="line"><span style="color: #24292EFF">  $parseSuccessful </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">DateTime</span><span style="color: #24292EFF">]::TryParse(</span><span style="color: #1976D2">$_.pubDate</span><span style="color: #D32F2F">,</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">ref</span><span style="color: #24292EFF">]$pubDate)</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">-not</span><span style="color: #24292EFF"> $parseSuccessful </span><span style="color: #D32F2F">-or</span><span style="color: #24292EFF"> $pubDate </span><span style="color: #D32F2F">-lt</span><span style="color: #24292EFF"> (</span><span style="color: #6F42C1">Get-Date</span><span style="color: #24292EFF">).AddYears(</span><span style="color: #1976D2">-2</span><span style="color: #24292EFF">)) {</span></span>
<span class="line"><span style="color: #24292EFF">    $xml.rss.channel.RemoveChild(</span><span style="color: #1976D2">$_</span><span style="color: #24292EFF">) </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Out-Null</span></span>
<span class="line"><span style="color: #24292EFF">  }</span></span>
<span class="line"><span style="color: #24292EFF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #C2C3C5"># Create the new entry</span></span>
<span class="line"><span style="color: #24292EFF">$rssChannel </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $xml.rss.channel</span></span>
<span class="line"><span style="color: #24292EFF">$thisItem </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> createRssElement </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">elementName </span><span style="color: #22863A">&#39;item&#39;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">value </span><span style="color: #22863A">&#39;&#39;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">parent $rssChannel</span></span>
<span class="line"><span style="color: #1976D2">$null</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> createRssElement </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">elementName </span><span style="color: #22863A">&#39;title&#39;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">value $title </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">parent $thisItem</span></span>
<span class="line"><span style="color: #1976D2">$null</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> createRssElement </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">elementName </span><span style="color: #22863A">&#39;link&#39;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">value </span><span style="color: #22863A">&#39;http://vbrownbag.com&#39;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">parent $thisItem</span></span>
<span class="line"><span style="color: #1976D2">$null</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> createRssElement </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">elementName </span><span style="color: #22863A">&#39;description&#39;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">value $description </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">parent $thisItem</span></span>
<span class="line"><span style="color: #1976D2">$null</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> createRssElement </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">elementName </span><span style="color: #22863A">&#39;guid&#39;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">value $url </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">parent $thisItem</span></span>
<span class="line"><span style="color: #24292EFF">$enclosure </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> createRssElement </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">elementName </span><span style="color: #22863A">&#39;enclosure&#39;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">parent $thisItem</span></span>
<span class="line"><span style="color: #1976D2">$null</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $enclosure.SetAttribute(</span><span style="color: #22863A">&#39;url&#39;</span><span style="color: #D32F2F">,</span><span style="color: #24292EFF"> $url)</span></span>
<span class="line"><span style="color: #1976D2">$null</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $enclosure.SetAttribute(</span><span style="color: #22863A">&#39;length&#39;</span><span style="color: #D32F2F">,</span><span style="color: #24292EFF"> $mp4file.Size)</span></span>
<span class="line"><span style="color: #1976D2">$null</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $enclosure.SetAttribute(</span><span style="color: #22863A">&#39;type&#39;</span><span style="color: #D32F2F">,</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&#39;video/mp4&#39;</span><span style="color: #24292EFF">)</span></span>
<span class="line"><span style="color: #1976D2">$null</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> createRssElement </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">elementName </span><span style="color: #22863A">&#39;category&#39;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">value </span><span style="color: #22863A">&quot;Podcasts&quot;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">parent $thisItem</span></span>
<span class="line"><span style="color: #24292EFF">$pubDate </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Get-Date</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Format </span><span style="color: #22863A">&#39;r&#39;</span></span>
<span class="line"><span style="color: #1976D2">$null</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> createRssElement </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">elementName </span><span style="color: #22863A">&#39;pubDate&#39;</span><span style="color: #24292EFF"> $pubDate </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">parent $thisItem</span></span>
<span class="line"></span>
<span class="line"><span style="color: #C2C3C5"># Sort by date and remove any duplicates</span></span>
<span class="line"><span style="color: #24292EFF">$orderedItems </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $rssChannel.Item </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Group-Object</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Property pubdate </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">ForEach-Object</span><span style="color: #24292EFF"> { </span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #1976D2">$_.Group.Count</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-gt</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">1</span><span style="color: #24292EFF">) {</span></span>
<span class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">for</span><span style="color: #24292EFF"> ($i </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">1</span><span style="color: #24292EFF">; $i </span><span style="color: #D32F2F">-lt</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">$_.Group.Count</span><span style="color: #24292EFF">; $i</span><span style="color: #D32F2F">++</span><span style="color: #24292EFF">) {</span></span>
<span class="line"><span style="color: #24292EFF">        </span><span style="color: #6F42C1">Write-Output</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;Duplicate item: </span><span style="color: #D32F2F">$</span><span style="color: #22863A">(</span><span style="color: #1976D2">$_.Group</span><span style="color: #22863A">[$i].title)</span><span style="color: #22863A">&quot;</span></span>
<span class="line"><span style="color: #24292EFF">    }</span></span>
<span class="line"><span style="color: #24292EFF">  }</span></span>
<span class="line"><span style="color: #24292EFF">  $duplicatesCount </span><span style="color: #D32F2F">+=</span><span style="color: #24292EFF"> (</span><span style="color: #1976D2">$_.Group.Count</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">1</span><span style="color: #24292EFF">)</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #1976D2">$_.Group</span><span style="color: #24292EFF">[</span><span style="color: #1976D2">0</span><span style="color: #24292EFF">]</span></span>
<span class="line"><span style="color: #24292EFF">} </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF">  </span><span style="color: #6F42C1">Sort-Object</span><span style="color: #24292EFF"> { [</span><span style="color: #D32F2F">DateTime</span><span style="color: #24292EFF">]::Parse(</span><span style="color: #1976D2">$_.pubDate</span><span style="color: #24292EFF">) } </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Descending</span></span>
<span class="line"></span>
<span class="line"><span style="color: #C2C3C5"># Only use the first 300 because Apple doesn&#39;t like more than that</span></span>
<span class="line"><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> ($orderedItems.Count </span><span style="color: #D32F2F">-gt</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">300</span><span style="color: #24292EFF">) {</span></span>
<span class="line"><span style="color: #24292EFF">  $orderedItems </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $orderedItems </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Select-Object</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">First </span><span style="color: #1976D2">300</span></span>
<span class="line"><span style="color: #24292EFF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #C2C3C5"># Remove all items from the channel</span></span>
<span class="line"><span style="color: #24292EFF">$itemsToRemove </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $xml.rss.channel.item.Clone()</span></span>
<span class="line"><span style="color: #D32F2F">foreach</span><span style="color: #24292EFF"> ($item </span><span style="color: #D32F2F">in</span><span style="color: #24292EFF"> $itemsToRemove) {</span></span>
<span class="line"><span style="color: #24292EFF">  $xml.rss.channel.RemoveChild($item) </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Out-Null</span></span>
<span class="line"><span style="color: #24292EFF">}</span></span>
<span class="line"><span style="color: #24292EFF">        </span></span>
<span class="line"><span style="color: #C2C3C5"># Add the ordered items back to the channel</span></span>
<span class="line"><span style="color: #D32F2F">foreach</span><span style="color: #24292EFF"> ($item </span><span style="color: #D32F2F">in</span><span style="color: #24292EFF"> $orderedItems) {</span></span>
<span class="line"><span style="color: #24292EFF">  $xml.rss.channel.AppendChild($item) </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Out-Null</span></span>
<span class="line"><span style="color: #24292EFF">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #C2C3C5"># Timestamp this newest version of the file and write to disk</span></span>
<span class="line"><span style="color: #24292EFF">$xml.rss.channel.lastBuildDate </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> (</span><span style="color: #6F42C1">Get-Date</span><span style="color: #24292EFF">).ToUniversalTime().ToString(</span><span style="color: #22863A">&quot;r&quot;</span><span style="color: #24292EFF">)</span></span>
<span class="line"><span style="color: #24292EFF">$xmlWriterSettings </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">new-object</span><span style="color: #24292EFF"> System.Xml.XmlWriterSettings</span></span>
<span class="line"><span style="color: #24292EFF">$xmlWriterSettings.CloseOutput </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">$true</span></span>
<span class="line"><span style="color: #24292EFF">$xmlWriterSettings.IndentChars </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;    &quot;</span></span>
<span class="line"><span style="color: #24292EFF">$xmlWriterSettings.Indent </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">$true</span></span>
<span class="line"><span style="color: #24292EFF">$xmlWriterSettings.NewLineOnAttributes </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">$true</span></span>
<span class="line"><span style="color: #24292EFF">$xmlWriterSettings.NewLineHandling </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">System.Xml.NewLineHandling</span><span style="color: #24292EFF">]::Replace</span></span>
<span class="line"><span style="color: #24292EFF">$xmlWriter </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">System.Xml.XmlWriter</span><span style="color: #24292EFF">]::Create($xmlbackupPath</span><span style="color: #D32F2F">,</span><span style="color: #24292EFF"> $xmlWriterSettings)</span></span>
<span class="line"><span style="color: #1976D2">$null</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $xml.WriteTo($xmlWriter)</span></span>
<span class="line"><span style="color: #24292EFF">$xmlWriter.Close()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #C2C3C5"># SFTP the file to where it lives</span></span>
<span class="line"><span style="color: #24292EFF">$sftp_password </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> decrypt </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">ciphertext $</span><span style="color: #1976D2">env:</span><span style="color: #24292EFF">sftp_password</span></span>
<span class="line"><span style="color: #24292EFF">$sftp_credential </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">New-Object</span><span style="color: #24292EFF"> System.Management.Automation.PSCredential($</span><span style="color: #1976D2">env:</span><span style="color: #24292EFF">sftp_username</span><span style="color: #D32F2F">,</span><span style="color: #24292EFF"> (</span><span style="color: #6F42C1">ConvertTo-SecureString</span><span style="color: #24292EFF"> $sftp_password </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">AsPlainText </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Force))</span></span>
<span class="line"><span style="color: #24292EFF">$sftpClient </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Connect-SFTP</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Server $</span><span style="color: #1976D2">env:</span><span style="color: #24292EFF">sftp_host </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Credential $sftp_credential </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Port $</span><span style="color: #1976D2">env:</span><span style="color: #24292EFF">sftp_port</span></span>
<span class="line"><span style="color: #6F42C1">Send-SFTPFile</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">SftpClient $SftpClient </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">LocalPath $filepath </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">RemotePath (</span><span style="color: #6F42C1">Join-Path</span><span style="color: #24292EFF"> $</span><span style="color: #1976D2">env:</span><span style="color: #24292EFF">sftp_destination $</span><span style="color: #1976D2">env:</span><span style="color: #24292EFF">rssxml_filename) </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">AllowOverride</span></span></code></pre></div>



<p class="wp-block-paragraph">Well, I think I&#8217;ve reached the logical end of this series. We&#8217;ve covered a lot of ground, and I&#8217;ve learned a whole lot. Thanks for reading along with me! <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
]]></content:encoded>
					
		
		
		
		<series:name><![CDATA[What's in the bag? Behind the scenes at vBrownBag.com]]></series:name>
<post-id xmlns="com-wordpress:feed-additions:1">1667</post-id>	</item>
		<item>
		<title>Part 5 of &#8220;What&#8217;s in the bag?&#8221; Behind the scenes at vBrownBag.com</title>
		<link>https://damiankarlson.com/2024/05/22/part-5-of-whats-in-the-bag-behind-the-scenes-at-vbrownbag-com/</link>
		
		<dc:creator><![CDATA[Damian Karlson]]></dc:creator>
		<pubDate>Wed, 22 May 2024 20:56:48 +0000</pubDate>
				<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[#vBrownBag]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Lambda]]></category>
		<guid isPermaLink="false">https://damiankarlson.com/?p=1650</guid>

					<description><![CDATA[Part 4 of this series covered orchestration of the AWS Lambda function using AWS Step Functions &#38; EventBridge. In this post, I&#8217;d like to cover Google OAuth with PSAuthClient, decrypting AWS Lambda environment variables, and more. Google OAuth with PSAuthClient Some quick introductory info: there are two ways of authenticating with the YouTube data API. [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Part 4 of this series covered orchestration of the AWS Lambda function using AWS Step Functions &amp; EventBridge. In this post, I&#8217;d like to cover Google OAuth with PSAuthClient, decrypting AWS Lambda environment variables, and more.</p>



<h2 class="wp-block-heading">Google OAuth with PSAuthClient</h2>



<p class="wp-block-paragraph">Some quick introductory info: there are <a href="https://developers.google.com/youtube/registering_an_application">two ways of authenticating</a> with the YouTube data API. The first is through an API key created through <a href="https://console.cloud.google.com">https://console.cloud.google.com</a>. It&#8217;s an alpha-numeric string that can be appended to REST API calls against endpoints such as <a href="https://www.googleapis.com/youtube/v3/videos">https://www.googleapis.com/youtube/v3/videos</a>. However, the API key doesn&#8217;t allow privileged access to an account, and for that you need to use OAuth.</p>



<p class="wp-block-paragraph">There&#8217;s a ton of helpful documentation on <a href="https://developers.google.com/identity/protocols/oauth2/web-server">Google OAuth</a>, but my goal is not to repeat all of that here, nor talk about creating an app in Google&#8217;s Cloud console. Rather, I&#8217;d like to talk about how I used <a href="https://github.com/alflokken/PSAuthClient">PSAuthClient</a> to meet my OAuth needs using PowerShell. The two most important pieces of information needed to get started are the <code>client_id</code> and <code>client_secret</code>. The first uniquely identifies the Google app, and the second is a client secret string. </p>



<p class="wp-block-paragraph">After installing PSAuthClient using <code>Install-Module</code>, I used <code>Invoke-OAuth2AuthorizationEndpoint</code> to make the initial <a href="https://developers.google.com/identity/protocols/oauth2/web-server#obtainingaccesstokens">OAuth request</a> in order to begin building my <code>$authorization</code> object. The <code>customParameters</code> hashtable is important as I need to be able to refresh the OAuth token without requiring human interaction every time, since this is running in a Lambda function. Google OAuth also needs the <code>client_secret</code> in the <code>$authorization</code> object, so I add it to the object. Finally, I use the <code>Invoke-OAuth2TokenEndpoint</code> cmdlet to get the <code>$token</code> object by splatting the <code>$authorization</code> object (passing a collection of parameter values to a command as a unit).</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:0.9em;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-width:calc(1 * 0.6 * 0.9em);line-height:1.25em;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#f2f2f2;color:#2f363c">PowerShell</span><span role="button" tabindex="0" data-code="$authorization = Invoke-OAuth2AuthorizationEndpoint -uri $uri -client_id $client_id -redirect_uri $redirect_uri -response_type &quot;code&quot; -scope $scope -customParameters @{ access_type = &quot;offline&quot;; include_granted_scopes = &quot;true&quot; }
$authorization.Add(&quot;client_secret&quot;, $client_secret)
$token = Invoke-OAuth2TokenEndpoint -uri $uri @authorization" style="color:#24292eff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki min-light" style="background-color: #ffffff" tabindex="0"><code><span class="line"><span style="color: #24292EFF">$authorization </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Invoke-OAuth2AuthorizationEndpoint</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">uri $uri </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">client_id $client_id </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">redirect_uri $redirect_uri </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">response_type </span><span style="color: #22863A">&quot;code&quot;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">scope $scope </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">customParameters </span><span style="color: #D32F2F">@</span><span style="color: #24292EFF">{ access_type </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;offline&quot;</span><span style="color: #24292EFF">; include_granted_scopes </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;true&quot;</span><span style="color: #24292EFF"> }</span></span>
<span class="line"><span style="color: #24292EFF">$authorization.Add(</span><span style="color: #22863A">&quot;client_secret&quot;</span><span style="color: #D32F2F">,</span><span style="color: #24292EFF"> $client_secret)</span></span>
<span class="line"><span style="color: #24292EFF">$token </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Invoke-OAuth2TokenEndpoint</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">uri $uri @authorization</span></span></code></pre></div>



<p class="wp-block-paragraph"><code>$token</code> is a PSCustom object that contains <code>access_token</code> and <code>refresh_token</code> strings. The <code>access_token</code> expires in about an hour, which means that I need to get a new one whenever I&#8217;m making an OAuth request. This is accomplished through <code>Invoke-OAuth2TokenEndpoint</code>.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:0.9em;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25em;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#f2f2f2;color:#2f363c">PowerShell</span><span role="button" tabindex="0" data-code="$token = Invoke-OAuth2TokenEndpoint -uri $uri -refresh_token $refresh_token -client_id $client_id -client_secret $client_secret" style="color:#24292eff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki min-light" style="background-color: #ffffff" tabindex="0"><code><span class="line"><span style="color: #24292EFF">$token </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Invoke-OAuth2TokenEndpoint</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">uri $uri </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">refresh_token $refresh_token </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">client_id $client_id </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">client_secret $client_secret</span></span></code></pre></div>



<p class="wp-block-paragraph">Now I can begin building a new <code>$youtubeService</code> object for uploading new videos, assigning them to playlists, searching, etc.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:0.9em;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25em;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#f2f2f2;color:#2f363c">PowerShell</span><span role="button" tabindex="0" data-code="$googleCredential = [Google.Apis.Auth.OAuth2.GoogleCredential]::FromAccessToken($token.access_token).CreateScoped($token.scope)

# Create a new YouTube service
$youtubeService = New-Object Google.Apis.YouTube.v3.YouTubeService(
    New-Object Google.Apis.Services.BaseClientService+Initializer -Property @{
        HttpClientInitializer = $googleCredential
        ApplicationName       = &quot;YouTube Uploader&quot;
    }
)" style="color:#24292eff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki min-light" style="background-color: #ffffff" tabindex="0"><code><span class="line"><span style="color: #24292EFF">$googleCredential </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">Google.Apis.Auth.OAuth2.GoogleCredential</span><span style="color: #24292EFF">]::FromAccessToken($token.access_token).CreateScoped($token.scope)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #C2C3C5"># Create a new YouTube service</span></span>
<span class="line"><span style="color: #24292EFF">$youtubeService </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">New-Object</span><span style="color: #24292EFF"> Google.Apis.YouTube.v3.YouTubeService(</span></span>
<span class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">New-Object</span><span style="color: #24292EFF"> Google.Apis.Services.BaseClientService</span><span style="color: #D32F2F">+</span><span style="color: #24292EFF">Initializer </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Property </span><span style="color: #D32F2F">@</span><span style="color: #24292EFF">{</span></span>
<span class="line"><span style="color: #24292EFF">        HttpClientInitializer </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $googleCredential</span></span>
<span class="line"><span style="color: #24292EFF">        ApplicationName       </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;YouTube Uploader&quot;</span></span>
<span class="line"><span style="color: #24292EFF">    }</span></span>
<span class="line"><span style="color: #24292EFF">)</span></span></code></pre></div>



<p class="wp-block-paragraph">I&#8217;d like to note for the purposes of clarity, that I had to use <code>Invoke-OAuth2AuthorizationEndpoint</code> from my desktop, as it uses <a href="https://learn.microsoft.com/en-us/microsoft-edge/webview2/">WebView2</a> to handle web interaction/approving the credentials. PSAuthClient is a PowerShell Core &amp; Desk module which throws errors when I require the entire module in my Lambda script. However, <code>Invoke-OAuth2TokenEndpoint</code> is a PSCore cmdlet, so I&#8217;m able to include the .ps1 file directly in my Lambda zip file.</p>



<h2 class="wp-block-heading" id="variables">Encrypted environment variables in Lambda</h2>


<div class="wp-block-image">
<figure class="alignright size-full is-resized"><img fetchpriority="high" decoding="async" width="594" height="641" src="https://damiankarlson.com/wp-content/uploads/2024/05/aws_lambda_env_var.png" alt="Screenshot of the AWS Lambda configuration panel showing the definition of environment variables." class="wp-image-1653" style="width:490px" srcset="https://damiankarlson.com/wp-content/uploads/2024/05/aws_lambda_env_var.png 594w, https://damiankarlson.com/wp-content/uploads/2024/05/aws_lambda_env_var-278x300.png 278w, https://damiankarlson.com/wp-content/uploads/2024/05/aws_lambda_env_var-139x150.png 139w" sizes="(max-width: 594px) 100vw, 594px" /></figure>
</div>


<p class="wp-block-paragraph">As you can see above, I&#8217;m storing a lot of sensitive information in variables. I decided during the early stages of working on Meatgrinder to use <a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html?icmpid=docs_lambda_help">Lambda environment variables</a> instead of storing those values directly in the script, or a secrets file, etc. The variables &amp; their values are defined in the AWS Lambda configuration panel. When the Lambda function runs, those variables are available to the script with the <code>$env</code> prefix, so the value of <code>oauth_client_id</code> is available as <code>$env:oauth_client_id</code>, and so on. For the sensitive variables, I chose to create an AWS KMS symmetric key to encrypt &amp; decrypt my environment variables, and require the <code>AWS.Tools.KeyManagementService</code> module in my script, which includes the <code>Invoke-KMSDecrypt</code> cmdlet. Now you might be thinking, &#8220;oh, I can just use that cmdlet to directly decrypt my variable!&#8221; and you&#8217;d be wrong just like I was. There&#8217;s actually a few extra steps required. </p>



<p class="wp-block-paragraph">First, the encryption context needs to be defined, and in this case, it&#8217;s the environment variable <code>AWS_LAMBDA_FUNCTION_NAME</code>. (The encryption context is defined in the execution role policy that allows the function the <code>kms:decrypt</code> action using the resource arn of the KMS key.) Then the ciphertext value stored in the encrypted environment variable needs to be converted from a base64-encoded string to a byte array. The context and the encrypted byte array are passed to the <code>Invoke-KMSDecrypt</code> cmdlet with the <code>-Select Plaintext</code> parameter which asks for the plaintext value instead of the whole service response (which is the default), and a decrypted byte array is returned. That byte array is converted to a string which is the decrypted plaintext value. That&#8217;s a lot, right? If you were setting up the encryption configuration for environment variables chose the &#8220;enable helpers&#8221; checkbox, AWS would present you with popup containing a .NET 8 snippet that does all of this, but it doesn&#8217;t show it in PowerShell. So, here&#8217;s the PowerShell equivalent:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:0.9em;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25em;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#f2f2f2;color:#2f363c">PowerShell</span><span role="button" tabindex="0" data-code="&lt;#
.SYNOPSIS
   Helper function to decrypt a ciphertext using the AWS Key Management Service (KMS).
.PARAMETER ciphertext
    The ciphertext to decrypt.
.OUTPUTS
    Returns the plain text
.EXAMPLE
   decrypt -ciphertext &quot;ciphertext&quot;
#&gt;
function decrypt {
    param (
        [Parameter(Mandatory = $true)]
        [string]$ciphertext
    )
   
    $encryptionContext = @{&quot;LambdaFunctionName&quot; = $env:AWS_LAMBDA_FUNCTION_NAME }
    $cipherbytes = [System.Convert]::FromBase64String($ciphertext)
    $response = Invoke-KMSDecrypt -CiphertextBlob $cipherbytes -EncryptionContext $encryptionContext -Select Plaintext
    return [System.Text.Encoding]::UTF8.GetString($response.ToArray())
}" style="color:#24292eff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki min-light" style="background-color: #ffffff" tabindex="0"><code><span class="line"><span style="color: #C2C3C5">&lt;#</span></span>
<span class="line"><span style="color: #C2C3C5">.</span><span style="color: #D32F2F">SYNOPSIS</span></span>
<span class="line"><span style="color: #C2C3C5">   Helper function to decrypt a ciphertext using the AWS Key Management Service (KMS).</span></span>
<span class="line"><span style="color: #C2C3C5">.</span><span style="color: #D32F2F">PARAMETER</span><span style="color: #C2C3C5"> </span><span style="color: #D32F2F">ciphertext</span></span>
<span class="line"><span style="color: #C2C3C5">    The ciphertext to decrypt.</span></span>
<span class="line"><span style="color: #C2C3C5">.</span><span style="color: #D32F2F">OUTPUTS</span></span>
<span class="line"><span style="color: #C2C3C5">    Returns the plain text</span></span>
<span class="line"><span style="color: #C2C3C5">.</span><span style="color: #D32F2F">EXAMPLE</span></span>
<span class="line"><span style="color: #C2C3C5">   decrypt -ciphertext &quot;ciphertext&quot;</span></span>
<span class="line"><span style="color: #C2C3C5">#&gt;</span></span>
<span class="line"><span style="color: #D32F2F">function</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">decrypt</span><span style="color: #24292EFF"> {</span></span>
<span class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">param</span><span style="color: #24292EFF"> (</span></span>
<span class="line"><span style="color: #24292EFF">        [</span><span style="color: #6F42C1">Parameter</span><span style="color: #24292EFF">(Mandatory </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">$true</span><span style="color: #24292EFF">)]</span></span>
<span class="line"><span style="color: #24292EFF">        [</span><span style="color: #D32F2F">string</span><span style="color: #24292EFF">]$ciphertext</span></span>
<span class="line"><span style="color: #24292EFF">    )</span></span>
<span class="line"><span style="color: #24292EFF">   </span></span>
<span class="line"><span style="color: #24292EFF">    $encryptionContext </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">@</span><span style="color: #24292EFF">{</span><span style="color: #22863A">&quot;LambdaFunctionName&quot;</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> $</span><span style="color: #1976D2">env:</span><span style="color: #24292EFF">AWS_LAMBDA_FUNCTION_NAME }</span></span>
<span class="line"><span style="color: #24292EFF">    $cipherbytes </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">System.Convert</span><span style="color: #24292EFF">]::FromBase64String($ciphertext)</span></span>
<span class="line"><span style="color: #24292EFF">    $response </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Invoke-KMSDecrypt</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">CiphertextBlob $cipherbytes </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">EncryptionContext $encryptionContext </span><span style="color: #D32F2F">-</span><span style="color: #24292EFF">Select Plaintext</span></span>
<span class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">System.Text.Encoding</span><span style="color: #24292EFF">]::UTF8.GetString($response.ToArray())</span></span>
<span class="line"><span style="color: #24292EFF">}</span></span></code></pre></div>



<p class="wp-block-paragraph">Well, if you read this far, congratulations! Or, hi Mom, whichever the case may be. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /> In the next post, I&#8217;d like to cover WordPress REST API and RSS XML updates. Cheers!</p>
]]></content:encoded>
					
		
		
		
		<series:name><![CDATA[What's in the bag? Behind the scenes at vBrownBag.com]]></series:name>
<post-id xmlns="com-wordpress:feed-additions:1">1650</post-id>	</item>
		<item>
		<title>Part 4 of &#8220;What&#8217;s in the bag?&#8221; Behind the scenes at vBrownBag.com</title>
		<link>https://damiankarlson.com/2024/05/13/part-4-of-whats-in-the-bag-behind-the-scenes-at-vbrownbag-com/</link>
		
		<dc:creator><![CDATA[Damian Karlson]]></dc:creator>
		<pubDate>Mon, 13 May 2024 19:35:21 +0000</pubDate>
				<category><![CDATA[AWS]]></category>
		<category><![CDATA[Lambda]]></category>
		<category><![CDATA[#vBrownBag]]></category>
		<category><![CDATA[PowerShell]]></category>
		<guid isPermaLink="false">https://damiankarlson.com/?p=1632</guid>

					<description><![CDATA[In Part 3 of this series, I covered the development environment &#38; tools. In this installment, I&#8217;d like to show you how I&#8217;ve decided to orchestrate the workflow around the Lambda function. You may also want to review the overall process in Part 2, as I&#8217;ll be referring to how I decided to implement the [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In <a href="https://damiankarlson.com/2024/05/05/part-3-of-of-whats-in-the-bag-behind-the-scenes-at-the-vbrownbag/" data-type="post" data-id="1617">Part 3</a> of this series, I covered the development environment &amp; tools. In this installment, I&#8217;d like to show you how I&#8217;ve decided to orchestrate the workflow around the Lambda function. You may also want to review the overall process in <a href="https://damiankarlson.com/2024/04/21/part-2-of-whats-in-the-bag-behind-the-scenes-at-vbrownbag-com/" data-type="post" data-id="1535">Part 2</a>, as I&#8217;ll be referring to how I decided to implement the process on AWS.</p>



<p class="wp-block-paragraph">One of this function&#8217;s primary design goals was for it to be entirely event-driven. I didn&#8217;t want to be concerned with giving future friends of the vBrownBag access to invoking the Lambda function directly, or to be concerned with any other details than simply uploading a video file along with a metadata file. However, I couldn&#8217;t quite figure out a graceful way to make sure both the .mp4 &amp; .csv files were present before moving forward with the automation process. If you&#8217;ve dealt with AWS, you&#8217;re probably well aware that there are many different ways to solve a problem. That&#8217;s the benefits of AWS&#8217; focus on cloud primitives (vs a cloud like Azure that tends to focus more on solutions). Here&#8217;s a few examples: 1.) I could&#8217;ve simply triggered the Lambda function from an S3 PUT and then let Lambda exit if both files weren&#8217;t present, but that would&#8217;ve introduced unnecessary Lambda executions that would&#8217;ve been unsuccessful half the time. 2.) I could&#8217;ve allowed a human to invoke the function directly, after uploading both files. This wasn&#8217;t a bad idea, but still required more interaction than I wanted. 3.) I could&#8217;ve had Lambda run regularly to check for new files in an S3 bucket, but that&#8217;s a clumsier approach. Honestly, the list could go on and on. I finally decided to build an AWS EventBridge rule that would start a Step Functions state machine execution after a .csv file is uploaded to an S3 bucket. As these notifications happen in millisecond time frames, the first thing Step Functions does is wait before asking the Lambda function to check if both files are present. Ultimately, I decided that being event-driven doesn&#8217;t mean that event responses have to happen immediately, as there&#8217;s no point in having millisecond responses to find files that aren&#8217;t done uploading. </p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="720" height="869" src="https://damiankarlson.com/wp-content/uploads/2024/05/stepfunctions_graph.png" alt="An AWS Step Function graph depicting the state machine workflow that orchestrates the overall Meatgrinder process." class="wp-image-1633" srcset="https://damiankarlson.com/wp-content/uploads/2024/05/stepfunctions_graph.png 720w, https://damiankarlson.com/wp-content/uploads/2024/05/stepfunctions_graph-249x300.png 249w, https://damiankarlson.com/wp-content/uploads/2024/05/stepfunctions_graph-124x150.png 124w" sizes="auto, (max-width: 720px) 100vw, 720px" /></figure>



<p class="wp-block-paragraph">As you can see in the graph, there&#8217;s an initial 5 minute wait to allow for large .mp4 files to finish uploading. After that, the Lambda function is invoked to check for matching .mp4 &amp; .csv files in a private S3 bucket. If they exist, JSON is returned to the state machine indicating that the process can move forward. If not, then JSON is returned that it&#8217;s still waiting. This loop eventually breaks out and exits after running a few times, just to make sure that this doesn&#8217;t run indefinitely during an incomplete upload. Each JSON output is interpreted and acted upon by the Lambda function, which also constructs JSON responses to the state machine upon completion. </p>



<p class="wp-block-paragraph">Here&#8217;s example JSON that EventBridge sends to Step Functions (using InputPath filtering on <em>$.detail.object</em>). Step Functions invokes the Lambda function with the JSON as input. Inside the Lambda function, the input is received as <em>$LambdaInput</em> which is a PSObject that contains input to the function&#8217;s configured <a href="https://docs.aws.amazon.com/lambda/latest/dg/powershell-handler.html" data-type="link" data-id="https://docs.aws.amazon.com/lambda/latest/dg/powershell-handler.html">handler method</a>. </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:0.9em;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25em;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#f2f2f2;color:#2f363c">JSON</span><span role="button" tabindex="0" data-code="{
  &quot;key&quot;: &quot;test2-long.csv&quot;,
  &quot;size&quot;: 644,
  &quot;etag&quot;: &quot;26f04fed485a8b0487b17974b313b52d&quot;,
  &quot;version-id&quot;: &quot;w9ywc77rdDz49dYmUPRRTEVzcQqAifIM&quot;,
  &quot;sequencer&quot;: &quot;00663D74124FC29E94&quot;
}" style="color:#24292eff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki min-light" style="background-color: #ffffff" tabindex="0"><code><span class="line"><span style="color: #24292EFF">{</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">&quot;key&quot;</span><span style="color: #212121">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;test2-long.csv&quot;</span><span style="color: #212121">,</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">&quot;size&quot;</span><span style="color: #212121">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">644</span><span style="color: #212121">,</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">&quot;etag&quot;</span><span style="color: #212121">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;26f04fed485a8b0487b17974b313b52d&quot;</span><span style="color: #212121">,</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">&quot;version-id&quot;</span><span style="color: #212121">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;w9ywc77rdDz49dYmUPRRTEVzcQqAifIM&quot;</span><span style="color: #212121">,</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">&quot;sequencer&quot;</span><span style="color: #212121">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;00663D74124FC29E94&quot;</span></span>
<span class="line"><span style="color: #24292EFF">}</span></span></code></pre></div>



<p class="wp-block-paragraph">The Meatgrinder Lambda function processes this input, checks to see if a matching .mp4 file exists, and then responds with JSON to the Step Function state machine. All of the JSON returned by the function is built as a <a href="https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-pscustomobject?view=powershell-7.4">PSCustomObject</a> and then converted to JSON using <a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertto-json?view=powershell-7.4">ConvertTo-JSON</a>.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:0.9em;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25em;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#f2f2f2;color:#2f363c">JSON</span><span role="button" tabindex="0" data-code="{
  &quot;action&quot;: &quot;upload&quot;,
  &quot;video&quot;: {
    &quot;name&quot;: &quot;test2-long.mp4&quot;,
    &quot;privacyStatus&quot;: &quot;public&quot;,
    &quot;csv&quot;: &quot;test2-long.csv&quot;
  }
}" style="color:#24292eff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki min-light" style="background-color: #ffffff" tabindex="0"><code><span class="line"><span style="color: #24292EFF">{</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">&quot;action&quot;</span><span style="color: #212121">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;upload&quot;</span><span style="color: #212121">,</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">&quot;video&quot;</span><span style="color: #212121">:</span><span style="color: #24292EFF"> {</span></span>
<span class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">&quot;name&quot;</span><span style="color: #212121">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;test2-long.mp4&quot;</span><span style="color: #212121">,</span></span>
<span class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">&quot;privacyStatus&quot;</span><span style="color: #212121">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;public&quot;</span><span style="color: #212121">,</span></span>
<span class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">&quot;csv&quot;</span><span style="color: #212121">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;test2-long.csv&quot;</span></span>
<span class="line"><span style="color: #24292EFF">  }</span></span>
<span class="line"><span style="color: #24292EFF">}</span></span></code></pre></div>



<p class="wp-block-paragraph">Meatgrinder refreshes the OAuth token used to authenticate to our YouTube channel, builds a <em><a href="https://developers.google.com/youtube/v3/docs/videos">Google.Apis.YouTube.v3.Data.Video</a></em> object containing information about the video, uploads to YouTube, and then copies the .mp4 from the S3 working bucket over to our public bucket named as <em>videoId.mp4</em>, which reflects the unique ID that YouTube assigned the upload. At this point in the process, the YouTube data API is the source of truth for publish times, content duration, thumbnail images, etc. so an OAuth authenticated <em>Google.Apis.YouTube.v3.Data.VideoListResponse</em> object is converted to JSON and saved in the working bucket.</p>



<p class="wp-block-paragraph">An additional wait period is introduced to wait on the YouTube data API to signal that the video is done processing. As there&#8217;s no exponential backoff in Step Functions, I chose a wait period of 15 minutes, as an hour of HD video may take 20-30 minutes to process. Once the API says the video has been processed, Meatgrinder is invoked again to finish the vBrownBag.com blog post &amp; Apple Podcasts RSS update.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:0.9em;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25em;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#f2f2f2;color:#2f363c">JSON</span><span role="button" tabindex="0" data-code="{
  &quot;action&quot;: &quot;finalize&quot;,
  &quot;video&quot;: {
    &quot;id&quot;: &quot;ZshoNZd2-_g&quot;
  },
  &quot;status&quot;: &quot;processed&quot;
}" style="color:#24292eff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki min-light" style="background-color: #ffffff" tabindex="0"><code><span class="line"><span style="color: #24292EFF">{</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">&quot;action&quot;</span><span style="color: #212121">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;finalize&quot;</span><span style="color: #212121">,</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">&quot;video&quot;</span><span style="color: #212121">:</span><span style="color: #24292EFF"> {</span></span>
<span class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">&quot;id&quot;</span><span style="color: #212121">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;ZshoNZd2-_g&quot;</span></span>
<span class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span></span>
<span class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">&quot;status&quot;</span><span style="color: #212121">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">&quot;processed&quot;</span></span>
<span class="line"><span style="color: #24292EFF">}</span></span></code></pre></div>



<p class="wp-block-paragraph">I spent some time trying to figure out how to make sure that the function was idempotent, as I didn&#8217;t want to accidentally duplicate vBrownBag.com blog posts or RSS entries. I solved this by appending a <em>wpPost_Id</em> key/value pair in the JSON which won&#8217;t trigger a new blog post if the value isn&#8217;t null (meaning, a blog post with that id exists). Additionally, by using the YouTube videoId as a unique identifier, I&#8217;m able to find any XML entries that match that videoId and not add a new one if it already exists.</p>



<h2 class="wp-block-heading">Lambda function internal structure</h2>



<p class="wp-block-paragraph">In addition to the handler() function itself, the rest of the Meatgrinder Lambda function consists of PowerShell functions that are named like PowerShell cmdlets with <a href="https://learn.microsoft.com/en-us/powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands?view=powershell-7.4">approved verbs</a> such as <em>New-YouTubeVideo</em>, <em>Get-OAuthRefresh</em>, <em>Update-RSS</em>, etc. I also wrote extensive <a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comment_based_help?view=powershell-7.4" data-type="page" data-id="584">comment-based help</a> for the PowerShell functions. Parameters are defined (string, object, etc), consistently named, and enforced when passed between other PowerShell functions. I&#8217;ve also tried to consistently use <a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_try_catch_finally?view=powershell-7.4">try/catch blocks</a> for error handling, and logging Meatgrinder process steps to AWS CloudWatch using PowerShell&#8217;s Write-Host. These were intentional choices designed to document the code and to make it easier to understand for future maintainers (or myself, in 6 months!)</p>



<p class="wp-block-paragraph">Stay tuned for Part 5, where I&#8217;ll cover Google OAuth &amp; PSAuthClient, decrypting AWS Lambda environment variables, and more. Thanks for reading!</p>
]]></content:encoded>
					
		
		
		
		<series:name><![CDATA[What's in the bag? Behind the scenes at vBrownBag.com]]></series:name>
<post-id xmlns="com-wordpress:feed-additions:1">1632</post-id>	</item>
		<item>
		<title>Part 3 of “What’s in the bag?” Behind the scenes at vBrownBag.com</title>
		<link>https://damiankarlson.com/2024/05/05/part-3-of-of-whats-in-the-bag-behind-the-scenes-at-the-vbrownbag/</link>
		
		<dc:creator><![CDATA[Damian Karlson]]></dc:creator>
		<pubDate>Sun, 05 May 2024 23:33:49 +0000</pubDate>
				<category><![CDATA[Windows]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[#vBrownBag]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Lambda]]></category>
		<guid isPermaLink="false">https://damiankarlson.com/?p=1617</guid>

					<description><![CDATA[In Part 2, we covered the Meatgrinder process flow. In this post, we&#8217;ll cover the development environment and change management tools. I decided to write this Lambda function on my Windows 11 machine because I&#8217;d started this effort by writing code to &#8220;catch up&#8221; the website &#38; the podcast RSS file, and naively thought that [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In <a href="https://damiankarlson.com/2024/04/21/part-2-of-whats-in-the-bag-behind-the-scenes-at-vbrownbag-com/">Part 2</a>, we covered the Meatgrinder process flow. In this post, we&#8217;ll cover the development environment and change management tools.</p>



<p class="wp-block-paragraph">I decided to write this Lambda function on my Windows 11 machine because I&#8217;d started this effort by writing code to &#8220;catch up&#8221; the website &amp; the podcast RSS file, and naively thought that I could just move the code from Windows to Lambda. While it&#8217;s mostly true that the code is cross-platform there are some differences, such as case-sensitivity for paths/filenames/variables on Linux vs Windows&#8217; case-insensitivity. Having the benefit of hindsight, I should&#8217;ve developed in an environment that resembles where the code was running as I probably would&#8217;ve had fewer portability issues.</p>



<p class="wp-block-paragraph">That being said, here&#8217;s the environment I ended up with:</p>



<ul class="wp-block-list">
<li>Primary development on Windows 11 running PowerShell 7 and Visual Studio Code, connected to a GitHub repository and using <a href="https://github.com/features/copilot">GitHub Copilot</a></li>



<li>Amazon Linux running in WSL2.
<ul class="wp-block-list">
<li>This was a multi-step process, as I spun up a Ubuntu distro on WSL, then <a href="https://www.awsjunkie.com/install-docker-engine-containerd-and-docker-compose-on-wsl-2-docker-desktop-windows-subsystem-for-linux-windows/">installed Docker</a>.</li>



<li>After Docker was setup, I installed <a href="https://hub.docker.com/_/amazonlinux">Amazon Linux in Docker</a>, then exported the filesytem and imported into WSL as the default distro. <a href="https://www.awsjunkie.com/how-to-run-amazon-linux-2023-al2023-locally/">Instructions here</a>. Note, I could&#8217;ve installed Docker on Windows to perform the import/export, but I didn&#8217;t.</li>



<li>The default Amazon Linux image runs a minimal set of packages, so I used yum to groupinstall &#8220;Development Tools&#8221;, and installed wget to help pull down the rest of what I needed.</li>
</ul>
</li>



<li>Both operating systems run the <a href="https://learn.microsoft.com/en-us/dotnet/core/install/linux-fedora">dotnet 8 SDK</a>, <a href="https://docs.aws.amazon.com/lambda/latest/dg/csharp-package-cli.html">aws.lambda.tools for dotnet</a>, <a href="https://learn.microsoft.com/en-us/powershell/scripting/install/install-rhel?view=powershell-7.4">PowerShell 7</a>, the modular <a href="https://aws.amazon.com/powershell/">AWS PowerShell modules</a>, and the <a href="https://github.com/aws/aws-lambda-dotnet/tree/master/PowerShell">AWS Lambda Tools for PowerShell</a>.</li>



<li>Other PowerShell modules I&#8217;m using include <a href="https://github.com/alflokken/PSAuthClient">PSAuthClient</a> for handling OAuth2 tokens and <a href="https://github.com/darkoperator/Posh-SSH">Posh-SSH</a> to move a file from Lambda to <a href="https://vbrownbag.com">vbrownbag.com</a>.</li>
</ul>



<p class="wp-block-paragraph">I prefer the modular <a href="https://aws.amazon.com/powershell/">AWS PowerShell modules</a> as I can limit the module groups that are loaded into my Lambda function vs the <a href="https://www.powershellgallery.com/packages/AWSPowerShell">monolithic AWS PowerShell module</a> that includes everything but add unneeded start times to the function.</p>



<p class="wp-block-paragraph">The dotnet 8 SDK is a requirement for packaging Lambda functions on any platform. The <a href="https://docs.aws.amazon.com/lambda/latest/dg/csharp-package-cli.html">aws.lambda.tools for dotnet</a> are very helpful in creating new dotnet projects that will run on Lambda. The <a href="https://github.com/aws/aws-lambda-dotnet/tree/master/PowerShell">AWS Lambda Tools for PowerShell</a> are effectively wrapper cmdlets that simplify packaging of a function that lives in a single script, or is part of a bigger project.</p>



<h2 class="wp-block-heading">Thoughts on GitHub Copilot</h2>



<p class="wp-block-paragraph">First off &#8211; Copilot is darn near magical at times. Being able to have a conversation about code, solving specific problems, or even getting answers to things like &#8220;why am I getting this error?&#8221; is simply amazing. </p>


<div class="wp-block-image is-style-default">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="586" height="452" src="https://damiankarlson.com/wp-content/uploads/2024/05/copilot_chat.png" alt="A screenshot of me having a short discussion with GitHub Copilot" class="wp-image-1621" srcset="https://damiankarlson.com/wp-content/uploads/2024/05/copilot_chat.png 586w, https://damiankarlson.com/wp-content/uploads/2024/05/copilot_chat-300x231.png 300w, https://damiankarlson.com/wp-content/uploads/2024/05/copilot_chat-150x116.png 150w" sizes="auto, (max-width: 586px) 100vw, 586px" /><figcaption class="wp-element-caption">Emojis: the bane of my existence</figcaption></figure>
</div>


<p class="wp-block-paragraph">That said, it&#8217;s certainly not perfect. In fact, there have been a few times that I swear Copilot was drinking on the job. Sometimes it would answer in circles, give me bad ideas on how to solve things, forget the context of our conversation, or start using the &#8220;I&#8217;m just an AI&#8221; response when I asked it broader questions about my code, which is nearing about 1000 lines at this point. One time in particular stands out to me where it suggested a block of code that repeated a line twice. When I asked &#8220;why did you suggest doing this twice?&#8221; it denied that it did. When I said said that it absolutely <em>did</em> suggest it to me, it remembered that it did and then apologized about it.</p>



<p class="wp-block-paragraph">I think it would be helpful if there was clearer communication that Copilot can&#8217;t &#8220;see&#8221; everything you&#8217;re working on because it isn&#8217;t always clear. Its responses will say that it&#8217;s referencing lines of code, but why it chooses those lines isn&#8217;t obvious if it&#8217;s based on cursor position, the question itself, or what&#8217;s in the VS Code view port. Copilot also doesn&#8217;t ask clarifying questions; often it will just give an answer that&#8217;s not appropriate in context of the code being written or where it&#8217;s running.</p>



<p class="wp-block-paragraph">All of that said, I&#8217;m really excited to see where Copilot is headed. Having a tool like this one lowers the barrier to entry for everyone as Copilot can explain unfamiliar code, help translate from one type of code to another, and even translate into another spoken language. If you&#8217;re curious about trying out Copilot, as of this writing there&#8217;s a 30 day free trial for individual use. </p>



<h2 class="wp-block-heading">Coming up next</h2>



<p class="wp-block-paragraph">I&#8217;ve spent a lot of time detailing what I want to do and the tools I&#8217;m using to do it. Next up in the series, let&#8217;s start looking at code with specific examples. Talk to you then!</p>
]]></content:encoded>
					
		
		
		
		<series:name><![CDATA[What's in the bag? Behind the scenes at vBrownBag.com]]></series:name>
<post-id xmlns="com-wordpress:feed-additions:1">1617</post-id>	</item>
		<item>
		<title>Part 2 of &#8220;What&#8217;s in the bag?&#8221; Behind the scenes at vBrownBag.com</title>
		<link>https://damiankarlson.com/2024/04/21/part-2-of-whats-in-the-bag-behind-the-scenes-at-vbrownbag-com/</link>
		
		<dc:creator><![CDATA[Damian Karlson]]></dc:creator>
		<pubDate>Mon, 22 Apr 2024 03:21:13 +0000</pubDate>
				<category><![CDATA[#vBrownBag]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Lambda]]></category>
		<guid isPermaLink="false">https://damiankarlson.com/?p=1535</guid>

					<description><![CDATA[OK, so now that I&#8217;ve got this blog dusted off, decided on a new direction and archived all of the old posts, let&#8217;s get cracking on part 2 of this series. I&#8217;ll be going into very specific detail about what the new meatgrinder/automator process needs to do, and then branch out into how each of [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">OK, so now that I&#8217;ve got this blog dusted off, decided on a new direction and archived all of the old posts, let&#8217;s get cracking on part 2 of this series. I&#8217;ll be going into very specific detail about what the new meatgrinder/automator process needs to do, and then branch out into how each of those steps are accomplished in future posts.</p>



<h2 class="wp-block-heading">Quick note</h2>



<p class="wp-block-paragraph">Before we get started, I&#8217;d like to talk about what&#8217;s going on in a general sense. The majority of the new meatgrinder functionality will be done in PowerShell. Why PowerShell? Well, it&#8217;s what <a href="https://twitter.com/DemitasseNZ">Al</a> wrote for the previous iteration of the meatgrinder and I want to continue using it because <a href="https://damiankarlson.com/2024/04/21/whats-in-the-bag-behind-the-scenes-at-vbrownbag-com/#excitement">I love it</a>. Most of the PowerShell Internet calls will either be <a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-restmethod?view=powershell-7.4">Invoke-RestMethod</a> or <a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-webrequest?view=powershell-7.4">Invoke-WebRequest</a>. The AWS calls will use cmdlets from <a href="https://www.powershellgallery.com/packages/AWS.Tools.Common/">AWS.Tools.Common</a> and <a href="https://www.powershellgallery.com/packages/AWS.Tools.S3">AWS.Tools.S3</a> for reading/writing/deleting S3 objects. Output logging writes to AWS Cloudwatch, due to the way that Lambda handles output. Packaging the PowerShell function for Lambda will be covered later.</p>



<h2 class="wp-block-heading">Meatgrinder process</h2>



<ul class="wp-block-list">
<li>After a <a href="https://vbrownbag.com">vBrownBag</a> recording is live-streamed, the YouTube recording is manually downloaded locally for editing &amp; fancy on-screen graphics, YouTube shorts creation, etc.</li>



<li>A file containing metadata such as video title, description, tags, etc. and an .mp4 file with same name (but different extension) are uploaded to a dedicated AWS S3 bucket via <a href="https://aws.amazon.com/cli/">AWS CLI</a> or the <a href="https://www.powershellgallery.com/packages/AWS.Tools.S3/">AWS S3 PowerShell module</a>.</li>



<li>The Lambda function is invoked via the AWS CLI along with a JSON payload that specifies the video.mp4 &amp; video metadata file, and the function proceeds to do a number of things:
<ul class="wp-block-list">
<li>Verifies both the video file &amp; metadata file exist in the S3 bucket</li>



<li>Parses the metadata to create variables with the title, description, tags, etc.</li>



<li>Adds on a promotional content text block to the description, refreshes the OAuth token and then <code>POST</code>s the video to the <a href="https://developers.google.com/youtube/v3/docs/videos/insert">YouTube API videos:insert endpoint</a>, which responds in JSON. The unique YouTube video id is parsed from this response. Note: I&#8217;ll cover the OAuth token component in a future post about <a href="https://github.com/alflokken/PSAuthClient">PSAuthClient</a>.</li>



<li>Lambda asks for more information on that video id from <a href="https://developers.google.com/youtube/v3/docs/videos/list">YouTube&#8217;s videos:list endpoint</a> which responds in JSON with video id, <em>title</em>, <em>excerpt</em>, long <em>description</em>, <em>thumbnail</em> details, <em>publishedAt</em> (and more) and saves a copy of the media file (named as $videoId.mp4) &amp; $videoId.jpg thumbnail to our public S3 bucket.
<ul class="wp-block-list">
<li><em>I really just want the YouTube excerpt and publishedAt values versus reusing the original metadata, as I want the blog post &amp; RSS feed entry to have the same excerpt &amp; timestamp as it makes the blog post look cleaner in case of a longer description, or if I want to backdate the blog post in case this step is done later. An example of this would be the &#8220;catch-up&#8221; process I went through to get the website &amp; RSS feed caught up on the last 6 months of the YouTube channel when the former iteration of the meatgrinder script was inoperable.</em></li>
</ul>
</li>



<li>Lambda deletes the working bucket upload, as processing is done and any video &amp; thumbnails in our public bucket should avoid name collisions anyway by way of YouTube&#8217;s naming conventions.</li>



<li>Lambda creates the vBrownBag.com blogpost from the YouTube details using the original description (without the promotional text block), and <code>POST</code>s to <a href="https://vbrownbag.com">vBrownBag.com</a> using the WordPress REST API. More on that in a future post.</li>



<li>Lambda then <code>POST</code>s the YouTube thumbnail image to be used as the featured media (the post thumbnail), then links the featured media to the post. The WordPress REST API doesn&#8217;t allow side-loading the media, so we have to create the post, upload the media, then associate the post &amp; media.</li>



<li>Finally, Lambda grabs the Apple podcast RSS XML file, saves a backup to S3, adds the latest post to the top of the XML body, and sends it back to where it lives. I&#8217;ll show more on that process later, as it&#8217;s also pretty nifty. If you&#8217;re wondering &#8220;hey, what about us Android folks?!&#8221; the answer is that Google has decided YouTube will be its podcasting source too, so it&#8217;s effectively built-in to the <a href="https://www.youtube.com/@vBrownBag">vBrownBag YouTube channel</a>.</li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph">Here&#8217;s a rather simplified flow that I made with <a href="https://www.lucidchart.com/pages/">Lucidchart</a>. It&#8217;s not 100% exact, but it&#8217;s enough to get the idea across. In my mind, I&#8217;d like to actually map out the <code>POST</code>s and responses, but that feels a bit too&#8230; <em>extra</em>. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="658" src="https://damiankarlson.com/wp-content/uploads/2024/04/automator_workflow-1024x658.png" alt="" class="wp-image-1549" srcset="https://damiankarlson.com/wp-content/uploads/2024/04/automator_workflow-1024x658.png 1024w, https://damiankarlson.com/wp-content/uploads/2024/04/automator_workflow-300x193.png 300w, https://damiankarlson.com/wp-content/uploads/2024/04/automator_workflow-150x96.png 150w, https://damiankarlson.com/wp-content/uploads/2024/04/automator_workflow-768x494.png 768w, https://damiankarlson.com/wp-content/uploads/2024/04/automator_workflow-1536x987.png 1536w, https://damiankarlson.com/wp-content/uploads/2024/04/automator_workflow-2048x1317.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">That&#8217;s all for now. The next post in this series will be a brief overview of the development environment, change management, and the tools necessary to make all of this work.</p>
]]></content:encoded>
					
		
		
		
		<series:name><![CDATA[What's in the bag? Behind the scenes at vBrownBag.com]]></series:name>
<post-id xmlns="com-wordpress:feed-additions:1">1535</post-id>	</item>
		<item>
		<title>What&#8217;s in the bag? Behind the scenes at vBrownBag.com</title>
		<link>https://damiankarlson.com/2024/04/21/whats-in-the-bag-behind-the-scenes-at-vbrownbag-com/</link>
					<comments>https://damiankarlson.com/2024/04/21/whats-in-the-bag-behind-the-scenes-at-vbrownbag-com/#comments</comments>
		
		<dc:creator><![CDATA[Damian Karlson]]></dc:creator>
		<pubDate>Mon, 22 Apr 2024 00:32:45 +0000</pubDate>
				<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[#vBrownBag]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Lambda]]></category>
		<guid isPermaLink="false">https://damiankarlson.com/?p=1513</guid>

					<description><![CDATA[A few weeks ago, I asked my good friends Alastair Cooke &#38; Chris Williams how I could be useful to them &#38; the vBrownBag.com website. Al suggested that I take a look at the automation script, affectionately known as &#8220;the meatgrinder&#8221; that had been running behind the scenes for years but hadn&#8217;t been running for [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">A few weeks ago, I asked my good friends <a href="https://twitter.com/DemitasseNZ">Alastair Cooke</a> &amp; <a href="https://twitter.com/mistwire">Chris Williams</a> how I could be useful to them &amp; the <a href="https://vbrownbag.com" data-type="link" data-id="https://vbrownbag.com">vBrownBag.com</a> website. Al suggested that I take a look at the automation script, affectionately known as &#8220;the meatgrinder&#8221; that had been running behind the scenes for years but hadn&#8217;t been running for about the past 6 months. It was written mostly in PowerShell and ran on a Windows VM and its job was to process a video file (such as one from a live <a href="https://vbrownbag.com/category/vbrownbag/vbbtechtalks/">vBrownBag TechTalk</a> recorded at a conference, or a recording from the <a href="https://www.youtube.com/@vBrownBag">weekly video podcast</a>). The script would upload the file to AWS S3, post to the YouTube channel, update the Apple Podcasts RSS feed, and post by email to the WordPress install that powers vBrownBag. Finally, Al wanted it to run as an AWS Lambda function.</p>



<a href="#excitement" id="excitement"> </a>
<h2 class="wp-block-heading">Excitement intensifies</h2>



<p class="wp-block-paragraph">I was very excited, and still am, because I just love PowerShell. I find it fascinating and always have. I think it&#8217;s because I used to love bash scripting and PowerShell&#8217;s pipeline reminds me of that. Also, there&#8217;s super cool things like <a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_splatting?view=powershell-7.4">splatting</a> that just make life easier. Looking back on the first few weeks of working through this process, I realize that I&#8217;ve learned way more than merely reacquainting myself with PowerShell in a very short period of time. And, due to the fact that I&#8217;m running PowerShell on Lambda (which is an Amazon Linux image underneath) I&#8217;ve run into some unique issues that there aren&#8217;t a lot of Google results about. So I decided that I should start writing these down in public for a few reasons. The first one is documentation for myself &amp; any friend of the vBrownBag who may need to work on the meatgrinder in the future. Second, it&#8217;s for folks like me who are stuck trying to figure out things like the WordPress REST API, working through Google&#8217;s OAuth framework in order to post to YouTube, optimizing Lambda layers, and more.</p>



<h2 class="wp-block-heading">Bad data &amp; a good ending</h2>



<p class="wp-block-paragraph">However, before I get to all of that, I&#8217;d like to start with the first problem I had to solve which was (what appeared to be) corrupted data in the vBrownBag.com database. I&#8217;m not entirely sure when it happened, but there were a lot of characters that didn&#8217;t belong in the posts, pingbacks, and comments such as Â, €, or <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2122.png" alt="™" class="wp-smiley" style="height: 1em; max-height: 1em;" />. There were even some non-printable characters that wouldn&#8217;t render properly in a decent text editor like <a href="https://notepad-plus-plus.org/">Notepad++</a>. I&#8217;m not sure if it was a bad automated restore, or a past friend of the vBrownBag that accidentally messed it up, but it was a problem that impacted the majority of the site content. I tried a simple find &amp; replace but working with database dump files is a risky business and I could&#8217;ve easily made the problem worse. I did a lot of googling too, like any decent technologist would do. 😉 Finally, I found the name of the problem: <a href="https://en.wikipedia.org/wiki/Mojibake">mojibake</a>. It&#8217;s garbled or gibberish text that is the result of text being decoded using an unintended character encoding. That took me down a wild rabbit hole full of arcane magic and dark spells, but eventually I stumbled onto <a href="https://mysql.rjweb.org/doc.php/charcoll#fixes_for_various_cases">this page</a>. Turns out that a fellow nerd and former MySQL guy for Yahoo decided to write down all sorts of random database things on one very long page. The fix that I was looking for was in the &#8220;fixes for various cases&#8221; section. Specifically, this command:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:0.9em;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25em;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#f2f2f2;color:#2f363c">SQL</span><span role="button" tabindex="0" data-code="UPDATE tbl SET col = CONVERT(BINARY(CONVERT(col USING latin1)) USING utf8mb4);" style="color:#24292eff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki min-light" style="background-color: #ffffff" tabindex="0"><code><span class="line"><span style="color: #D32F2F">UPDATE</span><span style="color: #24292EFF"> tbl </span><span style="color: #D32F2F">SET</span><span style="color: #24292EFF"> col </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">CONVERT</span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">BINARY</span><span style="color: #24292EFF">(</span><span style="color: #6F42C1">CONVERT</span><span style="color: #24292EFF">(col </span><span style="color: #D32F2F">USING</span><span style="color: #24292EFF"> latin1)) </span><span style="color: #D32F2F">USING</span><span style="color: #24292EFF"> utf8mb4);</span></span></code></pre></div>



<p class="wp-block-paragraph">The command converts the <em>col</em> column to latin1 encoding, then converts it to a binary string, then converts the string to utf8mb4 encoding. The fix was as easy as that. I was amazed. Thank you, Mr Rick &#8220;MySQL Superman&#8221; James for helping to fix the website.</p>



<p class="wp-block-paragraph">For my next post in the series, I&#8217;ll break down the meatgrinder steps into smaller pieces and more of the things I&#8217;ve learned along the way.</p>



<p class="wp-block-paragraph">Edit: Read <a href="https://damiankarlson.com/2024/04/21/part-2-of-whats-in-the-bag-behind-the-scenes-at-vbrownbag-com/">part 2</a> in the series. You know you want to.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://damiankarlson.com/2024/04/21/whats-in-the-bag-behind-the-scenes-at-vbrownbag-com/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
		
		<series:name><![CDATA[What's in the bag? Behind the scenes at vBrownBag.com]]></series:name>
<post-id xmlns="com-wordpress:feed-additions:1">1513</post-id>	</item>
		<item>
		<title>Thoughts on VMworld 2018</title>
		<link>https://damiankarlson.com/2018/09/10/thoughts-on-vmworld-2018/</link>
		
		<dc:creator><![CDATA[Damian Karlson]]></dc:creator>
		<pubDate>Mon, 10 Sep 2018 14:09:59 +0000</pubDate>
				<category><![CDATA[Random Stuff]]></category>
		<category><![CDATA[VMworld]]></category>
		<guid isPermaLink="false">https://damiankarlson.com/?p=1390</guid>

					<description><![CDATA[I had the privilege of attending VMworld 2018 in Las Vegas as a blogger (thank you, VMware social team!). It was a unique experience for me, as I&#8217;ve attended the show as a vendor employee for a number of years. Going as a blogger enabled me to catch up with folks I hadn&#8217;t seen since [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>I had the privilege of attending VMworld 2018 in Las Vegas as a blogger (thank you, VMware social team!). It was a unique experience for me, as I&#8217;ve attended the show as a vendor employee for a number of years. Going as a blogger enabled me to catch up with folks I hadn&#8217;t seen since last year, meet new ones, and hang out with old friends.</p>
<p>A recurring theme of many conversations was VMware&#8217;s focus on AWS. In fact, one off-hand comment that resonated with me was &#8220;whose conference is this?&#8221; While that was said in jest, it is interesting to see how the relationship between VMware and AWS has shifted over the years from competing on all fronts to the realization that many customers&#8217; IT strategies feature offerings from both companies and that joint solutions are needed.One of the biggest announcements was <a href="https://aws.amazon.com/rds/vmware/">Amazon RDS on VMware</a>, which should prove to be a rather interesting solution marrying the best of both VMware and AWS.</p>
<p>It will be interesting to see what the future holds for both companies because, while there are great synergies, there is still strong competition.</p>
<p>VMworld has published most (all?) of the content from the conference, and there&#8217;s a tremendous amount of sessions related to AWS in the <a href="https://videos.vmworld.com/searchsite/2018">catalog</a>. One of the highest rated sessions covered migrating to VMware Cloud on AWS (<a href="https://videos.vmworld.com/searchsite/2018/videoplayer/24007">video</a> &amp; <a href="https://cms.vmworldonline.com/event_data/10/session_notes/HYP1187BU.pdf">PDF</a>) and I found it to be quite interesting. There&#8217;s a ton of great sessions that have been published &#8211; too many to list! Be sure to check them out.</p>
<h3>Community</h3>
<p>As always, the VMworld community is one of the best things about the show. Two things that stood out to me &#8211; and I fully admit that I&#8217;m a little bit biased <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> &#8211; were the <a href="https://blog.vmunderground.com/">VMunderground</a> party and the live <a href="https://vbrownbag.com/">#vBrownBag</a> TechTalks. Both have been going on for many years, and while this year was the first that I wasn&#8217;t involved in some way with either (whether it was organizing, presenting, or supporting) it gave me the opportunity to experience them with fresh eyes. The VMunderground party continues to be the best community networking event, and this year&#8217;s was no exception. The #vBrownBag TechTalks have continued to mature and expand, and its success is due to a combination of passionate leadership and an even more passionate technical community. There&#8217;s over 60 recorded sessions on <a href="https://www.youtube.com/watch?v=YFqngD0UDz4&amp;list=PL2rC-8e38bUUV6GHmxM-K4fc750HYWF12">#vBrownBag&#8217;s YouTube playlist</a>, and they&#8217;re full of solid content, with minimal fluff. I highly recommend checking them out.</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1390</post-id>	</item>
		<item>
		<title>Importing vRealize Application Services Blueprints from the Solution Exchange Marketplace</title>
		<link>https://damiankarlson.com/2014/10/23/importing-vrealize-application-services-blueprints-from-the-solution-exchange-marketplace/</link>
					<comments>https://damiankarlson.com/2014/10/23/importing-vrealize-application-services-blueprints-from-the-solution-exchange-marketplace/#comments</comments>
		
		<dc:creator><![CDATA[Damian Karlson]]></dc:creator>
		<pubDate>Thu, 23 Oct 2014 21:31:13 +0000</pubDate>
				<category><![CDATA[vCloud Automation Center Application Services]]></category>
		<category><![CDATA[vRealize Application Services]]></category>
		<category><![CDATA[vCloud Automation Center]]></category>
		<guid isPermaLink="false">https://damiankarlson.com/?p=1360</guid>

					<description><![CDATA[(Note: I think this is the first time I&#8217;ve called it vRealize. /sigh) Earlier today I was trying to add application blueprints from the VMware Solution Exchange Marketplace to my Application Services appliance. The &#8220;Try&#8221; page for the blueprints features an option to login to your Application Services appliance and deploy the blueprint directly. That, [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>(Note: I think this is the first time I&#8217;ve called it vRealize. /sigh)</p>
<p>Earlier today I was trying to add application blueprints from the VMware Solution Exchange Marketplace to my Application Services appliance. The &#8220;Try&#8221; page for the blueprints features an option to login to your Application Services appliance and deploy the blueprint directly. That, however, doesn&#8217;t work. As it turns out, Marketplace access isn&#8217;t available on 6.1 (<a href="https://www.vmware.com/support/vcac/doc/vcloud-automation-center-61-release-notes.html">release notes</a>).</p>
<p>In order to import the blueprints, download each file and upload it to the Application Services appliance. Then SSH into the appliance and start <a href="http://pubs.vmware.com/vCAC-61/topic/com.vmware.vcac.appservices.all.doc/GUID-D39A5F38-7EEF-43A3-8CF2-7ADA4C1E03F2.html#GUID-D39A5F38-7EEF-43A3-8CF2-7ADA4C1E03F2">darwin-cli.jar</a>. Login to the Application Services darwin instance and run the <a href="http://pubs.vmware.com/vCAC-61/topic/com.vmware.vcac.appservices.all.doc/GUID-789A618E-90E6-4E1A-86F9-A365C6E297F8.html">import-package</a> command with the correct flags in order to import the XML. Be sure to SSH into the appliance itself, as import-package looks for <em>importFilePath</em> on the local machine.</p>
<p>Pay special attention to the <em>ConflictResolutionAction</em> flag as overwriting or skipping a piece of the import could have undesired consequences.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://damiankarlson.com/2014/10/23/importing-vrealize-application-services-blueprints-from-the-solution-exchange-marketplace/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1360</post-id>	</item>
	</channel>
</rss>
