<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[cdrani]]></title><description><![CDATA[cdrani]]></description><link>https://cdrani.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 16:21:13 GMT</lastBuildDate><atom:link href="https://cdrani.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Today I Learned: Series Can Be Edited in Post]]></title><description><![CDATA[I learned today that my main blog posts should be called "Today I Learned"s. I'm surprised my last post was over two months ago. I've been working on my new Chrome extension project, Chorus, making features, fixing problems, and getting it ready for ...]]></description><link>https://cdrani.dev/today-i-learned-series-can-be-edited-in-post</link><guid isPermaLink="true">https://cdrani.dev/today-i-learned-series-can-be-edited-in-post</guid><category><![CDATA[TIL]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Fri, 18 Aug 2023 04:51:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/OgvqXGL7XO4/upload/3ceaa6d27bfa702022d205646e996129.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I learned today that my main blog posts should be called "Today I Learned"s. I'm surprised my last post was over two months ago. I've been working on my new Chrome extension project, <a target="_blank" href="https://github.com/cdrani/chorus">Chorus</a>, making features, fixing problems, and getting it ready for a public introduction here, on LinkedIn, and/or Twitter. The project series will demonstrate how to build these features, illustrate how to automate releases, and explain how to upload and publish to the Chrome dashboard. Additionally, I have established guides for open-source assistance and created templates and comprehensive guides for contributors to resolve issues.</p>
<p>While working on the extension, I learned a great deal about GitHub Actions, creating tags and artefacts for releases, and utilizing the Web Audio API to incorporate effects such as reverb and pitch shifts on audio. However, I felt I couldn't write posts about those topics until I had officially released the extension to the public. Moreover, I hadn't begun planning how to incorporate it into the series. This wasn't ideal. I need to transition from series-focused posts to standalone posts, which can include quick thoughts, intriguing features and/or bugs I encountered, life and career updates, and more. Then, perhaps, I can group related topics into a series.</p>
<p>That's it. Simple and short. I hope to keep a steady pace by reducing the pressure of having many planned posts on a subject.</p>
]]></content:encoded></item><item><title><![CDATA[YQM: Release and Open Source]]></title><description><![CDATA[TLDR: Fix a bug, package and upload to Developer Dashboard, fill forms, submit for review, and look at analytics after going live.
In this post, we want to create and submit an initial release for the Chrome Web Store and upload it to Github. Before ...]]></description><link>https://cdrani.dev/yqm-release-and-open-source</link><guid isPermaLink="true">https://cdrani.dev/yqm-release-and-open-source</guid><category><![CDATA[chrome extension]]></category><category><![CDATA[youtube]]></category><category><![CDATA[OSS]]></category><category><![CDATA[yqm]]></category><category><![CDATA[chrome web store]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Mon, 19 Jun 2023 23:07:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/GmvH5v9l3K4/upload/d3578f379633aeec6a71d69818ab31d8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>TLDR: Fix a bug, package and upload to Developer Dashboard, fill forms, submit for review, and look at analytics after going live.</strong></p>
<p>In this post, we want to create and submit an initial release for the Chrome Web Store and upload it to Github. Before that step though, let's fix a bug encountered.</p>
<h2 id="heading-bug-fix">Bug Fix</h2>
<h3 id="heading-issue">Issue</h3>
<p>The issue originates from the removal of the "Add to queue" option from the menu and the thumbnail on disconnectObserver. The remove() method eliminates the node and its space in the DOM, rendering it inaccessible.</p>
<pre><code class="lang-javascript"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ObserverWrapper</span> </span>{
    <span class="hljs-comment">//</span>
    _removeQueueUI(mutationsList) {
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> mutation <span class="hljs-keyword">of</span> mutationsList) {
            <span class="hljs-keyword">const</span> { target, addedNodes } = mutation

            <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>._isPlaylistIconHover(mutation)) {
                <span class="hljs-keyword">const</span> playlistIcon = <span class="hljs-built_in">Array</span>.from(addedNodes)
                  .find(<span class="hljs-function"><span class="hljs-params">a</span> =&gt;</span> a.ariaLabel === <span class="hljs-string">'Add to queue'</span>)
                playlistIcon?.remove()
            }

            <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>._isMenuOption(mutation)) {
                target?.parentElement?.parentElement?.remove()
            }
        }
    }  
}
</code></pre>
<p>So, when we turn off the extension and disconnect the observer, we can't undo the changes we made to the user interface. We'd need to recreate the menu options and playlist icons we removed. This is a tough job because each interface item has many parts, like children, styles, and events, that we need to rebuild and put back into the DOM. Yikes.</p>
<h3 id="heading-solution">Solution</h3>
<p>Instead of removing the DOM elements, we can update their style instead. By setting a style of "display: none," we can hide the UI from view while still keeping it accessible. Therefore, when toggling off, we can reset the display attribute to "block" to bring it back into view. In our <strong>ObserverWrapper</strong> class, we now need to track a new property to either hideUI (set display: none) or showUI (set display: block).</p>
<pre><code class="lang-javascript"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ObserverWrapper</span> </span>{
    <span class="hljs-keyword">constructor</span>() {
        <span class="hljs-comment">// </span>
        <span class="hljs-built_in">this</span>._isHidden = <span class="hljs-literal">true</span>
    }

    <span class="hljs-comment">// . . </span>

    _toggleQueueUI(mutationsList) {
        <span class="hljs-keyword">const</span> display = <span class="hljs-built_in">this</span>._isHidden ? <span class="hljs-string">'none'</span> : <span class="hljs-string">'block'</span>

        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> mutation <span class="hljs-keyword">of</span> mutationsList) {
            <span class="hljs-keyword">const</span> { target, addedNodes } = mutation

            <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>._isPlaylistIconHover(mutation)) {
                <span class="hljs-keyword">const</span> playlistIcon = <span class="hljs-built_in">Array</span>.from(addedNodes)
                  .find(<span class="hljs-function"><span class="hljs-params">a</span> =&gt;</span> a.ariaLabel === <span class="hljs-string">'Add to queue'</span>)
                playlistIcon?.setAttribute(<span class="hljs-string">'style'</span>, <span class="hljs-string">`display: <span class="hljs-subst">${display}</span>`</span>)
            }

            <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>._isMenuOption(mutation)) {
                <span class="hljs-keyword">const</span> element = target.parentElement.parentElement
                element?.setAttribute(<span class="hljs-string">'style'</span>, <span class="hljs-string">`display: <span class="hljs-subst">${display}</span>`</span>)
            }
        }
    }

    hideUI() {
        <span class="hljs-built_in">this</span>._isHidden = <span class="hljs-literal">true</span>
        <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>._observer) { 
            <span class="hljs-built_in">this</span>._composeObserver()
        }
    }

    showUI() {
        <span class="hljs-built_in">this</span>._isHidden = <span class="hljs-literal">false</span> 
    } 
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">init</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> observerWrapper = <span class="hljs-keyword">new</span> ObserverWrapper()

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">messageCallback</span>(<span class="hljs-params">message</span>) </span>{
        message.active
          ? observerWrapper.hideUI() 
          : observerWrapper.showUI()
    }

    listenForMessage(messageCallback)
}

init()
</code></pre>
<h2 id="heading-release">Release</h2>
<p>Releasing an extension to the Chrome Web Store requires a developer account and a one-time payment of $5. Reference the <a target="_blank" href="https://developer.chrome.com/docs/webstore/publish/">steps for publishing extensions</a> to set up an account and access the Developer Dashboard. Whether manual or automated deployments, we still need to zip our extension as a package, upload it to our account, submit it for review, and wait for 1-2 business days for review. If approved then our extension is published. This happens every single time we cut a release.</p>
<ol>
<li>Zip the extension with only the necessary files. The "-x" flag is to not include our readme and license markdown files.</li>
</ol>
<pre><code class="lang-bash">zip -r yqm.zip * -x <span class="hljs-string">'*.md'</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686265933646/f5bb53fe-ddd3-48b7-8a36-91832fcd75b8.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>Either drag or browse for and select the "yqm.zip" folder from the drag zone container in the Dashboard.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686266200603/fec6df68-6474-402a-b9f1-7a595c490349.png" alt /></p>
</li>
</ol>
<ol>
<li><p>Follow along with the listing form and enter input where necessary. Include links for support, which in this case will be the repo.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687197938962/d011d72d-6ce8-407e-9b43-f9b8c15e303a.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Same for Privacy Policy where we need to justify our use of permissions such as activeTab, storage, etc. No need to be overly descriptive here.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687197810904/75b77324-6e0d-4bfe-8b0b-cb6960a73658.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Distribution is the final step before submitting for review. The only concern with making the extension available worldwide is I18n, but we currently only have "on/off" text in our action icon. A quick solution would be either disabling/enabling the extension on action click or using a more universal on/off signifier like a lit/unlit lightbulb icon.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687197725835/689bd04b-08d4-4bde-bcbe-0f7e91edf898.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<h2 id="heading-publish">Publish</h2>
<p>The package uploads and form edits are all in draft until one clicks the "Submit for Review" button. YQM's review took about 2 days, but this could have been due to it being submitted over the weekend, in other words, it was pretty fast.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687195562265/1e46f832-2f55-405d-b478-8906faeee9fd.png" alt class="image--center mx-auto" /></p>
<p>Here it is on the web store: <a target="_blank" href="https://chrome.google.com/webstore/detail/youtube-queue-manager/ffdonhchkjfnjhklpjijaihndnlkdbho">YQM</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687197617914/4eb258d0-5077-438e-ba43-0a5eeb9d9baa.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-analytics">Analytics</h2>
<p>Chrome Extensions feature built-in analytics for tracking actions such as adding or removing, as well as impressions and more. Furthermore, they can be easily integrated with Google Analytics for even more detailed data. There's a wealth of information available on the dashboard, but since this is a personal passion project, not much attention will be given to these dashboards. Instead, they will primarily be used to determine which project to prioritize based on the number of installations and active users.</p>
<h4 id="heading-developer-dashboard-analytics">Developer Dashboard Analytics</h4>
<p>Impressions</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687197129671/0f2f0dfb-8130-41e8-858a-68933f51402f.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687197159391/2d9b4899-1386-4fdd-8aba-e26e29e0961d.png" alt class="image--center mx-auto" /></p>
<p>Installs/Uninstalls</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687197244516/97555a8b-d768-4565-80d3-7521583f93c1.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-google-analytics">Google Analytics</h4>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687196874602/7695d010-db03-4dd1-9974-51b060dd5b1a.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-release-1">Release</h2>
<p>Here is the GitHub link: <a target="_blank" href="https://github.com/cdrani/yqm">YQM</a>. There's a general roadmap for what the finalized extension might have in terms of features, i18n, cross-browser support, etc. Contributors are welcome in any form - filing issues, updating docs/readme, updating icons, feature implementation, automating releases, etc.</p>
<h3 id="heading-wrap-up">Wrap Up</h3>
<p>Okay. This was a good introduction to setting up an extension and developing it from ideation to a v1 release. Additionally, we discovered the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver">MutationObserver</a> web API to observe and manipulate the DOM based on changes in specific nodes. We started working on the next version's features, but right now, we're focusing more on a new project about controlling the web Spotify player, changing the playback speed, and playing only a specific part of a song.</p>
]]></content:encoded></item><item><title><![CDATA[YQM: Setup & Disable Queues]]></title><description><![CDATA[Here's a recap of what we want to accomplish for the first feature:

Either disable or remove all the Add to queue UI (on hover and via the dropdown:
 

Enable or disable the first feature by clicking on the extension in the toolbar, which controls t...]]></description><link>https://cdrani.dev/yqm-setup-disable-queues</link><guid isPermaLink="true">https://cdrani.dev/yqm-setup-disable-queues</guid><category><![CDATA[chrome extension]]></category><category><![CDATA[youtube]]></category><category><![CDATA[Queues]]></category><category><![CDATA[queue management]]></category><category><![CDATA[chrome apis]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Wed, 07 Jun 2023 02:37:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/J75utbpxgTA/upload/d4392cc7dfef88752203404e176b255c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Here's a recap of what we want to accomplish for the first feature:</p>
<ol>
<li><p>Either disable or remove all the <code>Add to queue</code> UI (on hover and via the dropdown:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684342075626/41b55093-bd9e-4bfb-8d87-093495d1ace3.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Enable or disable the first feature by clicking on the extension in the toolbar, which controls the availability of the "Add to queue" buttons.</p>
</li>
</ol>
<h2 id="heading-setup">Setup</h2>
<p>All Chrome extensions require a manifest.json file, which is similar to a package.json file. In the manifest.json file, define the essential fields, focusing on the bare minimum needed for the setup. Fields marked with an asterisk are mandatory in every file.</p>
<p>The final <code>manifest.json</code> file at the end of the project will likely include additional fields not mentioned below, but these can be found on the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json">MDN manifest.json documentation</a>.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Field</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td>name *</td><td>Name of the extension</td></tr>
<tr>
<td>manifest_version *</td><td>The version of the manifest to use. Strongly encouraged to use v3.</td></tr>
<tr>
<td>author</td><td>Author of the extension</td></tr>
<tr>
<td>description</td><td>Description of the extension</td></tr>
<tr>
<td>version *</td><td>The version of the extension. Update version for new releases</td></tr>
<tr>
<td>icons</td><td>Icons used for the extension in the toolbar, chrome web store, etc</td></tr>
</tbody>
</table>
</div><pre><code class="lang-json">{
      <span class="hljs-attr">"manifest_version"</span>: <span class="hljs-number">3</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"YouTube Queue Manager"</span>,
      <span class="hljs-attr">"short_name"</span>: <span class="hljs-string">"YQM"</span>,
      <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0"</span>,
      <span class="hljs-attr">"author"</span>: <span class="hljs-string">"cdrani"</span>,
      <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Manage Youtube Queue Playlist"</span>,
      <span class="hljs-attr">"icons"</span>: {
            <span class="hljs-attr">"16"</span>: <span class="hljs-string">"icons/icon16.png"</span>,
            <span class="hljs-attr">"48"</span>: <span class="hljs-string">"icons/icon48.png"</span>,
            <span class="hljs-attr">"128"</span>: <span class="hljs-string">"icons/icon128.png"</span>
      }
}
</code></pre>
<blockquote>
<p>You should always provide a 128x128 icon; it's used during installation and by the Chrome Web Store. Extensions should also provide a 48x48 icon, which is used on the extensions management page (chrome://extensions). You can also specify a 16x16 icon to be used as the favicon for an extension's pages.</p>
</blockquote>
<p>From the information provided, we have set up our manifest.json file and created an icons folder containing 16x16, 48x48, and 128x128 pixel PNG files. These images were derived from a queue-list SVG icon from <a target="_blank" href="https://heroicons.com/">Heroicons</a>, which was subsequently resized to the necessary dimensions and converted into PNG files.</p>
<h2 id="heading-load-extension">Load Extension</h2>
<p>For testing and development purposes, we want to load the current state of our extension in our browser. To achieve this, simply enter chrome://extensions in the search bar, enable Developer mode, click on "Load unpacked," and select the extension folder. Counter 4 showcases the information from our manifest file. We will frequently use the reload button (Counter 5) to keep changes in our project synchronized with the browser.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684860934746/33304580-10b3-49cc-b14e-ef96020c0b03.png" alt class="image--center mx-auto" /></p>
<p>For now, we want our extension to be activated manually by clicking on it in our toolbar. Therefore, we need to pin the extension to our toolbar, ensuring it is always present for easy access.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684861619527/f1c3b4b4-2ff2-4b7f-8a59-3bda8b816186.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-feature-1-remove-the-add-to-queue-ui">Feature 1: Remove the 'Add to queue' UI</h2>
<p>It took two different approaches to implement this feature. For educational purposes, both approaches will be highlighted to demonstrate the thought processes and issues which led to a pivot.</p>
<h3 id="heading-approach-1-utilize-event-handlers">Approach 1: Utilize Event Handlers</h3>
<h4 id="heading-thought-process">Thought Process</h4>
<p>Currently, the "Add to Queue" user interface (UI) has two variations: the first features a playlist icon with a tooltip, and the second is an option within a menu item on every video. Both of them appear when hovering over a video, but the menu options are only displayed when the menu dropdown is clicked.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684887532265/0e130d0d-5c86-4b4e-811f-a0697c8705a2.png" alt class="image--center mx-auto" /></p>
<p>To remove the option from the menu, simply locate the dropdown menu component in the DOM, identify the event that triggers the display of menu options, and update the event listener to eliminate the 'Add to Queue' option.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686104028871/bcc0833f-9e44-4418-b34e-46d331d38528.png" alt class="image--center mx-auto" /></p>
<p>From above we see that's it the <code>yt-icon-button</code> element and the <code>click</code> event on it that reveals the menu. The menu option can be found the same way:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686103898527/edcb729e-80ca-4e3e-9ba6-efe5531add0c.png" alt class="image--center mx-auto" /></p>
<p>We have all the necessary information, so we can now create a search-and-remove script to eliminate the "Add to Queue" option based on the <code>yt-formatted-string</code> component. <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate">https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate</a></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> menuButtons = <span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'yt-icon-button#button.dropdown-trigger'</span>)

menuButtons.forEach(<span class="hljs-function"><span class="hljs-params">mb</span> =&gt;</span> {
    mb.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-comment">// find the specific `yt-formatted-string` element in the menu </span>
        <span class="hljs-keyword">const</span> menuOptionString = <span class="hljs-built_in">document</span>.evaluate(<span class="hljs-string">"//yt-formatted-string[contains(., 'Add to queue')]"</span>)
        <span class="hljs-keyword">const</span> addToQueueOption = menuOptionString.iterateNext()
        addToQueueOption?.parentElement?.parentElement?.remove()     
    }
})
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684903135818/111ee523-61ac-4f13-88cd-bb4bc3a0c5b1.png" alt class="image--center mx-auto" /></p>
<p>Repeat this process for the 'Add to queue' icon on the thumbnail. It's a bit harder in Chrome, but Firefox Devtools lets us see and change toggle events on elements. We want to turn off the "mouseleave" event to keep the playlist icon visible for inspection.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684944037945/821ac364-7ce9-4b86-ade3-30863f316ab3.png" alt class="image--center mx-auto" /></p>
<p>We now have the component for the playlist icon.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684944271024/c21408ec-f7d3-4ce8-9e59-168eff8b4d8c.png" alt class="image--center mx-auto" /></p>
<p>Here's the script to search and remove it.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> sectionItems = <span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'ytd-video-renderer'</span>)
sectionItems.forEach(<span class="hljs-function"><span class="hljs-params">si</span> =&gt;</span> {
    si.addEventListener(<span class="hljs-string">'mouseenter'</span>, <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> playlistIcon = <span class="hljs-built_in">document</span>.evaluate(<span class="hljs-string">"//ytd-thumbnail-overlay-toggle-button-renderer[contains(@aria-label, 'Add to queue')]"</span>)
        <span class="hljs-keyword">const</span> playlistIconRenderer = playlistIcon?.iterateNext()
        playlistIconRenderer?.remove()
    })
})
</code></pre>
<p>And it's gone.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684945584567/527a5f59-76fb-4164-8732-7125330d257a.png" alt class="image--center mx-auto" /></p>
<p>We have a small issue in that YouTube has different components for the video section item depending on if one's on the search, home, subscription, or playlist page. The variants are <code>ytd-grid-video-renderer</code>, <code>ytd-rich-grid-media</code> ,and <code>ytd-video-renderer</code> . They don't all appear on the same page, so we run our script based on the youtube tab path names.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> SELECTORS = {
    <span class="hljs-string">'/'</span>: [<span class="hljs-string">'ytd-rich-grid-media'</span>],
    <span class="hljs-string">'/feed/subscriptions'</span>: [<span class="hljs-string">'ytd-grid-video-renderer'</span>],
    <span class="hljs-string">'/playlist'</span>: [<span class="hljs-string">'ytd-video-renderer'</span>],
    <span class="hljs-string">'/@'</span>: [<span class="hljs-string">'ytd-grid-video-renderer'</span>, <span class="hljs-string">'ytd-video-renderer'</span>]
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTabPathName</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> selectorKeys = <span class="hljs-built_in">Object</span>.keys(SELECTORS)
    <span class="hljs-keyword">const</span> pathName = <span class="hljs-built_in">window</span>.location.pathname

    <span class="hljs-keyword">const</span> currentSelectorKey = <span class="hljs-built_in">Object</span>.keys(SELECTORS)
        .find(<span class="hljs-function">(<span class="hljs-params">key</span>) =&gt;</span> {
            <span class="hljs-keyword">return</span> (key === <span class="hljs-string">'/'</span> &amp;&amp; pathName === <span class="hljs-string">'/'</span>) || 
                (key !== <span class="hljs-string">'/'</span> &amp;&amp; pathName.includes(key))
        });

    <span class="hljs-keyword">return</span> currentSelectorKey
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getQuerySelector</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> tabPathName = getTabPathName()
    <span class="hljs-keyword">return</span> SELECTORS[tabPathName]
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">searchAndRemoveMenuOption</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> menuButtons = <span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'yt-icon-button#button.dropdown-trigger.ytd-menu-renderer'</span>);

    menuButtons.forEach(<span class="hljs-function"><span class="hljs-params">mb</span> =&gt;</span> {
        mb.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function">() =&gt;</span> {
            <span class="hljs-comment">// find `yt-formatted-string` component in menu options</span>
            <span class="hljs-keyword">const</span> menuOptionString = <span class="hljs-built_in">document</span>.evaluate(
                <span class="hljs-string">"//yt-formatted-string[contains(., 'Add to queue')]"</span>,         <span class="hljs-built_in">document</span>)
            <span class="hljs-keyword">const</span> addToQueueOption = menuOptionString?.iterateNext()
            addToQueueOption?.parentElement?.parentElement?.remove()      
        }
    })
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">searchAndRemoveQueueIcon</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> querySelector =  getQuerySelector()
    <span class="hljs-keyword">const</span> sectionItems = <span class="hljs-built_in">document</span>.querySelectorAll(querySelector)

    sectionItems.forEach(<span class="hljs-function"><span class="hljs-params">si</span> =&gt;</span> {
        si.addEventListener(<span class="hljs-string">'mouseenter'</span>, <span class="hljs-function">() =&gt;</span> {
            <span class="hljs-keyword">const</span> playlistIconElement = <span class="hljs-built_in">document</span>.evaluate(<span class="hljs-string">"//ytd-thumbnail-overlay-toggle-button-renderer[contains(@aria-label, 'Add to queue')]"</span>, <span class="hljs-built_in">document</span>)?.iterateNext()
            playlistIcon?.remove()
        })
    })
}
</code></pre>
<h4 id="heading-cons">Cons</h4>
<p>The code works, so what's wrong? Why not use it, add Chrome extension features, put everything together, and start version 1? Let's talk about the current code using a QnA format:</p>
<p>Q: We have added "click" and "mouseenter" events to some UI elements. This is fine initially, as there are only a few elements, but more appear as users scroll down. Should we run our script again for the entire page? How can we monitor page changes?</p>
<p>A: No, that would cause performance issues. We could use a <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver">MutationObserver</a> to watch for only <strong><em>new</em></strong> changes in the DOM and re-run the script for the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord/addedNodes">addedNodes</a>.</p>
<p>Q: Okay, the main worry is adding new events to each item. Users might not use every updated part and can switch the extension on or off. If they turn it off, should we refresh the page to start over with the events? That could annoy users. Or, should we take away the events? It's possible, but it might cause problems since every video card item would have an event connected to it.</p>
<p>A: We need to consider our options. We can use the MutationObserver to check for new UI when a user hovers over a video card, locate the "Add to Queue" button, and remove it. This is faster because we only deal with one element that the user interacts with. When a user turns off the extension, we can <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/disconnect">disconnect</a> from our MutationObserver to reset everything without harsh reloading or numerous event listeners. We can do the same for the dropdown menu options.</p>
<p>Q: Yes, yes, and yes! This appears to be a better approach to pursue.</p>
<h3 id="heading-approach-2-mutationobservers">Approach 2: MutationObservers</h3>
<p>Settling on MutationObservers, we have to narrow down what DOM changes to watch for and the updated UI to take action upon.</p>
<p>For the dropdown menu, in the Firefox Inspector tab within the developer tools, we identify that the <strong><em>ytd-popup-container</em></strong> is the component to observe, as it renders the menu options when the dropdown is clicked.</p>
<p>/insert media of finding <code>ytd-pop-container</code></p>
<p>Let's see the changes by testing our observer in the console:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> mutationObserver = <span class="hljs-keyword">new</span> MutationObserver(<span class="hljs-function"><span class="hljs-params">mutations</span> =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(mutations)
})
<span class="hljs-keyword">const</span> target = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'ytd-popup-container'</span>)
<span class="hljs-keyword">const</span> config = { <span class="hljs-attr">childList</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">subtree</span>: <span class="hljs-literal">true</span> }
mutationObserver.observe(target, config)
</code></pre>
<p>Here are the changes when you click the dropdown. The important part is when the "Add to queue" text appears in the <strong><em>ytd-formatted-string</em></strong> component.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1685555505049/b58ad0ab-982a-499b-8d01-d3e7e7c7e7a1.png" alt class="image--center mx-auto" /></p>
<p>Upon identifying the precise target, we discover that the actual parent component of the <strong><em>yt-formatted-string</em></strong> is <strong><em>ytd-menu-service-item-renderer</em></strong>, which serves as the grandparent of our current element.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1685556821762/35febb1e-8e3f-4ae2-afe5-b29e436a60cd.png" alt class="image--center mx-auto" /></p>
<p>Removing it is quite simple.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isMenuOption</span>(<span class="hljs-params">mutation</span>) </span>{
    <span class="hljs-keyword">const</span> { localName, innerText } = mutation.target
    <span class="hljs-keyword">return</span> localName == <span class="hljs-string">'yt-formatted-string'</span> 
        &amp;&amp; innerText === <span class="hljs-string">'Add to queue'</span>
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">clearQueueUI</span>(<span class="hljs-params">mutationsList</span>) </span>{
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> mutation <span class="hljs-keyword">of</span> mutationsList) {
        <span class="hljs-keyword">const</span> { target, addedNodes } = mutation

        <span class="hljs-keyword">if</span> (isMenuOption(mutation)) {
            target?.parentElement?.parentElement?.remove()
        }
    }
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setUpObservers</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">const</span> mo = <span class="hljs-keyword">new</span> MutationObserver(clearQueueUI)
    <span class="hljs-keyword">const</span> target = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'ytd-popup-container'</span>)
    <span class="hljs-keyword">const</span> config = { <span class="hljs-attr">childList</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">subtree</span>: <span class="hljs-literal">true</span> }
    mo.observe(target, config)
}

setUpObservers()
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1685558355403/1ad488ad-dc80-4a77-b12f-6b1787ebc907.png" alt class="image--center mx-auto" /></p>
<p>Video card components on YouTube pages have varying names, such as <strong><em>ytd-grid-media</em></strong>, <strong><em>ytd-rich-grid-media</em></strong>, and <strong><em>ytd-grid-video-renderer</em></strong>. While they appear similar, they have minor differences based on their containers. Rather than using multiple observers for each container, let's utilize the primary app container, <strong><em>ytd-app</em></strong>, which also houses the <strong><em>ytd-popup-container</em></strong>. This approach is more straightforward and doesn't cause slowdowns since we only handle a few events. Consequently, we'll modify our script to manage the menu option and playlist hover features.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isChildList</span>(<span class="hljs-params">mutation</span>) </span>{
    <span class="hljs-keyword">return</span> mutation.type === <span class="hljs-string">'childList'</span>
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isPlaylistIconHover</span>(<span class="hljs-params">mutation</span>) </span>{
    <span class="hljs-keyword">const</span> { target, addedNodes } = mutation
    <span class="hljs-keyword">return</span> !!addedNodes?.length &amp;&amp; target.id === <span class="hljs-string">'hover-overlays'</span> 
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isMenuOption</span>(<span class="hljs-params">mutation</span>) </span>{
    <span class="hljs-keyword">const</span> { localName, innerText } = mutation.target
    <span class="hljs-keyword">return</span> localName == <span class="hljs-string">'yt-formatted-string'</span> 
        &amp;&amp; innerText === <span class="hljs-string">'Add to queue'</span>
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">clearQueueUI</span>(<span class="hljs-params">mutationsList</span>) </span>{
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> mutation <span class="hljs-keyword">of</span> mutationsList) {
        <span class="hljs-keyword">const</span> { target, addedNodes } = mutation

        <span class="hljs-keyword">if</span> (isPlaylistIconHover(mutation)) {
            <span class="hljs-keyword">const</span> playlistIcon = <span class="hljs-built_in">Array</span>.from(addedNodes)
                  .find(<span class="hljs-function"><span class="hljs-params">a</span> =&gt;</span> a.ariaLabel === <span class="hljs-string">'Add to queue'</span>)
                playlistIcon?.remove()
        }

        <span class="hljs-keyword">if</span> (isMenuOption(mutation)) {
            target?.parentElement?.parentElement?.remove()
        }
    }
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setUpObservers</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">const</span> mutationObserver = <span class="hljs-keyword">new</span> MutationObserver(clearQueueUI)
    <span class="hljs-keyword">const</span> target = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'ytd-app'</span>)
    <span class="hljs-keyword">const</span> config = { <span class="hljs-attr">childList</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">subtree</span>: <span class="hljs-literal">true</span> }
    mutationObserver.observe(target, config)
}

setUpObservers()
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1685640562429/8d7a4cb8-8437-4255-917d-7a5370edcbb1.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-implementation">Implementation</h3>
<p>We have tested the above script in our console and it works across all youtube pages. Let's move it into our extension. Since the script is just a group of related functions, why not restructure it into an ObserverWrapper class? Additionally, let's add methods to disconnect our observer.</p>
<pre><code class="lang-javascript"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ObserverWrapper</span> </span>{
    <span class="hljs-keyword">constructor</span>() {
        <span class="hljs-built_in">this</span>._observer = <span class="hljs-literal">undefined</span> 
        <span class="hljs-built_in">this</span>._selector = <span class="hljs-string">'ytd-app'</span>
        <span class="hljs-built_in">this</span>._config = { <span class="hljs-attr">childList</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">subtree</span>: <span class="hljs-literal">true</span> }
    }

    _composeObserver() {
        <span class="hljs-keyword">const</span> composeBox = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-built_in">this</span>._selector)

        <span class="hljs-keyword">const</span> mutationObserver = <span class="hljs-keyword">new</span> MutationObserver(<span class="hljs-built_in">this</span>._handleMutations)
        mutationObserver.observe(composeBox, <span class="hljs-built_in">this</span>._config)

        <span class="hljs-built_in">this</span>._observer = mutationObserver
    }

    connectObserver() {
        <span class="hljs-built_in">this</span>._composeObserver()
    }

    _isMenuOption(mutation) {
        <span class="hljs-keyword">const</span> { localName, innerText } = mutation.target
        <span class="hljs-keyword">return</span> localName === <span class="hljs-string">'yt-formatted-string'</span> &amp;&amp; innerText === <span class="hljs-string">'Add to queue'</span>
    }

    _isPlaylistIconHover(mutation) {
        <span class="hljs-keyword">const</span> { target, addedNodes } = mutation
        <span class="hljs-keyword">return</span> !!addedNodes?.length &amp;&amp; target.id === <span class="hljs-string">'hover-overlays'</span> 
    }

    _isChildList(mutation) {
        <span class="hljs-keyword">return</span> mutation.type === <span class="hljs-string">'childList'</span>
    }

    _filterMutations = <span class="hljs-function">(<span class="hljs-params">mutation</span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>._isChildList(mutation) &amp;&amp; 
                (<span class="hljs-built_in">this</span>._isPlaylistIconHover(mutation) ||     <span class="hljs-built_in">this</span>._isMenuOption(mutation))
    }

    _handleMutations = <span class="hljs-function">(<span class="hljs-params">mutationsList</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> filteredList = mutationsList.filter(<span class="hljs-built_in">this</span>._filterMutations)

        <span class="hljs-keyword">if</span> (!filteredList?.length) <span class="hljs-keyword">return</span>
        <span class="hljs-built_in">this</span>._removeQueueUI(filteredList)
    }

    _removeQueueUI(mutationsList) {
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> mutation <span class="hljs-keyword">of</span> mutationsList) {
            <span class="hljs-keyword">const</span> { target, addedNodes } = mutation

            <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>._isPlaylistIconHover(mutation)) {
                <span class="hljs-keyword">const</span> playlistIcon = <span class="hljs-built_in">Array</span>.from(addedNodes)
                  .find(<span class="hljs-function"><span class="hljs-params">a</span> =&gt;</span> a.ariaLabel === <span class="hljs-string">'Add to queue'</span>)
                playlistIcon?.remove()
            }

            <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>._isMenuOption(mutation)) {
                target?.parentElement?.parentElement?.remove()
            }
        }
    }
}
</code></pre>
<h3 id="heading-manifest-scripts-andamp-permissions">Manifest Scripts &amp; Permissions</h3>
<p>Chrome Extensions are by default very restrictive and we have to be explicit about our intentions. We have access to a ton of <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/">Chrome APIs</a>, but we need to declare them in the <code>manifest.json</code> file. In our case, here's the list of our intents and permissions:</p>
<pre><code class="lang-javascript">{
    <span class="hljs-string">"manifest_version"</span>: <span class="hljs-number">3</span>,
    <span class="hljs-string">"name"</span>: <span class="hljs-string">"YouTube Queue Manager"</span>,
    <span class="hljs-string">"version"</span>: <span class="hljs-string">"1.0"</span>,
    <span class="hljs-string">"description"</span>: <span class="hljs-string">"Be in control of your YouTube queues."</span>,
    <span class="hljs-string">"icons"</span>: {
        <span class="hljs-string">"16"</span>: <span class="hljs-string">"icons/icon16.png"</span>,
        <span class="hljs-string">"48"</span>: <span class="hljs-string">"icons/icon48.png"</span>,
        <span class="hljs-string">"128"</span>: <span class="hljs-string">"icons/icon128.png"</span>
    },
    <span class="hljs-string">"actions"</span>: {},
    <span class="hljs-string">"permissions"</span>: [
        <span class="hljs-string">"tabs"</span>,
        <span class="hljs-string">"activeTab"</span>,
        <span class="hljs-string">"scripting"</span>,
        <span class="hljs-string">"storage"</span>
    ],
    <span class="hljs-string">"host_permissions"</span>: [<span class="hljs-string">"*://*.youtube.com/*"</span>],
    <span class="hljs-string">"background"</span>: {
        <span class="hljs-string">"service_worker"</span>: <span class="hljs-string">"background.js"</span>
    }
}
</code></pre>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Intents</td><td>Purpose</td></tr>
</thead>
<tbody>
<tr>
<td><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Browser_actions">actions</a></td><td>The button that is shown on the toolbar to interact with the extension. We will utilize this to toggle on/off our extension.</td></tr>
<tr>
<td><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions">permissions</a></td><td>Request users for certain privileges, such as running a script, sending a message to tabs, accessing tab(s) info, storage, etc.</td></tr>
<tr>
<td><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/host_permissions">host_permissions</a></td><td>Request access to youtube domain pages</td></tr>
<tr>
<td><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background">background</a></td><td>background scripts will allow us to react to browser events using Chrome APIs, such as clicking on our extension, tab updates, etc (ref: <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Background_scripts">Background_scripts</a>)</td></tr>
</tbody>
</table>
</div><p>Let's start with creating a <code>background.js</code> file. In it, we want to register a script that contains the logic to clear the queue UI when the extension is loaded. We will make use of the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/registerContentScripts">registerContentScripts</a> API.</p>
<pre><code class="lang-javascript">chrome.scripting.registerContentScripts([{
    <span class="hljs-attr">id</span>: <span class="hljs-string">'script-content'</span>,
    <span class="hljs-attr">js</span>: [<span class="hljs-string">'toggleQueue.js'</span>],
    <span class="hljs-attr">runAt</span>: <span class="hljs-string">'document_idle'</span>,
    <span class="hljs-attr">matches</span>: [<span class="hljs-string">'*://*.youtube.com/*'</span>]
}])
</code></pre>
<p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts">Content Scripts</a> run in the context of a particular webpage, in our case any page that matches a youtube URL. Our <code>toggleQueue.js</code> file is our ObserverWrapper class with an initializer:</p>
<pre><code class="lang-javascript"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ObserverWrapper</span> </span>{
    <span class="hljs-comment">//</span>
} 

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">init</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> observerWrapper = <span class="hljs-keyword">new</span> ObserverWrapper()
    observerWrapper.connectObserver()
}

init()
</code></pre>
<p>Reloading the extension from the chrome://extensions page and a youtube tab will now show an updated UI with any form of queue UI removed.</p>
<h2 id="heading-feature-2-toggle-extension-onoff">Feature 2: Toggle Extension On/Off</h2>
<p>We need to be able to toggle our extension on/off when a user clicks on the extension icon in the toolbar. Therefore, we will need to take the following steps:</p>
<ol>
<li><p>Upon installing the extension, we must create a state object that stores the extension's current status (active/inactive). The initial state will be set to active.</p>
</li>
<li><p>Display a badge on the extension to consistently indicate its current state. For readability, it will simply show "on" or "off" with a green or red background, respectively.</p>
</li>
<li><p>Establish a messaging system between our service worker (background.js) and content script (toggleQueue.js). The service worker will transmit the current state of the extension, while the content script will listen for these messages. If the state is active, we will connect our observers to monitor changes in the DOM and update our UI accordingly (as currently implemented); otherwise, we will disconnect our observers.</p>
</li>
<li><p>Add an onClicked event listener in our service worker that listens for clicks on the extension icon. These clicks will toggle the active state and send a message containing the updated state. The incoming message will initiate the same actions described in the second part of point 3.</p>
</li>
<li><p>Additional event listeners for active tab updates such as reload, path changes such as from "/" to "/feed/subscriptions".</p>
</li>
</ol>
<h4 id="heading-defining-state-object">Defining State Object</h4>
<p>Our state object will be simple, with just an <code>active</code> key storing a boolean value, and an <code>event</code> key (optional for debugging) with a string value of the event listener in which the state is sent to the content script. There are multiple storage options provided by the API, but we will utilize the <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/storage/#usage">local</a> option. Additionally, with the active state set, we can display a <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/setBadgeText">badge</a> based on the state.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setBadge</span>(<span class="hljs-params">active</span>) </span>{
    chrome.action.setBadgeText({ <span class="hljs-attr">text</span>: active ? <span class="hljs-string">'on'</span> : <span class="hljs-string">'off'</span> });
    chrome.action.setBadgeBackgroundColor({ <span class="hljs-attr">color</span>: active ? <span class="hljs-string">'green'</span>: <span class="hljs-string">'gray'</span> }); 
}

<span class="hljs-comment">// Currently no event will be used to set this state</span>
chrome.storage.sync.set({ <span class="hljs-attr">state</span>: { <span class="hljs-attr">active</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">event</span>: <span class="hljs-string">''</span> } }, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (chrome.runtime.lastError) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error while saving the updated value:'</span>, chrome.runtime.lastError)
        <span class="hljs-keyword">return</span>;
    } 
    setBadge(<span class="hljs-literal">true</span>);
})
</code></pre>
<p>Our state object will only be initialized or updated through an event listener. The first event listener will be triggered by the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onInstalled">onInstalled</a> event when a user installs the extension. This would be an ideal location to register content scripts, initialize our state, and send the first message.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setBadgeInfo</span>(<span class="hljs-params">active</span>) </span>{
    chrome.action.setBadgeText({ <span class="hljs-attr">text</span>: active ? <span class="hljs-string">'on'</span> : <span class="hljs-string">'off'</span> });
    chrome.action.setBadgeBackgroundColor({ <span class="hljs-attr">color</span>: active ? <span class="hljs-string">'green'</span>: <span class="hljs-string">'gray'</span> });
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">stateResolver</span>(<span class="hljs-params">{ resolve, reject, result, key }</span>) </span>{
    <span class="hljs-keyword">if</span> (chrome.runtime.lastError) {
        <span class="hljs-keyword">return</span> reject({ <span class="hljs-attr">error</span>: chrome.runtime.lastError })
    } 
    <span class="hljs-keyword">return</span> key ? resolve(result[key]) : resolve()
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getState</span>(<span class="hljs-params">{ key = <span class="hljs-string">'state'</span> }</span>) </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
        chrome.storage.local.get(key, <span class="hljs-function">(<span class="hljs-params">result</span>) =&gt;</span> {
            <span class="hljs-keyword">return</span> stateResolver({ key, resolve, reject, result })
        })
    })    
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setState</span>(<span class="hljs-params">{ key = <span class="hljs-string">'state'</span>, value = {} }</span>) </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
        chrome.storage.local.set({ [key]: value }, <span class="hljs-function">(<span class="hljs-params">result</span>) =&gt;</span> {
            <span class="hljs-keyword">return</span> stateResolver({ resolve, reject, result })
        })
    })
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">registerScripts</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> chrome.scripting.registerContentScripts([{
        <span class="hljs-attr">id</span>: <span class="hljs-string">'script-content'</span>,
        <span class="hljs-attr">js</span>: [<span class="hljs-string">'toggleQueue.js'</span>],
        <span class="hljs-attr">runAt</span>: <span class="hljs-string">'document_idle'</span>,
        <span class="hljs-attr">matches</span>: [<span class="hljs-string">'*://*.youtube.com/*'</span>]
    }])
}

chrome.runtime.onInstalled.addListener(<span class="hljs-keyword">async</span> () =&gt; {   
    <span class="hljs-keyword">await</span> registerScripts()
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> setState({ 
        <span class="hljs-attr">key</span>: <span class="hljs-string">'state'</span>,
        <span class="hljs-attr">value</span> : { <span class="hljs-attr">active</span>: <span class="hljs-literal">true</span>,  <span class="hljs-attr">event</span>: <span class="hljs-string">'onInstalled'</span> }
    })

    <span class="hljs-keyword">if</span> (!result?.error) setBadgeInfo(<span class="hljs-literal">true</span>)
}
</code></pre>
<p>We set the badge info on the onInstalled event listener, but on subsequent state changes, we have access to an <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/onChanged">onChanged</a> event listener for storage. In this listener, is where it would be ideal to update our badge info and send messages to our content script about these changes.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getActiveTab</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> chrome.tabs.query({ 
        <span class="hljs-attr">active</span>: <span class="hljs-literal">true</span>, 
        <span class="hljs-attr">currentWindow</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">url</span>: [<span class="hljs-string">'*://*.youtube.com/*'</span>],
    })

    <span class="hljs-keyword">return</span> result?.at(<span class="hljs-number">0</span>)
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">messenger</span>(<span class="hljs-params">{ tabId, message }</span>) </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">reject, resolve</span>) =&gt;</span> {
        chrome.tabs.sendMessage(tabId, message, <span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> {
            <span class="hljs-keyword">if</span> (chrome.runtime.lastError) {
                <span class="hljs-keyword">return</span> reject({ <span class="hljs-attr">error</span>: chrome.runtime.lastError })
            } 
            <span class="hljs-keyword">return</span> resolve(response)
        })
    })
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendMessage</span>(<span class="hljs-params">{ message }</span>) </span>{
    <span class="hljs-keyword">const</span> activeTab = <span class="hljs-keyword">await</span> getActiveTab()
    <span class="hljs-keyword">if</span> (!activeTab) <span class="hljs-keyword">return</span>

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> messenger({ <span class="hljs-attr">tabId</span>: activeTab.id, message })
}

chrome.storage.onChanged.addListener(<span class="hljs-keyword">async</span> changes =&gt; {
    <span class="hljs-keyword">const</span> { newValue } = changes.state
    setBadgeInfo(newValue.active)
    <span class="hljs-keyword">await</span> sendMessage({ <span class="hljs-attr">message</span>: newValue })
})
</code></pre>
<p>The second to last step is accounting for toggling the extension on/off on an <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/onClicked">onClicked</a> event upon clicking the extension, an <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/onActivated">onActivated</a> event for when a tab is switched to, i.e clicked on and made active, and an <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/onUpdated">onUpdated</a> if the tab has some updates, such as a path change from "/" to "/feed/subscriptions" or page reload.</p>
<pre><code class="lang-javascript">chrome.tabs.onActivated.addListener(<span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> state = <span class="hljs-keyword">await</span> getState({ <span class="hljs-attr">key</span>: <span class="hljs-string">'state'</span> })
    <span class="hljs-keyword">await</span> sendMessage({ <span class="hljs-attr">message</span>: { <span class="hljs-attr">active</span>: state.active, <span class="hljs-attr">event</span>: <span class="hljs-string">'onActivated'</span> } })
})

chrome.action.onClicked.addListener(<span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> state = <span class="hljs-keyword">await</span> getState(<span class="hljs-string">'state'</span>)
    <span class="hljs-keyword">await</span> setState({ 
        <span class="hljs-attr">key</span>: <span class="hljs-string">'state'</span>, 
        <span class="hljs-attr">value</span>: { <span class="hljs-attr">active</span>: !state.active, <span class="hljs-attr">event</span>: <span class="hljs-string">'onClicked'</span> } 
    })
})

chrome.tabs.onUpdated.addListener(<span class="hljs-keyword">async</span> (_, changeInfo, tab) =&gt; {
    <span class="hljs-keyword">if</span> (tab.active &amp;&amp; changeInfo?.status === <span class="hljs-string">'complete'</span> &amp;&amp; tab.url.includes(<span class="hljs-string">'youtube.com'</span>)) {
        <span class="hljs-keyword">const</span> state = <span class="hljs-keyword">await</span> getState({ <span class="hljs-attr">key</span>: <span class="hljs-string">'state'</span> })
        sendMessage({ <span class="hljs-attr">message</span>: { <span class="hljs-attr">active</span>: state.active, <span class="hljs-attr">event</span>: <span class="hljs-string">'onUpdated'</span> } })
    }
})
</code></pre>
<p>The final step involves listening for any messages sent from our background script and taking appropriate action. If the message object's 'active' value is true, connect the observer; otherwise, disconnect the observer. Let's implement the message listener and disconnection functionality.</p>
<pre><code class="lang-javascript">Class ObserverWrapper {
    <span class="hljs-comment">// ...</span>

    <span class="hljs-comment">// disconnect observer logic</span>
    _reset() {
        <span class="hljs-built_in">this</span>._observer?.disconnect()
        <span class="hljs-built_in">this</span>._observer = <span class="hljs-literal">undefined</span>
    }

    disconnectObserver() {
        <span class="hljs-built_in">this</span>._reset()
    }
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">listenForMessage</span>(<span class="hljs-params">callback</span>) </span>{
    chrome.runtime.onMessage.addListener(<span class="hljs-function"><span class="hljs-params">message</span> =&gt;</span> {
        callback(message)
    })
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">init</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> observerWrapper = <span class="hljs-keyword">new</span> ObserverWrapper()

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">messageCallback</span>(<span class="hljs-params">message</span>) </span>{
        message.active
          ? observerWrapper.connectObserver() 
          : observerWrapper.disconnectObserver()
    }

    listenForMessage(messageCallback)
}

init()
</code></pre>
<h3 id="heading-wrap-up">Wrap Up</h3>
<p>In conclusion, this article demonstrates how to create a YouTube Chrome Extension that effectively disables the "Add to Queue" UI on YouTube pages. By utilizing Chrome APIs and JavaScript, we implemented a functional extension that can be toggled on and off by the user. This tutorial showcases the thought process, challenges, and solutions involved in developing a Chrome Extension, ultimately providing a solid foundation for further exploration and development in this area. The next post in this series will detail how to package this extension for release on the Chrome Web Store.</p>
]]></content:encoded></item><item><title><![CDATA[YQM: Chrome Extension Concept]]></title><description><![CDATA[I love YouTube. It's the go-to platform for research, education, and entertainment. I don't watch any TV except for live sports (and even then, only during playoffs), and I can't seem to finish any TV shows even though the number of episodes has been...]]></description><link>https://cdrani.dev/yqm-chrome-extension-concept</link><guid isPermaLink="true">https://cdrani.dev/yqm-chrome-extension-concept</guid><category><![CDATA[chrome extension]]></category><category><![CDATA[youtube]]></category><category><![CDATA[Idea to build project]]></category><category><![CDATA[#ChromeExtensions]]></category><category><![CDATA[queue management]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Fri, 02 Jun 2023 22:05:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/4QmSdCP4bhM/upload/2f1057c3f7dec5d92b01371d59c8a1ee.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I love YouTube. It's the go-to platform for research, education, and entertainment. I don't watch any TV except for live sports (and even then, only during playoffs), and I can't seem to finish any TV shows even though the number of episodes has been reduced from 22-25 to 13, 10, and finally 4-6 for mini-series or UK shows. However, I can watch entire documentaries or analysis videos spanning hours or multiple parts on YouTube.</p>
<p>I spend most of my time on YouTube, beginning with some "Skip and Shannon: Undisputed" for sports recaps and analysis, although I mainly watch it for their banter. Later in the afternoon, I watch videos from my subscriptions, and I end the day with videos from my "Watch Later" playlist. This routine might seem reasonable, and it could be, except for the fact that I often watch videos through a queue, starting with my daily routine videos and gradually adding any recommended ones. The problem is that my queues in the mornings usually consist of only 4-5 videos, totalling 25-40 minutes, which can easily fit between deep work sessions. However, now they are approaching 40-60 minutes, and I'm concerned that it may get further out of hand if I don't limit this practice.</p>
<h2 id="heading-youtube-add-on-objective">Youtube Add-on Objective</h2>
<p>I need to create a Chrome Extension that will do the following:</p>
<ul>
<li><p>Either disable or remove all the <code>Add to queue</code> buttons (on hover and via the dropdown:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684342075626/41b55093-bd9e-4bfb-8d87-093495d1ace3.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Ability to toggle this feature on/off from a popup:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684342301450/cb878bae-8e6b-4a1c-8530-262a6fa75e2a.png" alt class="image--center mx-auto" /></p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684342361965/c0706767-4eeb-449f-a4a8-7226bb308035.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Features to limit videos added to the queue, either by setting a queue length or setting a max total time for videos. Update the queue playlist ui to display the total time of videos in the queue.</p>
</li>
<li><p>Display a tooltip, alert, etc if the <code>Add to queue</code> button is disabled (if that's what's chosen in the first bullet) and the reason is due to the set queue length or max total time.</p>
</li>
<li><p>Fix this UI issue:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684361705034/88bf1be6-5283-40da-9bbb-4ed402f33247.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Enlarge Video Duration so a user is doubly aware of what they are potentially adding to a queue</p>
</li>
<li><p>(Optional) Display an analysis or recap if this extension is functioning to limit watch time or queue length when it's enabled, and also unintentionally when it's disabled. Ideally, I should be able to gradually reduce my reliance on this self-imposed restriction and only activate it when I start slacking off, based on the usage review.</p>
</li>
<li><p>(Optional) Automate video removal from queue upon video end. Upon queue completion also clear the queue without having to interact with the modal</p>
</li>
<li><p>(Optional) Automate queue generation, such as in the mornings, create a queue with 2-3 of the top Undisputed videos that are only 10-13 minutes. Additionally, maybe delete/clear the queue at a certain time. Do the same for the afternoon but focused on subscriptions. For subscriptions, further filtering will be needed to prioritize channels weighed with watch time. For later in the evening, maybe disable functionality altogether as it's my time.</p>
</li>
<li><p>(Optional) Maybe in the evening automate plucking some videos from my ever-increasing "Watch later" playlist into a queue. After viewing a video selected from the "Watch Later" playlist, the video should be removed from the queue and playlist.</p>
</li>
</ul>
<p>So, we have a plan on what to create, now it's just a matter of breaking ground on it. There are a few features that are a must-have for a v1 release (if I plan on releasing it on the web store), but I hope to keep on working on it to either implement the optional features or at least open source it and invite and help others contribute them.</p>
<p>In the next article in this series, we will start with setting up the structure and prerequisites for a Chrome Extension and implement the first two features.</p>
]]></content:encoded></item><item><title><![CDATA[Present Spotify Data in Tmux with Applescript]]></title><description><![CDATA[TLDR: This article demonstrates how to use AppleScript to control Spotify and display song information in a tmux status bar.

Tmux is a terminal multiplexer: it enables several terminals to be created, accessed, and controlled from a single screen. T...]]></description><link>https://cdrani.dev/present-spotify-data-in-tmux-with-applescript</link><guid isPermaLink="true">https://cdrani.dev/present-spotify-data-in-tmux-with-applescript</guid><category><![CDATA[Spotify]]></category><category><![CDATA[tmux]]></category><category><![CDATA[applescript]]></category><category><![CDATA[Scripting]]></category><category><![CDATA[scripting languages]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Sun, 21 May 2023 20:35:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/NIo8Fd-RngE/upload/968e0d5d66834050868df98269d2c69a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><mark>TLDR: This article demonstrates how to use AppleScript to control Spotify and display song information in a tmux status bar.</mark></p>
<blockquote>
<p>Tmux is a terminal multiplexer: it enables several terminals to be created, accessed, and controlled from a single screen. Tmux may be detached from a screen and continue running in the background, then later reattached.</p>
<p>source: <a target="_blank" href="https://github.com/tmux/tmux">https://github.com/tmux/tmux</a></p>
<p><strong>AppleScript</strong> is a <a target="_blank" href="https://en.wikipedia.org/wiki/Scripting_language">scripting language</a> created by <a target="_blank" href="https://en.wikipedia.org/wiki/Apple_Inc.">Apple Inc.</a> that facilitates automated control over scriptable <a target="_blank" href="https://en.wikipedia.org/wiki/Mac_(computer)">Mac</a> applications. The term "AppleScript" may refer to the language itself, to an individual script written in the language, or, informally, to the macOS <a target="_blank" href="https://en.wikipedia.org/wiki/AppleScript#Open_Scripting_Architecture">Open Scripting Architecture</a> that underlies the language.<a target="_blank" href="https://en.wikipedia.org/wiki/AppleScript#cite_note-Goldstein-4"><sup>[4]</sup></a><a target="_blank" href="https://en.wikipedia.org/wiki/AppleScript#cite_note-Sanderson-5"><sup>[5]</sup></a></p>
<p>source: <a target="_blank" href="https://en.wikipedia.org/wiki/AppleScript">https://en.wikipedia.org/wiki/AppleScript</a></p>
</blockquote>
<h2 id="heading-intro">INTRO</h2>
<p>I have been using Tmux in my workflow with the <a target="_blank" href="https://github.com/gpakosz/.tmux">https://github.com/gpakosz/.tmux</a> configuration. I've done further research into how to personalize it. Unfortunately, most plugins are written in a shell script, which is understandable as shells like <strong>bash</strong> are available (or built-in) in most distros or OSs. I do want to learn how to write shell scripts, not just for Tmux, but as well as the universality of it.</p>
<p><strong><em><mark>NOTE: AppleScripts will only work on a macOS ecosystem.</mark></em></strong></p>
<p>Let's create a script that's <strong>human-readable</strong> using AppleScript to get a feel for the language and then expand on it by utilizing a shell script afterwards.</p>
<h2 id="heading-implementation-setup">IMPLEMENTATION: SETUP</h2>
<p>The purpose of the following script is to explore the AppleScript language and how to utilize it to interact with the Spotify app. Additionally, we aim to display the current song information (track and artist name), as well as the player's state (song playing or paused), and whether Spotify has been launched and displays all this data in a tmux status bar.</p>
<p>As for most script files, the heading should be the environment that the file should be run in:</p>
<blockquote>
<p>#!/usr/bin/env osascript</p>
</blockquote>
<ol>
<li><p>First, we must ascertain whether Spotify is running; if it isn't, display that Spotify is currently inactive. Take note of the language's readability and the keywords employed.</p>
<pre><code class="lang-bash"> tell application <span class="hljs-string">"Spotify"</span>
     <span class="hljs-keyword">if</span> it is running <span class="hljs-keyword">then</span>
         <span class="hljs-comment"># </span>
     <span class="hljs-keyword">else</span>
         <span class="hljs-string">"♫ 💤"</span> 
     end <span class="hljs-keyword">if</span>
 end
</code></pre>
</li>
<li><p>Now, we want to obtain and store the current track and artist's name and the player state (playing or paused) in variables, specifically, track_name, artist_name, and player_state, respectively. We could also retrieve other information, such as the album_name,</p>
<pre><code class="lang-diff"> tell application "Spotify"
     if it is running then
 +       set track_name to name of current track
 +       set artist_name to artist of current track
 +       set player_state to player state as string
     else
         "♫ 💤" 
     end if
 end
</code></pre>
</li>
<li><p>We now want to display the info we gathered from above. We can use an if/else statement to branch between what we display based on the player state.</p>
<pre><code class="lang-diff"> tell application "Spotify"
     if it is running then
         set track_name to name of current track
         set artist_name to artist of current track
         set player_state to player state as string
 +       if player_state is equal "playing"
 +           "♫ ⏵ " &amp; track_name &amp; " + " &amp; artist_name
 +       else
 +           "♫ ⏸ " &amp; track_name &amp; " + " &amp; artist_name
 +       end if
     else
         "♫ 💤" 
     end if
 end
</code></pre>
</li>
<li><p>And that's it. However, we can slightly refactor the code and introduce functions. Finally, let's include the maintainer's information (optional, but helpful for directing complaints when the script doesn't work as intended) and a description of the script's function for users.</p>
<pre><code class="lang-diff"> #!/usr/bin/env osascript

 + -- maintainer: cdrani
 + -- Returns Spotify current player state and song info

 + on getPlayerState(player_state)
 +     if player_state is "playing" then return "♫ ⏵ "
 +      if player_state is "paused" then return "♫ ⏸ "
 + end getPlayerState

 + on printSongInfo(player_state, track_name, artist_name)
 +    getPlayerState(player_state) &amp; track_name &amp; " - " &amp; + artist_name
 + end printSongInfo

 tell application "Spotify"
 +    if it is not running then
 +       return "♫ 💤"
 +   end

     set track_name to name of current track
     set artist_name to artist of current track
     set player_state to player state as string

 +    my printSongInfo(player_state, track_name, artist_name)
 end tell
</code></pre>
</li>
<li><p>Now, we want to make the script executable. I saved mine as "spotify.scpt" (<code>scpt</code> is the filename for AppleScript) in a scripts directory (which I am currently inside):</p>
<blockquote>
<p>chmod +x ~/scripts/spotify.scpt</p>
</blockquote>
</li>
<li><p>The final step is to integrate it into our Tmux status line. I opted for the right-hand side. Inside a Tmux session, enter your command prompt using <code>Prefix + :</code> and type the following based on the path the script file</p>
<blockquote>
<p>set -g status-right '#(~/scripts/spotify.scpt)'</p>
</blockquote>
</li>
</ol>
<p>Great! Implementing this idea is quite simple, from conception to full realization, except for the need to close each 'tell' and 'if' statement. The song information is displayed in the top-right corner and updates upon song change, although there is a slight ~2-second delay.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684534839974/e0dc02d7-eeda-4b8e-9e53-a337853547c8.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-applescript-editor">AppleScript Editor</h2>
<p>To begin, it's a good idea to use the AppleScript Editor, as it offers language features such as syntax highlighting, error compilation, quick access to language documentation, and more. Below is the aforementioned script within the editor. In the editor, we build the script to verify that there are no issues, and then we can run it, with errors and results displayed in the bottom pane.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684534122147/2267fe0a-055f-4b81-a719-8b127cad016a.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-javascript-editor">JavaScript Editor</h2>
<p>AppleScript also supports JavaScript. Our script can be rewritten in JavaScript:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// maintainer: cdrani</span>
<span class="hljs-comment">// Returns the current player state and song info for Spotify</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getPlayerState</span>(<span class="hljs-params">playerState</span>) </span>{
  <span class="hljs-keyword">if</span> (playerState === <span class="hljs-string">"playing"</span>) <span class="hljs-keyword">return</span> <span class="hljs-string">"♫ ⏵ "</span>;
  <span class="hljs-keyword">if</span> (playerState === <span class="hljs-string">"paused"</span>) <span class="hljs-keyword">return</span> <span class="hljs-string">"♫ ⏸ "</span>;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">printSongInfo</span>(<span class="hljs-params">playerState, trackName, artistName</span>) </span>{
  <span class="hljs-keyword">return</span> getPlayerState(playerState) + trackName + <span class="hljs-string">" - "</span> + artistName;
}

<span class="hljs-keyword">const</span> spotify = Application(<span class="hljs-string">"Spotify"</span>);

<span class="hljs-keyword">if</span> (!spotify.running()) {
    <span class="hljs-string">"♫ 💤"</span>;
} <span class="hljs-keyword">else</span> {
  <span class="hljs-keyword">const</span> trackName = spotify.currentTrack.name();
  <span class="hljs-keyword">const</span> artistName = spotify.currentTrack.artist();
  <span class="hljs-keyword">const</span> playerState = spotify.playerState();

  printSongInfo(playerState, trackName, artistName);
}
</code></pre>
<p>Similar to the first script, save this one as ~/scripts/spotify.js. You can now run it in your Tmux command prompt. Since this is a JS file, you need to inform AppleScript about it:</p>
<p><strong><em><mark>-l language | Override the language for any plain text files. Normally, plain text files are compiled as AppleScript.</mark></em></strong></p>
<pre><code class="lang-javascript">set -g status-right <span class="hljs-string">'#(osascript -l JavaScript ~/scripts/spotify.js)'</span>
</code></pre>
<p>The integration of our scripts into Tmux via the command prompt is temporary and gets cleared when the session or server is terminated. To make it permanent, we need to transfer it to our configuration file, typically saved as ~/.tmux.conf, and then source the config file to apply the changes.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">"set -g status-right '#(~/scripts/spotify.scpt)'"</span> &gt;&gt; .tmux.conf
tmux source-file ~/.tmux.conf
</code></pre>
<h2 id="heading-bonus-feature">Bonus Feature</h2>
<p>Announcement Feature</p>
<p>The final feature we will incorporate enables the announcement of the current song, as well as its play/pause status, whenever the song changes.</p>
<ol>
<li><p>Let's first introduce a new <code>say</code>, which will speak out loud our songInfo.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684642126852/379891ca-e3b4-489b-9edc-7fadf2724429.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-diff"> +    on sayPlayerState(player_state, track_name, artist_name)
 +        say player_state &amp; " " &amp; track_name &amp; " by " &amp; artist_name
 +    end sayPlayerState

 tell application "Spotify"
      -- truncated
 +    sayPlayerState(player_state, track_name, artist_name)
      my printSongInfo(player_state, track_name, artist_name)
 end tell
</code></pre>
</li>
<li><p>An issue with the above text is that the song information is repeated multiple times per song. This is likely because new data is being fed into our script every few seconds, based on the current song's state - its name, artist, whether it's playing or paused, etc. We want to limit this repetition to only occur when the song changes or the song's play/pause state changes. Let's track the song and state by defining their properties set to "missing value". jj</p>
<pre><code class="lang-diff"> -- properties to track song and state; set to non value
 +     property previous_song : missing value
 +     property previous_state : missing value

 --- 

 +    set stateExistsAndIsDifferent to previous_state is equal to missing value or previous_state is not equal to player_state
 +    set isPlayingAndSongChanged to previous_state is "playing" and previous_song is not equal to track_name

 +    -- update previous_state and previous_song with new states
 +    if (stateExistsAndIsDifferent or isPlayingAndSongChanged)
 +        set previous_state to player_state
 +        set previous_song to track_name
 +        sayPlayerState(player_state, track_name, artist_name)
 + 
 +        -- send command to update tmux status 
 +        do shell script "tmux set-option -g status-right '" &amp; my printSongInfo(player_state, track_name, artist_name) &amp; "'"
 +   end if
</code></pre>
</li>
<li><p>Finally, continuously refresh the data by running the script in a loop with a 1-second delay.</p>
<pre><code class="lang-diff"> #!/usr/bin/env osascript

 # maintainer: cdrani
 # Returns the current player state and song info for Spotify

 property previous_song : missing value
 property previous_state : missing value

 on getPlayerState(player_state)
     if player_state is "playing" then return "♫ ⏵ "
     if player_state is "paused" then return "♫ ⏸ "
 end getPlayerState

 on printSongInfo(player_state, track_name, artist_name)
     getPlayerState(player_state) &amp; track_name &amp; " - " &amp; artist_name
 end printSongInfo

 on sayPlayerState(player_state, track_name, artist_name)
     say player_state &amp; " " &amp; track_name &amp; " by " &amp; artist_name
 end sayPlayerState

 + repeat
     tell application "Spotify"
         if it is not running then
             return "♫ 💤"
         end if

         set track_name to name of current track
         set artist_name to artist of current track
         set player_state to player state as string
     end tell

     set stateExistsAndIsDifferent to previous_state is equal to missing value or previous_state is not equal to player_state
     set isPlayingAndSongChanged to previous_state is "playing" and previous_song is not equal to track_name

     if (stateExistsAndIsDifferent or isPlayingAndSongChanged)
         set previous_state to player_state
         set previous_song to track_name

         sayPlayerState(player_state, track_name, artist_name)
         do shell script "tmux set-option -g status-right '" &amp; my printSongInfo(player_state, track_name, artist_name) &amp; "'"
      end if

 +    delay 1
 + end repeat
</code></pre>
</li>
</ol>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=v3p5hnHV1jU">https://www.youtube.com/watch?v=v3p5hnHV1jU</a></div>
<p> </p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This was a simple script to showcase AppleScript and to show how it can integrate with the Apple ecosystem, here focusing on extracting Spotify info to display in a Tmux status bar. In the next article, we will expand on it to add additional controls such as next, previous, pause/play, and repeat.</p>
]]></content:encoded></item><item><title><![CDATA[Simplify Rails App Deployment Using Docker]]></title><description><![CDATA[I can honestly say that I have given up on managing multiple Ruby and Rails versions on my computer. Regardless of whether it's homebrew, asdf, rbenv, or any other new package manager, I've had enough. This applies to open-source projects, take-home ...]]></description><link>https://cdrani.dev/simplify-rails-app-deployment-using-docker</link><guid isPermaLink="true">https://cdrani.dev/simplify-rails-app-deployment-using-docker</guid><category><![CDATA[Docker]]></category><category><![CDATA[Rails]]></category><category><![CDATA[Dockerfile]]></category><category><![CDATA[containers]]></category><category><![CDATA[docker-rails]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Mon, 15 May 2023 06:04:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/jOqJbvo1P9g/upload/06f53435135a6917bd9a48ce8afd3270.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I can honestly say that I have given up on managing multiple Ruby and Rails versions on my computer. Regardless of whether it's homebrew, asdf, rbenv, or any other new package manager, I've had enough. This applies to open-source projects, take-home assessments for interviews, and even pair-programming sessions. All Rails apps/projects should be Dockerized, and if they aren't, rest assured that a pull request is on its way to set it up accordingly by yours truly. In fact, in the upcoming <a target="_blank" href="https://github.com/rails/rails/commit/4f3af4a67f227ed7998fed570b9aa671e1b74117">Rails 7.1 release</a>, by default a Dockerfile will be included.</p>
<h2 id="heading-fresh-setup">Fresh Setup</h2>
<p>By "Fresh", I mean just having Docker Desktop and a Text Editor - No Rails necessary.</p>
<ol>
<li><p>Confirm installation of <a target="_blank" href="https://docs.docker.com/get-docker/">Docker</a> - (Community Edition will work fine) - with</p>
<p> <code>docker -v</code></p>
<pre><code class="lang-bash"> docker -v
 <span class="hljs-comment"># Docker version 23.0.5, build bc4487a</span>
</code></pre>
</li>
<li><p>Set up a docker container with a ruby image in which to create the rails app.</p>
<pre><code class="lang-bash"> docker run --rm -it -v <span class="hljs-string">"<span class="hljs-variable">$PWD</span>"</span>:/usr/src/app -w /usr/src/app ruby:3.2.2-bullseye bash -c <span class="hljs-string">"gem install rails &amp;&amp; rails new . -d postgresql -T"</span>
</code></pre>
<p> This command runs a Docker container using the <code>ruby:3.2.2-bullseye</code> image, with several options:</p>
<p> <code>docker run</code>: Run a Docker container.</p>
<p> <code>--rm</code>: Automatically remove the container when it exits.</p>
<p> <code>-it</code>: Allocate a tty for interactive input and output.</p>
<p> <code>-v "$PWD":/usr/src/app</code>: Mount the current directory as a volume inside the container at the path <code>/usr/src/app</code>.</p>
<p> <code>-w /usr/src/app</code>: Set the working directory inside the container to <code>/usr/src/app</code>.</p>
<p> <code>ruby:3.2.2-bullseye</code>: Use the <code>ruby:3.2.2-bullseye</code> image as the base image for the container.</p>
<p> <code>/bin/bash</code>: Start an interactive shell inside the container.</p>
<p> <code>-c "gem install rails &amp;&amp; rails new . -d postgresql -T"</code>: Execute a shell command inside the container that installs Rails using <code>gem install</code> and then runs the <code>rails new</code> command within the current directory with the specified options - setting the database to PostgreSQL and optionally skipping tests setup.</p>
</li>
<li><p>We should now have a Rails app generated on our machine. Next, we want to dockerize it so that we can run all processes, such as the server, console, migrations, and more within containers. Let's begin with the server; to do this, create a Dockerfile (also written as dockerfile, which is my preference).</p>
<pre><code class="lang-yaml"> <span class="hljs-string">FROM</span> <span class="hljs-string">ruby:3.2.2-bullseye</span>

 <span class="hljs-comment"># Set the working directory inside the container</span>
 <span class="hljs-string">WORKDIR</span> <span class="hljs-string">/app</span>

 <span class="hljs-comment"># Update Dependencies</span>
 <span class="hljs-string">RUN</span> <span class="hljs-string">apt-get</span> <span class="hljs-string">update</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">\</span>
     <span class="hljs-string">apt-get</span> <span class="hljs-string">clean</span> 

 <span class="hljs-comment"># Copy the Gemfile and Gemfile.lock from the host into the container</span>
 <span class="hljs-string">COPY</span> <span class="hljs-string">Gemfile</span> <span class="hljs-string">Gemfile.lock</span> <span class="hljs-string">./</span>

 <span class="hljs-comment"># Install the RubyGems</span>
 <span class="hljs-string">RUN</span> <span class="hljs-string">gem</span> <span class="hljs-string">install</span> <span class="hljs-string">bundler:2.4.13</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">\</span>
     <span class="hljs-string">bundle</span> <span class="hljs-string">config</span> <span class="hljs-string">--global</span> <span class="hljs-string">frozen</span> <span class="hljs-number">1</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">\</span>
     <span class="hljs-string">bundle</span> <span class="hljs-string">install</span> <span class="hljs-string">--jobs</span> <span class="hljs-number">4</span> <span class="hljs-string">--retry</span> <span class="hljs-number">3</span>

 <span class="hljs-comment"># Copy the rest of the application into the container</span>
 <span class="hljs-string">COPY</span> <span class="hljs-string">.</span> <span class="hljs-string">.</span>

 <span class="hljs-comment"># Expose port 3000</span>
 <span class="hljs-string">EXPOSE</span> <span class="hljs-number">3000</span>

 <span class="hljs-comment"># Start the Rails server</span>
 <span class="hljs-string">CMD</span> [<span class="hljs-string">"rails"</span>, <span class="hljs-string">"server"</span>, <span class="hljs-string">"-b"</span>, <span class="hljs-string">"0.0.0.0"</span>]
</code></pre>
</li>
<li><p>Now, we need a container that includes a PostgreSQL image and connects it to the Rails app on the same network. An easy approach is to use a docker-compose.yaml file (or compose.yaml, depending on your preference).</p>
<pre><code class="lang-yaml"> <span class="hljs-attr">services:</span>
   <span class="hljs-attr">db:</span>
     <span class="hljs-attr">container_name:</span> <span class="hljs-string">db</span>
     <span class="hljs-attr">image:</span> <span class="hljs-string">postgres:14-alpine</span>
     <span class="hljs-attr">environment:</span>
       <span class="hljs-attr">POSTGRES_USER:</span> <span class="hljs-string">postgres</span>
       <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">postgres</span>
       <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">attic_development</span>
     <span class="hljs-attr">volumes:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">db_date:/var/lib/postgresql/data</span>

   <span class="hljs-attr">web:</span>
     <span class="hljs-attr">container_name:</span> <span class="hljs-string">web</span>
     <span class="hljs-attr">build:</span> <span class="hljs-string">.</span>
     <span class="hljs-attr">ports:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">"3000:3000"</span>
     <span class="hljs-attr">depends_on:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">db</span>
     <span class="hljs-attr">environment:</span>
       <span class="hljs-attr">DATABASE_URL:</span> <span class="hljs-string">postgresql://postgres:postgres@db/attic_development</span>
     <span class="hljs-attr">volumes:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">.:/app</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">gem_cache:/usr/local/bundle/gems</span>

 <span class="hljs-attr">volumes:</span>
   <span class="hljs-attr">gem_cache:</span>
   <span class="hljs-attr">db_data:</span>
</code></pre>
<p> The <code>compose.yaml</code> file is used to define a multi-container Docker application. It consists of a version number (in this case, version 3), and a list of services that make up the application.</p>
<p> The <code>services</code> section of the file contains two services:</p>
<p> <code>db</code>: This service is defined using the official PostgreSQL Docker image tagged <code>14-alpine</code>. It sets three environment variables:</p>
<ul>
<li><p><code>POSTGRES_USER</code>: The username for the default PostgreSQL user.</p>
</li>
<li><p><code>POSTGRES_PASSWORD</code>: The password for the default PostgreSQL user.</p>
</li>
<li><p><code>POSTGRES_DB</code>: The name of the default database to be created.</p>
</li>
</ul>
</li>
</ol>
<p>    <code>web</code>: This service is defined to <strong>build</strong> a Docker image from the <code>dockerfile</code> in the current directory (<code>.</code>). It maps <strong>port</strong> <code>3000</code> of the host to port <code>3000</code> of the container. It depends on the <code>db</code> service, which means that the <code>db</code> service will be started before the <code>web</code> service. It also sets the <code>DATABASE_URL</code> environment variable to connect the Rails app to the PostgreSQL database. The <code>depends_on</code> section specifies that the <code>db</code> service should be started before the <code>web</code> service. The <code>environment</code> section sets the <code>DATABASE_URL</code> environment variable to the URL of the PostgreSQL database. The format of the URL is <code>postgresql://&lt;username&gt;:&lt;password&gt;@&lt;hostname&gt;/&lt;database&gt;</code>. In this case, the username is <code>postgres</code>, the password is <code>postgres</code>, the hostname is <code>db</code> (the name of the <code>db</code> service), and the database name is <code>attic_development</code>.</p>
<ol>
<li><p>Now we can run both services together:</p>
<pre><code class="lang-bash"> docker compose up -d
</code></pre>
<p> The "-d" option is for detached mode, which means running in the background. In most cases, this is the ideal mode, as without this flag, our terminal will stream the Rails server logs. This would require us to open a new terminal tab to run other commands for migrations, interact with the console, and so on.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683790081061/f4ad0553-6a71-4a59-866e-9ddb95d7e701.png" alt /></p>
<h3 id="heading-common-commands">Common Commands:</h3>
<p> Running various Rails commands within the web container:</p>
<pre><code class="lang-bash"> docker compose run web rails c
 docker compose run web rails g model|controller|migration ...
 docker compose run web rails db:migrate
 docker compose run web rails t
 docker compose run web bundle <span class="hljs-built_in">exec</span> rspec
 docker compose run web bundle add|install|update|remove [gem]
</code></pre>
<p> The above <code>docker compose run web rails</code> should be saved into an alias such as:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">alias</span> dcr=<span class="hljs-string">"docker compose run web rails"</span>
 <span class="hljs-built_in">alias</span> dcb=<span class="hljs-string">"docker compuse bundle"</span>
 <span class="hljs-built_in">alias</span> dcbr=<span class="hljs-string">"docker compose bundle exec rspec"</span>
</code></pre>
<p> View <code>rails s</code> logs:</p>
<pre><code class="lang-bash"> docker logs -f web
</code></pre>
</li>
</ol>
<h2 id="heading-best-practices">Best Practices</h2>
<p>The above is a minimal dockerized rails app sufficient for development. However, there are additional changes we would need to make to adhere to some best practices and make it ready for deployment in a production environment. For instance, we could use a slimmer image of Ruby, create and use a non-root user in our container, set up multi-stage builds, etc. There's a lot to glean from the examples in these posts for better <a target="_blank" href="https://lipanski.com/posts/dockerfile-ruby-best-practices">Dockerfile</a> and <a target="_blank" href="https://nickjanetakis.com/blog/best-practices-around-production-ready-web-apps-with-docker-compose">Docker Compose</a> files. Additionally, Docker has an <a target="_blank" href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/">official post</a> on this matter.</p>
<h2 id="heading-setup-for-optimal-developmentproduction">Setup for Optimal Development/Production</h2>
<p>This is most likely the most difficult part as there will likely be variances in how lean and performant one's production docker setup is in comparison to others found online. Furthermore, this comes with experience and testing. As in most cases where I am my own DevOps team, it's better to lean on the expertise of others who readily provide it. There are numerous rails-centric templates with fully-configured docker setup with GitHub actions for automated deployments on GitHub.</p>
<p>I came across <a target="_blank" href="https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development">Ruby on Whales</a> post and it was truly a godsend. They clearly outline and explain their decisions for their particular setup and keep the post updated. Furthermore, they provide a template so anyone can duplicate their configurations. The script run in their generator can be found here. The setup is quite straightforward by running the below inside our rails app directory:</p>
<pre><code class="lang-bash">gem install rbytes
gem install dip
rbytes install https://railsbytes.com/script/z5OsoB
dip provision
</code></pre>
<p>The <a target="_blank" href="https://github.com/palkan/rbytes">rbytes</a> command will install an interactive script to set up the docker environment in a <strong>.dockerdev</strong> folder and include a <code>dip.yml</code> file. The script itself can be found <a target="_blank" href="https://railsbytes.com/public/templates/z5OsoB">here</a> for reference and forked for modification. For a full scope of the inner workings of the generator reference the <a target="_blank" href="https://github.com/evilmartians/ruby-on-whales">Ruby on Whales Github repo</a>.</p>
<p><a target="_blank" href="https://github.com/bibendi/dip">Dip</a> Dip (Docker Interaction Program) is a tool to simplify the previously complicated method of utilizing Docker Compose, making the process smoother. Review the <code>dip.yml</code> to see the commands we can run:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'7.1'</span>

<span class="hljs-comment"># Define default environment variables to pass</span>
<span class="hljs-comment"># to Docker Compose</span>
<span class="hljs-attr">environment:</span>
  <span class="hljs-attr">RAILS_ENV:</span> <span class="hljs-string">development</span>

<span class="hljs-attr">compose:</span>
  <span class="hljs-attr">files:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">.dockerdev/compose.yml</span>
  <span class="hljs-attr">project_name:</span> <span class="hljs-string">attic</span>

<span class="hljs-attr">interaction:</span>
  <span class="hljs-comment"># This command spins up a Rails container with the required dependencies (such as databases),</span>
  <span class="hljs-comment"># and opens a terminal within it.</span>
  <span class="hljs-attr">runner:</span>
    <span class="hljs-attr">description:</span> <span class="hljs-string">Open</span> <span class="hljs-string">a</span> <span class="hljs-string">Bash</span> <span class="hljs-string">shell</span> <span class="hljs-string">within</span> <span class="hljs-string">a</span> <span class="hljs-string">Rails</span> <span class="hljs-string">container</span> <span class="hljs-string">(with</span> <span class="hljs-string">dependencies</span> <span class="hljs-string">up)</span>
    <span class="hljs-attr">service:</span> <span class="hljs-string">rails</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">/bin/bash</span>

  <span class="hljs-comment"># Run a Rails container without any dependent services (useful for non-Rails scripts)</span>
  <span class="hljs-attr">bash:</span>
    <span class="hljs-attr">description:</span> <span class="hljs-string">Run</span> <span class="hljs-string">an</span> <span class="hljs-string">arbitrary</span> <span class="hljs-string">script</span> <span class="hljs-string">within</span> <span class="hljs-string">a</span> <span class="hljs-string">container</span> <span class="hljs-string">(or</span> <span class="hljs-string">open</span> <span class="hljs-string">a</span> <span class="hljs-string">shell</span> <span class="hljs-string">without</span> <span class="hljs-string">deps)</span>
    <span class="hljs-attr">service:</span> <span class="hljs-string">rails</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">/bin/bash</span>
    <span class="hljs-attr">compose_run_options:</span> [ <span class="hljs-literal">no</span><span class="hljs-string">-deps</span> ]

  <span class="hljs-comment"># A shortcut to run Bundler commands</span>
  <span class="hljs-attr">bundle:</span>
    <span class="hljs-attr">description:</span> <span class="hljs-string">Run</span> <span class="hljs-string">Bundler</span> <span class="hljs-string">commands</span>
    <span class="hljs-attr">service:</span> <span class="hljs-string">rails</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">bundle</span>
    <span class="hljs-attr">compose_run_options:</span> [ <span class="hljs-literal">no</span><span class="hljs-string">-deps</span> ]

  <span class="hljs-attr">rails:</span>
    <span class="hljs-attr">description:</span> <span class="hljs-string">Run</span> <span class="hljs-string">Rails</span> <span class="hljs-string">commands</span>
    <span class="hljs-attr">service:</span> <span class="hljs-string">rails</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">bundle</span> <span class="hljs-string">exec</span> <span class="hljs-string">rails</span>
    <span class="hljs-attr">subcommands:</span>
      <span class="hljs-attr">s:</span>
        <span class="hljs-attr">description:</span> <span class="hljs-string">Run</span> <span class="hljs-string">Rails</span> <span class="hljs-string">server</span> <span class="hljs-string">at</span> <span class="hljs-string">http://localhost:3000</span>
        <span class="hljs-attr">service:</span> <span class="hljs-string">web</span>
        <span class="hljs-attr">compose:</span>
          <span class="hljs-attr">run_options:</span> [ <span class="hljs-string">service-ports</span>, <span class="hljs-string">use-aliases</span> ]
      <span class="hljs-attr">test:</span>
        <span class="hljs-attr">description:</span> <span class="hljs-string">Run</span> <span class="hljs-string">unit</span> <span class="hljs-string">tests</span>
        <span class="hljs-attr">service:</span> <span class="hljs-string">rails</span>
        <span class="hljs-attr">command:</span> <span class="hljs-string">bundle</span> <span class="hljs-string">exec</span> <span class="hljs-string">rails</span> <span class="hljs-string">test</span>
        <span class="hljs-attr">environment:</span>
          <span class="hljs-attr">RAILS_ENV:</span> <span class="hljs-string">test</span>

  <span class="hljs-attr">yarn:</span>
    <span class="hljs-attr">description:</span> <span class="hljs-string">Run</span> <span class="hljs-string">Yarn</span> <span class="hljs-string">commands</span>
    <span class="hljs-attr">service:</span> <span class="hljs-string">rails</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">yarn</span>
    <span class="hljs-attr">compose_run_options:</span> [ <span class="hljs-literal">no</span><span class="hljs-string">-deps</span> ]

  <span class="hljs-attr">psql:</span>
    <span class="hljs-attr">description:</span> <span class="hljs-string">Run</span> <span class="hljs-string">Postgres</span> <span class="hljs-string">psql</span> <span class="hljs-string">console</span>
    <span class="hljs-attr">service:</span> <span class="hljs-string">postgres</span>
    <span class="hljs-attr">default_args:</span> <span class="hljs-string">attic_development</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">psql</span> <span class="hljs-string">-h</span> <span class="hljs-string">postgres</span> <span class="hljs-string">-U</span> <span class="hljs-string">postgres</span>

<span class="hljs-attr">provision:</span>
  <span class="hljs-comment"># We need the `|| true` part because some docker-compose versions</span>
  <span class="hljs-comment"># cannot down a non-existent container without an error,</span>
  <span class="hljs-comment"># see https://github.com/docker/compose/issues/9426</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">dip</span> <span class="hljs-string">compose</span> <span class="hljs-string">down</span> <span class="hljs-string">--volumes</span> <span class="hljs-string">||</span> <span class="hljs-literal">true</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">dip</span> <span class="hljs-string">compose</span> <span class="hljs-string">up</span> <span class="hljs-string">-d</span> <span class="hljs-string">postgres</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">dip</span> <span class="hljs-string">bash</span> <span class="hljs-string">-c</span> <span class="hljs-string">bin/setup</span>
</code></pre>
<p>The first command to run would be <code>dip provision</code> which as shown above is for a fresh setup - shutting down volumes, starting postgres service, and running the <code>bin/setup</code> script which will install/update gems, prepare the database, and restart the rails:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/usr/bin/env ruby</span>
require <span class="hljs-string">"fileutils"</span>

<span class="hljs-comment"># path to our application root.</span>
APP_ROOT = File.expand_path(<span class="hljs-string">".."</span>, __dir__)

def system!(*args)
  system(*args) || abort(<span class="hljs-string">"\n== Command #{args} failed =="</span>)
end

FileUtils.chdir APP_ROOT <span class="hljs-keyword">do</span>
  <span class="hljs-comment"># This script is a way to set up or update our development environment automatically.</span>
  <span class="hljs-comment"># This script is idempotent, so that we can run it at any time and get an expectable outcome.</span>
  <span class="hljs-comment"># Add necessary setup steps to this file.</span>

  puts <span class="hljs-string">"== Installing dependencies =="</span>
  system! <span class="hljs-string">"gem install bundler --conservative"</span>
  system(<span class="hljs-string">"bundle check"</span>) || system!(<span class="hljs-string">"bundle install"</span>)

  <span class="hljs-comment"># puts "\n== Copying sample files =="</span>
  <span class="hljs-comment"># unless File.exist?("config/database.yml")</span>
  <span class="hljs-comment">#   FileUtils.cp "config/database.yml.sample", "config/database.yml"</span>
  <span class="hljs-comment"># end</span>

  puts <span class="hljs-string">"\n== Preparing database =="</span>
  system! <span class="hljs-string">"bin/rails db:prepare"</span>

  puts <span class="hljs-string">"\n== Removing old logs and tempfiles =="</span>
  system! <span class="hljs-string">"bin/rails log:clear tmp:clear"</span>

  puts <span class="hljs-string">"\n== Restarting application server =="</span>
  system! <span class="hljs-string">"bin/rails restart"</span>
end
</code></pre>
<p>We can run rails-specific commands defined in the <code>dip.yml</code> just like we did previously but prefixed with <code>dip</code>, but a more extensive list of available commands are written in the <code>.dockerdev/README.md</code> file:</p>
<pre><code class="lang-markdown"><span class="hljs-section"># snippet from .dockerdev/README.md # </span>
<span class="hljs-section"># run rails server</span>
dip rails s

<span class="hljs-section"># run rails console</span>
dip rails c

<span class="hljs-section"># run rails server with debugging capabilities (i.e., `debugger` would work)</span>
dip rails s

<span class="hljs-section"># or run the while web app (with all the dependencies)</span>
dip up web

<span class="hljs-section"># run migrations</span>
dip rails db:migrate

<span class="hljs-section"># pass env variables into application</span>
dip VERSION=20100905201547 rails db:migrate:down

<span class="hljs-section"># simply launch bash within app directory (with dependencies up)</span>
dip runner

<span class="hljs-section"># execute an arbitrary command via Bash</span>
dip bash -c 'ls -al tmp/cache'

<span class="hljs-section"># Additional commands</span>

<span class="hljs-section"># update gems or packages</span>
dip bundle install
dip yarn install

<span class="hljs-section"># run psql console</span>
dip psql

<span class="hljs-section"># run tests</span>
<span class="hljs-section"># TIP: `dip rails test` is already auto prefixed with `RAILS<span class="hljs-emphasis">_ENV=test`
dip rails test

# shutdown all containers
dip down</span></span>
</code></pre>
<p>From the snippet above we can run the same rails-centric commands prefixed with <code>dip</code>, however, there is also an option that makes the prefix unnecessary depending on your shell.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>I have found the interactive CLI option to be my go-to approach as it adheres to best practices and doubly gives the options to set the Rails and PostgreSQL versions (among other environment variables) which I usually hardcode. Currently working on various self and OSS projects with differing rails versions and I am at peace with integrating them with docker and contributing without the hassle of wrangling with the setup of multiple rails versions and gems issues based on my OS.</p>
]]></content:encoded></item><item><title><![CDATA[Exploring Rails Again: A Fresh Look]]></title><description><![CDATA[As someone who loves Ruby and Rails, I'm taking advantage of my sabbatical to re-learn these technologies. In the past, I learned just enough to complete a feature, but I also picked up some bad practices along the way, such as bloated controllers an...]]></description><link>https://cdrani.dev/exploring-rails-again-a-fresh-look</link><guid isPermaLink="true">https://cdrani.dev/exploring-rails-again-a-fresh-look</guid><category><![CDATA[Rails]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Ruby]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Wed, 26 Apr 2023 20:18:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/S5jD0E8DOC0/upload/68a6fe751c8e20bd045ab023affb35dd.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As someone who loves Ruby and Rails, I'm taking advantage of my sabbatical to re-learn these technologies. In the past, I learned just enough to complete a feature, but I also picked up some bad practices along the way, such as bloated controllers and models, not writing tests, and not fully understanding service objects and PORO. To atone for my mistakes, I want to document the errors I made and their solutions as I learn Rails anew. While reflecting on my past projects, I've realized there are many lessons I missed, details I overlooked, and bad practices I need to purge. This article is my attempt to share my experience and help others avoid making the same mistakes.</p>
]]></content:encoded></item><item><title><![CDATA[AWS Certifications Journey]]></title><description><![CDATA[I have decided to tackle the AWS Associate Level Certifications. I plan to do them in this order: Solutions Architect, Developer, and SysOps. Within the next few years, I would love to transition to working as a Cloud/Solutions Architect and/or DevOp...]]></description><link>https://cdrani.dev/aws-certifications-journey</link><guid isPermaLink="true">https://cdrani.dev/aws-certifications-journey</guid><category><![CDATA[AWS]]></category><category><![CDATA[Certificates]]></category><category><![CDATA[Cloud]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Wed, 15 Mar 2023 23:47:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1680208757934/7e1fea53-8bcd-4184-840d-a4b9d999dc08.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have decided to tackle the AWS Associate Level Certifications. I plan to do them in this order: Solutions Architect, Developer, and SysOps. Within the next few years, I would love to transition to working as a Cloud/Solutions Architect and/or DevOps Engineer, and certification would greatly help with the switch. At my current position, there's not much opportunity to have hands-on experience utilizing AWS services to create infrastructure in which to deploy applications. I have thus decided to learn it on my own through the <a target="_blank" href="https://cdrani.dev/project-based-learning">Project-Based Learning</a> series.</p>
<p>I find that I retain knowledge better through reading than via videos as one requires more focus, therefore my study will be textbook based. I will generally follow this study plan for each certification:</p>
<ol>
<li><p>Read the study guide provided by Wiley for the certifications, such as AWS</p>
<p> Certified Solutions Architect Study Guide Associate (SAA-C03) Exam Fourth Edition. I plan to read 1-2 chapters a day depending on how meaty the content proves to be.</p>
</li>
<li><p>Do the practice tests at the end of the chapter.</p>
</li>
<li><p>Before reading the next chapter, redo the practice tests for all the prior chapters. Hopefully, this helps with some memory retention and faster recall of previous knowledge.</p>
</li>
</ol>
<p>After completing the book and feeling confident about the practice tests I have taken, I will schedule the certification test about a week out. This will give me ample time to review and do some full practice exams to simulate the certification exam. I estimate that preparing for each certification will take me 1 - 1.5 months, so I figure, conservatively and with no retests, to complete all the certs by end of July? Fingers crossed.</p>
<p>This series won't be too long I believe, as there are only 3 certifications I have planned for this year. I only plan on writing a post once I pass each one and linking to the certification. Any change in pace might also warrant a small post to reset my resolve, but I plan to adhere to the timeline above.</p>
]]></content:encoded></item><item><title><![CDATA[Linux String Substition (sed)]]></title><description><![CDATA[Sed is a computer program that is used to make changes to text files. It reads the file line by line and performs operations on each line, such as finding and replacing text, deleting lines that match a certain pattern, or adding or removing text fro...]]></description><link>https://cdrani.dev/linux-string-substition-sed</link><guid isPermaLink="true">https://cdrani.dev/linux-string-substition-sed</guid><category><![CDATA[sed]]></category><category><![CDATA[Linux]]></category><category><![CDATA[sysadmin]]></category><category><![CDATA[KodeKloud]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Sat, 25 Feb 2023 20:38:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1677616614276/c22e8ffd-6576-409a-b4e7-e7de04d076c6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Sed</strong> is a computer program that is used to make changes to text files. It reads the file line by line and performs operations on each line, such as finding and replacing text, deleting lines that match a certain pattern, or adding or removing text from certain lines. Sed is often used together with other programs to perform more complex text-processing tasks. It is mostly used on Unix/Linux systems and can be run from the command line.</p>
<h2 id="heading-task">Task</h2>
<blockquote>
<p>There is some data on <code>Nautilus App Server 2</code> in <code>Stratos DC</code>. which needs to be altered in several of the files. On <code>Nautilus App Server 2</code>, alter the <code>/home/BSD.txt</code> file as per the details given below:</p>
<p>a. Delete all lines containing word <code>software</code> and save results in <code>/home/BSD_DELETE.txt</code> file. (Please be aware of case sensitivity)</p>
<p>b. Replace all occurrences of the word <code>or</code> to <code>is</code> and save results in <code>/home/BSD_REPLACE.txt</code> file.</p>
<p><code>Note:</code> Let's say you are asked to replace the word <code>to</code> with <code>from</code>. In that case, make sure not to alter any words containing this string; for example <code>upto</code>, <code>contributor</code>, etc.</p>
</blockquote>
<h2 id="heading-information">Information</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677294685541/587837be-e9f9-4f69-a1b1-f91dcd6bd99e.png" alt /></p>
<h2 id="heading-implementation">Implementation</h2>
<ol>
<li><p><code>ssh</code> into App Server 2 and enter the password of <strong>BigGr33n</strong> when prompted.</p>
<pre><code class="lang-bash"> ssh steve@172.16.238.11
</code></pre>
</li>
<li><p>Delete all lines containing word <code>software</code> and save results in <code>/home/BSD_DELETE.txt</code> file. (Please be aware of case sensitivity):</p>
<pre><code class="lang-bash"> sed <span class="hljs-string">'/software/d'</span> BSD.txt &gt; BSD_DELETE.txt
</code></pre>
<p> This command uses <strong>sed</strong> to modify the contents of <code>BSD.txt</code> and write the results to a new file called <code>BSD_DELETE</code>. Specifically, the command does the following:</p>
<ol>
<li><p><code>/software/d</code> is a command that specifies a pattern to match and an action to perform. In this case, the pattern is the string "software", and the action is to <strong>delete</strong> any line that contains that string.</p>
</li>
<li><p><code>BSD.txt</code> is the input file that sed should read and apply the command</p>
</li>
<li><p><code>&gt;</code> is a redirection operator that tells the shell to redirect the output of the command to a new file instead of the screen.</p>
</li>
<li><p><code>BSD_REPLACE.txt</code> is the name of the file that sed should write the modified output to.</p>
</li>
</ol>
</li>
</ol>
<p>    ***Please be aware of case sensitivity*** is already taken care of here as sed by default is case sensitive and will match the exact case in the pattern. To make it case insensitive, i.e match both <strong>Software</strong>, <strong>software</strong>, <strong>SOFTWARE,</strong> etc, we can add the <code>I</code> flag to the action: <code>/software/dI</code>.</p>
<ol>
<li><p>Replace all occurrences of the word <code>or</code> to <code>is</code> and save results in <code>/home/BSD_REPLACE.txt</code> file.</p>
<pre><code class="lang-bash"> sed <span class="hljs-string">'s_\bor\b_is_g'</span> BSD.txt &gt; BSD_REPLACE.txt
</code></pre>
<ol>
<li><p><code>s</code> is a sed command that stands for "substitute". It tells sed to search for a pattern and replace it with something else.</p>
</li>
<li><p><code>_\bor\b_is</code> is the search pattern that sed will look for in the input text. This pattern consists of the following components:</p>
<ul>
<li><p><code>_</code> is a delimiter character that separates the search pattern from the replacement text. It can be any character, but in this case, it is used instead of the more common <code>/</code> character, to avoid conflicts with the <code>/</code> characters in the pattern.</p>
</li>
<li><p><code>\b</code> is a regular expression metacharacter that matches a word <strong>boundary</strong>, i.e. the transition between a word character (alphanumeric or underscore) and a non-word character (such as space or punctuation). The word boundary ensures that only the exact word between the boundaries will be matched.</p>
</li>
<li><p><code>or</code> is the exact string that sed will look for in the input text. Only whole words that match the pattern will be replaced, not substrings that contain "or", such as "for", "gore", etc.</p>
</li>
<li><p><code>is</code> is the replacement text that sed will substitute for the matched pattern. In this case, the replacement text is simply the word "is".</p>
</li>
</ul>
</li>
<li><p><code>g</code> is an option that stands for "global". It tells sed to perform the substitution operation globally, i.e. on all occurrences of the pattern in each line of the input text, rather than just the first occurrence.</p>
</li>
</ol>
</li>
</ol>
<h3 id="heading-learning-takeaways">Learning Takeaways</h3>
<ol>
<li><p>There are a lot of differences between <strong>sed</strong> on macOS and <strong>gnu-sed</strong> used in Linux distros. For example, for this sample text, I had to the command substitution command differently:</p>
<pre><code class="lang-bash"> <span class="hljs-comment"># me.md</span>
 hello world
 goodbye world
 World Empire
 them form forth <span class="hljs-keyword">for</span> maybe
 angry sleight <span class="hljs-keyword">for</span> fore
</code></pre>
<p> 0n macOS, notice how I had to write the word boundaries:</p>
<pre><code class="lang-bash"> sed <span class="hljs-string">'s/[[:&lt;:]]for[[:&gt;:]]/pour/g'</span> me.md

 hello world
 goodbye world
 World Empire
 them form forth pour maybe
 angry sleight pour fore
</code></pre>
</li>
<li><p>Brew has a <code>gnu-sed</code> formula that can be run on macOS. This to me is a better option than learning two different commands' quirks:</p>
<pre><code class="lang-bash"> brew install gnu-sed
</code></pre>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677615452777/31c1cb44-bfdc-4e92-ac8e-408f14b3813a.png" alt class="image--center mx-auto" /></p>
<p> I will replace my default <strong>sed</strong> with <strong>gsed</strong> with the approach above. In my <code>.zshrc</code> file I can add the above <strong>PATH</strong> variable and then source the file:</p>
<pre><code class="lang-bash"> vim ~/.zshrc

 <span class="hljs-comment"># replace gsed (gnu-sed from homebrew) with native sed</span>
 <span class="hljs-built_in">export</span> PATH=<span class="hljs-string">"/opt/homebrew/opt/gnu-sed/libexec/gnubin:<span class="hljs-variable">$PATH</span>"</span>

 <span class="hljs-built_in">source</span> ~/.zshrc

 sed --version

 sed (GNU sed) 4.9
 Copyright (C) 2022 Free Software Foundation, Inc.
 License GPLv3+: GNU GPL version 3 or later &lt;https://gnu.org/licenses/gpl.html&gt;.
 This is free software: you are free to change and redistribute it.
 There is NO WARRANTY, to the extent permitted by law.

 Written by Jay Fenlason, Tom Lord, Ken Pizzini,
 Paolo Bonzini, Jim Meyering, and Assaf Gordon.

 This sed program was built without SELinux support.

 GNU sed home page: &lt;https://www.gnu.org/software/sed/&gt;.
 General <span class="hljs-built_in">help</span> using GNU software: &lt;https://www.gnu.org/gethelp/&gt;.
 E-mail bug reports to: &lt;bug-sed@gnu.org&gt;.
</code></pre>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Linux TimeZones Setting]]></title><description><![CDATA[Task

During the daily standup, it was pointed out that the timezone across Nautilus Application Servers in Stratos Datacenter doesn't match the local datacenter's timezone, which is America/Asuncion.

Information

Implementation
We have three App Se...]]></description><link>https://cdrani.dev/linux-timezones-setting</link><guid isPermaLink="true">https://cdrani.dev/linux-timezones-setting</guid><category><![CDATA[Linux]]></category><category><![CDATA[linux for beginners]]></category><category><![CDATA[sysadmin]]></category><category><![CDATA[KodeKloud]]></category><category><![CDATA[datetimectl]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Thu, 23 Feb 2023 19:29:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1677439685203/f85e925a-c8af-433b-83e0-bd5137e2c3a1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-task">Task</h2>
<blockquote>
<p>During the daily standup, it was pointed out that the timezone across <code>Nautilus Application Servers</code> in <code>Stratos Datacenter</code> doesn't match the local datacenter's timezone, which is <code>America/Asuncion</code>.</p>
</blockquote>
<h2 id="heading-information">Information</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677294685541/587837be-e9f9-4f69-a1b1-f91dcd6bd99e.png" alt /></p>
<h2 id="heading-implementation">Implementation</h2>
<p>We have three App Servers in which we have to update the timezones to <strong>America/Asuncion</strong> if not already to it. Therefore it's just a matter of accessing each server and using <strong>timedatectl</strong> to update the timezone. Repeat the steps below for each server:</p>
<ol>
<li><p><code>ssh</code> into the App Server with login details above:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677438222621/53936f78-0470-44b6-b673-982a0a6a5a7f.png" alt /></p>
</li>
</ol>
<ol>
<li><p>View the currently set Time Zone to determine if it needs to be updated. Update if not set to <code>America/Asuncion</code>:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677438386205/4597837f-2949-4d52-8f39-6abde2b8727b.png" alt class="image--center mx-auto" /></p>
<p> Looks like it needs to be updated. We can do so using <code>timedatectl set-timezone ZONE</code> command. Reference <code>timedatectl --help</code> for other commands.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677438905527/ac98d503-146e-4911-a3c5-137296f782b5.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<h2 id="heading-learning-takeaways">Learning Takeaways</h2>
<p>A great command to keep in the toolbox. Never had any use for it before, but I just used this to update my timezone to <code>America/Edmonton</code> on my local ubuntu server. Granted, it won't pay any dividends, other than when running the <strong>date</strong> command to give the current time in <strong>MST.</strong></p>
]]></content:encoded></item><item><title><![CDATA[Create User with Non-Interactive Shell]]></title><description><![CDATA[Task

The System admin team of xFusionCorp Industries has installed a backup agent tool on all app servers. As per the tool's requirements, they need to create a user with a non-interactive shell.
Therefore, create a user named javed with a non-inter...]]></description><link>https://cdrani.dev/create-user-with-non-interactive-shell</link><guid isPermaLink="true">https://cdrani.dev/create-user-with-non-interactive-shell</guid><category><![CDATA[KodeKloud]]></category><category><![CDATA[Linux]]></category><category><![CDATA[linux for beginners]]></category><category><![CDATA[sysadmin]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Thu, 23 Feb 2023 03:57:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1677294482162/0874c7fc-b68c-4864-bcb2-dec66b283aa7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-task">Task</h2>
<blockquote>
<p>The System admin team of <code>xFusionCorp Industries</code> has installed a backup agent tool on all app servers. As per the tool's requirements, they need to create a user with a non-interactive shell.</p>
<p>Therefore, create a user named <code>javed</code> with a non-interactive shell on the <code>App Server 2</code></p>
</blockquote>
<h3 id="heading-information">Information</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677294685541/587837be-e9f9-4f69-a1b1-f91dcd6bd99e.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-implementation">Implementation</h2>
<ol>
<li><p>ssh into <code>App Server 2</code> using as <code>steve</code> : <code>ssh steve@172.16.238.11</code> . Enter password when prompted. Currently only users <code>steve</code> and <code>ansible</code> exist in this server</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677295895067/997a5cc1-531c-46f0-9d32-adf2222c6c46.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Create user <code>javed</code> with non-interactive shell: <code>sudo adduser -s /sbin/nologin javed</code> . <code>-s</code> is a flag to set the shell for a user. Below I authenticate as <code>root</code> so the <code>sudo</code> prefixes are unnecessary.</p>
</li>
</ol>
<blockquote>
<p><strong><mark>nologin</mark></strong> displays a message that an account is not available and closes the connection and returns non-zero. It is intended as a replacement shell field to deny login access to an account.</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677296174159/d4507bc4-38f7-421d-a7b3-d8e8800a460b.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-learnings">Learnings</h2>
<ol>
<li><p>Creating a user with a non-default $SHELL. Th <code>nonlogin</code> shell is non-interactive and used for disabling a user account in the case of suspicious activity or upon user workplace/contract termination. The shell can also be updated using <code>usermod -s</code> .</p>
</li>
<li><p>A custom message can be set for any attempts to log in as the disabled user account by editing/creating a <code>/etc/nologin.txt</code> file.</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Linux File Permissions]]></title><description><![CDATA[Task

There are new requirements to automate a backup process that was performed manually by the xFusionCorp Industries system admins team earlier. To automate this task, the team has developed a new bash script xfusioncorp.sh. They have already copi...]]></description><link>https://cdrani.dev/linux-file-permissions</link><guid isPermaLink="true">https://cdrani.dev/linux-file-permissions</guid><category><![CDATA[KodeKloud]]></category><category><![CDATA[Linux]]></category><category><![CDATA[sysadmin]]></category><category><![CDATA[linux for beginners]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Tue, 21 Feb 2023 18:11:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1677294397932/1c6ae42c-9e00-4443-9aa2-38dea27880eb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-task">Task</h2>
<blockquote>
<p>There are new requirements to automate a backup process that was performed manually by the <code>xFusionCorp Industries</code> system admins team earlier. To automate this task, the team has developed a new bash script <a target="_blank" href="http://xfusioncorp.sh"><code>xfusioncorp.sh</code></a>. They have already copied the script on all required servers, however they did not make it executable on one the app server i.e <code>App Server 3</code> in <code>Stratos Datacenter</code>.</p>
<p>Please give executable permissions to <code>/tmp/xfusioncorp.sh</code> script on <code>App Server 3</code>. Also make sure every user can execute it.</p>
</blockquote>
<h2 id="heading-information">Information</h2>
<p>We have access to credentials servers from the <a target="_blank" href="https://kodekloudhub.github.io/kodekloud-engineer/docs/projects/nautilus#infrastructure-details">infrastructure details</a>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677001104705/14f3570e-1eed-475f-916c-dce1014c04c5.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-implementation">Implementation</h2>
<ol>
<li>Starting from a <strong>jump_host</strong> we ssh into <strong>App Server 3</strong> as the user <strong>banner</strong>:</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677001291105/a988a430-e8ee-4e52-8b3e-cd412f7872fe.png" alt class="image--center mx-auto" /></p>
<p>In the <code>/tmp</code> directory we can view the current permissions and ownership of the <code>/tmp/xfusioncorp.sh</code> file. We can see that it belongs to <code>root</code> user and it has not been made executable:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677001447022/09207122-aa29-4aca-bbe8-29626e662ea6.png" alt class="image--center mx-auto" /></p>
<ol>
<li>Make <code>xfusioncorp.sh</code> <strong>executable</strong> for all everyone (users, groups, other):</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677001715766/0e742867-ed9b-4837-a421-6c9ebf556d38.png" alt class="image--center mx-auto" /></p>
<ol>
<li>Make <code>xfusioncorp.sh</code> <strong>readable.</strong> A script can't be executed if it can't be read - at least for Bourne (again) shells I believe? Let's give <code>read</code> access to everyone and see the contents of the script.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677002625010/936c3c28-eb23-4c75-9ce5-46db26e327fc.png" alt class="image--center mx-auto" /></p>
<ol>
<li>Execute the script:</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677002732962/a5d1e518-a4b8-471f-87e2-aea5450ada32.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-learning-takeaways">Learning Takeaways</h2>
<ol>
<li>Does a script file need to be <strong><em>readable</em></strong>? I guess all the script files I have ever written or run have always had <code>read</code> permissions set. I guess since a script is a text file it would require this permission, while binary files would not.</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Load Balancing Types & Methods for Nginx & Apache]]></title><description><![CDATA[Load balancing, as explored in a previous project, is a technique used to distribute incoming network traffic across multiple servers to improve performance, scalability, and availability. Nginx and Apache are popular web servers that support load ba...]]></description><link>https://cdrani.dev/load-balancing-types-methods-for-nginx-apache</link><guid isPermaLink="true">https://cdrani.dev/load-balancing-types-methods-for-nginx-apache</guid><category><![CDATA[Load Balancing]]></category><category><![CDATA[apache]]></category><category><![CDATA[nginx]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Thu, 09 Feb 2023 16:13:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Cf0UvX2T87o/upload/03117cae313dd6ecc2b31d6a7e6cc6d7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Load balancing, as explored in a previous project, is a technique used to distribute incoming network traffic across multiple servers to improve performance, scalability, and availability. Nginx and Apache are popular web servers that support load balancing.</p>
<h2 id="heading-types-of-load-balancing"><strong>Types of Load Balancing</strong></h2>
<p>There are three main types of load balancing:</p>
<ol>
<li><p>Round-robin: Incoming requests are distributed to each server in the pool in a circular fashion.</p>
</li>
<li><p>Least connections: Incoming requests are distributed to the server with the least number of active connections.</p>
</li>
<li><p>IP hash: The client's IP address is used to determine which server in the pool should handle the request.</p>
</li>
</ol>
<h2 id="heading-load-balancing-methods-for-nginx-and-apache"><strong>Load Balancing Methods for Nginx and Apache</strong></h2>
<h3 id="heading-nginx-load-balancing-methods"><strong>Nginx Load Balancing Methods</strong></h3>
<ol>
<li><p>Round-robin: Default method in Nginx.</p>
</li>
<li><p>Least connections: Enabled with the <code>least_conn</code> directive.</p>
</li>
<li><p>IP hash: Enabled with the <code>ip_hash</code> directive.</p>
</li>
</ol>
<h3 id="heading-apache-load-balancing-methods"><strong>Apache Load Balancing Methods</strong></h3>
<p>Apache supports various load balancing methods through the mod_proxy_balancer module, including:</p>
<ol>
<li><p><strong>Round-robin:</strong> Incoming requests are distributed to each server in the pool in a circular fashion.</p>
</li>
<li><p><strong>Least connections:</strong> Incoming requests are distributed to the server with the least number of active connections.</p>
</li>
<li><p><strong>Session-based:</strong> Incoming requests are distributed based on session information, ensuring that requests from the same session are sent to the same server.</p>
</li>
<li><p><strong>URL hash:</strong> Incoming requests are distributed based on the URL requested, ensuring that requests for the same URL are sent to the same server.</p>
</li>
</ol>
<p>In addition to load balancing methods, a load factor is an important consideration for efficient load balancing. Load factor refers to the server's ability to handle traffic based on its resources, such as CPU, memory, and disk I/O.</p>
<p>Load factor can be measured in various ways, such as the number of connections or requests per second, the CPU or memory usage, or the network throughput. Load balancing algorithms can take into account the load factor of each server and distribute traffic accordingly to ensure that the server with the least load factor gets the incoming traffic.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>By choosing the right load balancing method and considering the load factor of each server, web applications can handle high traffic loads and provide a smooth user experience.</p>
]]></content:encoded></item><item><title><![CDATA[DevOps PBL: DevOps Tooling Site]]></title><description><![CDATA[We want to implement a tooling website solution that makes access to DevOps tools within the corporate infrastructure easily accessible.
The tools we want our team to be able to use are well-known and widely used by multiple DevOps teams, so we will ...]]></description><link>https://cdrani.dev/tooling-site</link><guid isPermaLink="true">https://cdrani.dev/tooling-site</guid><category><![CDATA[pbl]]></category><category><![CDATA[DevOps Journey]]></category><category><![CDATA[Nfs]]></category><category><![CDATA[LVM]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Tue, 07 Feb 2023 14:46:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1676473817010/b2942d47-596d-420a-9798-7d329fa73b27.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We want to implement a tooling website solution that makes access to DevOps tools within the corporate infrastructure easily accessible.</p>
<p>The tools we want our team to be able to use are well-known and widely used by multiple DevOps teams, so we will introduce a single DevOps Tooling Solution that will consist of these tools:</p>
<ol>
<li><p><a target="_blank" href="https://www.jenkins.io/"><strong>Jenkins</strong></a> <strong>– free and open source automation server used to build</strong> <a target="_blank" href="https://en.wikipedia.org/wiki/CI/CD"><strong>CI/CD</strong></a> <strong>pipelines.</strong></p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/"><strong>Kubernetes</strong></a> <strong>– open-source container-orchestration system for automating computer application deployment, scaling, and management.</strong></p>
</li>
<li><p><a target="_blank" href="https://jfrog.com/artifactory/"><strong>Jfrog Artifactory</strong></a> <strong>– Universal Repository Manager supporting all major packaging formats, build tools and CI servers. Artifactory.</strong></p>
</li>
<li><p><strong>Rancher – an open-source software platform that enables organizations to run and manage</strong> <a target="_blank" href="https://en.wikipedia.org/wiki/Docker_(software)"><strong>Docker</strong></a> <strong>and Kubernetes in production.</strong></p>
</li>
<li><p><a target="_blank" href="https://grafana.com/"><strong>Grafana</strong></a> <strong>– a multi-platform open-source analytics and interactive visualization web application.</strong></p>
</li>
<li><p><a target="_blank" href="https://prometheus.io/"><strong>Prometheus</strong></a> <strong>– An open-source monitoring system with a dimensional data model, flexible query language, efficient time series database and modern alerting approach.</strong></p>
</li>
<li><p><a target="_blank" href="https://www.elastic.co/kibana"><strong>Kibana</strong></a> <strong>– Kibana is a free and open user interface that lets you visualize your</strong> <a target="_blank" href="https://www.elastic.co/elasticsearch/"><strong>Elasticsearch</strong></a> <strong>data and navigate the</strong> <a target="_blank" href="https://www.elastic.co/elastic-stack"><strong>Elastic Stack</strong></a><strong>.</strong></p>
</li>
</ol>
<p>In this project you will implement a solution that consists of the following components:</p>
<ol>
<li><p><strong>Infrastructure</strong>: AWS</p>
</li>
<li><p><strong>Webserver Linux</strong>: RHEL 9</p>
</li>
<li><p><strong>Database Server</strong>: Ubuntu 22.04 + MySQL</p>
</li>
<li><p><strong>Storage Server</strong>: RHEL 9 + NFS Server</p>
</li>
<li><p><strong>Programming Language</strong>: PHP</p>
</li>
<li><p><strong>Code Repository</strong>: GitHub</p>
</li>
</ol>
<p>In the diagram below you can see a common pattern where several stateless Web Servers share a common database and also access the same files using <a target="_blank" href="https://en.wikipedia.org/wiki/Network_File_System">Network File System (NFS)</a> as shared file storage. Even though the NFS server might be located on completely separate hardware for Web Servers it will resemble a local file system from where they can serve the same files.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676473255925/c534a2d5-277d-4401-97f0-c1b667f7d2eb.png" alt class="image--center mx-auto" /></p>
<p>It is important to know what storage solution is suitable for what use cases, thus we need to ask the following questions: what data will be stored, in what format, how this data will be accessed, by whom, from where, how frequently, etc. Based on this we will be able to choose the right storage system for your solution.</p>
<h2 id="heading-step-1-prepare-nfs-server">Step 1: Prepare NFS Server</h2>
<h3 id="heading-setup-lvm-on-rhel-9-os">Setup LVM on RHEL 9 OS</h3>
<p>This setup will be very similar to the last project's LVM setup. To avoid extra work on my end of rewriting the same steps, I instead will embed the setup from the last project as a gist here. Notable changes we want to account for are:</p>
<ol>
<li><p>Formatting the disks as <code>xfs</code> instead of <code>ext4</code></p>
</li>
<li><p>The volume names will be <code>opt-lv</code>, <code>apps-lv</code>, and <code>logs-lv</code></p>
</li>
<li><p>Mount points will be on <code>/mnt</code> directory for the logical volumes as follows:</p>
<ol>
<li><p>Mount <code>lv-apps</code> on <code>/mnt/apps</code> – To be used by web servers</p>
</li>
<li><p>Mount <code>lv-logs</code> on <code>/mnt/logs</code> – To be used by web server logs</p>
</li>
<li><p>Mount <code>lv-opt</code> on <code>/mnt/opt</code> – To be used by the Jenkins server in Next Project</p>
</li>
</ol>
</li>
</ol>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="7a69cf05152c2891ee33fa2c7b8a2902"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/cdrani/7a69cf05152c2891ee33fa2c7b8a2902" class="embed-card">https://gist.github.com/cdrani/7a69cf05152c2891ee33fa2c7b8a2902</a></div><p> </p>
<p>Here's our setup of the logical volumes:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675550992268/e5db7d02-6d3f-4eab-a00f-29a0d7e13beb.png" alt class="image--center mx-auto" /></p>
<p>Mount the logical volumes:</p>
<pre><code class="lang-bash">sudo mkdir -p /mnt/apps /mnt/logs /mnt/opt

<span class="hljs-comment"># syncing logs and mounting</span>
sudo rsync -av /var/<span class="hljs-built_in">log</span> /mnt/logs
sudo mount /dev/nfsdata-vg/logs-lv /mnt/logs
sudo rsync -av /mnt/logs/ /var/<span class="hljs-built_in">log</span>

<span class="hljs-comment"># mount apps-lv &amp; opt-lv</span>
sudo mount /dev/nfsdata-vg/apps-lv /mnt/app
sudo mount /dev/nfsdata-vg/opt-lv /mnt/opt
</code></pre>
<p>We can view our newly created mounts. We can see the <strong>target</strong> (<code>/mnt/logs</code>), <strong>source</strong> (<code>/dev/mapper/nfsdata-vg-logs--lv</code>), and <strong>fstype (</strong><code>xfs</code><strong>).</strong></p>
<pre><code class="lang-bash">sudo findmnt | grep <span class="hljs-string">'/mnt'</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675552974978/dcadf7c2-cdce-41cd-b6b7-8309102f6c9b.png" alt class="image--center mx-auto" /></p>
<p>Install the NFS server, configure it to start on reboot and make sure it is up and running:</p>
<pre><code class="lang-bash">sudo yum -y update
sudo yum -y install nfs-utils
sudo systemctl <span class="hljs-built_in">enable</span> nfs-server.service
sudo systemctl start nfs-server.service
sudo systemctl status nfs-server.service
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675553473643/a929709e-e388-4dcb-97cb-62f79607a596.png" alt class="image--center mx-auto" /></p>
<p>Set up permissions that will allow our Web servers to read, write and execute files on NFS.</p>
<pre><code class="lang-bash">sudo chown -R nobody: /mnt/apps
sudo chown -R nobody: /mnt/logs
sudo chown -R nobody: /mnt/opt

sudo chmod -R 777 /mnt/apps
sudo chmod -R 777 /mnt/logs
sudo chmod -R 777 /mnt/opt
</code></pre>
<p>Furthermore, we want our webservers (not created yet) to be able to access our mounts as clients. For simplicity, the webservers will all be installed within the same subnet.</p>
<p>We need to retrieve out <code>subnet cidr</code> value to configure access to our NFS for our web servers. The subnet value can be found within the instance <code>Networking</code> tab and following the <code>subnet</code> link:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675563345708/105b518a-444c-4616-aa49-e45874b4ac9c.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675563442339/26c08ab8-e4a3-4cec-bbd2-a6efd210c647.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-bash">sudo vi /etc/exports

<span class="hljs-comment"># /etc/exports</span>
/mnt/apps 172.31.16.0/20(rw,sync,no_all_squash,no_root_squash)
/mnt/logs 172.31.16.0/20(rw,sync,no_all_squash,no_root_squash)
/mnt/opt  172.31.16.0/20(rw,sync,no_all_squash,no_root_squash)

<span class="hljs-comment"># exit vi</span>
sudo exportfs -arv
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675563927023/31d9dfdf-b53b-4bf2-a1f3-5e5a10b7ed7f.png" alt class="image--center mx-auto" /></p>
<p>Use <code>rcpinfo -p | grep nfs</code> to check the port used by NFS and include it as a rule in the Security Groups.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675564214367/614b3a90-4373-4773-9ed7-498d67fd8555.png" alt class="image--center mx-auto" /></p>
<p>From the above, we need to open port <code>2049</code>. Additionally, to allow access to our NFS server from clients, we also need to open <strong>TCP 111</strong>, <strong>UDP 111</strong>, and <strong>UDP 2049.</strong> With all these related rules to our NFS, I created a specific SG for it. The <code>source</code> field is our <code>subnet cidr</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675572200831/8098bc48-6640-4b80-97ed-28d9af0b2473.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-configure-the-database-server">Configure the Database Server</h2>
<p>As we have done multiple times in previous projects, we want to install MySQL, create a database and user, and grant the user access to the database from the webservers <code>subnet cidr</code>:</p>
<pre><code class="lang-bash">sudo yum -y update
sudo yum -y install mysql-server

<span class="hljs-comment"># start the mysql services</span>
sudo systemctl <span class="hljs-built_in">enable</span> mysqld
sudo systemctl restart mysqld
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675568052266/59b0f076-9a62-4c03-befc-6d1d24257869.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-prepare-the-web-servers">Prepare the Web Servers</h2>
<p>Our Web Servers need to be able to serve the same content from shared storage solutions, in our case – NFS Server and MySQL database. We have already seen how to access a MySQL server from a client. For storing shared files that our Web Servers will use – we will utilize <strong>NFS</strong> and mount previously created Logical Volume <code>apps-lv</code> to the folder where Apache stores its files to be served to the users (<code>/var/www</code>).</p>
<p>This approach will make our Web Servers <code>stateless</code>, which means we will be able to add new ones or remove them whenever we need, and the integrity of the data (in the database and on NFS) will be preserved.</p>
<p>In the next steps, we will do the following 3 times:</p>
<ul>
<li><p>Launch an RHEL 9 EC2 instance</p>
</li>
<li><p>Configure the NFS client (this step must be done on all three servers)</p>
</li>
<li><p>Deploy a Tooling application to our Web Servers into a shared NFS folder</p>
</li>
<li><p>Configure the Web Servers to work with a single MySQL database</p>
</li>
</ul>
<p>We need to first install our NFS client on our Web Server instances:</p>
<pre><code class="lang-bash">sudo yum -y install nfs-utils nfs4-acl-tools
</code></pre>
<p>Then mount <code>/var/www/</code> and target the NFS server's export for `apps`. We should see the NFS mounted on our web server:</p>
<pre><code class="lang-bash">sudo mkdir /var/www
sudo mount -t nfs -o rw,nosuid 172.31.25.225:/mnt/apps /var/www
sudo mount -t nfs -o rw,nosuid 172.31.25.225:/mnt/logs /var/<span class="hljs-built_in">log</span>/httpd
df -h
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675584349453/9d63b5de-4b22-451b-b57e-43a090ad5c0e.png" alt class="image--center mx-auto" /></p>
<p>Additionally, we can persist the above changes by adding them to our <code>/etc/fstab</code> :</p>
<pre><code class="lang-bash"><span class="hljs-comment"># /etc/fstab</span>
172.31.25.225:/mnt/apps /var/www nfs defaults 0 0
172.31.25.225:/mnt/logs /var/<span class="hljs-built_in">log</span>/httpd nfs defaults 0 0
</code></pre>
<h3 id="heading-install-remis-repositoryhttpwwwservermomorghow-to-enable-remi-repo-on-centos-7-6-and-52790-apache-and-php">Install <a target="_blank" href="http://www.servermom.org/how-to-enable-remi-repo-on-centos-7-6-and-5/2790/">Remi’s repository</a>, Apache and PHP</h3>
<pre><code class="lang-bash">sudo yum -y install httpd

sudo dnf -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm

sudo dnf -y install dnf-utils http://rpms.remirepo.net/enterprise/remi-release-9.rpm

sudo dnf -y module reset php

sudo dnf -y module <span class="hljs-built_in">enable</span> php:remi-8.1

sudo dnf -y install php php-opcache php-gd php-curl php-mysqlnd

sudo systemctl <span class="hljs-built_in">enable</span> php-fpm

sudo systemctl start php-fpm

sudo setsebool -P httpd_execmem 1
</code></pre>
<p>We can ascertain that our NFS is mounted properly on our web server by verifying that both <code>/var/www</code> in our Web Server(s) and <code>/mnt/apps</code> in our NFS have the same Apache files and directories. Apply the same verification for our logs - <code>/var/log</code> for our Web Servers and <code>/mnt/logs</code> for our NFS.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675573517765/9bb16be3-017b-462a-9115-7a97f8f271d3.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675573542541/6f3e6028-4ae1-4af5-90b7-70c8d61a940d.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675584251920/caeda0e3-64c0-43c8-8e60-390bc24b1f1f.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675584291019/7c0e9a88-bcfa-4da5-b1f6-b6f95cfa3258.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>Fork the tooling source code from <a target="_blank" href="http://Darey.io">Darey.io</a> <a target="_blank" href="https://github.com/darey-io/tooling.git">Github Account</a> to your Github account.</p>
</li>
<li><p>Deploy the tooling website’s code to the Webserver. Ensure that the <strong>html</strong> folder from the repository is deployed to <code>/var/www/html</code></p>
</li>
</ol>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> /var/www/html
sudo git <span class="hljs-built_in">clone</span> https://github.com/cdrani/tooling.git
sudo mv tooling/. .
sudo mv -R html/. .
sudo rm -R html
</code></pre>
<p>Again our web servers should have the files in <code>/var/www/html</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675585478662/045a0494-a2a0-465e-bc83-22f3e20cfd33.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675585510807/5945e514-303c-4aa0-879b-801a4ecc6bb1.png" alt class="image--center mx-auto" /></p>
<p>We need to update permissions on <code>/var/www/html</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675586165636/6cc5da7c-77d8-4a6c-a6e4-7e323974e93c.png" alt class="image--center mx-auto" /></p>
<p>Update the website’s configuration to connect to the database (in <code>/var/www/html/functions.php</code> file). We will make use of the <code>webaccess</code> user with a password of <code>Super01!</code> we created earlier which has access to the `tooling` database:</p>
<pre><code class="lang-bash"><span class="hljs-variable">$db</span> = mysqli_connect(<span class="hljs-string">'&lt;DB Server Private IP Address&gt;'</span>, <span class="hljs-string">'&lt;MySQL username&gt;'</span>, <span class="hljs-string">'&lt;MySQL password&gt;'</span>, <span class="hljs-string">'&lt;Database&gt;'</span>);
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675626157939/5a0c2c3f-027d-4082-8814-d76f2fb74199.png" alt class="image--center mx-auto" /></p>
<p>Now we need to set up a users table in our tooling database. Fortunately, we already have a pre-configured <code>/var/www/html/tooling-db.sql</code> file that will create a <code>users</code> table for us. All we have to do is import it into our <code>tooling</code> database. We can import <code>tooling-db.sql</code> script to our database with this command <code>mysql -h &lt;databse-private-ip&gt; -u &lt;db-username&gt; -p &lt;db-pasword&gt; &lt; tooling-db.sql</code>.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># mysql -h &lt;databse-private-ip&gt; -u &lt;db-username&gt; -p &lt;db-pasword&gt; &lt; tooling-db.sql</span>
mysql -u webaccess -h 172.31.18.126 -p tooling &lt; /var/www/html/tooling-db.sql
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675625328790/17c6f884-a7e6-4239-8e1a-420d7d5d288a.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-bash">sudo sed -i <span class="hljs-string">'s/SELINUX=enforcing/SELINUX=disabled/'</span> /etc/sysconfig/selinux
sudo setenforce 0
sudo systemctl restart httpd
</code></pre>
<p>Visiting either of our web servers (<code>http://&lt;Public IP Address&gt;/index.php</code>) should display the following login page and home page upon entering our credentials:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675626219763/fce5b45f-b7bf-4423-9e6b-5e74f4932885.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675630363386/98f5235d-ced3-4779-9185-8cf13b33a3fb.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-learning-outcomes">Learning Outcomes</h2>
<ol>
<li><p>Configure an NFS client.</p>
</li>
<li><p>Deploy a Tooling application to our Web Servers into a shared NFS folder.</p>
</li>
<li><p>Configure the Web Servers to work with a single MySQL database.</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Devops Apache Load Balancer]]></title><description><![CDATA[You might have noticed the implications of having multiple web servers from the previous project - each web server has 3 different IP addresses or DNS names. This issue will be further exacerbated if we need to add more web servers. So why do we both...]]></description><link>https://cdrani.dev/apache-load-balancer</link><guid isPermaLink="true">https://cdrani.dev/apache-load-balancer</guid><category><![CDATA[pbl]]></category><category><![CDATA[DevOps Journey]]></category><category><![CDATA[apache]]></category><category><![CDATA[Load Balancing]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Mon, 06 Feb 2023 05:12:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/DX5r6BNoWVE/upload/2a726c08686fd17f0d68e2aa503dd87f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You might have noticed the implications of having multiple web servers from the previous project - each web server has 3 different IP addresses or DNS names. This issue will be further exacerbated if we need to add more web servers. So why do we bother with this multiple servers structure, considering they are essentially clones of each other?</p>
<p>The rationale for this level of redundancy is a scalability issue. Many times a post/link receives an onslaught of upvotes on a site like <a target="_blank" href="https://news.ycombinator.com/">HackerNews</a> causing thousands to millions of visitors to the link and causing the page to crash as the server was not optimized to handle the level of traffic, thus causing slow page loads or killing the server altogether. This necessitated the need for multiple servers to handle these volumes of loads by distributing these requests across their servers. Sites like <strong>Google</strong> and <strong>Reddit these types</strong> of issues to serve millions of users every day.</p>
<p><strong>Scalability</strong> is the property of a system to be able to handle a growing load by increasing resources. There are two types of scalability approaches to consider:</p>
<ol>
<li><p><strong>vertical scaling:</strong> Increasing the CPU and ram on our current server or configuring a more powerful server as a replacement. The limitation is that there's only so much CPU and ram that can be installed on a server.</p>
</li>
<li><p><strong>horizontal scaling</strong>: Spreading the load across multiple servers. This is the more common approach as it can be applied seamlessly and almost infinitely. Additionally, this can also be automated to spin up (<strong>scale out</strong>) or spin down (<strong>scale in</strong>) servers based on CPU and Memory load monitored metrics.</p>
</li>
</ol>
<p>Currently, we have 2 web servers set up from the previous project. For a user/client to access them they would need to either or both of the IP addresses. This is not too bad with our limited number of servers, but this is not feasible for sites like google where they have <strong><em>millions</em></strong> of servers.</p>
<p>We can alleviate clients/users from this bad UX by providing only a single point of access to reach our servers. A <strong>Load Balancer (LB)</strong> solves this by distributing clients' requests among multiple servers and ensuring that the load is shared in an optimized manner.</p>
<p>Let's update our current solution architecture with an LB sandwiched between our Client and web servers. This will allow our users to access our site using a single URL/IP Address. We will make use of only 2 web servers, but the following approach will not change regardless of the number of web servers.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675707395046/6a922056-331f-4e1c-bb4b-a71808a2f61b.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>We already have the following infrastructure setup from the previous project:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675707672130/f998b395-cfcc-4e09-b9df-9b8edb05752f.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-configure-apache-as-a-load-balancer">Configure Apache As A Load Balancer</h2>
<p>For our LB we will set up an L7 Application LB with Apache. Therefore, let's create an Ubuntu EC2 instance and open port <strong>80</strong> in the SG.</p>
<ul>
<li>insert image here</li>
</ul>
<h3 id="heading-install-apache">Install Apache</h3>
<pre><code class="lang-bash">sudo apt -y update
sudo apt -y install apache2
sudo apt -y install libxml2-dev
</code></pre>
<h3 id="heading-enable-apache-modules">Enable Apache modules:</h3>
<pre><code class="lang-bash">sudo a2enmod rewrite proxy proxy_balancer proxy_http headers lbmethod_bytraffic

<span class="hljs-comment"># restart apache to activate enabled modules</span>
sudo systemctl restart apache2

sudo systemctl status apache2
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675709457134/514b7cbb-735c-49ae-b591-0a095ee16eb6.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-configure-load-balancing">Configure Load Balancing</h3>
<p>Our LB configuration is done in the <code>/etc/apache/sites-available/000-default.conf</code> file. Here we can define the Web Servers and the balancing method. There are several choices for load balancing methods: <a target="_blank" href="https://httpd.apache.org/docs/2.4/mod/mod_lbmethod_bytraffic.html"><code>bytraffic</code></a>, <a target="_blank" href="https://httpd.apache.org/docs/2.4/mod/mod_lbmethod_bybusyness.html"><code>bybusyness</code></a><strong>,</strong> <a target="_blank" href="https://httpd.apache.org/docs/2.4/mod/mod_lbmethod_byrequests.html"><code>byrequests</code></a><strong>,</strong> and by <a target="_blank" href="https://httpd.apache.org/docs/2.4/mod/mod_lbmethod_heartbeat.html"><code>heartbeat</code></a> . We can control in which proportion the traffic must be distributed by <code>loadfactor</code> parameter. Setting the same <code>loadfactor</code> to both servers will evenly distribute the traffic. I made the following changes to the file with the Private IP address of my Web Servers:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675710694661/ee1f6279-481d-4528-94b4-5afea9ca8f6c.png" alt class="image--center mx-auto" /></p>
<p>After the above changes to our config file, we need to restart our Apache server. Following that, we should be able to now visit our site using the LB's Public IP Address: `<a target="_blank" href="http://54.185.27.126/login.php">http://54.185.27.126/index.php</a>`. We should be able to log in and see the same admin page from the previous project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675711251969/338394fa-abf7-4d2a-bfcf-d1b2e474d88c.png" alt class="image--center mx-auto" /></p>
<p>We can view our Apache logs by running the following in both Web Server instances:</p>
<pre><code class="lang-bash">sudo tail -f /var/<span class="hljs-built_in">log</span>/httpd/access_log
</code></pre>
<p>We can see below that the logs are the same - the same requests at the same times. This is due to them both sharing the same mount point <code>/var/log/httpd</code> on our NFS server. We can't differentiate between which Web Server is being selected by our LB to serve.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675711699286/316b1cf4-5948-4230-9158-e58891d55212.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675711735980/d5bacf23-b0e7-4ec6-b898-d8dbf5ef8f70.png" alt class="image--center mx-auto" /></p>
<p>To resolve this we need to unmount our Web Servers from the NFS Server so that the Web Servers have their separate logs. Let's do the following for both Web Servers:</p>
<pre><code class="lang-bash">sudo systemctl stop httpd
sudo df -h
sudo umount /var/<span class="hljs-built_in">log</span>/httpd
sudo df -h
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675714742959/fa5a5814-c96e-4be5-a8bd-ebd2a952e578.png" alt class="image--center mx-auto" /></p>
<p>Additionally, we need to prevent the <code>/var/log/http</code> directory from being mounted on boot as we currently have it configured in the <code>/etc/fstab</code> file. Let's comment it out on both servers:</p>
<pre><code class="lang-bash"><span class="hljs-comment">#/etc/fstab</span>

172.31.25.225:/mnt/apps /var/www nfs defaults 0 0
<span class="hljs-comment"># 172.31.25.225:/mnt/logs /var/log/httpd nfs defaults 0 0</span>
</code></pre>
<p>Then reload our mounts so these changes apply on boot as well and restart our Apache server:</p>
<pre><code class="lang-bash">sudo mount -a
sudo systemctl start httpd
</code></pre>
<p>Now visiting our site via our LB IP address we can view Web Server is running our requests. Below we see that our requests are being distributed equally between our two Web Servers on every page reload:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675717647606/f3ae9d67-58e0-4a72-a082-1108658684f0.gif" alt class="image--center mx-auto" /></p>
<p>This is our final setup now:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675718156670/a33894dc-9796-4dc1-bc82-fba10e8ea7f3.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-learning-outcomes">Learning Outcomes:</h2>
<ol>
<li><p>What is scalability? What are vertical and horizontal scaling?</p>
</li>
<li><p>What is Load Balancing? What is the purpose?</p>
</li>
<li><p>How to set up an Apache LB (install + modules + configuration with Web Servers)</p>
</li>
<li><p>View Web Servers logs</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[DevOps PBL: WordPress Web Solution]]></title><description><![CDATA[For this project, we want to prepare storage infrastructure on two Linux servers and implement a basic web solution using WordPress.

Configure storage subsystem for Web and Database servers based on Linux OS. The focus of this part is to give you pr...]]></description><link>https://cdrani.dev/wordpress-web-solution</link><guid isPermaLink="true">https://cdrani.dev/wordpress-web-solution</guid><category><![CDATA[pbl]]></category><category><![CDATA[DevOps Journey]]></category><category><![CDATA[WordPress]]></category><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Fri, 03 Feb 2023 08:37:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1675413386326/3ecedff6-469b-4a2f-ad49-25a3fb81c784.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For this project, we want to prepare storage infrastructure on two Linux servers and implement a basic web solution using <a target="_blank" href="https://en.wikipedia.org/wiki/WordPress">WordPress</a>.</p>
<ol>
<li><p>Configure storage subsystem for Web and Database servers based on Linux OS. The focus of this part is to give you practical experience in working with disks, partitions and volumes in Linux.</p>
</li>
<li><p>Install WordPress and connect it to a remote MySQL database server. This part of the project will solidify your skills in deploying Web and DB tiers of Web solutions.</p>
</li>
</ol>
<p>Three-tier Architecture is a client-server software architecture pattern that comprises 3 separate layers.</p>
<ol>
<li><p><strong>Presentation Layer</strong> (PL): This is the user interface such as the client-server or browser on your laptop.</p>
</li>
<li><p><strong>Business Layer</strong> (BL): This is the backend program that implements business logic. It can be an Application and/or Webserver.</p>
</li>
<li><p><strong>Data Access or Management Layer</strong> (DAL): This is the layer for computer data storage and data access. <a target="_blank" href="https://www.computerhope.com/jargon/d/database-server.htm">Database Server</a> or File System Server such as <a target="_blank" href="https://titanftp.com/2018/09/11/what-is-an-ftp-server/">FTP server</a>, or <a target="_blank" href="https://searchenterprisedesktop.techtarget.com/definition/Network-File-System">NFS Server</a>.</p>
</li>
</ol>
<h5 id="heading-our-3-tier-setup">Our 3-Tier Setup</h5>
<ol>
<li><p>A Laptop or PC to serve as a client</p>
</li>
<li><p>An EC2 Linux Server as a web server (This is where we will install WordPress)</p>
</li>
<li><p>An EC2 Linux server as a database (DB) server</p>
</li>
</ol>
<h2 id="heading-step-1-setup-web-server">STEP 1: Setup Web Server</h2>
<p>We will use an RHEL9 instance as our server. Additionally, we need to create and attach three 10GB volumes to our instance.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675399339736/299183cc-052f-4d6f-904d-fe5bebec21a2.png" alt class="image--center mx-auto" /></p>
<p>The volume must be in the same Availability Zone as the instance:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675399297199/f9a02f16-843f-4621-8ce2-17d1b1b86b55.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675399544387/8da6a901-56c1-49d2-92a1-3dbd7a82d6b9.png" alt class="image--center mx-auto" /></p>
<p>Make sure to select the correct instance for which to attach the volumes:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675399590343/10e40461-bb86-4550-92ae-25eb4f5c5623.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-volume-configuration">Volume Configuration</h3>
<p>Use <a target="_blank" href="https://man7.org/linux/man-pages/man8/lsblk.8.html"><code>lsblk</code></a> command to inspect what block devices are attached to the server. The three newly created block devices will likely have names like <code>xvdf</code>, <code>xvdh</code>, and <code>xvdg</code> .</p>
<pre><code class="lang-bash">lsblk
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675399946562/c655be40-21f0-45a1-a5c3-4962ddf83bf1.png" alt class="image--center mx-auto" /></p>
<p>We can use <code>df -h</code> to view all mounts and free space on our instance</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675400057429/82e8fadc-0856-4ba4-be74-25f62c413e1d.png" alt class="image--center mx-auto" /></p>
<p>Use the <code>gdisk</code> utility to create a single partition on each of the 3 disks. The utility is interactive with some command options. We only need to make use of the <code>n</code> to create a new partition and <code>w</code> to save the new partition to the disk and exit.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># sudo gdisk &lt;disk&gt;</span>
sudo gdisk /dev/xvdf
sudo gdisk /dev/xvdg
sudo gdisk /dev/xvdh
</code></pre>
<p>Here's an example of partitioning one of the disks using the default (pressing ENTER/RETURN key):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675400350275/c9376521-f703-4b4d-ae40-c7081d6975b4.png" alt class="image--center mx-auto" /></p>
<p>We can now see the partitions we created in each disk now:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675400671119/46a7a172-6734-4b84-8835-ad8f3b83c9e7.png" alt class="image--center mx-auto" /></p>
<p>Run <code>sudo lvmdiskscan</code> command to check for available partitions and the type of volumes</p>
<pre><code class="lang-bash">sudo yum install -y lvm2
sudo lvmdiskscan
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675401222696/f8508100-b68a-4ebb-965b-43eb490dc1db.png" alt class="image--center mx-auto" /></p>
<p>Use <a target="_blank" href="https://linux.die.net/man/8/pvcreate"><code>pvcreate</code></a> utility to mark each of the 3 disks as physical volumes (PVs) to be used by LVM.</p>
<pre><code class="lang-bash">sudo pvcreate /dev/xvdf1
sudo pvcreate /dev/xvdg1
sudo pvcreate /dev/xvdh1
sudo pvs
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675401517236/1be10449-0dbc-4592-a12c-2b38bbd6772e.png" alt class="image--center mx-auto" /></p>
<p>Use <a target="_blank" href="https://linux.die.net/man/8/vgcreate"><code>vgcreate</code></a> utility to add all 3 PVs to a volume group (VG). Name the VG <strong>webdata-vg.</strong> We can verify the creation of the VG using <code>sudo vgs</code> :</p>
<pre><code class="lang-bash">sudo vgcreate webdata-vg /dev/xvdf1 /dev/xvdg1 /dev/xvdh1
sudo vgs
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675401622500/cca8378c-5263-4ee3-9b59-03062c9ec53f.png" alt class="image--center mx-auto" /></p>
<p>Use <a target="_blank" href="https://linux.die.net/man/8/lvcreate"><code>lvcreate</code></a> utility to create 2 logical volumes. <strong>apps-lv</strong> (<strong><em>Use half of the PV size</em></strong>), and <strong>logs-lv</strong> <strong><em>Use the remaining space of the PV size</em></strong>. <strong>NOTE</strong>: <code>apps-lv</code> will be used to store data for the Website while <code>logs-lv</code> will be used to store data for logs.</p>
<pre><code class="lang-bash">sudo lvcreate -n apps-lv -L 14G webdata-vg
sudo lvcreate -n logs-lv -L 14G webdata-vg
sudo vgs
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675401855842/60864e61-026e-4684-b6ab-4762684b1d65.png" alt class="image--center mx-auto" /></p>
<p>Verify the entire setup:</p>
<pre><code class="lang-bash">sudo vgdisplay -v <span class="hljs-comment">#view complete setup - VG, PV, and LV</span>
sudo lsblk
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675402013004/85402570-d39b-40aa-8d41-1ca20e60b6b9.png" alt class="image--center mx-auto" /></p>
<p>From above we can see that <code>apps-lv</code> and <code>logs-lv</code> are of type <code>lvm</code>, but we want to reformat it to an <a target="_blank" href="https://en.wikipedia.org/wiki/Ext4"><strong>ext4</strong></a> filesystem.</p>
<pre><code class="lang-bash">sudo mkfs -t ext4 /dev/webdata-vg/apps-lv
sudo mkfs -t ext4 /dev/webdata-vg/logs-lv
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675403795221/845be9dd-c390-4a0a-85f7-9d54a948de84.png" alt class="image--center mx-auto" /></p>
<p>Create <code>/var/www/html</code> directory to store website files and mount them on apps-lv LV.</p>
<pre><code class="lang-bash">sudo mkdir -p /var/www/html
sudo mount /dev/webdata-vg/apps-lv /var/www/html
</code></pre>
<p>We follow the same step as above for the logs, but first, we need to back up the files in <code>/var/log</code> using <a target="_blank" href="https://linux.die.net/man/1/rsync"><code>rsync</code></a> before mounting it on the new destination folder:</p>
<pre><code class="lang-bash">sudo mkdir -p /home/recovery/logs
sudo rsync -av /var/<span class="hljs-built_in">log</span>/ /home/recovery/logs/
sudo mount /dev/webdata-vg/logs-lv /var/<span class="hljs-built_in">log</span>
sudo rsync -av /home/recovery/logs/. /var/<span class="hljs-built_in">log</span>
</code></pre>
<h2 id="heading-update-etcfstab-file">Update <code>/etc/fstab</code> file</h2>
<p>The UUID of the device will be used to update the <code>/etc/fstab</code> file.</p>
<pre><code class="lang-bash">sudo blkid | grep <span class="hljs-string">'webdata'</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675404356463/0951c0cd-660a-4bbd-9921-3c17d8c79b47.png" alt class="image--center mx-auto" /></p>
<p>The <a target="_blank" href="https://linuxconfig.org/how-fstab-works-introduction-to-the-etc-fstab-file-on-linux"><code>/etc/fstab</code></a> file stores static information about filesystems, their mount points and mount options. This file is read at boot time to determine the overall file system structure, and thereafter when a user executes the <code>mount</code> command to modify that structure.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675404665584/74c843ab-b85f-428e-82ee-401b7d6d1d86.png" alt class="image--center mx-auto" /></p>
<p>Test the configuration, reload the daemon, and verify the setup:</p>
<pre><code class="lang-bash">sudo mount -a
sudo systemctl daemon-reload
sudo df -h
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675405168239/b5ab6c26-a5c1-44c4-88ab-e1cf6d3fbed1.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-install-wordpress-on-webserver">Install WordPress on WebServer</h3>
<p>Install Apache and its dependencies and start the web server:</p>
<pre><code class="lang-bash">sudo yum -y update
sudo yum -y install wget httpd php php-mysqlnd php-fpm php-json

sudo systemctl <span class="hljs-built_in">enable</span> httpd
sudo systemctl start httpd
</code></pre>
<p>Install PHP and its dependencies:</p>
<pre><code class="lang-bash">sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
sudo yum install yum-utils http://rpms.remirepo.net/enterprise/remi-release-9.rpm

sudo yum module list php
sudo yum module reset php
sudo yum module <span class="hljs-built_in">enable</span> php:remi-8.1

sudo yum -y install php php-opcache php-gd php-curl php-mysqlnd

sudo systemctl <span class="hljs-built_in">enable</span> php-fpm
sudo systemctl start php-fpm

sudo setsebool -P httpd_execmem 1

sudo systemctl restart httpd
</code></pre>
<h3 id="heading-download-wordpress">Download WordPress</h3>
<pre><code class="lang-bash">mkdir wordpress; <span class="hljs-built_in">cd</span> wordpress;

wget http://wordpress.org/latest.tar.gz
sudo tar xzvf latest.tar.gz
rm -rf latest.tar.gz

cp wordpress/wp-config-sample.php wordpress/wp-config.php
cp -R wordpress /var/www/html/
</code></pre>
<h3 id="heading-configure-selinux-policies">Configure SELinux Policies</h3>
<pre><code class="lang-bash">sudo chown -R apache:apache /var/www/html/wordpress
sudo chcon -t httpd_sys_rw_content_t /var/www/html/wordpress -R
sudo setsebool -P httpd_can_network_connect=1
</code></pre>
<h2 id="heading-setup-database-server">Setup Database Server</h2>
<p>Launch a second RedHat EC2 instance that will have a role – ‘DB Server’<br />Repeat the same steps as for the Web Server, but instead of <code>apps-lv</code> create <code>db-lv</code> and mount it to <code>/db</code> directory instead of <code>/var/www/html/</code>.</p>
<h3 id="heading-install-mysql-on-db-server">Install MySQL on DB Server</h3>
<pre><code class="lang-bash">sudo yum -y update
sudo yum -y install mysql-server

sudo systemctl <span class="hljs-built_in">enable</span> mysqld
sudo systemctl restart mysqld
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675409363078/1d0d5938-c2e1-42f0-b98d-fb42f53bc418.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-configure-db-to-work-with-wordpress">Configure DB to work with WordPress</h3>
<p>Use <code>sudo mysql</code> to login as <code>root</code> and setup a database and a user with access to the database:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675409631305/3d72a6d3-2a30-4adf-8e08-d4c958a5c987.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-configure-wordpress-to-connect-to-a-remote-database">Configure WordPress to Connect to a Remote Database</h3>
<p>For this, we need to open port <strong>3306</strong> on the DB Server instance's Security Group (SG). Furthermore, we should limit the source to our Web Server's private IP address:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675409884294/f53316bb-d195-4349-afa6-887de421074a.png" alt class="image--center mx-auto" /></p>
<p>Additionally, we forgot to open port <strong>80</strong> on our Web Server's instance. I have already set up a specific SG for web servers, but reference the first rule in the image above for how it should appear.</p>
<p>We should now be able to access our DB Server from our Web Server as we did in the previous project:</p>
<pre><code class="lang-bash">sudo mysql -u admin -p -h 172.31.28.148
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675410553319/cef33283-1901-42e8-86bd-bcf412533141.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-view-wordpress-site">View WordPress Site</h3>
<p>With our Web Server running we should be able to view our WordPress site using the server's public IP address: `<a target="_blank" href="http://54.245.4.149/wordpress">http://54.245.4.149/wordpress</a>/`:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675410878028/a59f78b5-bf4b-4f38-8800-f45af03c2ca7.png" alt class="image--center mx-auto" /></p>
<p>However, we come across the above error. All that's required on our part is to update our <code>wp-config.php</code> file to provide details regarding our database settings. This file is located in <code>/var/www/html/wordpress/</code> . The <code>DB_HOST</code> value is our DB Server private IP address:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675411176583/099b36ee-c4bd-4c23-8e6d-4e9144b35c05.png" alt class="image--center mx-auto" /></p>
<p>After reloading the page we should enter the install flow:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675411343699/dfd6d89f-3a20-4a46-9322-dddda85ee5c8.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675411429847/8f39bfc4-11a4-4aa2-9780-d9cb5856d1b9.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675411455810/0f097b04-621b-4e8b-9011-264d9b9b0a33.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675411520150/22427c67-76c1-41d7-9491-e265b8f2964d.png" alt class="image--center mx-auto" /></p>
<p>Note: Remember to Terminate the two instances and delete the volumes if no longer needed lest you incur added AWS costs.</p>
<h3 id="heading-learning-outcomes">Learning Outcomes</h3>
<ol>
<li><p>Creating &amp; attaching volumes to EC2 instances</p>
</li>
<li><p>Creating Partitions, Physical Volumes, Volume Groups, and Logical Volumes</p>
</li>
<li><p>Mounting</p>
</li>
<li><p>Installing WordPress and configuring Database</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Onboard Users]]></title><description><![CDATA[In this project, we want to onboard 20 new Linux users onto a server. Create a shell script that reads a CSV file of a list of users to be onboarded. Each user requires the following:

Must have a default /home directory, ie /home/cdrani/ for user cd...]]></description><link>https://cdrani.dev/onboard-users</link><guid isPermaLink="true">https://cdrani.dev/onboard-users</guid><category><![CDATA[pbl]]></category><category><![CDATA[Scripting]]></category><category><![CDATA[DevOps Journey]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Thu, 02 Feb 2023 03:54:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1675309593714/72e3725d-3c36-437b-a3dc-e86bd81d6d4f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this project, we want to onboard 20 new Linux users onto a server. Create a shell script that reads a <code>CSV</code> file of a list of users to be onboarded. Each user requires the following:</p>
<ul>
<li><p>Must have a default <code>/home</code> directory, ie <code>/home/cdrani/</code> for user <code>cdrani</code></p>
</li>
<li><p>Each user must be added to a <code>developers</code> group</p>
</li>
<li><p>An <code>.ssh</code> folder with an <code>authorized_keys</code> file that includes the <code>rsa.pub</code> key of the current user ($USER)</p>
</li>
</ul>
<h3 id="heading-step-1-setup-private-andamp-public-keys-for-dollaruser">STEP 1: Setup Private &amp; Public keys for $USER</h3>
<ol>
<li><p>Generating ssh private (<code>id_rsa</code>) and public key (<code>id_rsa.pb</code>) for $USER. The public key will be copied over to each onboarded user's <code>.ssh/authorized_keys</code> file. We will make use of <code>ssh-keygen</code> and just press <code>Enter/Return</code> key to accept the default file names and placement. In the end, we should have both <code>id_rsa</code> and <code>id_rsa.pub</code> keys in <code>~/.ssh</code> folder.</p>
<pre><code class="lang-bash"> sudo ssh-keygen
</code></pre>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675276274598/d2c35830-467f-4d35-834c-187feac40d42.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<h3 id="heading-step-2-onboarding-script">STEP 2: Onboarding Script</h3>
<p>Now for the script itself. It's heavily documented with comments, but some additional criteria I included are to verify that the script is run by a <code>root</code> user, a <code>.csv</code> file is passed as an argument and a default password of <code>password</code> is set for each onboarded user with an expiration date of 7 days.</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-comment"># Exit if script is not run as root</span>
<span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-subst">$(id -u)</span>"</span> != <span class="hljs-string">"0"</span> ]; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"This script must be run as root."</span>
  <span class="hljs-built_in">exit</span> 1
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Exit if file is not provided</span>
<span class="hljs-keyword">if</span> [ <span class="hljs-variable">$#</span> -eq 0 ]; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"Please provide the name of the CSV file."</span>
  <span class="hljs-built_in">exit</span> 1
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Exit if file provided is not a .csv file</span>
file=<span class="hljs-string">"<span class="hljs-variable">$1</span>"</span>
<span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">${file##*.}</span>"</span> != <span class="hljs-string">"csv"</span> ]; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"The file must be a CSV file."</span>
  <span class="hljs-built_in">exit</span> 1
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Create developers group if it doesn't exist</span>
<span class="hljs-keyword">if</span> ! getent group developers &gt; /dev/null 2&gt;&amp;1; <span class="hljs-keyword">then</span>
  groupadd developers
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Read the CSV file</span>
<span class="hljs-keyword">while</span> IFS=<span class="hljs-string">','</span> <span class="hljs-built_in">read</span> -r name
<span class="hljs-keyword">do</span>
  username=<span class="hljs-variable">$name</span>

  <span class="hljs-comment"># Check if user exists</span>
  <span class="hljs-keyword">if</span> id <span class="hljs-string">"<span class="hljs-variable">$username</span>"</span> &gt;/dev/null 2&gt;&amp;1; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"User <span class="hljs-variable">$username</span> already exists."</span>
    <span class="hljs-built_in">continue</span>
  <span class="hljs-keyword">fi</span>

  <span class="hljs-comment"># Create user</span>
  useradd <span class="hljs-string">"<span class="hljs-variable">$username</span>"</span> -g developers -m
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$username</span>:default_password"</span> | chpasswd

  <span class="hljs-comment"># Set password expiration to 7 days</span>
  <span class="hljs-comment"># After expiration user needs to set/change password</span>
  chage -m 7 <span class="hljs-string">"<span class="hljs-variable">$username</span>"</span>

  <span class="hljs-comment"># Ensure .ssh folder exists for user, otherwise create one</span>
  home_dir=$(<span class="hljs-built_in">eval</span> <span class="hljs-built_in">echo</span> <span class="hljs-string">"~<span class="hljs-variable">$username</span>"</span>)
  ssh_dir=<span class="hljs-string">"<span class="hljs-variable">$home_dir</span>/.ssh"</span>
  <span class="hljs-keyword">if</span> [ ! -d <span class="hljs-string">"<span class="hljs-variable">$ssh_dir</span>"</span> ]; <span class="hljs-keyword">then</span>
    mkdir <span class="hljs-string">"<span class="hljs-variable">$ssh_dir</span>"</span>
    chmod 700 <span class="hljs-string">"<span class="hljs-variable">$ssh_dir</span>"</span> <span class="hljs-variable">$username</span>
  <span class="hljs-keyword">fi</span>

  <span class="hljs-comment"># Add public key to users' ~/.ssh/authorized_keys</span>
  auth_keys=<span class="hljs-string">"<span class="hljs-variable">$ssh_dir</span>/authorized_keys"</span>
  touch <span class="hljs-string">"<span class="hljs-variable">$auth_keys</span>"</span>
  chmod 600 <span class="hljs-string">"<span class="hljs-variable">$auth_keys</span>"</span> <span class="hljs-variable">$username</span>
  <span class="hljs-comment"># Need to give ownership to user else cannot login as them!!!</span>
  chown <span class="hljs-variable">$username</span>:developers <span class="hljs-string">"<span class="hljs-variable">$auth_keys</span>"</span>
  <span class="hljs-built_in">echo</span> $(cat ~/.ssh/id_rsa.pub) &gt;&gt; <span class="hljs-string">"<span class="hljs-variable">$auth_key</span>"</span>
<span class="hljs-keyword">done</span> &lt; <span class="hljs-string">"<span class="hljs-variable">$file</span>"</span>
</code></pre>
<p>To run the script we need a <code>*.csv</code> file with a list of names:</p>
<pre><code class="lang-plaintext"># names.csv
beth
mark,
lucy,
fred,
liza
</code></pre>
<p>Then we need our script to be executable before we run it:</p>
<pre><code class="lang-bash">sudo chmod +x onboard_users.sh
sudo ./onboard_users.sh names.csv
</code></pre>
<p>Let's test out some of the conditionals we have in place:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675276127609/0250f382-5c23-4fc9-810c-178b5bb01a06.png" alt class="image--center mx-auto" /></p>
<p>We now should have <code>/home/$user</code> (for ex. <code>/home/beth/</code>) directories for each user in the <code>names.csv</code> file:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675277842824/e2d663bd-cfcb-4e5b-bcb9-2ca8eb98e374.png" alt class="image--center mx-auto" /></p>
<p>Finally, we should be able to log in as any of the newly onboarded users, however, our new users don't use the same private key that we set up when launching our ec2 instance - we must use the one we generated above (<code>id_rsa</code>).</p>
<p>First, we need to ensure our key is not publicly viewable, i.e it is not writable:</p>
<pre><code class="lang-bash">sudo chmod -w id_rsa
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675279320610/eade7910-dea6-4a81-8b11-c4927bf92235.png" alt class="image--center mx-auto" /></p>
<p>After the above, we can then copy the private key from our remote server (ec2) to our local environment using <code>scp</code>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># scp -i &lt;key&gt; &lt;remote_username&gt;@&lt;Host&gt;:&lt;PathToFile&gt;   &lt;LocalFileLocation&gt;</span>
scp -i ~/Desktop/webserver-ec2.pem ubuntu@35.85.56.192:~/.ssh/id_rsa ~/.ssh/aux_id_rsa
</code></pre>
<p>With that complete, from our local computer, we can log in as an onboarded user:</p>
<pre><code class="lang-bash">ssh -i ~/.ssh/aux_id_rsa fred@38.85.56.192
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675308445380/f6c00995-abd7-4175-9e20-1e161fdb96a8.png" alt class="image--center mx-auto" /></p>
<p>Only users with the auth key can log in:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675309204647/3544ed25-64df-4bf1-81c7-cc2dfe122e70.png" alt class="image--center mx-auto" /></p>
<p>Note: One can log in using the <code>*.pem</code> key for the ec2 instance as well.</p>
<h3 id="heading-bonus">Bonus:</h3>
<p>Testing the script several times to address issues necessitated removing users as well. This quickly turned into a pain point that required a <code>deboard.sh</code> script. It has the same checks as the <code>onboard.sh</code> one which could be refactored out into a separate script that runs before both of them, but it's good enough.</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-subst">$(id -u)</span>"</span> != <span class="hljs-string">"0"</span> ]; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"This script must be run as root."</span>
  <span class="hljs-built_in">exit</span> 1
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Check if argument is provided</span>
<span class="hljs-keyword">if</span> [ <span class="hljs-variable">$#</span> -eq 0 ]; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"Please provide the name of the CSV file."</span>
  <span class="hljs-built_in">exit</span> 1
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Check if argument is a .csv file</span>
file=<span class="hljs-string">"<span class="hljs-variable">$1</span>"</span>
<span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">${file##*.}</span>"</span> != <span class="hljs-string">"csv"</span> ]; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"The file must be a CSV file."</span>
  <span class="hljs-built_in">exit</span> 1
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Read the CSV file</span>
<span class="hljs-keyword">while</span> IFS=<span class="hljs-string">','</span> <span class="hljs-built_in">read</span> -r fname lname
<span class="hljs-keyword">do</span>
  username=<span class="hljs-variable">$fname</span>

  <span class="hljs-comment"># Check if user exists</span>
  <span class="hljs-keyword">if</span> ! id <span class="hljs-string">"<span class="hljs-variable">$username</span>"</span> &gt;/dev/null 2&gt;&amp;1; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"User <span class="hljs-variable">$username</span> does not exist."</span>
    <span class="hljs-built_in">continue</span>
  <span class="hljs-keyword">fi</span>

  <span class="hljs-comment"># Remove user</span>
  sudo userdel -r <span class="hljs-string">"<span class="hljs-variable">$username</span>"</span>
<span class="hljs-keyword">done</span> &lt; <span class="hljs-string">"<span class="hljs-variable">$file</span>"</span>
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Client-Server Architecture With MySQL]]></title><description><![CDATA[Client-Server refers to an architecture in which multiple computers are connected over a network to send and receive requests from one another. In their communication, each machine has its role: the machine sending requests is usually referred to as ...]]></description><link>https://cdrani.dev/client-server-architecture-using-a-mysql-rdms</link><guid isPermaLink="true">https://cdrani.dev/client-server-architecture-using-a-mysql-rdms</guid><category><![CDATA[pbl]]></category><category><![CDATA[DevOps Journey]]></category><category><![CDATA[client-server]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Tue, 31 Jan 2023 22:18:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1675270320450/dca0f1d6-06ab-44ae-a53b-b7a63644c87c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Client-Server refers to an architecture in which multiple computers are connected over a network to send and receive requests from one another. In their communication, each machine has its role: the machine sending requests is usually referred to as the "Client" and the machine responding (serving) is called the "Server".</p>
<p>In this case, our Web Server has a role of a "Client" that connects and reads/writes to/from a Database (DB) Server (MySQL, MongoDB, Oracle, SQL Server or any other), and the communication between them happens over a Local Network (it can also be an Internet connection, but it is a common practice to place Web Server and DB Server close to each other in a local network).</p>
<h2 id="heading-implement-a-client-server-architecture">Implement A Client-Server Architecture</h2>
<p>We need to start with two Linux ec2 instances - one to act as the <code>client</code>, and the other as the <code>server</code></p>
<p>In the <code>client</code> one we need to setup <code>mysql-client</code> :</p>
<pre><code class="lang-bash">sudo apt update -y
sudo apt install mysql-client -y
</code></pre>
<p>As for <code>server</code>, we require <code>mysql-server</code> :</p>
<pre><code class="lang-bash">sudo apt update -y
sudo apt install mysql-server -y
</code></pre>
<p>Additionally, it would be best practice to update the <code>root</code> user password, run <code>mysql_secure_installation</code> , and reset some MySQL defaults.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="037763d3524dd23f0a3add18a6e5784d"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/cdrani/037763d3524dd23f0a3add18a6e5784d" class="embed-card">https://gist.github.com/cdrani/037763d3524dd23f0a3add18a6e5784d</a></div><p> </p>
<p>Completing the above for the <code>client</code> is unnecessary since the client will just be accessing the <code>server</code> .</p>
<p>By default, both of your EC2 virtual servers are located in the same local virtual network, so they can communicate with each other using local IP addresses. MySQL server uses TCP port 3306 by default, so you will have to open it by creating a new entry in ‘Inbound rules’ in ‘MySQL server’ Security Groups. For extra security, do not allow all IP addresses to reach your ‘MySQL server’ – allow access only to the specific local IP address of the ‘MySQL client’.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675152881755/f270a5f1-6c58-4b2a-8573-36ec107ca8ed.png" alt class="image--center mx-auto" /></p>
<p>You might need to configure the MySQL server to allow connections from remote hosts.</p>
<pre><code class="lang-bash">cat /etc/mysql.conf.d/mysqld.cnf | grep <span class="hljs-string">'^bind-address'</span>
<span class="hljs-comment"># bind-address = 127.0.0.1</span>


sed -i <span class="hljs-string">'s/bind-address.*/bind-address = 0.0.0.0/'</span> /etc/mysql/mysql.conf.d/mysqld.cnf

cat /etc/mysql.conf.d/mysqld.cnf | grep <span class="hljs-string">'^bind-address'</span>
<span class="hljs-comment"># bind-address = 0.0.0.0</span>

sudo systemctl restart mysql
</code></pre>
<p>Let's create an <code>admin</code> user and <code>test_db</code> database from our <code>server</code> instance:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675154862151/4a81fbcd-7668-4b89-9f50-752af0215771.png" alt class="image--center mx-auto" /></p>
<p>With the above setup, we can now connect to our MySQL <code>server</code> from our <code>client</code> using the above <code>admin</code> user. The <code>-h</code> option allows us to specify the IP or hostname of the MySQL server</p>
<pre><code class="lang-bash"><span class="hljs-comment"># mysql -u &lt;user&gt; -p -h &lt;host or ip address of mysql server&gt;</span>
mysql -u admin -p -h 172.31.30.207
</code></pre>
<p>We have now accessed the server as the client. Let's create a contacts table and populate it:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675156057946/e7f60893-ecb8-4637-b903-264f795e1b6e.png" alt class="image--center mx-auto" /></p>
<p>We should also be able to view that table and its row from the server instance and add additional rows to it.</p>
]]></content:encoded></item><item><title><![CDATA[MEAN Stack]]></title><description><![CDATA[MEAN Stack is a combination of the following components:

MongoDB (Document database) – Storage and retrieval of data

Express (Back-end application framework) – Makes requests to Database for Reads and Writes.

Angular (Front-end application framewo...]]></description><link>https://cdrani.dev/mean-stack</link><guid isPermaLink="true">https://cdrani.dev/mean-stack</guid><category><![CDATA[pbl]]></category><category><![CDATA[MEAN Stack]]></category><category><![CDATA[DevOps Journey]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Tue, 31 Jan 2023 22:16:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1675310202887/a9d5d800-7c14-47a0-8b18-0ee0a06094d0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h4 id="heading-mean-stack-is-a-combination-of-the-following-components">MEAN Stack is a combination of the following components:</h4>
<ol>
<li><p><a target="_blank" href="https://www.mongodb.com/"><strong>M</strong>ongoDB</a> (Document database) – Storage and retrieval of data</p>
</li>
<li><p><a target="_blank" href="https://expressjs.com/"><strong>E</strong>xpress</a> (Back-end application framework) – Makes requests to Database for Reads and Writes.</p>
</li>
<li><p><a target="_blank" href="https://angular.io/"><strong>A</strong>ngular</a> (Front-end application framework) – Handles Client and Server Requests</p>
</li>
<li><p><a target="_blank" href="https://nodejs.org"><strong>N</strong>odeJS</a> (JavaScript runtime environment) – Accepts requests and displays results to end user</p>
</li>
</ol>
<p>To integrate the above technologies let's build a book register.</p>
<h3 id="heading-nodejs-setup">NodeJS Setup</h3>
<pre><code class="lang-bash">sudo apt update -y
curl -fsSL https://deb.nodesource.com/setup_current.x | sudo -E bash -
sudo apt update -y <span class="hljs-comment"># might be necessary to update source index</span>
sudo apt install -y nodejs
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675198039983/86d28b4d-55ff-4c81-93be-37bff03b4e02.png" alt class="image--center mx-auto" /></p>
<p>After installing NodeJS let's create the project directory, run <code>npm init</code> in it and create the folder structure:</p>
<pre><code class="lang-bash">sudo mkdir Book; <span class="hljs-built_in">cd</span> Book; sudo npm init -y
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675198208177/a51073ef-9232-4a4f-9b1f-2276a9de4f0a.png" alt class="image--center mx-auto" /></p>
<p>We should now have a <code>package.json</code> file inside of the <code>Book</code> directory. At the end of the project we should have a folder structure like this:</p>
<pre><code class="lang-markdown">├── Book
│   ├── apps
│   │   ├── routes.js
│   │   ├── models
│   │   │   ├── book.js
├── public
│   ├── index.js
│   ├── index.html
├── node<span class="hljs-emphasis">_modules
├── server.js
├── package.json
└── package-lock.json</span>
</code></pre>
<h3 id="heading-express-setup">Express Setup</h3>
<p>Express is a minimal and flexible <code>Node.js</code> web application framework that provides features for web and mobile applications. We will use Express to pass book information to and from our MongoDB database.</p>
<pre><code class="lang-bash">sudo npm i express body-parser
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-comment">// ~/Book/server.js</span>

<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
<span class="hljs-keyword">const</span> bodyParser = <span class="hljs-built_in">require</span>(<span class="hljs-string">'body-parser'</span>);
<span class="hljs-keyword">const</span> app = express();

app.use(express.static(__dirname + <span class="hljs-string">'/public'</span>));
app.use(bodyParser.json());

<span class="hljs-built_in">require</span>(<span class="hljs-string">'./apps/routes'</span>)(app);

app.set(<span class="hljs-string">'port'</span>, <span class="hljs-number">3300</span>);
app.listen(app.get(<span class="hljs-string">'port'</span>), <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Server up: http://localhost:'</span> + app.get(<span class="hljs-string">'port'</span>));
});
</code></pre>
<p>Additionally, we need to include port <code>3300</code> in our EC2 instance's security group to be able to access the express server.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675200841698/52fcdb5e-1d33-4668-ab06-71a6b98ccfbf.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-mongodb-setup">MongoDB Setup</h3>
<p>MongoDB stores data in flexible, <code>JSON-like</code> documents. Fields in a database can vary from document to document and data structure can be changed over time. For our example application, we are adding book records to MongoDB that contain a book's name, ISBN, author, and number of pages.</p>
<h3 id="heading-installation">Installation</h3>
<p>Let's install the latest version of the MongoDB community edition. The commands below are pulled from the official docs:</p>
<pre><code class="lang-bash">wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add -

<span class="hljs-built_in">echo</span> <span class="hljs-string">"deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse"</span> | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list

sudo apt update
sudo apt install -y mongodb-org
sudo systemctl daemon-reload
sudo systemctl start mongod
</code></pre>
<p>We should see the service running now:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675200901322/12cb313c-a126-4e3c-8840-cf5ec68a1141.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-mongoose-setup">Mongoose Setup</h3>
<p>We will use Mongoose to establish a schema for the database to store data of our book register. Let's create the schema for our book model with fields for name, ISBN, author, and pages:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// ~/Book/apps/models/book.js</span>

<span class="hljs-keyword">const</span> mongoose = <span class="hljs-built_in">require</span>(<span class="hljs-string">'mongoose'</span>);
<span class="hljs-keyword">const</span> dbHost = <span class="hljs-string">'mongodb://localhost:27017/test'</span>;

mongoose.connect(dbHost);
mongoose.connection;

mongoose.set(<span class="hljs-string">'debug'</span>, <span class="hljs-literal">true</span>);

<span class="hljs-keyword">const</span> bookSchema = mongoose.Schema({
  <span class="hljs-attr">name</span>: <span class="hljs-built_in">String</span>,
  <span class="hljs-attr">isbn</span>: { <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>, <span class="hljs-attr">index</span>: <span class="hljs-literal">true</span> },
  <span class="hljs-attr">author</span>: <span class="hljs-built_in">String</span>,
  <span class="hljs-attr">pages</span>: <span class="hljs-built_in">Number</span>
});

<span class="hljs-keyword">const</span> Book = mongoose.model(<span class="hljs-string">'Book'</span>, bookSchema);

<span class="hljs-built_in">module</span>.exports = mongoose.model(<span class="hljs-string">'Book'</span>, bookSchema);
</code></pre>
<h3 id="heading-express-routes">Express Routes</h3>
<p>We need to define routes for which our express server should handle when a request is made to it. The following lists <code>/book</code> route for requests for GET &amp; POST, as well as a <code>/book/:isbn</code> to delete a book record when a DELETE request is provided with an <code>isbn</code> value for that book.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// ~/Book/apps/routes.js</span>

<span class="hljs-keyword">const</span> Book = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./models/book'</span>);

<span class="hljs-built_in">module</span>.exports = <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">app</span>) </span>{
    <span class="hljs-comment">// Returns all books</span>
    app.get(<span class="hljs-string">'/book'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">req, res</span>) </span>{
        Book.find({}, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">err, result</span>) </span>{
            <span class="hljs-keyword">if</span> ( err ) <span class="hljs-keyword">throw</span> err;
            res.json(result);
        });
    }); 

    app.post(<span class="hljs-string">'/book'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">req, res</span>) </span>{
        <span class="hljs-comment">// Adds book to book collection</span>
        <span class="hljs-keyword">const</span> { name, isbn, author, pages } = req.body;
        <span class="hljs-keyword">const</span> book = <span class="hljs-keyword">new</span> Book({ name, isbn, author, pages });
        book.save(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">err, result</span>) </span>{
            <span class="hljs-keyword">if</span> (err) <span class="hljs-keyword">throw</span> err;
            res.json( {
                <span class="hljs-attr">message</span>:<span class="hljs-string">"Successfully added book"</span>,
                <span class="hljs-attr">book</span>: result
            });
        });
    });

    app.delete(<span class="hljs-string">"/book/:isbn"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">req, res</span>) </span>{
        <span class="hljs-comment">// Find book based on isbn value and remove from collection</span>
        Book.findOneAndRemove(req.query, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">err, result</span>) </span>{
            <span class="hljs-keyword">if</span> (err) <span class="hljs-keyword">throw</span> err;
            res.json( {
                <span class="hljs-attr">message</span>: <span class="hljs-string">"Successfully deleted the book"</span>,
                <span class="hljs-attr">book</span>: result
            });
        });
    });

    <span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);

    app.get(<span class="hljs-string">'*'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">req, res</span>) </span>{
        res.sendfile(path.join(__dirname + <span class="hljs-string">'/public'</span>, <span class="hljs-string">'index.html'</span>));
    });
};
</code></pre>
<h2 id="heading-angularjs-setup">AngularJS Setup</h2>
<p><a target="_blank" href="https://angularjs.org">AngularJS</a> provides a web framework for creating dynamic views in your web applications. We need an <code>index.html</code> to present our data and a <code>index.js</code> to handle user input for adding or removing books from our records.</p>
<pre><code class="lang-bash">sudo npm i angularjs
</code></pre>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- ~/Book/public/index.html --&gt;</span>

<span class="hljs-meta">&lt;!doctype <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">ng-app</span>=<span class="hljs-string">"myApp"</span> <span class="hljs-attr">ng-controller</span>=<span class="hljs-string">"myCtrl"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"index.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">table</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>Name:<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">ng-model</span>=<span class="hljs-string">"Name"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>Isbn:<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">ng-model</span>=<span class="hljs-string">"Isbn"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>Author:<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">ng-model</span>=<span class="hljs-string">"Author"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>Pages:<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"number"</span> <span class="hljs-attr">ng-model</span>=<span class="hljs-string">"Pages"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">table</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">ng-click</span>=<span class="hljs-string">"add_book()"</span>&gt;</span>Add<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">hr</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">table</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>Name<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>Isbn<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>Author<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>Pages<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>

        <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">tr</span> <span class="hljs-attr">ng-repeat</span>=<span class="hljs-string">"book in books"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>{{book.name}}<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>{{book.isbn}}<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>{{book.author}}<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>{{book.pages}}<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>

          <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"Delete"</span> <span class="hljs-attr">data-ng-click</span>=<span class="hljs-string">"del_book(book)"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">table</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Now for the interactivity connection to the UI:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// ~/Book/public/script.js</span>

<span class="hljs-keyword">const</span> app = angular.module(<span class="hljs-string">'myApp'</span>, []);

app.controller(<span class="hljs-string">'myCtrl'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">$scope, $http</span>) </span>{
    $http({
        <span class="hljs-attr">method</span>: <span class="hljs-string">'GET'</span>,
        <span class="hljs-attr">url</span>: <span class="hljs-string">'/book'</span>
    }).then(<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">successCallback</span>(<span class="hljs-params">response</span>) </span>{
        $scope.books = response.data;
    }, <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">errorCallback</span>(<span class="hljs-params">response</span>) </span>{
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Error: '</span> + response);
    });

    $scope.del_book = <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">book</span>) </span>{
        $http( {
            <span class="hljs-attr">method</span>: <span class="hljs-string">'DELETE'</span>,
            <span class="hljs-attr">url</span>: <span class="hljs-string">'/book/:isbn'</span>,
            <span class="hljs-attr">params</span>: { <span class="hljs-string">'isbn'</span>: book.isbn }
        }).then(<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">successCallback</span>(<span class="hljs-params">response</span>) </span>{
            <span class="hljs-built_in">console</span>.log(response);
        }, <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">errorCallback</span>(<span class="hljs-params">response</span>) </span>{
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Error: '</span> + response);
        });
    };

    $scope.add_book = <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-keyword">const</span> body = {
            <span class="hljs-attr">name</span>: $scope.Name,
            <span class="hljs-attr">isbn</span>: $scope.Isbn,
            <span class="hljs-attr">author</span>: $scope.Author,
            <span class="hljs-attr">pages</span>: $scope.Pages,
        };

        $http({
            <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,
            <span class="hljs-attr">url</span>: <span class="hljs-string">'/book'</span>,
            <span class="hljs-attr">data</span>: body
        }).then(<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">successCallback</span>(<span class="hljs-params">response</span>) </span>{
            <span class="hljs-built_in">console</span>.log(response);
        }, <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">errorCallback</span>(<span class="hljs-params">response</span>) </span>{
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Error: '</span> + response);
        });
    };
});
</code></pre>
<p>Our app is now complete with simple CRD (Create, Read, Delete) functionality. Start the express server with <code>node index.js</code> in the <code>~/Book</code> directory.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675201001607/bd7eff44-1ea5-45b9-a633-c2b0b1dbd3ef.png" alt class="image--center mx-auto" /></p>
<p>Opening the app on our public IP address on port <code>3300</code> (for ex: <code>http://54.213.246.86:3300</code>) should display the below UI:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675201811877/f57a6108-4977-435e-8b80-35a2682736ff.png" alt class="image--center mx-auto" /></p>
<p>The below image shows the insertion of a book into the collection and a query to return all books:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675201767631/cdb3118b-2a34-4571-903e-eea323d27e25.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[LEMP Stack]]></title><description><![CDATA[LEMP refers to a collection of open-source software that is commonly used together to serve web applications. The term LEMP is an acronym that represents the configuration of a Linux operating system with an Nginx (pronounced engine-x, hence the E in...]]></description><link>https://cdrani.dev/lemp-stack</link><guid isPermaLink="true">https://cdrani.dev/lemp-stack</guid><category><![CDATA[DevOps Journey]]></category><category><![CDATA[pbl]]></category><dc:creator><![CDATA[Charles Drani]]></dc:creator><pubDate>Sat, 28 Jan 2023 03:34:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1674841663978/155aa4e9-6044-4ba2-a11b-b54860a0bf0e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>LEMP refers to a collection of open-source software that is commonly used together to serve web applications. The term <strong>LEMP</strong> is an acronym that represents the configuration of a <strong>L</strong>inux operating system with an Nginx (pronounced <em>engine-x</em>, hence the <strong>E</strong> in the acronym) web server, with site data stored in a <strong>M</strong>ySQL database and dynamic content processed by <strong>P</strong>HP.</p>
<p>The LEMP stack represents one way to configure a web server and is used in many highly-scaleable applications across the web.</p>
<p>The setup for a LEMP vs LAMP stack is quite similar as they share the same technologies other than the web server. We will make use of <code>ubuntu</code> for this project as well on an <code>ec2</code> instance.</p>
<h2 id="heading-step-1-install-nginx">STEP 1: Install Nginx</h2>
<pre><code class="lang-bash">sudo apt update -y
sudo apt install -y nginx
sudo systemctl status nginx
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674843320996/315a280a-ba69-4fd6-a2c2-f21cbfe1c401.png" alt class="image--center mx-auto" /></p>
<p>We need to have port <code>80</code> open for our instance just like in Project 1. It's wise to have different security groups depending on what port access your technologies require. For web access, I have the <code>default</code> security group (SG) limited to just port 22 (for ssh access) and <code>80</code> for web server access. For projects requiring additional rules, I will clone the <code>default</code> SG and append the new rules.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674753623011/b8126038-a694-43f6-961b-7c07976ec4f3.png" alt class="image--center mx-auto" /></p>
<p>Our server is now accessible on port <code>80</code> using our public IP Address (ex. <code>http://35.93.24.167</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674844276826/1a404f4a-661c-4b43-bb88-13e90ff4304d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-2-setup-mysql">STEP 2: Setup MySQL</h2>
<p>Setting MySQL for LEMP is identical to LAMP so I have just copied the steps from the last project into a gist and embedded it below.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="037763d3524dd23f0a3add18a6e5784d"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/cdrani/037763d3524dd23f0a3add18a6e5784d" class="embed-card">https://gist.github.com/cdrani/037763d3524dd23f0a3add18a6e5784d</a></div><p> </p>
<h2 id="heading-step-3-install-php">STEP 3: Install PHP</h2>
<p>Nginx requires an external program to handle PHP processing and act as a bridge between the PHP interpreter itself and the web server. We need <code>php-fpm</code> (“PHP fastCGI process manager”) which tells Nginx to pass PHP requests to itself for processing. Additionally, we will need <code>php-mysql</code>, a PHP module that allows PHP to communicate with MySQL-based databases.</p>
<pre><code class="lang-bash">sudo apt install -y php-fpm php-mysql
</code></pre>
<h2 id="heading-step-4-configure-nginx-to-use-php-fpm">STEP 4: Configure Nginx to use PHP-FPM</h2>
<p>Nginx has something similar to Apache's virtual hosts called server blocks. They both encapsulate configuration details and allow hosting multiple domains on a single server.</p>
<p>Let's create a domain for this project and transfer its ownership from <code>root</code> to the <code>USER</code>.</p>
<pre><code class="lang-bash">sudo mkdir /var/www/lemp
sudo chown -R <span class="hljs-variable">$USER</span>:<span class="hljs-variable">$USER</span> /var/www/lemp
</code></pre>
<h3 id="heading-configuration">Configuration</h3>
<pre><code class="lang-nginx"><span class="hljs-comment">#/etc/nginx/sites-available/lemp</span>

<span class="hljs-section">server</span> {
    <span class="hljs-attribute">listen</span> <span class="hljs-number">80</span>;
    <span class="hljs-attribute">server_name</span> lemp www.lemp;
    <span class="hljs-attribute">root</span> /var/www/lemp;

    <span class="hljs-attribute">index</span> index.html index.htm index.php;

    <span class="hljs-attribute">location</span> / {
        <span class="hljs-attribute">try_files</span> <span class="hljs-variable">$uri</span> <span class="hljs-variable">$uri</span>/ =<span class="hljs-number">404</span>;
    }

    <span class="hljs-attribute">location</span> <span class="hljs-regexp">~ \.php$</span> {
        <span class="hljs-attribute">include</span> snippets/fastcgi-php.conf;
        <span class="hljs-attribute">fastcgi_pass</span> unix:/var/run/php/php8.1-fpm.sock;
     }

    <span class="hljs-attribute">location</span> <span class="hljs-regexp">~ /\.ht</span> {
        <span class="hljs-attribute">deny</span> all;
    }

}
</code></pre>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Config Keyword</td><td>Purpose</td></tr>
</thead>
<tbody>
<tr>
<td><code>listen</code></td><td>sets port Nginx will listen on</td></tr>
<tr>
<td><code>root</code></td><td>document root where the files served by this website are stored</td></tr>
<tr>
<td><code>index</code></td><td>Order in which nginx will prioritize index files.</td></tr>
<tr>
<td><code>server_name</code></td><td>domain names and/or IP address this server block should respond</td></tr>
<tr>
<td><code>location /</code></td><td>checks for the existence of files or directories matching a URI request. If Nginx cannot find the appropriate resource, it will return a 404 error</td></tr>
<tr>
<td><code>location ~ \.php$</code></td><td><strong>h</strong>andles PHP processing by pointing Nginx to the <code>fastcgi-php.conf</code> configuration file and the <code>php8.1-fpm.sock file</code>, which declares what socket is associated with <code>php-fpm</code></td></tr>
<tr>
<td><code>location ~ /\.ht</code></td><td>deals <code>.htaccess</code> files, which Nginx does not process.</td></tr>
</tbody>
</table>
</div><p>After setting the configuration above, we need to let Nginx be aware of it, verify it has no syntax errors, unlink the default site (with the Nginx landing page pictured above), and reload the Nginx:</p>
<pre><code class="lang-bash">sudo ln -s /etc/nginx/sites-available/lemp /etc/nginx/sites-enabled/
sudo nginx -t
sudo unlink /etc/nginx/sites-enabled/default
sudo systemctl reload nginx
</code></pre>
<p>Our site is currently empty as it's bereft of any files - <code>*.html</code> or <code>*.php</code> , and will thus show a 403 message. Let's rectify this by adding a simple <code>index.html</code> file:</p>
<pre><code class="lang-bash">sudo <span class="hljs-built_in">echo</span> <span class="hljs-string">'Hello LEMP from hostname'</span> $(curl -s http://169.254.169.254/latest/meta-data/public-hostname) <span class="hljs-string">'with public IP'</span> $(curl -s http://169.254.169.254/latest/meta-data/public-ipv4) &gt; /var/www/lemp/index.html
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674847447899/1046aa92-5383-4b16-ac40-45b2dfd54a5e.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-5-testing-php-with-nginx">Step 5: Testing PHP with Nginx</h2>
<p>Let's verify that our nginx + php integration (using <code>php-fpm</code>) can process <code>.php</code> files.</p>
<pre><code class="lang-bash">sudo <span class="hljs-built_in">echo</span> <span class="hljs-string">'&lt;?php phpinfo();'</span> &gt; /var/www/lemp/info.php
</code></pre>
<p>We should see the following PHP info page on the <code>http:35.93.24.167/info.php</code> URL:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674847851887/67df053a-9325-4e57-93f3-89891762986f.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-6-retrieve-data-from-mysql-db-with-php">Step 6: Retrieve Data from MySQL DB with PHP</h2>
<p>For a final integration, let's create a <code>todolist</code> database, configure access to it and have Nginx query our database and display it.</p>
<p>In the MySQL console, we'll create a database and an <code>admin</code> user with all privileges to the new database. This user will now be able to create a table in the database and insert data into it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674853938554/756443c2-0672-44e0-a247-178e51b30fd5.png" alt class="image--center mx-auto" /></p>
<p>What's the use of a database without any data? Let's populate it with some sample todo list items:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674855202360/63445e94-e546-42a8-9d50-11ce6f2d2cc5.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-display-todo">Display Todo</h3>
<p>To display our data from our <code>todo_list</code> we need access our <code>test_db</code>database, query our <code>todo_list</code> table, and finally create an HTML skeleton to display the data in a list:</p>
<pre><code class="lang-php"><span class="hljs-comment"># /var/www/lemp/todo_list.php</span>

<span class="hljs-meta">&lt;?php</span>
$user = <span class="hljs-string">"admin"</span>;
$password = <span class="hljs-string">"Super01!"</span>;
$database = <span class="hljs-string">"test_db"</span>;
$table = <span class="hljs-string">"todo_list"</span>;

<span class="hljs-keyword">try</span> {
  $db = <span class="hljs-keyword">new</span> PDO(<span class="hljs-string">"mysql:host=localhost;dbname=<span class="hljs-subst">$database</span>"</span>, $user, $password);
  <span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;h2&gt;TODO&lt;/h2&gt;&lt;ol&gt;"</span>;
  <span class="hljs-keyword">foreach</span>($db-&gt;query(<span class="hljs-string">"SELECT content FROM <span class="hljs-subst">$table</span>"</span>) <span class="hljs-keyword">as</span> $row) {
    <span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;li&gt;"</span> . $row[<span class="hljs-string">'content'</span>] . <span class="hljs-string">"&lt;/li&gt;"</span>;
  }
  <span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;/ol&gt;"</span>;
} <span class="hljs-keyword">catch</span> (PDOException $e) {
    <span class="hljs-keyword">print</span> <span class="hljs-string">"Error!: "</span> . $e-&gt;getMessage() . <span class="hljs-string">"&lt;br/&gt;"</span>;
    <span class="hljs-keyword">die</span>();
}
</code></pre>
<p>Visiting <code>http://35.93.24.167/todo_list.php</code> , should display our todo list:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674855907625/50cbf48e-ff4f-4bbd-9501-f3b202ff3135.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-learning-outcomes">Learning Outcomes</h2>
<ol>
<li><p>Set up Nginx Server &amp; connect with PHP.</p>
</li>
<li><p>Use PHP to connect to MySQL database, query a table, and display data.</p>
</li>
</ol>
]]></content:encoded></item></channel></rss>