<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Posts on random thoughts</title><link>https://awesomeprogrammer.com/post/</link><description>Recent content in Posts on random thoughts</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Sun, 12 Jul 2026 13:50:55 +0000</lastBuildDate><atom:link href="https://awesomeprogrammer.com/post/index.xml" rel="self" type="application/rss+xml"/><item><title>Rewrite in Go</title><link>https://awesomeprogrammer.com/blog/2026/07/12/rewrite-in-go/</link><pubDate>Sun, 12 Jul 2026 13:50:55 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2026/07/12/rewrite-in-go/</guid><description><![CDATA[<p>I did a useless experiment and decided to rewrite a pretty standard Rails app in Go.</p>
<p>Obviously not <em>me</em> directly, I just had my clankers do it. Did I learn anything? Not really. Was it worth it? Probably not.</p>
<p>But first, some history.</p>
<p>The app I&rsquo;m referring to is almost-dead, mostly visited by bots and spammers, it&rsquo;s a <a href="https://quake.net.pl/">service about Quake</a>. It has some old content, some forums, and once every few months somebody will post a &lsquo;hey guys, are you still alive?&rsquo; comment which will get 1 response after maybe a few weeks. It&rsquo;s fascinating to observe. I even got sentimental and wrote some news after the migration.</p>
<p>I still keep it because it was my first &lsquo;real&rsquo; web app written in Ruby on Rails. Plus I <del>wasted</del> spent my youth there as a kid arguing with strangers on the forums, posting stuff and whatnot. It&rsquo;s mostly a CRUD, but unfortunately when you look closer you will see a myriad of functionalities buried underneath - permissions with roles, versioning, moderation panels, preferences, in-app notifications, file upload and file post-processing, a few background workers etc. Mostly basic stuff - but it does add up in terms of lines of code.</p>
<p>Obviously I would never rewrite it myself - I <em>had</em> that idea a few times, but given the amount of effort - why bother? But nowadays, armed with clankers in the form of codex, opencode, claude and a myriad of LLM models I could <em>just do it</em>.</p>
<p>So slowly, over two weeks I did that after hours, because - <em>why not?</em> After all I had some spare allowance to burn tokens in a few subscriptions.</p>
<figure><img src="/images/rewrite-in-go.jpg"/><figcaption>
            <h4>Slopcoding at its finest</h4>
        </figcaption>
</figure>

<h3 id="so-what-are-the-learnings-here">So what are the learnings here?</h3>
<p>There are no learnings, really. Figuratively and literally. Because - as said - scope adds up when you look at the app as a whole, and tracking any progress gets mentally taxing pretty quickly.</p>
<p>With such a rewrite you&rsquo;re probably gonna tell your agents to track progress somewhere, but despite that things will fall through the cracks and I don&rsquo;t see how you can <em>not</em> correct the course during the process; one wrongly interpreted sentence and agents will start building some really <em>crazy</em> (and totally useless) functionalities or abstractions; then again I didn&rsquo;t spend weeks reviewing the <em>perfect migration plan</em>.</p>
<p>There is also no learning from a <em>human</em> perspective - because code grows so fast it&rsquo;s obviously getting impossible to track or build any mental model of it, so it all feels alien and I found myself asking the fundamental question of how things were constructed, which is not a great feeling.</p>
<p>However, it&rsquo;s amazing such things are even possible nowadays and the app <em>mostly</em> worked - I had to do a few follow-up sessions to fix broken things, do security-related improvements and port things that the llm <em>forgot</em> (gotta love context rot) - but the fact is I would never be able to do it myself given time constraints.</p>
<p>Indeed I feel like software economics will change, unless of course tokens start costing us <em>actual money</em> and we&rsquo;ll be back to replacing expensive LLMs with cheaper junior developers, lol. At the same time I wonder who&rsquo;s gonna support all those machine generated lines of code generated at massive scale nowadays.</p>
<p>As a bonus - some useless stats from <a href="https://github.com/boyter/scc">scc</a>:</p>
<pre tabindex="0"><code>scc rails/
───────────────────────────────────────────────────────────────────────────────
Language            Files       Lines    Blanks  Comments       Code Complexity
───────────────────────────────────────────────────────────────────────────────
Ruby                  667      42,902     7,199     7,350     28,353        838
Sass                   29       4,534       533       314      3,687          0
Ruby HTML              28         866       109         0        757         80
YAML                   22       1,619       104        33      1,482          0
Markdown               16       2,663       684         0      1,979          0
JavaScript             15       2,301       160       391      1,750        297
CoffeeScript           11       1,182       128       376        678         64
Plain Text              8         890       155         0        735          0
Gemfile                 7         294        39        91        164          0
Rakefile                7         584        65        85        434         18
HTML                    6         344        46        13        285          0
BASH                    5          24         7         5         12          3
CSS                     4         319        29        27        263          0
JSON                    4      11,837         0         0     11,837          0
Dockerfile              1          68        10         0         58         10
License                 1          21         4         0         17          0
SQL                     1       1,187         3         0      1,184          0
SVG                     1         438         0         0        438          0
Shell                   1          18         2         4         12          2
XML                     1       6,899         0         1      6,898          0
───────────────────────────────────────────────────────────────────────────────
Total                 835      78,990     9,277     8,690     61,023      1,312
───────────────────────────────────────────────────────────────────────────────

scc go/
───────────────────────────────────────────────────────────────────────────────
Language            Files       Lines    Blanks  Comments       Code Complexity
───────────────────────────────────────────────────────────────────────────────
Go                    471     107,544    10,464    12,060     85,020     10,166
HTML                   52         439        54        13        372          0
SQL                    37       5,656       547     1,661      3,448          8
Sass                   31       4,719       543       364      3,812          0
JavaScript             24       3,976       230       560      3,186        412
Markdown               23          41         4         0         37          0
Templ                  22       1,324        69       193      1,062         63
YAML                   11       1,166        35        39      1,092          0
XML                     4          34         0         0         34          0
CSS                     3         243        18        27        198          0
JSON                    2          27         0         0         27          0
Ruby                    2          87        10        29         48          0
Docker ignore           1           6         0         0          6          0
Dockerfile              1          60        14        18         28          1
Plain Text              1          19         4         0         15          0
SVG                     1         438         0         0        438          0
───────────────────────────────────────────────────────────────────────────────
Total                 686     125,779    11,992    14,964     98,823     10,650
───────────────────────────────────────────────────────────────────────────────
</code></pre><p>Not very interesting numbers due to the sheer difference between stacks (a lot of rails code was just hidden in gems). If you drop <a href="https://github.com/sqlc-dev/sqlc">sqlc</a> queries/generated code and <a href="https://github.com/a-h/templ">generated templates</a>, exclude vendored gems and vendored js libs in both apps, some helper bash/ruby scripts and so on - you would see something like:</p>
<pre tabindex="0"><code>┌────────────────────┬───────┬────────────┐
│                    │ Files │ Code lines │
├────────────────────┼───────┼────────────┤
│ Go app (go/)       │ 603   │ 74,481     │
├────────────────────┼───────┼────────────┤
│ Rails app (rails/) │ 625   │ 23,778     │
└────────────────────┴───────┴────────────┘
</code></pre><p>The only problem is that I can somewhat reason behind this Rails code from 2011, but I have mostly no idea what&rsquo;s going on in the Go port. But who cares, right? <em>If it works, it works?</em></p>
<p>So the natural next step would be <em>rewrite in Rust</em>, I guess?</p>]]></description><content:encoded><![CDATA[<p>I did a useless experiment and decided to rewrite a pretty standard Rails app in Go.</p>
<p>Obviously not <em>me</em> directly, I just had my clankers do it. Did I learn anything? Not really. Was it worth it? Probably not.</p>
<p>But first, some history.</p>
<p>The app I&rsquo;m referring to is almost-dead, mostly visited by bots and spammers, it&rsquo;s a <a href="https://quake.net.pl/">service about Quake</a>. It has some old content, some forums, and once every few months somebody will post a &lsquo;hey guys, are you still alive?&rsquo; comment which will get 1 response after maybe a few weeks. It&rsquo;s fascinating to observe. I even got sentimental and wrote some news after the migration.</p>
<p>I still keep it because it was my first &lsquo;real&rsquo; web app written in Ruby on Rails. Plus I <del>wasted</del> spent my youth there as a kid arguing with strangers on the forums, posting stuff and whatnot. It&rsquo;s mostly a CRUD, but unfortunately when you look closer you will see a myriad of functionalities buried underneath - permissions with roles, versioning, moderation panels, preferences, in-app notifications, file upload and file post-processing, a few background workers etc. Mostly basic stuff - but it does add up in terms of lines of code.</p>
<p>Obviously I would never rewrite it myself - I <em>had</em> that idea a few times, but given the amount of effort - why bother? But nowadays, armed with clankers in the form of codex, opencode, claude and a myriad of LLM models I could <em>just do it</em>.</p>
<p>So slowly, over two weeks I did that after hours, because - <em>why not?</em> After all I had some spare allowance to burn tokens in a few subscriptions.</p>
<figure><img src="/images/rewrite-in-go.jpg"/><figcaption>
            <h4>Slopcoding at its finest</h4>
        </figcaption>
</figure>

<h3 id="so-what-are-the-learnings-here">So what are the learnings here?</h3>
<p>There are no learnings, really. Figuratively and literally. Because - as said - scope adds up when you look at the app as a whole, and tracking any progress gets mentally taxing pretty quickly.</p>
<p>With such a rewrite you&rsquo;re probably gonna tell your agents to track progress somewhere, but despite that things will fall through the cracks and I don&rsquo;t see how you can <em>not</em> correct the course during the process; one wrongly interpreted sentence and agents will start building some really <em>crazy</em> (and totally useless) functionalities or abstractions; then again I didn&rsquo;t spend weeks reviewing the <em>perfect migration plan</em>.</p>
<p>There is also no learning from a <em>human</em> perspective - because code grows so fast it&rsquo;s obviously getting impossible to track or build any mental model of it, so it all feels alien and I found myself asking the fundamental question of how things were constructed, which is not a great feeling.</p>
<p>However, it&rsquo;s amazing such things are even possible nowadays and the app <em>mostly</em> worked - I had to do a few follow-up sessions to fix broken things, do security-related improvements and port things that the llm <em>forgot</em> (gotta love context rot) - but the fact is I would never be able to do it myself given time constraints.</p>
<p>Indeed I feel like software economics will change, unless of course tokens start costing us <em>actual money</em> and we&rsquo;ll be back to replacing expensive LLMs with cheaper junior developers, lol. At the same time I wonder who&rsquo;s gonna support all those machine generated lines of code generated at massive scale nowadays.</p>
<p>As a bonus - some useless stats from <a href="https://github.com/boyter/scc">scc</a>:</p>
<pre tabindex="0"><code>scc rails/
───────────────────────────────────────────────────────────────────────────────
Language            Files       Lines    Blanks  Comments       Code Complexity
───────────────────────────────────────────────────────────────────────────────
Ruby                  667      42,902     7,199     7,350     28,353        838
Sass                   29       4,534       533       314      3,687          0
Ruby HTML              28         866       109         0        757         80
YAML                   22       1,619       104        33      1,482          0
Markdown               16       2,663       684         0      1,979          0
JavaScript             15       2,301       160       391      1,750        297
CoffeeScript           11       1,182       128       376        678         64
Plain Text              8         890       155         0        735          0
Gemfile                 7         294        39        91        164          0
Rakefile                7         584        65        85        434         18
HTML                    6         344        46        13        285          0
BASH                    5          24         7         5         12          3
CSS                     4         319        29        27        263          0
JSON                    4      11,837         0         0     11,837          0
Dockerfile              1          68        10         0         58         10
License                 1          21         4         0         17          0
SQL                     1       1,187         3         0      1,184          0
SVG                     1         438         0         0        438          0
Shell                   1          18         2         4         12          2
XML                     1       6,899         0         1      6,898          0
───────────────────────────────────────────────────────────────────────────────
Total                 835      78,990     9,277     8,690     61,023      1,312
───────────────────────────────────────────────────────────────────────────────

scc go/
───────────────────────────────────────────────────────────────────────────────
Language            Files       Lines    Blanks  Comments       Code Complexity
───────────────────────────────────────────────────────────────────────────────
Go                    471     107,544    10,464    12,060     85,020     10,166
HTML                   52         439        54        13        372          0
SQL                    37       5,656       547     1,661      3,448          8
Sass                   31       4,719       543       364      3,812          0
JavaScript             24       3,976       230       560      3,186        412
Markdown               23          41         4         0         37          0
Templ                  22       1,324        69       193      1,062         63
YAML                   11       1,166        35        39      1,092          0
XML                     4          34         0         0         34          0
CSS                     3         243        18        27        198          0
JSON                    2          27         0         0         27          0
Ruby                    2          87        10        29         48          0
Docker ignore           1           6         0         0          6          0
Dockerfile              1          60        14        18         28          1
Plain Text              1          19         4         0         15          0
SVG                     1         438         0         0        438          0
───────────────────────────────────────────────────────────────────────────────
Total                 686     125,779    11,992    14,964     98,823     10,650
───────────────────────────────────────────────────────────────────────────────
</code></pre><p>Not very interesting numbers due to the sheer difference between stacks (a lot of rails code was just hidden in gems). If you drop <a href="https://github.com/sqlc-dev/sqlc">sqlc</a> queries/generated code and <a href="https://github.com/a-h/templ">generated templates</a>, exclude vendored gems and vendored js libs in both apps, some helper bash/ruby scripts and so on - you would see something like:</p>
<pre tabindex="0"><code>┌────────────────────┬───────┬────────────┐
│                    │ Files │ Code lines │
├────────────────────┼───────┼────────────┤
│ Go app (go/)       │ 603   │ 74,481     │
├────────────────────┼───────┼────────────┤
│ Rails app (rails/) │ 625   │ 23,778     │
└────────────────────┴───────┴────────────┘
</code></pre><p>The only problem is that I can somewhat reason behind this Rails code from 2011, but I have mostly no idea what&rsquo;s going on in the Go port. But who cares, right? <em>If it works, it works?</em></p>
<p>So the natural next step would be <em>rewrite in Rust</em>, I guess?</p>]]></content:encoded></item><item><title>Synologymaxxing</title><link>https://awesomeprogrammer.com/blog/2026/07/04/synologymaxxing/</link><pubDate>Sat, 04 Jul 2026 12:06:01 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2026/07/04/synologymaxxing/</guid><description><![CDATA[<p>I do own DS918+ with just received its last <a href="https://www.synology.com/en-global/DSM74">DSM update - 7.4</a> (you can skip the AI nonsense).</p>
<p>I am really happy with this purchase, as it&rsquo;s been serving me well since 2018, and I hope it will run for few more years. I run ot it myriad of containers, and fairly recently I even tried moving some workloads from Hetzner to a local Ubuntu VM running on DSM. It&rsquo;s packed with four 4TB WD Red HDDs since I owned, and in general whole unit has been hassle-free.</p>
<p>If you want to <em>max out</em> this unit further, you can consider the following.</p>
<ol>
<li>Adding SSD drives for storage (I was lucky to buy two 2TB Samsung EVO drives before price hike) - you can work around cache-only limitation <a href="https://github.com/007revad/Synology_HDD_db">using this tool</a>. After major DSM upgrade (which you&rsquo;re gonna do once as this device reached EOL, lol) you might need to do a full reboot as Synology might be beeping and reporting faulty drives. Worry not - you <em>should not</em> lose any data there.</li>
<li>Upgrading RAM to 16GB - it seems 2x8GB is the max what Celeron J3455 can handle (according to <a href="https://www.intel.com/content/www/us/en/products/sku/95594/intel-celeron-processor-j3455-2m-cache-up-to-2-30-ghz/specifications.html">random sources</a>), but I didn&rsquo;t verify it personally; full mem test finishes successfully with two 8GB sticks, so I&rsquo;m pretty happy with that. In case somebody gonna Google that - I bought used Samsung M471B1G73EB0-YK0 and had no problem with it.</li>
<li>Getting UPS (d&rsquo;oh) - I use some old CyberPower where I replaced battery fairly recently - Synology can read UPS status via USB connection, so you can see power status/remaining battery and configure graceful shutdown if needed.</li>
<li>Last, but not least - getting some external SSD/HDD for backups of NAS itself. I do backup <em>most precious</em> stuff - like photos and some stuff in home directories - <a href="https://www.synology.com/en-global/dsm/packages/HyperBackup">HyperBackup</a> works like a charm.</li>
</ol>
<p>The only thing i wish this unit had is a little bit of <em>more juice</em> - the CPU is definitely dated; on the other hand that kinda forced me to optimize and trim my workloads, so I&rsquo;m gonna look at the bright side.</p>]]></description><content:encoded><![CDATA[<p>I do own DS918+ with just received its last <a href="https://www.synology.com/en-global/DSM74">DSM update - 7.4</a> (you can skip the AI nonsense).</p>
<p>I am really happy with this purchase, as it&rsquo;s been serving me well since 2018, and I hope it will run for few more years. I run ot it myriad of containers, and fairly recently I even tried moving some workloads from Hetzner to a local Ubuntu VM running on DSM. It&rsquo;s packed with four 4TB WD Red HDDs since I owned, and in general whole unit has been hassle-free.</p>
<p>If you want to <em>max out</em> this unit further, you can consider the following.</p>
<ol>
<li>Adding SSD drives for storage (I was lucky to buy two 2TB Samsung EVO drives before price hike) - you can work around cache-only limitation <a href="https://github.com/007revad/Synology_HDD_db">using this tool</a>. After major DSM upgrade (which you&rsquo;re gonna do once as this device reached EOL, lol) you might need to do a full reboot as Synology might be beeping and reporting faulty drives. Worry not - you <em>should not</em> lose any data there.</li>
<li>Upgrading RAM to 16GB - it seems 2x8GB is the max what Celeron J3455 can handle (according to <a href="https://www.intel.com/content/www/us/en/products/sku/95594/intel-celeron-processor-j3455-2m-cache-up-to-2-30-ghz/specifications.html">random sources</a>), but I didn&rsquo;t verify it personally; full mem test finishes successfully with two 8GB sticks, so I&rsquo;m pretty happy with that. In case somebody gonna Google that - I bought used Samsung M471B1G73EB0-YK0 and had no problem with it.</li>
<li>Getting UPS (d&rsquo;oh) - I use some old CyberPower where I replaced battery fairly recently - Synology can read UPS status via USB connection, so you can see power status/remaining battery and configure graceful shutdown if needed.</li>
<li>Last, but not least - getting some external SSD/HDD for backups of NAS itself. I do backup <em>most precious</em> stuff - like photos and some stuff in home directories - <a href="https://www.synology.com/en-global/dsm/packages/HyperBackup">HyperBackup</a> works like a charm.</li>
</ol>
<p>The only thing i wish this unit had is a little bit of <em>more juice</em> - the CPU is definitely dated; on the other hand that kinda forced me to optimize and trim my workloads, so I&rsquo;m gonna look at the bright side.</p>]]></content:encoded></item><item><title>Zero Flaky Test - ZFT (tm) - project</title><link>https://awesomeprogrammer.com/blog/2026/06/13/zero-flaky-test-zft-tm-project/</link><pubDate>Sat, 13 Jun 2026 17:43:54 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2026/06/13/zero-flaky-test-zft-tm-project/</guid><description><![CDATA[<p>Three years ago, I wrote about <a href="https://awesomeprogrammer.com/blog/2023/03/26/auto-ticketing-flaky-rspec-tests/">auto-ticketing flaky specs</a> (dear lord, please slow down time maybe a bit? kthx).</p>
<p>In the time since, I was loosely consulting on a small/medium Rails project that was struggling with some technical debt - one aspect of which was a flaky test suite. The project had 231 flaky test reports, around 6,600 unit tests, and 342 feature specs running on RSpec, Capybara, and Selenium-Chrome.</p>
<p>Eventually, it reached 0 (zero) flaky tests - which, I think, makes it the only &ldquo;production grade&rdquo; project with such metrics I have ever seen in my life. So, one might ask - how can I get there?</p>
<h3 id="time">Time</h3>
<p>It just takes time. Flaky unit tests are usually easy to reason about; the biggest chunk of work was solving various issues with feature specs, as that area is the Wild Wild West. For example, a rogue request from some container leaking into another test (there was a tusd container for uploads and a test that didn&rsquo;t wait properly for the upload to finish, meaning a webhook would fail a subsequent, random test). Other issues included custom smooth-scroll logic causing clicks on unintended elements, or JavaScript components not being mounted before an interaction with the page occurred - and the list goes on.</p>
<h3 id="trying-a-different-driver">Trying a different driver</h3>
<p>I tried using Ferrum (with the Cuprite driver). It won&rsquo;t auto-magically speed up your test suite, but using a different driver might surface different problems, as Ferrum speaks directly to the browser over a WebSocket (a huge gain since you don&rsquo;t have to install the right version of <code>chromedriver</code>).</p>
<p>For example, it turned out a lot of Stimulus-related tests now failed because the test tried to click on a button before the Stimulus controller managed to connect. In that case, I would suggest rendering such a problematic button as disabled and re-enabling it in the <code>connect()</code> method. In practice, this shouldn&rsquo;t be visible to the end-user, and Capybara will wait for the button to be enabled when doing <code>click_on</code>. That simple change fixes your test without any weird wait-for/sleep hacks.</p>
<p>Speaking of which - if you&rsquo;re explicitly waiting for some elements or interactions outside of Capybara&rsquo;s default wait behavior, there is a high chance you&rsquo;re doing something wrong, or maybe your UI lacks a UX interaction you can assert on.</p>
<p>In the end, the project stayed on Ferrum, as it also solved Selenium &ldquo;invalid session id&rdquo; exceptions, which turned out to be really difficult to debug in a CI environment.</p>
<h3 id="logging">Logging</h3>
<p>Some feature specs can feel impossible to debug. Ferrum offers really in-depth logs that you can shove into your favorite LLM for cross-referencing. In some cases, it can work wonders. <code>Capybara::Cuprite::Driver</code> accepts a <code>logger</code>, so you can save these as CI artifacts in case a test fails. Then just tell <em>your agent</em> to analyze it and you might just get lucky.</p>
<h3 id="persistence">Persistence</h3>
<p>When you have 100+ flaky tests, the whole thing seems overwhelming. See point 1 and remember - it will take time. Treat it as a marathon, take the easy wins, fix one test a week, and you will be done after 2 years. Having no flaky specs feels really good, and in case something new surfaces, the resistance to addressing it is much smaller.</p>
<p>I know some companies <em>just love</em> heroic efforts, but solving this isn&rsquo;t about a single weekend of slopfixing - it&rsquo;s just <em>work</em>, sometimes a bit tedious one.</p>]]></description><content:encoded><![CDATA[<p>Three years ago, I wrote about <a href="https://awesomeprogrammer.com/blog/2023/03/26/auto-ticketing-flaky-rspec-tests/">auto-ticketing flaky specs</a> (dear lord, please slow down time maybe a bit? kthx).</p>
<p>In the time since, I was loosely consulting on a small/medium Rails project that was struggling with some technical debt - one aspect of which was a flaky test suite. The project had 231 flaky test reports, around 6,600 unit tests, and 342 feature specs running on RSpec, Capybara, and Selenium-Chrome.</p>
<p>Eventually, it reached 0 (zero) flaky tests - which, I think, makes it the only &ldquo;production grade&rdquo; project with such metrics I have ever seen in my life. So, one might ask - how can I get there?</p>
<h3 id="time">Time</h3>
<p>It just takes time. Flaky unit tests are usually easy to reason about; the biggest chunk of work was solving various issues with feature specs, as that area is the Wild Wild West. For example, a rogue request from some container leaking into another test (there was a tusd container for uploads and a test that didn&rsquo;t wait properly for the upload to finish, meaning a webhook would fail a subsequent, random test). Other issues included custom smooth-scroll logic causing clicks on unintended elements, or JavaScript components not being mounted before an interaction with the page occurred - and the list goes on.</p>
<h3 id="trying-a-different-driver">Trying a different driver</h3>
<p>I tried using Ferrum (with the Cuprite driver). It won&rsquo;t auto-magically speed up your test suite, but using a different driver might surface different problems, as Ferrum speaks directly to the browser over a WebSocket (a huge gain since you don&rsquo;t have to install the right version of <code>chromedriver</code>).</p>
<p>For example, it turned out a lot of Stimulus-related tests now failed because the test tried to click on a button before the Stimulus controller managed to connect. In that case, I would suggest rendering such a problematic button as disabled and re-enabling it in the <code>connect()</code> method. In practice, this shouldn&rsquo;t be visible to the end-user, and Capybara will wait for the button to be enabled when doing <code>click_on</code>. That simple change fixes your test without any weird wait-for/sleep hacks.</p>
<p>Speaking of which - if you&rsquo;re explicitly waiting for some elements or interactions outside of Capybara&rsquo;s default wait behavior, there is a high chance you&rsquo;re doing something wrong, or maybe your UI lacks a UX interaction you can assert on.</p>
<p>In the end, the project stayed on Ferrum, as it also solved Selenium &ldquo;invalid session id&rdquo; exceptions, which turned out to be really difficult to debug in a CI environment.</p>
<h3 id="logging">Logging</h3>
<p>Some feature specs can feel impossible to debug. Ferrum offers really in-depth logs that you can shove into your favorite LLM for cross-referencing. In some cases, it can work wonders. <code>Capybara::Cuprite::Driver</code> accepts a <code>logger</code>, so you can save these as CI artifacts in case a test fails. Then just tell <em>your agent</em> to analyze it and you might just get lucky.</p>
<h3 id="persistence">Persistence</h3>
<p>When you have 100+ flaky tests, the whole thing seems overwhelming. See point 1 and remember - it will take time. Treat it as a marathon, take the easy wins, fix one test a week, and you will be done after 2 years. Having no flaky specs feels really good, and in case something new surfaces, the resistance to addressing it is much smaller.</p>
<p>I know some companies <em>just love</em> heroic efforts, but solving this isn&rsquo;t about a single weekend of slopfixing - it&rsquo;s just <em>work</em>, sometimes a bit tedious one.</p>]]></content:encoded></item><item><title>Ending the ARM life</title><link>https://awesomeprogrammer.com/blog/2026/02/12/ending-the-arm-life/</link><pubDate>Thu, 12 Feb 2026 19:05:59 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2026/02/12/ending-the-arm-life/</guid><description><![CDATA[<p>Once again I did <a href="/blog/2023/04/27/living-the-arm-life/">a full circle</a> and got back to amd64 architecture, technically replacing everything I setup around 3 years ago. <em>This time I will do it right</em>.</p>
<p>After Hetzner rolled out their <a href="https://www.hetzner.com/news/new-cloud-plans/">new cloud offering</a>, it seems like AMD on Hetzner is kinda dead end at this point (don&rsquo;t quote me on this). So I ended up in a weird situation where I had some old dangling ARM k3s node, some new PoC Coolify ARM-based server and it was time to put it all back together - obviously by setting up <em>a third</em> new project.</p>
<p><strong>What went right</strong></p>
<ul>
<li>
<p>having argocd + kustomize manifests + some ansible sprinkles for initial bootstrap allowed me to quickly re-create foundation of the infrastructure; once I had that, re-LLM-ing refreshed stack with ArgoCD in mind was fairly pleasant (as I only had to scream at the computer to fix those yamls instead of doing it myself)</p>
</li>
<li>
<p>I had to refresh one of my app as it was full of security holes, as usual best practices like <em><em>cough</em></em> test suite and whole CI/CD pipeline (which needed a nudge) made my life much easier</p>
</li>
<li>
<p>streamlining my <em>other</em> clusters prior to the move - I didn&rsquo;t have to think how to spin up basic stuff like redis-es or postgres-es</p>
</li>
<li>
<p>terraform for, you know, avoiding clickops</p>
</li>
</ul>
<p><strong>What went wrong</strong></p>
<ul>
<li>
<p>general software rot; surprise, surprise - not maintained stuff becomes a tangled mess; expired tokens, outdated keys, broken authorizations, dependencies that no longer exist; the resistance to even <em>start</em> thinking about it is huge</p>
</li>
<li>
<p>amount of dependencies, forked dependencies (what was I thinking 12 years ago?!), frontend dependencies in particular (I will live with Bootstrap 2 till the day I die); some hard to update/migrate software - e.g. had to give up <a href="/blog/2021/11/29/open-source-analytics-in-2021/">on plausible</a>, didn&rsquo;t want to deal with 2 -&gt; 3 upgrade, clickhouse - I just scraped everything and moved to <a href="https://github.com/umami-software/umami">umami</a></p>
</li>
<li>
<p>terraform; as I had like 3 different repos which managed different parts of infra, and everything eventually got out of date (went through local state, atlantis to terraform cloud route) - but that&rsquo;s on me</p>
</li>
<li>
<p>I made some weird decisions e.g. to host minio - which seems like <a href="https://www.reddit.com/r/selfhosted/comments/1pd97nq/minio_is_in_maintenance_mode_and_is_no_longer/">another dead end</a> - I don&rsquo;t recall if Cloudflare R2 wasn&rsquo;t <em>a thing</em> back then but I gladly moved few GBs of data I own there</p>
</li>
<li>
<p>coolify is not that cool; <a href="/blog/2025/11/24/just-learn-goddamn-kubernetes/">just use k3s</a></p>
</li>
</ul>]]></description><content:encoded><![CDATA[<p>Once again I did <a href="/blog/2023/04/27/living-the-arm-life/">a full circle</a> and got back to amd64 architecture, technically replacing everything I setup around 3 years ago. <em>This time I will do it right</em>.</p>
<p>After Hetzner rolled out their <a href="https://www.hetzner.com/news/new-cloud-plans/">new cloud offering</a>, it seems like AMD on Hetzner is kinda dead end at this point (don&rsquo;t quote me on this). So I ended up in a weird situation where I had some old dangling ARM k3s node, some new PoC Coolify ARM-based server and it was time to put it all back together - obviously by setting up <em>a third</em> new project.</p>
<p><strong>What went right</strong></p>
<ul>
<li>
<p>having argocd + kustomize manifests + some ansible sprinkles for initial bootstrap allowed me to quickly re-create foundation of the infrastructure; once I had that, re-LLM-ing refreshed stack with ArgoCD in mind was fairly pleasant (as I only had to scream at the computer to fix those yamls instead of doing it myself)</p>
</li>
<li>
<p>I had to refresh one of my app as it was full of security holes, as usual best practices like <em><em>cough</em></em> test suite and whole CI/CD pipeline (which needed a nudge) made my life much easier</p>
</li>
<li>
<p>streamlining my <em>other</em> clusters prior to the move - I didn&rsquo;t have to think how to spin up basic stuff like redis-es or postgres-es</p>
</li>
<li>
<p>terraform for, you know, avoiding clickops</p>
</li>
</ul>
<p><strong>What went wrong</strong></p>
<ul>
<li>
<p>general software rot; surprise, surprise - not maintained stuff becomes a tangled mess; expired tokens, outdated keys, broken authorizations, dependencies that no longer exist; the resistance to even <em>start</em> thinking about it is huge</p>
</li>
<li>
<p>amount of dependencies, forked dependencies (what was I thinking 12 years ago?!), frontend dependencies in particular (I will live with Bootstrap 2 till the day I die); some hard to update/migrate software - e.g. had to give up <a href="/blog/2021/11/29/open-source-analytics-in-2021/">on plausible</a>, didn&rsquo;t want to deal with 2 -&gt; 3 upgrade, clickhouse - I just scraped everything and moved to <a href="https://github.com/umami-software/umami">umami</a></p>
</li>
<li>
<p>terraform; as I had like 3 different repos which managed different parts of infra, and everything eventually got out of date (went through local state, atlantis to terraform cloud route) - but that&rsquo;s on me</p>
</li>
<li>
<p>I made some weird decisions e.g. to host minio - which seems like <a href="https://www.reddit.com/r/selfhosted/comments/1pd97nq/minio_is_in_maintenance_mode_and_is_no_longer/">another dead end</a> - I don&rsquo;t recall if Cloudflare R2 wasn&rsquo;t <em>a thing</em> back then but I gladly moved few GBs of data I own there</p>
</li>
<li>
<p>coolify is not that cool; <a href="/blog/2025/11/24/just-learn-goddamn-kubernetes/">just use k3s</a></p>
</li>
</ul>]]></content:encoded></item><item><title>I setup self-hosted Github runners and I hated it</title><link>https://awesomeprogrammer.com/blog/2026/01/27/i-setup-self-hosted-github-runners-and-i-hated-it/</link><pubDate>Tue, 27 Jan 2026 18:56:21 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2026/01/27/i-setup-self-hosted-github-runners-and-i-hated-it/</guid><description><![CDATA[<p>I wasted a few last weekends setting up a <a href="https://github.com/actions/actions-runner-controller">self-hosted runner set</a> on a Kubernetes cluster just to&hellip; that’s actually a good question - I’ve already forgotten why.</p>
<p>So, I have a fairly easy CircleCI configuration for my Rails app. There are a few jobs running on different containers that prepare the dependencies cache, followed by some linters. Then, there are a few parallelized jobs that run tests, a fan-in job that collects test coverage results, and uploads them to Codecov. There’s also an optional step that builds a Docker image, pushes it to ECR, and optionally triggers ArgoCD deployments.</p>
<p>It’s not much, but at the same time, it mimics a setup that you might call a &ldquo;production&rdquo; step. There’s caching involved, an automated repository that maintains a CI-relevant Docker image used by CircleCI, and overall, the &ldquo;waste&rdquo; in that pipeline is really minimal. CircleCI can also cache those containers on its end (depending on which VM I&rsquo;ll hit), which is also a nice addition. And Docker layer caching seems to mostly work.</p>
<p>I kind of like CircleCI as a product, but it has those periods where barely anything works; jobs hanging, never finishing. So, I decided to take a peek at the other side.</p>
<h3 id="getting-github-actions-production-ready">Getting GitHub Actions “production ready”</h3>
<p>Let&rsquo;s recap what I need from the CI:</p>
<ul>
<li>Aggressive caching of dependencies from different ecosystems</li>
<li>Docker layer caching for the Docker build step</li>
<li>The ability to parallelize a single job</li>
<li>(Nice to have) Test timing so I can distribute the tests based on historical execution time evenly</li>
</ul>
<h3 id="spoiler-alert">Spoiler alert</h3>
<p>In the end, I abandoned this experiment. In the meantime, GitHub <a href="https://github.com/resources/insights/2026-pricing-changes-for-github-actions">announced changes in the pricing model</a> for self-hosted runners (just to postpone it after backslash). I encountered numerous basic issues, such as the <a href="https://github.com/actions/runner/pull/1152">inability to provide container arguments</a>, it turned out even if you try to spin up two containers using same image it will fail to start Eventually, the entire setup stopped working because the <code>actions-runner</code> image I was using had become outdated and refused to accept any new jobs (TIL).</p>
<h3 id="caching">Caching</h3>
<p>Biggest PITA was caching everything - dependencies and docker layers. I had to hack together:</p>
<ol>
<li>
<p><a href="https://github.com/falcondev-oss/github-actions-cache-server">actions-cache-server</a> - which forces you to build <a href="https://gha-cache-server.falcondev.io/getting-started#_2-self-hosted-runner-setup">your own runner image</a> (but thankfully this whole even exists in the first place)</p>
</li>
<li>
<p>Customization of Docker images: you&rsquo;ll likely need to customize your existing Docker images to use the <code>runner</code> (<code>1001</code>) user and <code>/home/runner</code> work directory. Otherwise all hell breaks loose due to myriad of assumptions along the way</p>
</li>
<li>
<p>Docker Layer Caching: to cache Docker layers, you’ll need to spin up your Docker registry and make some crazy hacks within the action to force its usage. Here’s an example of how you can set up Docker Buildx:</p>
</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># NOTE: This spins up a new deployment in Kubernetes and you might need to patch the RBAC provided by ARC.</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Set up Docker Buildx</span>
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/setup-buildx-action@v3</span>
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">driver</span>: <span style="color:#ae81ff">kubernetes</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># my specific node selector and tolerations so pods land on CI nodes</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">driver-opts</span>: <span style="color:#ae81ff">|</span>
</span></span><span style="display:flex;"><span>		<span style="color:#e6db74">&#34;nodeselector=ci=true&#34;</span>,<span style="color:#e6db74">&#34;tolerations=key=ci,value=true,effect=NoSchedule&#34;</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#75715e"># I spin up basic registry, no auth, access from within cluster only</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">buildkitd-config-inline</span>: <span style="color:#ae81ff">|</span>
</span></span><span style="display:flex;"><span>		[<span style="color:#ae81ff">registry.&#34;docker-registry.docker-registry.svc.cluster.local:5000&#34;]</span>
</span></span><span style="display:flex;"><span>		  <span style="color:#ae81ff">http = true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ....</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build and push</span>
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/build-push-action@v6</span>
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">context</span>: <span style="color:#ae81ff">.</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">file</span>: <span style="color:#ae81ff">Dockerfile</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">push</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">tags</span>: <span style="color:#ae81ff">${{ steps.meta.outputs.tags }}</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">cache-from</span>: <span style="color:#ae81ff">type=registry,ref=docker-registry.docker-registry.svc.cluster.local:5000/my-app:latest</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">cache-to</span>: <span style="color:#ae81ff">type=registry,ref=docker-registry.docker-registry.svc.cluster.local:5000/my-app:latest,mode=max</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">provenance</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">DOCKER_BUILD_SUMMARY</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">DOCKER_BUILD_RECORD_UPLOAD</span>: <span style="color:#66d9ef">false</span>
</span></span></code></pre></div><h3 id="test-balancing">Test Balancing</h3>
<p>For splitting test execution based on timing, I found the best solution I could find: <a href="https://github.com/mtsmfm/split-test">split-test</a>. I just preinstalled it on my custom runner image. Assuming you&rsquo;ll keep timings only on the <code>main</code> branch, it should work pretty well.</p>
<h3 id="parallelization">Parallelization</h3>
<p>In general github actions provide out of the box support for job matrixes which works well. The ability to split CI config into smaller chunks is also a nice addition (along with <a href="https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows#creating-a-reusable-workflow">reusable workflows</a> syntax which is pretty neat once you wrap your head around it).</p>
<h3 id="conclusion">Conclusion</h3>
<p>As I like the Github&rsquo;s UI around actions and general dev experience when it comes to setting up <em>simple</em> workflows - self hosting it, trying to understand rather not ideal runners source code and all the quirks along the way was not fun. Next I will look into <a href="https://github.com/woodpecker-ci/woodpecker">woodpecker</a> because I really miss my Drone CI which I used to use a few years back (where all you had to do was setup two docker containers and things used to work 😆)</p>]]></description><content:encoded><![CDATA[<p>I wasted a few last weekends setting up a <a href="https://github.com/actions/actions-runner-controller">self-hosted runner set</a> on a Kubernetes cluster just to&hellip; that’s actually a good question - I’ve already forgotten why.</p>
<p>So, I have a fairly easy CircleCI configuration for my Rails app. There are a few jobs running on different containers that prepare the dependencies cache, followed by some linters. Then, there are a few parallelized jobs that run tests, a fan-in job that collects test coverage results, and uploads them to Codecov. There’s also an optional step that builds a Docker image, pushes it to ECR, and optionally triggers ArgoCD deployments.</p>
<p>It’s not much, but at the same time, it mimics a setup that you might call a &ldquo;production&rdquo; step. There’s caching involved, an automated repository that maintains a CI-relevant Docker image used by CircleCI, and overall, the &ldquo;waste&rdquo; in that pipeline is really minimal. CircleCI can also cache those containers on its end (depending on which VM I&rsquo;ll hit), which is also a nice addition. And Docker layer caching seems to mostly work.</p>
<p>I kind of like CircleCI as a product, but it has those periods where barely anything works; jobs hanging, never finishing. So, I decided to take a peek at the other side.</p>
<h3 id="getting-github-actions-production-ready">Getting GitHub Actions “production ready”</h3>
<p>Let&rsquo;s recap what I need from the CI:</p>
<ul>
<li>Aggressive caching of dependencies from different ecosystems</li>
<li>Docker layer caching for the Docker build step</li>
<li>The ability to parallelize a single job</li>
<li>(Nice to have) Test timing so I can distribute the tests based on historical execution time evenly</li>
</ul>
<h3 id="spoiler-alert">Spoiler alert</h3>
<p>In the end, I abandoned this experiment. In the meantime, GitHub <a href="https://github.com/resources/insights/2026-pricing-changes-for-github-actions">announced changes in the pricing model</a> for self-hosted runners (just to postpone it after backslash). I encountered numerous basic issues, such as the <a href="https://github.com/actions/runner/pull/1152">inability to provide container arguments</a>, it turned out even if you try to spin up two containers using same image it will fail to start Eventually, the entire setup stopped working because the <code>actions-runner</code> image I was using had become outdated and refused to accept any new jobs (TIL).</p>
<h3 id="caching">Caching</h3>
<p>Biggest PITA was caching everything - dependencies and docker layers. I had to hack together:</p>
<ol>
<li>
<p><a href="https://github.com/falcondev-oss/github-actions-cache-server">actions-cache-server</a> - which forces you to build <a href="https://gha-cache-server.falcondev.io/getting-started#_2-self-hosted-runner-setup">your own runner image</a> (but thankfully this whole even exists in the first place)</p>
</li>
<li>
<p>Customization of Docker images: you&rsquo;ll likely need to customize your existing Docker images to use the <code>runner</code> (<code>1001</code>) user and <code>/home/runner</code> work directory. Otherwise all hell breaks loose due to myriad of assumptions along the way</p>
</li>
<li>
<p>Docker Layer Caching: to cache Docker layers, you’ll need to spin up your Docker registry and make some crazy hacks within the action to force its usage. Here’s an example of how you can set up Docker Buildx:</p>
</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># NOTE: This spins up a new deployment in Kubernetes and you might need to patch the RBAC provided by ARC.</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Set up Docker Buildx</span>
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/setup-buildx-action@v3</span>
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">driver</span>: <span style="color:#ae81ff">kubernetes</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># my specific node selector and tolerations so pods land on CI nodes</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">driver-opts</span>: <span style="color:#ae81ff">|</span>
</span></span><span style="display:flex;"><span>		<span style="color:#e6db74">&#34;nodeselector=ci=true&#34;</span>,<span style="color:#e6db74">&#34;tolerations=key=ci,value=true,effect=NoSchedule&#34;</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#75715e"># I spin up basic registry, no auth, access from within cluster only</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">buildkitd-config-inline</span>: <span style="color:#ae81ff">|</span>
</span></span><span style="display:flex;"><span>		[<span style="color:#ae81ff">registry.&#34;docker-registry.docker-registry.svc.cluster.local:5000&#34;]</span>
</span></span><span style="display:flex;"><span>		  <span style="color:#ae81ff">http = true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ....</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build and push</span>
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/build-push-action@v6</span>
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">context</span>: <span style="color:#ae81ff">.</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">file</span>: <span style="color:#ae81ff">Dockerfile</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">push</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">tags</span>: <span style="color:#ae81ff">${{ steps.meta.outputs.tags }}</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">cache-from</span>: <span style="color:#ae81ff">type=registry,ref=docker-registry.docker-registry.svc.cluster.local:5000/my-app:latest</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">cache-to</span>: <span style="color:#ae81ff">type=registry,ref=docker-registry.docker-registry.svc.cluster.local:5000/my-app:latest,mode=max</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">provenance</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">DOCKER_BUILD_SUMMARY</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#f92672">DOCKER_BUILD_RECORD_UPLOAD</span>: <span style="color:#66d9ef">false</span>
</span></span></code></pre></div><h3 id="test-balancing">Test Balancing</h3>
<p>For splitting test execution based on timing, I found the best solution I could find: <a href="https://github.com/mtsmfm/split-test">split-test</a>. I just preinstalled it on my custom runner image. Assuming you&rsquo;ll keep timings only on the <code>main</code> branch, it should work pretty well.</p>
<h3 id="parallelization">Parallelization</h3>
<p>In general github actions provide out of the box support for job matrixes which works well. The ability to split CI config into smaller chunks is also a nice addition (along with <a href="https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows#creating-a-reusable-workflow">reusable workflows</a> syntax which is pretty neat once you wrap your head around it).</p>
<h3 id="conclusion">Conclusion</h3>
<p>As I like the Github&rsquo;s UI around actions and general dev experience when it comes to setting up <em>simple</em> workflows - self hosting it, trying to understand rather not ideal runners source code and all the quirks along the way was not fun. Next I will look into <a href="https://github.com/woodpecker-ci/woodpecker">woodpecker</a> because I really miss my Drone CI which I used to use a few years back (where all you had to do was setup two docker containers and things used to work 😆)</p>]]></content:encoded></item><item><title>Mysql operator on k3s</title><link>https://awesomeprogrammer.com/blog/2025/12/06/mysql-operator-on-k3s/</link><pubDate>Sat, 06 Dec 2025 10:58:59 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2025/12/06/mysql-operator-on-k3s/</guid><description><![CDATA[<p>Almost a year ago, I wrote <a href="/blog/2024/05/28/simple-mysql-8-replication-on-k8s/">about how to deploy a simple MySQL leader/follower setup</a> on Kubernetes.</p>
<p>It&rsquo;s been a while, and I have decided to look into &lsquo;operator&rsquo; solutions, which should be fully compatible with native MySQL implementations (MariaDB was out of the question because of <em>reasons</em>).</p>
<p>If you want something fully MySQL-compatible, you have two major options (I&rsquo;m not going to mention operators, which seem to be abandonware at this point):</p>
<ul>
<li><a href="https://github.com/percona/percona-server-mysql-operator">Percona Server MySQL Operator</a> - which went from technical preview to general availability fairly recently; it can run <code>percona-server-mysql</code>, which should be a drop-in MySQL 8.x replacement</li>
<li>official <a href="https://github.com/mysql/mysql-operator">MySQL-Operator</a> from Oracle - where you just run <em>official</em> MySQL database (8.x/9.x)</li>
</ul>
<p>I tested <code>MySQL-Operator</code> on a small testing cluster and decided to move my small workload after a month of battle testing. Migration was supposed to be a quick <em>20-minute adventure</em>.</p>
<figure><img src="/images/20-minutes-migration.jpg"/>
</figure>

<h3 id="migration-gotchas">Migration gotchas</h3>
<p>I wanted to clone data from an existing &amp; running MySQL instance. It turned out ARM won&rsquo;t be compatible with AMD. You have to run the same patch version of MySQL, but the architecture difference will cause clone init crash, and you will be forced to scrape your cluster and start over once you fix the problem. That really surprised me (prior to battle testing, all my nodes were running on ARM; I did some shuffling in-between). What I had to do is basically reschedule my existing MySQL to an AMD node as my operator was running on AMD CPUs.</p>
<p>Operator doesn&rsquo;t handle failure upon that initial <code>initDB.clone</code> nicely - once you try to restart it, it will just ignore clone/donor options - the best bet is to scrape the whole configuration (including volumes and any potential configs leftovers - important) and just start over.</p>
<p>Forming a cluster also takes a while - I have a really tiny workload (~12GB of pure data), but it took like a good few minutes for the cluster to establish and for the operator to finally spin up <code>mysql-router</code> pods - as establishing group replication means you have to duplicate that data 3 (or more, depending on your settings) times - for each MySQL pod.</p>
<h3 id="backup-gotchas">Backup gotchas</h3>
<p>MySQL shell dump utility still suffers from a <code>max_execution_time</code> timeout problem during backups, as it seems like there is still no obvious workaround for that.</p>
<p>You can:</p>
<ul>
<li>
<p>try to set <code>dumpInstance.dumpOptions</code> and, e.g. lower the <code>bytesPerChunk</code> value - beware that the official Helm chart&hellip; doesn&rsquo;t render at all those extra options, despite the fact that it&rsquo;s respected by the operator 🤦 so you will have to manually patch the <code>InnoDBCluster</code> resource</p>
</li>
<li>
<p>just increase/remove the timeout and move the execution timeout option logic to your app or <em>somewhere</em> (which is what I did in the end)</p>
</li>
</ul>
<h3 id="general-gotchas">General gotchas</h3>
<p>Some stuff is simply broken - e.g. <a href="https://github.com/mysql/mysql-operator/pull/36">unix socket for monitoring</a> is broken; <code>monitorSpec</code> is not being respected at all - I worked around those problems by using Argocd multiple sources and just overriding whole <code>ServiceMonitor</code> definition and its config. Seems like patches done by some random contributors come to the Oracle internal bug tracker to die, which is a pity and makes me question the future of this whole <em>official product</em>.</p>
<h3 id="operational-gotchas">Operational gotchas</h3>
<p>The operator doesn&rsquo;t observe changes in <code>podSpec</code>, which is quite frustrating as to change resource allocation, you will be patching your stateful set by hand, which seems like a half-baked way to operate your database instance.</p>
<h3 id="the-good-stuff">The good stuff</h3>
<p>Seems like mysql upgrades seems to be handled well - bumping <code>serverVersion</code> in helm values does a nice rolling upgrade on the cluster. In all fairness mysql was always pretty good at in-place upgrades (I&rsquo;m looking at you PostgreSQL).</p>
<p>All the heavy lifting related to managing group replication, leader election, setting up mysql-router, S3 backups seems to <em>just work</em> - which was a major selling point for me despite shortycomings mentioned above.</p>]]></description><content:encoded><![CDATA[<p>Almost a year ago, I wrote <a href="/blog/2024/05/28/simple-mysql-8-replication-on-k8s/">about how to deploy a simple MySQL leader/follower setup</a> on Kubernetes.</p>
<p>It&rsquo;s been a while, and I have decided to look into &lsquo;operator&rsquo; solutions, which should be fully compatible with native MySQL implementations (MariaDB was out of the question because of <em>reasons</em>).</p>
<p>If you want something fully MySQL-compatible, you have two major options (I&rsquo;m not going to mention operators, which seem to be abandonware at this point):</p>
<ul>
<li><a href="https://github.com/percona/percona-server-mysql-operator">Percona Server MySQL Operator</a> - which went from technical preview to general availability fairly recently; it can run <code>percona-server-mysql</code>, which should be a drop-in MySQL 8.x replacement</li>
<li>official <a href="https://github.com/mysql/mysql-operator">MySQL-Operator</a> from Oracle - where you just run <em>official</em> MySQL database (8.x/9.x)</li>
</ul>
<p>I tested <code>MySQL-Operator</code> on a small testing cluster and decided to move my small workload after a month of battle testing. Migration was supposed to be a quick <em>20-minute adventure</em>.</p>
<figure><img src="/images/20-minutes-migration.jpg"/>
</figure>

<h3 id="migration-gotchas">Migration gotchas</h3>
<p>I wanted to clone data from an existing &amp; running MySQL instance. It turned out ARM won&rsquo;t be compatible with AMD. You have to run the same patch version of MySQL, but the architecture difference will cause clone init crash, and you will be forced to scrape your cluster and start over once you fix the problem. That really surprised me (prior to battle testing, all my nodes were running on ARM; I did some shuffling in-between). What I had to do is basically reschedule my existing MySQL to an AMD node as my operator was running on AMD CPUs.</p>
<p>Operator doesn&rsquo;t handle failure upon that initial <code>initDB.clone</code> nicely - once you try to restart it, it will just ignore clone/donor options - the best bet is to scrape the whole configuration (including volumes and any potential configs leftovers - important) and just start over.</p>
<p>Forming a cluster also takes a while - I have a really tiny workload (~12GB of pure data), but it took like a good few minutes for the cluster to establish and for the operator to finally spin up <code>mysql-router</code> pods - as establishing group replication means you have to duplicate that data 3 (or more, depending on your settings) times - for each MySQL pod.</p>
<h3 id="backup-gotchas">Backup gotchas</h3>
<p>MySQL shell dump utility still suffers from a <code>max_execution_time</code> timeout problem during backups, as it seems like there is still no obvious workaround for that.</p>
<p>You can:</p>
<ul>
<li>
<p>try to set <code>dumpInstance.dumpOptions</code> and, e.g. lower the <code>bytesPerChunk</code> value - beware that the official Helm chart&hellip; doesn&rsquo;t render at all those extra options, despite the fact that it&rsquo;s respected by the operator 🤦 so you will have to manually patch the <code>InnoDBCluster</code> resource</p>
</li>
<li>
<p>just increase/remove the timeout and move the execution timeout option logic to your app or <em>somewhere</em> (which is what I did in the end)</p>
</li>
</ul>
<h3 id="general-gotchas">General gotchas</h3>
<p>Some stuff is simply broken - e.g. <a href="https://github.com/mysql/mysql-operator/pull/36">unix socket for monitoring</a> is broken; <code>monitorSpec</code> is not being respected at all - I worked around those problems by using Argocd multiple sources and just overriding whole <code>ServiceMonitor</code> definition and its config. Seems like patches done by some random contributors come to the Oracle internal bug tracker to die, which is a pity and makes me question the future of this whole <em>official product</em>.</p>
<h3 id="operational-gotchas">Operational gotchas</h3>
<p>The operator doesn&rsquo;t observe changes in <code>podSpec</code>, which is quite frustrating as to change resource allocation, you will be patching your stateful set by hand, which seems like a half-baked way to operate your database instance.</p>
<h3 id="the-good-stuff">The good stuff</h3>
<p>Seems like mysql upgrades seems to be handled well - bumping <code>serverVersion</code> in helm values does a nice rolling upgrade on the cluster. In all fairness mysql was always pretty good at in-place upgrades (I&rsquo;m looking at you PostgreSQL).</p>
<p>All the heavy lifting related to managing group replication, leader election, setting up mysql-router, S3 backups seems to <em>just work</em> - which was a major selling point for me despite shortycomings mentioned above.</p>]]></content:encoded></item><item><title>Just learn goddamn kubernetes</title><link>https://awesomeprogrammer.com/blog/2025/11/24/just-learn-goddamn-kubernetes/</link><pubDate>Mon, 24 Nov 2025 18:01:35 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2025/11/24/just-learn-goddamn-kubernetes/</guid><description><![CDATA[<p><em>Another click-baity headline, don&rsquo;t go, but hear me out, <a href="https://www.youtube.com/watch?v=XbGchfT3yMs">it&rsquo;s a good idea</a></em>.</p>
<p>Every now and then I try to look into &ldquo;simpler&rdquo; deployment options than my forgotten kubernetes cluster. I eventually abandoned <a href="https://awesomeprogrammer.com/blog/2021/12/10/new-infrastructure-recap/">nomad</a> for <a href="https://awesomeprogrammer.com/blog/2023/04/27/living-the-arm-life/">k3s</a> after running into constant pain from insufficient tooling. Then I forgot about that cluster for like 2 years and I&rsquo;m waiting for an epic crash which would force me to redo the whole thing again.</p>
<p>Fairly recently I looked into <a href="https://github.com/coollabsio/coolify">coolify</a>, happily deployed an app via <em>click ops</em> only to realize it <a href="https://github.com/coollabsio/coolify/discussions/3862">doesn&rsquo;t allow you to even scale a single container</a>. I had to do some weird app-on-app deployment, copy ENVs from one app to another just to spin up a background worker. There is a <a href="https://github.com/SierraJC/terraform-provider-coolify">terraform provider</a> which seems mostly blocked by insufficient APIs exposed by coolify.</p>
<p>On the other hand we have tools like <a href="https://github.com/basecamp/kamal">kamal</a> which seems to be a &ldquo;capistrano on docker&rdquo; solution (as per their <a href="https://kamal-deploy.org/">website</a>). I&rsquo;m quite confused why Basecamp decides to reinvent the wheel here using plain docker containers; they&rsquo;re even <a href="https://github.com/basecamp/kamal-proxy">writing their own proxy now?</a></p>
<p>Then there are a myriad of other solutions like <a href="https://github.com/swiftwave-org/swiftwave">swiftwave</a>, <a href="https://github.com/cozystack/cozystack">cozystack</a>, <a href="https://github.com/dokku/dokku/">dokku</a> or <a href="https://github.com/Dokploy/dokploy">dokploy</a>.</p>
<p>I think the whole deployment/orchestration management is a <em>really</em> difficult problem - that&rsquo;s why there are maybe so many solutions as in a way it&rsquo;s also an exciting problem. But IMHO? It&rsquo;s not a problem worth solving as it was already solved. k3s by stripping all cloud-related irrelevant stuff and baking in the <em>batteries</em> required to run &ldquo;The Actual Cluster&rdquo; makes kubernetes really approachable. This tiny distribution* allows you to do easy stuff, but also is flexible enough to adjust it for your own needs without a ton of hacks, duct tape and gum. Because really quickly you&rsquo;re going to run into basic problems of scaling stuff up and down, cronjobs, secrets/envs management, observability, zero downtime deployments, need for various one-shot jobs, processes, whatever else. That and more is already covered.</p>
<p>The biggest pro and possibly a con is that the kubernetes ecosystem is so goddamn huge - which <em>is</em> overwhelming at first. It&rsquo;s difficult to decide what&rsquo;s actually useful and what is not and it just indeed takes time to learn a bit. But that investment is worth it - as there&rsquo;s pretty much a guarantee that your problem was already solved by someone else. Boring? Maybe. Pragmatic? Definitely.</p>
<p><em>*Or you know, just pay some cloud/hosting provider to run cluster for you</em></p>]]></description><content:encoded><![CDATA[<p><em>Another click-baity headline, don&rsquo;t go, but hear me out, <a href="https://www.youtube.com/watch?v=XbGchfT3yMs">it&rsquo;s a good idea</a></em>.</p>
<p>Every now and then I try to look into &ldquo;simpler&rdquo; deployment options than my forgotten kubernetes cluster. I eventually abandoned <a href="https://awesomeprogrammer.com/blog/2021/12/10/new-infrastructure-recap/">nomad</a> for <a href="https://awesomeprogrammer.com/blog/2023/04/27/living-the-arm-life/">k3s</a> after running into constant pain from insufficient tooling. Then I forgot about that cluster for like 2 years and I&rsquo;m waiting for an epic crash which would force me to redo the whole thing again.</p>
<p>Fairly recently I looked into <a href="https://github.com/coollabsio/coolify">coolify</a>, happily deployed an app via <em>click ops</em> only to realize it <a href="https://github.com/coollabsio/coolify/discussions/3862">doesn&rsquo;t allow you to even scale a single container</a>. I had to do some weird app-on-app deployment, copy ENVs from one app to another just to spin up a background worker. There is a <a href="https://github.com/SierraJC/terraform-provider-coolify">terraform provider</a> which seems mostly blocked by insufficient APIs exposed by coolify.</p>
<p>On the other hand we have tools like <a href="https://github.com/basecamp/kamal">kamal</a> which seems to be a &ldquo;capistrano on docker&rdquo; solution (as per their <a href="https://kamal-deploy.org/">website</a>). I&rsquo;m quite confused why Basecamp decides to reinvent the wheel here using plain docker containers; they&rsquo;re even <a href="https://github.com/basecamp/kamal-proxy">writing their own proxy now?</a></p>
<p>Then there are a myriad of other solutions like <a href="https://github.com/swiftwave-org/swiftwave">swiftwave</a>, <a href="https://github.com/cozystack/cozystack">cozystack</a>, <a href="https://github.com/dokku/dokku/">dokku</a> or <a href="https://github.com/Dokploy/dokploy">dokploy</a>.</p>
<p>I think the whole deployment/orchestration management is a <em>really</em> difficult problem - that&rsquo;s why there are maybe so many solutions as in a way it&rsquo;s also an exciting problem. But IMHO? It&rsquo;s not a problem worth solving as it was already solved. k3s by stripping all cloud-related irrelevant stuff and baking in the <em>batteries</em> required to run &ldquo;The Actual Cluster&rdquo; makes kubernetes really approachable. This tiny distribution* allows you to do easy stuff, but also is flexible enough to adjust it for your own needs without a ton of hacks, duct tape and gum. Because really quickly you&rsquo;re going to run into basic problems of scaling stuff up and down, cronjobs, secrets/envs management, observability, zero downtime deployments, need for various one-shot jobs, processes, whatever else. That and more is already covered.</p>
<p>The biggest pro and possibly a con is that the kubernetes ecosystem is so goddamn huge - which <em>is</em> overwhelming at first. It&rsquo;s difficult to decide what&rsquo;s actually useful and what is not and it just indeed takes time to learn a bit. But that investment is worth it - as there&rsquo;s pretty much a guarantee that your problem was already solved by someone else. Boring? Maybe. Pragmatic? Definitely.</p>
<p><em>*Or you know, just pay some cloud/hosting provider to run cluster for you</em></p>]]></content:encoded></item><item><title>Cloudflare WARP in Github Codespaces</title><link>https://awesomeprogrammer.com/blog/2025/08/25/cloudflare-warp-in-github-codespaces/</link><pubDate>Mon, 25 Aug 2025 14:56:46 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2025/08/25/cloudflare-warp-in-github-codespaces/</guid><description><![CDATA[<p>Let&rsquo;s say you&rsquo;re going on a trip for few weeks and you prefer not to take your laptop with you. But you manage some small kubernetes cluster which is behind Cloudflare Zero Trust which can be only accessed:</p>
<ul>
<li>using <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/download-warp/">Cloudflare WARP</a></li>
<li>using google sign-in <a href="https://github.com/int128/kubelogin">via kubelogin</a></li>
</ul>
<p>And you want to have some sort of emergency access while you&rsquo;re away from your iPad.</p>
<p>You can do it using Github Codespaces, but whole process is not well documented, let&rsquo;s start with some references which were helpful when I glued all this hack together:</p>
<ul>
<li><a href="https://github.com/cmj2002/warp-docker/blob/main/entrypoint.sh">https://github.com/cmj2002/warp-docker/blob/main/entrypoint.sh</a></li>
<li><a href="https://community.cloudflare.com/t/how-to-register-into-a-team-with-linux-and-warp-cli/627971/3">https://community.cloudflare.com/t/how-to-register-into-a-team-with-linux-and-warp-cli/627971/3</a></li>
<li><a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/deployment/mdm-deployment/#linux">https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/deployment/mdm-deployment/#linux</a></li>
<li><a href="https://github.com/spotsnel/cloudflare-client/discussions/1">https://github.com/spotsnel/cloudflare-client/discussions/1</a></li>
<li><a href="https://community.cloudflare.com/t/cloudflare-zero-trust-for-service-token/432243">https://community.cloudflare.com/t/cloudflare-zero-trust-for-service-token/432243</a></li>
</ul>
<h2 id="first-what-you-wanna-do-is-to-configure-zero-trust">First what you wanna do is to configure zero trust</h2>
<p>You gonna need a service token:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_zero_trust_access_service_token&#34;</span> <span style="color:#e6db74">&#34;warp_codespaces_cli&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       = <span style="color:#e6db74">&#34;warp-cli-service-token&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">duration</span>   = <span style="color:#e6db74">&#34;forever&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A new policy:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_zero_trust_access_policy&#34;</span> <span style="color:#e6db74">&#34;warp_cli&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span>       = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>             = <span style="color:#e6db74">&#34;warp-enrollment-cli&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">session_duration</span> = <span style="color:#e6db74">&#34;24h&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">decision</span>         = <span style="color:#e6db74">&#34;non_identity&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">include</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">service_token</span> = [<span style="color:#a6e22e">cloudflare_zero_trust_access_service_token</span>.<span style="color:#a6e22e">warp_codespaces_cli</span>.<span style="color:#a6e22e">id</span>]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Moreover - you will need a to link this policy to your existing warp settings:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_zero_trust_access_application&#34;</span> <span style="color:#e6db74">&#34;warp&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span>           = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>                 = <span style="color:#e6db74">&#34;Warp Login App&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">domain</span>               = <span style="color:#e6db74">&#34;&lt;your domain&gt;.cloudflareaccess.com/warp&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">app_launcher_visible</span> = <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>                 = <span style="color:#e6db74">&#34;warp&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">policies</span> = [
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;</span> <span style="color:#a6e22e">any</span> <span style="color:#a6e22e">existing</span> <span style="color:#a6e22e">policies</span> <span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cloudflare_zero_trust_access_policy</span>.<span style="color:#a6e22e">warp_cli</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>So your <strong>Device enrollment permissions</strong> should look like this:</p>
<figure><img src="/images/cloudflare-warp-1.png"/>
</figure>

<p>I don&rsquo;t recall how I created profile settings for the CLI, but I think that was one thing I did manually:</p>
<figure><img src="/images/cloudflare-warp-2.png"/>
</figure>

<h2 id="then-you-need-some-stuff-in-devcontainer-directory">Then you need some stuff in <code>.devcontainer/</code> directory</h2>
<p><code>devcontainer.json</code> example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Development Container&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;build&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;dockerfile&#34;</span>: <span style="color:#e6db74">&#34;Dockerfile&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;context&#34;</span>: <span style="color:#e6db74">&#34;..&#34;</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;customizations&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;vscode&#34;</span>: {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;extensions&#34;</span>: [
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;redhat.ansible&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;ms-python.python&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;ms-azuretools.vscode-docker&#34;</span>
</span></span><span style="display:flex;"><span>            ],
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;settings&#34;</span>: {
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&#34;terminal.integrated.profiles.linux&#34;</span>: {
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">&#34;bash&#34;</span>: {
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">&#34;path&#34;</span>: <span style="color:#e6db74">&#34;/bin/bash&#34;</span>,
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">&#34;icon&#34;</span>: <span style="color:#e6db74">&#34;terminal-bash&#34;</span>
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                },
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&#34;terminal.integrated.defaultProfile.linux&#34;</span>: <span style="color:#e6db74">&#34;bash&#34;</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;workspaceFolder&#34;</span>: <span style="color:#e6db74">&#34;/workspaces&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;secrets&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;WARP_CLIENT_ID&#34;</span>: {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;WARP client id, provided by organization secret&#34;</span>
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;WARP_CLIENT_SECRET&#34;</span>: {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;WARP client secret, provided by organization secret&#34;</span>
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;OIDC_CLIENT_SECRET&#34;</span>: {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;OIDC client secret, provided by organization secret&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;capAdd&#34;</span>: [<span style="color:#e6db74">&#34;NET_ADMIN&#34;</span>],
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;remoteUser&#34;</span>: <span style="color:#e6db74">&#34;vscode&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>You will want to set secrets <a href="https://docs.github.com/en/codespaces/managing-your-codespaces/managing-your-account-specific-secrets-for-github-codespaces">in the codespaces settings</a>.  Warp id/secret is obtained from <code>warp_codespaces_cli</code> (if created via terraform). You can remove <code>OIDC</code> if you&rsquo;re not using kubelogin.</p>
<p>Then goes <code>Dockerfile</code> - I used python as I also had ansible in that repo:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> mcr.microsoft.com/devcontainers/python:3.13</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> apt-get update <span style="color:#f92672">&amp;&amp;</span> apt-get install -y --no-install-recommends <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    apt-transport-https <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    ca-certificates <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    curl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    gnupg <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    lsb-release <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    git <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    iputils-ping <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    iptables <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> rm -rf /var/lib/apt/lists/*<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># Install Cloudflare WARP client</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> curl https://pkg.cloudflareclient.com/pubkey.gpg | gpg --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> echo <span style="color:#e6db74">&#34;deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ </span><span style="color:#66d9ef">$(</span>lsb_release -cs<span style="color:#66d9ef">)</span><span style="color:#e6db74"> main&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>        &gt; /etc/apt/sources.list.d/cloudflare-client.list <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> apt-get update <span style="color:#f92672">&amp;&amp;</span> apt-get install -y cloudflare-warp <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> rm -rf /var/lib/apt/lists/*<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># Install kubectl and k9s</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ARG</span> KUBECTL_VERSION<span style="color:#f92672">=</span>v1.33.1<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ARG</span> K9S_VERSION<span style="color:#f92672">=</span>v0.50.9<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> set -eux; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    ARCH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>uname -m<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$ARCH<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;x86_64&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    KARCH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;amd64&#34;</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">elif</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$ARCH<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;aarch64&#34;</span> <span style="color:#f92672">]</span> <span style="color:#f92672">||</span> echo <span style="color:#e6db74">&#34;</span>$ARCH<span style="color:#e6db74">&#34;</span> | grep -q <span style="color:#e6db74">&#39;^arm&#39;</span>; <span style="color:#66d9ef">then</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    KARCH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;arm64&#34;</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">else</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    echo <span style="color:#e6db74">&#34;Unsupported architecture: </span>$ARCH<span style="color:#e6db74">&#34;</span>; exit 1; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">fi</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    curl -LO <span style="color:#e6db74">&#34;https://dl.k8s.io/release/</span><span style="color:#e6db74">${</span>KUBECTL_VERSION<span style="color:#e6db74">}</span><span style="color:#e6db74">/bin/linux/</span><span style="color:#e6db74">${</span>KARCH<span style="color:#e6db74">}</span><span style="color:#e6db74">/kubectl&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> install -o root -g root -m <span style="color:#ae81ff">0755</span> kubectl /usr/local/bin/kubectl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> rm kubectl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> curl -L <span style="color:#e6db74">&#34;https://github.com/derailed/k9s/releases/download/</span><span style="color:#e6db74">${</span>K9S_VERSION<span style="color:#e6db74">}</span><span style="color:#e6db74">/k9s_Linux_</span><span style="color:#e6db74">${</span>KARCH<span style="color:#e6db74">}</span><span style="color:#e6db74">.tar.gz&#34;</span> -o k9s.tar.gz <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> tar -zxvf k9s.tar.gz -C /usr/local/bin k9s <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> rm k9s.tar.gz<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">USER</span><span style="color:#e6db74"> vscode</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># Install krew and oidc-login plugin under vscode user</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ARG</span> KREW_VERSION<span style="color:#f92672">=</span>v0.4.5<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> ARCH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>uname -m<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$ARCH<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;x86_64&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    KARCH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;amd64&#34;</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">elif</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$ARCH<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;aarch64&#34;</span> <span style="color:#f92672">]</span> <span style="color:#f92672">||</span> echo <span style="color:#e6db74">&#34;</span>$ARCH<span style="color:#e6db74">&#34;</span> | grep -q <span style="color:#e6db74">&#39;^arm&#39;</span>; <span style="color:#66d9ef">then</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    KARCH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;arm64&#34;</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">else</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    echo <span style="color:#e6db74">&#34;Unsupported architecture: </span>$ARCH<span style="color:#e6db74">&#34;</span>; exit 1; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">fi</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    curl -L <span style="color:#e6db74">&#34;https://github.com/kubernetes-sigs/krew/releases/download/</span><span style="color:#e6db74">${</span>KREW_VERSION<span style="color:#e6db74">}</span><span style="color:#e6db74">/krew-linux_</span><span style="color:#e6db74">${</span>KARCH<span style="color:#e6db74">}</span><span style="color:#e6db74">.tar.gz&#34;</span> -o /tmp/krew.tar.gz <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> tar -zxvf /tmp/krew.tar.gz -C /tmp <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> /tmp/krew-linux_<span style="color:#e6db74">${</span>KARCH<span style="color:#e6db74">}</span> install krew <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> rm /tmp/krew.tar.gz <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> echo <span style="color:#e6db74">&#39;export PATH=&#34;${HOME}/.krew/bin:$PATH&#34;&#39;</span> &gt;&gt; ~/.bashrc <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> export PATH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>HOME<span style="color:#e6db74">}</span><span style="color:#e6db74">/.krew/bin:</span>$PATH<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> kubectl krew install oidc-login <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> rm -rf ~/.krew/tmp<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>That should give you a container with python, kubectl, k9s, Cloudflare WARP and kubelogin ready to go in Codespaces - fire it up and see!</p>
<p>Now what&rsquo;s left is to glue it all together, I decided to go with a simple bash script which fires all this machinery up:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> ! -e /dev/net/tun <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;- TUN device not found, creating...&#34;</span>
</span></span><span style="display:flex;"><span>    sudo mkdir -p /dev/net
</span></span><span style="display:flex;"><span>    sudo mknod /dev/net/tun c <span style="color:#ae81ff">10</span> <span style="color:#ae81ff">200</span>
</span></span><span style="display:flex;"><span>    sudo chmod <span style="color:#ae81ff">600</span> /dev/net/tun
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;- Starting dbus service...&#34;</span>
</span></span><span style="display:flex;"><span>sudo service dbus start
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># give dbus some time to start</span>
</span></span><span style="display:flex;"><span>sleep <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Check if envs are set</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -z <span style="color:#e6db74">&#34;</span>$WARP_CLIENT_ID<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span> <span style="color:#f92672">||</span> <span style="color:#f92672">[</span> -z <span style="color:#e6db74">&#34;</span>$WARP_CLIENT_SECRET<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;WARP_CLIENT_ID and WARP_CLIENT_SECRET must be set as environment variables.&#34;</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>file_content<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>cat <span style="color:#e6db74">&lt;&lt;EOF
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&lt;dict&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &lt;key&gt;organization&lt;/key&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &lt;string&gt;!!! --- PUT YOUR CLOUDFLARE ORG NAME HERE -- !!!&lt;/string&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &lt;key&gt;auth_client_id&lt;/key&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &lt;string&gt;${WARP_CLIENT_ID}&lt;/string&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &lt;key&gt;auth_client_secret&lt;/key&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &lt;string&gt;${WARP_CLIENT_SECRET}&lt;/string&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&lt;/dict&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">EOF</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>sudo mkdir -p /var/lib/cloudflare-warp
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;</span>$file_content<span style="color:#e6db74">&#34;</span> | sudo tee /var/lib/cloudflare-warp/mdm.xml &gt; /dev/null
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;- Starting Cloudflare WARP service (logs at /tmp/cloudflare-warp)...&#34;</span>
</span></span><span style="display:flex;"><span>sudo warp-svc --accept-tos &gt;/tmp/cloudflare-warp 2&gt;&amp;<span style="color:#ae81ff">1</span> &amp;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># give warp some time to start</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;- Waiting for Cloudflare WARP to start...&#34;</span>
</span></span><span style="display:flex;"><span>sleep <span style="color:#ae81ff">5</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;- Connecting to Cloudflare WARP...&#34;</span>
</span></span><span style="display:flex;"><span>warp-cli --accept-tos connect
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -z <span style="color:#e6db74">&#34;</span>$OIDC_CLIENT_SECRET<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;OIDC_CLIENT_SECRET must be set as an environment variable.&#34;</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I keep kube-config in my repo, reference below</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;- Configuring kube config...&#34;</span>
</span></span><span style="display:flex;"><span>mkdir -p ~/.kube
</span></span><span style="display:flex;"><span>envsubst &lt; .devcontainer/kube-config.yaml &gt; ~/.kube/config
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;All set! You can now access your Kubernetes cluster using k9s.&#34;</span>
</span></span></code></pre></div><p>Run that while in codespace&rsquo;s terminal and that will configure Cloudflare WARP and now you should be able to access your internal network from codespaces container which is pretty neat-o.</p>
<p>For reference my <code>kube-config.yaml</code> (truncated)</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">clusters</span>:
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">cluster</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">certificate-authority-data</span>: <span style="color:#ae81ff">Redacted</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server</span>: <span style="color:#ae81ff">https://&lt;internal network ip&gt;:6443</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">my-cluster</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">users</span>:
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">my-oidc</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">user</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">exec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">client.authentication.k8s.io/v1beta1</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">args</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">oidc-login</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">get-token</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">oidc-issuer-url=https://accounts.google.com</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">oidc-extra-scope=email</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">oidc-client-id=&lt;&lt;MY CLIENT ID&gt;&gt;</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">oidc-client-secret=${OIDC_CLIENT_SECRET}</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">grant-type=device-code</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">command</span>: <span style="color:#ae81ff">kubectl</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">env</span>: <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">interactiveMode</span>: <span style="color:#ae81ff">IfAvailable</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">provideClusterInfo</span>: <span style="color:#66d9ef">false</span>
</span></span></code></pre></div><p>Remember if you want to use <code>device-code</code> you need to configure oauth <a href="https://developers.google.com/identity/protocols/oauth2/limited-input-device">for limited input</a> - as there is no way to open a browser while in codespaces container, but PIN login still should work nicely.</p>
]]></description><content:encoded><![CDATA[<p>Let&rsquo;s say you&rsquo;re going on a trip for few weeks and you prefer not to take your laptop with you. But you manage some small kubernetes cluster which is behind Cloudflare Zero Trust which can be only accessed:</p>
<ul>
<li>using <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/download-warp/">Cloudflare WARP</a></li>
<li>using google sign-in <a href="https://github.com/int128/kubelogin">via kubelogin</a></li>
</ul>
<p>And you want to have some sort of emergency access while you&rsquo;re away from your iPad.</p>
<p>You can do it using Github Codespaces, but whole process is not well documented, let&rsquo;s start with some references which were helpful when I glued all this hack together:</p>
<ul>
<li><a href="https://github.com/cmj2002/warp-docker/blob/main/entrypoint.sh">https://github.com/cmj2002/warp-docker/blob/main/entrypoint.sh</a></li>
<li><a href="https://community.cloudflare.com/t/how-to-register-into-a-team-with-linux-and-warp-cli/627971/3">https://community.cloudflare.com/t/how-to-register-into-a-team-with-linux-and-warp-cli/627971/3</a></li>
<li><a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/deployment/mdm-deployment/#linux">https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/deployment/mdm-deployment/#linux</a></li>
<li><a href="https://github.com/spotsnel/cloudflare-client/discussions/1">https://github.com/spotsnel/cloudflare-client/discussions/1</a></li>
<li><a href="https://community.cloudflare.com/t/cloudflare-zero-trust-for-service-token/432243">https://community.cloudflare.com/t/cloudflare-zero-trust-for-service-token/432243</a></li>
</ul>
<h2 id="first-what-you-wanna-do-is-to-configure-zero-trust">First what you wanna do is to configure zero trust</h2>
<p>You gonna need a service token:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_zero_trust_access_service_token&#34;</span> <span style="color:#e6db74">&#34;warp_codespaces_cli&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       = <span style="color:#e6db74">&#34;warp-cli-service-token&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">duration</span>   = <span style="color:#e6db74">&#34;forever&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A new policy:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_zero_trust_access_policy&#34;</span> <span style="color:#e6db74">&#34;warp_cli&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span>       = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>             = <span style="color:#e6db74">&#34;warp-enrollment-cli&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">session_duration</span> = <span style="color:#e6db74">&#34;24h&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">decision</span>         = <span style="color:#e6db74">&#34;non_identity&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">include</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">service_token</span> = [<span style="color:#a6e22e">cloudflare_zero_trust_access_service_token</span>.<span style="color:#a6e22e">warp_codespaces_cli</span>.<span style="color:#a6e22e">id</span>]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Moreover - you will need a to link this policy to your existing warp settings:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_zero_trust_access_application&#34;</span> <span style="color:#e6db74">&#34;warp&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span>           = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>                 = <span style="color:#e6db74">&#34;Warp Login App&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">domain</span>               = <span style="color:#e6db74">&#34;&lt;your domain&gt;.cloudflareaccess.com/warp&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">app_launcher_visible</span> = <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>                 = <span style="color:#e6db74">&#34;warp&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">policies</span> = [
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;</span> <span style="color:#a6e22e">any</span> <span style="color:#a6e22e">existing</span> <span style="color:#a6e22e">policies</span> <span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cloudflare_zero_trust_access_policy</span>.<span style="color:#a6e22e">warp_cli</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>So your <strong>Device enrollment permissions</strong> should look like this:</p>
<figure><img src="/images/cloudflare-warp-1.png"/>
</figure>

<p>I don&rsquo;t recall how I created profile settings for the CLI, but I think that was one thing I did manually:</p>
<figure><img src="/images/cloudflare-warp-2.png"/>
</figure>

<h2 id="then-you-need-some-stuff-in-devcontainer-directory">Then you need some stuff in <code>.devcontainer/</code> directory</h2>
<p><code>devcontainer.json</code> example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Development Container&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;build&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;dockerfile&#34;</span>: <span style="color:#e6db74">&#34;Dockerfile&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;context&#34;</span>: <span style="color:#e6db74">&#34;..&#34;</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;customizations&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;vscode&#34;</span>: {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;extensions&#34;</span>: [
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;redhat.ansible&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;ms-python.python&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;ms-azuretools.vscode-docker&#34;</span>
</span></span><span style="display:flex;"><span>            ],
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;settings&#34;</span>: {
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&#34;terminal.integrated.profiles.linux&#34;</span>: {
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">&#34;bash&#34;</span>: {
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">&#34;path&#34;</span>: <span style="color:#e6db74">&#34;/bin/bash&#34;</span>,
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">&#34;icon&#34;</span>: <span style="color:#e6db74">&#34;terminal-bash&#34;</span>
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                },
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&#34;terminal.integrated.defaultProfile.linux&#34;</span>: <span style="color:#e6db74">&#34;bash&#34;</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;workspaceFolder&#34;</span>: <span style="color:#e6db74">&#34;/workspaces&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;secrets&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;WARP_CLIENT_ID&#34;</span>: {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;WARP client id, provided by organization secret&#34;</span>
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;WARP_CLIENT_SECRET&#34;</span>: {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;WARP client secret, provided by organization secret&#34;</span>
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;OIDC_CLIENT_SECRET&#34;</span>: {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;OIDC client secret, provided by organization secret&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;capAdd&#34;</span>: [<span style="color:#e6db74">&#34;NET_ADMIN&#34;</span>],
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;remoteUser&#34;</span>: <span style="color:#e6db74">&#34;vscode&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>You will want to set secrets <a href="https://docs.github.com/en/codespaces/managing-your-codespaces/managing-your-account-specific-secrets-for-github-codespaces">in the codespaces settings</a>.  Warp id/secret is obtained from <code>warp_codespaces_cli</code> (if created via terraform). You can remove <code>OIDC</code> if you&rsquo;re not using kubelogin.</p>
<p>Then goes <code>Dockerfile</code> - I used python as I also had ansible in that repo:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> mcr.microsoft.com/devcontainers/python:3.13</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> apt-get update <span style="color:#f92672">&amp;&amp;</span> apt-get install -y --no-install-recommends <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    apt-transport-https <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    ca-certificates <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    curl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    gnupg <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    lsb-release <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    git <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    iputils-ping <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    iptables <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> rm -rf /var/lib/apt/lists/*<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># Install Cloudflare WARP client</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> curl https://pkg.cloudflareclient.com/pubkey.gpg | gpg --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> echo <span style="color:#e6db74">&#34;deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ </span><span style="color:#66d9ef">$(</span>lsb_release -cs<span style="color:#66d9ef">)</span><span style="color:#e6db74"> main&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>        &gt; /etc/apt/sources.list.d/cloudflare-client.list <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> apt-get update <span style="color:#f92672">&amp;&amp;</span> apt-get install -y cloudflare-warp <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> rm -rf /var/lib/apt/lists/*<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># Install kubectl and k9s</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ARG</span> KUBECTL_VERSION<span style="color:#f92672">=</span>v1.33.1<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ARG</span> K9S_VERSION<span style="color:#f92672">=</span>v0.50.9<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> set -eux; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    ARCH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>uname -m<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$ARCH<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;x86_64&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    KARCH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;amd64&#34;</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">elif</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$ARCH<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;aarch64&#34;</span> <span style="color:#f92672">]</span> <span style="color:#f92672">||</span> echo <span style="color:#e6db74">&#34;</span>$ARCH<span style="color:#e6db74">&#34;</span> | grep -q <span style="color:#e6db74">&#39;^arm&#39;</span>; <span style="color:#66d9ef">then</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    KARCH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;arm64&#34;</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">else</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    echo <span style="color:#e6db74">&#34;Unsupported architecture: </span>$ARCH<span style="color:#e6db74">&#34;</span>; exit 1; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">fi</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    curl -LO <span style="color:#e6db74">&#34;https://dl.k8s.io/release/</span><span style="color:#e6db74">${</span>KUBECTL_VERSION<span style="color:#e6db74">}</span><span style="color:#e6db74">/bin/linux/</span><span style="color:#e6db74">${</span>KARCH<span style="color:#e6db74">}</span><span style="color:#e6db74">/kubectl&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> install -o root -g root -m <span style="color:#ae81ff">0755</span> kubectl /usr/local/bin/kubectl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> rm kubectl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> curl -L <span style="color:#e6db74">&#34;https://github.com/derailed/k9s/releases/download/</span><span style="color:#e6db74">${</span>K9S_VERSION<span style="color:#e6db74">}</span><span style="color:#e6db74">/k9s_Linux_</span><span style="color:#e6db74">${</span>KARCH<span style="color:#e6db74">}</span><span style="color:#e6db74">.tar.gz&#34;</span> -o k9s.tar.gz <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> tar -zxvf k9s.tar.gz -C /usr/local/bin k9s <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> rm k9s.tar.gz<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">USER</span><span style="color:#e6db74"> vscode</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e"># Install krew and oidc-login plugin under vscode user</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ARG</span> KREW_VERSION<span style="color:#f92672">=</span>v0.4.5<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> ARCH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>uname -m<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$ARCH<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;x86_64&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    KARCH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;amd64&#34;</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">elif</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$ARCH<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;aarch64&#34;</span> <span style="color:#f92672">]</span> <span style="color:#f92672">||</span> echo <span style="color:#e6db74">&#34;</span>$ARCH<span style="color:#e6db74">&#34;</span> | grep -q <span style="color:#e6db74">&#39;^arm&#39;</span>; <span style="color:#66d9ef">then</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    KARCH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;arm64&#34;</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">else</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    echo <span style="color:#e6db74">&#34;Unsupported architecture: </span>$ARCH<span style="color:#e6db74">&#34;</span>; exit 1; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#66d9ef">fi</span>; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    curl -L <span style="color:#e6db74">&#34;https://github.com/kubernetes-sigs/krew/releases/download/</span><span style="color:#e6db74">${</span>KREW_VERSION<span style="color:#e6db74">}</span><span style="color:#e6db74">/krew-linux_</span><span style="color:#e6db74">${</span>KARCH<span style="color:#e6db74">}</span><span style="color:#e6db74">.tar.gz&#34;</span> -o /tmp/krew.tar.gz <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> tar -zxvf /tmp/krew.tar.gz -C /tmp <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> /tmp/krew-linux_<span style="color:#e6db74">${</span>KARCH<span style="color:#e6db74">}</span> install krew <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> rm /tmp/krew.tar.gz <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> echo <span style="color:#e6db74">&#39;export PATH=&#34;${HOME}/.krew/bin:$PATH&#34;&#39;</span> &gt;&gt; ~/.bashrc <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> export PATH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>HOME<span style="color:#e6db74">}</span><span style="color:#e6db74">/.krew/bin:</span>$PATH<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> kubectl krew install oidc-login <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> rm -rf ~/.krew/tmp<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>That should give you a container with python, kubectl, k9s, Cloudflare WARP and kubelogin ready to go in Codespaces - fire it up and see!</p>
<p>Now what&rsquo;s left is to glue it all together, I decided to go with a simple bash script which fires all this machinery up:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> ! -e /dev/net/tun <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;- TUN device not found, creating...&#34;</span>
</span></span><span style="display:flex;"><span>    sudo mkdir -p /dev/net
</span></span><span style="display:flex;"><span>    sudo mknod /dev/net/tun c <span style="color:#ae81ff">10</span> <span style="color:#ae81ff">200</span>
</span></span><span style="display:flex;"><span>    sudo chmod <span style="color:#ae81ff">600</span> /dev/net/tun
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;- Starting dbus service...&#34;</span>
</span></span><span style="display:flex;"><span>sudo service dbus start
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># give dbus some time to start</span>
</span></span><span style="display:flex;"><span>sleep <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Check if envs are set</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -z <span style="color:#e6db74">&#34;</span>$WARP_CLIENT_ID<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span> <span style="color:#f92672">||</span> <span style="color:#f92672">[</span> -z <span style="color:#e6db74">&#34;</span>$WARP_CLIENT_SECRET<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;WARP_CLIENT_ID and WARP_CLIENT_SECRET must be set as environment variables.&#34;</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>file_content<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>cat <span style="color:#e6db74">&lt;&lt;EOF
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&lt;dict&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &lt;key&gt;organization&lt;/key&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &lt;string&gt;!!! --- PUT YOUR CLOUDFLARE ORG NAME HERE -- !!!&lt;/string&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &lt;key&gt;auth_client_id&lt;/key&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &lt;string&gt;${WARP_CLIENT_ID}&lt;/string&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &lt;key&gt;auth_client_secret&lt;/key&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &lt;string&gt;${WARP_CLIENT_SECRET}&lt;/string&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&lt;/dict&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">EOF</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>sudo mkdir -p /var/lib/cloudflare-warp
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;</span>$file_content<span style="color:#e6db74">&#34;</span> | sudo tee /var/lib/cloudflare-warp/mdm.xml &gt; /dev/null
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;- Starting Cloudflare WARP service (logs at /tmp/cloudflare-warp)...&#34;</span>
</span></span><span style="display:flex;"><span>sudo warp-svc --accept-tos &gt;/tmp/cloudflare-warp 2&gt;&amp;<span style="color:#ae81ff">1</span> &amp;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># give warp some time to start</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;- Waiting for Cloudflare WARP to start...&#34;</span>
</span></span><span style="display:flex;"><span>sleep <span style="color:#ae81ff">5</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;- Connecting to Cloudflare WARP...&#34;</span>
</span></span><span style="display:flex;"><span>warp-cli --accept-tos connect
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -z <span style="color:#e6db74">&#34;</span>$OIDC_CLIENT_SECRET<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;OIDC_CLIENT_SECRET must be set as an environment variable.&#34;</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I keep kube-config in my repo, reference below</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;- Configuring kube config...&#34;</span>
</span></span><span style="display:flex;"><span>mkdir -p ~/.kube
</span></span><span style="display:flex;"><span>envsubst &lt; .devcontainer/kube-config.yaml &gt; ~/.kube/config
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;All set! You can now access your Kubernetes cluster using k9s.&#34;</span>
</span></span></code></pre></div><p>Run that while in codespace&rsquo;s terminal and that will configure Cloudflare WARP and now you should be able to access your internal network from codespaces container which is pretty neat-o.</p>
<p>For reference my <code>kube-config.yaml</code> (truncated)</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">clusters</span>:
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">cluster</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">certificate-authority-data</span>: <span style="color:#ae81ff">Redacted</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server</span>: <span style="color:#ae81ff">https://&lt;internal network ip&gt;:6443</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">my-cluster</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">users</span>:
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">my-oidc</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">user</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">exec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">client.authentication.k8s.io/v1beta1</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">args</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">oidc-login</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">get-token</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">oidc-issuer-url=https://accounts.google.com</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">oidc-extra-scope=email</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">oidc-client-id=&lt;&lt;MY CLIENT ID&gt;&gt;</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">oidc-client-secret=${OIDC_CLIENT_SECRET}</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">grant-type=device-code</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">command</span>: <span style="color:#ae81ff">kubectl</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">env</span>: <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">interactiveMode</span>: <span style="color:#ae81ff">IfAvailable</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">provideClusterInfo</span>: <span style="color:#66d9ef">false</span>
</span></span></code></pre></div><p>Remember if you want to use <code>device-code</code> you need to configure oauth <a href="https://developers.google.com/identity/protocols/oauth2/limited-input-device">for limited input</a> - as there is no way to open a browser while in codespaces container, but PIN login still should work nicely.</p>
]]></content:encoded></item><item><title>My first remote gig</title><link>https://awesomeprogrammer.com/blog/2025/06/05/my-first-remote-gig/</link><pubDate>Thu, 05 Jun 2025 19:47:19 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2025/06/05/my-first-remote-gig/</guid><description><![CDATA[<p>Rollback to July 2003. I&rsquo;m something like 17 years old, and I play video games. I mostly play Quake 3. And <em>mostly</em> Quake 3 with a <a href="https://en.wikipedia.org/wiki/DeFRaG">DeFRaG mod</a>.</p>
<p>We - and by we - I mean me with a group of random internet folks - just released something called &ldquo;Polish Defrag MAPpack no.1&rdquo; - 9 maps made by 9 different folks. I met those randos online on an internet forum located under <code>q3arena.gry.wp.pl</code> - a site which was moved around between domains since then, but that&rsquo;s not the point.</p>
<p>Then we did that again - in March 2004, we released part 2 - 9 maps from 9 different folks.</p>
<p>And the last time we did it in October 2005 (took us more than a year to wrap it up) - again, 9 different folks released 9 different maps. At this point, I don&rsquo;t recall if the &ldquo;nine&rdquo; was a coincidence or if we had some hard limit of how many folks could contribute. Probably the first one.</p>
<p>In total, 16 people contributed to 27 maps over a span of 2 years. Those are the nicknames of those randos which I have never met in real life (and the number of maps created by them):</p>
<ul>
<li>Pea_IMPOSTOR x2</li>
<li>ImOoR x1</li>
<li>gorat x1</li>
<li>Progner x1</li>
<li>RuSHeR x2</li>
<li>T4m8uryn0 x3</li>
<li>vic x1</li>
<li>dAde x2</li>
<li>NemiX x1</li>
<li>SzakaL x2</li>
<li>Tek x2</li>
<li>TymoN x2</li>
<li>Yacek x2</li>
<li>Marko x1</li>
<li>DyZi0 x1</li>
<li>and me x3</li>
</ul>
<figure><img src="/images/pdm01.jpg"/><figcaption>
            <h4>First pack</h4>
        </figcaption>
</figure>

<figure><img src="/images/pdm02.jpg"/><figcaption>
            <h4>Second pack</h4>
        </figcaption>
</figure>

<figure><img src="/images/pdm03.jpg"/><figcaption>
            <h4>Third, last pack - seems I was trying to find my inner artist ;) (I did the covers on all packs) </h4>
        </figcaption>
</figure>

<p>It all happened when I spoke with T4m8uryn0, gave him an idea, he started a single thread on the forum, and then it took off on its own. I never thought about this much.</p>
<p>It was 2003, and yet we managed to collaborate fully async on the private forum on this project - I <em>think</em> we moved to a private forum eventually; it seems it is still online and holds 527 posts and 19 topics - I think we had some huge single public discussion before that. On top, we used IRC from time to time for some real-time interactions. We provided feedback, ran tests on early versions of the maps, created a unified visual identity of map screenshots, and eventually managed to ship something in a self-organized manner. All that twenty-two freaking years ago with a total of zero video calls - I didn&rsquo;t even knew how those people looked in real life.</p>
<p>In a perspective I find it kinda amazing - we were teenagers, sometimes still kids - and it was - in fact - my very first exposure to a remote <em>work</em> (or rather collaboration) - I just didn&rsquo;t know it was called like that.</p>
<p>As said - I never managed to meet with any of those folks; we were spread out across whole Poland, a random group of geeks who loved to play very weird Quake 3 modification who decided to make some maps. It&rsquo;s a pity that Quake was losing its playerbase and the whole forum went dead not so long after. <em>And now I wonder how those people are doing.</em></p>
<p>The majority of the content from that era is gone and lost forever, I still host <a href="https://quake.net.pl">quake.net.pl</a> (which I eventually rewrote from PHP as my first Ruby on Rails project). There are some videos on YT but for some reason finding it is extremely hard - <a href="https://www.youtube.com/watch?v=dc15QNafXzU&amp;list=PLwJPKGDNY4xj6yaQS4nryKxwsAdVx4p23">thus I created a playlist</a> for anyone who was part of <em>the scene</em> who wishes to take a trip back memory lane.</p>]]></description><content:encoded><![CDATA[<p>Rollback to July 2003. I&rsquo;m something like 17 years old, and I play video games. I mostly play Quake 3. And <em>mostly</em> Quake 3 with a <a href="https://en.wikipedia.org/wiki/DeFRaG">DeFRaG mod</a>.</p>
<p>We - and by we - I mean me with a group of random internet folks - just released something called &ldquo;Polish Defrag MAPpack no.1&rdquo; - 9 maps made by 9 different folks. I met those randos online on an internet forum located under <code>q3arena.gry.wp.pl</code> - a site which was moved around between domains since then, but that&rsquo;s not the point.</p>
<p>Then we did that again - in March 2004, we released part 2 - 9 maps from 9 different folks.</p>
<p>And the last time we did it in October 2005 (took us more than a year to wrap it up) - again, 9 different folks released 9 different maps. At this point, I don&rsquo;t recall if the &ldquo;nine&rdquo; was a coincidence or if we had some hard limit of how many folks could contribute. Probably the first one.</p>
<p>In total, 16 people contributed to 27 maps over a span of 2 years. Those are the nicknames of those randos which I have never met in real life (and the number of maps created by them):</p>
<ul>
<li>Pea_IMPOSTOR x2</li>
<li>ImOoR x1</li>
<li>gorat x1</li>
<li>Progner x1</li>
<li>RuSHeR x2</li>
<li>T4m8uryn0 x3</li>
<li>vic x1</li>
<li>dAde x2</li>
<li>NemiX x1</li>
<li>SzakaL x2</li>
<li>Tek x2</li>
<li>TymoN x2</li>
<li>Yacek x2</li>
<li>Marko x1</li>
<li>DyZi0 x1</li>
<li>and me x3</li>
</ul>
<figure><img src="/images/pdm01.jpg"/><figcaption>
            <h4>First pack</h4>
        </figcaption>
</figure>

<figure><img src="/images/pdm02.jpg"/><figcaption>
            <h4>Second pack</h4>
        </figcaption>
</figure>

<figure><img src="/images/pdm03.jpg"/><figcaption>
            <h4>Third, last pack - seems I was trying to find my inner artist ;) (I did the covers on all packs) </h4>
        </figcaption>
</figure>

<p>It all happened when I spoke with T4m8uryn0, gave him an idea, he started a single thread on the forum, and then it took off on its own. I never thought about this much.</p>
<p>It was 2003, and yet we managed to collaborate fully async on the private forum on this project - I <em>think</em> we moved to a private forum eventually; it seems it is still online and holds 527 posts and 19 topics - I think we had some huge single public discussion before that. On top, we used IRC from time to time for some real-time interactions. We provided feedback, ran tests on early versions of the maps, created a unified visual identity of map screenshots, and eventually managed to ship something in a self-organized manner. All that twenty-two freaking years ago with a total of zero video calls - I didn&rsquo;t even knew how those people looked in real life.</p>
<p>In a perspective I find it kinda amazing - we were teenagers, sometimes still kids - and it was - in fact - my very first exposure to a remote <em>work</em> (or rather collaboration) - I just didn&rsquo;t know it was called like that.</p>
<p>As said - I never managed to meet with any of those folks; we were spread out across whole Poland, a random group of geeks who loved to play very weird Quake 3 modification who decided to make some maps. It&rsquo;s a pity that Quake was losing its playerbase and the whole forum went dead not so long after. <em>And now I wonder how those people are doing.</em></p>
<p>The majority of the content from that era is gone and lost forever, I still host <a href="https://quake.net.pl">quake.net.pl</a> (which I eventually rewrote from PHP as my first Ruby on Rails project). There are some videos on YT but for some reason finding it is extremely hard - <a href="https://www.youtube.com/watch?v=dc15QNafXzU&amp;list=PLwJPKGDNY4xj6yaQS4nryKxwsAdVx4p23">thus I created a playlist</a> for anyone who was part of <em>the scene</em> who wishes to take a trip back memory lane.</p>]]></content:encoded></item><item><title>Rails turbo form's CSRF token</title><link>https://awesomeprogrammer.com/blog/2025/03/30/rails-turbo-forms-csrf-token/</link><pubDate>Sun, 30 Mar 2025 13:52:04 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2025/03/30/rails-turbo-forms-csrf-token/</guid><description><![CDATA[<p>You might be surprised by Rails&rsquo; behavior when rendering a partial via Turbo.</p>
<p>There is no session context - thus <code>form_tag</code> won&rsquo;t render the &ldquo;authenticity token&rdquo; hidden field - it will silently omit it. This behavior was changed <a href="https://github.com/rails/rails/pull/43427/files">around 2021</a>, and you can check out <a href="https://github.com/hotwired/turbo-rails/issues/243">this lengthy discussion</a> about the problem.</p>
<p>Rails Turbo will <a href="https://github.com/hotwired/turbo-rails/blob/v2.0.13/app/assets/javascripts/turbo.js#L983-L993">automagically attach the CSRF token to the request</a> obtained from a meta tag:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#a6e22e">prepareRequest</span>(<span style="color:#a6e22e">request</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">request</span>.<span style="color:#a6e22e">isSafe</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">token</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">getCookieValue</span>(<span style="color:#a6e22e">getMetaContent</span>(<span style="color:#e6db74">&#34;csrf-param&#34;</span>)) <span style="color:#f92672">||</span> <span style="color:#a6e22e">getMetaContent</span>(<span style="color:#e6db74">&#34;csrf-token&#34;</span>);
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">token</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">request</span>.<span style="color:#a6e22e">headers</span>[<span style="color:#e6db74">&#34;X-CSRF-Token&#34;</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">token</span>;
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">requestAcceptsTurboStreamResponse</span>(<span style="color:#a6e22e">request</span>)) {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">request</span>.<span style="color:#a6e22e">acceptResponseType</span>(<span style="color:#a6e22e">StreamMessage</span>.<span style="color:#a6e22e">contentType</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span></code></pre></div><p>So if you render a form via Turbo which you intend to submit further <em>without</em> Turbo, you will need to re-attach the CSRF token in a similar manner. You can use a Stimulus controller as shown in the example below:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Controller</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;@hotwired/stimulus&#34;</span>;
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">import</span> <span style="color:#a6e22e">Rails</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;@rails/ujs&#34;</span>;
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">class</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Controller</span> {
</span></span><span style="display:flex;"><span>   <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">csrfParam</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">Rails</span>.<span style="color:#a6e22e">csrfParam</span>();
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span>   <span style="color:#a6e22e">connect</span>() {
</span></span><span style="display:flex;"><span>     <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">form</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">element</span>;
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span>     <span style="color:#75715e">// assuming page can be refreshed by hard refresh and form will by
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>     <span style="color:#75715e">// rendered with the session context - thus with csrf token
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>     <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">form</span>.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">`input[name=&#39;</span><span style="color:#e6db74">${</span><span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">csrfParam</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;]`</span>) <span style="color:#f92672">===</span> <span style="color:#66d9ef">null</span>) {
</span></span><span style="display:flex;"><span>       <span style="color:#a6e22e">form</span>.<span style="color:#a6e22e">appendChild</span>(<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">authenticityTokenInput</span>);
</span></span><span style="display:flex;"><span>     }
</span></span><span style="display:flex;"><span>   }
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span>   <span style="color:#a6e22e">get</span> <span style="color:#a6e22e">authenticityTokenInput</span>() {
</span></span><span style="display:flex;"><span>     <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">input</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">createElement</span>(<span style="color:#e6db74">&#34;input&#34;</span>);
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">type</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;hidden&#34;</span>;
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">csrfParam</span>;
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">autocomplete</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;off&#34;</span>;
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">Rails</span>.<span style="color:#a6e22e">csrfToken</span>();
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span>     <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">input</span>;
</span></span><span style="display:flex;"><span>   }
</span></span><span style="display:flex;"><span> }
</span></span></code></pre></div><p>Then you attach this controller to your <code>form</code> via <code>data-controller</code> (the Stimulus way).</p>]]></description><content:encoded><![CDATA[<p>You might be surprised by Rails&rsquo; behavior when rendering a partial via Turbo.</p>
<p>There is no session context - thus <code>form_tag</code> won&rsquo;t render the &ldquo;authenticity token&rdquo; hidden field - it will silently omit it. This behavior was changed <a href="https://github.com/rails/rails/pull/43427/files">around 2021</a>, and you can check out <a href="https://github.com/hotwired/turbo-rails/issues/243">this lengthy discussion</a> about the problem.</p>
<p>Rails Turbo will <a href="https://github.com/hotwired/turbo-rails/blob/v2.0.13/app/assets/javascripts/turbo.js#L983-L993">automagically attach the CSRF token to the request</a> obtained from a meta tag:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#a6e22e">prepareRequest</span>(<span style="color:#a6e22e">request</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">request</span>.<span style="color:#a6e22e">isSafe</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">token</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">getCookieValue</span>(<span style="color:#a6e22e">getMetaContent</span>(<span style="color:#e6db74">&#34;csrf-param&#34;</span>)) <span style="color:#f92672">||</span> <span style="color:#a6e22e">getMetaContent</span>(<span style="color:#e6db74">&#34;csrf-token&#34;</span>);
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">token</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">request</span>.<span style="color:#a6e22e">headers</span>[<span style="color:#e6db74">&#34;X-CSRF-Token&#34;</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">token</span>;
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">requestAcceptsTurboStreamResponse</span>(<span style="color:#a6e22e">request</span>)) {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">request</span>.<span style="color:#a6e22e">acceptResponseType</span>(<span style="color:#a6e22e">StreamMessage</span>.<span style="color:#a6e22e">contentType</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span></code></pre></div><p>So if you render a form via Turbo which you intend to submit further <em>without</em> Turbo, you will need to re-attach the CSRF token in a similar manner. You can use a Stimulus controller as shown in the example below:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Controller</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;@hotwired/stimulus&#34;</span>;
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">import</span> <span style="color:#a6e22e">Rails</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;@rails/ujs&#34;</span>;
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">class</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Controller</span> {
</span></span><span style="display:flex;"><span>   <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">csrfParam</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">Rails</span>.<span style="color:#a6e22e">csrfParam</span>();
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span>   <span style="color:#a6e22e">connect</span>() {
</span></span><span style="display:flex;"><span>     <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">form</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">element</span>;
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span>     <span style="color:#75715e">// assuming page can be refreshed by hard refresh and form will by
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>     <span style="color:#75715e">// rendered with the session context - thus with csrf token
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>     <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">form</span>.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">`input[name=&#39;</span><span style="color:#e6db74">${</span><span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">csrfParam</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;]`</span>) <span style="color:#f92672">===</span> <span style="color:#66d9ef">null</span>) {
</span></span><span style="display:flex;"><span>       <span style="color:#a6e22e">form</span>.<span style="color:#a6e22e">appendChild</span>(<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">authenticityTokenInput</span>);
</span></span><span style="display:flex;"><span>     }
</span></span><span style="display:flex;"><span>   }
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span>   <span style="color:#a6e22e">get</span> <span style="color:#a6e22e">authenticityTokenInput</span>() {
</span></span><span style="display:flex;"><span>     <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">input</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">createElement</span>(<span style="color:#e6db74">&#34;input&#34;</span>);
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">type</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;hidden&#34;</span>;
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">csrfParam</span>;
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">autocomplete</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;off&#34;</span>;
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">Rails</span>.<span style="color:#a6e22e">csrfToken</span>();
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span>     <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">input</span>;
</span></span><span style="display:flex;"><span>   }
</span></span><span style="display:flex;"><span> }
</span></span></code></pre></div><p>Then you attach this controller to your <code>form</code> via <code>data-controller</code> (the Stimulus way).</p>]]></content:encoded></item><item><title>Solving k3s DNS warning</title><link>https://awesomeprogrammer.com/blog/2024/11/30/solving-k3s-dns-warning/</link><pubDate>Sat, 30 Nov 2024 08:42:27 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2024/11/30/solving-k3s-dns-warning/</guid><description><![CDATA[<blockquote>
<p>Nameserver limits were exceeded, some nameservers have been omitted.</p>
</blockquote>
<p>Have you ever run into such errors on your k3s cluster?</p>
<p>The problem was already mentioned in a few places:</p>
<ul>
<li><a href="https://vadosware.io/post/sometimes-the-problem-is-dns-on-hetzner/">https://vadosware.io/post/sometimes-the-problem-is-dns-on-hetzner/</a></li>
<li><a href="https://github.com/syself/cluster-api-provider-hetzner/issues/77">https://github.com/syself/cluster-api-provider-hetzner/issues/77</a></li>
</ul>
<p>Some folks suggest modifying <code>etc/resolved.conf</code>, but as my node shipped with <code>netplan</code> (and I didn&rsquo;t want to deal with that, I trust my hosting provider can handle networking), I decided to create a dedicated resolv config for <code>kubelet</code> needs.</p>
<p>It&rsquo;s mentioned in the <a href="https://kubernetes.io/docs/tasks/administer-cluster/dns-custom-nameservers/#introduction">official docs</a></p>
<p>Technically, you just create a file like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span># /etc/resolv.conf.kubelet
</span></span><span style="display:flex;"><span>nameserver 1.1.1.1
</span></span><span style="display:flex;"><span>nameserver 2606:4700:4700::1111
</span></span></code></pre></div><p>And configure your <a href="https://docs.k3s.io/cli/agent#customized-flags">pass extra kublet arg</a> to your k3s agent:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>--resolv-conf<span style="color:#f92672">=</span>/etc/resolv.conf.kubelet
</span></span></code></pre></div><p>And that should solve this quite annoying warning message as now you will have two nameservers entries with is within kubernetes limits.</p>]]></description><content:encoded><![CDATA[<blockquote>
<p>Nameserver limits were exceeded, some nameservers have been omitted.</p>
</blockquote>
<p>Have you ever run into such errors on your k3s cluster?</p>
<p>The problem was already mentioned in a few places:</p>
<ul>
<li><a href="https://vadosware.io/post/sometimes-the-problem-is-dns-on-hetzner/">https://vadosware.io/post/sometimes-the-problem-is-dns-on-hetzner/</a></li>
<li><a href="https://github.com/syself/cluster-api-provider-hetzner/issues/77">https://github.com/syself/cluster-api-provider-hetzner/issues/77</a></li>
</ul>
<p>Some folks suggest modifying <code>etc/resolved.conf</code>, but as my node shipped with <code>netplan</code> (and I didn&rsquo;t want to deal with that, I trust my hosting provider can handle networking), I decided to create a dedicated resolv config for <code>kubelet</code> needs.</p>
<p>It&rsquo;s mentioned in the <a href="https://kubernetes.io/docs/tasks/administer-cluster/dns-custom-nameservers/#introduction">official docs</a></p>
<p>Technically, you just create a file like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span># /etc/resolv.conf.kubelet
</span></span><span style="display:flex;"><span>nameserver 1.1.1.1
</span></span><span style="display:flex;"><span>nameserver 2606:4700:4700::1111
</span></span></code></pre></div><p>And configure your <a href="https://docs.k3s.io/cli/agent#customized-flags">pass extra kublet arg</a> to your k3s agent:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>--resolv-conf<span style="color:#f92672">=</span>/etc/resolv.conf.kubelet
</span></span></code></pre></div><p>And that should solve this quite annoying warning message as now you will have two nameservers entries with is within kubernetes limits.</p>]]></content:encoded></item><item><title>How to monitor your self-hosted Sentry instance</title><link>https://awesomeprogrammer.com/blog/2024/11/17/how-to-monitor-your-self-hosted-sentry-instance/</link><pubDate>Sun, 17 Nov 2024 10:30:58 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2024/11/17/how-to-monitor-your-self-hosted-sentry-instance/</guid><description><![CDATA[<p>I love Sentry since I discovered it many years ago. Back in the days, self-hosting it was really easy - a single Docker image which you would use for spinning up like 2-3 separate services, Postgres, Redis, a few lines of config, and you&rsquo;re done.</p>
<p>Nowadays (2024), self-hosting Sentry requires spinning up 50+ different services - and that is of course without any fancy HA setup. It&rsquo;s still doable, <a href="https://github.com/sentry-kubernetes/charts">especially with Kubernetes</a>, but the learning curve is definitely more steep. Then again, the feature set of Sentry itself is much richer - it&rsquo;s not about just catching errors anymore; you have full-fledged build-in performance monitoring, session recording, and tons of other observation-related goodies.</p>
<p>One thing is kinda tricky is monitoring of your Sentry instance. Here is how I do it.</p>
<h3 id="use-built-in-endpoints">Use built-in endpoints</h3>
<ul>
<li><code>/_health/</code> - to ensure your <em>web</em> is working</li>
<li><code>/api/relay/healthcheck/live/</code> - to ensure your <a href="https://github.com/getsentry/relay">Relay</a> is alive</li>
<li><code>/api/relay/healthcheck/ready/</code> - to ensure your Relay is ready for events ingestion</li>
</ul>
<h3 id="and-its-not-enough">…and it&rsquo;s not enough</h3>
<p>I ran into a situation where <code>Relay</code> went into some reconnecting spree (despite being live/ready) - at the moment I don&rsquo;t remember the exact root cause, but it took me a while to realize that events are <em>not</em> being properly ingested. In the end, I decided to configure a cronjob which would trigger a Sentry event (exception) to one of my projects every ~hour. Then I muted that exception as obviously it was not actionable.</p>
<p>Once you have that exception <em>in</em> Sentry, it&rsquo;s time to check if it&rsquo;s being <em>registered</em> every hour.</p>
<p>Sentry exposes <a href="https://docs.sentry.io/api/events/retrieve-an-issue/">API endpoint</a> where you can obtain issue details; one interesting part in that JSON response is <code>lastEvent</code> - you can query this endpoint every ~hour, parse the response, ensure it&rsquo;s no older than ~2h (so you can have some overlap), and if it&rsquo;s not - meaning that Sentry is nicely ingesting events.</p>
<p>You can do that using Cloudflare Workers, here is a part of Terraform code which I used to configure it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_worker_script&#34;</span> <span style="color:#e6db74">&#34;sentry_monitoring&#34;</span> {  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       = <span style="color:#e6db74">&#34;sentry-monitoring&#34;</span>  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">content</span>    = file(<span style="color:#e6db74">&#34;sentry.js&#34;</span>)  
</span></span><span style="display:flex;"><span>  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">plain_text_binding</span> {  
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">name</span> = <span style="color:#e6db74">&#34;ISSUE_ID&#34;</span>  
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">text</span> = <span style="color:#e6db74">&#34;&lt;your issue id&gt;&#34;</span>  
</span></span><span style="display:flex;"><span>  }  
</span></span><span style="display:flex;"><span>  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">plain_text_binding</span> {  
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">name</span> = <span style="color:#e6db74">&#34;SENTRY_DOMAIN&#34;</span>  
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">text</span> = <span style="color:#a6e22e">cloudflare_record</span>.<span style="color:#a6e22e">sentry</span>.<span style="color:#a6e22e">name</span><span style="color:#75715e"> // assuming you have this configured
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  }  
</span></span><span style="display:flex;"><span>}  
</span></span><span style="display:flex;"><span>  
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_worker_secret&#34;</span> <span style="color:#e6db74">&#34;sentry_auth_token&#34;</span> {  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span>  = var.<span style="color:#a6e22e">cloudflare_account_id</span>  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">script_name</span> = <span style="color:#a6e22e">cloudflare_worker_script</span>.<span style="color:#a6e22e">sentry_monitoring</span>.<span style="color:#a6e22e">name</span>  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>        = <span style="color:#e6db74">&#34;SENTRY_API_TOKEN&#34;</span><span style="color:#75715e"> // issues read-only token 
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">secret_text</span> = var.<span style="color:#a6e22e">sentry_api_token</span><span style="color:#75715e"> // I&#39;m using Terraform Cloud to set this variable
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}  
</span></span><span style="display:flex;"><span>  
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_worker_route&#34;</span> <span style="color:#e6db74">&#34;sentry_monitoring&#34;</span> {  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">zone_id</span>     = <span style="color:#a6e22e">cloudflare_zone</span>.<span style="color:#a6e22e">your_zone_id</span>.<span style="color:#a6e22e">id</span><span style="color:#75715e"> // ssuming you have this configured
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">script_name</span> = <span style="color:#a6e22e">cloudflare_worker_script</span>.<span style="color:#a6e22e">sentry_monitoring</span>.<span style="color:#a6e22e">name</span>  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">pattern</span>     = <span style="color:#e6db74">&#34;https://</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">cloudflare_record</span>.<span style="color:#a6e22e">sentry</span>.<span style="color:#a6e22e">name</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/uptime*&#34;</span>  
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Here is the worker JS file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#a6e22e">addEventListener</span>(<span style="color:#e6db74">&#39;fetch&#39;</span>, <span style="color:#a6e22e">event</span> =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">respondWith</span>(<span style="color:#a6e22e">handleRequest</span>())
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">handleRequest</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// you might want to pass project id as argument here
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">apiUrl</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`https://</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">SENTRY_DOMAIN</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/api/0/issues/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">ISSUE_ID</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/`</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">token</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">SENTRY_API_TOKEN</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">response</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fetch</span>(<span style="color:#a6e22e">apiUrl</span>, {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;Authorization&#39;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`Bearer </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">token</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">data</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">json</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">ok</span>) {
</span></span><span style="display:flex;"><span>     <span style="color:#75715e">// worker logs is currently in beta, you can enable it in Cloudflare dashboard,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>     <span style="color:#75715e">// seems like terraform provider is lagging behind
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>     <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">`Error: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">status</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> - </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">statusText</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>);
</span></span><span style="display:flex;"><span>     <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#e6db74">&#39;API Error&#39;</span>, { <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">500</span> });
</span></span><span style="display:flex;"><span>   }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">lastEventStr</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">lastSeen</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">lastEventDate</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Date(<span style="color:#a6e22e">lastEventStr</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">currentTime</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Date()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">diffInHours</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">currentTime</span> <span style="color:#f92672">-</span> <span style="color:#a6e22e">lastEventDate</span>) <span style="color:#f92672">/</span> (<span style="color:#ae81ff">1000</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">60</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">60</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">diffInHours</span> <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">2</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#a6e22e">lastEventStr</span>, { <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">200</span> })
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#a6e22e">lastEventStr</span>, { <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">500</span> })
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  } <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;Error:&#39;</span>, <span style="color:#a6e22e">error</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#e6db74">&#34;Unhandled error&#34;</span>, { <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">500</span> })
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then under <code>&lt;your Sentry domain&gt;/uptime</code> I would have a Cloudflare worker returning 200/500 response - now it&rsquo;s easy enough to add that endpoint as well to monitoring software of your choice.</p>
<p>Now you should be monitoring 4 different endpoints which should give you enough confidence that Sentry is indeed up &amp; properly processing events.</p>]]></description><content:encoded><![CDATA[<p>I love Sentry since I discovered it many years ago. Back in the days, self-hosting it was really easy - a single Docker image which you would use for spinning up like 2-3 separate services, Postgres, Redis, a few lines of config, and you&rsquo;re done.</p>
<p>Nowadays (2024), self-hosting Sentry requires spinning up 50+ different services - and that is of course without any fancy HA setup. It&rsquo;s still doable, <a href="https://github.com/sentry-kubernetes/charts">especially with Kubernetes</a>, but the learning curve is definitely more steep. Then again, the feature set of Sentry itself is much richer - it&rsquo;s not about just catching errors anymore; you have full-fledged build-in performance monitoring, session recording, and tons of other observation-related goodies.</p>
<p>One thing is kinda tricky is monitoring of your Sentry instance. Here is how I do it.</p>
<h3 id="use-built-in-endpoints">Use built-in endpoints</h3>
<ul>
<li><code>/_health/</code> - to ensure your <em>web</em> is working</li>
<li><code>/api/relay/healthcheck/live/</code> - to ensure your <a href="https://github.com/getsentry/relay">Relay</a> is alive</li>
<li><code>/api/relay/healthcheck/ready/</code> - to ensure your Relay is ready for events ingestion</li>
</ul>
<h3 id="and-its-not-enough">…and it&rsquo;s not enough</h3>
<p>I ran into a situation where <code>Relay</code> went into some reconnecting spree (despite being live/ready) - at the moment I don&rsquo;t remember the exact root cause, but it took me a while to realize that events are <em>not</em> being properly ingested. In the end, I decided to configure a cronjob which would trigger a Sentry event (exception) to one of my projects every ~hour. Then I muted that exception as obviously it was not actionable.</p>
<p>Once you have that exception <em>in</em> Sentry, it&rsquo;s time to check if it&rsquo;s being <em>registered</em> every hour.</p>
<p>Sentry exposes <a href="https://docs.sentry.io/api/events/retrieve-an-issue/">API endpoint</a> where you can obtain issue details; one interesting part in that JSON response is <code>lastEvent</code> - you can query this endpoint every ~hour, parse the response, ensure it&rsquo;s no older than ~2h (so you can have some overlap), and if it&rsquo;s not - meaning that Sentry is nicely ingesting events.</p>
<p>You can do that using Cloudflare Workers, here is a part of Terraform code which I used to configure it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_worker_script&#34;</span> <span style="color:#e6db74">&#34;sentry_monitoring&#34;</span> {  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       = <span style="color:#e6db74">&#34;sentry-monitoring&#34;</span>  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">content</span>    = file(<span style="color:#e6db74">&#34;sentry.js&#34;</span>)  
</span></span><span style="display:flex;"><span>  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">plain_text_binding</span> {  
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">name</span> = <span style="color:#e6db74">&#34;ISSUE_ID&#34;</span>  
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">text</span> = <span style="color:#e6db74">&#34;&lt;your issue id&gt;&#34;</span>  
</span></span><span style="display:flex;"><span>  }  
</span></span><span style="display:flex;"><span>  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">plain_text_binding</span> {  
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">name</span> = <span style="color:#e6db74">&#34;SENTRY_DOMAIN&#34;</span>  
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">text</span> = <span style="color:#a6e22e">cloudflare_record</span>.<span style="color:#a6e22e">sentry</span>.<span style="color:#a6e22e">name</span><span style="color:#75715e"> // assuming you have this configured
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  }  
</span></span><span style="display:flex;"><span>}  
</span></span><span style="display:flex;"><span>  
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_worker_secret&#34;</span> <span style="color:#e6db74">&#34;sentry_auth_token&#34;</span> {  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span>  = var.<span style="color:#a6e22e">cloudflare_account_id</span>  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">script_name</span> = <span style="color:#a6e22e">cloudflare_worker_script</span>.<span style="color:#a6e22e">sentry_monitoring</span>.<span style="color:#a6e22e">name</span>  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>        = <span style="color:#e6db74">&#34;SENTRY_API_TOKEN&#34;</span><span style="color:#75715e"> // issues read-only token 
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">secret_text</span> = var.<span style="color:#a6e22e">sentry_api_token</span><span style="color:#75715e"> // I&#39;m using Terraform Cloud to set this variable
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}  
</span></span><span style="display:flex;"><span>  
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_worker_route&#34;</span> <span style="color:#e6db74">&#34;sentry_monitoring&#34;</span> {  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">zone_id</span>     = <span style="color:#a6e22e">cloudflare_zone</span>.<span style="color:#a6e22e">your_zone_id</span>.<span style="color:#a6e22e">id</span><span style="color:#75715e"> // ssuming you have this configured
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">script_name</span> = <span style="color:#a6e22e">cloudflare_worker_script</span>.<span style="color:#a6e22e">sentry_monitoring</span>.<span style="color:#a6e22e">name</span>  
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">pattern</span>     = <span style="color:#e6db74">&#34;https://</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">cloudflare_record</span>.<span style="color:#a6e22e">sentry</span>.<span style="color:#a6e22e">name</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/uptime*&#34;</span>  
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Here is the worker JS file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#a6e22e">addEventListener</span>(<span style="color:#e6db74">&#39;fetch&#39;</span>, <span style="color:#a6e22e">event</span> =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">respondWith</span>(<span style="color:#a6e22e">handleRequest</span>())
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">handleRequest</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// you might want to pass project id as argument here
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">apiUrl</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`https://</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">SENTRY_DOMAIN</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/api/0/issues/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">ISSUE_ID</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/`</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">token</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">SENTRY_API_TOKEN</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">response</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fetch</span>(<span style="color:#a6e22e">apiUrl</span>, {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;Authorization&#39;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`Bearer </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">token</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">data</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">json</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">ok</span>) {
</span></span><span style="display:flex;"><span>     <span style="color:#75715e">// worker logs is currently in beta, you can enable it in Cloudflare dashboard,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>     <span style="color:#75715e">// seems like terraform provider is lagging behind
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>     <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">`Error: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">status</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> - </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">statusText</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>);
</span></span><span style="display:flex;"><span>     <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#e6db74">&#39;API Error&#39;</span>, { <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">500</span> });
</span></span><span style="display:flex;"><span>   }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">lastEventStr</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">lastSeen</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">lastEventDate</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Date(<span style="color:#a6e22e">lastEventStr</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">currentTime</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Date()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">diffInHours</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">currentTime</span> <span style="color:#f92672">-</span> <span style="color:#a6e22e">lastEventDate</span>) <span style="color:#f92672">/</span> (<span style="color:#ae81ff">1000</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">60</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">60</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">diffInHours</span> <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">2</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#a6e22e">lastEventStr</span>, { <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">200</span> })
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#a6e22e">lastEventStr</span>, { <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">500</span> })
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  } <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;Error:&#39;</span>, <span style="color:#a6e22e">error</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Response</span>(<span style="color:#e6db74">&#34;Unhandled error&#34;</span>, { <span style="color:#a6e22e">status</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">500</span> })
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then under <code>&lt;your Sentry domain&gt;/uptime</code> I would have a Cloudflare worker returning 200/500 response - now it&rsquo;s easy enough to add that endpoint as well to monitoring software of your choice.</p>
<p>Now you should be monitoring 4 different endpoints which should give you enough confidence that Sentry is indeed up &amp; properly processing events.</p>]]></content:encoded></item><item><title>Using AI (but mostly programming) to automate declutter</title><link>https://awesomeprogrammer.com/blog/2024/09/10/using-ai-but-mostly-programming-to-automate-declutter/</link><pubDate>Tue, 10 Sep 2024 18:29:53 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2024/09/10/using-ai-but-mostly-programming-to-automate-declutter/</guid><description><![CDATA[<p>Things. You need things in your life. Up to a point where those things start to own you. Your space, your mind, your time.<br>
I like some of my things, some I simply need, and some, well, do not <em>spark joy</em>. I declutter mostly using three options: giving stuff away, selling and throwing away. The last is last resort option - I tend to lean toward the two first one.</p>
<p>As the process is usually quite a chore - you have to snap some pics, write some meaningful description, establish reasonable price, then post it in one or two different places - I thought about automating it a little bit. Seems like LLM APIs are a commodity nowadays I went with <a href="https://platform.openai.com/docs/guides/vision">vision offered by openai</a> - it turned out single API call with a simple prompt and few pics attached can yield some meaningful results.</p>
<p><em>Aw shiet, another weekend project it is then.</em></p>
<h3 id="iteration-iteration-iteration">Iteration, iteration, iteration</h3>
<p>I knew I would new need some sort of UI - I started to look into Phoenix - because why not plan with Elixir <a href="/blog/2022/06/30/from-crystal-to-elixir/">again</a>? Framework introduced a new concepts (live view) and overall I felt extremely rusty and overwhelmed and made zero progress in an hour or so.</p>
<p>When quickly bootstrapped Rails just to be annoyed with the complexity of the front-end/JS world, also learning <a href="https://github.com/rails/jsbundling-rails">another</a> <a href="https://github.com/rails/propshaft">assets</a> <a href="https://github.com/shakacode/shakapacker">pipeline</a> <a href="https://github.com/rails/sprockets">tool</a> felt like waste of time for what I was trying to achieve.</p>
<p>I quickly ditched whole web-app idea and decided that simple CLI would do the job. I thought that it&rsquo;s a great time to play with <a href="https://github.com/charmbracelet/bubbletea">bubbletea</a>, but I struggled to structure maintainable multi-step so in the end I laded with Ruby again as <a href="https://github.com/piotrmurach/tty-prompt">tty-prompt</a> covered like 99% of my needs. Ruby again it was then.</p>
<h3 id="flow">Flow</h3>
<p>The data flow was relatively simple:</p>
<ul>
<li>ask for directory with prepped (properly cropped images)</li>
<li>ask for optional context about <em>the things</em> I&rsquo;m trying to sell</li>
<li>ask which images should be sent to openai for analysis</li>
<li>resize all images, upload selected ones to <a href="https://www.cloudflare.com/developer-platform/r2/">Cloudflare R2</a></li>
<li>fire it away with a simple prompt, attach images using links to R2</li>
</ul>
<p>As a result I would get a json response with <code>title</code>, <code>description</code> and suggested <code>price</code> as a response</p>
<h3 id="getting-more-price-data">Getting more price data</h3>
<p>The thing with price it that it usually will be off, so I quickly fired up <a href="https://mitmproxy.org">mitmproxy</a>, opened local OLX app on my phone and decoded how their search works to get some more price-related data.</p>
<p>As a follow-up step I prepped a new prompt which exposed a <a href="https://platform.openai.com/docs/guides/function-calling">function</a> to item search that was piggybacked on the OLX one—just to have some confirmation on the suggested price.</p>
<h3 id="posting-an-ad">Posting an ad</h3>
<p>I wanted to automate posting ad as well on two different services, but after looking what happens in the mobile OLX API I decided it will be way easier just to automate browser at this point just to fill like 90% of the form. I decided to connect with existing Chrome profile to not have to deal with constant sign-in/captchas which turned out to be more complicated than expected; thankfully someone solved this problem <a href="https://github.com/rubycdp/ferrum/pull/471">in this pull request</a>; after monkey patching Ferrum I was ready to go. I appreciate that both services were nice enough to have <code>data-testid</code> attributes on elements I needed so scripting whole thing took minutes in the end.</p>
<h3 id="show-me-the-money">Show me the money</h3>
<p>I failed to record proper demo of this thing but I made a poor screenshot for posterity.</p>
<figure><img src="/images/olx-cli.png"/><figcaption>
            <h4>The answer is obviously in Polish as I&#39;m posting it on OLX and Allegrolokalnie at the moment of writing this.</h4>
        </figcaption>
</figure>

<p>The cool thing about it in the example above is that I attached a SMART scan screenshot and told llm to read it&rsquo;s technical condition (and hoped for the best).</p>
]]></description><content:encoded><![CDATA[<p>Things. You need things in your life. Up to a point where those things start to own you. Your space, your mind, your time.<br>
I like some of my things, some I simply need, and some, well, do not <em>spark joy</em>. I declutter mostly using three options: giving stuff away, selling and throwing away. The last is last resort option - I tend to lean toward the two first one.</p>
<p>As the process is usually quite a chore - you have to snap some pics, write some meaningful description, establish reasonable price, then post it in one or two different places - I thought about automating it a little bit. Seems like LLM APIs are a commodity nowadays I went with <a href="https://platform.openai.com/docs/guides/vision">vision offered by openai</a> - it turned out single API call with a simple prompt and few pics attached can yield some meaningful results.</p>
<p><em>Aw shiet, another weekend project it is then.</em></p>
<h3 id="iteration-iteration-iteration">Iteration, iteration, iteration</h3>
<p>I knew I would new need some sort of UI - I started to look into Phoenix - because why not plan with Elixir <a href="/blog/2022/06/30/from-crystal-to-elixir/">again</a>? Framework introduced a new concepts (live view) and overall I felt extremely rusty and overwhelmed and made zero progress in an hour or so.</p>
<p>When quickly bootstrapped Rails just to be annoyed with the complexity of the front-end/JS world, also learning <a href="https://github.com/rails/jsbundling-rails">another</a> <a href="https://github.com/rails/propshaft">assets</a> <a href="https://github.com/shakacode/shakapacker">pipeline</a> <a href="https://github.com/rails/sprockets">tool</a> felt like waste of time for what I was trying to achieve.</p>
<p>I quickly ditched whole web-app idea and decided that simple CLI would do the job. I thought that it&rsquo;s a great time to play with <a href="https://github.com/charmbracelet/bubbletea">bubbletea</a>, but I struggled to structure maintainable multi-step so in the end I laded with Ruby again as <a href="https://github.com/piotrmurach/tty-prompt">tty-prompt</a> covered like 99% of my needs. Ruby again it was then.</p>
<h3 id="flow">Flow</h3>
<p>The data flow was relatively simple:</p>
<ul>
<li>ask for directory with prepped (properly cropped images)</li>
<li>ask for optional context about <em>the things</em> I&rsquo;m trying to sell</li>
<li>ask which images should be sent to openai for analysis</li>
<li>resize all images, upload selected ones to <a href="https://www.cloudflare.com/developer-platform/r2/">Cloudflare R2</a></li>
<li>fire it away with a simple prompt, attach images using links to R2</li>
</ul>
<p>As a result I would get a json response with <code>title</code>, <code>description</code> and suggested <code>price</code> as a response</p>
<h3 id="getting-more-price-data">Getting more price data</h3>
<p>The thing with price it that it usually will be off, so I quickly fired up <a href="https://mitmproxy.org">mitmproxy</a>, opened local OLX app on my phone and decoded how their search works to get some more price-related data.</p>
<p>As a follow-up step I prepped a new prompt which exposed a <a href="https://platform.openai.com/docs/guides/function-calling">function</a> to item search that was piggybacked on the OLX one—just to have some confirmation on the suggested price.</p>
<h3 id="posting-an-ad">Posting an ad</h3>
<p>I wanted to automate posting ad as well on two different services, but after looking what happens in the mobile OLX API I decided it will be way easier just to automate browser at this point just to fill like 90% of the form. I decided to connect with existing Chrome profile to not have to deal with constant sign-in/captchas which turned out to be more complicated than expected; thankfully someone solved this problem <a href="https://github.com/rubycdp/ferrum/pull/471">in this pull request</a>; after monkey patching Ferrum I was ready to go. I appreciate that both services were nice enough to have <code>data-testid</code> attributes on elements I needed so scripting whole thing took minutes in the end.</p>
<h3 id="show-me-the-money">Show me the money</h3>
<p>I failed to record proper demo of this thing but I made a poor screenshot for posterity.</p>
<figure><img src="/images/olx-cli.png"/><figcaption>
            <h4>The answer is obviously in Polish as I&#39;m posting it on OLX and Allegrolokalnie at the moment of writing this.</h4>
        </figcaption>
</figure>

<p>The cool thing about it in the example above is that I attached a SMART scan screenshot and told llm to read it&rsquo;s technical condition (and hoped for the best).</p>
]]></content:encoded></item><item><title>Nice Human Being: Remote Employee Guide</title><link>https://awesomeprogrammer.com/blog/2024/07/26/nice-human-being-remote-employee-guide/</link><pubDate>Fri, 26 Jul 2024 15:02:20 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2024/07/26/nice-human-being-remote-employee-guide/</guid><description><![CDATA[<p>Want to be a Nice Human Being and be loved by your coworkers? <em>Just follow these 4 easy steps!</em></p>
<h3 id="own-your-notifications">Own your notifications</h3>
<p>If team members are pinging you somewhere, ensure it doesn&rsquo;t go to <code>/dev/null</code>. Streamline your notifications from the apps you&rsquo;re using. I avoid email like the plague, so I tend to integrate almost everything with Slack - like product management and knowledge base tools. I handle GitHub notifications via a <a href="https://www.raycast.com/raycast/github">Raycast plugin</a>, so those are nicely grouped per PR/issue and quickly available from my toolbar.</p>
<h3 id="own-your-calendar">Own your calendar</h3>
<p>Set busy slots when you&rsquo;re, well, busy. Subscribe to calendars worth following (e.g., your PagerDuty schedule or your team&rsquo;s time off). Moreover, integrating those calendars with your system might help you see the bigger picture.</p>
<h3 id="own-scheduled-meetings">Own scheduled meetings</h3>
<p>Are you scheduling a meeting? Assuming it&rsquo;s not a regular coffee chat, set the agenda up front. Make notes during the meeting and ensure you leave with some action points. If you receive an invite, accept or reject it - don&rsquo;t leave it hanging forever. And most importantly, try to be on time.</p>
<h3 id="own-your-statuses">Own your statuses</h3>
<p>Going on PTO? Set your Slack status, set your GitHub status (and mark yourself as <em>busy</em> so you won&rsquo;t be auto-assigned code reviews). If you&rsquo;re going to be away for a while, set your status. Leaving for a prolonged lunch break? Status. Have a doctor&rsquo;s appointment - <a href="https://www.youtube.com/watch?v=eiyfwZVAzGw">believe it or not</a> - status. Our best collaboration happens because of statuses.</p>
<hr>
<p>These small steps can make a big difference in how you&rsquo;re seen and how well your team runs. So give it a try - your coworkers will love you for it!*</p>
<p><em>*Results not guaranteed, no refunds</em></p>]]></description><content:encoded><![CDATA[<p>Want to be a Nice Human Being and be loved by your coworkers? <em>Just follow these 4 easy steps!</em></p>
<h3 id="own-your-notifications">Own your notifications</h3>
<p>If team members are pinging you somewhere, ensure it doesn&rsquo;t go to <code>/dev/null</code>. Streamline your notifications from the apps you&rsquo;re using. I avoid email like the plague, so I tend to integrate almost everything with Slack - like product management and knowledge base tools. I handle GitHub notifications via a <a href="https://www.raycast.com/raycast/github">Raycast plugin</a>, so those are nicely grouped per PR/issue and quickly available from my toolbar.</p>
<h3 id="own-your-calendar">Own your calendar</h3>
<p>Set busy slots when you&rsquo;re, well, busy. Subscribe to calendars worth following (e.g., your PagerDuty schedule or your team&rsquo;s time off). Moreover, integrating those calendars with your system might help you see the bigger picture.</p>
<h3 id="own-scheduled-meetings">Own scheduled meetings</h3>
<p>Are you scheduling a meeting? Assuming it&rsquo;s not a regular coffee chat, set the agenda up front. Make notes during the meeting and ensure you leave with some action points. If you receive an invite, accept or reject it - don&rsquo;t leave it hanging forever. And most importantly, try to be on time.</p>
<h3 id="own-your-statuses">Own your statuses</h3>
<p>Going on PTO? Set your Slack status, set your GitHub status (and mark yourself as <em>busy</em> so you won&rsquo;t be auto-assigned code reviews). If you&rsquo;re going to be away for a while, set your status. Leaving for a prolonged lunch break? Status. Have a doctor&rsquo;s appointment - <a href="https://www.youtube.com/watch?v=eiyfwZVAzGw">believe it or not</a> - status. Our best collaboration happens because of statuses.</p>
<hr>
<p>These small steps can make a big difference in how you&rsquo;re seen and how well your team runs. So give it a try - your coworkers will love you for it!*</p>
<p><em>*Results not guaranteed, no refunds</em></p>]]></content:encoded></item><item><title>Cloudflare Zero Trust for small teams</title><link>https://awesomeprogrammer.com/blog/2024/06/06/cloudflare-zero-trust-for-small-teams/</link><pubDate>Thu, 06 Jun 2024 17:09:26 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2024/06/06/cloudflare-zero-trust-for-small-teams/</guid><description><![CDATA[<p>Cloudflare Zero trust - which is planned be rebrand to Cloudflare One is pretty awesome platform that can accommodate small engineering teams when it comes to strickling security.</p>
<p>Unfortunately the Cloudflare docs can be quite confusing and sometimes even contradicting - it seems the product evolved quite rapidly and the documentation or blog posts were lagging behind and the old one - were never updated.</p>
<p>Let&rsquo;s go over common use cases we could use Zero Trust. I will assume that you&rsquo;re familiar with Terraform and skip irrelevant code bits.</p>
<h3 id="locking-domain-behind-github-auth">Locking domain behind github auth</h3>
<ol>
<li><a href="https://developers.cloudflare.com/cloudflare-one/identity/idp-integration/github/">Add new github auth</a> per official docs</li>
<li>I will assume cloudflare account ID to be stored in <code>cloudflare_account_id</code> variable</li>
<li>I will assume <code>your_zone</code> zone and some <code>your_domain</code> A record being configured</li>
</ol>
<p>Start with identity provider.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_access_identity_provider&#34;</span> <span style="color:#e6db74">&#34;github_oauth&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       = <span style="color:#e6db74">&#34;GitHub&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>       = <span style="color:#e6db74">&#34;github&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">config</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">client_id</span>     = <span style="color:#e6db74">&#34;&lt;your GH app client id&gt;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">client_secret</span> = <span style="color:#e6db74">&#34;&lt;your GH app secret&gt;&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then grant access to your GH organization.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_access_group&#34;</span> <span style="color:#e6db74">&#34;github_org&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       = <span style="color:#e6db74">&#34;Github Organization Group&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">include</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">github</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">identity_provider_id</span> = <span style="color:#a6e22e">cloudflare_access_identity_provider</span>.<span style="color:#a6e22e">github_oauth</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">name</span>                 = <span style="color:#e6db74">&#34;&lt;name of your github org&gt;&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>And glue it together by protecting your domain within the zone.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_access_application&#34;</span> <span style="color:#e6db74">&#34;example&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">zone_id</span>                   = <span style="color:#a6e22e">cloudflare_zone</span>.<span style="color:#a6e22e">your_zone</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>                      = <span style="color:#a6e22e">cloudflare_record</span>.<span style="color:#a6e22e">your_domain</span>.<span style="color:#a6e22e">name</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">domain</span>                    = <span style="color:#a6e22e">cloudflare_record</span>.<span style="color:#a6e22e">your_domain</span>.<span style="color:#a6e22e">name</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>                      = <span style="color:#e6db74">&#34;self_hosted&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">session_duration</span>          = <span style="color:#e6db74">&#34;24h&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">auto_redirect_to_identity</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">allowed_idps</span> = [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cloudflare_access_identity_provider</span>.<span style="color:#a6e22e">github_oauth</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">policies</span> = [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cloudflare_access_policy</span>.<span style="color:#a6e22e">example</span>.<span style="color:#a6e22e">id</span>,
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_access_policy&#34;</span> <span style="color:#e6db74">&#34;example&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span>       = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>             = <span style="color:#e6db74">&#34;github auth&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">session_duration</span> = <span style="color:#e6db74">&#34;24h&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">decision</span>         = <span style="color:#e6db74">&#34;allow&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">include</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">group</span> = [
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">cloudflare_access_group</span>.<span style="color:#a6e22e">github_org</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="locking-specific-path">Locking specific path</h3>
<p>It&rsquo;s exactly the same as the above, but you customize the <code>domain</code> bit.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_access_application&#34;</span> <span style="color:#e6db74">&#34;example&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">zone_id</span>                   = <span style="color:#a6e22e">cloudflare_zone</span>.<span style="color:#a6e22e">your_zone</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>                      = <span style="color:#a6e22e">cloudflare_record</span>.<span style="color:#a6e22e">your_domain</span>.<span style="color:#a6e22e">name</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">domain</span>                    = <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">cloudflare_record</span>.<span style="color:#a6e22e">your_domain</span>.<span style="color:#a6e22e">name</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/some-path&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>                      = <span style="color:#e6db74">&#34;self_hosted&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">session_duration</span>          = <span style="color:#e6db74">&#34;24h&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">auto_redirect_to_identity</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">allowed_idps</span> = [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cloudflare_access_identity_provider</span>.<span style="color:#a6e22e">github_oauth</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">policies</span> = [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cloudflare_access_policy</span>.<span style="color:#a6e22e">example</span>.<span style="color:#a6e22e">id</span>,
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="using-warp-client-to-access-internal-network">Using WARP client to access internal network</h3>
<p>One cool thing you can do is install <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/">cloudflared</a> daemon on one of your host in the internal network and create a secure tunnel between that network and grant access to it using <a href="https://developers.cloudflare.com/warp-client/">WARP client</a>. There are tons and tons options there, but I will provide most basic example.</p>
<p>I&rsquo;m not gonna lie that it&rsquo;s been ages since I set this one up, so my knowledge is a little bit rusty here. Example below allows traffic <code>10.0.0.0/16</code> to be routed by the tunnel and accessed when <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/deployment/manual-deployment/#windows-macos-and-linux">signed into Zero Trust</a> account via WARP client using Github auth (from the example above).</p>
<p>Cloudflared itself can be installed on the host e.g via <a href="https://github.com/cloudflare/cloudflared/releases">debian/rpm package</a>. Once installed configure tunnel via <code>cloudflared service install &lt;your tunnel secret&gt;</code> command.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_device_settings_policy&#34;</span> <span style="color:#e6db74">&#34;warp_policy&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span>            = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>                  = <span style="color:#e6db74">&#34;WARP settings policy&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">description</span>           = <span style="color:#e6db74">&#34;WARP settings policy&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">default</span>               = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">enabled</span>               = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">allow_mode_switch</span>     = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">allow_updates</span>         = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">allowed_to_leave</span>      = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">auto_connect</span>          = <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">disable_auto_fallback</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_split_tunnel&#34;</span> <span style="color:#e6db74">&#34;internal_network&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">policy_id</span>  = <span style="color:#a6e22e">cloudflare_device_settings_policy</span>.<span style="color:#a6e22e">warp_policy</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">mode</span>       = <span style="color:#e6db74">&#34;include&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">tunnels</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">address</span>     = <span style="color:#e6db74">&#34;10.0.0.0/16&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">description</span> = <span style="color:#e6db74">&#34;internal network&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_device_posture_rule&#34;</span> <span style="color:#e6db74">&#34;device_rules&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       = <span style="color:#e6db74">&#34;Devices posture rule&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>       = <span style="color:#e6db74">&#34;warp&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;cloudflare_access_application&#34;</span> <span style="color:#e6db74">&#34;warp&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">domain</span>     = <span style="color:#e6db74">&#34;&lt;cloudflare org name&gt;.cloudflareaccess.com/warp&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_access_policy&#34;</span> <span style="color:#e6db74">&#34;warp&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span>       = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">application_id</span>   = data.<span style="color:#a6e22e">cloudflare_access_application</span>.<span style="color:#a6e22e">warp</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>             = <span style="color:#e6db74">&#34;warp enrollment policy&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">precedence</span>       = <span style="color:#e6db74">&#34;2&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">session_duration</span> = <span style="color:#e6db74">&#34;24h&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">decision</span>         = <span style="color:#e6db74">&#34;allow&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">include</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">group</span> = [<span style="color:#a6e22e">cloudflare_access_group</span>.<span style="color:#a6e22e">github_org</span>.<span style="color:#a6e22e">id</span>]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_tunnel&#34;</span> <span style="color:#e6db74">&#34;example&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       = <span style="color:#e6db74">&#34;&lt;your-server-name&gt;&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">secret</span>     = base64encode(<span style="color:#e6db74">&#34;&lt;your tunnel secret&gt;&#34;</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_tunnel_config&#34;</span> <span style="color:#e6db74">&#34;example_config&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">tunnel_id</span>  = <span style="color:#a6e22e">cloudflare_tunnel</span>.<span style="color:#a6e22e">example</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">config</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">warp_routing</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">enabled</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ingress_rule</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">service</span> = <span style="color:#e6db74">&#34;http://127.0.0.1&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_tunnel_route&#34;</span> <span style="color:#e6db74">&#34;tunnel_route&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">tunnel_id</span>  = <span style="color:#a6e22e">cloudflare_tunnel</span>.<span style="color:#a6e22e">example</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">network</span>    = <span style="color:#e6db74">&#34;10.0.0.0/16&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Once connected you can just do <code>ssh 10.0.X.Y</code> from your computer and things should <em>just work</em> (assuming there is a working node in your internal network with that IP that is).</p>]]></description><content:encoded><![CDATA[<p>Cloudflare Zero trust - which is planned be rebrand to Cloudflare One is pretty awesome platform that can accommodate small engineering teams when it comes to strickling security.</p>
<p>Unfortunately the Cloudflare docs can be quite confusing and sometimes even contradicting - it seems the product evolved quite rapidly and the documentation or blog posts were lagging behind and the old one - were never updated.</p>
<p>Let&rsquo;s go over common use cases we could use Zero Trust. I will assume that you&rsquo;re familiar with Terraform and skip irrelevant code bits.</p>
<h3 id="locking-domain-behind-github-auth">Locking domain behind github auth</h3>
<ol>
<li><a href="https://developers.cloudflare.com/cloudflare-one/identity/idp-integration/github/">Add new github auth</a> per official docs</li>
<li>I will assume cloudflare account ID to be stored in <code>cloudflare_account_id</code> variable</li>
<li>I will assume <code>your_zone</code> zone and some <code>your_domain</code> A record being configured</li>
</ol>
<p>Start with identity provider.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_access_identity_provider&#34;</span> <span style="color:#e6db74">&#34;github_oauth&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       = <span style="color:#e6db74">&#34;GitHub&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>       = <span style="color:#e6db74">&#34;github&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">config</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">client_id</span>     = <span style="color:#e6db74">&#34;&lt;your GH app client id&gt;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">client_secret</span> = <span style="color:#e6db74">&#34;&lt;your GH app secret&gt;&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then grant access to your GH organization.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_access_group&#34;</span> <span style="color:#e6db74">&#34;github_org&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       = <span style="color:#e6db74">&#34;Github Organization Group&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">include</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">github</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">identity_provider_id</span> = <span style="color:#a6e22e">cloudflare_access_identity_provider</span>.<span style="color:#a6e22e">github_oauth</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">name</span>                 = <span style="color:#e6db74">&#34;&lt;name of your github org&gt;&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>And glue it together by protecting your domain within the zone.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_access_application&#34;</span> <span style="color:#e6db74">&#34;example&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">zone_id</span>                   = <span style="color:#a6e22e">cloudflare_zone</span>.<span style="color:#a6e22e">your_zone</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>                      = <span style="color:#a6e22e">cloudflare_record</span>.<span style="color:#a6e22e">your_domain</span>.<span style="color:#a6e22e">name</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">domain</span>                    = <span style="color:#a6e22e">cloudflare_record</span>.<span style="color:#a6e22e">your_domain</span>.<span style="color:#a6e22e">name</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>                      = <span style="color:#e6db74">&#34;self_hosted&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">session_duration</span>          = <span style="color:#e6db74">&#34;24h&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">auto_redirect_to_identity</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">allowed_idps</span> = [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cloudflare_access_identity_provider</span>.<span style="color:#a6e22e">github_oauth</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">policies</span> = [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cloudflare_access_policy</span>.<span style="color:#a6e22e">example</span>.<span style="color:#a6e22e">id</span>,
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_access_policy&#34;</span> <span style="color:#e6db74">&#34;example&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span>       = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>             = <span style="color:#e6db74">&#34;github auth&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">session_duration</span> = <span style="color:#e6db74">&#34;24h&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">decision</span>         = <span style="color:#e6db74">&#34;allow&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">include</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">group</span> = [
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">cloudflare_access_group</span>.<span style="color:#a6e22e">github_org</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="locking-specific-path">Locking specific path</h3>
<p>It&rsquo;s exactly the same as the above, but you customize the <code>domain</code> bit.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_access_application&#34;</span> <span style="color:#e6db74">&#34;example&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">zone_id</span>                   = <span style="color:#a6e22e">cloudflare_zone</span>.<span style="color:#a6e22e">your_zone</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>                      = <span style="color:#a6e22e">cloudflare_record</span>.<span style="color:#a6e22e">your_domain</span>.<span style="color:#a6e22e">name</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">domain</span>                    = <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">cloudflare_record</span>.<span style="color:#a6e22e">your_domain</span>.<span style="color:#a6e22e">name</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/some-path&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>                      = <span style="color:#e6db74">&#34;self_hosted&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">session_duration</span>          = <span style="color:#e6db74">&#34;24h&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">auto_redirect_to_identity</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">allowed_idps</span> = [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cloudflare_access_identity_provider</span>.<span style="color:#a6e22e">github_oauth</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">policies</span> = [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cloudflare_access_policy</span>.<span style="color:#a6e22e">example</span>.<span style="color:#a6e22e">id</span>,
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="using-warp-client-to-access-internal-network">Using WARP client to access internal network</h3>
<p>One cool thing you can do is install <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/">cloudflared</a> daemon on one of your host in the internal network and create a secure tunnel between that network and grant access to it using <a href="https://developers.cloudflare.com/warp-client/">WARP client</a>. There are tons and tons options there, but I will provide most basic example.</p>
<p>I&rsquo;m not gonna lie that it&rsquo;s been ages since I set this one up, so my knowledge is a little bit rusty here. Example below allows traffic <code>10.0.0.0/16</code> to be routed by the tunnel and accessed when <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/deployment/manual-deployment/#windows-macos-and-linux">signed into Zero Trust</a> account via WARP client using Github auth (from the example above).</p>
<p>Cloudflared itself can be installed on the host e.g via <a href="https://github.com/cloudflare/cloudflared/releases">debian/rpm package</a>. Once installed configure tunnel via <code>cloudflared service install &lt;your tunnel secret&gt;</code> command.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_device_settings_policy&#34;</span> <span style="color:#e6db74">&#34;warp_policy&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span>            = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>                  = <span style="color:#e6db74">&#34;WARP settings policy&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">description</span>           = <span style="color:#e6db74">&#34;WARP settings policy&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">default</span>               = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">enabled</span>               = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">allow_mode_switch</span>     = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">allow_updates</span>         = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">allowed_to_leave</span>      = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">auto_connect</span>          = <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">disable_auto_fallback</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_split_tunnel&#34;</span> <span style="color:#e6db74">&#34;internal_network&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">policy_id</span>  = <span style="color:#a6e22e">cloudflare_device_settings_policy</span>.<span style="color:#a6e22e">warp_policy</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">mode</span>       = <span style="color:#e6db74">&#34;include&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">tunnels</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">address</span>     = <span style="color:#e6db74">&#34;10.0.0.0/16&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">description</span> = <span style="color:#e6db74">&#34;internal network&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_device_posture_rule&#34;</span> <span style="color:#e6db74">&#34;device_rules&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       = <span style="color:#e6db74">&#34;Devices posture rule&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>       = <span style="color:#e6db74">&#34;warp&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;cloudflare_access_application&#34;</span> <span style="color:#e6db74">&#34;warp&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">domain</span>     = <span style="color:#e6db74">&#34;&lt;cloudflare org name&gt;.cloudflareaccess.com/warp&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_access_policy&#34;</span> <span style="color:#e6db74">&#34;warp&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span>       = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">application_id</span>   = data.<span style="color:#a6e22e">cloudflare_access_application</span>.<span style="color:#a6e22e">warp</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>             = <span style="color:#e6db74">&#34;warp enrollment policy&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">precedence</span>       = <span style="color:#e6db74">&#34;2&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">session_duration</span> = <span style="color:#e6db74">&#34;24h&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">decision</span>         = <span style="color:#e6db74">&#34;allow&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">include</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">group</span> = [<span style="color:#a6e22e">cloudflare_access_group</span>.<span style="color:#a6e22e">github_org</span>.<span style="color:#a6e22e">id</span>]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_tunnel&#34;</span> <span style="color:#e6db74">&#34;example&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       = <span style="color:#e6db74">&#34;&lt;your-server-name&gt;&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">secret</span>     = base64encode(<span style="color:#e6db74">&#34;&lt;your tunnel secret&gt;&#34;</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_tunnel_config&#34;</span> <span style="color:#e6db74">&#34;example_config&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">tunnel_id</span>  = <span style="color:#a6e22e">cloudflare_tunnel</span>.<span style="color:#a6e22e">example</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">config</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">warp_routing</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">enabled</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ingress_rule</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">service</span> = <span style="color:#e6db74">&#34;http://127.0.0.1&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;cloudflare_tunnel_route&#34;</span> <span style="color:#e6db74">&#34;tunnel_route&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">account_id</span> = var.<span style="color:#a6e22e">cloudflare_account_id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">tunnel_id</span>  = <span style="color:#a6e22e">cloudflare_tunnel</span>.<span style="color:#a6e22e">example</span>.<span style="color:#a6e22e">id</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">network</span>    = <span style="color:#e6db74">&#34;10.0.0.0/16&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Once connected you can just do <code>ssh 10.0.X.Y</code> from your computer and things should <em>just work</em> (assuming there is a working node in your internal network with that IP that is).</p>]]></content:encoded></item><item><title>Simple Mysql 8 replication on k8s</title><link>https://awesomeprogrammer.com/blog/2024/05/28/simple-mysql-8-replication-on-k8s/</link><pubDate>Tue, 28 May 2024 17:56:22 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2024/05/28/simple-mysql-8-replication-on-k8s/</guid><description><![CDATA[<blockquote>
<p>💡 NOTE: Interested in mysql-operator instead? <a href="/blog/2025/12/06/mysql-operator-on-k3s/">See this blogpost</a>.</p>
</blockquote>
<p>Official kubernetes docs <a href="https://kubernetes.io/docs/tasks/run-application/run-replicated-stateful-application/">have a page about running replicated statefulset</a> using mysql, unfortunately:</p>
<ul>
<li>example uses mysql 5.7</li>
<li>it&rsquo;s quite hard to grasp - it uses some custom percona-based image and assumes that you might want to scale statefulset into infinity</li>
<li>as both images are dated it just won&rsquo;t work on ARM machines</li>
</ul>
<p>I think in small to medium organisations/teams/projects what you usually want it some <em>finite</em> amount of replicas - maybe one that you could use for some read-only operations and for doing backups from. So let&rsquo;s try to do that.</p>
<p>Let&rsquo;s create a two <strong>separate</strong> statefulsets under same namespace - one will be leader and another one follower - we will implemented <a href="https://dev.mysql.com/doc/refman/8.4/en/replication-gtids-howto.html">GTID-based replication</a>.</p>
<p>First we&rsquo;re gonna need to prepare <code>xtrabackup</code> docker image:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> bitnami/percona-xtrabackup:8.2.0-1</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">USER</span><span style="color:#e6db74"> root</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> apt update <span style="color:#f92672">&amp;&amp;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    apt install -y ncat<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>The extra bit is <code>ncat</code> which is technically <em>a network cat command</em>. Build it and push it into the wild so your k8s cluster can pull it from <em>somewhere</em>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker build . -t my-image/percona-xtrabackup:8.2.0-1
</span></span></code></pre></div><p>Then let&rsquo;s prepare a leader statefulset.</p>
<p>Note: I&rsquo;m going to simplify the yamls below a little bit and omit irrelevant bits</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">StatefulSet</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">serviceName</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">replicas</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">updateStrategy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">RollingUpdate</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">app</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">mysql:8.2</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">args</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#75715e"># leader options</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--server-id=1&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--gtid-mode=ON&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">MYSQL_USER</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">MYSQL_USER</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">MYSQL_DATABASE</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">MYSQL_DATABASE</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">MYSQL_PASSWORD</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">MYSQL_PASSWORD</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">containerPort</span>: <span style="color:#ae81ff">3306</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">volumeMounts</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/var/lib/mysql</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">xtrabackup</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">my-image/percona-xtrabackup:8.2.0-1</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">xtrabackup</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">containerPort</span>: <span style="color:#ae81ff">3307</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#ae81ff">bash</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;-c&#34;</span>
</span></span><span style="display:flex;"><span>            - |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              set -ex
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              cd /var/lib/mysql
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              # Start a server to send backups when requested by peers.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              &#34;xtrabackup --backup --slave-info --stream=xbstream --host=127.0.0.1 --user=root --password=$MYSQL_ROOT_PASSWORD&#34;</span>              
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">volumeMounts</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-cloud</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/var/lib/mysql</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">persistentVolumeClaim</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">claimName</span>: <span style="color:#ae81ff">mysql</span>
</span></span></code></pre></div><ol>
<li>We&rsquo;re gonna explicitly <a href="https://dev.mysql.com/doc/refman/8.0/en/replication-options.html#sysvar_server_id">set the server_id</a> and enable <a href="https://dev.mysql.com/doc/refman/8.0/en/replication-options-gtids.html#sysvar_gtid_mode">GTID based logging</a> in our mysql container.</li>
<li>We&rsquo;re gonna start <a href="https://docs.percona.com/percona-xtrabackup/2.4/intro.html#what-are-the-features-of-percona-xtrabackup">xtrabackup</a> so follower can request fresh copy of the database</li>
</ol>
<p>That alone <em>should work</em> - you should have a pod with two containers running - mysql and xtrabackup waiting for the requests.</p>
<p>Now let&rsquo;s create a follower mysql user using mysql command line:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">USER</span> <span style="color:#e6db74">&#39;follower&#39;</span><span style="color:#f92672">@</span><span style="color:#e6db74">&#39;%&#39;</span> IDENTIFIED <span style="color:#66d9ef">BY</span> <span style="color:#e6db74">&#39;&lt;your password&gt;&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">GRANT</span> REPLICATION SLAVE <span style="color:#66d9ef">ON</span> <span style="color:#f92672">*</span>.<span style="color:#f92672">*</span> <span style="color:#66d9ef">TO</span> <span style="color:#e6db74">&#39;follower&#39;</span><span style="color:#f92672">@</span><span style="color:#e6db74">&#39;%&#39;</span>;
</span></span><span style="display:flex;"><span>FLUSH <span style="color:#66d9ef">PRIVILEGES</span>;
</span></span></code></pre></div><p>and store that <code>REPLICATION_USER</code> and  <code>REPLICATION_PASSWORD</code> in <code>mysql-follower</code> secret - we&rsquo;re going to need that in the follower.</p>
<p>Speaking of - the follower bit:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">StatefulSet</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">serviceName</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">replicas</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">updateStrategy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">RollingUpdate</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">app</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">initContainers</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">clone-mysql</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">my-image/percona-xtrabackup:8.2.0-1</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#ae81ff">bash</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;-c&#34;</span>
</span></span><span style="display:flex;"><span>            - |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              set -ex
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              # Skip the clone if data already exists.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              [[ -d /var/lib/mysql/mysql ]] &amp;&amp; exit 0
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              # Clone data from leader
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              ncat --recv-only mysql 3307 | xbstream -x -C /var/lib/mysql
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              # Prepare the backup.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              xtrabackup --prepare --target-dir=/var/lib/mysql</span>              
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">volumeMounts</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/var/lib/mysql</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">mysql:8.2</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">args</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#75715e"># follower options</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--server-id=2&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--gtid-mode=ON&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--log-bin=OFF&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--enforce-gtid-consistency=ON&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--super-read-only&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--skip-replica-start=ON&#34;</span> <span style="color:#75715e"># IMPORTANT: You will want to switch it to OFF after you set everything up</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">containerPort</span>: <span style="color:#ae81ff">3306</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">volumeMounts</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/var/lib/mysql</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">replication</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">mysql:8.2</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#ae81ff">bash</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;-c&#34;</span>
</span></span><span style="display:flex;"><span>            - |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              set -e
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              cd /var/lib/mysql
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              # This file should be created by init container:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              # https://docs.percona.com/percona-xtrabackup/2.4/xtrabackup_bin/working_with_binary_logs.html
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              if [[ -f xtrabackup_binlog_info ]]; then
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                echo &#34;xtrabackup_binlog_info exists&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                cat xtrabackup_binlog_info
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                [[ $(cat xtrabackup_binlog_info) =~ ^([^.]*.[0-9]+)[[:space:]]+([0-9]+)[[:space:]]+([0-9a-zA-Z:-]+) ]] || exit 1
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                rm -f xtrabackup_binlog_info xtrabackup_slave_info
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                GTID_MATCHED=${BASH_REMATCH[3]}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                echo &#34;Found GTID match: ${GTID_MATCHED}&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                echo &#34;SET GLOBAL gtid_purged=&#39;${GTID_MATCHED}&#39;;&#34; &gt; gtid_purged.sql.in
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              fi
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              # Check if we need to complete a clone by starting replication.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              if [[ -f gtid_purged.sql.in ]]; then
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                echo &#34;Waiting for mysqld to be ready (accepting connections)&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                until mysql -p&#34;$MYSQL_ROOT_PASSWORD&#34; -h 127.0.0.1 -e &#34;SELECT 1&#34;; do sleep 2; done
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                echo &#34;Initializing replication&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                mysql -h 127.0.0.1 \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      -p&#34;$MYSQL_ROOT_PASSWORD&#34; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      -e &#34;RESET MASTER; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      $(&lt;gtid_purged.sql.in) \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      CHANGE REPLICATION SOURCE TO \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      SOURCE_HOST=&#39;mysql&#39;, \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      SOURCE_USER=&#39;$REPLICATION_USER&#39;, \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      SOURCE_PASSWORD=&#39;$REPLICATION_PASSWORD&#39;, \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      SOURCE_AUTO_POSITION = 1, \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      SOURCE_CONNECT_RETRY = 30; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      START REPLICA;&#34; || exit 1
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                # In case of container restart, attempt this at-most-once.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                mv gtid_purged.sql.in gtid_purged.sql.orig
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              fi
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              echo &#34;All done&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              trap : TERM INT; sleep infinity &amp; wait</span>              
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">REPLICATION_USER</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">REPLICATION_USER</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">REPLICATION_PASSWORD</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">REPLICATION_PASSWORD</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">volumeMounts</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/var/lib/mysql</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">persistentVolumeClaim</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">claimName</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span></code></pre></div><p>This can be hard to grasp at first glance, what is happening here is:</p>
<ul>
<li>in <code>initContainers</code> we&rsquo;re going to try to <em>clone</em> the mysql using percona <code>xtrabackup</code> from the leader (thus the container with the <code>ncat</code> in our leader statefulset)</li>
<li>xtrabackup <a href="https://docs.percona.com/percona-xtrabackup/2.4/xtrabackup_bin/working_with_binary_logs.html">will create a file</a> called <code>xtrabackup_binlog_info</code> and based on that file we will find <a href="https://dev.mysql.com/doc/refman/8.0/en/replication-options-gtids.html#sysvar_gtid_purged">transaction id</a>  so replica starts replicating from the correct point in the transaction history and <em>then</em> we will start replication. Thus the <code>skip-replica-start</code> definition - honestly speaking I&rsquo;m not sure atm if you can get away with setting this to <code>ON</code> straight away as it&rsquo;s been a while since I set it up - but give it a try and let me know!</li>
</ul>
<p>If everything went right you should see in follower logs something like:</p>
<blockquote>
<p>Replica receiver thread for channel &lsquo;&rsquo;: connected to source &lsquo;follower@mysql:3306&rsquo; with server_uuid=7ee6b0b5-3370-11ee-bd46-be │
│ 0882b78b76, server_id=1. Starting GTID-based replication.</p>
</blockquote>]]></description><content:encoded><![CDATA[<blockquote>
<p>💡 NOTE: Interested in mysql-operator instead? <a href="/blog/2025/12/06/mysql-operator-on-k3s/">See this blogpost</a>.</p>
</blockquote>
<p>Official kubernetes docs <a href="https://kubernetes.io/docs/tasks/run-application/run-replicated-stateful-application/">have a page about running replicated statefulset</a> using mysql, unfortunately:</p>
<ul>
<li>example uses mysql 5.7</li>
<li>it&rsquo;s quite hard to grasp - it uses some custom percona-based image and assumes that you might want to scale statefulset into infinity</li>
<li>as both images are dated it just won&rsquo;t work on ARM machines</li>
</ul>
<p>I think in small to medium organisations/teams/projects what you usually want it some <em>finite</em> amount of replicas - maybe one that you could use for some read-only operations and for doing backups from. So let&rsquo;s try to do that.</p>
<p>Let&rsquo;s create a two <strong>separate</strong> statefulsets under same namespace - one will be leader and another one follower - we will implemented <a href="https://dev.mysql.com/doc/refman/8.4/en/replication-gtids-howto.html">GTID-based replication</a>.</p>
<p>First we&rsquo;re gonna need to prepare <code>xtrabackup</code> docker image:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> bitnami/percona-xtrabackup:8.2.0-1</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">USER</span><span style="color:#e6db74"> root</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> apt update <span style="color:#f92672">&amp;&amp;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    apt install -y ncat<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>The extra bit is <code>ncat</code> which is technically <em>a network cat command</em>. Build it and push it into the wild so your k8s cluster can pull it from <em>somewhere</em>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker build . -t my-image/percona-xtrabackup:8.2.0-1
</span></span></code></pre></div><p>Then let&rsquo;s prepare a leader statefulset.</p>
<p>Note: I&rsquo;m going to simplify the yamls below a little bit and omit irrelevant bits</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">StatefulSet</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">serviceName</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">replicas</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">updateStrategy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">RollingUpdate</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">app</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">mysql:8.2</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">args</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#75715e"># leader options</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--server-id=1&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--gtid-mode=ON&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">MYSQL_USER</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">MYSQL_USER</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">MYSQL_DATABASE</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">MYSQL_DATABASE</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">MYSQL_PASSWORD</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">MYSQL_PASSWORD</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">containerPort</span>: <span style="color:#ae81ff">3306</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">volumeMounts</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/var/lib/mysql</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">xtrabackup</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">my-image/percona-xtrabackup:8.2.0-1</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">xtrabackup</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">containerPort</span>: <span style="color:#ae81ff">3307</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#ae81ff">bash</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;-c&#34;</span>
</span></span><span style="display:flex;"><span>            - |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              set -ex
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              cd /var/lib/mysql
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              # Start a server to send backups when requested by peers.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              &#34;xtrabackup --backup --slave-info --stream=xbstream --host=127.0.0.1 --user=root --password=$MYSQL_ROOT_PASSWORD&#34;</span>              
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">volumeMounts</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-cloud</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/var/lib/mysql</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">persistentVolumeClaim</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">claimName</span>: <span style="color:#ae81ff">mysql</span>
</span></span></code></pre></div><ol>
<li>We&rsquo;re gonna explicitly <a href="https://dev.mysql.com/doc/refman/8.0/en/replication-options.html#sysvar_server_id">set the server_id</a> and enable <a href="https://dev.mysql.com/doc/refman/8.0/en/replication-options-gtids.html#sysvar_gtid_mode">GTID based logging</a> in our mysql container.</li>
<li>We&rsquo;re gonna start <a href="https://docs.percona.com/percona-xtrabackup/2.4/intro.html#what-are-the-features-of-percona-xtrabackup">xtrabackup</a> so follower can request fresh copy of the database</li>
</ol>
<p>That alone <em>should work</em> - you should have a pod with two containers running - mysql and xtrabackup waiting for the requests.</p>
<p>Now let&rsquo;s create a follower mysql user using mysql command line:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">USER</span> <span style="color:#e6db74">&#39;follower&#39;</span><span style="color:#f92672">@</span><span style="color:#e6db74">&#39;%&#39;</span> IDENTIFIED <span style="color:#66d9ef">BY</span> <span style="color:#e6db74">&#39;&lt;your password&gt;&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">GRANT</span> REPLICATION SLAVE <span style="color:#66d9ef">ON</span> <span style="color:#f92672">*</span>.<span style="color:#f92672">*</span> <span style="color:#66d9ef">TO</span> <span style="color:#e6db74">&#39;follower&#39;</span><span style="color:#f92672">@</span><span style="color:#e6db74">&#39;%&#39;</span>;
</span></span><span style="display:flex;"><span>FLUSH <span style="color:#66d9ef">PRIVILEGES</span>;
</span></span></code></pre></div><p>and store that <code>REPLICATION_USER</code> and  <code>REPLICATION_PASSWORD</code> in <code>mysql-follower</code> secret - we&rsquo;re going to need that in the follower.</p>
<p>Speaking of - the follower bit:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">StatefulSet</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">serviceName</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">replicas</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">updateStrategy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">RollingUpdate</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">app</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">initContainers</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">clone-mysql</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">my-image/percona-xtrabackup:8.2.0-1</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#ae81ff">bash</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;-c&#34;</span>
</span></span><span style="display:flex;"><span>            - |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              set -ex
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              # Skip the clone if data already exists.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              [[ -d /var/lib/mysql/mysql ]] &amp;&amp; exit 0
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              # Clone data from leader
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              ncat --recv-only mysql 3307 | xbstream -x -C /var/lib/mysql
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              # Prepare the backup.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              xtrabackup --prepare --target-dir=/var/lib/mysql</span>              
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">volumeMounts</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/var/lib/mysql</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">mysql:8.2</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">args</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#75715e"># follower options</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--server-id=2&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--gtid-mode=ON&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--log-bin=OFF&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--enforce-gtid-consistency=ON&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--super-read-only&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;--skip-replica-start=ON&#34;</span> <span style="color:#75715e"># IMPORTANT: You will want to switch it to OFF after you set everything up</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">containerPort</span>: <span style="color:#ae81ff">3306</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">volumeMounts</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/var/lib/mysql</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">replication</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">mysql:8.2</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#ae81ff">bash</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#e6db74">&#34;-c&#34;</span>
</span></span><span style="display:flex;"><span>            - |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              set -e
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              cd /var/lib/mysql
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              # This file should be created by init container:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              # https://docs.percona.com/percona-xtrabackup/2.4/xtrabackup_bin/working_with_binary_logs.html
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              if [[ -f xtrabackup_binlog_info ]]; then
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                echo &#34;xtrabackup_binlog_info exists&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                cat xtrabackup_binlog_info
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                [[ $(cat xtrabackup_binlog_info) =~ ^([^.]*.[0-9]+)[[:space:]]+([0-9]+)[[:space:]]+([0-9a-zA-Z:-]+) ]] || exit 1
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                rm -f xtrabackup_binlog_info xtrabackup_slave_info
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                GTID_MATCHED=${BASH_REMATCH[3]}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                echo &#34;Found GTID match: ${GTID_MATCHED}&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                echo &#34;SET GLOBAL gtid_purged=&#39;${GTID_MATCHED}&#39;;&#34; &gt; gtid_purged.sql.in
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              fi
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              # Check if we need to complete a clone by starting replication.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              if [[ -f gtid_purged.sql.in ]]; then
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                echo &#34;Waiting for mysqld to be ready (accepting connections)&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                until mysql -p&#34;$MYSQL_ROOT_PASSWORD&#34; -h 127.0.0.1 -e &#34;SELECT 1&#34;; do sleep 2; done
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                echo &#34;Initializing replication&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                mysql -h 127.0.0.1 \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      -p&#34;$MYSQL_ROOT_PASSWORD&#34; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      -e &#34;RESET MASTER; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      $(&lt;gtid_purged.sql.in) \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      CHANGE REPLICATION SOURCE TO \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      SOURCE_HOST=&#39;mysql&#39;, \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      SOURCE_USER=&#39;$REPLICATION_USER&#39;, \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      SOURCE_PASSWORD=&#39;$REPLICATION_PASSWORD&#39;, \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      SOURCE_AUTO_POSITION = 1, \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      SOURCE_CONNECT_RETRY = 30; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                      START REPLICA;&#34; || exit 1
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                # In case of container restart, attempt this at-most-once.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                mv gtid_purged.sql.in gtid_purged.sql.orig
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              fi
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              echo &#34;All done&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              trap : TERM INT; sleep infinity &amp; wait</span>              
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">REPLICATION_USER</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">REPLICATION_USER</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">REPLICATION_PASSWORD</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">REPLICATION_PASSWORD</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">MYSQL_ROOT_PASSWORD</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">volumeMounts</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/var/lib/mysql</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">persistentVolumeClaim</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">claimName</span>: <span style="color:#ae81ff">mysql-follower</span>
</span></span></code></pre></div><p>This can be hard to grasp at first glance, what is happening here is:</p>
<ul>
<li>in <code>initContainers</code> we&rsquo;re going to try to <em>clone</em> the mysql using percona <code>xtrabackup</code> from the leader (thus the container with the <code>ncat</code> in our leader statefulset)</li>
<li>xtrabackup <a href="https://docs.percona.com/percona-xtrabackup/2.4/xtrabackup_bin/working_with_binary_logs.html">will create a file</a> called <code>xtrabackup_binlog_info</code> and based on that file we will find <a href="https://dev.mysql.com/doc/refman/8.0/en/replication-options-gtids.html#sysvar_gtid_purged">transaction id</a>  so replica starts replicating from the correct point in the transaction history and <em>then</em> we will start replication. Thus the <code>skip-replica-start</code> definition - honestly speaking I&rsquo;m not sure atm if you can get away with setting this to <code>ON</code> straight away as it&rsquo;s been a while since I set it up - but give it a try and let me know!</li>
</ul>
<p>If everything went right you should see in follower logs something like:</p>
<blockquote>
<p>Replica receiver thread for channel &lsquo;&rsquo;: connected to source &lsquo;follower@mysql:3306&rsquo; with server_uuid=7ee6b0b5-3370-11ee-bd46-be │
│ 0882b78b76, server_id=1. Starting GTID-based replication.</p>
</blockquote>]]></content:encoded></item><item><title>How to upgrade your project to ESLint 9</title><link>https://awesomeprogrammer.com/blog/2024/04/30/how-to-upgrade-your-project-to-eslint-9/</link><pubDate>Tue, 30 Apr 2024 05:29:29 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2024/04/30/how-to-upgrade-your-project-to-eslint-9/</guid><description><![CDATA[<p>Follow this simple, 5-step program to save your sanity!</p>
<ol>
<li>Remove ESLint.</li>
<li><a href="https://prettier.io/docs/en/install">Install Prettier</a>.</li>
<li>Rewrite yourcodebase with <code>npx prettier . --write</code></li>
<li>Commit changes.</li>
<li>Update <a href="https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view">.git-blame-ignore-revs</a> to remove noise from <code>git blame</code>.</li>
</ol>
<p>/s</p>]]></description><content:encoded><![CDATA[<p>Follow this simple, 5-step program to save your sanity!</p>
<ol>
<li>Remove ESLint.</li>
<li><a href="https://prettier.io/docs/en/install">Install Prettier</a>.</li>
<li>Rewrite yourcodebase with <code>npx prettier . --write</code></li>
<li>Commit changes.</li>
<li>Update <a href="https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view">.git-blame-ignore-revs</a> to remove noise from <code>git blame</code>.</li>
</ol>
<p>/s</p>]]></content:encoded></item><item><title>Goodbye Dependabot, hello Renovate</title><link>https://awesomeprogrammer.com/blog/2024/01/28/goodbye-dependabot-hello-renovate/</link><pubDate>Sun, 28 Jan 2024 19:21:07 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2024/01/28/goodbye-dependabot-hello-renovate/</guid><description><![CDATA[<p>For years, I have been ignoring Renovate because Dependabot <em>was just there</em> - it seems like it was build-into Github forever (it was not; Github acquired dependabot mid 2019).</p>
<p>As <a href="https://github.com/dependabot/dependabot-core#new-ecosystems">dependabot is not accepting</a> support for new ecosystems, I decided to give Renovate a try, and <em>OH BOY</em> did I missed out on much. In comparison, it makes dependabot looks like toy for little kids. I have been using it for few weeks now and some pros.</p>
<ul>
<li>
<p>If you&rsquo;re running on Kubernetes, it&rsquo;s extremely easy to self-host via the <a href="https://github.com/renovatebot/helm-charts">official Helm chart</a>.</p>
</li>
<li>
<p>Even if you&rsquo;re not, you can run it as part of your CI pipeline as a scheduled job.</p>
</li>
<li>
<p>It ships with its <a href="https://docs.renovatebot.com/key-concepts/dashboard/">dashboard concept</a>, which greatly improves visibility regarding outdated dependencies.</p>
</li>
<li>
<p>It is highly configurable when it comes to grouping updates - that includes grouping updates across different ecosystems (need to upgrade some Ruby &amp; NPM packages in one go? <em>Checked</em>)</p>
</li>
<li>
<p>have great out-of-the-box support for popular ecosystems; updating both orbs and docker images in your CircleCI configuration? <em>Checked</em></p>
</li>
<li>
<p>Have crazy feature called <a href="https://docs.renovatebot.com/modules/manager/regex/">regexp manager</a>, which in practice will allow you to update anything; the downside - it&rsquo;s regular expression based</p>
</li>
</ul>
<h3 id="regex-matcher---real-life-example">Regex matcher - Real-life example</h3>
<p>Let&rsquo;s say you&rsquo;re using Terraform to manage a project in ArgoCD using the ArgoCD provider.</p>
<p>Renovate supports out of the box <code>helm_release</code>, but the ArgoCD provider defines its own application where <em>you can</em> specify <code>helm</code> attributes.</p>
<p>One way to work around the problem is to shove versions into variable file(s) and annotate those using comment so renovate can parse those.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;argocd_application&#34;</span> <span style="color:#e6db74">&#34;kurded&#34;</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # details ommited
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">spec</span> {  
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">source</span> {  
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">repo_url</span>        = <span style="color:#e6db74">&#34;https://kubereboot.github.io/charts&#34;</span>  
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">chart</span>           = <span style="color:#e6db74">&#34;kured&#34;</span>  
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">target_revision</span> = var.<span style="color:#a6e22e">kured_version</span>  
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">helm</span> {  
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">release_name</span> = <span style="color:#e6db74">&#34;kured&#34;</span>  
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">values</span> = yamlencode({  
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">configuration</span> = {  
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">rebootDays</span>   = [<span style="color:#e6db74">&#34;su&#34;</span>]  
</span></span><span style="display:flex;"><span>          }  
</span></span><span style="display:flex;"><span>        })  
</span></span><span style="display:flex;"><span>      }  
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># variables-helm.tf
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;kured_version&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">default</span> = <span style="color:#e6db74">&#34;5.3.2&#34;</span><span style="color:#75715e"> // renovate: dep=kured chart=https://kubereboot.github.io/charts  
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><p>We&rsquo;re going to write custom matcher that will lookup <code>variables-helm.tf</code> file(s) and match whole <code>default... // renovate:</code> lines with capture groups</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;customManagers&#34;</span>: [  
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;customType&#34;</span>: <span style="color:#e6db74">&#34;regex&#34;</span>,  
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;datasourceTemplate&#34;</span>: <span style="color:#e6db74">&#34;helm&#34;</span>,  
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;fileMatch&#34;</span>: [  
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;(^|/)variables\\-helm\\.tf$&#34;</span>  
</span></span><span style="display:flex;"><span>      ],  
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;matchStrings&#34;</span>: [  
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;default = \&#34;+(?&lt;currentValue&gt;[^&#39;\&#34; ]+)\&#34; +\\/\\/ renovate: dep=(?&lt;depName&gt;[\\a-z\\\\]+)chart=(?&lt;registryUrl&gt;[^ \\n]+)&#34;</span>  
</span></span><span style="display:flex;"><span>      ]  
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The whole notation is quite cryptic at first glance and everything <a href="https://docs.renovatebot.com/modules/manager/regex/">is well explained</a> in Renovate&rsquo;s official docs, but what we&rsquo;re really doing here is telling Renovate &ldquo;hey, this is a helm chart definition - grab name + version + chart url from the string using predefined capture groups&rdquo;. Note: you might need to adapt <code>depName</code> matcher for you needs as it&rsquo;s not very greedy.</p>
<p>This is pretty powerful feature as you can adapt Renovate for your own custom needs without writing any &ldquo;real&rdquo; code.</p>
<figure><img src="/images/dependabot.png"/><figcaption>
            <h4>Yes, I can also generate images with DALL·E (Source: prompt)</h4>
        </figcaption>
</figure>]]></description><content:encoded><![CDATA[<p>For years, I have been ignoring Renovate because Dependabot <em>was just there</em> - it seems like it was build-into Github forever (it was not; Github acquired dependabot mid 2019).</p>
<p>As <a href="https://github.com/dependabot/dependabot-core#new-ecosystems">dependabot is not accepting</a> support for new ecosystems, I decided to give Renovate a try, and <em>OH BOY</em> did I missed out on much. In comparison, it makes dependabot looks like toy for little kids. I have been using it for few weeks now and some pros.</p>
<ul>
<li>
<p>If you&rsquo;re running on Kubernetes, it&rsquo;s extremely easy to self-host via the <a href="https://github.com/renovatebot/helm-charts">official Helm chart</a>.</p>
</li>
<li>
<p>Even if you&rsquo;re not, you can run it as part of your CI pipeline as a scheduled job.</p>
</li>
<li>
<p>It ships with its <a href="https://docs.renovatebot.com/key-concepts/dashboard/">dashboard concept</a>, which greatly improves visibility regarding outdated dependencies.</p>
</li>
<li>
<p>It is highly configurable when it comes to grouping updates - that includes grouping updates across different ecosystems (need to upgrade some Ruby &amp; NPM packages in one go? <em>Checked</em>)</p>
</li>
<li>
<p>have great out-of-the-box support for popular ecosystems; updating both orbs and docker images in your CircleCI configuration? <em>Checked</em></p>
</li>
<li>
<p>Have crazy feature called <a href="https://docs.renovatebot.com/modules/manager/regex/">regexp manager</a>, which in practice will allow you to update anything; the downside - it&rsquo;s regular expression based</p>
</li>
</ul>
<h3 id="regex-matcher---real-life-example">Regex matcher - Real-life example</h3>
<p>Let&rsquo;s say you&rsquo;re using Terraform to manage a project in ArgoCD using the ArgoCD provider.</p>
<p>Renovate supports out of the box <code>helm_release</code>, but the ArgoCD provider defines its own application where <em>you can</em> specify <code>helm</code> attributes.</p>
<p>One way to work around the problem is to shove versions into variable file(s) and annotate those using comment so renovate can parse those.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;argocd_application&#34;</span> <span style="color:#e6db74">&#34;kurded&#34;</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # details ommited
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">spec</span> {  
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">source</span> {  
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">repo_url</span>        = <span style="color:#e6db74">&#34;https://kubereboot.github.io/charts&#34;</span>  
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">chart</span>           = <span style="color:#e6db74">&#34;kured&#34;</span>  
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">target_revision</span> = var.<span style="color:#a6e22e">kured_version</span>  
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">helm</span> {  
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">release_name</span> = <span style="color:#e6db74">&#34;kured&#34;</span>  
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">values</span> = yamlencode({  
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">configuration</span> = {  
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">rebootDays</span>   = [<span style="color:#e6db74">&#34;su&#34;</span>]  
</span></span><span style="display:flex;"><span>          }  
</span></span><span style="display:flex;"><span>        })  
</span></span><span style="display:flex;"><span>      }  
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># variables-helm.tf
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;kured_version&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">default</span> = <span style="color:#e6db74">&#34;5.3.2&#34;</span><span style="color:#75715e"> // renovate: dep=kured chart=https://kubereboot.github.io/charts  
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><p>We&rsquo;re going to write custom matcher that will lookup <code>variables-helm.tf</code> file(s) and match whole <code>default... // renovate:</code> lines with capture groups</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;customManagers&#34;</span>: [  
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;customType&#34;</span>: <span style="color:#e6db74">&#34;regex&#34;</span>,  
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;datasourceTemplate&#34;</span>: <span style="color:#e6db74">&#34;helm&#34;</span>,  
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;fileMatch&#34;</span>: [  
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;(^|/)variables\\-helm\\.tf$&#34;</span>  
</span></span><span style="display:flex;"><span>      ],  
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;matchStrings&#34;</span>: [  
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;default = \&#34;+(?&lt;currentValue&gt;[^&#39;\&#34; ]+)\&#34; +\\/\\/ renovate: dep=(?&lt;depName&gt;[\\a-z\\\\]+)chart=(?&lt;registryUrl&gt;[^ \\n]+)&#34;</span>  
</span></span><span style="display:flex;"><span>      ]  
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The whole notation is quite cryptic at first glance and everything <a href="https://docs.renovatebot.com/modules/manager/regex/">is well explained</a> in Renovate&rsquo;s official docs, but what we&rsquo;re really doing here is telling Renovate &ldquo;hey, this is a helm chart definition - grab name + version + chart url from the string using predefined capture groups&rdquo;. Note: you might need to adapt <code>depName</code> matcher for you needs as it&rsquo;s not very greedy.</p>
<p>This is pretty powerful feature as you can adapt Renovate for your own custom needs without writing any &ldquo;real&rdquo; code.</p>
<figure><img src="/images/dependabot.png"/><figcaption>
            <h4>Yes, I can also generate images with DALL·E (Source: prompt)</h4>
        </figcaption>
</figure>]]></content:encoded></item><item><title>Chrome 116+ refusing to ask for camera/microphone permissions (jitsi example)</title><link>https://awesomeprogrammer.com/blog/2023/08/29/chrome-116-refusing-to-ask-for-camera/microphone-permissions-jitsi-example/</link><pubDate>Tue, 29 Aug 2023 14:56:06 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2023/08/29/chrome-116-refusing-to-ask-for-camera/microphone-permissions-jitsi-example/</guid><description><![CDATA[<p>This is a very brief post as I wasted literally <em>hours</em> of this (mostly running in circles) which turned out to be simply trivial. I hope that maybe it might help some lost soul someday.</p>
<h3 id="the-problem">The problem</h3>
<p>You are using self-hosted jitsi and Chrome 116+ refuses to ask the user for camera/microphone permissions.</p>
<h3 id="the-reason">The reason</h3>
<p>Jitsi up-to-moment-of-writing this uses <a href="https://www.chromium.org/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes">deprecated iframe permissions</a>. That will be painful if you&rsquo;re embedding jitsi under a different subdomain for example. If you look into <a href="https://meet.jit.si/external_api.js">it&rsquo;s source</a> you will find:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">_frame</span>.<span style="color:#a6e22e">allow</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;camera; microphone; display-capture; autoplay; clipboard-write; hid; screen-wake-lock&#34;</span>
</span></span></code></pre></div><h3 id="the-solution">The solution</h3>
<p>I don&rsquo;t have a good one, you can copy-paste the whole <code>external_api.js</code> under a <code>&lt;script&gt;</code> tag and just modify the offensive line accordingly. Something like this should do it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">_frame</span>.<span style="color:#a6e22e">allow</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;camera *; microphone *; display-capture *; autoplay *; clipboard-write *; hid *; screen-wake-lock *&#34;</span>
</span></span></code></pre></div><p>You might want to tweak <code>*</code> to something more specific.</p>]]></description><content:encoded><![CDATA[<p>This is a very brief post as I wasted literally <em>hours</em> of this (mostly running in circles) which turned out to be simply trivial. I hope that maybe it might help some lost soul someday.</p>
<h3 id="the-problem">The problem</h3>
<p>You are using self-hosted jitsi and Chrome 116+ refuses to ask the user for camera/microphone permissions.</p>
<h3 id="the-reason">The reason</h3>
<p>Jitsi up-to-moment-of-writing this uses <a href="https://www.chromium.org/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes">deprecated iframe permissions</a>. That will be painful if you&rsquo;re embedding jitsi under a different subdomain for example. If you look into <a href="https://meet.jit.si/external_api.js">it&rsquo;s source</a> you will find:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">_frame</span>.<span style="color:#a6e22e">allow</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;camera; microphone; display-capture; autoplay; clipboard-write; hid; screen-wake-lock&#34;</span>
</span></span></code></pre></div><h3 id="the-solution">The solution</h3>
<p>I don&rsquo;t have a good one, you can copy-paste the whole <code>external_api.js</code> under a <code>&lt;script&gt;</code> tag and just modify the offensive line accordingly. Something like this should do it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">_frame</span>.<span style="color:#a6e22e">allow</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;camera *; microphone *; display-capture *; autoplay *; clipboard-write *; hid *; screen-wake-lock *&#34;</span>
</span></span></code></pre></div><p>You might want to tweak <code>*</code> to something more specific.</p>]]></content:encoded></item><item><title>I have a single 1.4TB mysql table, human help</title><link>https://awesomeprogrammer.com/blog/2023/08/28/i-have-a-single-1.4tb-mysql-table-human-help/</link><pubDate>Mon, 28 Aug 2023 19:34:47 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2023/08/28/i-have-a-single-1.4tb-mysql-table-human-help/</guid><description><![CDATA[<p>Data is akin to code - the best data is data that is never stored. Unfortunately, this is not how products operate, and you might find yourself in a situation where data becomes a liability and a burden.</p>
<p>Recently, I have been working on a side project where the majority of the data in the app was stored in a single table. That table grew beyond a maintainable point; major pain points are nothing new, and in my case, it boiled down to:</p>
<ul>
<li>
<p>except single index I was screwed when it comes to querying it; every query becomes gymnastic where I had to iterate over your collection as a straightforward query would probably hang for days in the database</p>
</li>
<li>
<p>forget about adding new columns or modifying the structure; if you&rsquo;re lucky you might run on a recent postgres/mariadb/whatever engine that allows you to add columns in an <em>instant</em> manner (or you are living the NoSQL live and have a different set of problems); given data growth you&rsquo;re probably running some legacy version and you&rsquo;re screwed again</p>
</li>
<li>
<p>assuming you <em>can</em> schedule a maintenance window it would probably take days <em>and</em> you have to have another table-size-TB free space for potential table rebuild</p>
</li>
<li>
<p>operational-wise it&rsquo;s another rotten egg - think backups/restore pain</p>
</li>
</ul>
<p>So how to deal with it? As usual - <em>it depends</em>; the natural solution would be sharding/partitioning or even a mix of both. Then with partitioning, you might look for native solutions (provided by your RDBMS) or do it on the application level - which <em>also</em> can make sense.</p>
<p>I was working with MariaDB and Rails and in the end, I chose partial sharding (moving majority of the data into brand new DB with MariaDB&rsquo;s <a href="https://mariadb.com/kb/en/innodb-page-compression/">page compression</a> enabled) and partitioning on application level based on a <em>range</em> - I&rsquo;m not going to go into technical details here as it&rsquo;s very application specific, but majority of the new data lands in <em>new</em> partitions (or in my case - physical tables).</p>
<p>As a side product of this decision - separating tables physically allowed me to deal with some minor optimizations per table level. The data was 14y old and there were some inconsistencies caused by various bugs introduced (and then fixed) over the years.</p>
<h4 id="how-to-do-it-safely">How to do it safely?</h4>
<p>I have no idea, but this is how I did it:</p>
<p>First I updated rails, d&rsquo;oh. Rails 6.1 ships with REALLY good support for multiple databases; you don&rsquo;t have to think about migrations, or managing multiple schemas - everything <em>just works</em>. Kudos to the Rails team for this feature.</p>
<p>Then - no <em>magical</em> solution - I had to wrap every call that touched that table in some abstraction class. That allowed me to put the read/write from old table/new tables behind a global feature flag. I was also able to control read/writes in a controlled manner so the app could continue to run while we worked on the data migration plan.</p>
<p>As the application was relatively simple narrowing down the entry points was relatively easy, but work itself was still <em>painful</em>. Obviously, with sharding - everything that does <em>joins</em> becomes problematic, and up to this day I still have to rethink few problems and how to solve them given the new database structure.</p>
<p><em>Moving</em> the data was also quite tricky, Ruby quickly crumbled under the amount of data (hello strings allocation) and in the end I had to hack spaghetti code CLI tool in golang; few takes away for future self:</p>
<ul>
<li>
<p>using SQLite as a local cache layer for speeding up batching over IDs was a nice idea; the cache grew to a few GB which was a little bit too much for Redis, but SQLite took it without a problem</p>
</li>
<li>
<p>trying to use Ruby for this particular problem wasn&rsquo;t the best idea to begin with - I had a good experience with <a href="https://awesomeprogrammer.com/blog/2021/07/09/what-i-have-done-so-far-part-9/">postgres_fwd</a> but most heavy-lifting was done on the DB level there; in the end I re-used some tests written in rspec and had some <em>integration</em> tests in a way for the whole thing</p>
</li>
<li>
<p>I wish coded the ability for graceful resume sooner; thankfully I was able to run the process for the given date range (with some limitations)</p>
</li>
<li>
<p>I wish that I added simple Slack notifications sooner - were extra useful just to monitor the process as everything was ad-hoc running on a single node in a screen (welp, I got spoiled with the simplicity of &ldquo;deployment&rdquo; - or rather a lack of it - when it comes to shipping small golang apps)</p>
</li>
</ul>
<h4 id="was-mariadbss-page-compression-any-good">Was MariaDBs&rsquo;s page compression any good?</h4>
<p>Yes! We managed to fit all the stuff along with some relationships within ~600GB, so in our case compression ratio was quite pleasing.</p>]]></description><content:encoded><![CDATA[<p>Data is akin to code - the best data is data that is never stored. Unfortunately, this is not how products operate, and you might find yourself in a situation where data becomes a liability and a burden.</p>
<p>Recently, I have been working on a side project where the majority of the data in the app was stored in a single table. That table grew beyond a maintainable point; major pain points are nothing new, and in my case, it boiled down to:</p>
<ul>
<li>
<p>except single index I was screwed when it comes to querying it; every query becomes gymnastic where I had to iterate over your collection as a straightforward query would probably hang for days in the database</p>
</li>
<li>
<p>forget about adding new columns or modifying the structure; if you&rsquo;re lucky you might run on a recent postgres/mariadb/whatever engine that allows you to add columns in an <em>instant</em> manner (or you are living the NoSQL live and have a different set of problems); given data growth you&rsquo;re probably running some legacy version and you&rsquo;re screwed again</p>
</li>
<li>
<p>assuming you <em>can</em> schedule a maintenance window it would probably take days <em>and</em> you have to have another table-size-TB free space for potential table rebuild</p>
</li>
<li>
<p>operational-wise it&rsquo;s another rotten egg - think backups/restore pain</p>
</li>
</ul>
<p>So how to deal with it? As usual - <em>it depends</em>; the natural solution would be sharding/partitioning or even a mix of both. Then with partitioning, you might look for native solutions (provided by your RDBMS) or do it on the application level - which <em>also</em> can make sense.</p>
<p>I was working with MariaDB and Rails and in the end, I chose partial sharding (moving majority of the data into brand new DB with MariaDB&rsquo;s <a href="https://mariadb.com/kb/en/innodb-page-compression/">page compression</a> enabled) and partitioning on application level based on a <em>range</em> - I&rsquo;m not going to go into technical details here as it&rsquo;s very application specific, but majority of the new data lands in <em>new</em> partitions (or in my case - physical tables).</p>
<p>As a side product of this decision - separating tables physically allowed me to deal with some minor optimizations per table level. The data was 14y old and there were some inconsistencies caused by various bugs introduced (and then fixed) over the years.</p>
<h4 id="how-to-do-it-safely">How to do it safely?</h4>
<p>I have no idea, but this is how I did it:</p>
<p>First I updated rails, d&rsquo;oh. Rails 6.1 ships with REALLY good support for multiple databases; you don&rsquo;t have to think about migrations, or managing multiple schemas - everything <em>just works</em>. Kudos to the Rails team for this feature.</p>
<p>Then - no <em>magical</em> solution - I had to wrap every call that touched that table in some abstraction class. That allowed me to put the read/write from old table/new tables behind a global feature flag. I was also able to control read/writes in a controlled manner so the app could continue to run while we worked on the data migration plan.</p>
<p>As the application was relatively simple narrowing down the entry points was relatively easy, but work itself was still <em>painful</em>. Obviously, with sharding - everything that does <em>joins</em> becomes problematic, and up to this day I still have to rethink few problems and how to solve them given the new database structure.</p>
<p><em>Moving</em> the data was also quite tricky, Ruby quickly crumbled under the amount of data (hello strings allocation) and in the end I had to hack spaghetti code CLI tool in golang; few takes away for future self:</p>
<ul>
<li>
<p>using SQLite as a local cache layer for speeding up batching over IDs was a nice idea; the cache grew to a few GB which was a little bit too much for Redis, but SQLite took it without a problem</p>
</li>
<li>
<p>trying to use Ruby for this particular problem wasn&rsquo;t the best idea to begin with - I had a good experience with <a href="https://awesomeprogrammer.com/blog/2021/07/09/what-i-have-done-so-far-part-9/">postgres_fwd</a> but most heavy-lifting was done on the DB level there; in the end I re-used some tests written in rspec and had some <em>integration</em> tests in a way for the whole thing</p>
</li>
<li>
<p>I wish coded the ability for graceful resume sooner; thankfully I was able to run the process for the given date range (with some limitations)</p>
</li>
<li>
<p>I wish that I added simple Slack notifications sooner - were extra useful just to monitor the process as everything was ad-hoc running on a single node in a screen (welp, I got spoiled with the simplicity of &ldquo;deployment&rdquo; - or rather a lack of it - when it comes to shipping small golang apps)</p>
</li>
</ul>
<h4 id="was-mariadbss-page-compression-any-good">Was MariaDBs&rsquo;s page compression any good?</h4>
<p>Yes! We managed to fit all the stuff along with some relationships within ~600GB, so in our case compression ratio was quite pleasing.</p>]]></content:encoded></item><item><title>Backup AWS S3 bucket to external location</title><link>https://awesomeprogrammer.com/blog/2023/06/20/backup-aws-s3-bucket-to-external-location/</link><pubDate>Tue, 20 Jun 2023 06:11:18 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2023/06/20/backup-aws-s3-bucket-to-external-location/</guid><description><![CDATA[<p>Aside from S3 <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/replication.html">bucket replication</a> feature you might want to backup your data <em>outside</em> of your current cloud provider (for a myriad of valid reasons).</p>
<p>If you&rsquo;re in Europe you might consider <a href="https://www.hetzner.com/storage/storage-box">Hetzner</a> which once again offers the best bang for your buck; alternatively - if you&rsquo;re based in the US - <a href="https://www.backblaze.com">Backblaze</a> might be a better fit. Either way, I recommend using <a href="https://rclone.org/">rclone</a> which integrates out-of-the-box with S3 and all the protocols you can think of.</p>
<p>Few tips based on my personal experience:</p>
<ul>
<li>
<p><a href="https://rclone.org/crypt/">enable encryption</a> right away, but be aware that you might run into limitations if you have long filenames, like <a href="https://rclone.org/crypt/#file-name-encryption-modes">over 143 characters</a></p>
</li>
<li>
<p>observe AWS costs, as S3 can skyrocket, depending on your buckets structure (and of course - bucket size); to make the backup job less API/transfer heavy a lot of folks experiment with <a href="https://rclone.org/docs/#fast-list">fast-list</a>, <a href="https://rclone.org/docs/#size-only">size-only</a>, <a href="https://rclone.org/docs/#no-traverse">no-traverse</a> and <a href="https://rclone.org/filtering/#max-age-don-t-transfer-any-file-older-than-this">max-age</a>; all this depends on how you structure your data and might depend on backup destination, just don&rsquo;t be surprised about monthly AWS bill.</p>
</li>
<li>
<p>if you&rsquo;re experimenting with <code>rclone</code> on a metal node using crontab you can stream the logs to syslog from your crontab entry for easier debugging <code>(/usr/local/bin/rclone copy -v rest-of-your-command) 2&gt;&amp;1 | logger -t backup-rclone</code></p>
</li>
<li>
<p>technically you can <a href="https://rclone.org/commands/rclone_mount/">mount</a> even encrypted drive directly to your local system (<a href="https://osxfuse.github.io/">even on MacOS</a>) - it can be <em>slightly</em> slow if you try to open ridiculously big bucket/directory</p>
</li>
</ul>
<p>For remote-to-local backups with versioning I also use <a href="https://restic.net/">restic</a> which <em>can</em> be paired with <a href="https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#other-services-via-rclone">rclone</a>, but I recall running into some edge cases and abandoning the idea.</p>]]></description><content:encoded><![CDATA[<p>Aside from S3 <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/replication.html">bucket replication</a> feature you might want to backup your data <em>outside</em> of your current cloud provider (for a myriad of valid reasons).</p>
<p>If you&rsquo;re in Europe you might consider <a href="https://www.hetzner.com/storage/storage-box">Hetzner</a> which once again offers the best bang for your buck; alternatively - if you&rsquo;re based in the US - <a href="https://www.backblaze.com">Backblaze</a> might be a better fit. Either way, I recommend using <a href="https://rclone.org/">rclone</a> which integrates out-of-the-box with S3 and all the protocols you can think of.</p>
<p>Few tips based on my personal experience:</p>
<ul>
<li>
<p><a href="https://rclone.org/crypt/">enable encryption</a> right away, but be aware that you might run into limitations if you have long filenames, like <a href="https://rclone.org/crypt/#file-name-encryption-modes">over 143 characters</a></p>
</li>
<li>
<p>observe AWS costs, as S3 can skyrocket, depending on your buckets structure (and of course - bucket size); to make the backup job less API/transfer heavy a lot of folks experiment with <a href="https://rclone.org/docs/#fast-list">fast-list</a>, <a href="https://rclone.org/docs/#size-only">size-only</a>, <a href="https://rclone.org/docs/#no-traverse">no-traverse</a> and <a href="https://rclone.org/filtering/#max-age-don-t-transfer-any-file-older-than-this">max-age</a>; all this depends on how you structure your data and might depend on backup destination, just don&rsquo;t be surprised about monthly AWS bill.</p>
</li>
<li>
<p>if you&rsquo;re experimenting with <code>rclone</code> on a metal node using crontab you can stream the logs to syslog from your crontab entry for easier debugging <code>(/usr/local/bin/rclone copy -v rest-of-your-command) 2&gt;&amp;1 | logger -t backup-rclone</code></p>
</li>
<li>
<p>technically you can <a href="https://rclone.org/commands/rclone_mount/">mount</a> even encrypted drive directly to your local system (<a href="https://osxfuse.github.io/">even on MacOS</a>) - it can be <em>slightly</em> slow if you try to open ridiculously big bucket/directory</p>
</li>
</ul>
<p>For remote-to-local backups with versioning I also use <a href="https://restic.net/">restic</a> which <em>can</em> be paired with <a href="https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#other-services-via-rclone">rclone</a>, but I recall running into some edge cases and abandoning the idea.</p>]]></content:encoded></item><item><title>Getting SSL working for Traefik Dashboard with k3s and cert-manager</title><link>https://awesomeprogrammer.com/blog/2023/05/06/getting-ssl-working-for-traefik-dashboard-with-k3s-and-cert-manager/</link><pubDate>Sat, 06 May 2023 07:53:13 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2023/05/06/getting-ssl-working-for-traefik-dashboard-with-k3s-and-cert-manager/</guid><description><![CDATA[<p>k3s ships with traefik that works out of the box, but for certificate generation while running on Kubernetes you should consider using <a href="https://github.com/cert-manager/cert-manager">cert-manager</a> which is apparently a go-to tool for certs management in the k8s world.</p>
<p>For the simplicity of this example let&rsquo;s assume you have a single node k3s cluster running which doesn&rsquo;t have any external load balance in front of it (otherwise you might need a DNS challenge and here we will use HTTP-01).</p>
<p>The official way of installing cert-manager seems to be just <code>kubectl apply -f</code>, which reminds me of piping response from curl directly to bash with a <code>sudo</code> on top. Anyway, I shall talk about ArgoCD in some other blog post. So:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.1/cert-manager.yaml
</span></span></code></pre></div><p>Should spin up 3 new pods in <code>cert-manager</code> namespace and create a bunch of other resources.</p>
<p>Now let&rsquo;s configure <a href="https://cert-manager.io/docs/configuration/acme/http01/">ACME HTTP-01</a> using Terraform.</p>
<p>First, configure let&rsquo;s encrypt ClusterIssuer - we&rsquo;re going to use <code>ClusterIssuer</code> and not <code>Issuer</code> so we can issue certificates in any namespace. Is that somehow less secure? I have no idea, but for the sake of let&rsquo;s just go with it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-terraform" data-lang="terraform"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;kubernetes_manifest&#34;</span> <span style="color:#e6db74">&#34;letsencrypt_issuer&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">manifest</span> = {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;apiVersion&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cert-manager.io/v1&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;kind&#34;</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ClusterIssuer&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;metadata&#34;</span>   <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;name&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;letsencrypt-prod&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;spec&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;acme&#34;</span> <span style="color:#f92672">=</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # NOTE: remember to update email here
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#e6db74">&#34;email&#34;</span>               <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;your@email&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;server&#34;</span>              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://acme-v02.api.letsencrypt.org/directory&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;privateKeySecretRef&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#e6db74">&#34;name&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;letsencrypt-prod&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;solvers&#34;</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>          {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;http01&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;ingress&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;class&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;traefik&#34;</span>
</span></span><span style="display:flex;"><span>              }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Applying the plan should create a new resource, but nothing interesting will happen yet. Now let&rsquo;s try to generate a new certificate; at this point, you should have a domain (see <code>traefik.mydomain.com</code> part) pointing to your k3s node.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-terraform" data-lang="terraform"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;kubernetes_manifest&#34;</span> <span style="color:#e6db74">&#34;traefik_cert&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">manifest</span> = {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;apiVersion&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cert-manager.io/v1&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;kind&#34;</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Certificate&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;metadata&#34;</span>   <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;name&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;traefik-cert&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;namespace&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;kube-system&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;spec&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;secretName&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;traefik-cert&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;issuerRef&#34;</span>  <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;name&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;letsencrypt-prod&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;kind&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ClusterIssuer&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;dnsNames&#34;</span> <span style="color:#f92672">=</span> [<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # NOTE: remember to change this
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#e6db74">&#34;traefik.mydomain.com&#34;</span>
</span></span><span style="display:flex;"><span>      ]
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>When you apply the plan, the cert-manager should create a new resource - <code>cert-manager.io/v1/certificaterequests</code> - and will spin a new temporary pod that will serve an HTTP-01 request. Everything should work automagically with the default k3s traefik configuration (as we specified <code>ingress</code> class in the <code>ClusterIssuer</code> definition).</p>
<p>The new certificate should be stored in <code>secrets</code> as <code>kubernetes.io/tls</code>. Now we can expose traefik dashboard to the world.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-terraform" data-lang="terraform"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;kubernetes_manifest&#34;</span> <span style="color:#e6db74">&#34;traefik_route&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">manifest</span> = {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;apiVersion&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;traefik.containo.us/v1alpha1&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;kind&#34;</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;IngressRoute&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;metadata&#34;</span>   <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;name&#34;</span>        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;dashboard-ingressroute&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;namespace&#34;</span>   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;kube-system&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;spec&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;entryPoints&#34;</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;websecure&#34;</span>
</span></span><span style="display:flex;"><span>      ]
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;routes&#34;</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>          <span style="color:#e6db74">&#34;kind&#34;</span>        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Rule&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          # NOTE: remember to change the domain
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>          <span style="color:#e6db74">&#34;match&#34;</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Host(`traefik.mydomain.com`) &amp;&amp; (PathPrefix(`/dashboard`) || PathPrefix(`/api`))&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#e6db74">&#34;services&#34;</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;name&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;api@internal&#34;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;kind&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;TraefikService&#34;</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          ]
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      ]
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;tls&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;secretName&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;traefik-cert&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>After applying plan once you go to <code>traefik.mydomain.com/dashboard</code> you should see good, old traefik web ui dashboard.</p>
<figure><img src="/images/k3s-traefik-web-ui.png"/><figcaption>
            <h4>Hello, old friend.</h4>
        </figcaption>
</figure>

<h3 id="is-there-a-simpler-way">Is there a simpler way?</h3>
<p>If prefer achieving same result using more <em>automagical</em> approach you can try using <code>annotations</code> - look for <a href="https://cert-manager.io/docs/usage/ingress/">cert-manager.io/cluster-issuer</a>.</p>
<h3 id="what-about-auto-https-redirect">What about auto https redirect?</h3>
<p>You might be tempted to redirect all non-http traffic to https automatically by configuring a traefik helm chart shipped with k3s by setting chart <code>values</code> to something like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">web</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">redirectTo</span>: <span style="color:#ae81ff">websecure</span>
</span></span></code></pre></div><p>That seems to be breaking the cert-manager HTTP-01 challenge so I would recommend doing http-&gt;https redirect explicitly by specifying redirect middleware. That said <a href="https://doc.traefik.io/traefik/providers/kubernetes-crd/#allowcrossnamespace">allowCrossNamespace</a> might interest you in case you don&rsquo;t want to define same middleware in every namespace.</p>
<p>&hellip;or you can try using <a href="https://cert-manager.io/docs/configuration/acme/dns01/">DNS01</a> challenge instead (you can configure two or more separate <code>ClusterIssuer</code>s, which what I ended up eventually due to my DNS entries scattered across few providers <em>because some historical reasons</em>).</p>]]></description><content:encoded><![CDATA[<p>k3s ships with traefik that works out of the box, but for certificate generation while running on Kubernetes you should consider using <a href="https://github.com/cert-manager/cert-manager">cert-manager</a> which is apparently a go-to tool for certs management in the k8s world.</p>
<p>For the simplicity of this example let&rsquo;s assume you have a single node k3s cluster running which doesn&rsquo;t have any external load balance in front of it (otherwise you might need a DNS challenge and here we will use HTTP-01).</p>
<p>The official way of installing cert-manager seems to be just <code>kubectl apply -f</code>, which reminds me of piping response from curl directly to bash with a <code>sudo</code> on top. Anyway, I shall talk about ArgoCD in some other blog post. So:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.1/cert-manager.yaml
</span></span></code></pre></div><p>Should spin up 3 new pods in <code>cert-manager</code> namespace and create a bunch of other resources.</p>
<p>Now let&rsquo;s configure <a href="https://cert-manager.io/docs/configuration/acme/http01/">ACME HTTP-01</a> using Terraform.</p>
<p>First, configure let&rsquo;s encrypt ClusterIssuer - we&rsquo;re going to use <code>ClusterIssuer</code> and not <code>Issuer</code> so we can issue certificates in any namespace. Is that somehow less secure? I have no idea, but for the sake of let&rsquo;s just go with it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-terraform" data-lang="terraform"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;kubernetes_manifest&#34;</span> <span style="color:#e6db74">&#34;letsencrypt_issuer&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">manifest</span> = {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;apiVersion&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cert-manager.io/v1&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;kind&#34;</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ClusterIssuer&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;metadata&#34;</span>   <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;name&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;letsencrypt-prod&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;spec&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;acme&#34;</span> <span style="color:#f92672">=</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # NOTE: remember to update email here
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#e6db74">&#34;email&#34;</span>               <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;your@email&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;server&#34;</span>              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://acme-v02.api.letsencrypt.org/directory&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;privateKeySecretRef&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#e6db74">&#34;name&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;letsencrypt-prod&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;solvers&#34;</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>          {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;http01&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;ingress&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;class&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;traefik&#34;</span>
</span></span><span style="display:flex;"><span>              }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Applying the plan should create a new resource, but nothing interesting will happen yet. Now let&rsquo;s try to generate a new certificate; at this point, you should have a domain (see <code>traefik.mydomain.com</code> part) pointing to your k3s node.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-terraform" data-lang="terraform"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;kubernetes_manifest&#34;</span> <span style="color:#e6db74">&#34;traefik_cert&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">manifest</span> = {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;apiVersion&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cert-manager.io/v1&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;kind&#34;</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Certificate&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;metadata&#34;</span>   <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;name&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;traefik-cert&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;namespace&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;kube-system&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;spec&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;secretName&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;traefik-cert&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;issuerRef&#34;</span>  <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;name&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;letsencrypt-prod&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;kind&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ClusterIssuer&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;dnsNames&#34;</span> <span style="color:#f92672">=</span> [<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # NOTE: remember to change this
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#e6db74">&#34;traefik.mydomain.com&#34;</span>
</span></span><span style="display:flex;"><span>      ]
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>When you apply the plan, the cert-manager should create a new resource - <code>cert-manager.io/v1/certificaterequests</code> - and will spin a new temporary pod that will serve an HTTP-01 request. Everything should work automagically with the default k3s traefik configuration (as we specified <code>ingress</code> class in the <code>ClusterIssuer</code> definition).</p>
<p>The new certificate should be stored in <code>secrets</code> as <code>kubernetes.io/tls</code>. Now we can expose traefik dashboard to the world.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-terraform" data-lang="terraform"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;kubernetes_manifest&#34;</span> <span style="color:#e6db74">&#34;traefik_route&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">manifest</span> = {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;apiVersion&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;traefik.containo.us/v1alpha1&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;kind&#34;</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;IngressRoute&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;metadata&#34;</span>   <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;name&#34;</span>        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;dashboard-ingressroute&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;namespace&#34;</span>   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;kube-system&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;spec&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;entryPoints&#34;</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;websecure&#34;</span>
</span></span><span style="display:flex;"><span>      ]
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;routes&#34;</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>          <span style="color:#e6db74">&#34;kind&#34;</span>        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Rule&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          # NOTE: remember to change the domain
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>          <span style="color:#e6db74">&#34;match&#34;</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Host(`traefik.mydomain.com`) &amp;&amp; (PathPrefix(`/dashboard`) || PathPrefix(`/api`))&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#e6db74">&#34;services&#34;</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;name&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;api@internal&#34;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;kind&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;TraefikService&#34;</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          ]
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      ]
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;tls&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;secretName&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;traefik-cert&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>After applying plan once you go to <code>traefik.mydomain.com/dashboard</code> you should see good, old traefik web ui dashboard.</p>
<figure><img src="/images/k3s-traefik-web-ui.png"/><figcaption>
            <h4>Hello, old friend.</h4>
        </figcaption>
</figure>

<h3 id="is-there-a-simpler-way">Is there a simpler way?</h3>
<p>If prefer achieving same result using more <em>automagical</em> approach you can try using <code>annotations</code> - look for <a href="https://cert-manager.io/docs/usage/ingress/">cert-manager.io/cluster-issuer</a>.</p>
<h3 id="what-about-auto-https-redirect">What about auto https redirect?</h3>
<p>You might be tempted to redirect all non-http traffic to https automatically by configuring a traefik helm chart shipped with k3s by setting chart <code>values</code> to something like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">web</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">redirectTo</span>: <span style="color:#ae81ff">websecure</span>
</span></span></code></pre></div><p>That seems to be breaking the cert-manager HTTP-01 challenge so I would recommend doing http-&gt;https redirect explicitly by specifying redirect middleware. That said <a href="https://doc.traefik.io/traefik/providers/kubernetes-crd/#allowcrossnamespace">allowCrossNamespace</a> might interest you in case you don&rsquo;t want to define same middleware in every namespace.</p>
<p>&hellip;or you can try using <a href="https://cert-manager.io/docs/configuration/acme/dns01/">DNS01</a> challenge instead (you can configure two or more separate <code>ClusterIssuer</code>s, which what I ended up eventually due to my DNS entries scattered across few providers <em>because some historical reasons</em>).</p>]]></content:encoded></item><item><title>Living the ARM life</title><link>https://awesomeprogrammer.com/blog/2023/04/27/living-the-arm-life/</link><pubDate>Thu, 27 Apr 2023 17:44:13 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2023/04/27/living-the-arm-life/</guid><description><![CDATA[<p>Almost right after Hetzner <a href="https://www.hetzner.com/press-release/arm64-cloud/">announced their ARM offering</a> on 12th April 2023 I started to experiment with new infrastructure approach. Note: <a href="https://hetzner.cloud/?ref=bajSDn7obaDR">get 20EUR for your project via this referral</a> - just in case you want to play around.</p>
<p><a href="/blog/2021/12/10/new-infrastructure-recap/">It&rsquo;s been over a year</a> since I started to play around with Nomad and Nomad served me well. CSI support become more stable and Nomad itself become more mature in terms of feature set (native service discovery and support for encrypted secrets were nice).</p>
<p>Unfortunately, the ecosystem was kinda lacking - <a href="https://github.com/hashicorp/levant">levant</a> was left in a weird state when it was kinda <a href="https://discuss.hashicorp.com/t/nomad-pack-vs-levant/45654">supported, but kinda replaced</a> by <a href="https://github.com/hashicorp/nomad-pack">nomad-pack</a> - which I guess is kinda a <em>Helm for Nomad</em>?</p>
<p>I love Hashicorp&rsquo;s work, but I think they are stretched thin in Nomad-space (and do we really need a <a href="https://www.waypointproject.io/">waypoint?</a>) and after running into <a href="https://github.com/hashicorp/nomad/issues/6151">more various issues</a> I decided it&rsquo;s time for another redo! Because <em>this time I will do it right</em>. Right?</p>
<p>I was reluctant for years towards Kubernetes due to its complexity and resource-heaviness. I was fiddling with <a href="https://k3s.io/">k3s</a> around my experiments with Nomad, but the overall experience then was not great at that time.</p>
<p>I decided to give it another go by using my new ARM-powered VM from Hetzner. I gave up on High-Avability tho - it&rsquo;s just not worth the effort for side/home projects. Some things eventually <em>will</em> go sideways and restoring some weird state on 1/3 third of the servers during the weekend is the last thing I want to be doing nowadays.</p>
<p>Kubernetes-quirks aside some takeaways regarding moving to ARM:</p>
<ul>
<li>
<p>majority of the ecosystem seems to be <em>just there</em>. I guess Apple with their M1 really pushed the ecosystem for daily open-source consumers like myself</p>
</li>
<li>
<p>you will have a hard time with truly outdated dependencies - who would guess that unmaintained software will just rot? If only one could predict that; I had to replace some ancient ElasticSearch version with <a href="https://github.com/zincsearch/zincsearch">zincsearch</a> and plug it into an existing Rails app using some forked gem, <em>ugh</em></p>
</li>
<li>
<p>keeping good practices - IaC, CI/CD pipelines, auto-dependencies upgrades - made the transition so smoother. Once again - who would guess that regular maintenance would make things <em>easier</em>? I feel like I really should write a book about it or something. Or <em>at least</em> record a podcast episode.</p>
</li>
<li>
<p>building docker images on Github actions via QEMU is <em>shaite</em> for anything bigger than a &ldquo;hello world&rdquo; project (or you can say - it&rsquo;s <em>extremly</em> slow); one of my build was running for 40+ min (!); do yourself a favor and check out <a href="https://circleci.com/docs/using-arm/">CircleCI ARM offering</a> - simply moving workflow there gave me 20x performance boost</p>
</li>
</ul>
<p>Aside few hiccups my overall experience is truly positive so expect some k3s/ARM infra-related blog posts coming up!</p>]]></description><content:encoded><![CDATA[<p>Almost right after Hetzner <a href="https://www.hetzner.com/press-release/arm64-cloud/">announced their ARM offering</a> on 12th April 2023 I started to experiment with new infrastructure approach. Note: <a href="https://hetzner.cloud/?ref=bajSDn7obaDR">get 20EUR for your project via this referral</a> - just in case you want to play around.</p>
<p><a href="/blog/2021/12/10/new-infrastructure-recap/">It&rsquo;s been over a year</a> since I started to play around with Nomad and Nomad served me well. CSI support become more stable and Nomad itself become more mature in terms of feature set (native service discovery and support for encrypted secrets were nice).</p>
<p>Unfortunately, the ecosystem was kinda lacking - <a href="https://github.com/hashicorp/levant">levant</a> was left in a weird state when it was kinda <a href="https://discuss.hashicorp.com/t/nomad-pack-vs-levant/45654">supported, but kinda replaced</a> by <a href="https://github.com/hashicorp/nomad-pack">nomad-pack</a> - which I guess is kinda a <em>Helm for Nomad</em>?</p>
<p>I love Hashicorp&rsquo;s work, but I think they are stretched thin in Nomad-space (and do we really need a <a href="https://www.waypointproject.io/">waypoint?</a>) and after running into <a href="https://github.com/hashicorp/nomad/issues/6151">more various issues</a> I decided it&rsquo;s time for another redo! Because <em>this time I will do it right</em>. Right?</p>
<p>I was reluctant for years towards Kubernetes due to its complexity and resource-heaviness. I was fiddling with <a href="https://k3s.io/">k3s</a> around my experiments with Nomad, but the overall experience then was not great at that time.</p>
<p>I decided to give it another go by using my new ARM-powered VM from Hetzner. I gave up on High-Avability tho - it&rsquo;s just not worth the effort for side/home projects. Some things eventually <em>will</em> go sideways and restoring some weird state on 1/3 third of the servers during the weekend is the last thing I want to be doing nowadays.</p>
<p>Kubernetes-quirks aside some takeaways regarding moving to ARM:</p>
<ul>
<li>
<p>majority of the ecosystem seems to be <em>just there</em>. I guess Apple with their M1 really pushed the ecosystem for daily open-source consumers like myself</p>
</li>
<li>
<p>you will have a hard time with truly outdated dependencies - who would guess that unmaintained software will just rot? If only one could predict that; I had to replace some ancient ElasticSearch version with <a href="https://github.com/zincsearch/zincsearch">zincsearch</a> and plug it into an existing Rails app using some forked gem, <em>ugh</em></p>
</li>
<li>
<p>keeping good practices - IaC, CI/CD pipelines, auto-dependencies upgrades - made the transition so smoother. Once again - who would guess that regular maintenance would make things <em>easier</em>? I feel like I really should write a book about it or something. Or <em>at least</em> record a podcast episode.</p>
</li>
<li>
<p>building docker images on Github actions via QEMU is <em>shaite</em> for anything bigger than a &ldquo;hello world&rdquo; project (or you can say - it&rsquo;s <em>extremly</em> slow); one of my build was running for 40+ min (!); do yourself a favor and check out <a href="https://circleci.com/docs/using-arm/">CircleCI ARM offering</a> - simply moving workflow there gave me 20x performance boost</p>
</li>
</ul>
<p>Aside few hiccups my overall experience is truly positive so expect some k3s/ARM infra-related blog posts coming up!</p>]]></content:encoded></item><item><title>Auto-ticketing flaky rspec tests</title><link>https://awesomeprogrammer.com/blog/2023/03/26/auto-ticketing-flaky-rspec-tests/</link><pubDate>Sun, 26 Mar 2023 08:24:27 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2023/03/26/auto-ticketing-flaky-rspec-tests/</guid><description><![CDATA[<p>tl;dr: you can <a href="https://github.com/rwojsznis/rspec-sanity">try this rspec wrapper</a> and <a href="https://github.com/rwojsznis/rspec-sanity-example/">here is a working example</a> regarding how to ticket your flaky tests running on CircleCI using GitHub issues.</p>
<p>One of the common ways of dealing with flaky tests in your test suite is slapping <a href="https://github.com/NoRedInk/rspec-retry">rspec-retry</a> on top of your test suite. Unfortunately, most of the time that&rsquo;s the end of a solution. Without any sort of logging around it, you&rsquo;re completely in the dark. Do you know how often things are failing and why? Are you introducing new flaky tests? Are you solving those?</p>
<p>On the other hand I only partially agree with the radical approach regarding simply <a href="https://semaphoreci.com/blog/2017/04/20/flaky-tests.html">removing or fixing such tests right away</a>. As usual in real-life scenarios - <em>it depends</em>. It&rsquo;s all about context, you might have a myriad of other issues at hand, you might lack context, or you might be new to the codebase altogether. Just blindly chopping random code down doesn&rsquo;t seem like a reasonable approach. On the other hand, Hiding from the problem on the other hand and just letting tests retry silently seems like a good technical debt-collecting recipe.</p>
<p>Recently I have been working on a project riddled with flaky specs, and the majority of those specs were backed by the convoluted, unclear, and obscure implementation of a given feature. You&rsquo;re not going &lsquo;just fix or delete&rsquo; everything in such a case.</p>
<p>Moreover - flaky tests can be a valuable source of potential bugs buried in the codebase. It&rsquo;s not always a bad test code, quite often it&rsquo;s a sign of bad architecture and unhandled edge-case scenarios.</p>
<p>I have decided to extract <a href="https://github.com/rwojsznis/rspec-sanity">one common approach</a> that doesn&rsquo;t require 3rd party gems and integrations. It just relies <a href="https://rubydoc.info/gems/rspec-core/RSpec%2FCore%2FConfiguration:example_status_persistence_file_path">rspec persistence file</a> and just spins a fresh rspec process again.</p>
<p>One key benefit is that you start with a clean env and that can sometimes reveal really interesting findings regarding the mess underneath; sometimes just re-running a given test/s (as <code>rspec-retry</code> does) is not enough as your global state is messed up somewhere, somehow already.</p>
<p>It doesn&rsquo;t try to be smart and does not try to fail to build when &rsquo;new&rsquo; flaky test is introduced, because in the end - you never know if that&rsquo;s a truly new flaky spec or some new issue emerged because the test was run on 11:23 PM Friday, during a full moon. Especially with dynamically typed languages like Ruby - correlating new code/new tests with a new flaky test reports seems hopeless.</p>
<p>Such approach gives you basic visibility and creates an actionable item in form of an issue in your issue tracker - which should be just enough to get you started.</p>]]></description><content:encoded><![CDATA[<p>tl;dr: you can <a href="https://github.com/rwojsznis/rspec-sanity">try this rspec wrapper</a> and <a href="https://github.com/rwojsznis/rspec-sanity-example/">here is a working example</a> regarding how to ticket your flaky tests running on CircleCI using GitHub issues.</p>
<p>One of the common ways of dealing with flaky tests in your test suite is slapping <a href="https://github.com/NoRedInk/rspec-retry">rspec-retry</a> on top of your test suite. Unfortunately, most of the time that&rsquo;s the end of a solution. Without any sort of logging around it, you&rsquo;re completely in the dark. Do you know how often things are failing and why? Are you introducing new flaky tests? Are you solving those?</p>
<p>On the other hand I only partially agree with the radical approach regarding simply <a href="https://semaphoreci.com/blog/2017/04/20/flaky-tests.html">removing or fixing such tests right away</a>. As usual in real-life scenarios - <em>it depends</em>. It&rsquo;s all about context, you might have a myriad of other issues at hand, you might lack context, or you might be new to the codebase altogether. Just blindly chopping random code down doesn&rsquo;t seem like a reasonable approach. On the other hand, Hiding from the problem on the other hand and just letting tests retry silently seems like a good technical debt-collecting recipe.</p>
<p>Recently I have been working on a project riddled with flaky specs, and the majority of those specs were backed by the convoluted, unclear, and obscure implementation of a given feature. You&rsquo;re not going &lsquo;just fix or delete&rsquo; everything in such a case.</p>
<p>Moreover - flaky tests can be a valuable source of potential bugs buried in the codebase. It&rsquo;s not always a bad test code, quite often it&rsquo;s a sign of bad architecture and unhandled edge-case scenarios.</p>
<p>I have decided to extract <a href="https://github.com/rwojsznis/rspec-sanity">one common approach</a> that doesn&rsquo;t require 3rd party gems and integrations. It just relies <a href="https://rubydoc.info/gems/rspec-core/RSpec%2FCore%2FConfiguration:example_status_persistence_file_path">rspec persistence file</a> and just spins a fresh rspec process again.</p>
<p>One key benefit is that you start with a clean env and that can sometimes reveal really interesting findings regarding the mess underneath; sometimes just re-running a given test/s (as <code>rspec-retry</code> does) is not enough as your global state is messed up somewhere, somehow already.</p>
<p>It doesn&rsquo;t try to be smart and does not try to fail to build when &rsquo;new&rsquo; flaky test is introduced, because in the end - you never know if that&rsquo;s a truly new flaky spec or some new issue emerged because the test was run on 11:23 PM Friday, during a full moon. Especially with dynamically typed languages like Ruby - correlating new code/new tests with a new flaky test reports seems hopeless.</p>
<p>Such approach gives you basic visibility and creates an actionable item in form of an issue in your issue tracker - which should be just enough to get you started.</p>]]></content:encoded></item><item><title>How to force Telegram to upload MP4 as MP4 and not GIFs</title><link>https://awesomeprogrammer.com/blog/2023/01/15/how-to-force-telegram-to-upload-mp4-as-mp4-and-not-gifs/</link><pubDate>Sun, 15 Jan 2023 11:45:40 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2023/01/15/how-to-force-telegram-to-upload-mp4-as-mp4-and-not-gifs/</guid><description><![CDATA[<p>There is no official way of doing so as far as I know. Small MP4 files without audio are automatically converted into GIFs. But you can trick <a href="https://telegram.org/">Telegram</a> into doing what you want - this method seems to be working as of January 2023 on both the iOS desktop app and the official API.</p>
<p>You might want to do that because you cannot group GIFs in an album. It works fine with JPGs and MP4s, but when you mix GIFs into that - the API suddenly throws 400 errors. You can&rsquo;t create an album of GIFs alone either. Very annoying.</p>
<p>The trick is to add a silent audio track on top of your MP4 file; <a href="https://stackoverflow.com/questions/12368151/adding-silent-audio-in-ffmpeg#answer-18700245">I used this method</a>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ffmpeg -f lavfi -i aevalsrc<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> -i input.mp4 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -c:v copy -c:a aac -map <span style="color:#ae81ff">0</span> -map 1:v -shortest output.mp4
</span></span></code></pre></div><p>That seems to be doing the trick, if you want to call it in your Go program using <code>os/exec</code> it will look something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">cmnd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;ffmpeg&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-y&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-f&#34;</span>, <span style="color:#e6db74">&#34;lavfi&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-i&#34;</span>, <span style="color:#e6db74">&#34;aevalsrc=0&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-i&#34;</span>, <span style="color:#a6e22e">inputPath</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-c:v&#34;</span>, <span style="color:#e6db74">&#34;copy&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-c:a&#34;</span>, <span style="color:#e6db74">&#34;aac&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-map&#34;</span>, <span style="color:#e6db74">&#34;0&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-map&#34;</span>, <span style="color:#e6db74">&#34;1:v&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-shortest&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">outputPath</span>,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">output</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cmnd</span>.<span style="color:#a6e22e">CombinedOutput</span>()
</span></span></code></pre></div>]]></description><content:encoded><![CDATA[<p>There is no official way of doing so as far as I know. Small MP4 files without audio are automatically converted into GIFs. But you can trick <a href="https://telegram.org/">Telegram</a> into doing what you want - this method seems to be working as of January 2023 on both the iOS desktop app and the official API.</p>
<p>You might want to do that because you cannot group GIFs in an album. It works fine with JPGs and MP4s, but when you mix GIFs into that - the API suddenly throws 400 errors. You can&rsquo;t create an album of GIFs alone either. Very annoying.</p>
<p>The trick is to add a silent audio track on top of your MP4 file; <a href="https://stackoverflow.com/questions/12368151/adding-silent-audio-in-ffmpeg#answer-18700245">I used this method</a>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ffmpeg -f lavfi -i aevalsrc<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> -i input.mp4 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -c:v copy -c:a aac -map <span style="color:#ae81ff">0</span> -map 1:v -shortest output.mp4
</span></span></code></pre></div><p>That seems to be doing the trick, if you want to call it in your Go program using <code>os/exec</code> it will look something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">cmnd</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">exec</span>.<span style="color:#a6e22e">Command</span>(<span style="color:#e6db74">&#34;ffmpeg&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-y&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-f&#34;</span>, <span style="color:#e6db74">&#34;lavfi&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-i&#34;</span>, <span style="color:#e6db74">&#34;aevalsrc=0&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-i&#34;</span>, <span style="color:#a6e22e">inputPath</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-c:v&#34;</span>, <span style="color:#e6db74">&#34;copy&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-c:a&#34;</span>, <span style="color:#e6db74">&#34;aac&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-map&#34;</span>, <span style="color:#e6db74">&#34;0&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-map&#34;</span>, <span style="color:#e6db74">&#34;1:v&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;-shortest&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">outputPath</span>,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">output</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">cmnd</span>.<span style="color:#a6e22e">CombinedOutput</span>()
</span></span></code></pre></div>]]></content:encoded></item><item><title>How to deploy an application from Github Actions to your Synology NAS</title><link>https://awesomeprogrammer.com/blog/2023/01/15/how-to-deploy-an-application-from-github-actions-to-your-synology-nas/</link><pubDate>Sun, 15 Jan 2023 11:13:05 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2023/01/15/how-to-deploy-an-application-from-github-actions-to-your-synology-nas/</guid><description><![CDATA[<p>Recently I have been refreshing my various Telegram bots - I had 3 tiny go apps lying around and I have decided it&rsquo;s time to put them into a single app. I was just done with all that boilerplate and it just made more sense.</p>
<p>While doing so I thought it would be cool to finally push docker containers I was already building using Github Actions <em>directly</em> to my Synology - without any human interaction. The thing is I didn&rsquo;t want to expose the whole Synology to the <em>Evil Internet</em>. So I have decided to use Tailscale which is a truly <em>magical</em> product. It&rsquo;s built on top of <a href="https://www.wireguard.com/">WireGuard</a> but removes all the manual fiddling/configuration process. It&rsquo;s <em>install &amp; forget</em>.</p>
<p>Below is a quick-ish recipe I have decided to follow.</p>
<ol>
<li>
<p>You can install Tailscale on your NAS <a href="https://tailscale.com/kb/1131/synology/">using this guide</a>. Minor disclaimer here - I had a problem with re-authenticating the device on DMS7 <a href="https://github.com/tailscale/tailscale/issues/4648#issuecomment-1137710579">and found the solution here</a> - running <code>sudo tailscale up</code> after going to the Tailscale on the NAS (via web panel) solved the problem.</p>
</li>
<li>
<p>For the Github actions you can use <a href="https://github.com/tailscale/github-action">this official action</a>.</p>
</li>
<li>
<p>In the tailscale you need to generate <a href="https://login.tailscale.com/admin/settings/keys">Auth key</a> and make it both <code>Reusable</code> and <code>Ephemeral</code>. In the <a href="https://login.tailscale.com/admin/dns">DNS settings</a> I recommend enabling <code>MagicDNS</code> so you can access your NAS via the <code>&lt;hostname&gt;.&lt;tailnet&gt;.ts.net</code> instead of an IP address.</p>
</li>
<li>
<p>To glue it all together I chose the <a href="/blog/2021/04/18/deploy-docker-container-with-http-webhook-the-easy-way/">webhook approach</a> - I didn&rsquo;t want to connect via SSH from GH actions to my NAS or setup something <em>too fancy</em>.</p>
</li>
</ol>
<p>One-time steps I did on my Synology:</p>
<p>Save this <code>Dockerfile</code> somewhere and built it via <code>docker build . -t webhook:2.8.0</code></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> almir/webhook:2.8.0</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span>  apk --update --upgrade add docker curl bash <span style="color:#f92672">&amp;&amp;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>     rm -rf /var/cache/apk/*<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>I decided to put webhook config files in <code>/volume1/docker/webhook</code> on my NAS.</p>
<p>The content of the <code>/volume1/docker/webhook/hooks.json</code> looks something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>[
</span></span><span style="display:flex;"><span>  {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#e6db74">&#34;my-app&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;execute-command&#34;</span>: <span style="color:#e6db74">&#34;/etc/webhook/my-app&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;response-message&#34;</span>: <span style="color:#e6db74">&#34;Restarting container&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;pass-environment-to-command&#34;</span>: [
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;envname&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_PASSWORD&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;payload&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_PASSWORD&#34;</span>
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;envname&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_TAG&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;payload&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_TAG&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;trigger-rule-mismatch-http-response-code&#34;</span>: <span style="color:#ae81ff">411</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;trigger-rule&#34;</span>: {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;match&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;value&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;value&#34;</span>: <span style="color:#e6db74">&#34;SUPER-SECRET-TOKEN&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;parameter&#34;</span>: {
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;header&#34;</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;X-Token&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>I set the &ldquo;SUPER-SECRET-TOKEN&rdquo; as <code>WEBHOOK_TOKEN</code> (along with the <code>TAILSCALE_AUTHKEY</code> generated before) to in the GitHub repo secrets (you can use the <a href="https://cli.github.com/">github CLI</a> and check <code>gh secret set</code> command).</p>
<p>And the content of the <code>/volume1/docker/webhook/my-app</code> looks something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>set -e
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Login into ghcr&#34;</span>
</span></span><span style="display:flex;"><span>docker login ghcr.io -u your-username -p $DOCKER_PASSWORD
</span></span><span style="display:flex;"><span>docker pull ghcr.io/your-username/my-app:$DOCKER_TAG
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Stop running container&#34;</span>
</span></span><span style="display:flex;"><span>docker stop my-app <span style="color:#f92672">||</span> true
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Remove old container&#34;</span>
</span></span><span style="display:flex;"><span>docker rm my-app <span style="color:#f92672">||</span> true
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># here I set ENVs, container network, labels etc.</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Run new container&#34;</span>
</span></span><span style="display:flex;"><span>docker run <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -d <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name<span style="color:#f92672">=</span>my-app <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>5m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">3</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  ghcr.io/your-username/my-app:$DOCKER_TAG
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Logout from ghcr&#34;</span>
</span></span><span style="display:flex;"><span>docker logout ghcr.io
</span></span></code></pre></div><p>Having those two files and a custom-built image you can now run (still on your Synology):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker run -d <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 9000:9000 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/docker/webhook:/etc/webhook:ro <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /var/run/docker.sock:/var/run/docker.sock <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>10m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name<span style="color:#f92672">=</span>webhook <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  webhook:2.8.0 -verbose -hooks<span style="color:#f92672">=</span>/etc/webhook/hooks.json -hotreload
</span></span></code></pre></div><p>At this point, you should have a Docker container running on your NAS called <code>webhook</code> exposed on port <code>9000</code> that will try to handle <code>my-app</code> that is assumed to be built on <code>ghcr.io</code> as <code>your-username/my-app</code> (which should correspond to the the repo location/url on Github). It will try to log into ghcr using provided password &amp; fetch provided image tag (we will pass those using Github workflow). Hopefully, you will never have to touch those settings again.</p>
<p>Here is the GitHub actions workflow that does rest of the work:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yml" data-lang="yml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">build</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">push</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">main</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">build</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build image</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v3</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/login-action@v2</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">registry</span>: <span style="color:#ae81ff">ghcr.io</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">username</span>: <span style="color:#ae81ff">${{ github.repository_owner }}</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">password</span>: <span style="color:#ae81ff">${{ secrets.GITHUB_TOKEN }}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Set up Docker Buildx</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/setup-buildx-action@v2</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># caching docker layer ommited for brevity</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build and push</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/build-push-action@v3</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">context</span>: <span style="color:#ae81ff">.</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">push</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">tags</span>: <span style="color:#ae81ff">ghcr.io/${{ github.repository }}:${{ github.sha }}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Connect to tailscale</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">tailscale/github-action@ce41a99162202a647a4b24c30c558a567b926709</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">authkey</span>: <span style="color:#ae81ff">${{ secrets.TAILSCALE_AUTHKEY }}</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">hostname</span>: <span style="color:#e6db74">&#39;github-actions-runner&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># with MagicDNS you should be now able to connect to your NAS using</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># tailscale network + hostname of the nas</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy docker container webhook</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">joelwmale/webhook-action@448a17bf857ead98546cfbdbe3b9d4cf979dda95</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">url</span>: <span style="color:#ae81ff">http://&lt;nas hostname&gt;.&lt;tailscale network&gt;.ts.net:9000/hooks/my-app</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">headers</span>: <span style="color:#e6db74">&#39;{&#34;X-Token&#34;: &#34;${{ secrets.WEBHOOK_TOKEN  }}&#34;}&#39;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">body</span>: &gt;-<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            { &#34;DOCKER_PASSWORD&#34;: &#34;${{ secrets.GITHUB_TOKEN }}&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &#34;DOCKER_TAG&#34;: &#34;${{ github.sha }}&#34; }</span>            
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Synology can be slow-ish when it comes to pulling new image</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># GITHUB_TOKEN might expire before NAS will manage to pull the</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># new image (that token expires after action is done)</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># You might want to do some fancy pooling or just slap some</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># sleep if that&#39;s good enough for you</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># As we&#39;re not building a production system here I recommend the latter</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># - name: Sleep for 15 seconds</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">#   run: sleep 15</span>
</span></span></code></pre></div><p>And that&rsquo;s it - all the heavy lifting is being done via Tailscale, Github action with the webhook approach glues it nicely together.</p>]]></description><content:encoded><![CDATA[<p>Recently I have been refreshing my various Telegram bots - I had 3 tiny go apps lying around and I have decided it&rsquo;s time to put them into a single app. I was just done with all that boilerplate and it just made more sense.</p>
<p>While doing so I thought it would be cool to finally push docker containers I was already building using Github Actions <em>directly</em> to my Synology - without any human interaction. The thing is I didn&rsquo;t want to expose the whole Synology to the <em>Evil Internet</em>. So I have decided to use Tailscale which is a truly <em>magical</em> product. It&rsquo;s built on top of <a href="https://www.wireguard.com/">WireGuard</a> but removes all the manual fiddling/configuration process. It&rsquo;s <em>install &amp; forget</em>.</p>
<p>Below is a quick-ish recipe I have decided to follow.</p>
<ol>
<li>
<p>You can install Tailscale on your NAS <a href="https://tailscale.com/kb/1131/synology/">using this guide</a>. Minor disclaimer here - I had a problem with re-authenticating the device on DMS7 <a href="https://github.com/tailscale/tailscale/issues/4648#issuecomment-1137710579">and found the solution here</a> - running <code>sudo tailscale up</code> after going to the Tailscale on the NAS (via web panel) solved the problem.</p>
</li>
<li>
<p>For the Github actions you can use <a href="https://github.com/tailscale/github-action">this official action</a>.</p>
</li>
<li>
<p>In the tailscale you need to generate <a href="https://login.tailscale.com/admin/settings/keys">Auth key</a> and make it both <code>Reusable</code> and <code>Ephemeral</code>. In the <a href="https://login.tailscale.com/admin/dns">DNS settings</a> I recommend enabling <code>MagicDNS</code> so you can access your NAS via the <code>&lt;hostname&gt;.&lt;tailnet&gt;.ts.net</code> instead of an IP address.</p>
</li>
<li>
<p>To glue it all together I chose the <a href="/blog/2021/04/18/deploy-docker-container-with-http-webhook-the-easy-way/">webhook approach</a> - I didn&rsquo;t want to connect via SSH from GH actions to my NAS or setup something <em>too fancy</em>.</p>
</li>
</ol>
<p>One-time steps I did on my Synology:</p>
<p>Save this <code>Dockerfile</code> somewhere and built it via <code>docker build . -t webhook:2.8.0</code></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> almir/webhook:2.8.0</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span>  apk --update --upgrade add docker curl bash <span style="color:#f92672">&amp;&amp;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>     rm -rf /var/cache/apk/*<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>I decided to put webhook config files in <code>/volume1/docker/webhook</code> on my NAS.</p>
<p>The content of the <code>/volume1/docker/webhook/hooks.json</code> looks something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>[
</span></span><span style="display:flex;"><span>  {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#e6db74">&#34;my-app&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;execute-command&#34;</span>: <span style="color:#e6db74">&#34;/etc/webhook/my-app&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;response-message&#34;</span>: <span style="color:#e6db74">&#34;Restarting container&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;pass-environment-to-command&#34;</span>: [
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;envname&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_PASSWORD&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;payload&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_PASSWORD&#34;</span>
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;envname&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_TAG&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;payload&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_TAG&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;trigger-rule-mismatch-http-response-code&#34;</span>: <span style="color:#ae81ff">411</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;trigger-rule&#34;</span>: {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;match&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;value&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;value&#34;</span>: <span style="color:#e6db74">&#34;SUPER-SECRET-TOKEN&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;parameter&#34;</span>: {
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;header&#34;</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;X-Token&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>I set the &ldquo;SUPER-SECRET-TOKEN&rdquo; as <code>WEBHOOK_TOKEN</code> (along with the <code>TAILSCALE_AUTHKEY</code> generated before) to in the GitHub repo secrets (you can use the <a href="https://cli.github.com/">github CLI</a> and check <code>gh secret set</code> command).</p>
<p>And the content of the <code>/volume1/docker/webhook/my-app</code> looks something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>set -e
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Login into ghcr&#34;</span>
</span></span><span style="display:flex;"><span>docker login ghcr.io -u your-username -p $DOCKER_PASSWORD
</span></span><span style="display:flex;"><span>docker pull ghcr.io/your-username/my-app:$DOCKER_TAG
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Stop running container&#34;</span>
</span></span><span style="display:flex;"><span>docker stop my-app <span style="color:#f92672">||</span> true
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Remove old container&#34;</span>
</span></span><span style="display:flex;"><span>docker rm my-app <span style="color:#f92672">||</span> true
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># here I set ENVs, container network, labels etc.</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Run new container&#34;</span>
</span></span><span style="display:flex;"><span>docker run <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -d <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name<span style="color:#f92672">=</span>my-app <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>5m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">3</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  ghcr.io/your-username/my-app:$DOCKER_TAG
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Logout from ghcr&#34;</span>
</span></span><span style="display:flex;"><span>docker logout ghcr.io
</span></span></code></pre></div><p>Having those two files and a custom-built image you can now run (still on your Synology):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker run -d <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 9000:9000 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/docker/webhook:/etc/webhook:ro <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /var/run/docker.sock:/var/run/docker.sock <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>10m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name<span style="color:#f92672">=</span>webhook <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  webhook:2.8.0 -verbose -hooks<span style="color:#f92672">=</span>/etc/webhook/hooks.json -hotreload
</span></span></code></pre></div><p>At this point, you should have a Docker container running on your NAS called <code>webhook</code> exposed on port <code>9000</code> that will try to handle <code>my-app</code> that is assumed to be built on <code>ghcr.io</code> as <code>your-username/my-app</code> (which should correspond to the the repo location/url on Github). It will try to log into ghcr using provided password &amp; fetch provided image tag (we will pass those using Github workflow). Hopefully, you will never have to touch those settings again.</p>
<p>Here is the GitHub actions workflow that does rest of the work:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yml" data-lang="yml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">build</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">push</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">main</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">build</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build image</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v3</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/login-action@v2</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">registry</span>: <span style="color:#ae81ff">ghcr.io</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">username</span>: <span style="color:#ae81ff">${{ github.repository_owner }}</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">password</span>: <span style="color:#ae81ff">${{ secrets.GITHUB_TOKEN }}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Set up Docker Buildx</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/setup-buildx-action@v2</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># caching docker layer ommited for brevity</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build and push</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/build-push-action@v3</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">context</span>: <span style="color:#ae81ff">.</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">push</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">tags</span>: <span style="color:#ae81ff">ghcr.io/${{ github.repository }}:${{ github.sha }}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Connect to tailscale</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">tailscale/github-action@ce41a99162202a647a4b24c30c558a567b926709</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">authkey</span>: <span style="color:#ae81ff">${{ secrets.TAILSCALE_AUTHKEY }}</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">hostname</span>: <span style="color:#e6db74">&#39;github-actions-runner&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># with MagicDNS you should be now able to connect to your NAS using</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># tailscale network + hostname of the nas</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy docker container webhook</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">joelwmale/webhook-action@448a17bf857ead98546cfbdbe3b9d4cf979dda95</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">url</span>: <span style="color:#ae81ff">http://&lt;nas hostname&gt;.&lt;tailscale network&gt;.ts.net:9000/hooks/my-app</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">headers</span>: <span style="color:#e6db74">&#39;{&#34;X-Token&#34;: &#34;${{ secrets.WEBHOOK_TOKEN  }}&#34;}&#39;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">body</span>: &gt;-<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            { &#34;DOCKER_PASSWORD&#34;: &#34;${{ secrets.GITHUB_TOKEN }}&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &#34;DOCKER_TAG&#34;: &#34;${{ github.sha }}&#34; }</span>            
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Synology can be slow-ish when it comes to pulling new image</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># GITHUB_TOKEN might expire before NAS will manage to pull the</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># new image (that token expires after action is done)</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># You might want to do some fancy pooling or just slap some</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># sleep if that&#39;s good enough for you</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># As we&#39;re not building a production system here I recommend the latter</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># - name: Sleep for 15 seconds</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">#   run: sleep 15</span>
</span></span></code></pre></div><p>And that&rsquo;s it - all the heavy lifting is being done via Tailscale, Github action with the webhook approach glues it nicely together.</p>]]></content:encoded></item><item><title>How to remove Linkedin connection (kinda in) in bulk</title><link>https://awesomeprogrammer.com/blog/2022/12/06/how-to-remove-linkedin-connection-kinda-in-in-bulk/</link><pubDate>Tue, 06 Dec 2022 20:23:17 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2022/12/06/how-to-remove-linkedin-connection-kinda-in-in-bulk/</guid><description><![CDATA[<p><em>tldr:</em> it&rsquo;s not so straightforward and can&rsquo;t be done that quickly, but you can streamline the process, so it&rsquo;s not a huge PITA.</p>
<p>My LinkedIn profile literally become <a href="https://www.reddit.com/r/LinkedInLunatics/">LinkedInLunatics</a> subreddit. How did that happen? Oh, I know how, years ago, I just accepted almost every invite from almost every person. Fast forward 9 years later, I got a linked feed with a bunch of coaching BS, crypto boys fanatics, and people just building their <em>audience</em>. I decided it&rsquo;s time to revisit some of those old <em>contacts</em>.</p>
<p>How to do it so it won&rsquo;t take you five days? You can visit <a href="https://www.linkedin.com/mynetwork/invite-connect/connections/">my connections</a> - but each removal requires <strong>three</strong> (!) clicks. Opening dropdown -&gt; choosing <em>delete</em> from the dropdown -&gt; confirming the removal.</p>
<p>When you want to get rid of 300+ connections, that will not fly. But you can make your life easier by pasting this script into the web developer console:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#75715e">// this used to work in late-ish 2022, I do not guarantee it&#39;s not broken already tho
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">jq</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">createElement</span>(<span style="color:#e6db74">&#39;script&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">jq</span>.<span style="color:#a6e22e">src</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js&#34;</span>;
</span></span><span style="display:flex;"><span>document.<span style="color:#a6e22e">getElementsByTagName</span>(<span style="color:#e6db74">&#39;head&#39;</span>)[<span style="color:#ae81ff">0</span>].<span style="color:#a6e22e">appendChild</span>(<span style="color:#a6e22e">jq</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">setTimeout</span>(<span style="color:#66d9ef">function</span>() {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;Assuming jquery was loaded&#39;</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">jQuery</span>.<span style="color:#a6e22e">noConflict</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">jQuery</span>(<span style="color:#e6db74">&#39;.scaffold-finite-scroll&#39;</span>).<span style="color:#a6e22e">on</span>(<span style="color:#e6db74">&#39;click&#39;</span>, <span style="color:#e6db74">&#39;.artdeco-dropdown__trigger&#39;</span>, <span style="color:#66d9ef">function</span>(<span style="color:#a6e22e">el</span>){
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;On click dropdown&#39;</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#75715e">// trigger button click after dropdown 
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>		<span style="color:#a6e22e">setTimeout</span>(<span style="color:#66d9ef">function</span>() {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;Trigger dropdown button&#39;</span>)
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">jQuery</span>(<span style="color:#e6db74">&#39;.ember-view.mn-connection-card__dropdown-item button&#39;</span>).<span style="color:#a6e22e">click</span>()
</span></span><span style="display:flex;"><span>		}, <span style="color:#ae81ff">700</span>);
</span></span><span style="display:flex;"><span>	})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">jQuery</span>(<span style="color:#e6db74">&#39;.scaffold-finite-scroll&#39;</span>).<span style="color:#a6e22e">on</span>(<span style="color:#e6db74">&#39;click&#39;</span>, <span style="color:#e6db74">&#39;.ember-view.mn-connection-card__dropdown-item button&#39;</span>, <span style="color:#66d9ef">function</span>(){
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;Will remove connection in a sec&#39;</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">setTimeout</span>(<span style="color:#66d9ef">function</span>() {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;Confirm removal&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">jQuery</span>(<span style="color:#e6db74">&#39;.artdeco-modal__confirm-dialog-btn.artdeco-button--primary&#39;</span>).<span style="color:#a6e22e">click</span>();
</span></span><span style="display:flex;"><span>}, <span style="color:#ae81ff">700</span>);
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>}, <span style="color:#ae81ff">2000</span>)
</span></span></code></pre></div><p>That way you won&rsquo;t have to confirm every removal with extra two clicks. Just imagine how much time it will save you! 💰</p>
<p><em>60% of the time, this script works every time</em>. As it assumes that elements will load in 700ms, feel free to tinker with it for your needs.</p>
<p>You might need to tweak CSP policy (e.g., using a browser addon) to inject jQuery into Linkedin, or you might want to try using vanilla JS. But I do not recommend that - it seems like Linkedin is doing some javascript trickery, and I had very little work hooking event handlers using native JS code. But then again - I&rsquo;m not a JS expert.</p>
<p>Use it, abuse it but remember to accept my invite when I&rsquo;ll be building <em>my</em> audience! 🖖</p>]]></description><content:encoded><![CDATA[<p><em>tldr:</em> it&rsquo;s not so straightforward and can&rsquo;t be done that quickly, but you can streamline the process, so it&rsquo;s not a huge PITA.</p>
<p>My LinkedIn profile literally become <a href="https://www.reddit.com/r/LinkedInLunatics/">LinkedInLunatics</a> subreddit. How did that happen? Oh, I know how, years ago, I just accepted almost every invite from almost every person. Fast forward 9 years later, I got a linked feed with a bunch of coaching BS, crypto boys fanatics, and people just building their <em>audience</em>. I decided it&rsquo;s time to revisit some of those old <em>contacts</em>.</p>
<p>How to do it so it won&rsquo;t take you five days? You can visit <a href="https://www.linkedin.com/mynetwork/invite-connect/connections/">my connections</a> - but each removal requires <strong>three</strong> (!) clicks. Opening dropdown -&gt; choosing <em>delete</em> from the dropdown -&gt; confirming the removal.</p>
<p>When you want to get rid of 300+ connections, that will not fly. But you can make your life easier by pasting this script into the web developer console:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#75715e">// this used to work in late-ish 2022, I do not guarantee it&#39;s not broken already tho
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">jq</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">createElement</span>(<span style="color:#e6db74">&#39;script&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">jq</span>.<span style="color:#a6e22e">src</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js&#34;</span>;
</span></span><span style="display:flex;"><span>document.<span style="color:#a6e22e">getElementsByTagName</span>(<span style="color:#e6db74">&#39;head&#39;</span>)[<span style="color:#ae81ff">0</span>].<span style="color:#a6e22e">appendChild</span>(<span style="color:#a6e22e">jq</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">setTimeout</span>(<span style="color:#66d9ef">function</span>() {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;Assuming jquery was loaded&#39;</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">jQuery</span>.<span style="color:#a6e22e">noConflict</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">jQuery</span>(<span style="color:#e6db74">&#39;.scaffold-finite-scroll&#39;</span>).<span style="color:#a6e22e">on</span>(<span style="color:#e6db74">&#39;click&#39;</span>, <span style="color:#e6db74">&#39;.artdeco-dropdown__trigger&#39;</span>, <span style="color:#66d9ef">function</span>(<span style="color:#a6e22e">el</span>){
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;On click dropdown&#39;</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#75715e">// trigger button click after dropdown 
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>		<span style="color:#a6e22e">setTimeout</span>(<span style="color:#66d9ef">function</span>() {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;Trigger dropdown button&#39;</span>)
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">jQuery</span>(<span style="color:#e6db74">&#39;.ember-view.mn-connection-card__dropdown-item button&#39;</span>).<span style="color:#a6e22e">click</span>()
</span></span><span style="display:flex;"><span>		}, <span style="color:#ae81ff">700</span>);
</span></span><span style="display:flex;"><span>	})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">jQuery</span>(<span style="color:#e6db74">&#39;.scaffold-finite-scroll&#39;</span>).<span style="color:#a6e22e">on</span>(<span style="color:#e6db74">&#39;click&#39;</span>, <span style="color:#e6db74">&#39;.ember-view.mn-connection-card__dropdown-item button&#39;</span>, <span style="color:#66d9ef">function</span>(){
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;Will remove connection in a sec&#39;</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">setTimeout</span>(<span style="color:#66d9ef">function</span>() {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#39;Confirm removal&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">jQuery</span>(<span style="color:#e6db74">&#39;.artdeco-modal__confirm-dialog-btn.artdeco-button--primary&#39;</span>).<span style="color:#a6e22e">click</span>();
</span></span><span style="display:flex;"><span>}, <span style="color:#ae81ff">700</span>);
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>}, <span style="color:#ae81ff">2000</span>)
</span></span></code></pre></div><p>That way you won&rsquo;t have to confirm every removal with extra two clicks. Just imagine how much time it will save you! 💰</p>
<p><em>60% of the time, this script works every time</em>. As it assumes that elements will load in 700ms, feel free to tinker with it for your needs.</p>
<p>You might need to tweak CSP policy (e.g., using a browser addon) to inject jQuery into Linkedin, or you might want to try using vanilla JS. But I do not recommend that - it seems like Linkedin is doing some javascript trickery, and I had very little work hooking event handlers using native JS code. But then again - I&rsquo;m not a JS expert.</p>
<p>Use it, abuse it but remember to accept my invite when I&rsquo;ll be building <em>my</em> audience! 🖖</p>]]></content:encoded></item><item><title>Cleaning up side projects</title><link>https://awesomeprogrammer.com/blog/2022/10/09/cleaning-up-side-projects/</link><pubDate>Sun, 09 Oct 2022 18:06:57 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2022/10/09/cleaning-up-side-projects/</guid><description><![CDATA[<p>Over the years, I have accumulated various ongoing/past side projects where I tinkered with mostly unstructured <em>stuff</em>.</p>
<p>I decided it was time to let them go. My initial learnings were great, but I realized it&rsquo;s impossible to keep up with everything while having a full-time job. Maintenance cost - domains, servers/storage, and mostly - time - was not worth the return.</p>
<p>So I&rsquo;m saying goodbye to:</p>
<ul>
<li>
<p>moviestowatch - Crystal app, quite <a href="/blog/2022/06/30/from-crystal-to-elixir/">recently rewritten to Elixir</a> that operated on reverse-engineered mobile IMDB API (which is less complicated than it sounds really). Crawled 205k movies and over 5M reviews and processed 7M jobs (after rewrite).</p>
</li>
<li>
<p>xbox-price - a Microsoft/Xbox store crawler written in Ruby (some crawlers/web) and Go (old Microsoft store crawler). It processed a whooping 296M jobs (the number is skewed/high due to the number of redirects and whatnot crawler had to deal with). In the end, it fetched 647k games and 65M price records.</p>
</li>
<li>
<p>req.wtf - tiny app in Go that I used for debugging networking connectivity</p>
</li>
</ul>
<p>I will also part ways with some self-hosted tools like Grafana or Prometheus, as I realized I rarely use them anyway.</p>
<p>I will keep my <a href="/blog/2021/12/10/new-infrastructure-recap/">Nomad setup</a> and probably take Nomad 1.4 for a spin as I hope to get rid of Consul &amp; Vault in favor of Nomad&rsquo;s build-in secrets &amp; service discovery features. As a side note, having Nomad and sticking to docker significantly streamlined the continuous deployment process with Github actions (build an image, fire nomad job, call it a day)</p>]]></description><content:encoded><![CDATA[<p>Over the years, I have accumulated various ongoing/past side projects where I tinkered with mostly unstructured <em>stuff</em>.</p>
<p>I decided it was time to let them go. My initial learnings were great, but I realized it&rsquo;s impossible to keep up with everything while having a full-time job. Maintenance cost - domains, servers/storage, and mostly - time - was not worth the return.</p>
<p>So I&rsquo;m saying goodbye to:</p>
<ul>
<li>
<p>moviestowatch - Crystal app, quite <a href="/blog/2022/06/30/from-crystal-to-elixir/">recently rewritten to Elixir</a> that operated on reverse-engineered mobile IMDB API (which is less complicated than it sounds really). Crawled 205k movies and over 5M reviews and processed 7M jobs (after rewrite).</p>
</li>
<li>
<p>xbox-price - a Microsoft/Xbox store crawler written in Ruby (some crawlers/web) and Go (old Microsoft store crawler). It processed a whooping 296M jobs (the number is skewed/high due to the number of redirects and whatnot crawler had to deal with). In the end, it fetched 647k games and 65M price records.</p>
</li>
<li>
<p>req.wtf - tiny app in Go that I used for debugging networking connectivity</p>
</li>
</ul>
<p>I will also part ways with some self-hosted tools like Grafana or Prometheus, as I realized I rarely use them anyway.</p>
<p>I will keep my <a href="/blog/2021/12/10/new-infrastructure-recap/">Nomad setup</a> and probably take Nomad 1.4 for a spin as I hope to get rid of Consul &amp; Vault in favor of Nomad&rsquo;s build-in secrets &amp; service discovery features. As a side note, having Nomad and sticking to docker significantly streamlined the continuous deployment process with Github actions (build an image, fire nomad job, call it a day)</p>]]></content:encoded></item><item><title>Gaming on Apple M1</title><link>https://awesomeprogrammer.com/blog/2022/09/06/gaming-on-apple-m1/</link><pubDate>Tue, 06 Sep 2022 19:35:45 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2022/09/06/gaming-on-apple-m1/</guid><description><![CDATA[<p>Is it possible? Yes. Does it make sense? Probably no.</p>
<p>Under the Apple M1 chip, you have two options: <a href="https://www.codeweavers.com/">Crossover</a> or <a href="https://www.parallels.com/eu/">Parallels</a>. The first one is supposed to be more performant. The latter allows you to install Windows 11 ARM (get it via <a href="https://www.microsoft.com/en-us/software-download/windowsinsiderpreviewiso">Windows Insider Preview</a> program), which is supposed to cover more software and runs DirectX12.</p>
<p>After getting a new Mac Studio, I decided to try Parallels simply out of curiosity. And to my amusement - it <em>really</em> works. With some minor hiccups, I was able to play (and finish) the whole <em>Final Fantasy VII Remake Intergrade</em> (and <em>now</em> I&rsquo;m definitely getting the next revision of PS5). It even supported the Xbox Series gamepad out of the box; I just paired it on my Mac, and it magically appeared on Windows virtual machine. If you think about it - it&rsquo;s pretty rad how software can be emulated through so many layers and still work.</p>
<p>Performance-wise obviously, it&rsquo;s not superb, and I noticed significant FPS drops in some locations of the game; but again - the fact it even runs is mind-blowing.</p>
<p>It was a nice experiment, but obviously, I wouldn&rsquo;t recommend it long-term for gaming purposes. The license alone will cost you €120/year for the pro version that allows allocating more than 4vCPUs and 8GB of RAM. Using cloud gaming services like <a href="https://cloud.boosteroid.com">boosteroid</a> might have more sense long term.</p>]]></description><content:encoded><![CDATA[<p>Is it possible? Yes. Does it make sense? Probably no.</p>
<p>Under the Apple M1 chip, you have two options: <a href="https://www.codeweavers.com/">Crossover</a> or <a href="https://www.parallels.com/eu/">Parallels</a>. The first one is supposed to be more performant. The latter allows you to install Windows 11 ARM (get it via <a href="https://www.microsoft.com/en-us/software-download/windowsinsiderpreviewiso">Windows Insider Preview</a> program), which is supposed to cover more software and runs DirectX12.</p>
<p>After getting a new Mac Studio, I decided to try Parallels simply out of curiosity. And to my amusement - it <em>really</em> works. With some minor hiccups, I was able to play (and finish) the whole <em>Final Fantasy VII Remake Intergrade</em> (and <em>now</em> I&rsquo;m definitely getting the next revision of PS5). It even supported the Xbox Series gamepad out of the box; I just paired it on my Mac, and it magically appeared on Windows virtual machine. If you think about it - it&rsquo;s pretty rad how software can be emulated through so many layers and still work.</p>
<p>Performance-wise obviously, it&rsquo;s not superb, and I noticed significant FPS drops in some locations of the game; but again - the fact it even runs is mind-blowing.</p>
<p>It was a nice experiment, but obviously, I wouldn&rsquo;t recommend it long-term for gaming purposes. The license alone will cost you €120/year for the pro version that allows allocating more than 4vCPUs and 8GB of RAM. Using cloud gaming services like <a href="https://cloud.boosteroid.com">boosteroid</a> might have more sense long term.</p>]]></content:encoded></item><item><title>Problems with inexperienced teams and Ruby</title><link>https://awesomeprogrammer.com/blog/2022/08/30/problems-with-inexperienced-teams-and-ruby/</link><pubDate>Tue, 30 Aug 2022 19:37:20 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2022/08/30/problems-with-inexperienced-teams-and-ruby/</guid><description><![CDATA[<p>I have been coding in Ruby for over ten years, and I have had a chance to see some real-life codebases created by in-house teams, contracts, and software houses of all skills.</p>
<p>Inexperienced folks that don&rsquo;t have guardrails in the form of mentors can create stuff I have never dreamed of. In my nightmares, that is.</p>
<p>Ruby allows you to create all kinds of magic. If you want to shoot yourself in the face, it will gladly allow you to do so. The nature of Ruby - where every class can be open, anything can be redefined <em>anywhere</em> often makes change truly scary. Specifically in codebases where magic is abused to the limits.</p>
<p>On top of everything, Rails doesn&rsquo;t force you to encapsulate anything. Shoving all business logic in the model, including random modules whenever you need them, calling the database in view helpers? <em>We got you, bro.</em> The Rails Way is great for a simple blog but crumbles very quickly in a real production system. When you don&rsquo;t encapsulate logic, don&rsquo;t set proper boundaries system becomes coupled, and suddenly one simple change feels like building a Martian Rover.</p>
<p>Even the most popular test framework - <code>rspec</code> - with its ecosystem like <code>factory_bot</code> sprinkles even more magic on top. Magic that can be <em>very</em> easily abused. Randomly defined hooks, shared examples scattered all over the test suite, cascading factories and DSL that is so easy to misuse.</p>
<p>There is no takeaway here. It just got me wondering if any kind of programming language could help avoid at least some of those traps. Maybe it&rsquo;s time to give <a href="https://www.rust-lang.org">Rust</a> another go, a language that promises to save developers from themselves :).</p>]]></description><content:encoded><![CDATA[<p>I have been coding in Ruby for over ten years, and I have had a chance to see some real-life codebases created by in-house teams, contracts, and software houses of all skills.</p>
<p>Inexperienced folks that don&rsquo;t have guardrails in the form of mentors can create stuff I have never dreamed of. In my nightmares, that is.</p>
<p>Ruby allows you to create all kinds of magic. If you want to shoot yourself in the face, it will gladly allow you to do so. The nature of Ruby - where every class can be open, anything can be redefined <em>anywhere</em> often makes change truly scary. Specifically in codebases where magic is abused to the limits.</p>
<p>On top of everything, Rails doesn&rsquo;t force you to encapsulate anything. Shoving all business logic in the model, including random modules whenever you need them, calling the database in view helpers? <em>We got you, bro.</em> The Rails Way is great for a simple blog but crumbles very quickly in a real production system. When you don&rsquo;t encapsulate logic, don&rsquo;t set proper boundaries system becomes coupled, and suddenly one simple change feels like building a Martian Rover.</p>
<p>Even the most popular test framework - <code>rspec</code> - with its ecosystem like <code>factory_bot</code> sprinkles even more magic on top. Magic that can be <em>very</em> easily abused. Randomly defined hooks, shared examples scattered all over the test suite, cascading factories and DSL that is so easy to misuse.</p>
<p>There is no takeaway here. It just got me wondering if any kind of programming language could help avoid at least some of those traps. Maybe it&rsquo;s time to give <a href="https://www.rust-lang.org">Rust</a> another go, a language that promises to save developers from themselves :).</p>]]></content:encoded></item><item><title>Moddable by default</title><link>https://awesomeprogrammer.com/blog/2022/07/21/moddable-by-default/</link><pubDate>Thu, 21 Jul 2022 17:10:14 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2022/07/21/moddable-by-default/</guid><description><![CDATA[<p>I have a huge sentiment for Quake 3; that&rsquo;s no shock, given I&rsquo;m still keeping quake.net.pl running. The significant factor of its popularity was that it was moddable and <a href="https://www.moddb.com/mods/osp">OSP</a>, <a href="https://www.playmorepromode.com/">CPMA</a>, and many <a href="https://www.moddb.com/mods/defrag">more</a> had their communities of gamers, mappers, and content creators.</p>
<p>I recently picked up <a href="https://www.oculus.com/experiences/quest/2448060205267927">BeatSaber</a> for Oculus Quest again - it&rsquo;s a fun but simple game. Unfortunately, it seems the authors push hard against the modding community - obviously, money is a big factor here. With a huge <a href="https://bsaber.com/">unofficial library</a> of available songs, why would you bother with buying DLCs? So it seems every upgrade of the game breaks half of the mods. I got to the point where - even though I paid for a few DLCs - I can&rsquo;t run some of those as I don&rsquo;t try to update the game anymore not to break custom content. And frankly speaking - this game without custom songs loses most of its charm. The default song list is rather from a single genre, and if you don&rsquo;t like it - you&rsquo;re out of luck because this is the only thing you get (<em>unless</em> you buy some packs right after purchasing the game).</p>
<p>I wish more software were like Quake 3 - open for modification and extension.</p>]]></description><content:encoded><![CDATA[<p>I have a huge sentiment for Quake 3; that&rsquo;s no shock, given I&rsquo;m still keeping quake.net.pl running. The significant factor of its popularity was that it was moddable and <a href="https://www.moddb.com/mods/osp">OSP</a>, <a href="https://www.playmorepromode.com/">CPMA</a>, and many <a href="https://www.moddb.com/mods/defrag">more</a> had their communities of gamers, mappers, and content creators.</p>
<p>I recently picked up <a href="https://www.oculus.com/experiences/quest/2448060205267927">BeatSaber</a> for Oculus Quest again - it&rsquo;s a fun but simple game. Unfortunately, it seems the authors push hard against the modding community - obviously, money is a big factor here. With a huge <a href="https://bsaber.com/">unofficial library</a> of available songs, why would you bother with buying DLCs? So it seems every upgrade of the game breaks half of the mods. I got to the point where - even though I paid for a few DLCs - I can&rsquo;t run some of those as I don&rsquo;t try to update the game anymore not to break custom content. And frankly speaking - this game without custom songs loses most of its charm. The default song list is rather from a single genre, and if you don&rsquo;t like it - you&rsquo;re out of luck because this is the only thing you get (<em>unless</em> you buy some packs right after purchasing the game).</p>
<p>I wish more software were like Quake 3 - open for modification and extension.</p>]]></content:encoded></item><item><title>How to encode videos on Synology NAS</title><link>https://awesomeprogrammer.com/blog/2022/07/02/how-to-encode-videos-on-synology-nas/</link><pubDate>Sat, 02 Jul 2022 15:34:36 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2022/07/02/how-to-encode-videos-on-synology-nas/</guid><description><![CDATA[<p>If you&rsquo;re trying to get FFmpeg onto your NAS - try no more. There is a more user-friendly alternative -  an open-source video transcoder called <a href="https://handbrake.fr/">HandBreak</a>. Amazingly you can run it as a <a href="https://hub.docker.com/r/jlesage/handbrake">docker container</a>.</p>
<p>Here is how I run it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker run <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -d <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --cpuset-cpus<span style="color:#f92672">=</span>2,3 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 5800:5800 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e USER_ID<span style="color:#f92672">=</span><span style="color:#ae81ff">1024</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e GROUP_ID<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/docker/handbrake:/config:rw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume2/video:/storage:ro <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/output:/output:rw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name<span style="color:#f92672">=</span>handbrake <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  jlesage/handbrake:v1.25.1
</span></span></code></pre></div><p>Some explanation:</p>
<ul>
<li><code>cpuset-cpus</code> specifies usage cores 2 &amp; 3 - I&rsquo;m using NAS that have 4 CPU cores, and I don&rsquo;t want to overload the system</li>
<li><code>USER_ID</code> and <code>GROUP_ID</code> and the uids from my Synology system user (you can check it using <code>id</code> command)</li>
<li>volume mounts are rather self-explanatory</li>
<li>web UI will be exposed on the default 5800 port</li>
</ul>
<p>If you&rsquo;re getting directory permissions errors in the web UI - check chmod/chown settings on mounted directories.</p>
<p>It works pretty great. Encoding might take days - depending on your settings and file size - but if you&rsquo;re not in a hurry, you can utilize your NAS, which is probably powered on 24/7 anyhow.</p>]]></description><content:encoded><![CDATA[<p>If you&rsquo;re trying to get FFmpeg onto your NAS - try no more. There is a more user-friendly alternative -  an open-source video transcoder called <a href="https://handbrake.fr/">HandBreak</a>. Amazingly you can run it as a <a href="https://hub.docker.com/r/jlesage/handbrake">docker container</a>.</p>
<p>Here is how I run it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker run <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -d <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --cpuset-cpus<span style="color:#f92672">=</span>2,3 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 5800:5800 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e USER_ID<span style="color:#f92672">=</span><span style="color:#ae81ff">1024</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e GROUP_ID<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/docker/handbrake:/config:rw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume2/video:/storage:ro <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/output:/output:rw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name<span style="color:#f92672">=</span>handbrake <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  jlesage/handbrake:v1.25.1
</span></span></code></pre></div><p>Some explanation:</p>
<ul>
<li><code>cpuset-cpus</code> specifies usage cores 2 &amp; 3 - I&rsquo;m using NAS that have 4 CPU cores, and I don&rsquo;t want to overload the system</li>
<li><code>USER_ID</code> and <code>GROUP_ID</code> and the uids from my Synology system user (you can check it using <code>id</code> command)</li>
<li>volume mounts are rather self-explanatory</li>
<li>web UI will be exposed on the default 5800 port</li>
</ul>
<p>If you&rsquo;re getting directory permissions errors in the web UI - check chmod/chown settings on mounted directories.</p>
<p>It works pretty great. Encoding might take days - depending on your settings and file size - but if you&rsquo;re not in a hurry, you can utilize your NAS, which is probably powered on 24/7 anyhow.</p>]]></content:encoded></item><item><title>From Crystal to Elixir</title><link>https://awesomeprogrammer.com/blog/2022/06/30/from-crystal-to-elixir/</link><pubDate>Thu, 30 Jun 2022 10:00:54 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2022/06/30/from-crystal-to-elixir/</guid><description><![CDATA[<p>I babysit a few of my pet projects, including <a href="https://moviestowatch.app/">moviestowatch site</a> that used to query IMDB for ratings &amp; reviews and compare both (used to, after IMDB redesigned their page and everything broke).</p>
<p>I wrote it <a href="/blog/2018/05/18/back-to-the-crystal-thoughts-on-my-first-app/">4 years ago in Crystal</a> based on simple <a href="https://github.com/kemalcr/kemal">kemal web framework</a>. As you can imagine, a four-year-old app written in a language that wasn&rsquo;t even stable at that point is next to impossible to maintain. I took this as an opportunity to rewrite it using Elixir and <a href="https://www.phoenixframework.org/">Phoenix framework</a>. Just for the sake of the learning experience.</p>
<p>For the time being, I only rewrote a portion of the web interface and didn&rsquo;t have a chance to work on the scrapers just yet. It took me eight days of ad-hoc work from the initial commit to deployment using Nomad (via Docker). The complexity was mostly in SQL queries (usage of materialized views, various aggregate selects), once I got that out of the way rest was fairly straightforward.</p>
<p>It&rsquo;s still too early to tell if I will encounter that <em>wow factor</em>. In terms of <em>tedious</em> web development (the lens I&rsquo;m looking at), it still feels like another language and framework. I was excited about Crystal because it was efficient <em>compiled Ruby</em>. The promise was pretty simple, and it was delivered. I suppose I need to give Elixir a bit more time to surprise me (maybe real use of GenServer will be it?). Also general long-term maintainability plays a big part of the whole experience and my hopes are high here due to it&rsquo;s pretty mature ecosystem.</p>
<p>Regarding learning materials, I read  <a href="https://www.goodreads.com/book/show/38732242-elixir-in-action">Elixir in Action</a>, <a href="https://www.goodreads.com/book/show/26871792-programming-phoenix">Programming Phoenix</a> and currently reading <a href="https://www.goodreads.com/book/show/40582814-programming-ecto">Programming Ecto</a>, but frankly speaking, I end up searching for particular problems anyway. Unfortunately, it seems a lot of folks and companies tried to grab that free SEO traffic on the wave of Phoenix/Elixir hype period, and a lot of the results will be just, um, not so helpful. To give you an example: it&rsquo;s like when you look for recipe X, you lang on a site with three pages of content about how somebody&rsquo;s grandma inherited that recipe from their grandma, and it was cherished for generations? So it&rsquo;s the same but with, for example, Ecto.</p>]]></description><content:encoded><![CDATA[<p>I babysit a few of my pet projects, including <a href="https://moviestowatch.app/">moviestowatch site</a> that used to query IMDB for ratings &amp; reviews and compare both (used to, after IMDB redesigned their page and everything broke).</p>
<p>I wrote it <a href="/blog/2018/05/18/back-to-the-crystal-thoughts-on-my-first-app/">4 years ago in Crystal</a> based on simple <a href="https://github.com/kemalcr/kemal">kemal web framework</a>. As you can imagine, a four-year-old app written in a language that wasn&rsquo;t even stable at that point is next to impossible to maintain. I took this as an opportunity to rewrite it using Elixir and <a href="https://www.phoenixframework.org/">Phoenix framework</a>. Just for the sake of the learning experience.</p>
<p>For the time being, I only rewrote a portion of the web interface and didn&rsquo;t have a chance to work on the scrapers just yet. It took me eight days of ad-hoc work from the initial commit to deployment using Nomad (via Docker). The complexity was mostly in SQL queries (usage of materialized views, various aggregate selects), once I got that out of the way rest was fairly straightforward.</p>
<p>It&rsquo;s still too early to tell if I will encounter that <em>wow factor</em>. In terms of <em>tedious</em> web development (the lens I&rsquo;m looking at), it still feels like another language and framework. I was excited about Crystal because it was efficient <em>compiled Ruby</em>. The promise was pretty simple, and it was delivered. I suppose I need to give Elixir a bit more time to surprise me (maybe real use of GenServer will be it?). Also general long-term maintainability plays a big part of the whole experience and my hopes are high here due to it&rsquo;s pretty mature ecosystem.</p>
<p>Regarding learning materials, I read  <a href="https://www.goodreads.com/book/show/38732242-elixir-in-action">Elixir in Action</a>, <a href="https://www.goodreads.com/book/show/26871792-programming-phoenix">Programming Phoenix</a> and currently reading <a href="https://www.goodreads.com/book/show/40582814-programming-ecto">Programming Ecto</a>, but frankly speaking, I end up searching for particular problems anyway. Unfortunately, it seems a lot of folks and companies tried to grab that free SEO traffic on the wave of Phoenix/Elixir hype period, and a lot of the results will be just, um, not so helpful. To give you an example: it&rsquo;s like when you look for recipe X, you lang on a site with three pages of content about how somebody&rsquo;s grandma inherited that recipe from their grandma, and it was cherished for generations? So it&rsquo;s the same but with, for example, Ecto.</p>]]></content:encoded></item><item><title>Logging on Nomad with Loki and Grafana Cloud</title><link>https://awesomeprogrammer.com/blog/2022/06/09/logging-on-nomad-with-loki-and-grafana-cloud/</link><pubDate>Thu, 09 Jun 2022 09:42:25 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2022/06/09/logging-on-nomad-with-loki-and-grafana-cloud/</guid><description><![CDATA[<p><a href="https://atodorov.me/2021/07/09/logging-on-nomad-and-log-aggregation-with-loki/">This blog post</a> is an excellent point
of reference on how you can approach the logging aggregation problem while on Nomad.</p>
<p>As I&rsquo;m using Consul, I followed a slightly different approach. I have decided to deploy promtail as a global service on
each node and scrape the <code>/var/nomad/alloc/*logs/*</code> directory. Additionally, I slurp systemd-journal to debug potential
system services issues as well.</p>
<p>I&rsquo;m not fully convinced that this is <em>the right way</em>. Still, it seems to do the job - even if the given service doesn&rsquo;t
exist on the particular host that promtail is running on - nothing <em>terrible</em> happens - allocation id directory won&rsquo;t
exist on that host, so we will never scrape anything. The most significant upside here is that scrapings logs from new/existing services require adding a service tag - no need to configure extra sidecar service for each job. So it&rsquo;s <em>a really</em> continent solution.</p>
<p>Ultimately, I push everything to <a href="https://grafana.com/products/cloud/">Grafana Cloud</a>. It offers a free tier with 50GB
of log storage (14 days retention), a great deal for hobby usage.</p>
<p>Here is the final Nomad job:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">locals</span> {
</span></span><span style="display:flex;"><span>  image_tag <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;grafana/promtail:2.5.0&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">job</span> <span style="color:#e6db74">&#34;promtail&#34;</span> {
</span></span><span style="display:flex;"><span>  datacenters <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;dc1&#34;</span>]
</span></span><span style="display:flex;"><span>  type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;system&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # I&#39;m going to use vault to read some credentials below
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">vault</span> {
</span></span><span style="display:flex;"><span>    policies <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;nomad-read&#34;</span>]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">group</span> <span style="color:#e6db74">&#34;promtail&#34;</span> {
</span></span><span style="display:flex;"><span>    count <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">network</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">port</span> <span style="color:#e6db74">&#34;http&#34;</span> {
</span></span><span style="display:flex;"><span>        static <span style="color:#f92672">=</span> <span style="color:#ae81ff">3200</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # I&#39;m using internal network to expose ports
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        host_network <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;private&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">restart</span> {
</span></span><span style="display:flex;"><span>      attempts <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>      delay    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;20s&#34;</span>
</span></span><span style="display:flex;"><span>      mode     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;delay&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">task</span> <span style="color:#e6db74">&#34;promtail&#34;</span> {
</span></span><span style="display:flex;"><span>      driver <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;docker&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">logs</span> {
</span></span><span style="display:flex;"><span>        max_files     <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>        max_file_size <span style="color:#f92672">=</span> <span style="color:#ae81ff">5</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">env</span> {
</span></span><span style="display:flex;"><span>        HOSTNAME <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${attr.unique.hostname}&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">template</span> {
</span></span><span style="display:flex;"><span>data        <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">&lt;&lt;</span><span style="color:#66d9ef">EOTC</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">positions</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">filename</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">data</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">positions</span>.<span style="color:#66d9ef">yaml</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">server</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">log_level</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">info</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">clients</span><span style="color:#960050;background-color:#1e0010">:</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # Grab your token from: Security -&gt; Api Keys -&gt; MetricsPublisher token
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # Url and username can be found in Your stack -&gt; Loki -&gt; Details
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">url</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">https</span><span style="color:#960050;background-color:#1e0010">://</span><span style="color:#66d9ef">logs</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prod</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">eu</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">west</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">0</span>.<span style="color:#66d9ef">grafana</span>.<span style="color:#66d9ef">net</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">loki</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">api</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">v1</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">push</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">basic_auth</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span>      {{<span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">with</span> <span style="color:#66d9ef">secret</span> <span style="color:#e6db74">&#34;kv-v1/nomad/loki&#34;</span> }}
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">username</span><span style="color:#960050;background-color:#1e0010">:</span> {{ .<span style="color:#66d9ef">Data</span>.<span style="color:#66d9ef">username</span> }}
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">password</span><span style="color:#960050;background-color:#1e0010">:</span> {{ .<span style="color:#66d9ef">Data</span>.<span style="color:#66d9ef">password</span> }}
</span></span><span style="display:flex;"><span>      {{ <span style="color:#66d9ef">end</span> }}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">scrape_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">job_name</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">journal</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">journal</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">json</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">max_age</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#ae81ff">12</span><span style="color:#960050;background-color:#1e0010">h</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">path</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">var</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">log</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">journal</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">labels</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">job</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">systemd</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">journal</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">relabel_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">__journal__systemd_unit</span><span style="color:#960050;background-color:#1e0010">&#39;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">unit</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">__journal__hostname</span><span style="color:#960050;background-color:#1e0010">&#39;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">nodename</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">__journal_syslog_identifier</span><span style="color:#960050;background-color:#1e0010">&#39;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">syslog_identifier</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">job_name</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">nomad</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">logs</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">consul_sd_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">server</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span>{{ <span style="color:#66d9ef">env</span> <span style="color:#e6db74">&#34;NOMAD_IP_http&#34;</span> }}<span style="color:#960050;background-color:#1e0010">:</span><span style="color:#ae81ff">8500</span><span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      # I&#39;m using Consul&#39;s ACL thus I need token
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#66d9ef">token</span><span style="color:#960050;background-color:#1e0010">:</span> {{ <span style="color:#66d9ef">with</span> <span style="color:#66d9ef">secret</span> <span style="color:#e6db74">&#34;kv-v1/nomad/prometheus/consul&#34;</span> }}{{ .<span style="color:#66d9ef">Data</span>.<span style="color:#66d9ef">key</span> }}{{ <span style="color:#66d9ef">end</span> }}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">relabel_configs</span><span style="color:#960050;background-color:#1e0010">:</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    # If there is a nomad service registered in consul with &#39;logs.promtail=true&#39; tag - grab it
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_consul_tags</span>]
</span></span><span style="display:flex;"><span>      regex: &#39;(.*)logs\.promtail<span style="color:#f92672">=</span><span style="color:#66d9ef">true</span>(.<span style="color:#960050;background-color:#1e0010">*</span>)<span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">action</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">keep</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_consul_node</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">__host__</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_consul_service_metadata_external_source</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">source</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> (.<span style="color:#960050;background-color:#1e0010">*</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">replacement</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;$</span><span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_consul_service_id</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">_nomad</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">task</span><span style="color:#960050;background-color:#1e0010">-</span>([<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">8</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">4</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">4</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">4</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">12</span>})<span style="color:#960050;background-color:#1e0010">-</span>.<span style="color:#960050;background-color:#1e0010">*&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span>  <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">task_id</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">replacement</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;$</span><span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_consul_service</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">job</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">__meta_consul_node</span><span style="color:#960050;background-color:#1e0010">&#39;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span>         <span style="color:#960050;background-color:#1e0010">&#39;</span>(.<span style="color:#960050;background-color:#1e0010">*</span>)<span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span>  <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">instance</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">replacement</span><span style="color:#960050;background-color:#1e0010">:</span>   <span style="color:#960050;background-color:#1e0010">&#39;$</span><span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_consul_service_id</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">_nomad</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">task</span><span style="color:#960050;background-color:#1e0010">-</span>([<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">8</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">4</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">4</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">4</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">12</span>})<span style="color:#960050;background-color:#1e0010">-</span>.<span style="color:#960050;background-color:#1e0010">*&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span>  <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">__path__</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">replacement</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;/</span><span style="color:#66d9ef">nomad</span><span style="color:#960050;background-color:#1e0010">/$</span><span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">alloc</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">logs</span><span style="color:#75715e">/*std*.{?,??}&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">EOTC
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        destination = &#34;/local/promtail.yml&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      config {
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        image = local.image_tag
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        ports = [&#34;http&#34;]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        args = [
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          &#34;-config.file=/local/promtail.yml&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          &#34;-server.http-listen-port=${NOMAD_PORT_http}&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        ]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        volumes = [
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          # keep promtail&#39;s positions file on the host
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          &#34;/data/promtail:/data&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          # and read journal &amp; nomad logs
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          &#34;/var/nomad/alloc:/nomad/:ro&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          &#34;/var/log/journal/:/var/log/journal/:ro&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        ]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      resources {
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        cpu    = 100
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        memory = 50
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        memory_max = 100
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      service {
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        name = &#34;promtail&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        port = &#34;http&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        tags = [&#34;monitoring&#34;]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        check {
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          name     = &#34;Promtail HTTP&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          type     = &#34;http&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          path     = &#34;/targets&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          interval = &#34;5s&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          timeout  = &#34;2s&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          check_restart {
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            limit           = 2
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            grace           = &#34;60s&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            ignore_warnings = false
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">}
</span></span></span></code></pre></div><p>Once you have that up &amp; running you can register any nomad service with <code>logs.promtail=true</code> tag and it will be picked up by the promtail.</p>]]></description><content:encoded><![CDATA[<p><a href="https://atodorov.me/2021/07/09/logging-on-nomad-and-log-aggregation-with-loki/">This blog post</a> is an excellent point
of reference on how you can approach the logging aggregation problem while on Nomad.</p>
<p>As I&rsquo;m using Consul, I followed a slightly different approach. I have decided to deploy promtail as a global service on
each node and scrape the <code>/var/nomad/alloc/*logs/*</code> directory. Additionally, I slurp systemd-journal to debug potential
system services issues as well.</p>
<p>I&rsquo;m not fully convinced that this is <em>the right way</em>. Still, it seems to do the job - even if the given service doesn&rsquo;t
exist on the particular host that promtail is running on - nothing <em>terrible</em> happens - allocation id directory won&rsquo;t
exist on that host, so we will never scrape anything. The most significant upside here is that scrapings logs from new/existing services require adding a service tag - no need to configure extra sidecar service for each job. So it&rsquo;s <em>a really</em> continent solution.</p>
<p>Ultimately, I push everything to <a href="https://grafana.com/products/cloud/">Grafana Cloud</a>. It offers a free tier with 50GB
of log storage (14 days retention), a great deal for hobby usage.</p>
<p>Here is the final Nomad job:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">locals</span> {
</span></span><span style="display:flex;"><span>  image_tag <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;grafana/promtail:2.5.0&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">job</span> <span style="color:#e6db74">&#34;promtail&#34;</span> {
</span></span><span style="display:flex;"><span>  datacenters <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;dc1&#34;</span>]
</span></span><span style="display:flex;"><span>  type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;system&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # I&#39;m going to use vault to read some credentials below
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">vault</span> {
</span></span><span style="display:flex;"><span>    policies <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;nomad-read&#34;</span>]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">group</span> <span style="color:#e6db74">&#34;promtail&#34;</span> {
</span></span><span style="display:flex;"><span>    count <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">network</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">port</span> <span style="color:#e6db74">&#34;http&#34;</span> {
</span></span><span style="display:flex;"><span>        static <span style="color:#f92672">=</span> <span style="color:#ae81ff">3200</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # I&#39;m using internal network to expose ports
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        host_network <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;private&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">restart</span> {
</span></span><span style="display:flex;"><span>      attempts <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>      delay    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;20s&#34;</span>
</span></span><span style="display:flex;"><span>      mode     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;delay&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">task</span> <span style="color:#e6db74">&#34;promtail&#34;</span> {
</span></span><span style="display:flex;"><span>      driver <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;docker&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">logs</span> {
</span></span><span style="display:flex;"><span>        max_files     <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>        max_file_size <span style="color:#f92672">=</span> <span style="color:#ae81ff">5</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">env</span> {
</span></span><span style="display:flex;"><span>        HOSTNAME <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${attr.unique.hostname}&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">template</span> {
</span></span><span style="display:flex;"><span>data        <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">&lt;&lt;</span><span style="color:#66d9ef">EOTC</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">positions</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">filename</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">data</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">positions</span>.<span style="color:#66d9ef">yaml</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">server</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">log_level</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">info</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">clients</span><span style="color:#960050;background-color:#1e0010">:</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # Grab your token from: Security -&gt; Api Keys -&gt; MetricsPublisher token
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # Url and username can be found in Your stack -&gt; Loki -&gt; Details
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">url</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">https</span><span style="color:#960050;background-color:#1e0010">://</span><span style="color:#66d9ef">logs</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">prod</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">eu</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">west</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">0</span>.<span style="color:#66d9ef">grafana</span>.<span style="color:#66d9ef">net</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">loki</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">api</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">v1</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">push</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">basic_auth</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span>      {{<span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">with</span> <span style="color:#66d9ef">secret</span> <span style="color:#e6db74">&#34;kv-v1/nomad/loki&#34;</span> }}
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">username</span><span style="color:#960050;background-color:#1e0010">:</span> {{ .<span style="color:#66d9ef">Data</span>.<span style="color:#66d9ef">username</span> }}
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">password</span><span style="color:#960050;background-color:#1e0010">:</span> {{ .<span style="color:#66d9ef">Data</span>.<span style="color:#66d9ef">password</span> }}
</span></span><span style="display:flex;"><span>      {{ <span style="color:#66d9ef">end</span> }}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">scrape_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">job_name</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">journal</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">journal</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">json</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">max_age</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#ae81ff">12</span><span style="color:#960050;background-color:#1e0010">h</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">path</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">var</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">log</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">journal</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">labels</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">job</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">systemd</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">journal</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">relabel_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">__journal__systemd_unit</span><span style="color:#960050;background-color:#1e0010">&#39;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">unit</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">__journal__hostname</span><span style="color:#960050;background-color:#1e0010">&#39;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">nodename</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">__journal_syslog_identifier</span><span style="color:#960050;background-color:#1e0010">&#39;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">syslog_identifier</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">job_name</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">nomad</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">logs</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">consul_sd_configs</span><span style="color:#960050;background-color:#1e0010">:</span>
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">server</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span>{{ <span style="color:#66d9ef">env</span> <span style="color:#e6db74">&#34;NOMAD_IP_http&#34;</span> }}<span style="color:#960050;background-color:#1e0010">:</span><span style="color:#ae81ff">8500</span><span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      # I&#39;m using Consul&#39;s ACL thus I need token
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#66d9ef">token</span><span style="color:#960050;background-color:#1e0010">:</span> {{ <span style="color:#66d9ef">with</span> <span style="color:#66d9ef">secret</span> <span style="color:#e6db74">&#34;kv-v1/nomad/prometheus/consul&#34;</span> }}{{ .<span style="color:#66d9ef">Data</span>.<span style="color:#66d9ef">key</span> }}{{ <span style="color:#66d9ef">end</span> }}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">relabel_configs</span><span style="color:#960050;background-color:#1e0010">:</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    # If there is a nomad service registered in consul with &#39;logs.promtail=true&#39; tag - grab it
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_consul_tags</span>]
</span></span><span style="display:flex;"><span>      regex: &#39;(.*)logs\.promtail<span style="color:#f92672">=</span><span style="color:#66d9ef">true</span>(.<span style="color:#960050;background-color:#1e0010">*</span>)<span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">action</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">keep</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_consul_node</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">__host__</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_consul_service_metadata_external_source</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">source</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> (.<span style="color:#960050;background-color:#1e0010">*</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">replacement</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;$</span><span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_consul_service_id</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">_nomad</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">task</span><span style="color:#960050;background-color:#1e0010">-</span>([<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">8</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">4</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">4</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">4</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">12</span>})<span style="color:#960050;background-color:#1e0010">-</span>.<span style="color:#960050;background-color:#1e0010">*&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span>  <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">task_id</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">replacement</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;$</span><span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_consul_service</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#66d9ef">job</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">__meta_consul_node</span><span style="color:#960050;background-color:#1e0010">&#39;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span>         <span style="color:#960050;background-color:#1e0010">&#39;</span>(.<span style="color:#960050;background-color:#1e0010">*</span>)<span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span>  <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">instance</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">replacement</span><span style="color:#960050;background-color:#1e0010">:</span>   <span style="color:#960050;background-color:#1e0010">&#39;$</span><span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">source_labels</span><span style="color:#960050;background-color:#1e0010">:</span> [<span style="color:#66d9ef">__meta_consul_service_id</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">regex</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">_nomad</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">task</span><span style="color:#960050;background-color:#1e0010">-</span>([<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">8</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">4</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">4</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">4</span>}<span style="color:#960050;background-color:#1e0010">-</span>[<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#ae81ff">9</span><span style="color:#960050;background-color:#1e0010">a-</span><span style="color:#66d9ef">fA</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">F</span>]{<span style="color:#ae81ff">12</span>})<span style="color:#960050;background-color:#1e0010">-</span>.<span style="color:#960050;background-color:#1e0010">*&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">target_label</span><span style="color:#960050;background-color:#1e0010">:</span>  <span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#66d9ef">__path__</span><span style="color:#960050;background-color:#1e0010">&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">replacement</span><span style="color:#960050;background-color:#1e0010">:</span> <span style="color:#960050;background-color:#1e0010">&#39;/</span><span style="color:#66d9ef">nomad</span><span style="color:#960050;background-color:#1e0010">/$</span><span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">alloc</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">logs</span><span style="color:#75715e">/*std*.{?,??}&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">EOTC
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        destination = &#34;/local/promtail.yml&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      config {
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        image = local.image_tag
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        ports = [&#34;http&#34;]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        args = [
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          &#34;-config.file=/local/promtail.yml&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          &#34;-server.http-listen-port=${NOMAD_PORT_http}&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        ]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        volumes = [
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          # keep promtail&#39;s positions file on the host
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          &#34;/data/promtail:/data&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          # and read journal &amp; nomad logs
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          &#34;/var/nomad/alloc:/nomad/:ro&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          &#34;/var/log/journal/:/var/log/journal/:ro&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        ]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      resources {
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        cpu    = 100
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        memory = 50
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        memory_max = 100
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      service {
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        name = &#34;promtail&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        port = &#34;http&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        tags = [&#34;monitoring&#34;]
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        check {
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          name     = &#34;Promtail HTTP&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          type     = &#34;http&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          path     = &#34;/targets&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          interval = &#34;5s&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          timeout  = &#34;2s&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          check_restart {
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            limit           = 2
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            grace           = &#34;60s&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            ignore_warnings = false
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">}
</span></span></span></code></pre></div><p>Once you have that up &amp; running you can register any nomad service with <code>logs.promtail=true</code> tag and it will be picked up by the promtail.</p>]]></content:encoded></item><item><title>Remote communication tips</title><link>https://awesomeprogrammer.com/blog/2022/06/08/remote-communication-tips/</link><pubDate>Wed, 08 Jun 2022 11:00:07 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2022/06/08/remote-communication-tips/</guid><description><![CDATA[<p>It&rsquo;s astonishing how many companies are still struggling with the remote culture. The worst-case scenario is when people responsible for fostering culture don&rsquo;t see the problem. Communication was always crucial. With the remote setup, it&rsquo;s even more vital as writing is simply <em>hard</em> and requires practice.</p>
<p>Slack already has <a href="https://slack.com/blog/collaboration/etiquette-tips-in-slack">an excellent article about etiquette</a> - <a href="https://slack.com/help/articles/205239997-Pin-messages-and-bookmark-links">bookmark it</a> to your general channel or add it to the onboarding guide!</p>
<p>I want to recap a few points from that list and add a few own pointers.</p>
<p>Private messages are evil unless they need to stay private - if someone asks you something that you can share on a designated channel - redirect that question there. And use threads for further discussion; use threads in general - it avoids unnecessary noise and allows to keep the conversation contained in a single place.</p>
<p>Private channels are evil unless they need to stay private - relying on some person to <em>invite you to the club</em> creates a peculiar atmosphere and not a very inclusive culture in my book. By default, keeping channels public increases transparency and reduces knowledge sharing and discovery traction.</p>
<p>Use reactions. Seriously, it&rsquo;s a super-underrated feature, not only because of this:</p>
<blockquote>
<p>Emoji can be surprisingly complex for being such a simple tool. They can represent real workflows. For example, people can put their 👀 on something to tell everyone they&rsquo;re reviewing it and then a ✅ when they&rsquo;ve completed their review. You can use emoji to claim requests or ask for volunteers with a ✋as well.</p>
</blockquote>
<p>It&rsquo;s also great for mimicking social interactions - if some message made you laugh - use an appropriate reaction! Don&rsquo;t be afraid to <a href="https://slackmojis.com/">add custom emojis</a> that fit your mood! Reactions are a great and effortless way to give feedback to other people sitting behind the screen. There is nothing worse than posting a message in a vacuum.</p>
<p>Pre-recorded videos can be pretty effective. They have a downside as you can&rsquo;t just search the content of the video, it&rsquo;s not hearing-impaired friendly, but in some cases - like giving some status updates/preparing demos, asking questions while showing context at the same time - it can be a great tool. Not sure what to use? Try <a href="https://www.loom.com">Loom</a> or <a href="https://www.veed.io">Veed</a> or <a href="https://clickup.com/features/clip">Clickup clip</a> or <a href="https://www.getcloudapp.com/">CloudApp</a> or whatever that fits your needs.</p>
<p>Context is king. Don&rsquo;t assume folks <em>from the other side</em> know what you know. I can only recommend a fabulous talk by Jordan Raine presented at RailsConf 2021 - <a href="https://www.youtube.com/watch?v=BbIILUSmSk4">Refactoring: A developer&rsquo;s guide to writing well</a> that covers this and way way more - watch it, share it and let&rsquo;s get better at writing together!</p>]]></description><content:encoded><![CDATA[<p>It&rsquo;s astonishing how many companies are still struggling with the remote culture. The worst-case scenario is when people responsible for fostering culture don&rsquo;t see the problem. Communication was always crucial. With the remote setup, it&rsquo;s even more vital as writing is simply <em>hard</em> and requires practice.</p>
<p>Slack already has <a href="https://slack.com/blog/collaboration/etiquette-tips-in-slack">an excellent article about etiquette</a> - <a href="https://slack.com/help/articles/205239997-Pin-messages-and-bookmark-links">bookmark it</a> to your general channel or add it to the onboarding guide!</p>
<p>I want to recap a few points from that list and add a few own pointers.</p>
<p>Private messages are evil unless they need to stay private - if someone asks you something that you can share on a designated channel - redirect that question there. And use threads for further discussion; use threads in general - it avoids unnecessary noise and allows to keep the conversation contained in a single place.</p>
<p>Private channels are evil unless they need to stay private - relying on some person to <em>invite you to the club</em> creates a peculiar atmosphere and not a very inclusive culture in my book. By default, keeping channels public increases transparency and reduces knowledge sharing and discovery traction.</p>
<p>Use reactions. Seriously, it&rsquo;s a super-underrated feature, not only because of this:</p>
<blockquote>
<p>Emoji can be surprisingly complex for being such a simple tool. They can represent real workflows. For example, people can put their 👀 on something to tell everyone they&rsquo;re reviewing it and then a ✅ when they&rsquo;ve completed their review. You can use emoji to claim requests or ask for volunteers with a ✋as well.</p>
</blockquote>
<p>It&rsquo;s also great for mimicking social interactions - if some message made you laugh - use an appropriate reaction! Don&rsquo;t be afraid to <a href="https://slackmojis.com/">add custom emojis</a> that fit your mood! Reactions are a great and effortless way to give feedback to other people sitting behind the screen. There is nothing worse than posting a message in a vacuum.</p>
<p>Pre-recorded videos can be pretty effective. They have a downside as you can&rsquo;t just search the content of the video, it&rsquo;s not hearing-impaired friendly, but in some cases - like giving some status updates/preparing demos, asking questions while showing context at the same time - it can be a great tool. Not sure what to use? Try <a href="https://www.loom.com">Loom</a> or <a href="https://www.veed.io">Veed</a> or <a href="https://clickup.com/features/clip">Clickup clip</a> or <a href="https://www.getcloudapp.com/">CloudApp</a> or whatever that fits your needs.</p>
<p>Context is king. Don&rsquo;t assume folks <em>from the other side</em> know what you know. I can only recommend a fabulous talk by Jordan Raine presented at RailsConf 2021 - <a href="https://www.youtube.com/watch?v=BbIILUSmSk4">Refactoring: A developer&rsquo;s guide to writing well</a> that covers this and way way more - watch it, share it and let&rsquo;s get better at writing together!</p>]]></content:encoded></item><item><title>How to backup Postgres database with Nomad</title><link>https://awesomeprogrammer.com/blog/2022/06/05/how-to-backup-postgres-database-with-nomad/</link><pubDate>Sun, 05 Jun 2022 18:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2022/06/05/how-to-backup-postgres-database-with-nomad/</guid><description><![CDATA[<p>&hellip;or rather: How <em>I</em> choose to backup databases when using Nomad.</p>
<p>When I was researching backup options after switching to Nomad, I considered using something like <a href="https://github.com/tiredofit/docker-db-backup">docker-db-backup</a>. I quickly realized one downside of having to remember to align postgres-client (backup container) with the version of the server (database container). And as I was running at that time five different databases (Postgres/MySQL) it was a deal-breaker for me.</p>
<p>After more reading, I have decided to write a bash script that would be using Nomad&rsquo;s <a href="https://www.nomadproject.io/docs/drivers/raw_exec">raw_exec</a> and <a href="https://www.nomadproject.io/docs/job-specification/periodic">cron</a> capabilities.</p>
<p>Leveraging Consul will help obtain the allocation id of the task we&rsquo;re interested in. Then we can execute <code>nomad alloc exec</code> to call <code>pg_dump</code> within a database docker container.</p>
<p>Then it&rsquo;s up to us what to do with that dump - I have decided to pipe the output to docker again by using <a href="https://github.com/Angatar/s3cmd">s3cmd docker image</a> to put it on the S3 bucket (actually a Minio bucket).
Note: I recommend using a backup location outside your data center as a good practice. I was using Minio as a training exercise.</p>
<h2 id="preparing-the-acl-token-for-the-script">Preparing the ACL token for the script</h2>
<p>You can skip this part if you&rsquo;re not using <a href="https://learn.hashicorp.com/tutorials/nomad/access-control?in=nomad/access-control">Nomad&rsquo;s ACL capabilities</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#75715e"># nomad-exec-policy.hcl
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">namespace</span> <span style="color:#e6db74">&#34;default&#34;</span> {
</span></span><span style="display:flex;"><span>  policy <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;write&#34;</span>
</span></span><span style="display:flex;"><span>  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;alloc-exec&#34;</span>]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Create a new policy using the file above:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>nomad acl policy apply -description <span style="color:#e6db74">&#34;Nomad exec policy&#34;</span> nomad-exec nomad-exec-policy.hcl
</span></span></code></pre></div><p>Create new token - <code>Secret ID</code> is the one you will need:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>nomad acl token create --global -name<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Nomad exec token&#34;</span> -policy<span style="color:#f92672">=</span>nomad-exec -type<span style="color:#f92672">=</span>client
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Accessor ID  <span style="color:#f92672">=</span> xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx
</span></span><span style="display:flex;"><span>Secret ID    <span style="color:#f92672">=</span> xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx
</span></span></code></pre></div><h3 id="nomad-database-job-and-backup-job">Nomad database job and backup job</h3>
<p>Prerequisites: I&rsquo;m using <a href="/blog/2021/12/22/consul-dns-services-discovery-in-docker-gotchas/">Consul for service discovery</a> and Vault for fetching passwords, but your mileage may vary here.</p>
<p>After you <a href="https://www.nomadproject.io/docs/drivers/raw_exec#client-requirements">enabled raw_exec on the client</a>, you should be good to create a new backup job.</p>
<p>I&rsquo;m using Vault to obtain DB&rsquo;s credentials for the backup and S3 credentials for s3cmd. S3cmd will send a backup to Minio exposed somewhere within the private network on 9000 port.</p>
<p>To make it all work together, we need a database task that exposes its allocation id (Nomad allocation id). We can register service in Consul and use the <em>tags</em> feature to do that.</p>
<p>To give you a better picture here is the <em>database job</em> (slimmed-down version, removed irrelevant definitions)</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">job</span> <span style="color:#e6db74">&#34;db-task&#34;</span> {
</span></span><span style="display:flex;"><span>  datacenters <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;dc1&#34;</span>]
</span></span><span style="display:flex;"><span>  type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;service&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">vault</span> {
</span></span><span style="display:flex;"><span>    policies <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;nomad-read&#34;</span>]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">group</span> <span style="color:#e6db74">&#34;db-task&#34;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">network</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">port</span> <span style="color:#e6db74">&#34;db&#34;</span> {
</span></span><span style="display:flex;"><span>        to <span style="color:#f92672">=</span> <span style="color:#ae81ff">5432</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # I&#39;m using internal network called &#39;private&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        host_network <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;private&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">task</span> <span style="color:#e6db74">&#34;db-task&#34;</span> {
</span></span><span style="display:flex;"><span>      driver <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;docker&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">config</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # ommiting volume mount here for brievity
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        image <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;postgres:14.0-alpine3.14&#34;</span>
</span></span><span style="display:flex;"><span>        ports <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;db&#34;</span>]
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">template</span> {
</span></span><span style="display:flex;"><span>        data <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">&lt;&lt;</span><span style="color:#66d9ef">EOH</span>
</span></span><span style="display:flex;"><span>{{<span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">with</span> <span style="color:#66d9ef">secret</span> <span style="color:#e6db74">&#34;kv-v1/nomad/db/postgres&#34;</span> <span style="color:#960050;background-color:#1e0010">-</span>}}
</span></span><span style="display:flex;"><span>POSTGRES_PASSWORD<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.password }}&#34;</span>
</span></span><span style="display:flex;"><span>POSTGRES_USER<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.user }}&#34;</span>
</span></span><span style="display:flex;"><span>POSTGRES_DB<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.db }}&#34;</span>
</span></span><span style="display:flex;"><span>{{<span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">end</span> <span style="color:#960050;background-color:#1e0010">-</span>}}
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">EOH</span>
</span></span><span style="display:flex;"><span>        destination <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;secrets/file.env&#34;</span>
</span></span><span style="display:flex;"><span>        env         <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">resources</span> {
</span></span><span style="display:flex;"><span>        cpu    <span style="color:#f92672">=</span> <span style="color:#ae81ff">200</span>
</span></span><span style="display:flex;"><span>        memory <span style="color:#f92672">=</span> <span style="color:#ae81ff">200</span>
</span></span><span style="display:flex;"><span>        memory_max <span style="color:#f92672">=</span> <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">service</span> {
</span></span><span style="display:flex;"><span>        name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;db-task&#34;</span>
</span></span><span style="display:flex;"><span>        port <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;db&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">	      # backup service will rely on that particular &#39;alloc&#39; tag
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        tags <span style="color:#f92672">=</span> [&#34;alloc<span style="color:#f92672">=</span><span style="color:#e6db74">${</span><span style="color:#960050;background-color:#1e0010">NOMAD_ALLOC_ID</span><span style="color:#e6db74">}</span><span style="color:#960050;background-color:#1e0010">&#34;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">check</span> {
</span></span><span style="display:flex;"><span>          type     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
</span></span><span style="display:flex;"><span>          interval <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;10s&#34;</span>
</span></span><span style="display:flex;"><span>          timeout  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;2s&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>And here is - completely separated - <em>backup job</em>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">job</span> <span style="color:#e6db74">&#34;db-backup&#34;</span> {
</span></span><span style="display:flex;"><span>  datacenters <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;dc1&#34;</span>]
</span></span><span style="display:flex;"><span>  type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;batch&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">vault</span> {
</span></span><span style="display:flex;"><span>    policies <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;nomad-read&#34;</span>]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">periodic</span> {
</span></span><span style="display:flex;"><span>    cron             <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0 22 * * * *&#34;</span>
</span></span><span style="display:flex;"><span>    prohibit_overlap <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">group</span> <span style="color:#e6db74">&#34;db-backup&#34;</span> {  
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">task</span> <span style="color:#e6db74">&#34;postgres-backup&#34;</span> {
</span></span><span style="display:flex;"><span>      driver <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;raw_exec&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">config</span> {
</span></span><span style="display:flex;"><span>        command <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/bin/bash&#34;</span>
</span></span><span style="display:flex;"><span>        args    <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;local/script.sh&#34;</span>]
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">template</span> {
</span></span><span style="display:flex;"><span>        data        <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">&lt;&lt;</span><span style="color:#66d9ef">EOH</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">set</span> <span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">e</span>
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">nomad</span> <span style="color:#66d9ef">alloc</span> <span style="color:#66d9ef">exec</span> <span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">task</span> <span style="color:#66d9ef">db</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">task</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#66d9ef">DB_ALLOC_ID</span> \
</span></span><span style="display:flex;"><span>        bin/bash -c &#34;PGPASSWORD<span style="color:#f92672">=</span>$PGPASSWORD PGUSER<span style="color:#f92672">=</span>$PGUSER PGDATABASE<span style="color:#f92672">=</span>$PGDATABASE pg_dump --compress<span style="color:#f92672">=</span><span style="color:#ae81ff">4</span> <span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">v</span><span style="color:#960050;background-color:#1e0010">&#34;</span> <span style="color:#960050;background-color:#1e0010">|</span> \
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">docker</span> <span style="color:#66d9ef">run</span> <span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">i</span> <span style="color:#960050;background-color:#1e0010">--</span><span style="color:#66d9ef">rm</span> \
</span></span><span style="display:flex;"><span>        -e AWS_ACCESS_KEY_ID<span style="color:#f92672">=</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#66d9ef">AWS_ACCESS_KEY_ID</span> \
</span></span><span style="display:flex;"><span>        -e AWS_SECRET_ACCESS_KEY<span style="color:#f92672">=</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#66d9ef">AWS_SECRET_ACCESS_KEY</span> \
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">d3fk</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">s3cmd</span><span style="color:#960050;background-color:#1e0010">:</span><span style="color:#66d9ef">stable</span> \
</span></span><span style="display:flex;"><span>        --host<span style="color:#f92672">=</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#66d9ef">S3_HOST_BASE</span> \
</span></span><span style="display:flex;"><span>        <span style="color:#960050;background-color:#1e0010">--</span><span style="color:#66d9ef">no</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">ssl</span> \
</span></span><span style="display:flex;"><span>        --host-bucket<span style="color:#f92672">=</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#66d9ef">S3_HOST_BASE</span> <span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">v</span> \
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">put</span> <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">s3</span><span style="color:#960050;background-color:#1e0010">://$</span><span style="color:#66d9ef">S3_BUCKET</span><span style="color:#960050;background-color:#1e0010">/$</span>(<span style="color:#66d9ef">date</span> <span style="color:#e6db74">&#34;+%Y-%m-%d---%H-%M-%S&#34;</span>).<span style="color:#66d9ef">dump</span>.<span style="color:#66d9ef">gz</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">EOH</span>
</span></span><span style="display:flex;"><span>        destination <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;local/script.sh&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">template</span> {
</span></span><span style="display:flex;"><span>        data <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">&lt;&lt;</span><span style="color:#66d9ef">EOH</span>
</span></span><span style="display:flex;"><span>{{<span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">with</span> <span style="color:#66d9ef">secret</span> <span style="color:#e6db74">&#34;kv-v1/nomad/db/postgres&#34;</span> <span style="color:#960050;background-color:#1e0010">-</span>}}
</span></span><span style="display:flex;"><span>PGPASSWORD<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.password }}&#34;</span>
</span></span><span style="display:flex;"><span>PGUSER<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.user }}&#34;</span>
</span></span><span style="display:flex;"><span>PGDATABASE<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.db }}&#34;</span>
</span></span><span style="display:flex;"><span>{{ <span style="color:#66d9ef">end</span> }}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>{{<span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">with</span> <span style="color:#66d9ef">secret</span> <span style="color:#e6db74">&#34;kv-v1/nomad/s3/backup&#34;</span> <span style="color:#960050;background-color:#1e0010">-</span>}}
</span></span><span style="display:flex;"><span>AWS_ACCESS_KEY_ID<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.access_key_id }}&#34;</span>
</span></span><span style="display:flex;"><span>AWS_SECRET_ACCESS_KEY<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.secret_access_key }}&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># here you also might want to set NOMAD_TOKEN env
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># if you&#39;re using ACL capabilities
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>{{ <span style="color:#66d9ef">end</span> }}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># as service &#39;db-task&#39; is registered in Consul
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># we wat to grab its &#39;alloc&#39; tag
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>{{- range $tag, $services :<span style="color:#f92672">=</span> <span style="color:#66d9ef">service</span> <span style="color:#e6db74">&#34;db-task&#34;</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#66d9ef">byTag</span> <span style="color:#960050;background-color:#1e0010">-</span>}}
</span></span><span style="display:flex;"><span>{{<span style="color:#66d9ef">if</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#66d9ef">tag</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#66d9ef">contains</span> <span style="color:#e6db74">&#34;alloc&#34;</span>}}
</span></span><span style="display:flex;"><span>{{$allocId :<span style="color:#f92672">=</span> index ($tag | split &#34;<span style="color:#f92672">=</span><span style="color:#960050;background-color:#1e0010">&#34;</span>) <span style="color:#ae81ff">1</span>}}
</span></span><span style="display:flex;"><span>DB_ALLOC_ID<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ $allocId }}&#34;</span>
</span></span><span style="display:flex;"><span>{{<span style="color:#66d9ef">end</span>}}
</span></span><span style="display:flex;"><span>{{<span style="color:#66d9ef">end</span>}}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># relying on service DNS discovery provided by Consul
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># to obtain Minio IP address
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>S3_HOST_BASE<span style="color:#f92672">=</span><span style="color:#66d9ef">minio</span>.<span style="color:#66d9ef">service</span>.<span style="color:#66d9ef">consul</span><span style="color:#960050;background-color:#1e0010">:</span><span style="color:#ae81ff">9000</span>
</span></span><span style="display:flex;"><span>S3_BUCKET<span style="color:#f92672">=</span><span style="color:#66d9ef">my</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">bucket</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">name</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">EOH</span>
</span></span><span style="display:flex;"><span>        destination <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;secrets/file.env&#34;</span>
</span></span><span style="display:flex;"><span>        env         <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">resources</span> {
</span></span><span style="display:flex;"><span>        cpu    <span style="color:#f92672">=</span> <span style="color:#ae81ff">200</span>
</span></span><span style="display:flex;"><span>        memory <span style="color:#f92672">=</span> <span style="color:#ae81ff">200</span>
</span></span><span style="display:flex;"><span>        memory_max <span style="color:#f92672">=</span> <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>I like this approach as not much magic is going on here - we&rsquo;re simply calling a plain bash script and piping output from a running docker container to another docker container. As long as there are no breaking changes in <code>pg_dump</code>, we can forget about the backup job - it should <em>just work</em>.</p>]]></description><content:encoded><![CDATA[<p>&hellip;or rather: How <em>I</em> choose to backup databases when using Nomad.</p>
<p>When I was researching backup options after switching to Nomad, I considered using something like <a href="https://github.com/tiredofit/docker-db-backup">docker-db-backup</a>. I quickly realized one downside of having to remember to align postgres-client (backup container) with the version of the server (database container). And as I was running at that time five different databases (Postgres/MySQL) it was a deal-breaker for me.</p>
<p>After more reading, I have decided to write a bash script that would be using Nomad&rsquo;s <a href="https://www.nomadproject.io/docs/drivers/raw_exec">raw_exec</a> and <a href="https://www.nomadproject.io/docs/job-specification/periodic">cron</a> capabilities.</p>
<p>Leveraging Consul will help obtain the allocation id of the task we&rsquo;re interested in. Then we can execute <code>nomad alloc exec</code> to call <code>pg_dump</code> within a database docker container.</p>
<p>Then it&rsquo;s up to us what to do with that dump - I have decided to pipe the output to docker again by using <a href="https://github.com/Angatar/s3cmd">s3cmd docker image</a> to put it on the S3 bucket (actually a Minio bucket).
Note: I recommend using a backup location outside your data center as a good practice. I was using Minio as a training exercise.</p>
<h2 id="preparing-the-acl-token-for-the-script">Preparing the ACL token for the script</h2>
<p>You can skip this part if you&rsquo;re not using <a href="https://learn.hashicorp.com/tutorials/nomad/access-control?in=nomad/access-control">Nomad&rsquo;s ACL capabilities</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#75715e"># nomad-exec-policy.hcl
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">namespace</span> <span style="color:#e6db74">&#34;default&#34;</span> {
</span></span><span style="display:flex;"><span>  policy <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;write&#34;</span>
</span></span><span style="display:flex;"><span>  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;alloc-exec&#34;</span>]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Create a new policy using the file above:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>nomad acl policy apply -description <span style="color:#e6db74">&#34;Nomad exec policy&#34;</span> nomad-exec nomad-exec-policy.hcl
</span></span></code></pre></div><p>Create new token - <code>Secret ID</code> is the one you will need:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>nomad acl token create --global -name<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Nomad exec token&#34;</span> -policy<span style="color:#f92672">=</span>nomad-exec -type<span style="color:#f92672">=</span>client
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Accessor ID  <span style="color:#f92672">=</span> xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx
</span></span><span style="display:flex;"><span>Secret ID    <span style="color:#f92672">=</span> xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx
</span></span></code></pre></div><h3 id="nomad-database-job-and-backup-job">Nomad database job and backup job</h3>
<p>Prerequisites: I&rsquo;m using <a href="/blog/2021/12/22/consul-dns-services-discovery-in-docker-gotchas/">Consul for service discovery</a> and Vault for fetching passwords, but your mileage may vary here.</p>
<p>After you <a href="https://www.nomadproject.io/docs/drivers/raw_exec#client-requirements">enabled raw_exec on the client</a>, you should be good to create a new backup job.</p>
<p>I&rsquo;m using Vault to obtain DB&rsquo;s credentials for the backup and S3 credentials for s3cmd. S3cmd will send a backup to Minio exposed somewhere within the private network on 9000 port.</p>
<p>To make it all work together, we need a database task that exposes its allocation id (Nomad allocation id). We can register service in Consul and use the <em>tags</em> feature to do that.</p>
<p>To give you a better picture here is the <em>database job</em> (slimmed-down version, removed irrelevant definitions)</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">job</span> <span style="color:#e6db74">&#34;db-task&#34;</span> {
</span></span><span style="display:flex;"><span>  datacenters <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;dc1&#34;</span>]
</span></span><span style="display:flex;"><span>  type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;service&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">vault</span> {
</span></span><span style="display:flex;"><span>    policies <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;nomad-read&#34;</span>]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">group</span> <span style="color:#e6db74">&#34;db-task&#34;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">network</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">port</span> <span style="color:#e6db74">&#34;db&#34;</span> {
</span></span><span style="display:flex;"><span>        to <span style="color:#f92672">=</span> <span style="color:#ae81ff">5432</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # I&#39;m using internal network called &#39;private&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        host_network <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;private&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">task</span> <span style="color:#e6db74">&#34;db-task&#34;</span> {
</span></span><span style="display:flex;"><span>      driver <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;docker&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">config</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # ommiting volume mount here for brievity
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        image <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;postgres:14.0-alpine3.14&#34;</span>
</span></span><span style="display:flex;"><span>        ports <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;db&#34;</span>]
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">template</span> {
</span></span><span style="display:flex;"><span>        data <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">&lt;&lt;</span><span style="color:#66d9ef">EOH</span>
</span></span><span style="display:flex;"><span>{{<span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">with</span> <span style="color:#66d9ef">secret</span> <span style="color:#e6db74">&#34;kv-v1/nomad/db/postgres&#34;</span> <span style="color:#960050;background-color:#1e0010">-</span>}}
</span></span><span style="display:flex;"><span>POSTGRES_PASSWORD<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.password }}&#34;</span>
</span></span><span style="display:flex;"><span>POSTGRES_USER<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.user }}&#34;</span>
</span></span><span style="display:flex;"><span>POSTGRES_DB<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.db }}&#34;</span>
</span></span><span style="display:flex;"><span>{{<span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">end</span> <span style="color:#960050;background-color:#1e0010">-</span>}}
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">EOH</span>
</span></span><span style="display:flex;"><span>        destination <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;secrets/file.env&#34;</span>
</span></span><span style="display:flex;"><span>        env         <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">resources</span> {
</span></span><span style="display:flex;"><span>        cpu    <span style="color:#f92672">=</span> <span style="color:#ae81ff">200</span>
</span></span><span style="display:flex;"><span>        memory <span style="color:#f92672">=</span> <span style="color:#ae81ff">200</span>
</span></span><span style="display:flex;"><span>        memory_max <span style="color:#f92672">=</span> <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">service</span> {
</span></span><span style="display:flex;"><span>        name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;db-task&#34;</span>
</span></span><span style="display:flex;"><span>        port <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;db&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">	      # backup service will rely on that particular &#39;alloc&#39; tag
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        tags <span style="color:#f92672">=</span> [&#34;alloc<span style="color:#f92672">=</span><span style="color:#e6db74">${</span><span style="color:#960050;background-color:#1e0010">NOMAD_ALLOC_ID</span><span style="color:#e6db74">}</span><span style="color:#960050;background-color:#1e0010">&#34;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">check</span> {
</span></span><span style="display:flex;"><span>          type     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
</span></span><span style="display:flex;"><span>          interval <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;10s&#34;</span>
</span></span><span style="display:flex;"><span>          timeout  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;2s&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>And here is - completely separated - <em>backup job</em>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">job</span> <span style="color:#e6db74">&#34;db-backup&#34;</span> {
</span></span><span style="display:flex;"><span>  datacenters <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;dc1&#34;</span>]
</span></span><span style="display:flex;"><span>  type        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;batch&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">vault</span> {
</span></span><span style="display:flex;"><span>    policies <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;nomad-read&#34;</span>]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">periodic</span> {
</span></span><span style="display:flex;"><span>    cron             <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0 22 * * * *&#34;</span>
</span></span><span style="display:flex;"><span>    prohibit_overlap <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">group</span> <span style="color:#e6db74">&#34;db-backup&#34;</span> {  
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">task</span> <span style="color:#e6db74">&#34;postgres-backup&#34;</span> {
</span></span><span style="display:flex;"><span>      driver <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;raw_exec&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">config</span> {
</span></span><span style="display:flex;"><span>        command <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/bin/bash&#34;</span>
</span></span><span style="display:flex;"><span>        args    <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;local/script.sh&#34;</span>]
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">template</span> {
</span></span><span style="display:flex;"><span>        data        <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">&lt;&lt;</span><span style="color:#66d9ef">EOH</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">set</span> <span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">e</span>
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">nomad</span> <span style="color:#66d9ef">alloc</span> <span style="color:#66d9ef">exec</span> <span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">task</span> <span style="color:#66d9ef">db</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">task</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#66d9ef">DB_ALLOC_ID</span> \
</span></span><span style="display:flex;"><span>        bin/bash -c &#34;PGPASSWORD<span style="color:#f92672">=</span>$PGPASSWORD PGUSER<span style="color:#f92672">=</span>$PGUSER PGDATABASE<span style="color:#f92672">=</span>$PGDATABASE pg_dump --compress<span style="color:#f92672">=</span><span style="color:#ae81ff">4</span> <span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">v</span><span style="color:#960050;background-color:#1e0010">&#34;</span> <span style="color:#960050;background-color:#1e0010">|</span> \
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">docker</span> <span style="color:#66d9ef">run</span> <span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">i</span> <span style="color:#960050;background-color:#1e0010">--</span><span style="color:#66d9ef">rm</span> \
</span></span><span style="display:flex;"><span>        -e AWS_ACCESS_KEY_ID<span style="color:#f92672">=</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#66d9ef">AWS_ACCESS_KEY_ID</span> \
</span></span><span style="display:flex;"><span>        -e AWS_SECRET_ACCESS_KEY<span style="color:#f92672">=</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#66d9ef">AWS_SECRET_ACCESS_KEY</span> \
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">d3fk</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#66d9ef">s3cmd</span><span style="color:#960050;background-color:#1e0010">:</span><span style="color:#66d9ef">stable</span> \
</span></span><span style="display:flex;"><span>        --host<span style="color:#f92672">=</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#66d9ef">S3_HOST_BASE</span> \
</span></span><span style="display:flex;"><span>        <span style="color:#960050;background-color:#1e0010">--</span><span style="color:#66d9ef">no</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">ssl</span> \
</span></span><span style="display:flex;"><span>        --host-bucket<span style="color:#f92672">=</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#66d9ef">S3_HOST_BASE</span> <span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">v</span> \
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">put</span> <span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">s3</span><span style="color:#960050;background-color:#1e0010">://$</span><span style="color:#66d9ef">S3_BUCKET</span><span style="color:#960050;background-color:#1e0010">/$</span>(<span style="color:#66d9ef">date</span> <span style="color:#e6db74">&#34;+%Y-%m-%d---%H-%M-%S&#34;</span>).<span style="color:#66d9ef">dump</span>.<span style="color:#66d9ef">gz</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">EOH</span>
</span></span><span style="display:flex;"><span>        destination <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;local/script.sh&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">template</span> {
</span></span><span style="display:flex;"><span>        data <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">&lt;&lt;</span><span style="color:#66d9ef">EOH</span>
</span></span><span style="display:flex;"><span>{{<span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">with</span> <span style="color:#66d9ef">secret</span> <span style="color:#e6db74">&#34;kv-v1/nomad/db/postgres&#34;</span> <span style="color:#960050;background-color:#1e0010">-</span>}}
</span></span><span style="display:flex;"><span>PGPASSWORD<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.password }}&#34;</span>
</span></span><span style="display:flex;"><span>PGUSER<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.user }}&#34;</span>
</span></span><span style="display:flex;"><span>PGDATABASE<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.db }}&#34;</span>
</span></span><span style="display:flex;"><span>{{ <span style="color:#66d9ef">end</span> }}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>{{<span style="color:#960050;background-color:#1e0010">-</span> <span style="color:#66d9ef">with</span> <span style="color:#66d9ef">secret</span> <span style="color:#e6db74">&#34;kv-v1/nomad/s3/backup&#34;</span> <span style="color:#960050;background-color:#1e0010">-</span>}}
</span></span><span style="display:flex;"><span>AWS_ACCESS_KEY_ID<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.access_key_id }}&#34;</span>
</span></span><span style="display:flex;"><span>AWS_SECRET_ACCESS_KEY<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ .Data.secret_access_key }}&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># here you also might want to set NOMAD_TOKEN env
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># if you&#39;re using ACL capabilities
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>{{ <span style="color:#66d9ef">end</span> }}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># as service &#39;db-task&#39; is registered in Consul
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># we wat to grab its &#39;alloc&#39; tag
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>{{- range $tag, $services :<span style="color:#f92672">=</span> <span style="color:#66d9ef">service</span> <span style="color:#e6db74">&#34;db-task&#34;</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#66d9ef">byTag</span> <span style="color:#960050;background-color:#1e0010">-</span>}}
</span></span><span style="display:flex;"><span>{{<span style="color:#66d9ef">if</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#66d9ef">tag</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#66d9ef">contains</span> <span style="color:#e6db74">&#34;alloc&#34;</span>}}
</span></span><span style="display:flex;"><span>{{$allocId :<span style="color:#f92672">=</span> index ($tag | split &#34;<span style="color:#f92672">=</span><span style="color:#960050;background-color:#1e0010">&#34;</span>) <span style="color:#ae81ff">1</span>}}
</span></span><span style="display:flex;"><span>DB_ALLOC_ID<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ $allocId }}&#34;</span>
</span></span><span style="display:flex;"><span>{{<span style="color:#66d9ef">end</span>}}
</span></span><span style="display:flex;"><span>{{<span style="color:#66d9ef">end</span>}}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># relying on service DNS discovery provided by Consul
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># to obtain Minio IP address
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>S3_HOST_BASE<span style="color:#f92672">=</span><span style="color:#66d9ef">minio</span>.<span style="color:#66d9ef">service</span>.<span style="color:#66d9ef">consul</span><span style="color:#960050;background-color:#1e0010">:</span><span style="color:#ae81ff">9000</span>
</span></span><span style="display:flex;"><span>S3_BUCKET<span style="color:#f92672">=</span><span style="color:#66d9ef">my</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">bucket</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">name</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">EOH</span>
</span></span><span style="display:flex;"><span>        destination <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;secrets/file.env&#34;</span>
</span></span><span style="display:flex;"><span>        env         <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">resources</span> {
</span></span><span style="display:flex;"><span>        cpu    <span style="color:#f92672">=</span> <span style="color:#ae81ff">200</span>
</span></span><span style="display:flex;"><span>        memory <span style="color:#f92672">=</span> <span style="color:#ae81ff">200</span>
</span></span><span style="display:flex;"><span>        memory_max <span style="color:#f92672">=</span> <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>I like this approach as not much magic is going on here - we&rsquo;re simply calling a plain bash script and piping output from a running docker container to another docker container. As long as there are no breaking changes in <code>pg_dump</code>, we can forget about the backup job - it should <em>just work</em>.</p>]]></content:encoded></item><item><title>Maintaining 10-year-old Rails app</title><link>https://awesomeprogrammer.com/blog/2022/06/05/maintaining-10-year-old-rails-app/</link><pubDate>Sun, 05 Jun 2022 09:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2022/06/05/maintaining-10-year-old-rails-app/</guid><description><![CDATA[<p>I still maintain <a href="https://quake.net.pl">quake.org.pl</a> - a site established around 1998, which was the biggest portal dedicated to Quake 3 Arena on the polish side of the internet. Nowadays, the traffic there is insignificant, but it had a great run and a very vibrant community back in the <em>golden days</em>.</p>
<p>I inherited its original PHP codebase like 12-13 years ago, and over 10 years ago, I decided to rewrite it to Rails. So it was my first more significant, <em>serious</em> Rails project - which real users and actual data.</p>
<p>I support this app up to this date. It&rsquo;s nothing fancy - a classic monolith with a bunch of ancient javascript spaghetti on top (oh yeah, we&rsquo;re beyond <em>sprinkles</em> stage).</p>
<p>What&rsquo;s the takeaway from the maintenance point of view after all those years? I would say test suite, automation (CI/CD), and being aware of dependencies graph.</p>
<h2 id="test-suite-ci-and-automation">Test suite, CI, and automation</h2>
<p>Code can be terrible, but you should be more or less good if you have a good safety net - a balanced mix of unit and feature specs. Then, touching pretty lousy code is not scary, and a quick feedback loop thanks to the CI will tell you if you broke something. So you can take small steps (commits) and slowly get to the point you&rsquo;re heading. I often thanked my <em>younger self</em> for investing in higher levels specs.</p>
<p>Upgrading Ruby is another story - nowadays, I usually go with docker - it&rsquo;s just easier to build a stable environment with all the dependencies. A good automation process (part of a continuous deployment pipeline) running on any CI service again can make it more or less frictionless.</p>
<h2 id="dependencies-graph">Dependencies graph</h2>
<p>We often think of dependencies but forget about the dependencies of those dependencies. I prefer to upgrade six different lightweight gems than one that brings more <em>implicit dependencies</em>; and those are often quite tricky. Suddenly you can&rsquo;t upgrade your dependency A, because B, relies on C, C relies on D, and D depends on a particular version of A.</p>
<p>Nowadays, I&rsquo;m also cautious with libraries that hook <em>too deply</em> into the framework - such gems give you splendid power, but the long-term cost might be too high. I have some &ldquo;trusted&rdquo; libs that survived major Rails upgraded through versions 2 to 7, but I take everything else with a reasonable grain of salt.</p>
<hr>
<p>Overall, as per usual, I don&rsquo;t think there is any secret sauce here. Take small steps: upgrade a single dependency once a week instead of upgrading everything at once every year, refactor two lines every second day instead of rewriting half of trying to rewrite half of the codebase. Take care of the test suite (because it&rsquo;s also code). Ensure your CI/CD pipeline runs smoothly (that includes eliminating flaky specs). And accept that some less-than-ideal code can just be left alone.</p>]]></description><content:encoded><![CDATA[<p>I still maintain <a href="https://quake.net.pl">quake.org.pl</a> - a site established around 1998, which was the biggest portal dedicated to Quake 3 Arena on the polish side of the internet. Nowadays, the traffic there is insignificant, but it had a great run and a very vibrant community back in the <em>golden days</em>.</p>
<p>I inherited its original PHP codebase like 12-13 years ago, and over 10 years ago, I decided to rewrite it to Rails. So it was my first more significant, <em>serious</em> Rails project - which real users and actual data.</p>
<p>I support this app up to this date. It&rsquo;s nothing fancy - a classic monolith with a bunch of ancient javascript spaghetti on top (oh yeah, we&rsquo;re beyond <em>sprinkles</em> stage).</p>
<p>What&rsquo;s the takeaway from the maintenance point of view after all those years? I would say test suite, automation (CI/CD), and being aware of dependencies graph.</p>
<h2 id="test-suite-ci-and-automation">Test suite, CI, and automation</h2>
<p>Code can be terrible, but you should be more or less good if you have a good safety net - a balanced mix of unit and feature specs. Then, touching pretty lousy code is not scary, and a quick feedback loop thanks to the CI will tell you if you broke something. So you can take small steps (commits) and slowly get to the point you&rsquo;re heading. I often thanked my <em>younger self</em> for investing in higher levels specs.</p>
<p>Upgrading Ruby is another story - nowadays, I usually go with docker - it&rsquo;s just easier to build a stable environment with all the dependencies. A good automation process (part of a continuous deployment pipeline) running on any CI service again can make it more or less frictionless.</p>
<h2 id="dependencies-graph">Dependencies graph</h2>
<p>We often think of dependencies but forget about the dependencies of those dependencies. I prefer to upgrade six different lightweight gems than one that brings more <em>implicit dependencies</em>; and those are often quite tricky. Suddenly you can&rsquo;t upgrade your dependency A, because B, relies on C, C relies on D, and D depends on a particular version of A.</p>
<p>Nowadays, I&rsquo;m also cautious with libraries that hook <em>too deply</em> into the framework - such gems give you splendid power, but the long-term cost might be too high. I have some &ldquo;trusted&rdquo; libs that survived major Rails upgraded through versions 2 to 7, but I take everything else with a reasonable grain of salt.</p>
<hr>
<p>Overall, as per usual, I don&rsquo;t think there is any secret sauce here. Take small steps: upgrade a single dependency once a week instead of upgrading everything at once every year, refactor two lines every second day instead of rewriting half of trying to rewrite half of the codebase. Take care of the test suite (because it&rsquo;s also code). Ensure your CI/CD pipeline runs smoothly (that includes eliminating flaky specs). And accept that some less-than-ideal code can just be left alone.</p>]]></content:encoded></item><item><title>How to finish your side project?</title><link>https://awesomeprogrammer.com/blog/2022/04/08/how-to-finish-your-side-project/</link><pubDate>Fri, 08 Apr 2022 18:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2022/04/08/how-to-finish-your-side-project/</guid><description><![CDATA[<p>Want the real answer? Do the work. <em>Thank you for coming to my TED talk.</em></p>
<hr>
<p>Over one year ago, my GF and I had an idea for creating a simple web app that would extract Kindle clippings into some useful formats like Markdown (to store those in <a href="https://obsidian.md">Obsidian</a>) or CSV (for further import).</p>
<p>Almost a year ago, I registered a domain, set up a landing in one evening, we started some drafts on code together, and then&hellip; nothing. Why it&rsquo;s so hard to do actual work?</p>
<p>Aside from, you know, <em>life</em> why do developers so rarely finish their projects? I can speak only from my experience - but I think it&rsquo;s mostly the <em>boring bits</em>. Solving some new problem, trying new technology is always exciting, but you rarely can escape the dull part.</p>
<p>For the signup, do I do OAuth or password/email? What about password recovery? Or right, the SMTP then, so I need some mailers. What about error reporting? Do I care about logs? Do I deploy this in the cloud somewhere? I probably could use a landing. And some interface. Do I use bootstrap and plan HTML or&hellip; and the story goes on.</p>
<p>Most developers <em>hate</em> the boring bits. I know I hate those bits. After dealing with boring bits at your day job/gig, how does one force self to do it all over again?</p>
<p>My not-so-secret recipe is <a href="https://awesomeprogrammer.com/blog/2021/07/16/what-i-have-done-so-far-part-11">consistency</a>. We forced ourselves, made at least a single contribution a day, and <a href="https://highlightly.io">released</a> a half-baked MVP for our amusement. It took us 10 months of waiting and 20 days of work.</p>
]]></description><content:encoded><![CDATA[<p>Want the real answer? Do the work. <em>Thank you for coming to my TED talk.</em></p>
<hr>
<p>Over one year ago, my GF and I had an idea for creating a simple web app that would extract Kindle clippings into some useful formats like Markdown (to store those in <a href="https://obsidian.md">Obsidian</a>) or CSV (for further import).</p>
<p>Almost a year ago, I registered a domain, set up a landing in one evening, we started some drafts on code together, and then&hellip; nothing. Why it&rsquo;s so hard to do actual work?</p>
<p>Aside from, you know, <em>life</em> why do developers so rarely finish their projects? I can speak only from my experience - but I think it&rsquo;s mostly the <em>boring bits</em>. Solving some new problem, trying new technology is always exciting, but you rarely can escape the dull part.</p>
<p>For the signup, do I do OAuth or password/email? What about password recovery? Or right, the SMTP then, so I need some mailers. What about error reporting? Do I care about logs? Do I deploy this in the cloud somewhere? I probably could use a landing. And some interface. Do I use bootstrap and plan HTML or&hellip; and the story goes on.</p>
<p>Most developers <em>hate</em> the boring bits. I know I hate those bits. After dealing with boring bits at your day job/gig, how does one force self to do it all over again?</p>
<p>My not-so-secret recipe is <a href="https://awesomeprogrammer.com/blog/2021/07/16/what-i-have-done-so-far-part-11">consistency</a>. We forced ourselves, made at least a single contribution a day, and <a href="https://highlightly.io">released</a> a half-baked MVP for our amusement. It took us 10 months of waiting and 20 days of work.</p>
]]></content:encoded></item><item><title>Working remotely from a perspective</title><link>https://awesomeprogrammer.com/blog/2021/12/29/working-remotely-from-a-perspective/</link><pubDate>Wed, 29 Dec 2021 10:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/12/29/working-remotely-from-a-perspective/</guid><description><![CDATA[<p>I wrote (very vaguely) about remote work <a href="/blog/2014/05/04/working-remotely/">in mid-2014</a>. I need to stand by the first sentence tho - time does fly by fast! What changed in those 7 years?</p>
<p>I left a polish startup. Had a middle-life crisis. Bought a motorcycle. Travelled around souther Europe on that motorcycle. Found a place at a small, fully remote company from Switzerland. And never looked back at a office work since 2018.</p>
<p>I think the pros and cons of remote works have been mentioned a million times already. During the pandemic, a big chunk of the world suddenly was forced into remote work. I&rsquo;m just going to focus on a few things that matter <em>for me</em> and what <em>I think</em> are important when working fully remote.</p>
<h3 id="walks">Walks</h3>
<p>With remote work, it&rsquo;s ridiculously easy to <em>forget</em> that you need to move. In an office environment, it&rsquo;s effortless to <em>waste time</em> which is frankly good for you - taking a lunch, grabbing a tea/coffee/whatever in the office kitchen, going out to buy a snack. Plus, office spaces are usually much larger than our homes, so unconsciously, you&rsquo;re taking those walks anyway. I don&rsquo;t feel guilty anymore when I decide to do some small groceries mid-day or go out. If you feel exhausted after a day of work, that&rsquo;s an even better reason to take that walk - because most likely, you&rsquo;re tired mentally, not physically. Break the cycle, move your body, your mind will thank you later.</p>
<h3 id="good-enough-audio-video-setup">Good (enough) Audio-Video setup</h3>
<p>After you do something for yourself, it&rsquo;s time to do something for others. And I should have thought about it a little bit sooner, frankly speaking. If you&rsquo;re behind a screen for most of your day - when you have a call, make sure other people can see you and hear you clearly. I was guilty of not having a great setup, and it amazes me how many folks struggle with that still. I&rsquo;m not going to list the suggested hardware, but I will recommend one thing - record yourself. That&rsquo;s it. Open a video recorder and record yourself speaking for a few minutes. Do you use an appropriate microphone that cancels background noise? Should you maybe invest in a new web camera for those 1:1 sessions? Do you have a beard, and perhaps that wired microphone included with an iPhone isn&rsquo;t going to cut it because of the cable rubbing noise? (<em>guilty!</em>)</p>
<h3 id="building-meaningful-relationships">Building meaningful relationships</h3>
<p>I think it&rsquo;s the most challenging thing to pull off. I&rsquo;m the kind of person that likes to discuss complex subjects, get to know different points of view on things, hear about daily personal problems, struggles. I&rsquo;m not afraid of sharing and simply being vulnerable. I think it can help build <em>truly</em> meaningful relationships. Having such relationships - also at your workplace - makes life more fulfilling. As aside from day-to-day work, we&rsquo;re primarily human beings. Building such relationships takes a tremendous amount of time. I think building trust, making those connections by being behind a camera, behind words written on the screen - is even trickier.</p>
<p>Unfortunately, I don&rsquo;t have a piece of good advice here. I&rsquo;m still learning it as well. I&rsquo;m trying to make a conscious effort to build such relationships and be consistent about it.</p>]]></description><content:encoded><![CDATA[<p>I wrote (very vaguely) about remote work <a href="/blog/2014/05/04/working-remotely/">in mid-2014</a>. I need to stand by the first sentence tho - time does fly by fast! What changed in those 7 years?</p>
<p>I left a polish startup. Had a middle-life crisis. Bought a motorcycle. Travelled around souther Europe on that motorcycle. Found a place at a small, fully remote company from Switzerland. And never looked back at a office work since 2018.</p>
<p>I think the pros and cons of remote works have been mentioned a million times already. During the pandemic, a big chunk of the world suddenly was forced into remote work. I&rsquo;m just going to focus on a few things that matter <em>for me</em> and what <em>I think</em> are important when working fully remote.</p>
<h3 id="walks">Walks</h3>
<p>With remote work, it&rsquo;s ridiculously easy to <em>forget</em> that you need to move. In an office environment, it&rsquo;s effortless to <em>waste time</em> which is frankly good for you - taking a lunch, grabbing a tea/coffee/whatever in the office kitchen, going out to buy a snack. Plus, office spaces are usually much larger than our homes, so unconsciously, you&rsquo;re taking those walks anyway. I don&rsquo;t feel guilty anymore when I decide to do some small groceries mid-day or go out. If you feel exhausted after a day of work, that&rsquo;s an even better reason to take that walk - because most likely, you&rsquo;re tired mentally, not physically. Break the cycle, move your body, your mind will thank you later.</p>
<h3 id="good-enough-audio-video-setup">Good (enough) Audio-Video setup</h3>
<p>After you do something for yourself, it&rsquo;s time to do something for others. And I should have thought about it a little bit sooner, frankly speaking. If you&rsquo;re behind a screen for most of your day - when you have a call, make sure other people can see you and hear you clearly. I was guilty of not having a great setup, and it amazes me how many folks struggle with that still. I&rsquo;m not going to list the suggested hardware, but I will recommend one thing - record yourself. That&rsquo;s it. Open a video recorder and record yourself speaking for a few minutes. Do you use an appropriate microphone that cancels background noise? Should you maybe invest in a new web camera for those 1:1 sessions? Do you have a beard, and perhaps that wired microphone included with an iPhone isn&rsquo;t going to cut it because of the cable rubbing noise? (<em>guilty!</em>)</p>
<h3 id="building-meaningful-relationships">Building meaningful relationships</h3>
<p>I think it&rsquo;s the most challenging thing to pull off. I&rsquo;m the kind of person that likes to discuss complex subjects, get to know different points of view on things, hear about daily personal problems, struggles. I&rsquo;m not afraid of sharing and simply being vulnerable. I think it can help build <em>truly</em> meaningful relationships. Having such relationships - also at your workplace - makes life more fulfilling. As aside from day-to-day work, we&rsquo;re primarily human beings. Building such relationships takes a tremendous amount of time. I think building trust, making those connections by being behind a camera, behind words written on the screen - is even trickier.</p>
<p>Unfortunately, I don&rsquo;t have a piece of good advice here. I&rsquo;m still learning it as well. I&rsquo;m trying to make a conscious effort to build such relationships and be consistent about it.</p>]]></content:encoded></item><item><title>Consul DNS services discovery in docker - gotchas</title><link>https://awesomeprogrammer.com/blog/2021/12/22/consul-dns-services-discovery-in-docker-gotchas/</link><pubDate>Wed, 22 Dec 2021 10:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/12/22/consul-dns-services-discovery-in-docker-gotchas/</guid><description><![CDATA[<p><a href="https://learn.hashicorp.com/tutorials/consul/dns-forwarding">Official documentation</a> describes the DNS service discovery configuration rather clearly. If you&rsquo;re running consul as a system-wide service and running on the latest Ubuntu 20 LTS, you might encounter some gotchas as I did. I&rsquo;m going to list a few for future-me and random internet strangers ;).</p>
<h3 id="systemd-resolved-vs-dnsmasq">Systemd-resolved vs dnsmasq</h3>
<p>Latest ubuntu 20 LTS ships with systemd older than 246. That requires mapping ports with iptables. I had very little luck with that (for whatever reason, I don&rsquo;t even recall anymore 😅), so instead, I installed <code>Dnsmasq</code>. Remember to disable <code>systemd-resolved</code> <em>after</em> you install dnsmasq - otherwise, you might end up with a broken DNS configuration.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># once you have dnsmasq installed you can disable systemd-resolved</span>
</span></span><span style="display:flex;"><span>systemctl stop systemd-resolved
</span></span><span style="display:flex;"><span>systemctl disable systemd-resolved
</span></span></code></pre></div><h3 id="dnsmasq-configuration">Dnsmasq configuration</h3>
<p>Here is mine <code>/etc/resolv.conf</code> file. I prefer using Cloudflare DNS as a fallback if dnsmasq gets misconfigured or any other potential crash.</p>
<p>That should just work as expected <a href="https://man7.org/linux/man-pages/man5/resolv.conf.5.html">according to this doc</a>:</p>
<blockquote>
<p>If there are multiple servers, the resolver library queries them in the  order listed</p>
</blockquote>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># /etc/resolv.conf</span>
</span></span><span style="display:flex;"><span>nameserver 127.0.0.1
</span></span><span style="display:flex;"><span>nameserver 1.1.1.1
</span></span></code></pre></div><p>To configure dnsmasq for consul I created <code>/etc/dnsmasq.d/10-consul</code> file. Consul DNS service is running on port 8600, dnsmasq is listening on localhost and Docker network bridge (172.17.0.1).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>server<span style="color:#f92672">=</span>/consul/127.0.0.1#8600
</span></span><span style="display:flex;"><span>bind-interfaces
</span></span><span style="display:flex;"><span>no-resolv
</span></span><span style="display:flex;"><span>server<span style="color:#f92672">=</span>1.1.1.1
</span></span><span style="display:flex;"><span>server<span style="color:#f92672">=</span>1.0.0.1
</span></span><span style="display:flex;"><span>listen-address<span style="color:#f92672">=</span>127.0.0.1
</span></span><span style="display:flex;"><span>listen-address<span style="color:#f92672">=</span>172.17.0.1
</span></span></code></pre></div><p>Depending on how you install dnsmasq I recommend looking into <code>/lib/systemd/system/dnsmasq.service</code> and ensuring to always restart service on failure.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#f92672">[</span>Service<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>TimeoutStartSec<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>Restart<span style="color:#f92672">=</span>on-failure
</span></span><span style="display:flex;"><span>RestartSec<span style="color:#f92672">=</span>5s
</span></span><span style="display:flex;"><span>...
</span></span></code></pre></div><h3 id="consul-configuration">Consul configuration</h3>
<p>Consul DNS service listens on localhost on port 8600 - nothing fancy here.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># /etc/consul/config.json</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;addresses&#34;</span>: <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;dns&#34;</span>: <span style="color:#e6db74">&#34;127.0.0.1&#34;</span>,
</span></span><span style="display:flex;"><span>  ...
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>,
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;ports&#34;</span>: <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;dns&#34;</span>: 8600,
</span></span><span style="display:flex;"><span>  ...
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><h3 id="consul-dns-acl-gotchas">Consul DNS ACL gotchas</h3>
<p>One minor but crucial thing to notice here is that if you have acl enabled with <code>&quot;default_policy&quot;: &quot;deny&quot;</code> in consul, your DNS queries will <strong>NOT WORK</strong> by default. The irony here is that it&rsquo;s also extremely easy to miss.</p>
<p>To fix it first create <code>dns-request-policy.hcl</code> file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>node_prefix <span style="color:#e6db74">&#34;&#34;</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  policy <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;read&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>service_prefix <span style="color:#e6db74">&#34;&#34;</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  policy <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;read&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># only needed if using prepared queries</span>
</span></span><span style="display:flex;"><span>query_prefix <span style="color:#e6db74">&#34;&#34;</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  policy <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;read&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>Then create a new policy in consul:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>consul acl policy create -name <span style="color:#e6db74">&#34;dns-requests&#34;</span> -rules @dns-request-policy.hcl
</span></span></code></pre></div><p>Create a token and use that token as consul default agent token:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>consul acl token create -description <span style="color:#e6db74">&#34;Token for DNS Requests&#34;</span> -policy-name dns-requests
</span></span><span style="display:flex;"><span>consul acl set-agent-token default &lt;token from command above&gt;
</span></span></code></pre></div><h3 id="docker-configuration">Docker configuration</h3>
<p>To query consul service from within the docker container, we need to define dns servers in <code>/etc/docker/daemon.json</code>. We will point the first one to the docker bridge network and add a fallback to Cloudflare just in case.</p>
<p>Honestly speaking, I&rsquo;m not 100% sure this is still required as <a href="https://docs.docker.com/config/containers/container-networking/">according to this docs</a> all external DNS lookups will be forwarded to the host by default:</p>
<blockquote>
<p>By default, a container inherits the DNS settings of the host, as defined in the /etc/resolv.conf configuration file. Containers that use the default bridge network get a copy of this file, whereas containers that use a custom network use Docker&rsquo;s embedded DNS server, which forwards external DNS lookups to the DNS servers configured on the host.</p>
</blockquote>
<p>In case you would like to configure it explicitly regardless, you can just do:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># /etc/docker/daemon.json</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;dns&#34;</span>: <span style="color:#f92672">[</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;172.17.0.1&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;1.1.1.1&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">]</span>,
</span></span><span style="display:flex;"><span>  ...
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>Once everything is up and running you should be able to query your consul services from within the docker containers via <code>&lt;service-name&gt;.service.consul</code> address.</p>]]></description><content:encoded><![CDATA[<p><a href="https://learn.hashicorp.com/tutorials/consul/dns-forwarding">Official documentation</a> describes the DNS service discovery configuration rather clearly. If you&rsquo;re running consul as a system-wide service and running on the latest Ubuntu 20 LTS, you might encounter some gotchas as I did. I&rsquo;m going to list a few for future-me and random internet strangers ;).</p>
<h3 id="systemd-resolved-vs-dnsmasq">Systemd-resolved vs dnsmasq</h3>
<p>Latest ubuntu 20 LTS ships with systemd older than 246. That requires mapping ports with iptables. I had very little luck with that (for whatever reason, I don&rsquo;t even recall anymore 😅), so instead, I installed <code>Dnsmasq</code>. Remember to disable <code>systemd-resolved</code> <em>after</em> you install dnsmasq - otherwise, you might end up with a broken DNS configuration.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># once you have dnsmasq installed you can disable systemd-resolved</span>
</span></span><span style="display:flex;"><span>systemctl stop systemd-resolved
</span></span><span style="display:flex;"><span>systemctl disable systemd-resolved
</span></span></code></pre></div><h3 id="dnsmasq-configuration">Dnsmasq configuration</h3>
<p>Here is mine <code>/etc/resolv.conf</code> file. I prefer using Cloudflare DNS as a fallback if dnsmasq gets misconfigured or any other potential crash.</p>
<p>That should just work as expected <a href="https://man7.org/linux/man-pages/man5/resolv.conf.5.html">according to this doc</a>:</p>
<blockquote>
<p>If there are multiple servers, the resolver library queries them in the  order listed</p>
</blockquote>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># /etc/resolv.conf</span>
</span></span><span style="display:flex;"><span>nameserver 127.0.0.1
</span></span><span style="display:flex;"><span>nameserver 1.1.1.1
</span></span></code></pre></div><p>To configure dnsmasq for consul I created <code>/etc/dnsmasq.d/10-consul</code> file. Consul DNS service is running on port 8600, dnsmasq is listening on localhost and Docker network bridge (172.17.0.1).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>server<span style="color:#f92672">=</span>/consul/127.0.0.1#8600
</span></span><span style="display:flex;"><span>bind-interfaces
</span></span><span style="display:flex;"><span>no-resolv
</span></span><span style="display:flex;"><span>server<span style="color:#f92672">=</span>1.1.1.1
</span></span><span style="display:flex;"><span>server<span style="color:#f92672">=</span>1.0.0.1
</span></span><span style="display:flex;"><span>listen-address<span style="color:#f92672">=</span>127.0.0.1
</span></span><span style="display:flex;"><span>listen-address<span style="color:#f92672">=</span>172.17.0.1
</span></span></code></pre></div><p>Depending on how you install dnsmasq I recommend looking into <code>/lib/systemd/system/dnsmasq.service</code> and ensuring to always restart service on failure.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#f92672">[</span>Service<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>TimeoutStartSec<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>Restart<span style="color:#f92672">=</span>on-failure
</span></span><span style="display:flex;"><span>RestartSec<span style="color:#f92672">=</span>5s
</span></span><span style="display:flex;"><span>...
</span></span></code></pre></div><h3 id="consul-configuration">Consul configuration</h3>
<p>Consul DNS service listens on localhost on port 8600 - nothing fancy here.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># /etc/consul/config.json</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;addresses&#34;</span>: <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;dns&#34;</span>: <span style="color:#e6db74">&#34;127.0.0.1&#34;</span>,
</span></span><span style="display:flex;"><span>  ...
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>,
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;ports&#34;</span>: <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;dns&#34;</span>: 8600,
</span></span><span style="display:flex;"><span>  ...
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><h3 id="consul-dns-acl-gotchas">Consul DNS ACL gotchas</h3>
<p>One minor but crucial thing to notice here is that if you have acl enabled with <code>&quot;default_policy&quot;: &quot;deny&quot;</code> in consul, your DNS queries will <strong>NOT WORK</strong> by default. The irony here is that it&rsquo;s also extremely easy to miss.</p>
<p>To fix it first create <code>dns-request-policy.hcl</code> file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>node_prefix <span style="color:#e6db74">&#34;&#34;</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  policy <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;read&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>service_prefix <span style="color:#e6db74">&#34;&#34;</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  policy <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;read&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># only needed if using prepared queries</span>
</span></span><span style="display:flex;"><span>query_prefix <span style="color:#e6db74">&#34;&#34;</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  policy <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;read&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>Then create a new policy in consul:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>consul acl policy create -name <span style="color:#e6db74">&#34;dns-requests&#34;</span> -rules @dns-request-policy.hcl
</span></span></code></pre></div><p>Create a token and use that token as consul default agent token:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>consul acl token create -description <span style="color:#e6db74">&#34;Token for DNS Requests&#34;</span> -policy-name dns-requests
</span></span><span style="display:flex;"><span>consul acl set-agent-token default &lt;token from command above&gt;
</span></span></code></pre></div><h3 id="docker-configuration">Docker configuration</h3>
<p>To query consul service from within the docker container, we need to define dns servers in <code>/etc/docker/daemon.json</code>. We will point the first one to the docker bridge network and add a fallback to Cloudflare just in case.</p>
<p>Honestly speaking, I&rsquo;m not 100% sure this is still required as <a href="https://docs.docker.com/config/containers/container-networking/">according to this docs</a> all external DNS lookups will be forwarded to the host by default:</p>
<blockquote>
<p>By default, a container inherits the DNS settings of the host, as defined in the /etc/resolv.conf configuration file. Containers that use the default bridge network get a copy of this file, whereas containers that use a custom network use Docker&rsquo;s embedded DNS server, which forwards external DNS lookups to the DNS servers configured on the host.</p>
</blockquote>
<p>In case you would like to configure it explicitly regardless, you can just do:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># /etc/docker/daemon.json</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;dns&#34;</span>: <span style="color:#f92672">[</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;172.17.0.1&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;1.1.1.1&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">]</span>,
</span></span><span style="display:flex;"><span>  ...
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>Once everything is up and running you should be able to query your consul services from within the docker containers via <code>&lt;service-name&gt;.service.consul</code> address.</p>]]></content:encoded></item><item><title>New infrastructure - recap</title><link>https://awesomeprogrammer.com/blog/2021/12/10/new-infrastructure-recap/</link><pubDate>Fri, 10 Dec 2021 18:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/12/10/new-infrastructure-recap/</guid><description><![CDATA[<p>So I started to <a href="/blog/2021/11/14/nomad-1.1.6-in-practice-first-impressions/">play around with Hasicorp Nomad</a>, and for the time being, I&rsquo;m <em>done</em> (well, not really).</p>
<p>I have built the new infrastructure around Nomad and tooling provided by Hetzner Cloud - cloud servers (as I was already using those anyway), their load balancer, internal networking, firewall functionality, and attachable data volumes.</p>
<p><strong>Advertisement:</strong>  If you would like to play around with Hetzner - <a href="https://hetzner.cloud/?ref=bajSDn7obaDR">get 20EUR for new Hetzner Cloud account via this referral link</a></p>
<p>It took me like a month to move everything - I&rsquo;m still fixing some issues - but overall, it was pretty fun!</p>
<h3 id="the-big-picture">The big picture</h3>
<figure><img src="/images/infra/big_picture.jpg"/><figcaption>
            <h4>High level overview of new setup</h4>
        </figcaption>
</figure>

<ol>
<li>I&rsquo;m using Cloudflare / OVH for managing DNS. Cloudflare also acts as a reverse proxy/CDN by its nature.</li>
<li>I&rsquo;m using a single Hetzner Load Balancer that is exposed to an external network and connected to an internal Hetzner Network along with all my cloud servers. Everything except a single SSH port is behind iptables <em>and</em> behind firewall rules provided by Hetzner (because I don&rsquo;t trust my iptables skills :P). Internal network is considered as <em>trusted</em> and all traffic is allowed there.</li>
<li>On each host, I have traefik running. Hetzner Load Balancer is handling certificates tho (using ACME DNS challenge) - traefik uses consul and services tags to handle routing (port <code>:443</code> on balancer -&gt; goes to <code>:80</code> on the nodes and that goes to the traefik).</li>
</ol>
<h3 id="single-node-details">Single node details</h3>
<figure><img src="/images/infra/single_node.jpg"/><figcaption>
            <h4>Single server composition</h4>
        </figcaption>
</figure>

<ol>
<li>No magic here, the latest Ubuntu LTS bootstrapped with some Ansible playbooks.</li>
<li>Every server has consul, vault and Nomad installed.</li>
<li>Nomad acts as both server and a client (runs jobs) - which works perfectly fine for my needs.</li>
<li>Because the CSI drivers are absolutely <a href="https://github.com/hashicorp/nomad/issues/8057">not reliable</a>, some services are bound to particular nodes - it&rsquo;s a less than ideal workaround for now.</li>
</ol>
<h3 id="how-to-and-tools">How-to and tools</h3>
<p>I quickly realized that the <em>how</em> part is too big to cover in a single blog post, and I hope to follow up on the detailed setup, quirks, and gotchas in the upcoming weeks. First, I need to think about a good starting point as there is a lot to cover here!</p>
<p>Everything was set up using Ansible (cloud servers part) and Terraform (infrastructure - servers &amp; services). Every project runs as a docker container, and almost everything has a CI configured via Github Actions.</p>
<figure><img src="/images/infra/nomad.jpg"/><figcaption>
            <h4>Server nodes are not heavily utilized, and I will probably drop one soon :).</h4>
        </figcaption>
</figure>]]></description><content:encoded><![CDATA[<p>So I started to <a href="/blog/2021/11/14/nomad-1.1.6-in-practice-first-impressions/">play around with Hasicorp Nomad</a>, and for the time being, I&rsquo;m <em>done</em> (well, not really).</p>
<p>I have built the new infrastructure around Nomad and tooling provided by Hetzner Cloud - cloud servers (as I was already using those anyway), their load balancer, internal networking, firewall functionality, and attachable data volumes.</p>
<p><strong>Advertisement:</strong>  If you would like to play around with Hetzner - <a href="https://hetzner.cloud/?ref=bajSDn7obaDR">get 20EUR for new Hetzner Cloud account via this referral link</a></p>
<p>It took me like a month to move everything - I&rsquo;m still fixing some issues - but overall, it was pretty fun!</p>
<h3 id="the-big-picture">The big picture</h3>
<figure><img src="/images/infra/big_picture.jpg"/><figcaption>
            <h4>High level overview of new setup</h4>
        </figcaption>
</figure>

<ol>
<li>I&rsquo;m using Cloudflare / OVH for managing DNS. Cloudflare also acts as a reverse proxy/CDN by its nature.</li>
<li>I&rsquo;m using a single Hetzner Load Balancer that is exposed to an external network and connected to an internal Hetzner Network along with all my cloud servers. Everything except a single SSH port is behind iptables <em>and</em> behind firewall rules provided by Hetzner (because I don&rsquo;t trust my iptables skills :P). Internal network is considered as <em>trusted</em> and all traffic is allowed there.</li>
<li>On each host, I have traefik running. Hetzner Load Balancer is handling certificates tho (using ACME DNS challenge) - traefik uses consul and services tags to handle routing (port <code>:443</code> on balancer -&gt; goes to <code>:80</code> on the nodes and that goes to the traefik).</li>
</ol>
<h3 id="single-node-details">Single node details</h3>
<figure><img src="/images/infra/single_node.jpg"/><figcaption>
            <h4>Single server composition</h4>
        </figcaption>
</figure>

<ol>
<li>No magic here, the latest Ubuntu LTS bootstrapped with some Ansible playbooks.</li>
<li>Every server has consul, vault and Nomad installed.</li>
<li>Nomad acts as both server and a client (runs jobs) - which works perfectly fine for my needs.</li>
<li>Because the CSI drivers are absolutely <a href="https://github.com/hashicorp/nomad/issues/8057">not reliable</a>, some services are bound to particular nodes - it&rsquo;s a less than ideal workaround for now.</li>
</ol>
<h3 id="how-to-and-tools">How-to and tools</h3>
<p>I quickly realized that the <em>how</em> part is too big to cover in a single blog post, and I hope to follow up on the detailed setup, quirks, and gotchas in the upcoming weeks. First, I need to think about a good starting point as there is a lot to cover here!</p>
<p>Everything was set up using Ansible (cloud servers part) and Terraform (infrastructure - servers &amp; services). Every project runs as a docker container, and almost everything has a CI configured via Github Actions.</p>
<figure><img src="/images/infra/nomad.jpg"/><figcaption>
            <h4>Server nodes are not heavily utilized, and I will probably drop one soon :).</h4>
        </figcaption>
</figure>]]></content:encoded></item><item><title>MariaDB upgrade gotchas</title><link>https://awesomeprogrammer.com/blog/2021/12/06/mariadb-upgrade-gotchas/</link><pubDate>Mon, 06 Dec 2021 19:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/12/06/mariadb-upgrade-gotchas/</guid><description><![CDATA[<p>Due to a recent infrastructure redo, I had to upgrade a stack that I did not update for around two years. I had MariaDB 10.3.4 and decided to try to upgrade it directly to 10.7.1.</p>
<p>The overall process was relatively painless (data volume wasn&rsquo;t that big), but a few things caught me by surprise.</p>
<ul>
<li>
<p>somehow, I lacked index on a pretty big table - <em>I have no idea</em> how it worked in the older version, but suddenly, after deploying the whole stack to new servers, all hell broke loose. Rails started throwing ActiveRecord connection pool-related exceptions. The entire service was down - after inspecting CPU usage on database container and taking <a href="https://mariadb.com/kb/en/show-processlist/">a look into process list</a> I realized what was going on - I have a ridiculous amount of running queries hanging there</p>
</li>
<li>
<p>so as you guessed from the above - MariaDB doesn&rsquo;t kill long-running queries by default - you can adjust that by setting <a href="https://mariadb.com/docs/reference/mdb/system-variables/max_statement_time/">max_statement_time</a> - it might be a good practice to set a reasonable timeout on that</p>
</li>
</ul>
<p><strong>Update 2021.12.19</strong>: I forgot to mention setting that setting <code>max_statement_time</code> affects your db backups - as mysqldump respects that value (which is not that obvious) - that might lead to incomplete sql dumps! <a href="https://dba.stackexchange.com/questions/223798/mysql-set-variable-in-mysqldump">See this stackexchange thread</a> for references. You might want to create a dedicated user for mysqldump or workaround the problem by setting the global value of <code>max_statement_time</code> during your backup (and restoring it after it&rsquo;s done) - both options are less than ideal. <a href="https://jira.mariadb.org/browse/MDEV-18702">This feature request from 2019</a> lists some reasonable suggestions (that will be implemented one day hopefully).</p>
<ul>
<li>
<p>when running MariaDB in docker, you can configure it on the CLI level. It&rsquo;s not documented <em>super clearly</em>, but if you pass something like <code>--max_statement_time=10 --slow_query_log=ON --long_query_time=2</code> as docker argument it should <em>just work</em></p>
</li>
<li>
<p>I never understood MySQL/MariaDB query planner (that&rsquo;s why I became Postgres fanboy :P) - sometimes seems it need help, and <a href="https://dev.mysql.com/doc/refman/8.0/en/index-hints.html">index hints</a> might come in handy in such cases (in case <code>ANALYZE TABLE</code> was no good)</p>
</li>
</ul>]]></description><content:encoded><![CDATA[<p>Due to a recent infrastructure redo, I had to upgrade a stack that I did not update for around two years. I had MariaDB 10.3.4 and decided to try to upgrade it directly to 10.7.1.</p>
<p>The overall process was relatively painless (data volume wasn&rsquo;t that big), but a few things caught me by surprise.</p>
<ul>
<li>
<p>somehow, I lacked index on a pretty big table - <em>I have no idea</em> how it worked in the older version, but suddenly, after deploying the whole stack to new servers, all hell broke loose. Rails started throwing ActiveRecord connection pool-related exceptions. The entire service was down - after inspecting CPU usage on database container and taking <a href="https://mariadb.com/kb/en/show-processlist/">a look into process list</a> I realized what was going on - I have a ridiculous amount of running queries hanging there</p>
</li>
<li>
<p>so as you guessed from the above - MariaDB doesn&rsquo;t kill long-running queries by default - you can adjust that by setting <a href="https://mariadb.com/docs/reference/mdb/system-variables/max_statement_time/">max_statement_time</a> - it might be a good practice to set a reasonable timeout on that</p>
</li>
</ul>
<p><strong>Update 2021.12.19</strong>: I forgot to mention setting that setting <code>max_statement_time</code> affects your db backups - as mysqldump respects that value (which is not that obvious) - that might lead to incomplete sql dumps! <a href="https://dba.stackexchange.com/questions/223798/mysql-set-variable-in-mysqldump">See this stackexchange thread</a> for references. You might want to create a dedicated user for mysqldump or workaround the problem by setting the global value of <code>max_statement_time</code> during your backup (and restoring it after it&rsquo;s done) - both options are less than ideal. <a href="https://jira.mariadb.org/browse/MDEV-18702">This feature request from 2019</a> lists some reasonable suggestions (that will be implemented one day hopefully).</p>
<ul>
<li>
<p>when running MariaDB in docker, you can configure it on the CLI level. It&rsquo;s not documented <em>super clearly</em>, but if you pass something like <code>--max_statement_time=10 --slow_query_log=ON --long_query_time=2</code> as docker argument it should <em>just work</em></p>
</li>
<li>
<p>I never understood MySQL/MariaDB query planner (that&rsquo;s why I became Postgres fanboy :P) - sometimes seems it need help, and <a href="https://dev.mysql.com/doc/refman/8.0/en/index-hints.html">index hints</a> might come in handy in such cases (in case <code>ANALYZE TABLE</code> was no good)</p>
</li>
</ul>]]></content:encoded></item><item><title>Open source analytics (in 2021)</title><link>https://awesomeprogrammer.com/blog/2021/11/29/open-source-analytics-in-2021/</link><pubDate>Mon, 29 Nov 2021 20:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/11/29/open-source-analytics-in-2021/</guid><description><![CDATA[<p>Over two years ago I wrote about <a href="/blog/2019/10/26/opensource-google-analytics-alternative-with-traefik/">open source alternatives to GA</a> and I was running <a href="https://count.ly/">countly</a> since then.</p>
<p>As I&rsquo;m <a href="/blog/2021/11/14/nomad-1.1.6-in-practice-first-impressions/">still redoing my infrastructure</a> using Nomad from Hashicorp after some struggle I realized that I don&rsquo;t want to move Countly after all. Reason below.</p>
<p>Unfortunately, Countly&rsquo;s <a href="https://support.count.ly/hc/en-us/articles/360037443652-Upgrading-Countly-server">upgrade process</a> on the self-hosted version running within a docker container was a little bit painful. After struggling with the mongodb upgrade itself and moving data between two hosts I kinda gave up and decided to look for something different. Given the number of different side projects I have any unpleasant maintenance is a big <em>no</em> in my book nowadays. <a href="https://usefathom.com/">Fathom</a> was still on the table as even I thought it was too limiting two years ago it turned out I don&rsquo;t spend that much time looking at those analytics ;).</p>
<p>The end goal is just to have some general overview of traffic - to see which content is being most engaging, nothing fancy. I went with <a href="https://plausible.io">plausible</a> that have pretty solid <a href="https://github.com/plausible/hosting/blob/master/docker-compose.yml">docker support</a> - UI wise it&rsquo;s pretty <em>very</em> similar to Fathom&rsquo;s, it integrates nicely with Google Search Console - so you can have even more insights in a single place without compromising your visitors&rsquo; privacy. Taking privacy to the extreme <a href="https://github.com/allinurl/goaccess">goaccess</a> might be next on my plate to check, but one thing at a time.</p>
<p>Hopefully, around the end of this year/early 2022, I will post how to set up Plausible using Nomad - first I need to fully migrate everything to new infra :).</p>
<figure><img src="/images/plausible.jpg"/><figcaption>
            <h4>Real-life screenshot from Plausible (added after the post was published)</h4>
        </figcaption>
</figure>]]></description><content:encoded><![CDATA[<p>Over two years ago I wrote about <a href="/blog/2019/10/26/opensource-google-analytics-alternative-with-traefik/">open source alternatives to GA</a> and I was running <a href="https://count.ly/">countly</a> since then.</p>
<p>As I&rsquo;m <a href="/blog/2021/11/14/nomad-1.1.6-in-practice-first-impressions/">still redoing my infrastructure</a> using Nomad from Hashicorp after some struggle I realized that I don&rsquo;t want to move Countly after all. Reason below.</p>
<p>Unfortunately, Countly&rsquo;s <a href="https://support.count.ly/hc/en-us/articles/360037443652-Upgrading-Countly-server">upgrade process</a> on the self-hosted version running within a docker container was a little bit painful. After struggling with the mongodb upgrade itself and moving data between two hosts I kinda gave up and decided to look for something different. Given the number of different side projects I have any unpleasant maintenance is a big <em>no</em> in my book nowadays. <a href="https://usefathom.com/">Fathom</a> was still on the table as even I thought it was too limiting two years ago it turned out I don&rsquo;t spend that much time looking at those analytics ;).</p>
<p>The end goal is just to have some general overview of traffic - to see which content is being most engaging, nothing fancy. I went with <a href="https://plausible.io">plausible</a> that have pretty solid <a href="https://github.com/plausible/hosting/blob/master/docker-compose.yml">docker support</a> - UI wise it&rsquo;s pretty <em>very</em> similar to Fathom&rsquo;s, it integrates nicely with Google Search Console - so you can have even more insights in a single place without compromising your visitors&rsquo; privacy. Taking privacy to the extreme <a href="https://github.com/allinurl/goaccess">goaccess</a> might be next on my plate to check, but one thing at a time.</p>
<p>Hopefully, around the end of this year/early 2022, I will post how to set up Plausible using Nomad - first I need to fully migrate everything to new infra :).</p>
<figure><img src="/images/plausible.jpg"/><figcaption>
            <h4>Real-life screenshot from Plausible (added after the post was published)</h4>
        </figcaption>
</figure>]]></content:encoded></item><item><title>Nomad 1.1.6 in practice - first impressions</title><link>https://awesomeprogrammer.com/blog/2021/11/14/nomad-1.1.6-in-practice-first-impressions/</link><pubDate>Sun, 14 Nov 2021 11:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/11/14/nomad-1.1.6-in-practice-first-impressions/</guid><description><![CDATA[<p><a href="https://www.nomadproject.io">Nomad by HashiCorp</a> is an interesting alternative for workload orchestration. As the project reached version 1.0 somewhere in late 2020 after checking out its current feature set I have decided to pull the trigger and migrate <em>all</em> of my side projects, tools, and whatnot into new infrastructure based on Nomad backed by Consul and Vault and even give a Terraform another try. So HashiCorp all way in ;).</p>
<p>The process is still ongoing - I have realized that my infra setup is around 2 years old already and during that time I setup <em>a lot</em> of different projects - some of them are tricky to update/move - eg. my Crystal app was created in Crystal 0.3.x era and basically <em>nothing works</em> nowadays. But that&rsquo;s a different story for a different post. Here are my first impressions regarding Nomad itself.</p>
<h3 id="deployment-without-tears-with-some-pain">Deployment without tears with some pain</h3>
<p>Nomad is a single binary so in theory deployment should be painless - there is even a pretty nice <a href="https://github.com/ansible-community/ansible-nomad">ansible playbook for doing so</a>. Yet when you need some more fine-grain configuration, Consul and Vault integration it suddenly becomes a little bit more tricky than advertised. Dealing with unsealing vault, configuring all the tokens, wrapping your head about ACL ideas and whatnot - it&rsquo;s <em>a lot</em> to take it. Once it works it works tho.</p>
<h3 id="nice-ui-with-nice-cli">Nice UI with nice CLI</h3>
<p>Nomad UI is pretty slick, single binary also acts as CLI - once you deal with all the authorization hassle it&rsquo;s cool to deploy new jobs straight from the terminal. UI provides just enough insight about what&rsquo;s going on there in terms of whole cluster, individual servers, and individual tasks even. So you don&rsquo;t have to throw a ton of other tools on top just to get started.</p>
<h3 id="no-networking-no-problem">No networking, no problem</h3>
<p>Nomad doesn&rsquo;t do a lot in terms of networking - but personally, I like that. I&rsquo;m running <em>trusted</em> environment so I can just leverage <em>internal</em> networking functionality that my hosting provider offers (so my nodes can speak with one another). Additionaly I have decided to use <em>their</em> load balancer and allow them to deal with the High Availability problem and I just route all traffic internally using Traefik that is running on every node.</p>
<h3 id="simple-yet-powerful-job-declaration">Simple yet powerful job declaration</h3>
<p>Once you wrap your head around ideas of <a href="https://www.nomadproject.io/docs/job-specification/job">job, group, and tasks</a> the whole thing just clicks. When you use Consul and Vault things become even more powerful - by using services discovery provided by Consul and secrets provided by Vault you can generate dynamic templates/env and Nomad will restart/reload your app when it detects changes. Allocating resources seems weird at first but then again you can have confidence that one misbehaving task won&rsquo;t take down your whole cluster.</p>
<h3 id="csi-volumes-in-practice---a-huge-disappointment">CSI Volumes in practice - a huge disappointment</h3>
<p>Support for <a href="https://github.com/container-storage-interface/spec">CSI volumes</a> sounded like heaven - FINALLY, I can run stateful jobs essentially with <em>almost a</em> HA setup - once node goes down, the job is reallocated to another node that picks up the volume and everything just continues to work - <em>brilliant</em>!</p>
<p>In practice it was a huge disappointment - because the whole concept is complex it is still <a href="https://github.com/hashicorp/nomad/issues/8057">quite buggy</a>. Unclear nomad shutdown can leave volumes in a <a href="https://github.com/hashicorp/nomad/issues/11477">zombie state</a> that are <a href="https://github.com/hashicorp/nomad/issues/10927">never re-attached properly</a> which defeats the purpose of this whole thing. Such a bummer!</p>
<p>For time being I just pinned volumes to particular nodes which again kinda defeats the purpose but at least until a node is up and running it works without surprises. Hopefully, I will revisit this problem soon-ish.</p>
<h3 id="lack-of-resources-and-sometimes-outdated-docs">Lack of resources and sometimes outdated docs</h3>
<p>Nomad community is not that huge and the docs <em>can</em> be sometimes outdated, various blog posts can be misleading - I hope this project will get more and more attention because it&rsquo;s a great alternative to what&rsquo;s on the market - and the marked is not that huge as k8s is eating the world. Before Nomad I even revisited Rancher (backed by k3s) and I hated what they have done to it in version 2.x. I very much preferred 1.x with their Cattle orchestration - seemed so much simpler and most of all reliable! (don&rsquo;t even start about k3s randomly crashing one me).</p>
<p>Nomad seems to hit the sweet spot - where you can fairly easily start with a small setup and potentially scale it to hundreds of servers if that may be needed.</p>
<hr>
<p>That&rsquo;s it for now when it comes to first impressions, I hope once I will manage to move all of my tooling to a new setup I will be able to provide more hands-on tips, tricks, and list potential pitfalls.</p>]]></description><content:encoded><![CDATA[<p><a href="https://www.nomadproject.io">Nomad by HashiCorp</a> is an interesting alternative for workload orchestration. As the project reached version 1.0 somewhere in late 2020 after checking out its current feature set I have decided to pull the trigger and migrate <em>all</em> of my side projects, tools, and whatnot into new infrastructure based on Nomad backed by Consul and Vault and even give a Terraform another try. So HashiCorp all way in ;).</p>
<p>The process is still ongoing - I have realized that my infra setup is around 2 years old already and during that time I setup <em>a lot</em> of different projects - some of them are tricky to update/move - eg. my Crystal app was created in Crystal 0.3.x era and basically <em>nothing works</em> nowadays. But that&rsquo;s a different story for a different post. Here are my first impressions regarding Nomad itself.</p>
<h3 id="deployment-without-tears-with-some-pain">Deployment without tears with some pain</h3>
<p>Nomad is a single binary so in theory deployment should be painless - there is even a pretty nice <a href="https://github.com/ansible-community/ansible-nomad">ansible playbook for doing so</a>. Yet when you need some more fine-grain configuration, Consul and Vault integration it suddenly becomes a little bit more tricky than advertised. Dealing with unsealing vault, configuring all the tokens, wrapping your head about ACL ideas and whatnot - it&rsquo;s <em>a lot</em> to take it. Once it works it works tho.</p>
<h3 id="nice-ui-with-nice-cli">Nice UI with nice CLI</h3>
<p>Nomad UI is pretty slick, single binary also acts as CLI - once you deal with all the authorization hassle it&rsquo;s cool to deploy new jobs straight from the terminal. UI provides just enough insight about what&rsquo;s going on there in terms of whole cluster, individual servers, and individual tasks even. So you don&rsquo;t have to throw a ton of other tools on top just to get started.</p>
<h3 id="no-networking-no-problem">No networking, no problem</h3>
<p>Nomad doesn&rsquo;t do a lot in terms of networking - but personally, I like that. I&rsquo;m running <em>trusted</em> environment so I can just leverage <em>internal</em> networking functionality that my hosting provider offers (so my nodes can speak with one another). Additionaly I have decided to use <em>their</em> load balancer and allow them to deal with the High Availability problem and I just route all traffic internally using Traefik that is running on every node.</p>
<h3 id="simple-yet-powerful-job-declaration">Simple yet powerful job declaration</h3>
<p>Once you wrap your head around ideas of <a href="https://www.nomadproject.io/docs/job-specification/job">job, group, and tasks</a> the whole thing just clicks. When you use Consul and Vault things become even more powerful - by using services discovery provided by Consul and secrets provided by Vault you can generate dynamic templates/env and Nomad will restart/reload your app when it detects changes. Allocating resources seems weird at first but then again you can have confidence that one misbehaving task won&rsquo;t take down your whole cluster.</p>
<h3 id="csi-volumes-in-practice---a-huge-disappointment">CSI Volumes in practice - a huge disappointment</h3>
<p>Support for <a href="https://github.com/container-storage-interface/spec">CSI volumes</a> sounded like heaven - FINALLY, I can run stateful jobs essentially with <em>almost a</em> HA setup - once node goes down, the job is reallocated to another node that picks up the volume and everything just continues to work - <em>brilliant</em>!</p>
<p>In practice it was a huge disappointment - because the whole concept is complex it is still <a href="https://github.com/hashicorp/nomad/issues/8057">quite buggy</a>. Unclear nomad shutdown can leave volumes in a <a href="https://github.com/hashicorp/nomad/issues/11477">zombie state</a> that are <a href="https://github.com/hashicorp/nomad/issues/10927">never re-attached properly</a> which defeats the purpose of this whole thing. Such a bummer!</p>
<p>For time being I just pinned volumes to particular nodes which again kinda defeats the purpose but at least until a node is up and running it works without surprises. Hopefully, I will revisit this problem soon-ish.</p>
<h3 id="lack-of-resources-and-sometimes-outdated-docs">Lack of resources and sometimes outdated docs</h3>
<p>Nomad community is not that huge and the docs <em>can</em> be sometimes outdated, various blog posts can be misleading - I hope this project will get more and more attention because it&rsquo;s a great alternative to what&rsquo;s on the market - and the marked is not that huge as k8s is eating the world. Before Nomad I even revisited Rancher (backed by k3s) and I hated what they have done to it in version 2.x. I very much preferred 1.x with their Cattle orchestration - seemed so much simpler and most of all reliable! (don&rsquo;t even start about k3s randomly crashing one me).</p>
<p>Nomad seems to hit the sweet spot - where you can fairly easily start with a small setup and potentially scale it to hundreds of servers if that may be needed.</p>
<hr>
<p>That&rsquo;s it for now when it comes to first impressions, I hope once I will manage to move all of my tooling to a new setup I will be able to provide more hands-on tips, tricks, and list potential pitfalls.</p>]]></content:encoded></item><item><title>Just Dance Controller on WearOS watch - proof of concept</title><link>https://awesomeprogrammer.com/blog/2021/10/25/just-dance-controller-on-wearos-watch-proof-of-concept/</link><pubDate>Mon, 25 Oct 2021 17:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/10/25/just-dance-controller-on-wearos-watch-proof-of-concept/</guid><description><![CDATA[<p>My partner really likes Just Dance - I wasn&rsquo;t aware of this game before tbh. But I quickly found it ridiculous that Just Dance doesn&rsquo;t have a wearable application that acts as a controller. The only way to run the game on Xbox S is to wave your phone in front of your TV. <em>What can possibly go wrong</em>, right?</p>
<p>I found <a href="https://www.reddit.com/r/JustDance/comments/kfclia/tutorial_just_dance_controller_on_android_smart/">numerous</a> examples of <a href="https://www.reddit.com/r/WearOS/comments/i2hfty/just_dance_controller_on_wear_os/">people running the controller app</a> on wear os devices thus I decided to give it a try myself!</p>
<p>Here are my steps:</p>
<ol>
<li>
<p>I bought a cheap WearOS device - got a really good deal on TicWatch E2 (rather ugly and slow but good enough)</p>
</li>
<li>
<p>Then I sideloaded <a href="https://apkpure.com/just-dance-controller/com.ubisoft.dance.justdance2015companion">Just Dance Controller APK v7.1.0</a> based on suggestions I found on Reddit threads linked above.</p>
</li>
<li>
<p>I scaled the screen using <code>adb</code> command to  <em>see</em> Just Dance controller app pairing screen - unfortunately it yielded the watch next to unusable - touch area within the app was so small that it was impossible to enter the pairing code.</p>
</li>
<li>
<p>So I worked around the problem - I ran <code>adb shell dumpsys input</code> to grab touch events and created a simple map for each clickable element - then wrote a telegram bot in golang to send those touch events (for pairing purposes) that used <a href="https://github.com/electricbubble/gadb">gadb</a> library to speak with <a href="https://hub.docker.com/r/sorccu/adb/">ADB docker container</a> and I ran both on my Synology NAS station within same VLAN (Xbox, watch and NAS device) - once you pass the pairing steps the rest <em>just works</em>.</p>
</li>
</ol>
<p>Funny enough even tho the setup looks really fragile it actually works pretty well - <a href="https://www.youtube.com/watch?v=_8wOveGl81o&amp;">see attached video</a>.</p>
]]></description><content:encoded><![CDATA[<p>My partner really likes Just Dance - I wasn&rsquo;t aware of this game before tbh. But I quickly found it ridiculous that Just Dance doesn&rsquo;t have a wearable application that acts as a controller. The only way to run the game on Xbox S is to wave your phone in front of your TV. <em>What can possibly go wrong</em>, right?</p>
<p>I found <a href="https://www.reddit.com/r/JustDance/comments/kfclia/tutorial_just_dance_controller_on_android_smart/">numerous</a> examples of <a href="https://www.reddit.com/r/WearOS/comments/i2hfty/just_dance_controller_on_wear_os/">people running the controller app</a> on wear os devices thus I decided to give it a try myself!</p>
<p>Here are my steps:</p>
<ol>
<li>
<p>I bought a cheap WearOS device - got a really good deal on TicWatch E2 (rather ugly and slow but good enough)</p>
</li>
<li>
<p>Then I sideloaded <a href="https://apkpure.com/just-dance-controller/com.ubisoft.dance.justdance2015companion">Just Dance Controller APK v7.1.0</a> based on suggestions I found on Reddit threads linked above.</p>
</li>
<li>
<p>I scaled the screen using <code>adb</code> command to  <em>see</em> Just Dance controller app pairing screen - unfortunately it yielded the watch next to unusable - touch area within the app was so small that it was impossible to enter the pairing code.</p>
</li>
<li>
<p>So I worked around the problem - I ran <code>adb shell dumpsys input</code> to grab touch events and created a simple map for each clickable element - then wrote a telegram bot in golang to send those touch events (for pairing purposes) that used <a href="https://github.com/electricbubble/gadb">gadb</a> library to speak with <a href="https://hub.docker.com/r/sorccu/adb/">ADB docker container</a> and I ran both on my Synology NAS station within same VLAN (Xbox, watch and NAS device) - once you pass the pairing steps the rest <em>just works</em>.</p>
</li>
</ol>
<p>Funny enough even tho the setup looks really fragile it actually works pretty well - <a href="https://www.youtube.com/watch?v=_8wOveGl81o&amp;">see attached video</a>.</p>
]]></content:encoded></item><item><title>Synology Home Video Server - attempt 2021</title><link>https://awesomeprogrammer.com/blog/2021/09/04/synology-home-video-server-attempt-2021/</link><pubDate>Sat, 04 Sep 2021 07:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/09/04/synology-home-video-server-attempt-2021/</guid><description><![CDATA[<p>Around two years ago I bought a new Synology DS918+ <a href="/blog/2019/03/09/how-to-migrate-your-old-synology-to-new-synology/">and wrote an article about my new media stack</a>. Since then over few iterations I recently ended up with yet another setup - that hopefully will be a final one ;).</p>
<p>Let&rsquo;s just jump straight into the how-to, I&rsquo;m gonna skip ansible syntax this time as we&rsquo;re gonna use <a href="https://containrrr.dev/watchtower/">Watchtower</a> to automatically upgrade running containers on our Synology.</p>
<h3 id="what-changed">What changed</h3>
<ul>
<li>
<p>I started using <a href="https://www.synology.com/en-us/dsm/feature/video_station">Video Station</a> a little bit more as they have quite a nice Android app that you can run on your TV, app can use an external player (I&rsquo;m using MX Player, but feel free to use VLC or whatever) to stream files directly using hardware acceleration. There is quite an interesting problem here regarding files indexing, but we will get to that later. As a side note the fact that often it&rsquo;s easier to download the show from torrent and stream it from the NAS than struggle with the unresponsiveness of HBO Go app for example is ridiculous. Talk about first-world problems, right?</p>
</li>
<li>
<p>Medusa was replaced by <a href="https://sonarr.tv/">Sonarr</a> - which is basically a Radarr, but for TV ;)</p>
</li>
<li>
<p>Switched back from rutorrent to transmission - I really like <a href="https://github.com/transmission-remote-gui/transgui">remote transmission gui</a> on my Mac (outdated, but still works)</p>
</li>
<li>
<p>to deal with auto unpack/extract I added <a href="https://github.com/davidnewhall/unpackerr">unpackerr</a> in the mix - rutorrent handled that for me before, <a href="https://github.com/Novik/ruTorrent/issues/1671">but it was really flaky</a></p>
</li>
<li>
<p>to deal with containers upgrade I decided to give a <a href="https://containrrr.dev/watchtower/">Watchtower</a> a go - so hopefully it&rsquo;s fire &amp; forget setup that will stay up to date and not break too soon ;)</p>
</li>
</ul>
<h3 id="prerequisites">Prerequisites</h3>
<ul>
<li>
<p>You need ssh access to your Synology with docker installed (via package manager), most of the docker related commands might be required to be ran via <code>sudo</code>.</p>
</li>
<li>
<p>I will be using <code>PUID</code> and <code>PGID</code> from my Synology <code>admin</code> user - you can obtain those values using <code>id</code> command from within the ssh Synology shell. We set it explicitly so it won&rsquo;t mess up file permissions on your system as we will be mounting host file system.</p>
</li>
<li>
<p>Make sure mounted directories are owned by the user &amp; group of your choice, plus check Read/Write permissions in <code>Settings</code> -&gt; <code>Shared folders</code> section on Synology - seems there is some extra security layer on top (at least in the newest DMS 7.x).</p>
</li>
<li>
<p>Change <code>TZ</code> (timezone) as well according to your location.</p>
</li>
</ul>
<h3 id="step-1---synoindex-docker-solution--workaround">Step 1 - synoindex docker solution / workaround</h3>
<p>As mentioned before we proceed we need to solve one issue regarding indexing files by Synology - so they will be visible in Video Station once downloaded. We need to inform Synoindex about that event - which is fine, both Radarr and Sonarr support that in their UI, but given the fact we run it in docker container there is a workaround we need to do. Thankfully a tool already exist called <a href="https://github.com/racklin/simple-synoindex-server">simple-synoindex-server</a> - just follow it&rsquo;s readme, create folder mappings and you can proceed!</p>
<p>Note: in case your process will be killed upon boot try running it via <code>nohup</code> or even adding <code>sleep 60</code> (or more) before the command - I&rsquo;m not sure how Synology boot process looks like and what is loaded when, but <em>it works for me</em> and I won&rsquo;t question that.</p>
<p>I will assume that extracted binaries are located under <code>/volume1/homes/admin/simple-synoindex-server</code>. You should have there <code>simple-synoindex-server.ini</code> with folder mappings, <code>synoindex</code> and <code>synoindex-server</code> executable files there.</p>
<p>My mappings look like the following:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[mappings]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">/tv</span><span style="color:#f92672">=</span><span style="color:#e6db74">/volume3/tv</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">/movies</span><span style="color:#f92672">=</span><span style="color:#e6db74">/volume2/movies</span>
</span></span></code></pre></div><h3 id="step-2---docker-network">Step 2 - Docker network</h3>
<p>First, let&rsquo;s create a network for all of our containers as we will need some between-containers communication. To keep things simple we will use one single network for that purpose. I will be called <code>media</code>, but feel free to use anything you like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker network create media
</span></span></code></pre></div><h3 id="step-3---jackett">Step 3 - Jackett</h3>
<p>This hasn&rsquo;t changed, I&rsquo;m still using <a href="https://github.com/Jackett/Jackett">Jackett</a> as a api-proxy for various private tracker sites. It worked great for years so I don&rsquo;t see the need to change it - once set up you don&rsquo;t have to touch it ever again.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker create <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name<span style="color:#f92672">=</span>jackett <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PUID<span style="color:#f92672">=</span><span style="color:#ae81ff">1024</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PGID<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e TZ<span style="color:#f92672">=</span>Europe/Warsaw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 9117:9117 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/docker/jackett:/config <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --network<span style="color:#f92672">=</span>media <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>10m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  linuxserver/jackett
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker start jackett
</span></span></code></pre></div><h3 id="step-4---radarr">Step 4 - Radarr</h3>
<p>The same goes for Radarr - worked great for years, one noticeable change is that we&rsquo;re gonna mount <code>simple-synoindex-server</code>. That will allow us  to enable indexing option found in <code>Settings</code> -&gt; <code>Connect</code> -&gt; <code>Synology Indexer</code>. It should work out of the box then with all the default settings.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>docker create <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name<span style="color:#f92672">=</span>radarr <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PUID<span style="color:#f92672">=</span><span style="color:#ae81ff">1024</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PGID<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e TZ<span style="color:#f92672">=</span>Europe/Warsaw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 7878:7878 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/docker/radarr:/config <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume2/movies:/movies <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume2/torrent:/downloads <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/homes/admin/simple-synoindex-server:/usr/syno/bin:ro <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --network<span style="color:#f92672">=</span>media <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>10m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  linuxserver/radarr
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker start radarr
</span></span></code></pre></div><h3 id="step-5---sonarr">Step 5 - Sonarr</h3>
<p>I moved from Medusa to Sonarr - mostly because Unpackerr supports both. I think Medusa did a pretty great job over the years, but Sonnarr is slick and you can just feel that the interface is much more responsive.</p>
<p>Once up &amp; running don&rsquo;t forget to enable Synology Indexer there as well.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker create <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name<span style="color:#f92672">=</span>sonarr <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PUID<span style="color:#f92672">=</span><span style="color:#ae81ff">1024</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PGID<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e TZ<span style="color:#f92672">=</span>Europe/Warsaw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 8989:8989 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/docker/sonarr:/config <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume3/tv:/tv <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume2/torrent:/downloads <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/homes/admin/simple-synoindex-server:/usr/syno/bin:ro <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --network<span style="color:#f92672">=</span>media <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>10m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  linuxserver/sonarr
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker start sonarr
</span></span></code></pre></div><h3 id="step-6---transmission">Step 6 - Transmission</h3>
<p>Transmission in my experience can lag a little bit in comparison to rtorrent when it comes to downloads speeds, but it&rsquo;s good enough for me and I like remote gui app on Mac.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker create <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name<span style="color:#f92672">=</span>transmission <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PUID<span style="color:#f92672">=</span><span style="color:#ae81ff">1024</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PGID<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e TZ<span style="color:#f92672">=</span>Europe/Warsaw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/docker/transmission:/config <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume2/torrent:/downloads <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 9091:9091 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 51413:51413 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 51413:51413/udp <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --network<span style="color:#f92672">=</span>media <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>10m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  linuxserver/transmission
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker start transmission
</span></span></code></pre></div><h3 id="step-7---unpackerr">Step 7 - Unpackerr</h3>
<p>Note: We will use hotio image as it allows to set user &amp; group id, seems linuxserver doesn&rsquo;t provide such image.</p>
<p>The great thing about unpackerr that it will clean up extracted files once they are imported by sonarr/radarr. Magic ✨. In short, it can pool various services and check their queues and orchestrate unpacking based on that  - straightforward, but smart!</p>
<p>You can find API keys to Sonarr &amp; Radarr in their <code>Settings</code> -&gt; <code>General</code> sections</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker create <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name unpackerr <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume2/torrent:/downloads <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PUID<span style="color:#f92672">=</span><span style="color:#ae81ff">1024</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PGID<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e TZ<span style="color:#f92672">=</span>Europe/Warsaw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e UN_SONARR_0_URL<span style="color:#f92672">=</span>http://sonarr:8989 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e UN_SONARR_0_API_KEY<span style="color:#f92672">=</span>SONARR-API-KEY <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e UN_RADARR_0_URL<span style="color:#f92672">=</span>http://radarr:7878 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e UN_RADARR_0_API_KEY<span style="color:#f92672">=</span>RADARR-API-KEY <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>10m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --network<span style="color:#f92672">=</span>media <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  hotio/unpackerr:release
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker start unpackerr
</span></span></code></pre></div><h3 id="step-8---watcher">Step 8 - Watcher</h3>
<p>One last step is to add watcher on top that will upgrade all the running containers on daily basis. We will also configure telegram <em>soft</em> notification.</p>
<p>Once you obtain your telegram token and chat id (out of scope of this article, but you can skip that part), replace <code>TELEGRAM-BOT-TOKEN</code> and <code>CHANNEL-ID</code> parts and run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker create <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name watchtower <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /var/run/docker.sock:/var/run/docker.sock <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /etc/localtime:/etc/localtime:ro <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e WATCHTOWER_NOTIFICATIONS<span style="color:#f92672">=</span>shoutrrr <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e WATCHTOWER_NOTIFICATION_URL<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;telegram://TELEGRAM-BOT-TOKEN@telegram/?channels=CHANNEL-ID&amp;notification=no&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e WATCHTOWER_CLEANUP<span style="color:#f92672">=</span>true <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>10m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  containrrr/watchtower
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker start unpackerr
</span></span></code></pre></div><p>And that&rsquo;s a wrap up! By now you should have 4 apps available on exposed ports and 2 apps just doing their jobs in the background :).</p>
<p>Looking for subtitles support? Check <a href="https://github.com/morpheus65535/bazarr">bazaar</a> that also works on top of Radarr and Sonarr!</p>
]]></description><content:encoded><![CDATA[<p>Around two years ago I bought a new Synology DS918+ <a href="/blog/2019/03/09/how-to-migrate-your-old-synology-to-new-synology/">and wrote an article about my new media stack</a>. Since then over few iterations I recently ended up with yet another setup - that hopefully will be a final one ;).</p>
<p>Let&rsquo;s just jump straight into the how-to, I&rsquo;m gonna skip ansible syntax this time as we&rsquo;re gonna use <a href="https://containrrr.dev/watchtower/">Watchtower</a> to automatically upgrade running containers on our Synology.</p>
<h3 id="what-changed">What changed</h3>
<ul>
<li>
<p>I started using <a href="https://www.synology.com/en-us/dsm/feature/video_station">Video Station</a> a little bit more as they have quite a nice Android app that you can run on your TV, app can use an external player (I&rsquo;m using MX Player, but feel free to use VLC or whatever) to stream files directly using hardware acceleration. There is quite an interesting problem here regarding files indexing, but we will get to that later. As a side note the fact that often it&rsquo;s easier to download the show from torrent and stream it from the NAS than struggle with the unresponsiveness of HBO Go app for example is ridiculous. Talk about first-world problems, right?</p>
</li>
<li>
<p>Medusa was replaced by <a href="https://sonarr.tv/">Sonarr</a> - which is basically a Radarr, but for TV ;)</p>
</li>
<li>
<p>Switched back from rutorrent to transmission - I really like <a href="https://github.com/transmission-remote-gui/transgui">remote transmission gui</a> on my Mac (outdated, but still works)</p>
</li>
<li>
<p>to deal with auto unpack/extract I added <a href="https://github.com/davidnewhall/unpackerr">unpackerr</a> in the mix - rutorrent handled that for me before, <a href="https://github.com/Novik/ruTorrent/issues/1671">but it was really flaky</a></p>
</li>
<li>
<p>to deal with containers upgrade I decided to give a <a href="https://containrrr.dev/watchtower/">Watchtower</a> a go - so hopefully it&rsquo;s fire &amp; forget setup that will stay up to date and not break too soon ;)</p>
</li>
</ul>
<h3 id="prerequisites">Prerequisites</h3>
<ul>
<li>
<p>You need ssh access to your Synology with docker installed (via package manager), most of the docker related commands might be required to be ran via <code>sudo</code>.</p>
</li>
<li>
<p>I will be using <code>PUID</code> and <code>PGID</code> from my Synology <code>admin</code> user - you can obtain those values using <code>id</code> command from within the ssh Synology shell. We set it explicitly so it won&rsquo;t mess up file permissions on your system as we will be mounting host file system.</p>
</li>
<li>
<p>Make sure mounted directories are owned by the user &amp; group of your choice, plus check Read/Write permissions in <code>Settings</code> -&gt; <code>Shared folders</code> section on Synology - seems there is some extra security layer on top (at least in the newest DMS 7.x).</p>
</li>
<li>
<p>Change <code>TZ</code> (timezone) as well according to your location.</p>
</li>
</ul>
<h3 id="step-1---synoindex-docker-solution--workaround">Step 1 - synoindex docker solution / workaround</h3>
<p>As mentioned before we proceed we need to solve one issue regarding indexing files by Synology - so they will be visible in Video Station once downloaded. We need to inform Synoindex about that event - which is fine, both Radarr and Sonarr support that in their UI, but given the fact we run it in docker container there is a workaround we need to do. Thankfully a tool already exist called <a href="https://github.com/racklin/simple-synoindex-server">simple-synoindex-server</a> - just follow it&rsquo;s readme, create folder mappings and you can proceed!</p>
<p>Note: in case your process will be killed upon boot try running it via <code>nohup</code> or even adding <code>sleep 60</code> (or more) before the command - I&rsquo;m not sure how Synology boot process looks like and what is loaded when, but <em>it works for me</em> and I won&rsquo;t question that.</p>
<p>I will assume that extracted binaries are located under <code>/volume1/homes/admin/simple-synoindex-server</code>. You should have there <code>simple-synoindex-server.ini</code> with folder mappings, <code>synoindex</code> and <code>synoindex-server</code> executable files there.</p>
<p>My mappings look like the following:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[mappings]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">/tv</span><span style="color:#f92672">=</span><span style="color:#e6db74">/volume3/tv</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">/movies</span><span style="color:#f92672">=</span><span style="color:#e6db74">/volume2/movies</span>
</span></span></code></pre></div><h3 id="step-2---docker-network">Step 2 - Docker network</h3>
<p>First, let&rsquo;s create a network for all of our containers as we will need some between-containers communication. To keep things simple we will use one single network for that purpose. I will be called <code>media</code>, but feel free to use anything you like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker network create media
</span></span></code></pre></div><h3 id="step-3---jackett">Step 3 - Jackett</h3>
<p>This hasn&rsquo;t changed, I&rsquo;m still using <a href="https://github.com/Jackett/Jackett">Jackett</a> as a api-proxy for various private tracker sites. It worked great for years so I don&rsquo;t see the need to change it - once set up you don&rsquo;t have to touch it ever again.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker create <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name<span style="color:#f92672">=</span>jackett <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PUID<span style="color:#f92672">=</span><span style="color:#ae81ff">1024</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PGID<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e TZ<span style="color:#f92672">=</span>Europe/Warsaw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 9117:9117 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/docker/jackett:/config <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --network<span style="color:#f92672">=</span>media <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>10m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  linuxserver/jackett
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker start jackett
</span></span></code></pre></div><h3 id="step-4---radarr">Step 4 - Radarr</h3>
<p>The same goes for Radarr - worked great for years, one noticeable change is that we&rsquo;re gonna mount <code>simple-synoindex-server</code>. That will allow us  to enable indexing option found in <code>Settings</code> -&gt; <code>Connect</code> -&gt; <code>Synology Indexer</code>. It should work out of the box then with all the default settings.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>docker create <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name<span style="color:#f92672">=</span>radarr <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PUID<span style="color:#f92672">=</span><span style="color:#ae81ff">1024</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PGID<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e TZ<span style="color:#f92672">=</span>Europe/Warsaw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 7878:7878 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/docker/radarr:/config <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume2/movies:/movies <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume2/torrent:/downloads <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/homes/admin/simple-synoindex-server:/usr/syno/bin:ro <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --network<span style="color:#f92672">=</span>media <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>10m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  linuxserver/radarr
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker start radarr
</span></span></code></pre></div><h3 id="step-5---sonarr">Step 5 - Sonarr</h3>
<p>I moved from Medusa to Sonarr - mostly because Unpackerr supports both. I think Medusa did a pretty great job over the years, but Sonnarr is slick and you can just feel that the interface is much more responsive.</p>
<p>Once up &amp; running don&rsquo;t forget to enable Synology Indexer there as well.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker create <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name<span style="color:#f92672">=</span>sonarr <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PUID<span style="color:#f92672">=</span><span style="color:#ae81ff">1024</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PGID<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e TZ<span style="color:#f92672">=</span>Europe/Warsaw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 8989:8989 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/docker/sonarr:/config <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume3/tv:/tv <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume2/torrent:/downloads <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/homes/admin/simple-synoindex-server:/usr/syno/bin:ro <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --network<span style="color:#f92672">=</span>media <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>10m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  linuxserver/sonarr
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker start sonarr
</span></span></code></pre></div><h3 id="step-6---transmission">Step 6 - Transmission</h3>
<p>Transmission in my experience can lag a little bit in comparison to rtorrent when it comes to downloads speeds, but it&rsquo;s good enough for me and I like remote gui app on Mac.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker create <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name<span style="color:#f92672">=</span>transmission <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PUID<span style="color:#f92672">=</span><span style="color:#ae81ff">1024</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PGID<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e TZ<span style="color:#f92672">=</span>Europe/Warsaw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume1/docker/transmission:/config <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume2/torrent:/downloads <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 9091:9091 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 51413:51413 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -p 51413:51413/udp <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --network<span style="color:#f92672">=</span>media <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>10m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  linuxserver/transmission
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker start transmission
</span></span></code></pre></div><h3 id="step-7---unpackerr">Step 7 - Unpackerr</h3>
<p>Note: We will use hotio image as it allows to set user &amp; group id, seems linuxserver doesn&rsquo;t provide such image.</p>
<p>The great thing about unpackerr that it will clean up extracted files once they are imported by sonarr/radarr. Magic ✨. In short, it can pool various services and check their queues and orchestrate unpacking based on that  - straightforward, but smart!</p>
<p>You can find API keys to Sonarr &amp; Radarr in their <code>Settings</code> -&gt; <code>General</code> sections</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker create <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name unpackerr <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /volume2/torrent:/downloads <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PUID<span style="color:#f92672">=</span><span style="color:#ae81ff">1024</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e PGID<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e TZ<span style="color:#f92672">=</span>Europe/Warsaw <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e UN_SONARR_0_URL<span style="color:#f92672">=</span>http://sonarr:8989 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e UN_SONARR_0_API_KEY<span style="color:#f92672">=</span>SONARR-API-KEY <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e UN_RADARR_0_URL<span style="color:#f92672">=</span>http://radarr:7878 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e UN_RADARR_0_API_KEY<span style="color:#f92672">=</span>RADARR-API-KEY <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>10m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --network<span style="color:#f92672">=</span>media <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  hotio/unpackerr:release
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker start unpackerr
</span></span></code></pre></div><h3 id="step-8---watcher">Step 8 - Watcher</h3>
<p>One last step is to add watcher on top that will upgrade all the running containers on daily basis. We will also configure telegram <em>soft</em> notification.</p>
<p>Once you obtain your telegram token and chat id (out of scope of this article, but you can skip that part), replace <code>TELEGRAM-BOT-TOKEN</code> and <code>CHANNEL-ID</code> parts and run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker create <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name watchtower <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /var/run/docker.sock:/var/run/docker.sock <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -v /etc/localtime:/etc/localtime:ro <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e WATCHTOWER_NOTIFICATIONS<span style="color:#f92672">=</span>shoutrrr <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e WATCHTOWER_NOTIFICATION_URL<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;telegram://TELEGRAM-BOT-TOKEN@telegram/?channels=CHANNEL-ID&amp;notification=no&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -e WATCHTOWER_CLEANUP<span style="color:#f92672">=</span>true <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-size<span style="color:#f92672">=</span>10m <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --log-opt max-file<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  containrrr/watchtower
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker start unpackerr
</span></span></code></pre></div><p>And that&rsquo;s a wrap up! By now you should have 4 apps available on exposed ports and 2 apps just doing their jobs in the background :).</p>
<p>Looking for subtitles support? Check <a href="https://github.com/morpheus65535/bazarr">bazaar</a> that also works on top of Radarr and Sonarr!</p>
]]></content:encoded></item><item><title>Nine years of blogging</title><link>https://awesomeprogrammer.com/blog/2021/07/23/nine-years-of-blogging/</link><pubDate>Fri, 23 Jul 2021 13:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/07/23/nine-years-of-blogging/</guid><description><![CDATA[<p>I started this blog 9 years ago. Time flies fast. Domain was coined from awesome-lists that were getting quite popular on Github back in those days. Nowadays I would go with something simpler I suppose, but what the hell, I come to like it.</p>
<p>I didn&rsquo;t have in mind what the blog would be about - I just wanted to put random thoughts on the internet - after all, I did that since I was a teenager during my time when I was running quake.net.pl</p>
<p>I put zero effort into positioning the blog, on marketing it anywhere really. Yet some people manage to still find with through search engines. Sometimes even it helps them to solve some problems. After recent googling, I discovered that in 2021 my post from 2012 <a href="/blog/2012/12/09/dealing-with-omniauth-redirect-uri-mismatch-invalid-port-number-gotcha/">about omniauth URI mismatch</a> is relevant for some folks. Internet is an amazing and funny place.</p>
<p>I don&rsquo;t write about rocket science stuff, because <em>I cannot into rockets</em>. But writing about some struggles with Linux libs or about how to overcome some js-library quirks can still be fun.</p>
<p>I don&rsquo;t care about pushing great content out there. In terms of quality and depth, I will go with mediocre every single day. Writing <em>is</em> hard and I think you just have to go out there and publish your stuff instead of trying to create that perfect blog with a perfect list of perfect blogposts. So here is 🥂 for the next nine years I suppose.</p>
]]></description><content:encoded><![CDATA[<p>I started this blog 9 years ago. Time flies fast. Domain was coined from awesome-lists that were getting quite popular on Github back in those days. Nowadays I would go with something simpler I suppose, but what the hell, I come to like it.</p>
<p>I didn&rsquo;t have in mind what the blog would be about - I just wanted to put random thoughts on the internet - after all, I did that since I was a teenager during my time when I was running quake.net.pl</p>
<p>I put zero effort into positioning the blog, on marketing it anywhere really. Yet some people manage to still find with through search engines. Sometimes even it helps them to solve some problems. After recent googling, I discovered that in 2021 my post from 2012 <a href="/blog/2012/12/09/dealing-with-omniauth-redirect-uri-mismatch-invalid-port-number-gotcha/">about omniauth URI mismatch</a> is relevant for some folks. Internet is an amazing and funny place.</p>
<p>I don&rsquo;t write about rocket science stuff, because <em>I cannot into rockets</em>. But writing about some struggles with Linux libs or about how to overcome some js-library quirks can still be fun.</p>
<p>I don&rsquo;t care about pushing great content out there. In terms of quality and depth, I will go with mediocre every single day. Writing <em>is</em> hard and I think you just have to go out there and publish your stuff instead of trying to create that perfect blog with a perfect list of perfect blogposts. So here is 🥂 for the next nine years I suppose.</p>
]]></content:encoded></item><item><title>What I have done so far? Part 13</title><link>https://awesomeprogrammer.com/blog/2021/07/18/what-i-have-done-so-far-part-13/</link><pubDate>Sun, 18 Jul 2021 10:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/07/18/what-i-have-done-so-far-part-13/</guid><description><![CDATA[<h3 id="learn-how-not-to-build-teams-and-how-not-to-communicate">Learn how not to build teams and how not to communicate</h3>
<p>I guess this will be the last part for time being, I skipped few less important things (or things I was too lazy to write about) - and this part is about when I joined a company as a contractor. No worries, you wont&rsquo; find the name of the company on my linked in profile, but I learned quite a lot about how <em>not</em> to do things when running a (remote) company.</p>
<h4 id="build-no-transparency-between-departments">Build no transparency between departments</h4>
<p>Let every department use a different tool to manage their work so everyone has to request permissions from different people to access anything. Let teams work in isolation, inform no one about their priorities, roadmaps, and goals. Have no single point of truth, no knowledge base, no wiki, no organization structure documented. This approach works even better when you have no dedicated product manager!</p>
<h4 id="build-no-transparency-within-teams">Build no transparency within teams</h4>
<p>Establish everything on private channels, email is great but having a call is even better. Documentation should exist only in heads of team members so new hires should just ask and figure out stuff on the go. Of course, takes your time when they as those questions and respond with proper delay. People should have their private priorities that should not be shared with anyone.</p>
<h4 id="build-no-trust">Build no trust</h4>
<p>Hire great people and treat them as an obstacle to your goals, listen to their suggestions, and immediately ignore those suggestions. After all, you know better what&rsquo;s best. Rejecting ideas is great but what&rsquo;s even better is simply ignoring those in the first place. The best feedback is no feedback.</p>
<h4 id="have-no-ownership-give-no-ownership">Have no ownership, give no ownership</h4>
<p>Speak about ownership, but provide no way to have ownership. Leave everything to some person who was the longest with the company and who <em>knows stuff</em>. That person is without a doubt a unicorn 10x employee so ignore any red flags about their behavior and communication style. Even better - adopt that style and make it a part of your &ldquo;culture&rdquo;.</p>
<p>Take away from that - not every place to work is a great place, even when you have privilege because your skills are in demand doesn&rsquo;t mean you won&rsquo;t get burned from time to time - but such failure can be an interesting learning experience (mentally taxing, but still).</p>
]]></description><content:encoded><![CDATA[<h3 id="learn-how-not-to-build-teams-and-how-not-to-communicate">Learn how not to build teams and how not to communicate</h3>
<p>I guess this will be the last part for time being, I skipped few less important things (or things I was too lazy to write about) - and this part is about when I joined a company as a contractor. No worries, you wont&rsquo; find the name of the company on my linked in profile, but I learned quite a lot about how <em>not</em> to do things when running a (remote) company.</p>
<h4 id="build-no-transparency-between-departments">Build no transparency between departments</h4>
<p>Let every department use a different tool to manage their work so everyone has to request permissions from different people to access anything. Let teams work in isolation, inform no one about their priorities, roadmaps, and goals. Have no single point of truth, no knowledge base, no wiki, no organization structure documented. This approach works even better when you have no dedicated product manager!</p>
<h4 id="build-no-transparency-within-teams">Build no transparency within teams</h4>
<p>Establish everything on private channels, email is great but having a call is even better. Documentation should exist only in heads of team members so new hires should just ask and figure out stuff on the go. Of course, takes your time when they as those questions and respond with proper delay. People should have their private priorities that should not be shared with anyone.</p>
<h4 id="build-no-trust">Build no trust</h4>
<p>Hire great people and treat them as an obstacle to your goals, listen to their suggestions, and immediately ignore those suggestions. After all, you know better what&rsquo;s best. Rejecting ideas is great but what&rsquo;s even better is simply ignoring those in the first place. The best feedback is no feedback.</p>
<h4 id="have-no-ownership-give-no-ownership">Have no ownership, give no ownership</h4>
<p>Speak about ownership, but provide no way to have ownership. Leave everything to some person who was the longest with the company and who <em>knows stuff</em>. That person is without a doubt a unicorn 10x employee so ignore any red flags about their behavior and communication style. Even better - adopt that style and make it a part of your &ldquo;culture&rdquo;.</p>
<p>Take away from that - not every place to work is a great place, even when you have privilege because your skills are in demand doesn&rsquo;t mean you won&rsquo;t get burned from time to time - but such failure can be an interesting learning experience (mentally taxing, but still).</p>
]]></content:encoded></item><item><title>What I have done so far? Part 12</title><link>https://awesomeprogrammer.com/blog/2021/07/17/what-i-have-done-so-far-part-12/</link><pubDate>Sat, 17 Jul 2021 12:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/07/17/what-i-have-done-so-far-part-12/</guid><description><![CDATA[<h3 id="build-yet-another-customer-successsupportcms-internal-tool">Build yet another customer success/support/CMS internal tool</h3>
<p>Over the period of the few last years, I have built several back-facing panels used by customer success teams, marketing folks, and other developers as well.</p>
<p>Various plug and play rails options like administrate or active admin are attractive options but in my experience quickly you will find them limiting and instead of speeding up do development process it will slow you down.</p>
<p>I think it&rsquo;s bound to happen by the design. Yes, at the beginning you will need some simple CRUD, but once you got a point where you start optimizing <em>the process</em> how a company does things you can throw that CRUD out of the window. You usually will need to automate use cases very specific to your business domain.</p>
<p>So simple update on subscription becomes subscription activation and deactivation process where a lot of different side effects might happen and various preconditions might be required. Or you find out that the customer support team needs to have some more information presented at glance to do their work efficiently. As a side note simply observing how people do their daily work (live or asking for a recording with out-loud thought process) can lead to amazing discoveries - you might be surprised how many low-hanging fruits you might find.</p>
<p>I tend to use all the good stuff from ruby/rails ecosystem. Simple_form with bootstrap or semantic ui, ransack for advanced search capabilities, or pundit to deal with permissions is my usual go-to kit. With very simple tooling you can build quite powerful apps to empower your organization.</p>
<p>I have some experience using tools like <a href="https://retool.com">Retool</a> and frankly speaking, it looks great on paper, but I found that: first, it&rsquo;s next to impossible to test in an automated manner; second, once you plug different sources into it (db and maybe some APIS) it becomes a bag of undocumented dependencies. Not to mention total vendor lock-in. Maybe it&rsquo;s great for small teams that need to implement simple things - hard to tell. But in the build vs buy department, I&rsquo;m usually in the <em>build</em> here.</p>
]]></description><content:encoded><![CDATA[<h3 id="build-yet-another-customer-successsupportcms-internal-tool">Build yet another customer success/support/CMS internal tool</h3>
<p>Over the period of the few last years, I have built several back-facing panels used by customer success teams, marketing folks, and other developers as well.</p>
<p>Various plug and play rails options like administrate or active admin are attractive options but in my experience quickly you will find them limiting and instead of speeding up do development process it will slow you down.</p>
<p>I think it&rsquo;s bound to happen by the design. Yes, at the beginning you will need some simple CRUD, but once you got a point where you start optimizing <em>the process</em> how a company does things you can throw that CRUD out of the window. You usually will need to automate use cases very specific to your business domain.</p>
<p>So simple update on subscription becomes subscription activation and deactivation process where a lot of different side effects might happen and various preconditions might be required. Or you find out that the customer support team needs to have some more information presented at glance to do their work efficiently. As a side note simply observing how people do their daily work (live or asking for a recording with out-loud thought process) can lead to amazing discoveries - you might be surprised how many low-hanging fruits you might find.</p>
<p>I tend to use all the good stuff from ruby/rails ecosystem. Simple_form with bootstrap or semantic ui, ransack for advanced search capabilities, or pundit to deal with permissions is my usual go-to kit. With very simple tooling you can build quite powerful apps to empower your organization.</p>
<p>I have some experience using tools like <a href="https://retool.com">Retool</a> and frankly speaking, it looks great on paper, but I found that: first, it&rsquo;s next to impossible to test in an automated manner; second, once you plug different sources into it (db and maybe some APIS) it becomes a bag of undocumented dependencies. Not to mention total vendor lock-in. Maybe it&rsquo;s great for small teams that need to implement simple things - hard to tell. But in the build vs buy department, I&rsquo;m usually in the <em>build</em> here.</p>
]]></content:encoded></item><item><title>What I have done so far? Part 11</title><link>https://awesomeprogrammer.com/blog/2021/07/16/what-i-have-done-so-far-part-11/</link><pubDate>Fri, 16 Jul 2021 10:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/07/16/what-i-have-done-so-far-part-11/</guid><description><![CDATA[<h3 id="fight-massive-technical-debt-on-all-fronts">Fight massive technical debt on all fronts</h3>
<p>Once I took over a project that was in a <em>really</em> bad shape. And it was not a joke - starting from useless tests that tested implementation itself, ridiculously complex codebase (everything interconnected with everything), and finishing with fragmented infrastructure (outdated Linode servers + stuff randomly deployed to Heroku).</p>
<p>Who would even pick up a project like this you ask? Probably not somebody sane. I realized it&rsquo;s often a matter of mindset. I treated that bulk of legacy code as a challenge and it was a very interesting learning experience. It&rsquo;s exciting to see how a living codebase can transform into something more manageable.</p>
<p>Sure, sometimes it was tidious and boring, but I was aware that it will take a while. I wrote while back about <a href="/blog/2020/05/21/delaying-gratification-in-software-development/">delaying gratification</a> and I think the same rule applies when pickup up project in such state - you have to accept that it will take time and you might see actual results awfully late. So moving that code around, adding tests will seems futile - but it will add up eventually. If I recall correctly power of small changes was also mentioned in <a href="https://www.goodreads.com/book/show/40121378-atomic-habits">Atomic habits</a> book (recommended btw!) - there will be a breaking point when those tiny improvements will be eventually noticable.</p>
<p>It took me a year to get it into a shape where we could bring another person on board. And from there on the work was much pleasant - having code in better shape with a safety net (test suite), up-to-date dependencies, clear CI &amp; CD pipelines is without a doubt more pleasant. I learned that with persistance and consistency you can change anything really.</p>
]]></description><content:encoded><![CDATA[<h3 id="fight-massive-technical-debt-on-all-fronts">Fight massive technical debt on all fronts</h3>
<p>Once I took over a project that was in a <em>really</em> bad shape. And it was not a joke - starting from useless tests that tested implementation itself, ridiculously complex codebase (everything interconnected with everything), and finishing with fragmented infrastructure (outdated Linode servers + stuff randomly deployed to Heroku).</p>
<p>Who would even pick up a project like this you ask? Probably not somebody sane. I realized it&rsquo;s often a matter of mindset. I treated that bulk of legacy code as a challenge and it was a very interesting learning experience. It&rsquo;s exciting to see how a living codebase can transform into something more manageable.</p>
<p>Sure, sometimes it was tidious and boring, but I was aware that it will take a while. I wrote while back about <a href="/blog/2020/05/21/delaying-gratification-in-software-development/">delaying gratification</a> and I think the same rule applies when pickup up project in such state - you have to accept that it will take time and you might see actual results awfully late. So moving that code around, adding tests will seems futile - but it will add up eventually. If I recall correctly power of small changes was also mentioned in <a href="https://www.goodreads.com/book/show/40121378-atomic-habits">Atomic habits</a> book (recommended btw!) - there will be a breaking point when those tiny improvements will be eventually noticable.</p>
<p>It took me a year to get it into a shape where we could bring another person on board. And from there on the work was much pleasant - having code in better shape with a safety net (test suite), up-to-date dependencies, clear CI &amp; CD pipelines is without a doubt more pleasant. I learned that with persistance and consistency you can change anything really.</p>
]]></content:encoded></item><item><title>What I have done so far? Part 10</title><link>https://awesomeprogrammer.com/blog/2021/07/10/what-i-have-done-so-far-part-10/</link><pubDate>Sat, 10 Jul 2021 10:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/07/10/what-i-have-done-so-far-part-10/</guid><description><![CDATA[<h3 id="empower-the-team-by-providing-tooling">Empower the team by providing tooling</h3>
<p>I wanted to provide some tooling for the team and at the same time, I didn&rsquo;t want to burden our devops person (yup, a single one, talk about efficiency and bus factor, right?) too much. After some research I discovered <a href="https://rancher.com/">Rancher</a> - and oh boy it was a crazy good call to use it!</p>
<p>We used it for hosting our <a href="https://sentry.io">Sentry</a> instance, we spin up <a href="https://grafana.com/">Grafana</a> and pushed a lot of data into <a href="https://www.influxdata.com/products/influxdb/">InfluxDB</a> to create some better observability around our apps, we spin up our CI instance using <a href="https://www.drone.io/">Drone</a> - and even used some gaming-dedicated boxes from hosting provider to speed up our test suites (as we could control the hardware underneath). Everything was configured via Ansible (server nodes) and rancher compose yaml files (applications stack).</p>
<p>We even had <a href="https://traefik.io/">traefik</a> so developers could spin up some APIs of their choice simply by adding a new stack there - but the latter wasn&rsquo;t that popular back in my days - having the ability to do so was IMO nice tho.</p>
<p>I think having a unified, easily accessible stack of basic tooling was a good call - every developer could access those tools and set up a configuration for their new app(s) as needed without too much of a hassle. Suddenly catching errors, adding CI &amp; CD deployment pipeline, and pushing some data so you can plot some graphs in Grafana was rather effortless - as you could sign in to most of those tools via Github we removed that traction. And having frictionless experience is even more important nowadays - in remote-first IT world.</p>
]]></description><content:encoded><![CDATA[<h3 id="empower-the-team-by-providing-tooling">Empower the team by providing tooling</h3>
<p>I wanted to provide some tooling for the team and at the same time, I didn&rsquo;t want to burden our devops person (yup, a single one, talk about efficiency and bus factor, right?) too much. After some research I discovered <a href="https://rancher.com/">Rancher</a> - and oh boy it was a crazy good call to use it!</p>
<p>We used it for hosting our <a href="https://sentry.io">Sentry</a> instance, we spin up <a href="https://grafana.com/">Grafana</a> and pushed a lot of data into <a href="https://www.influxdata.com/products/influxdb/">InfluxDB</a> to create some better observability around our apps, we spin up our CI instance using <a href="https://www.drone.io/">Drone</a> - and even used some gaming-dedicated boxes from hosting provider to speed up our test suites (as we could control the hardware underneath). Everything was configured via Ansible (server nodes) and rancher compose yaml files (applications stack).</p>
<p>We even had <a href="https://traefik.io/">traefik</a> so developers could spin up some APIs of their choice simply by adding a new stack there - but the latter wasn&rsquo;t that popular back in my days - having the ability to do so was IMO nice tho.</p>
<p>I think having a unified, easily accessible stack of basic tooling was a good call - every developer could access those tools and set up a configuration for their new app(s) as needed without too much of a hassle. Suddenly catching errors, adding CI &amp; CD deployment pipeline, and pushing some data so you can plot some graphs in Grafana was rather effortless - as you could sign in to most of those tools via Github we removed that traction. And having frictionless experience is even more important nowadays - in remote-first IT world.</p>
]]></content:encoded></item><item><title>What I have done so far? Part 9</title><link>https://awesomeprogrammer.com/blog/2021/07/09/what-i-have-done-so-far-part-9/</link><pubDate>Fri, 09 Jul 2021 12:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/07/09/what-i-have-done-so-far-part-9/</guid><description><![CDATA[<h3 id="account-importer-after-the-merger---hacks-on-hacks-with-postgres_fwd">Account Importer after the merger - hacks on hacks with postgres_fwd</h3>
<p>Once <a href="/blog/2021/07/08/what-i-have-done-so-for-part-8/">the merger</a> was done we immediately had a business need to start moving accounts from our old product to the new one.</p>
<p>As most of the data structure was rather similar I have decided to <em>absolutely</em> abuse <code>postgres_fwd</code>. The <code>postgres_fdw</code> module basically allows you to define remote servers and access them in your queries transparently.</p>
<p>So you can have <code>server_1</code> (localhost), <code>server_2</code> (remote), and <code>server_3</code> (other remote) and construct an insert query by selecting a bunch of stuff from <code>server_2</code> or/and <code>server_3</code> - and even do that in a single transaction - which is crazy powerful.</p>
<p>Leveraging that and a low memory footprint of <code>sequel</code> gem we were able to create a migration app that managed to move the account of any size without too much of a problem. Most of the heavy lifting was done directly on the database level and we simply were doing some simple mappings / small AWS S3 migrations in ruby.</p>
<p>The biggest downside of that is that such tool is really hard to test - as you do most of the stuff in the database you have a lot of dependencies straight away. Think about existing schemas on remote servers - now you have to bring all that complexity and structure to your tests as well. But as a tool used once per account in the system I suppose it was a pretty good bargain.</p>
<p>I think it was one of most out of the box approach I have ever come up with that paid off. Using Postgres capabilities to transparently move data between different servers was kinda a lucky shot - I did not run any benchmarks beforehand (time pressure) and just trusted that people behind Postgres are way smarter than me and they know what they are doing (and they do).</p>
]]></description><content:encoded><![CDATA[<h3 id="account-importer-after-the-merger---hacks-on-hacks-with-postgres_fwd">Account Importer after the merger - hacks on hacks with postgres_fwd</h3>
<p>Once <a href="/blog/2021/07/08/what-i-have-done-so-for-part-8/">the merger</a> was done we immediately had a business need to start moving accounts from our old product to the new one.</p>
<p>As most of the data structure was rather similar I have decided to <em>absolutely</em> abuse <code>postgres_fwd</code>. The <code>postgres_fdw</code> module basically allows you to define remote servers and access them in your queries transparently.</p>
<p>So you can have <code>server_1</code> (localhost), <code>server_2</code> (remote), and <code>server_3</code> (other remote) and construct an insert query by selecting a bunch of stuff from <code>server_2</code> or/and <code>server_3</code> - and even do that in a single transaction - which is crazy powerful.</p>
<p>Leveraging that and a low memory footprint of <code>sequel</code> gem we were able to create a migration app that managed to move the account of any size without too much of a problem. Most of the heavy lifting was done directly on the database level and we simply were doing some simple mappings / small AWS S3 migrations in ruby.</p>
<p>The biggest downside of that is that such tool is really hard to test - as you do most of the stuff in the database you have a lot of dependencies straight away. Think about existing schemas on remote servers - now you have to bring all that complexity and structure to your tests as well. But as a tool used once per account in the system I suppose it was a pretty good bargain.</p>
<p>I think it was one of most out of the box approach I have ever come up with that paid off. Using Postgres capabilities to transparently move data between different servers was kinda a lucky shot - I did not run any benchmarks beforehand (time pressure) and just trusted that people behind Postgres are way smarter than me and they know what they are doing (and they do).</p>
]]></content:encoded></item><item><title>What I have done so far? Part 8</title><link>https://awesomeprogrammer.com/blog/2021/07/08/what-i-have-done-so-far-part-8/</link><pubDate>Thu, 08 Jul 2021 14:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/07/08/what-i-have-done-so-far-part-8/</guid><description><![CDATA[<h3 id="company-merger---impossible-3-months-rewrite">Company merger - impossible 3-months rewrite</h3>
<p>It was a fun time, our company acquired two other companies and we were told that we have 3 months to integrate all the products into a single, unified platform. So you would sign in and could use one, two, or three products in any configuration seamlessly. We estimated the effort for around 1 year of work and we got 3 months to deliver it. Short version - we managed to do so. How we did it is a sum of many different things - mostly having very engaged members within our team. This is how it went down:</p>
<ul>
<li>
<p>1-2 people doing the groundwork - to cut down the communication cost up two people were responsible for preparing the ground for other teams. By ground, I mean building the core structure of the application and/or modifying existing application so it would communicate over API with other app(s) and whatnot</p>
</li>
<li>
<p>after groundwork was done different teams were taking over the work - we defined scope for around 2 months with a ~1-month buffer. The scope was cut down to the bare minimum but it was really tight. The upside here - it was pretty clearly defined so not like a ton of questions appeared during the development.</p>
</li>
<li>
<p>we outsourced the landing part to another company - the outcome wasn&rsquo;t great, but welp - we had <em>something</em> online (later maintenance was a true PITA, but that&rsquo;s a different story about outsourcing stuff)</p>
</li>
</ul>
<p>I think the important part here was that I and the technical cofounder knew the app very well and we were able to spot potential roadblocks (at least most of them) and have a rather clear vision of how the whole thing would work from top to bottom and we were able to document that vision clearly as JIRA tasks.</p>
]]></description><content:encoded><![CDATA[<h3 id="company-merger---impossible-3-months-rewrite">Company merger - impossible 3-months rewrite</h3>
<p>It was a fun time, our company acquired two other companies and we were told that we have 3 months to integrate all the products into a single, unified platform. So you would sign in and could use one, two, or three products in any configuration seamlessly. We estimated the effort for around 1 year of work and we got 3 months to deliver it. Short version - we managed to do so. How we did it is a sum of many different things - mostly having very engaged members within our team. This is how it went down:</p>
<ul>
<li>
<p>1-2 people doing the groundwork - to cut down the communication cost up two people were responsible for preparing the ground for other teams. By ground, I mean building the core structure of the application and/or modifying existing application so it would communicate over API with other app(s) and whatnot</p>
</li>
<li>
<p>after groundwork was done different teams were taking over the work - we defined scope for around 2 months with a ~1-month buffer. The scope was cut down to the bare minimum but it was really tight. The upside here - it was pretty clearly defined so not like a ton of questions appeared during the development.</p>
</li>
<li>
<p>we outsourced the landing part to another company - the outcome wasn&rsquo;t great, but welp - we had <em>something</em> online (later maintenance was a true PITA, but that&rsquo;s a different story about outsourcing stuff)</p>
</li>
</ul>
<p>I think the important part here was that I and the technical cofounder knew the app very well and we were able to spot potential roadblocks (at least most of them) and have a rather clear vision of how the whole thing would work from top to bottom and we were able to document that vision clearly as JIRA tasks.</p>
]]></content:encoded></item><item><title>What I have done so far? Part 7</title><link>https://awesomeprogrammer.com/blog/2021/07/06/what-i-have-done-so-far-part-7/</link><pubDate>Tue, 06 Jul 2021 12:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/07/06/what-i-have-done-so-far-part-7/</guid><description><![CDATA[<h3 id="introduce-go-that-had-great-adaptation">Introduce Go (that had great adaptation)</h3>
<p>We got to a point where we had to start deploying some apps on low-end machines and ruby stopped being the right tool for this problem. High memory footprint, tricky to track memory leaks pushed us to the corner.</p>
<p>Once we were in that corner I started looking into compiled languages that could be potentially easily adapted at our company. Go seems really attractive in that department - really simple syntax, solid tooling, ridiculously simple deployment (single binary) and it was gaining more and more attention (for example it had great <code>netlink</code> library that we could just effortlessly consume).</p>
<p>I drafted the first proof of concept of the app and we didn&rsquo;t have to observe results for long - really good CPU usage, very steady memory consumption - everything looked super promising!</p>
<p>Later on, the whole team took over Go as their primary programming language and they created more and more small binaries and APIs for different purposes while still using Ruby for most problems.</p>
<p>I still tent do use Go from time to time - especially almost free threading (go routines) is really useful - combine that with multi-stage docker compilation you can build things in almost no time.</p>
]]></description><content:encoded><![CDATA[<h3 id="introduce-go-that-had-great-adaptation">Introduce Go (that had great adaptation)</h3>
<p>We got to a point where we had to start deploying some apps on low-end machines and ruby stopped being the right tool for this problem. High memory footprint, tricky to track memory leaks pushed us to the corner.</p>
<p>Once we were in that corner I started looking into compiled languages that could be potentially easily adapted at our company. Go seems really attractive in that department - really simple syntax, solid tooling, ridiculously simple deployment (single binary) and it was gaining more and more attention (for example it had great <code>netlink</code> library that we could just effortlessly consume).</p>
<p>I drafted the first proof of concept of the app and we didn&rsquo;t have to observe results for long - really good CPU usage, very steady memory consumption - everything looked super promising!</p>
<p>Later on, the whole team took over Go as their primary programming language and they created more and more small binaries and APIs for different purposes while still using Ruby for most problems.</p>
<p>I still tent do use Go from time to time - especially almost free threading (go routines) is really useful - combine that with multi-stage docker compilation you can build things in almost no time.</p>
]]></content:encoded></item><item><title>What I have done so far? Part 6</title><link>https://awesomeprogrammer.com/blog/2021/06/28/what-i-have-done-so-far-part-6/</link><pubDate>Mon, 28 Jun 2021 10:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/06/28/what-i-have-done-so-far-part-6/</guid><description><![CDATA[<h3 id="weird-microservices-redis-setup-that-worked-nicely">Weird Microservices Redis setup that worked nicely</h3>
<p>It was a follow-up on that resque/sidekiq-based application - it was growing and growing, along with the business logic complexity. In 2016 I suggested splitting it into a bunch of smaller ruby-sidekiq apps that would communicate via Redis pipeline - after all, we already used Redis for communication and were relying on sidekiq heavily - so why reinvent the wheel?</p>
<p>We ended up with a bunch of small apps and two internal gems. In the end, this solution brought its challenges to the table - especially when it comes to orchestrating services. We ended up sharing the database (that acted as cache layer) across few services in order not to duplicate work which was less than ideal.</p>
<p>The biggest benefit was the ability to scale services independently (a huge problem in the original solution where we had to do a lot of tricks regarding how to limit the work execution in some cases), we could stop/start different parts of the pipeline individually and capability to simply plug new services with ease - and that was helpful when adding a new feature that had to do some extra processing on existing data.</p>
<p>For a while that bulky application was a holy grail and nobody had a clear idea what to do with it - it was extremely hard to maintain and it turned out that rewrite in this case was a good call. Plus the initial rewrite itself didn&rsquo;t take that long - as the basic idea and the architecture were rather simple on their own we managed to split the work pretty easily.</p>
<p>Re-using existing building blocks, parts of the infrastructure speed up the process as we didn&rsquo;t have to introduce a completely new stack in the company at that point (but more on introducing new tech later on!).</p>
]]></description><content:encoded><![CDATA[<h3 id="weird-microservices-redis-setup-that-worked-nicely">Weird Microservices Redis setup that worked nicely</h3>
<p>It was a follow-up on that resque/sidekiq-based application - it was growing and growing, along with the business logic complexity. In 2016 I suggested splitting it into a bunch of smaller ruby-sidekiq apps that would communicate via Redis pipeline - after all, we already used Redis for communication and were relying on sidekiq heavily - so why reinvent the wheel?</p>
<p>We ended up with a bunch of small apps and two internal gems. In the end, this solution brought its challenges to the table - especially when it comes to orchestrating services. We ended up sharing the database (that acted as cache layer) across few services in order not to duplicate work which was less than ideal.</p>
<p>The biggest benefit was the ability to scale services independently (a huge problem in the original solution where we had to do a lot of tricks regarding how to limit the work execution in some cases), we could stop/start different parts of the pipeline individually and capability to simply plug new services with ease - and that was helpful when adding a new feature that had to do some extra processing on existing data.</p>
<p>For a while that bulky application was a holy grail and nobody had a clear idea what to do with it - it was extremely hard to maintain and it turned out that rewrite in this case was a good call. Plus the initial rewrite itself didn&rsquo;t take that long - as the basic idea and the architecture were rather simple on their own we managed to split the work pretty easily.</p>
<p>Re-using existing building blocks, parts of the infrastructure speed up the process as we didn&rsquo;t have to introduce a completely new stack in the company at that point (but more on introducing new tech later on!).</p>
]]></content:encoded></item><item><title>What I have done so far? Part 5</title><link>https://awesomeprogrammer.com/blog/2021/06/26/what-i-have-done-so-far-part-5/</link><pubDate>Sat, 26 Jun 2021 14:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/06/26/what-i-have-done-so-far-part-5/</guid><description><![CDATA[<h3 id="optimize-database-for-fun--profit">Optimize database for fun &amp; profit</h3>
<p>We got to a point where we ran out of <code>int32</code> range on our biggest table - remember when Basecamp years later made a big blog post after the same issue caused some downtime for them? We were overflowing <code>int32</code> before it was cool!</p>
<p>Once you get to that point you have to think if you&rsquo;re using that <code>datetime</code> field and if maybe <code>date</code> won&rsquo;t do the job here (4 bytes less for each row!) or maybe is that <code>smallint</code> is enough there? Or do you even need that primary <code>int64</code> key?! It is funny what you can notice when you stop for a while and look at your data from a perspective. Sometimes it&rsquo;s hard to leave your code-database bubble - especially when you are in that bubble for a while.</p>
<p>Once we kept attention to field types and dropped the primary key (as we were relying on the unique compound key anyway) I think we cut the table size almost by half (and it was something around 500-600GB at that time).</p>
<p>Fun fact: once you run out of disk space Postgres <em>usually</em> reverts the transaction/whatever you are doing and releases the disk space back to the system. But sometimes it can just crash and exits - I had a small heart attack once that happened 😅, but it seems re-spinning the process was enough to bring it back online.</p>
<p>Oh yeah, running on bare-metal servers on OVH&rsquo;s vRack to keep everything cost-efficient as possible was fun! Thankfully our whole team was backed by a truly experienced devops/admin person!</p>
<p>Now when I see that company throws like $7k/mo at a single Heroku Postgres instance (that holds like less than 100GB of data) it just <em>hurts</em>. Vendor lock-in is for sure a thing and it seems current cloud platforms like GCP or AWS that were supposed to offload the work from the <em>developers</em> now require whole teams of <em>devops</em> to operate and maintain. Oh, the irony.</p>
<p>Infrastructure-wise I think you can achieve very much using well-tested and boring solutions. I don&rsquo;t think most companies need to <em>scale</em> that much frankly speaking - surely if you&rsquo;re a big player and handle tens of millions of requests per day you deal with a completely different set of problems, but again - I don&rsquo;t think most aspiring startups will ever get to that point and they spent too much preparing for something that is simply not necessary.</p>
]]></description><content:encoded><![CDATA[<h3 id="optimize-database-for-fun--profit">Optimize database for fun &amp; profit</h3>
<p>We got to a point where we ran out of <code>int32</code> range on our biggest table - remember when Basecamp years later made a big blog post after the same issue caused some downtime for them? We were overflowing <code>int32</code> before it was cool!</p>
<p>Once you get to that point you have to think if you&rsquo;re using that <code>datetime</code> field and if maybe <code>date</code> won&rsquo;t do the job here (4 bytes less for each row!) or maybe is that <code>smallint</code> is enough there? Or do you even need that primary <code>int64</code> key?! It is funny what you can notice when you stop for a while and look at your data from a perspective. Sometimes it&rsquo;s hard to leave your code-database bubble - especially when you are in that bubble for a while.</p>
<p>Once we kept attention to field types and dropped the primary key (as we were relying on the unique compound key anyway) I think we cut the table size almost by half (and it was something around 500-600GB at that time).</p>
<p>Fun fact: once you run out of disk space Postgres <em>usually</em> reverts the transaction/whatever you are doing and releases the disk space back to the system. But sometimes it can just crash and exits - I had a small heart attack once that happened 😅, but it seems re-spinning the process was enough to bring it back online.</p>
<p>Oh yeah, running on bare-metal servers on OVH&rsquo;s vRack to keep everything cost-efficient as possible was fun! Thankfully our whole team was backed by a truly experienced devops/admin person!</p>
<p>Now when I see that company throws like $7k/mo at a single Heroku Postgres instance (that holds like less than 100GB of data) it just <em>hurts</em>. Vendor lock-in is for sure a thing and it seems current cloud platforms like GCP or AWS that were supposed to offload the work from the <em>developers</em> now require whole teams of <em>devops</em> to operate and maintain. Oh, the irony.</p>
<p>Infrastructure-wise I think you can achieve very much using well-tested and boring solutions. I don&rsquo;t think most companies need to <em>scale</em> that much frankly speaking - surely if you&rsquo;re a big player and handle tens of millions of requests per day you deal with a completely different set of problems, but again - I don&rsquo;t think most aspiring startups will ever get to that point and they spent too much preparing for something that is simply not necessary.</p>
]]></content:encoded></item><item><title>What I have done so far? Part 4</title><link>https://awesomeprogrammer.com/blog/2021/06/25/what-i-have-done-so-far-part-4/</link><pubDate>Fri, 25 Jun 2021 12:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/06/25/what-i-have-done-so-far-part-4/</guid><description><![CDATA[<h3 id="disaster-recovery---8h-rewrite-from-mongodb-to-postgres">Disaster recovery - ~8h rewrite from MongoDB to Postgres</h3>
<p>I recall once we had a huge problem with our MongoDB cluster - without going into too many details - it was like a 3TB+ beast that we have used mostly as some sort of cache layer. Unfortunately one of the replica servers decided to die on us - plugging a new server into the cluster as you can imagine slowed down everything to the point everything almost stopped - new server tried to pull the data from the main server that was under heavy load already and we ended up in a weird spot.</p>
<p>I have decided to draft a new cache layer - this time based on Postgres - it was not the best decision to use Mongo for this purpose anyway - it turned out it wasn&rsquo;t the best tool for the problem.</p>
<p>In the end, we have abandoned the existing cluster and allowed for our background workers to start filling fresh Postgres instance with new data (we were in <em>very</em> lucky position we could do that in the first place, otherwise things could get ridiculously complex!) - and we used Postgres since then.</p>
<p>Much later - as the new Postgres database got bigger and bigger - we have decided to follow the hot/cold storage approach and split that into two separate instances. But that&rsquo;s a different story done by another team.</p>
<p>Again I think the most difficult part was making a call - it was a high urgency and high-risk decision, but in the end it everything turned out nicely.</p>
]]></description><content:encoded><![CDATA[<h3 id="disaster-recovery---8h-rewrite-from-mongodb-to-postgres">Disaster recovery - ~8h rewrite from MongoDB to Postgres</h3>
<p>I recall once we had a huge problem with our MongoDB cluster - without going into too many details - it was like a 3TB+ beast that we have used mostly as some sort of cache layer. Unfortunately one of the replica servers decided to die on us - plugging a new server into the cluster as you can imagine slowed down everything to the point everything almost stopped - new server tried to pull the data from the main server that was under heavy load already and we ended up in a weird spot.</p>
<p>I have decided to draft a new cache layer - this time based on Postgres - it was not the best decision to use Mongo for this purpose anyway - it turned out it wasn&rsquo;t the best tool for the problem.</p>
<p>In the end, we have abandoned the existing cluster and allowed for our background workers to start filling fresh Postgres instance with new data (we were in <em>very</em> lucky position we could do that in the first place, otherwise things could get ridiculously complex!) - and we used Postgres since then.</p>
<p>Much later - as the new Postgres database got bigger and bigger - we have decided to follow the hot/cold storage approach and split that into two separate instances. But that&rsquo;s a different story done by another team.</p>
<p>Again I think the most difficult part was making a call - it was a high urgency and high-risk decision, but in the end it everything turned out nicely.</p>
]]></content:encoded></item><item><title>What I have done so far? Part 3</title><link>https://awesomeprogrammer.com/blog/2021/06/24/what-i-have-done-so-far-part-3/</link><pubDate>Thu, 24 Jun 2021 15:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/06/24/what-i-have-done-so-far-part-3/</guid><description><![CDATA[<h3 id="start-hiring-from-the-ground-up-after-moving-to-another-city">Start hiring from the ground up after moving to another city</h3>
<p>I remember my first hr-talk - it was so awkward. Nobody prepared me for it. Nobody walked me through anything, gave me any tips, I was just left alone to ask some questions for a person who was applying for a senior role. It went so bad and I was so stressed! I cherished that memory in order not to make the same mistake with my peers :).</p>
<p>Fast forward and I just moved to a new city (my 4th one!) where I was supposed to start an R&amp;D office. This was definitely something out of my comfort zone but I was very lucky to find some great people - which most of them <a href="/blog/2018/02/26/5-short-reflections-from-5-years/">I had the luck to work with</a> for quite some time.</p>
<p>To be honest, I&rsquo;m not a fan of hiring - in a way, it&rsquo;s not my favorite thing in the world to do - properly preparing for an interview takes so much time and focus, the market nowadays is even crazier than it was a few years ago and finding right people is so hard that it can be frustrating.</p>
<p>Even if you try to remove all the bias and have really structured process (so you can have a point of reference) it can still feel blurry sometimes. After all, we&rsquo;re all just humans and some things are simply very difficult to quantize.</p>
<p>We try to put people in boxes called &ldquo;senior&rdquo;, &ldquo;lead material&rdquo;, &ldquo;strong junior with a lot of potentials&rdquo;, but even with long recruitment processes, references checks and so on we can fail miserably.</p>
<p>I&rsquo;m very grateful that I had soooo much luck finding so many great people who are crushing it at companies like Gitlab, Typeform, or are building digital currencies for countries in the Caribbean regions (<em>hi Bartosz</em>). I hope that someday I will find joy in hiring again because building teams <em>is</em> fun, it&rsquo;s challenging, and most of all, in my opinion - is very blurry. People are much more complex than code and debugging people can be difficult ;).</p>
]]></description><content:encoded><![CDATA[<h3 id="start-hiring-from-the-ground-up-after-moving-to-another-city">Start hiring from the ground up after moving to another city</h3>
<p>I remember my first hr-talk - it was so awkward. Nobody prepared me for it. Nobody walked me through anything, gave me any tips, I was just left alone to ask some questions for a person who was applying for a senior role. It went so bad and I was so stressed! I cherished that memory in order not to make the same mistake with my peers :).</p>
<p>Fast forward and I just moved to a new city (my 4th one!) where I was supposed to start an R&amp;D office. This was definitely something out of my comfort zone but I was very lucky to find some great people - which most of them <a href="/blog/2018/02/26/5-short-reflections-from-5-years/">I had the luck to work with</a> for quite some time.</p>
<p>To be honest, I&rsquo;m not a fan of hiring - in a way, it&rsquo;s not my favorite thing in the world to do - properly preparing for an interview takes so much time and focus, the market nowadays is even crazier than it was a few years ago and finding right people is so hard that it can be frustrating.</p>
<p>Even if you try to remove all the bias and have really structured process (so you can have a point of reference) it can still feel blurry sometimes. After all, we&rsquo;re all just humans and some things are simply very difficult to quantize.</p>
<p>We try to put people in boxes called &ldquo;senior&rdquo;, &ldquo;lead material&rdquo;, &ldquo;strong junior with a lot of potentials&rdquo;, but even with long recruitment processes, references checks and so on we can fail miserably.</p>
<p>I&rsquo;m very grateful that I had soooo much luck finding so many great people who are crushing it at companies like Gitlab, Typeform, or are building digital currencies for countries in the Caribbean regions (<em>hi Bartosz</em>). I hope that someday I will find joy in hiring again because building teams <em>is</em> fun, it&rsquo;s challenging, and most of all, in my opinion - is very blurry. People are much more complex than code and debugging people can be difficult ;).</p>
]]></content:encoded></item><item><title>What I have done so far? Part 2</title><link>https://awesomeprogrammer.com/blog/2021/06/23/what-i-have-done-so-far-part-2/</link><pubDate>Wed, 23 Jun 2021 13:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/06/23/what-i-have-done-so-far-part-2/</guid><description><![CDATA[<h3 id="introduce-react-the-wrong-way">Introduce React (the wrong way)</h3>
<p>We <em>(at the company)</em> were at the weird stage where we have some jQuery-glued libraries that were our &lsquo;framework&rsquo; driving frontend. I think at that time it was often the case and usually, you made the call to jump into the Angular or Ember.js bandwagon. React was new on the market (I think it was around 2015) and was kinda&hellip; awkward? It was still unclear how to structure some more complex interfaces, where to handle the http layer, how to deal with the complex state, and so on.</p>
<p>We had to do <em>something</em> nevertheless, the UI was getting ridiculously complex. I did some research on what we could potentially use and we simply tried to build <em>anything</em>. So we rebuild a simple section in our app that had <em>some</em> complexity (not too much, not too little) using both Angular and React.</p>
<p>The requirement was to use something that didn&rsquo;t bring all of its ecosystem with it and that would allow us to rewrite the application section-by-section and React felt really good.</p>
<p>We were quite lucky with the call. Obviously, the first version was thrown out of the window - along with the <a href="https://github.com/reflux/refluxjs">refluxjs</a> that I added, I had no idea what I was doing - but at least it was the first step and future adaptation was quite smooth overall (but then again - I was lucky to work with very smart and resourceful people).</p>
<p>I had a chance to use react later down the road again - and as it usually is in the javascript ecosystem - things tend to change quite often.</p>
<p>Now that I reflect on it the most difficult part was making the call - such technical decisions can be very difficult. <em>What if we chose the wrong tool and we shot ourselves in the foot?</em> Doing MVP first and iteration was a good decision - if you don&rsquo;t have to rewrite your whole system but can adapt technology on the go - so you can control the process with the tech and not the other ways around - means you probably should be a-okey.</p>
]]></description><content:encoded><![CDATA[<h3 id="introduce-react-the-wrong-way">Introduce React (the wrong way)</h3>
<p>We <em>(at the company)</em> were at the weird stage where we have some jQuery-glued libraries that were our &lsquo;framework&rsquo; driving frontend. I think at that time it was often the case and usually, you made the call to jump into the Angular or Ember.js bandwagon. React was new on the market (I think it was around 2015) and was kinda&hellip; awkward? It was still unclear how to structure some more complex interfaces, where to handle the http layer, how to deal with the complex state, and so on.</p>
<p>We had to do <em>something</em> nevertheless, the UI was getting ridiculously complex. I did some research on what we could potentially use and we simply tried to build <em>anything</em>. So we rebuild a simple section in our app that had <em>some</em> complexity (not too much, not too little) using both Angular and React.</p>
<p>The requirement was to use something that didn&rsquo;t bring all of its ecosystem with it and that would allow us to rewrite the application section-by-section and React felt really good.</p>
<p>We were quite lucky with the call. Obviously, the first version was thrown out of the window - along with the <a href="https://github.com/reflux/refluxjs">refluxjs</a> that I added, I had no idea what I was doing - but at least it was the first step and future adaptation was quite smooth overall (but then again - I was lucky to work with very smart and resourceful people).</p>
<p>I had a chance to use react later down the road again - and as it usually is in the javascript ecosystem - things tend to change quite often.</p>
<p>Now that I reflect on it the most difficult part was making the call - such technical decisions can be very difficult. <em>What if we chose the wrong tool and we shot ourselves in the foot?</em> Doing MVP first and iteration was a good decision - if you don&rsquo;t have to rewrite your whole system but can adapt technology on the go - so you can control the process with the tech and not the other ways around - means you probably should be a-okey.</p>
]]></content:encoded></item><item><title>What I have done so far? Part 1</title><link>https://awesomeprogrammer.com/blog/2021/06/22/what-i-have-done-so-far-part-1/</link><pubDate>Tue, 22 Jun 2021 18:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/06/22/what-i-have-done-so-far-part-1/</guid><description><![CDATA[<p>I&rsquo;m not the kind of person who keeps track of my work achievements, biggest challenges, and so forth.</p>
<p>I have a very bad memory (for that and in general :P) so sometimes is hard for me to easily reach into my mind and construct a story when being asked by a fellow dev or recruiter <em>so, what you have been doing so far?</em></p>
<p>Thus I have decided to collect some, um, personal stories? A memoir so to speak. This is part one of hopefully more - so I can reflect on those achievements, small victories, and learnings later down the road.</p>
<hr>
<h3 id="migrate-resque-to-sidekiq---200-processes-down-to-20-for-fun-and-profit">Migrate resque to sidekiq -&gt; 200 processes down to 20 for fun and profit</h3>
<p>I was (quite) a young and eager developer (with a lot of freedom, which is quite unusual nowadays I suppose) and I spent some overtime to migrate a part of the platform - today you would call it a service or a microservice - that was based on resque to sidekiq. It was a ruby app that did some business-related work and communicated via redis and http with another part of the platform.</p>
<p>Resque as you might know is based on processes - so one process executes one job. Now imagine how long it takes to restart over 200 processes during deployment. The answer is <em>too long</em>.</p>
<p>As those processes were doing mostly I/O work it would make sense to rewrite it to Sidekiq - even when using MRI. Once we started using sidekiq we have never looked back - I even made an open-source contribution to sidekiq back in those days only to a few years later do chip-in some contributions to its <a href="https://github.com/mperham/sidekiq.cr">Crystal version</a>.</p>
<p>It was interesting learning experience in terms of what to look for when working in concurrent environment, what is actually shared between threads and how you can mix concurrency with parallelism.</p>
<p>Nowadays sidekiq is my go-to tool in Ruby world, backend by redis (my other go-to service that can solve a ton of different problems) is just so easy to plug in and manage. I suppose I will love sidekiq forever. Thank you Mike Perham!</p>]]></description><content:encoded><![CDATA[<p>I&rsquo;m not the kind of person who keeps track of my work achievements, biggest challenges, and so forth.</p>
<p>I have a very bad memory (for that and in general :P) so sometimes is hard for me to easily reach into my mind and construct a story when being asked by a fellow dev or recruiter <em>so, what you have been doing so far?</em></p>
<p>Thus I have decided to collect some, um, personal stories? A memoir so to speak. This is part one of hopefully more - so I can reflect on those achievements, small victories, and learnings later down the road.</p>
<hr>
<h3 id="migrate-resque-to-sidekiq---200-processes-down-to-20-for-fun-and-profit">Migrate resque to sidekiq -&gt; 200 processes down to 20 for fun and profit</h3>
<p>I was (quite) a young and eager developer (with a lot of freedom, which is quite unusual nowadays I suppose) and I spent some overtime to migrate a part of the platform - today you would call it a service or a microservice - that was based on resque to sidekiq. It was a ruby app that did some business-related work and communicated via redis and http with another part of the platform.</p>
<p>Resque as you might know is based on processes - so one process executes one job. Now imagine how long it takes to restart over 200 processes during deployment. The answer is <em>too long</em>.</p>
<p>As those processes were doing mostly I/O work it would make sense to rewrite it to Sidekiq - even when using MRI. Once we started using sidekiq we have never looked back - I even made an open-source contribution to sidekiq back in those days only to a few years later do chip-in some contributions to its <a href="https://github.com/mperham/sidekiq.cr">Crystal version</a>.</p>
<p>It was interesting learning experience in terms of what to look for when working in concurrent environment, what is actually shared between threads and how you can mix concurrency with parallelism.</p>
<p>Nowadays sidekiq is my go-to tool in Ruby world, backend by redis (my other go-to service that can solve a ton of different problems) is just so easy to plug in and manage. I suppose I will love sidekiq forever. Thank you Mike Perham!</p>]]></content:encoded></item><item><title>Deploy docker container with http webhook the easy way</title><link>https://awesomeprogrammer.com/blog/2021/04/18/deploy-docker-container-with-http-webhook-the-easy-way/</link><pubDate>Sun, 18 Apr 2021 19:00:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2021/04/18/deploy-docker-container-with-http-webhook-the-easy-way/</guid><description><![CDATA[<p>Recently I have been researching a way to automate the process of deploying some of my hobby projects. Frankly speaking nowadays I just pack everything in a docker container and call it a day, but as I rather very low-end servers (💸) I didn&rsquo;t want to bring Swarm/Kubernetes overhead to the table (I had some pretty nice experience with Swarm a few years ago, but it seems k8s is eating the industry if you like it or not 🤷‍♀️).</p>
<p>There is <a href="https://dokku.com">dokku</a> - but it runs on a single server only (which doesn&rsquo;t fit into my existing setup where I have separated applications from database services). There is <a href="https://www.nomadproject.io">Nomad</a> by well-known Hashi Corp which seemed like a good fit - but at the same time, I wasn&rsquo;t into setting everything up from scratch. Then I stumbled upon <a href="https://github.com/adnanh/webhook">webhook</a> and with a help of Ansible it seemed like a right fit for my simple needs.</p>
<h3 id="current-approach">Current approach</h3>
<p>The way it worked so far is that I have services, a private docker registry and I simply build the images, push them to my private registry and redeploy containers - via ansible or other means, but obviously, that requires ssh access to the server. Once I started doing some pet projects with my partner I needed to automate it a little bit more.</p>
<h3 id="what-im-trying-to-solve-here">What I&rsquo;m trying to solve here</h3>
<p>Automate the way of building docker image in a repository - stopping container on the server, pulling new image, and spinning container with the updated image. No fancy orchestration is required, few seconds on downtime is acceptable.</p>
<h3 id="the-brand-new-setup">The brand new setup</h3>
<ul>
<li>I will utilize <a href="https://docs.github.com/en/actions">github actions</a> and their new shiny <a href="https://github.com/features/packages">github packages</a> so adding <em>workflow</em> file to the repository and a simple script on the server for deployment will make the problem go away</li>
<li>I&rsquo;m gonna deploy <em>webhook</em> in a <a href="https://github.com/almir/docker-webhook">docker container</a> as I&rsquo;m already using traefik with docker provider enabled</li>
</ul>
<h4 id="step-1">Step 1</h4>
<p>Configure <em>webhook</em> so it can take control over docker on the system. Things will get a little bit weird here - as I want to deploy <em>webhook</em> in a docker container and at the same time give it control over a <em>host</em> docker. How to do that? Well, first we&rsquo;re gonna customize the container we&rsquo;re gonna use:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> almir/webhook:2.8.0</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span>  apk --update --upgrade add docker curl bash <span style="color:#f92672">&amp;&amp;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>     rm -rf /var/cache/apk/*<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Then I&rsquo;m gonna mount <code>docker.sock</code> in that container as well as <code>image</code> and <code>overlay2</code> docker directories. The recipe is loosely based <a href="https://github.com/almir/docker-webhook/issues/2#issuecomment-254469814">on this comment</a>. You can find some ansible snippets below.</p>
<h3 id="step-2">Step 2</h3>
<p>Next let&rsquo;s create a github action workflow for one of the my projects:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># .github/workflows/deploy.yml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">deploy</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">push</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">build</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build image</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v2</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/login-action@v1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">registry</span>: <span style="color:#ae81ff">ghcr.io</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">username</span>: <span style="color:#ae81ff">${{ github.repository_owner }}</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">password</span>: <span style="color:#ae81ff">${{ secrets.GITHUB_TOKEN }}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Set up Docker Buildx</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/setup-buildx-action@v1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># caching docker layer ommited for brevity</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build and push</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/build-push-action@v2</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">context</span>: <span style="color:#ae81ff">.</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">push</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">tags</span>: <span style="color:#ae81ff">ghcr.io/${{ github.repository }}:latest</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">deploy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">needs</span>: [ <span style="color:#ae81ff">build ]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># One of the way of passing envs to the http endpoint</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># We&#39;re gonna pass it as base64 encoded string</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># And then allow &#39;webhook&#39; to generate a temporary file</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># which then will be passed down to docker run command</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Generate encoded env file</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">id</span>: <span style="color:#ae81ff">env_file</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          echo &#34;SOME_VARIABLE=123&#34; &gt;&gt; .env
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          echo &#34;SOME_TOKEN=${{ secrets.SOME_TOKEN }}&#34; &gt;&gt; .env
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          echo &#34;::set-output name=encoded::$(cat .env | base64 -w 0)&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          rm .env</span>          
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy docker container webhook</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">joelwmale/webhook-action@fd99bb3b8272237103e349e9bb4d9b0ead9a217c</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">url</span>: <span style="color:#ae81ff">https://yourwebhookdomain.com/hooks/your-deploy-endpoint</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">headers</span>: <span style="color:#e6db74">&#39;{&#34;X-Token&#34;: &#34;${{ secrets.WEBHOOK_TOKEN  }}&#34;}&#39;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">body</span>: &gt;-<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            { &#34;DOCKER_USERNAME&#34;: &#34;${{ github.repository_owner }}&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &#34;DOCKER_REGISTRY&#34;: &#34;ghcr.io&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &#34;DOCKER_PASSWORD&#34;: &#34;${{ secrets.GITHUB_TOKEN }}&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &#34;DOCKER_IMAGE&#34;: &#34;ghcr.io/${{ github.repository }}:latest&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &#34;ENV_FILE&#34;: &#34;${{ steps.env_file.outputs.encoded }}&#34; }</span>            
</span></span></code></pre></div><p>There are quite a lot of moving parts going on here, let&rsquo;s break it down:</p>
<ul>
<li>
<p>first, we&rsquo;re building a docker image using Github&rsquo;s ghcr.io registry - we&rsquo;re gonna use very convenient <a href="https://docs.github.com/en/actions/reference/authentication-in-a-workflow#about-the-github_token-secret">magical github token secret</a> that gives us scoped access to the repository and registry <em>during the execution of the workflow</em></p>
</li>
<li>
<p>then we&rsquo;re gonna prepare env file on the fly and base64 encode it for <em>webhook</em> - it&rsquo;s maybe not the best practice here, but later on, we will be able to use it as <code>--env-file</code> argument when we will re-spin our container</p>
</li>
<li>
<p>webhook should be accessible under <code>yourwebhookdomain.com</code> - we&rsquo;re also using <a href="https://docs.github.com/en/actions/reference/encrypted-secrets">github secrets</a> to set <code>WEBHOOK_TOKEN</code> secret - we will use simple token-based authentication for our deployment</p>
</li>
</ul>
<h3 id="step-3">Step 3</h3>
<p>Time for Webhook container configuration - here is ansible snippet with some comments:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create webhook container</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_container</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">webhook</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># we&#39;re using our customized webhook image - see dockerfile in step 1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">webhook-with-docker</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">started</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#66d9ef">yes</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart_policy</span>: <span style="color:#ae81ff">always</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/home/webhook/:/etc/webhook:ro</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># we&#39;re giving control to docker on the host</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/var/run/docker.sock:/var/run/docker.sock</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># and allow to write files to docker as well - you might not need that part</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/var/lib/docker/image:/var/lib/docker/image</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/var/lib/docker/overlay2:/var/lib/docker/overlay2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># add verbose flag for debugging purposes!</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># - &#39;-verbose&#39;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;-hooks=/etc/webhook/hooks.json&#39;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;-hotreload&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># traefik labels - your configuration may vary</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.enable</span>: <span style="color:#e6db74">&#39;true&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.docker.network</span>: <span style="color:#e6db74">&#39;traefik&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-webhook.rule</span>: <span style="color:#e6db74">&#39;Host(`yourwebhookdomain.com`)&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-webhook.entrypoints</span>: <span style="color:#e6db74">&#39;websecure&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-webhook.tls.certResolver</span>: <span style="color:#e6db74">&#39;letsencrypt&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.services.balancer-webhook.loadbalancer.server.port</span>: <span style="color:#e6db74">&#39;9000&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span></code></pre></div><p>Example webhook - that is accessible under <code>/home/webhook/hooks.json</code> on the host in my case:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>[
</span></span><span style="display:flex;"><span>  {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#e6db74">&#34;your-deploy-endpoint&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;execute-command&#34;</span>: <span style="color:#e6db74">&#34;/etc/webhook/your-deploy-endpoint&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;response-message&#34;</span>: <span style="color:#e6db74">&#34;Restarting container&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;pass-environment-to-command&#34;</span>:
</span></span><span style="display:flex;"><span>    [
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;envname&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_REGISTRY&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;payload&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_REGISTRY&#34;</span>
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;envname&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_USERNAME&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;payload&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_USERNAME&#34;</span>
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;envname&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_PASSWORD&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;payload&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_PASSWORD&#34;</span>
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;envname&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_IMAGE&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;payload&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_IMAGE&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;pass-file-to-command&#34;</span>:
</span></span><span style="display:flex;"><span>    [
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;payload&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;ENV_FILE&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;envname&#34;</span>: <span style="color:#e6db74">&#34;ENV_FILE&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;base64decode&#34;</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;trigger-rule-mismatch-http-response-code&#34;</span>: <span style="color:#ae81ff">411</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;trigger-rule&#34;</span>:
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;match&#34;</span>:
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;value&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;value&#34;</span>: <span style="color:#e6db74">&#34;{{ here goes value of WEBHOOK_TOKEN from Github secrets }}&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;parameter&#34;</span>:
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;header&#34;</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;X-Token&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>So finally how <code>/etc/webhook/your-deploy-endpoint</code> (mounted from <code>/home/webhook</code> on the host) looks like? It&rsquo;s a simple bash script that just log-in into the docker registry, pulls a new image, and re-spins the container - so it does exactly what we were planning to solve here, just in an automated manner.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>set -e
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker login $DOCKER_REGISTRY -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker pull $DOCKER_IMAGE
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker stop my-app <span style="color:#f92672">||</span> true
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker rm my-app <span style="color:#f92672">||</span> true
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker run -d <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name my-app <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --env-file<span style="color:#f92672">=</span>$ENV_FILE <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  $DOCKER_IMAGE
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker logout $DOCKER_REGISTRY
</span></span></code></pre></div><p>Don&rsquo;t worry that <code>$ENV_FILE</code> won&rsquo;t exist upon container manual restart (as it gets removed one webhook does its job) - if you do <code>docker inspect my-app</code> you should see that environment variables from the file were  already loaded and file itself is no longer required.</p>
<h3 id="summary">Summary</h3>
<p>So it might feel a little bit intimidating (and not easy!) at first but once you wrap your head about all the goodies that github provides and how it can fit nicely with <code>webhook</code> you might find should approach suits your needs.</p>
<p>Presented code shouldn&rsquo;t be treated as a production-ready example and more like a walk-through for automating the deployment of your hobby project - there are <em>a lot</em> of things that could potentially go wrong here - as we&rsquo;re happily passing envs to a shell command and spinning some images ;).</p>
<p><em>Happy deploying!</em></p>
]]></description><content:encoded><![CDATA[<p>Recently I have been researching a way to automate the process of deploying some of my hobby projects. Frankly speaking nowadays I just pack everything in a docker container and call it a day, but as I rather very low-end servers (💸) I didn&rsquo;t want to bring Swarm/Kubernetes overhead to the table (I had some pretty nice experience with Swarm a few years ago, but it seems k8s is eating the industry if you like it or not 🤷‍♀️).</p>
<p>There is <a href="https://dokku.com">dokku</a> - but it runs on a single server only (which doesn&rsquo;t fit into my existing setup where I have separated applications from database services). There is <a href="https://www.nomadproject.io">Nomad</a> by well-known Hashi Corp which seemed like a good fit - but at the same time, I wasn&rsquo;t into setting everything up from scratch. Then I stumbled upon <a href="https://github.com/adnanh/webhook">webhook</a> and with a help of Ansible it seemed like a right fit for my simple needs.</p>
<h3 id="current-approach">Current approach</h3>
<p>The way it worked so far is that I have services, a private docker registry and I simply build the images, push them to my private registry and redeploy containers - via ansible or other means, but obviously, that requires ssh access to the server. Once I started doing some pet projects with my partner I needed to automate it a little bit more.</p>
<h3 id="what-im-trying-to-solve-here">What I&rsquo;m trying to solve here</h3>
<p>Automate the way of building docker image in a repository - stopping container on the server, pulling new image, and spinning container with the updated image. No fancy orchestration is required, few seconds on downtime is acceptable.</p>
<h3 id="the-brand-new-setup">The brand new setup</h3>
<ul>
<li>I will utilize <a href="https://docs.github.com/en/actions">github actions</a> and their new shiny <a href="https://github.com/features/packages">github packages</a> so adding <em>workflow</em> file to the repository and a simple script on the server for deployment will make the problem go away</li>
<li>I&rsquo;m gonna deploy <em>webhook</em> in a <a href="https://github.com/almir/docker-webhook">docker container</a> as I&rsquo;m already using traefik with docker provider enabled</li>
</ul>
<h4 id="step-1">Step 1</h4>
<p>Configure <em>webhook</em> so it can take control over docker on the system. Things will get a little bit weird here - as I want to deploy <em>webhook</em> in a docker container and at the same time give it control over a <em>host</em> docker. How to do that? Well, first we&rsquo;re gonna customize the container we&rsquo;re gonna use:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> almir/webhook:2.8.0</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span>  apk --update --upgrade add docker curl bash <span style="color:#f92672">&amp;&amp;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>     rm -rf /var/cache/apk/*<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Then I&rsquo;m gonna mount <code>docker.sock</code> in that container as well as <code>image</code> and <code>overlay2</code> docker directories. The recipe is loosely based <a href="https://github.com/almir/docker-webhook/issues/2#issuecomment-254469814">on this comment</a>. You can find some ansible snippets below.</p>
<h3 id="step-2">Step 2</h3>
<p>Next let&rsquo;s create a github action workflow for one of the my projects:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># .github/workflows/deploy.yml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">deploy</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">push</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">build</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build image</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v2</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/login-action@v1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">registry</span>: <span style="color:#ae81ff">ghcr.io</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">username</span>: <span style="color:#ae81ff">${{ github.repository_owner }}</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">password</span>: <span style="color:#ae81ff">${{ secrets.GITHUB_TOKEN }}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Set up Docker Buildx</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/setup-buildx-action@v1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># caching docker layer ommited for brevity</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build and push</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">docker/build-push-action@v2</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">context</span>: <span style="color:#ae81ff">.</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">push</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">tags</span>: <span style="color:#ae81ff">ghcr.io/${{ github.repository }}:latest</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">deploy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">needs</span>: [ <span style="color:#ae81ff">build ]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># One of the way of passing envs to the http endpoint</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># We&#39;re gonna pass it as base64 encoded string</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># And then allow &#39;webhook&#39; to generate a temporary file</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># which then will be passed down to docker run command</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Generate encoded env file</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">id</span>: <span style="color:#ae81ff">env_file</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          echo &#34;SOME_VARIABLE=123&#34; &gt;&gt; .env
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          echo &#34;SOME_TOKEN=${{ secrets.SOME_TOKEN }}&#34; &gt;&gt; .env
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          echo &#34;::set-output name=encoded::$(cat .env | base64 -w 0)&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          rm .env</span>          
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy docker container webhook</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">joelwmale/webhook-action@fd99bb3b8272237103e349e9bb4d9b0ead9a217c</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">url</span>: <span style="color:#ae81ff">https://yourwebhookdomain.com/hooks/your-deploy-endpoint</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">headers</span>: <span style="color:#e6db74">&#39;{&#34;X-Token&#34;: &#34;${{ secrets.WEBHOOK_TOKEN  }}&#34;}&#39;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">body</span>: &gt;-<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            { &#34;DOCKER_USERNAME&#34;: &#34;${{ github.repository_owner }}&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &#34;DOCKER_REGISTRY&#34;: &#34;ghcr.io&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &#34;DOCKER_PASSWORD&#34;: &#34;${{ secrets.GITHUB_TOKEN }}&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &#34;DOCKER_IMAGE&#34;: &#34;ghcr.io/${{ github.repository }}:latest&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &#34;ENV_FILE&#34;: &#34;${{ steps.env_file.outputs.encoded }}&#34; }</span>            
</span></span></code></pre></div><p>There are quite a lot of moving parts going on here, let&rsquo;s break it down:</p>
<ul>
<li>
<p>first, we&rsquo;re building a docker image using Github&rsquo;s ghcr.io registry - we&rsquo;re gonna use very convenient <a href="https://docs.github.com/en/actions/reference/authentication-in-a-workflow#about-the-github_token-secret">magical github token secret</a> that gives us scoped access to the repository and registry <em>during the execution of the workflow</em></p>
</li>
<li>
<p>then we&rsquo;re gonna prepare env file on the fly and base64 encode it for <em>webhook</em> - it&rsquo;s maybe not the best practice here, but later on, we will be able to use it as <code>--env-file</code> argument when we will re-spin our container</p>
</li>
<li>
<p>webhook should be accessible under <code>yourwebhookdomain.com</code> - we&rsquo;re also using <a href="https://docs.github.com/en/actions/reference/encrypted-secrets">github secrets</a> to set <code>WEBHOOK_TOKEN</code> secret - we will use simple token-based authentication for our deployment</p>
</li>
</ul>
<h3 id="step-3">Step 3</h3>
<p>Time for Webhook container configuration - here is ansible snippet with some comments:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create webhook container</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_container</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">webhook</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># we&#39;re using our customized webhook image - see dockerfile in step 1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">webhook-with-docker</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">started</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#66d9ef">yes</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart_policy</span>: <span style="color:#ae81ff">always</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/home/webhook/:/etc/webhook:ro</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># we&#39;re giving control to docker on the host</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/var/run/docker.sock:/var/run/docker.sock</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># and allow to write files to docker as well - you might not need that part</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/var/lib/docker/image:/var/lib/docker/image</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/var/lib/docker/overlay2:/var/lib/docker/overlay2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># add verbose flag for debugging purposes!</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># - &#39;-verbose&#39;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;-hooks=/etc/webhook/hooks.json&#39;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;-hotreload&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># traefik labels - your configuration may vary</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.enable</span>: <span style="color:#e6db74">&#39;true&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.docker.network</span>: <span style="color:#e6db74">&#39;traefik&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-webhook.rule</span>: <span style="color:#e6db74">&#39;Host(`yourwebhookdomain.com`)&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-webhook.entrypoints</span>: <span style="color:#e6db74">&#39;websecure&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-webhook.tls.certResolver</span>: <span style="color:#e6db74">&#39;letsencrypt&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.services.balancer-webhook.loadbalancer.server.port</span>: <span style="color:#e6db74">&#39;9000&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span></code></pre></div><p>Example webhook - that is accessible under <code>/home/webhook/hooks.json</code> on the host in my case:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>[
</span></span><span style="display:flex;"><span>  {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#e6db74">&#34;your-deploy-endpoint&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;execute-command&#34;</span>: <span style="color:#e6db74">&#34;/etc/webhook/your-deploy-endpoint&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;response-message&#34;</span>: <span style="color:#e6db74">&#34;Restarting container&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;pass-environment-to-command&#34;</span>:
</span></span><span style="display:flex;"><span>    [
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;envname&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_REGISTRY&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;payload&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_REGISTRY&#34;</span>
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;envname&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_USERNAME&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;payload&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_USERNAME&#34;</span>
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;envname&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_PASSWORD&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;payload&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_PASSWORD&#34;</span>
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;envname&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_IMAGE&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;payload&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;DOCKER_IMAGE&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;pass-file-to-command&#34;</span>:
</span></span><span style="display:flex;"><span>    [
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;payload&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;ENV_FILE&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;envname&#34;</span>: <span style="color:#e6db74">&#34;ENV_FILE&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;base64decode&#34;</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;trigger-rule-mismatch-http-response-code&#34;</span>: <span style="color:#ae81ff">411</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;trigger-rule&#34;</span>:
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;match&#34;</span>:
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;value&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;value&#34;</span>: <span style="color:#e6db74">&#34;{{ here goes value of WEBHOOK_TOKEN from Github secrets }}&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;parameter&#34;</span>:
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;header&#34;</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;X-Token&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>So finally how <code>/etc/webhook/your-deploy-endpoint</code> (mounted from <code>/home/webhook</code> on the host) looks like? It&rsquo;s a simple bash script that just log-in into the docker registry, pulls a new image, and re-spins the container - so it does exactly what we were planning to solve here, just in an automated manner.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>set -e
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker login $DOCKER_REGISTRY -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker pull $DOCKER_IMAGE
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker stop my-app <span style="color:#f92672">||</span> true
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker rm my-app <span style="color:#f92672">||</span> true
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker run -d <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --restart unless-stopped <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name my-app <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --env-file<span style="color:#f92672">=</span>$ENV_FILE <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  $DOCKER_IMAGE
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker logout $DOCKER_REGISTRY
</span></span></code></pre></div><p>Don&rsquo;t worry that <code>$ENV_FILE</code> won&rsquo;t exist upon container manual restart (as it gets removed one webhook does its job) - if you do <code>docker inspect my-app</code> you should see that environment variables from the file were  already loaded and file itself is no longer required.</p>
<h3 id="summary">Summary</h3>
<p>So it might feel a little bit intimidating (and not easy!) at first but once you wrap your head about all the goodies that github provides and how it can fit nicely with <code>webhook</code> you might find should approach suits your needs.</p>
<p>Presented code shouldn&rsquo;t be treated as a production-ready example and more like a walk-through for automating the deployment of your hobby project - there are <em>a lot</em> of things that could potentially go wrong here - as we&rsquo;re happily passing envs to a shell command and spinning some images ;).</p>
<p><em>Happy deploying!</em></p>
]]></content:encoded></item><item><title>Installing ruby 2.6 via asdf on macOS 10.15 Catalina</title><link>https://awesomeprogrammer.com/blog/2020/06/22/installing-ruby-2.6-via-asdf-on-macos-10.15-catalina/</link><pubDate>Mon, 22 Jun 2020 18:58:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2020/06/22/installing-ruby-2.6-via-asdf-on-macos-10.15-catalina/</guid><description><![CDATA[<p>Recently I decided to give an <a href="https://github.com/asdf-vm/asdf">asdf</a> a go as I wanted to switch from my old rvm / nvm / homebrew-for-other-langs setup (plus I wanted to give a <a href="https://dart.dev/">dart</a> a try). Unfortunatelly installing ruby 2.6.x for one of the projects under Catalina was a true PITA.</p>
<p>If you encounter some errors like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>file.c:23:10: fatal error: <span style="color:#e6db74">&#39;CoreFoundation/CFString.h&#39;</span> file not found
</span></span><span style="display:flex;"><span><span style="color:#75715e">#include &lt;CoreFoundation/CFString.h&gt;</span>
</span></span></code></pre></div><p>or maybe like those:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>Last <span style="color:#ae81ff">10</span> log lines:
</span></span><span style="display:flex;"><span>int     sigvec<span style="color:#f92672">(</span>int, struct sigvec *, struct sigvec *<span style="color:#f92672">)</span>;
</span></span><span style="display:flex;"><span>                                                   ^
</span></span><span style="display:flex;"><span>                                                    _Nonnull
</span></span></code></pre></div><p>or even:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ld: symbol<span style="color:#f92672">(</span>s<span style="color:#f92672">)</span> not found <span style="color:#66d9ef">for</span> architecture x86_64
</span></span><span style="display:flex;"><span>clang-5.0: error: linker command failed with exit code <span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>You can try exporting some <em>magical ENVs</em> before executing <code>asdf install ruby</code> command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># don&#39;t forget $ for bash shell; using fish here</span>
</span></span><span style="display:flex;"><span>export CPATH<span style="color:#f92672">=(</span>xcrun --show-sdk-path<span style="color:#f92672">)</span>/usr/include
</span></span><span style="display:flex;"><span>export SDKROOT<span style="color:#f92672">=</span>/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
</span></span><span style="display:flex;"><span><span style="color:#75715e"># aaand we&#39;re switching compiler here as well</span>
</span></span><span style="display:flex;"><span>export CC<span style="color:#f92672">=</span>gcc
</span></span></code></pre></div><p>Of course no guarantees here. <em>Works on my machine</em> tho 😅.</p>]]></description><content:encoded><![CDATA[<p>Recently I decided to give an <a href="https://github.com/asdf-vm/asdf">asdf</a> a go as I wanted to switch from my old rvm / nvm / homebrew-for-other-langs setup (plus I wanted to give a <a href="https://dart.dev/">dart</a> a try). Unfortunatelly installing ruby 2.6.x for one of the projects under Catalina was a true PITA.</p>
<p>If you encounter some errors like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>file.c:23:10: fatal error: <span style="color:#e6db74">&#39;CoreFoundation/CFString.h&#39;</span> file not found
</span></span><span style="display:flex;"><span><span style="color:#75715e">#include &lt;CoreFoundation/CFString.h&gt;</span>
</span></span></code></pre></div><p>or maybe like those:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>Last <span style="color:#ae81ff">10</span> log lines:
</span></span><span style="display:flex;"><span>int     sigvec<span style="color:#f92672">(</span>int, struct sigvec *, struct sigvec *<span style="color:#f92672">)</span>;
</span></span><span style="display:flex;"><span>                                                   ^
</span></span><span style="display:flex;"><span>                                                    _Nonnull
</span></span></code></pre></div><p>or even:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ld: symbol<span style="color:#f92672">(</span>s<span style="color:#f92672">)</span> not found <span style="color:#66d9ef">for</span> architecture x86_64
</span></span><span style="display:flex;"><span>clang-5.0: error: linker command failed with exit code <span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>You can try exporting some <em>magical ENVs</em> before executing <code>asdf install ruby</code> command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># don&#39;t forget $ for bash shell; using fish here</span>
</span></span><span style="display:flex;"><span>export CPATH<span style="color:#f92672">=(</span>xcrun --show-sdk-path<span style="color:#f92672">)</span>/usr/include
</span></span><span style="display:flex;"><span>export SDKROOT<span style="color:#f92672">=</span>/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
</span></span><span style="display:flex;"><span><span style="color:#75715e"># aaand we&#39;re switching compiler here as well</span>
</span></span><span style="display:flex;"><span>export CC<span style="color:#f92672">=</span>gcc
</span></span></code></pre></div><p>Of course no guarantees here. <em>Works on my machine</em> tho 😅.</p>]]></content:encoded></item><item><title>PhotoSwipe video player (again with StimulusJS)</title><link>https://awesomeprogrammer.com/blog/2020/05/21/photoswipe-video-player-again-with-stimulusjs/</link><pubDate>Thu, 21 May 2020 10:05:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2020/05/21/photoswipe-video-player-again-with-stimulusjs/</guid><description><![CDATA[<p>I was surprised that previous blog post about <a href="/blog/2019/02/16/photoswipe-gallery-with-stimulusjs-on-rails/">how to implement PhotoSwipe gallery using StimulusJS</a> got so much traction (give the fact I spent 0 time promoting the blog and traffic is purely organic). So I decided to give it a follow-up and throw some ideas on how you can implement video support in PhotoSwipe using awesome <a href="https://plyr.io/">Plyr</a> library.</p>
<p>Some things to mention since the last post:</p>
<ul>
<li>I started using TypeScript more in the codebase so full gallery example is in <a href="https://www.typescriptlang.org/">TypeScript</a> (not the latest revision tho)</li>
<li>I&rsquo;m definitely not an expert when it comes to js and front-end in general, so if you find an issue somewhere - feel free to let me know!</li>
</ul>
<p>Inside the official PhotoSwipe repository you can find <a href="https://github.com/dimsemenov/PhotoSwipe/issues/651">huge discussion regarding video support</a> - it contains a bunch of ideas, code snippets, useful information and even full-blown photoswipe forks with video-support build-in. So I definitely recommend checking it out first.</p>
<p>Unfortunately I wasn&rsquo;t fully happy with suggested approaches so I decided to roll my own using that is backed by already mentioned <code>Plyr</code> library.</p>
<p>I assume you have your gallery in place, we need to glue some pieces to make it all happen.</p>
<h3 id="revisit-gallery-markup">Revisit gallery markup</h3>
<p>Do you remember this snippet mentioned in the previous post?</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">&lt;</span>%= link_to image.big_file_destination, title: image.title, data: { action: &#39;gallery#onImageClick&#39;, target: &#39;gallery.picture&#39; } do %&gt;
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">&lt;</span>%= image_tag image.small_thumb %&gt;
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">&lt;</span>% end %&gt;
</span></span></code></pre></div><p>We will modify this a little bit, first I&rsquo;m gonna pass <code>video</code> data attribute and <code>id</code> (or some sort of <code>uuid</code> - basically something unique per gallery item that I will re-use later in stimulus gallery controller). <code>link_to</code> will point to video (if gallery item is a video file - otherwise we will simply point to our big resolution image file). <code>image_tag</code> will point to thumbnail (video thumbnail in case it&rsquo;s a video or regular thumbnail of a static image in case it&rsquo;s not a video). Also let&rsquo;s rename <code>image</code> variable to <code>record</code> as we&rsquo;re dealing with more generic types now.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">&lt;</span>%= link_to record.file, title: record.title, data: { action: &#39;gallery#onImageClick&#39;, target: &#39;gallery.picture&#39;, video: record.video?, id: record.id } do %&gt;
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">&lt;</span>%= image_tag record.thumbnail %&gt;
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">&lt;</span>% end %&gt;
</span></span></code></pre></div><p>That&rsquo;s actually it when it comes to html markup, most work will happen in the gallery controller. So simply let&rsquo;s take a dive in.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#75715e">// First let&#39;s define custom interface based on PhotoSwipe.Item as we wanna know if we&#39;re
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// dealing with video or image; note I&#39;m also gonna define that `id` attribute I mentioned before
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ImageVideoItem</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">PhotoSwipe</span>.<span style="color:#a6e22e">Item</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">number</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">source</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">video?</span>: <span style="color:#66d9ef">boolean</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">class</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Controller</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">static</span> <span style="color:#a6e22e">targets</span> <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#39;picture&#39;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">pictureTargets</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// we&#39;re gonna store player instance and clean it up after gallery is closed
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">player</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">onImageClick</span>(<span style="color:#a6e22e">event</span>)<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">preventDefault</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// I&#39;m sill rendering gallery html markup somewhere outside the controller itself, but I&#39;m fine with it
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">galleryWrapper</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;.pswp&#39;</span>) <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">HTMLElement</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// you might want to adjust those according to your needs!
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">options</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">history</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">shareEl</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">closeOnScroll</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">closeOnVerticalDrag</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">focus</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// here we&#39;re gonna use custom source attribute instead of src to lookup the current item as videos
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#75715e">// are slightly different - we will simply render custom html there so we can initialize Plyr on those
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#a6e22e">index</span>: <span style="color:#66d9ef">this.items.findIndex</span>(<span style="color:#a6e22e">item</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">source</span> <span style="color:#f92672">===</span> <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">currentTarget</span>.<span style="color:#a6e22e">getAttribute</span>(<span style="color:#e6db74">&#39;href&#39;</span>))
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// using dynamic import here so we load everything only when needed thanks to webpack
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// note: you might need to set &#34;esModuleInterop&#34;: true in your tsconfig to get this to work properly
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">import</span>(<span style="color:#75715e">/* webpackChunkName: &#34;photoswipe&#34; */</span> <span style="color:#e6db74">&#39;photoswipe&#39;</span>).<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">PhotoSwipe</span> <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">import</span>(<span style="color:#75715e">/* webpackChunkName: &#34;photoswipe-ui-default&#34; */</span> <span style="color:#e6db74">&#39;photoswipe/dist/photoswipe-ui-default&#39;</span>).<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">PhotoSwipeUI_Default</span> <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">gallery</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">PhotoSwipe</span>.<span style="color:#66d9ef">default</span>(<span style="color:#a6e22e">galleryWrapper</span>, <span style="color:#a6e22e">PhotoSwipeUI_Default</span>.<span style="color:#66d9ef">default</span>, <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">items</span>, <span style="color:#a6e22e">options</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">listen</span>(<span style="color:#e6db74">&#39;destroy&#39;</span>, () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">cleanupPlayer</span>()
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">listen</span>(<span style="color:#e6db74">&#39;preventDragEvent&#39;</span>, (<span style="color:#a6e22e">event</span>, <span style="color:#a6e22e">isDown</span>, <span style="color:#a6e22e">preventObj</span>) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#75715e">// do not prevent touch events when player is up - this fixes seeking problems
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>          <span style="color:#a6e22e">preventObj</span>.<span style="color:#a6e22e">prevent</span> <span style="color:#f92672">=</span> <span style="color:#f92672">!</span><span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">player</span>
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">listen</span>(<span style="color:#e6db74">&#39;beforeChange&#39;</span>, () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">currItem</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">currItem</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">ImageVideoItem</span>
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">cleanupPlayer</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#75715e">// that&#39;s why we passed that video data attribute as I think
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>          <span style="color:#75715e">// it&#39;s simply easier to do detection on the backend
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>          <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">video</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">import</span>(<span style="color:#75715e">/* webpackChunkName: &#34;plyr&#34; */</span> <span style="color:#e6db74">&#39;plyr&#39;</span>).<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">plyr</span> <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>              <span style="color:#75715e">// we&#39;re gonna initialize plyr based on our html created for videos
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>              <span style="color:#75715e">// refer to Plyr docs for more info and tweak options as needed
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>              <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">player</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">plyr</span>.<span style="color:#66d9ef">default</span>(<span style="color:#e6db74">`#video-</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">id</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>, {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">hideControls</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">tooltips</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">controls</span>: <span style="color:#66d9ef">true</span>, <span style="color:#a6e22e">seek</span>: <span style="color:#66d9ef">true</span> },
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">volume</span>: <span style="color:#66d9ef">0.8</span>
</span></span><span style="display:flex;"><span>              })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>              <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">player</span>.<span style="color:#a6e22e">on</span>(<span style="color:#e6db74">&#39;ready&#39;</span>, () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">updateSize</span>(<span style="color:#66d9ef">false</span>)
</span></span><span style="display:flex;"><span>              })
</span></span><span style="display:flex;"><span>            })
</span></span><span style="display:flex;"><span>          } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">image</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Image</span>()
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">src</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">src</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">onload</span> <span style="color:#f92672">=</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">w</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">width</span>
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">h</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">height</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">updateSize</span>(<span style="color:#66d9ef">false</span>)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">init</span>()
</span></span><span style="display:flex;"><span>      })
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">get</span> <span style="color:#a6e22e">items</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">ImageVideoItem</span>[] {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">pictureTargets</span>.<span style="color:#a6e22e">map</span>(<span style="color:#a6e22e">item</span> <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">src</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">getAttribute</span>(<span style="color:#e6db74">&#39;href&#39;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">id</span> <span style="color:#f92672">=</span> parseInt(<span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">dataset</span>.<span style="color:#a6e22e">id</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// in case we&#39;re dealing with video we&#39;re gonna
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#75715e">// create html markup needed by Plyr
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#75715e">// maybe it&#39;s not the most elegant way, but it should do the job nicely
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">dataset</span>.<span style="color:#a6e22e">video</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;true&#39;</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">id</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">video</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">source</span>: <span style="color:#66d9ef">src</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#75715e">// on the backend my videos are compressed to mp4, you might need to adjust this further
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>          <span style="color:#a6e22e">html</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`&lt;video id=&#34;video-</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">id</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#34; playsinline controls&gt;`</span> <span style="color:#f92672">+</span>
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">`&lt;source src=&#34;</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">src</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#34; type=&#34;video/mp4&#34; /&gt;`</span> <span style="color:#f92672">+</span>
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#39;&lt;/video&gt;&#39;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">id</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">src</span>: <span style="color:#66d9ef">src</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">source</span>: <span style="color:#66d9ef">src</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">item.getAttribute</span>(<span style="color:#e6db74">&#39;title&#39;</span>),
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">w</span>: <span style="color:#66d9ef">0</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">h</span>: <span style="color:#66d9ef">0</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">cleanupPlayer</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">player</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">player</span>.<span style="color:#a6e22e">destroy</span>()
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">player</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">undefined</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>And that&rsquo;s pretty much it - it should give you general overview how you could potentially add video support to <code>PhotoSwipe</code> using <code>Plyr</code> and it should give your users pretty decent browsing experience.</p>]]></description><content:encoded><![CDATA[<p>I was surprised that previous blog post about <a href="/blog/2019/02/16/photoswipe-gallery-with-stimulusjs-on-rails/">how to implement PhotoSwipe gallery using StimulusJS</a> got so much traction (give the fact I spent 0 time promoting the blog and traffic is purely organic). So I decided to give it a follow-up and throw some ideas on how you can implement video support in PhotoSwipe using awesome <a href="https://plyr.io/">Plyr</a> library.</p>
<p>Some things to mention since the last post:</p>
<ul>
<li>I started using TypeScript more in the codebase so full gallery example is in <a href="https://www.typescriptlang.org/">TypeScript</a> (not the latest revision tho)</li>
<li>I&rsquo;m definitely not an expert when it comes to js and front-end in general, so if you find an issue somewhere - feel free to let me know!</li>
</ul>
<p>Inside the official PhotoSwipe repository you can find <a href="https://github.com/dimsemenov/PhotoSwipe/issues/651">huge discussion regarding video support</a> - it contains a bunch of ideas, code snippets, useful information and even full-blown photoswipe forks with video-support build-in. So I definitely recommend checking it out first.</p>
<p>Unfortunately I wasn&rsquo;t fully happy with suggested approaches so I decided to roll my own using that is backed by already mentioned <code>Plyr</code> library.</p>
<p>I assume you have your gallery in place, we need to glue some pieces to make it all happen.</p>
<h3 id="revisit-gallery-markup">Revisit gallery markup</h3>
<p>Do you remember this snippet mentioned in the previous post?</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">&lt;</span>%= link_to image.big_file_destination, title: image.title, data: { action: &#39;gallery#onImageClick&#39;, target: &#39;gallery.picture&#39; } do %&gt;
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">&lt;</span>%= image_tag image.small_thumb %&gt;
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">&lt;</span>% end %&gt;
</span></span></code></pre></div><p>We will modify this a little bit, first I&rsquo;m gonna pass <code>video</code> data attribute and <code>id</code> (or some sort of <code>uuid</code> - basically something unique per gallery item that I will re-use later in stimulus gallery controller). <code>link_to</code> will point to video (if gallery item is a video file - otherwise we will simply point to our big resolution image file). <code>image_tag</code> will point to thumbnail (video thumbnail in case it&rsquo;s a video or regular thumbnail of a static image in case it&rsquo;s not a video). Also let&rsquo;s rename <code>image</code> variable to <code>record</code> as we&rsquo;re dealing with more generic types now.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">&lt;</span>%= link_to record.file, title: record.title, data: { action: &#39;gallery#onImageClick&#39;, target: &#39;gallery.picture&#39;, video: record.video?, id: record.id } do %&gt;
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">&lt;</span>%= image_tag record.thumbnail %&gt;
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">&lt;</span>% end %&gt;
</span></span></code></pre></div><p>That&rsquo;s actually it when it comes to html markup, most work will happen in the gallery controller. So simply let&rsquo;s take a dive in.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#75715e">// First let&#39;s define custom interface based on PhotoSwipe.Item as we wanna know if we&#39;re
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// dealing with video or image; note I&#39;m also gonna define that `id` attribute I mentioned before
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ImageVideoItem</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">PhotoSwipe</span>.<span style="color:#a6e22e">Item</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">number</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">source</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">video?</span>: <span style="color:#66d9ef">boolean</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">class</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Controller</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">static</span> <span style="color:#a6e22e">targets</span> <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#39;picture&#39;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">pictureTargets</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// we&#39;re gonna store player instance and clean it up after gallery is closed
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">player</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">onImageClick</span>(<span style="color:#a6e22e">event</span>)<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">preventDefault</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// I&#39;m sill rendering gallery html markup somewhere outside the controller itself, but I&#39;m fine with it
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">galleryWrapper</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;.pswp&#39;</span>) <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">HTMLElement</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// you might want to adjust those according to your needs!
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">options</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">history</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">shareEl</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">closeOnScroll</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">closeOnVerticalDrag</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">focus</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// here we&#39;re gonna use custom source attribute instead of src to lookup the current item as videos
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#75715e">// are slightly different - we will simply render custom html there so we can initialize Plyr on those
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#a6e22e">index</span>: <span style="color:#66d9ef">this.items.findIndex</span>(<span style="color:#a6e22e">item</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">source</span> <span style="color:#f92672">===</span> <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">currentTarget</span>.<span style="color:#a6e22e">getAttribute</span>(<span style="color:#e6db74">&#39;href&#39;</span>))
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// using dynamic import here so we load everything only when needed thanks to webpack
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// note: you might need to set &#34;esModuleInterop&#34;: true in your tsconfig to get this to work properly
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">import</span>(<span style="color:#75715e">/* webpackChunkName: &#34;photoswipe&#34; */</span> <span style="color:#e6db74">&#39;photoswipe&#39;</span>).<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">PhotoSwipe</span> <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">import</span>(<span style="color:#75715e">/* webpackChunkName: &#34;photoswipe-ui-default&#34; */</span> <span style="color:#e6db74">&#39;photoswipe/dist/photoswipe-ui-default&#39;</span>).<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">PhotoSwipeUI_Default</span> <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">gallery</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">PhotoSwipe</span>.<span style="color:#66d9ef">default</span>(<span style="color:#a6e22e">galleryWrapper</span>, <span style="color:#a6e22e">PhotoSwipeUI_Default</span>.<span style="color:#66d9ef">default</span>, <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">items</span>, <span style="color:#a6e22e">options</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">listen</span>(<span style="color:#e6db74">&#39;destroy&#39;</span>, () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">cleanupPlayer</span>()
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">listen</span>(<span style="color:#e6db74">&#39;preventDragEvent&#39;</span>, (<span style="color:#a6e22e">event</span>, <span style="color:#a6e22e">isDown</span>, <span style="color:#a6e22e">preventObj</span>) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#75715e">// do not prevent touch events when player is up - this fixes seeking problems
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>          <span style="color:#a6e22e">preventObj</span>.<span style="color:#a6e22e">prevent</span> <span style="color:#f92672">=</span> <span style="color:#f92672">!</span><span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">player</span>
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">listen</span>(<span style="color:#e6db74">&#39;beforeChange&#39;</span>, () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">currItem</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">currItem</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">ImageVideoItem</span>
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">cleanupPlayer</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#75715e">// that&#39;s why we passed that video data attribute as I think
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>          <span style="color:#75715e">// it&#39;s simply easier to do detection on the backend
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>          <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">video</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">import</span>(<span style="color:#75715e">/* webpackChunkName: &#34;plyr&#34; */</span> <span style="color:#e6db74">&#39;plyr&#39;</span>).<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">plyr</span> <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>              <span style="color:#75715e">// we&#39;re gonna initialize plyr based on our html created for videos
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>              <span style="color:#75715e">// refer to Plyr docs for more info and tweak options as needed
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>              <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">player</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">plyr</span>.<span style="color:#66d9ef">default</span>(<span style="color:#e6db74">`#video-</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">id</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>, {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">hideControls</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">tooltips</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">controls</span>: <span style="color:#66d9ef">true</span>, <span style="color:#a6e22e">seek</span>: <span style="color:#66d9ef">true</span> },
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">volume</span>: <span style="color:#66d9ef">0.8</span>
</span></span><span style="display:flex;"><span>              })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>              <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">player</span>.<span style="color:#a6e22e">on</span>(<span style="color:#e6db74">&#39;ready&#39;</span>, () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">updateSize</span>(<span style="color:#66d9ef">false</span>)
</span></span><span style="display:flex;"><span>              })
</span></span><span style="display:flex;"><span>            })
</span></span><span style="display:flex;"><span>          } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">image</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Image</span>()
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">src</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">src</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">onload</span> <span style="color:#f92672">=</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">w</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">width</span>
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">h</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">height</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">updateSize</span>(<span style="color:#66d9ef">false</span>)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">init</span>()
</span></span><span style="display:flex;"><span>      })
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">get</span> <span style="color:#a6e22e">items</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">ImageVideoItem</span>[] {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">pictureTargets</span>.<span style="color:#a6e22e">map</span>(<span style="color:#a6e22e">item</span> <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">src</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">getAttribute</span>(<span style="color:#e6db74">&#39;href&#39;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">id</span> <span style="color:#f92672">=</span> parseInt(<span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">dataset</span>.<span style="color:#a6e22e">id</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// in case we&#39;re dealing with video we&#39;re gonna
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#75715e">// create html markup needed by Plyr
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#75715e">// maybe it&#39;s not the most elegant way, but it should do the job nicely
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">dataset</span>.<span style="color:#a6e22e">video</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;true&#39;</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">id</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">video</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">source</span>: <span style="color:#66d9ef">src</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#75715e">// on the backend my videos are compressed to mp4, you might need to adjust this further
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>          <span style="color:#a6e22e">html</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`&lt;video id=&#34;video-</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">id</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#34; playsinline controls&gt;`</span> <span style="color:#f92672">+</span>
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">`&lt;source src=&#34;</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">src</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#34; type=&#34;video/mp4&#34; /&gt;`</span> <span style="color:#f92672">+</span>
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#39;&lt;/video&gt;&#39;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">id</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">src</span>: <span style="color:#66d9ef">src</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">source</span>: <span style="color:#66d9ef">src</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">item.getAttribute</span>(<span style="color:#e6db74">&#39;title&#39;</span>),
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">w</span>: <span style="color:#66d9ef">0</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">h</span>: <span style="color:#66d9ef">0</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">cleanupPlayer</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">player</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">player</span>.<span style="color:#a6e22e">destroy</span>()
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">player</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">undefined</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>And that&rsquo;s pretty much it - it should give you general overview how you could potentially add video support to <code>PhotoSwipe</code> using <code>Plyr</code> and it should give your users pretty decent browsing experience.</p>]]></content:encoded></item><item><title>Delaying gratification (in software development)</title><link>https://awesomeprogrammer.com/blog/2020/05/21/delaying-gratification-in-software-development/</link><pubDate>Thu, 21 May 2020 08:36:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2020/05/21/delaying-gratification-in-software-development/</guid><description><![CDATA[<p>Below be some bunch of notes, for future self.</p>
<p>As I&rsquo;m getting older I like to think I&rsquo;m also getting wiser - at least in some areas of my life. When I look back at myself, as an eager and enthusiastic, but inexperienced developer and at myself now - I can clearly see that I&rsquo;m much more <em>patient</em> in general. I realized it&rsquo;s a marathon and not a sprint (Hopefully. Unless you know - we will all die soon in 2020).</p>
<p>As developers, I think we often like to see <em>results</em> very quickly. This might be a working product prototype (full of bugs), it might be the mighty refactor (that introduced bunch of regressions), it might be a pursue after new shiny tech (that is not the right tool for the job, but promised <em>a better way</em> of doing things) or it might be a new approach to the architecture you read on your favorite blog (that in the end doesn&rsquo;t mention all the new problems it brings to the table).</p>
<p>And being enthusiastic is great, researching for new ways of doing things is also great. But I think I&rsquo;m slowly getting to a point when I can see a balance between doing things <em>new way</em> and doing things <em>known way</em>. I think known way can be very under-appreciated. Known way was probably formed over the period the years and there might be a lot of wisdom in a known way.</p>
<p>Appreciate tools and technology that was battle-tested for years and years by hundreds of thousands of people. Appreciate the vast ecosystem and community around those tools. Don&rsquo;t be side blinded by this new thing that looks great on paper. On paper that &lsquo;hello world&rsquo; program example looks great. But is your product/project a hello world app? I doubt it.</p>
<p>Allow yourself to explore, but have a critical eye. Solving one problem usually brings another one. Use edge tooling in your projects, but don&rsquo;t burden your team with it (unless you know, you&rsquo;re all on the same page, I don&rsquo;t judge).</p>
<p>Don&rsquo;t try to rewrite half of the codebase on a single day and &ldquo;do it better&rdquo;. <em>Your</em> code will be legacy code soon enough - try to learn from existing code, often there is a lot of knowledge and struggle underneath (it&rsquo;s not always pretty tho!).</p>
<p>Take your time, delay design decisions in uncertain days (if that means, for example, introducing some duplication). I got a moment of clarity <em>months</em> after introducing some rather naive solution just to stumble upon the optimal one. Then things just fell into the right place nicely - it&rsquo;s a very good feeling.</p>
<p>As a side note delaying gratification was described/mentioned as a self-discipline tool in one of my favorite self-help/mental-health books - <a href="https://www.amazon.com/dp/0743243153">The Road Less Traveled</a>, I recommend giving it a try - hopefully you will find it interesting.</p>]]></description><content:encoded><![CDATA[<p>Below be some bunch of notes, for future self.</p>
<p>As I&rsquo;m getting older I like to think I&rsquo;m also getting wiser - at least in some areas of my life. When I look back at myself, as an eager and enthusiastic, but inexperienced developer and at myself now - I can clearly see that I&rsquo;m much more <em>patient</em> in general. I realized it&rsquo;s a marathon and not a sprint (Hopefully. Unless you know - we will all die soon in 2020).</p>
<p>As developers, I think we often like to see <em>results</em> very quickly. This might be a working product prototype (full of bugs), it might be the mighty refactor (that introduced bunch of regressions), it might be a pursue after new shiny tech (that is not the right tool for the job, but promised <em>a better way</em> of doing things) or it might be a new approach to the architecture you read on your favorite blog (that in the end doesn&rsquo;t mention all the new problems it brings to the table).</p>
<p>And being enthusiastic is great, researching for new ways of doing things is also great. But I think I&rsquo;m slowly getting to a point when I can see a balance between doing things <em>new way</em> and doing things <em>known way</em>. I think known way can be very under-appreciated. Known way was probably formed over the period the years and there might be a lot of wisdom in a known way.</p>
<p>Appreciate tools and technology that was battle-tested for years and years by hundreds of thousands of people. Appreciate the vast ecosystem and community around those tools. Don&rsquo;t be side blinded by this new thing that looks great on paper. On paper that &lsquo;hello world&rsquo; program example looks great. But is your product/project a hello world app? I doubt it.</p>
<p>Allow yourself to explore, but have a critical eye. Solving one problem usually brings another one. Use edge tooling in your projects, but don&rsquo;t burden your team with it (unless you know, you&rsquo;re all on the same page, I don&rsquo;t judge).</p>
<p>Don&rsquo;t try to rewrite half of the codebase on a single day and &ldquo;do it better&rdquo;. <em>Your</em> code will be legacy code soon enough - try to learn from existing code, often there is a lot of knowledge and struggle underneath (it&rsquo;s not always pretty tho!).</p>
<p>Take your time, delay design decisions in uncertain days (if that means, for example, introducing some duplication). I got a moment of clarity <em>months</em> after introducing some rather naive solution just to stumble upon the optimal one. Then things just fell into the right place nicely - it&rsquo;s a very good feeling.</p>
<p>As a side note delaying gratification was described/mentioned as a self-discipline tool in one of my favorite self-help/mental-health books - <a href="https://www.amazon.com/dp/0743243153">The Road Less Traveled</a>, I recommend giving it a try - hopefully you will find it interesting.</p>]]></content:encoded></item><item><title>Configuring ipv6 on OVH vps using netplan (Ubuntu)</title><link>https://awesomeprogrammer.com/blog/2020/01/04/configuring-ipv6-on-ovh-vps-using-netplan-ubuntu/</link><pubDate>Sat, 04 Jan 2020 12:21:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2020/01/04/configuring-ipv6-on-ovh-vps-using-netplan-ubuntu/</guid><description><![CDATA[<p>There is an <a href="https://docs.ovh.com/gb/en/vps/configuring-ipv6/">official guide</a> regarding ipv6 configuration using Ubuntu&rsquo;s netplan. It seems it skips one crucial thing tho - and that is default route. The route step is mentioned in <em>non-persistent</em> and <em>file interfaces</em> sections (the <code>ip -6 route add</code> part). In order to fully get v6 working on their VPS I had to add the route via <code>routes</code> declaration in netplan yaml configuration (Ubuntu LTS 18.04).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># /etc/netplan/51-cloud-init-ipv6.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">netplan</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">network</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">version</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ethernets</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">ens3</span>: <span style="color:#75715e"># you main eth device</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">dhcp6</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">match</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ens3</span> <span style="color:#75715e"># same ^</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">addresses</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#75715e"># ipv6 address can be obtained from their web ui</span>
</span></span><span style="display:flex;"><span>          <span style="color:#75715e"># netmask offered by ovh on their VPSes weirdly is not mentioned explicitly anywhere</span>
</span></span><span style="display:flex;"><span>          - <span style="color:#e6db74">&#34;YOUR_IPV6/128&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">gateway6</span>: <span style="color:#e6db74">&#34;IPv6_GATEWAY&#34;</span> <span style="color:#75715e"># v6 gateway can be obtained from their web ui panel</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routes</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">to</span>: <span style="color:#e6db74">&#34;IPv6_GATEWAY&#34;</span> <span style="color:#75715e"># same as above ^</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">scope</span>: <span style="color:#ae81ff">link</span> <span style="color:#75715e"># this should work just fine on modern (v0.98+) netplan</span>
</span></span></code></pre></div><p>Run <code>netplan apply</code> after introducing changes and try ping v6 host or simply execute some http request:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -v -6 google.com
</span></span></code></pre></div>]]></description><content:encoded><![CDATA[<p>There is an <a href="https://docs.ovh.com/gb/en/vps/configuring-ipv6/">official guide</a> regarding ipv6 configuration using Ubuntu&rsquo;s netplan. It seems it skips one crucial thing tho - and that is default route. The route step is mentioned in <em>non-persistent</em> and <em>file interfaces</em> sections (the <code>ip -6 route add</code> part). In order to fully get v6 working on their VPS I had to add the route via <code>routes</code> declaration in netplan yaml configuration (Ubuntu LTS 18.04).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># /etc/netplan/51-cloud-init-ipv6.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">netplan</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">network</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">version</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ethernets</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">ens3</span>: <span style="color:#75715e"># you main eth device</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">dhcp6</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">match</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ens3</span> <span style="color:#75715e"># same ^</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">addresses</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#75715e"># ipv6 address can be obtained from their web ui</span>
</span></span><span style="display:flex;"><span>          <span style="color:#75715e"># netmask offered by ovh on their VPSes weirdly is not mentioned explicitly anywhere</span>
</span></span><span style="display:flex;"><span>          - <span style="color:#e6db74">&#34;YOUR_IPV6/128&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">gateway6</span>: <span style="color:#e6db74">&#34;IPv6_GATEWAY&#34;</span> <span style="color:#75715e"># v6 gateway can be obtained from their web ui panel</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routes</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">to</span>: <span style="color:#e6db74">&#34;IPv6_GATEWAY&#34;</span> <span style="color:#75715e"># same as above ^</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">scope</span>: <span style="color:#ae81ff">link</span> <span style="color:#75715e"># this should work just fine on modern (v0.98+) netplan</span>
</span></span></code></pre></div><p>Run <code>netplan apply</code> after introducing changes and try ping v6 host or simply execute some http request:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -v -6 google.com
</span></span></code></pre></div>]]></content:encoded></item><item><title>How to handle kill signal in go (inside a docker container)</title><link>https://awesomeprogrammer.com/blog/2020/01/04/how-to-handle-kill-signal-in-go-inside-a-docker-container/</link><pubDate>Sat, 04 Jan 2020 07:04:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2020/01/04/how-to-handle-kill-signal-in-go-inside-a-docker-container/</guid><description><![CDATA[<p>Let&rsquo;s say you have a go app deployed under docker and you want to handle kill signal send by docker (when <em>docker stop</em> is executed or when your service is being updated by docker swarm or whatever orchestration service). This usually should happen on long-running processes that need to close some connections, stop accepting new work before shutdown and so on.</p>
<p>First, you need to make sure that your go app is running inside the container with PID 1 - take a look at official documentation <a href="https://docs.docker.com/engine/reference/builder/#cmd">here</a> and <a href="https://docs.docker.com/engine/reference/builder/#entrypoint">here</a>.</p>
<p>Tl;dr version is - use <em>exec</em> form of <code>CMD</code> / <code>ENTRYPOINT</code> in your <em>Dockerfile</em>, so do this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">CMD</span> [<span style="color:#e6db74">&#34;executable&#34;</span>, <span style="color:#e6db74">&#34;param1&#34;</span>, <span style="color:#e6db74">&#34;param2&#34;</span>]<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>and not this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">CMD</span> command param1 param2<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Details as usually matter. Once you have that sorted out let&rsquo;s take a look at how docker stops the container.</p>
<p>By default <code>SIGTERM</code> is sent and after 10 seconds, if an app is still not done (running) <code>SIGKILL</code> is added on top ;). Those defaults can be easily configured for example via your docker compose file using <a href="https://docs.docker.com/compose/compose-file/#stop_signal">stop_signal</a> and <a href="https://docs.docker.com/compose/compose-file/#stop_grace_period">stop_grace_period</a> options.</p>
<p>Now let&rsquo;s have a quick look at how you can trap that signal inside your go program.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// main.go
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// (...)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// our channel that will block execution until we catch kill signal
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">done</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">bool</span>, <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">signalChan</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Signal</span>, <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// note - you can catch multiple signals here, but for sake of example
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// let&#39;s stick to docker&#39;s default SIGTERM and assume we don&#39;t need
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// more complicated workflow
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">signal</span>.<span style="color:#a6e22e">Notify</span>(<span style="color:#a6e22e">signalChan</span>, <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SIGTERM</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">sig</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">signalChan</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;Captured SIG: %v. Existence is pain!&#34;</span>, <span style="color:#a6e22e">sig</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// here you can stop accepting new work
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// and close any opened connections
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// flush sentry and do general shutdown cleanup
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">done</span> <span style="color:#f92672">&lt;-</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  }()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;I assume app is doing some work at this point ;)&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">done</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;Good-bye, I&#39;m Mr. Meeseeks!&#34;</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Build &amp; run your container, and try to stop it via <code>docker stop &lt;id&gt;</code>. In logs you should see something like:</p>
<pre tabindex="0"><code class="language-log" data-lang="log">2020/01/03 18:10:57 I assume app is doing some work at this point ;)
2020/01/03 18:11:27 Captured SIG: terminated. Existence is pain!
2020/01/03 18:11:27 Good-bye, I&#39;m Mr. Meeseeks!
</code></pre><p>And that is more-or-less how you properly stop go-app running inside a docker container.</p>
<figure><img src="/images/meeeseeks.gif"/><figcaption>
            <h4>This blogpost was obviously sponsored by Mr. Meeseeks gang.</h4>
        </figcaption>
</figure>]]></description><content:encoded><![CDATA[<p>Let&rsquo;s say you have a go app deployed under docker and you want to handle kill signal send by docker (when <em>docker stop</em> is executed or when your service is being updated by docker swarm or whatever orchestration service). This usually should happen on long-running processes that need to close some connections, stop accepting new work before shutdown and so on.</p>
<p>First, you need to make sure that your go app is running inside the container with PID 1 - take a look at official documentation <a href="https://docs.docker.com/engine/reference/builder/#cmd">here</a> and <a href="https://docs.docker.com/engine/reference/builder/#entrypoint">here</a>.</p>
<p>Tl;dr version is - use <em>exec</em> form of <code>CMD</code> / <code>ENTRYPOINT</code> in your <em>Dockerfile</em>, so do this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">CMD</span> [<span style="color:#e6db74">&#34;executable&#34;</span>, <span style="color:#e6db74">&#34;param1&#34;</span>, <span style="color:#e6db74">&#34;param2&#34;</span>]<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>and not this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">CMD</span> command param1 param2<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Details as usually matter. Once you have that sorted out let&rsquo;s take a look at how docker stops the container.</p>
<p>By default <code>SIGTERM</code> is sent and after 10 seconds, if an app is still not done (running) <code>SIGKILL</code> is added on top ;). Those defaults can be easily configured for example via your docker compose file using <a href="https://docs.docker.com/compose/compose-file/#stop_signal">stop_signal</a> and <a href="https://docs.docker.com/compose/compose-file/#stop_grace_period">stop_grace_period</a> options.</p>
<p>Now let&rsquo;s have a quick look at how you can trap that signal inside your go program.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// main.go
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// (...)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// our channel that will block execution until we catch kill signal
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">done</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">bool</span>, <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">signalChan</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Signal</span>, <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// note - you can catch multiple signals here, but for sake of example
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// let&#39;s stick to docker&#39;s default SIGTERM and assume we don&#39;t need
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// more complicated workflow
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">signal</span>.<span style="color:#a6e22e">Notify</span>(<span style="color:#a6e22e">signalChan</span>, <span style="color:#a6e22e">syscall</span>.<span style="color:#a6e22e">SIGTERM</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">sig</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">signalChan</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;Captured SIG: %v. Existence is pain!&#34;</span>, <span style="color:#a6e22e">sig</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// here you can stop accepting new work
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// and close any opened connections
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// flush sentry and do general shutdown cleanup
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">done</span> <span style="color:#f92672">&lt;-</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  }()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;I assume app is doing some work at this point ;)&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">done</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;Good-bye, I&#39;m Mr. Meeseeks!&#34;</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Build &amp; run your container, and try to stop it via <code>docker stop &lt;id&gt;</code>. In logs you should see something like:</p>
<pre tabindex="0"><code class="language-log" data-lang="log">2020/01/03 18:10:57 I assume app is doing some work at this point ;)
2020/01/03 18:11:27 Captured SIG: terminated. Existence is pain!
2020/01/03 18:11:27 Good-bye, I&#39;m Mr. Meeseeks!
</code></pre><p>And that is more-or-less how you properly stop go-app running inside a docker container.</p>
<figure><img src="/images/meeeseeks.gif"/><figcaption>
            <h4>This blogpost was obviously sponsored by Mr. Meeseeks gang.</h4>
        </figcaption>
</figure>]]></content:encoded></item><item><title>Opensource Google Analytics alternative (with Traefik)</title><link>https://awesomeprogrammer.com/blog/2019/10/26/opensource-google-analytics-alternative-with-traefik/</link><pubDate>Sat, 26 Oct 2019 05:04:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2019/10/26/opensource-google-analytics-alternative-with-traefik/</guid><description><![CDATA[<p>I have been trying to tune down the usage of Google services in my life in the last couple of months.</p>
<p>The monopoly google has regarding the global Internet is simply not healthy and we should be more aware of whom we gave our data. Over the years we very easily traded privacy for convenience without even noticing. At least I did.</p>
<p>So first I switched to <a href="https://protonmail.com/">protonmail</a>, I started to keep more of my data locally / on my Synology NAS, I tend to use <a href="https://duckduckgo.com/">duckduckgo</a> more often. Just add a little bit of diversity to keep things healthy. Unfortunately, I noticed <em>by default</em> I add Google Analytics to all my sites - even tho I have <a href="https://pi-hole.net/">pihole</a> setup locally. Oh, the irony. I talk about privacy and I give the data about my visitors to Google without hesitation. And don&rsquo;t get me wrong - there is nothing wrong with analytics in general - I mean as an author/contributor you want to know how your content is performing. You have to measure it somehow.</p>
<p>So I started to look for an alternative. There is obviously <a href="https://matomo.org/">matomo</a> (former piwik). There is simple, but promising <a href="https://github.com/usefathom/fathom">phantom analytics</a>. After some more research, I finally stumbled upon <a href="https://count.ly/">countly</a>.</p>
<p>Matomo seems like a beast that collects waaay too much information about visitors by default, phantom seems too limiting so I went with countly. For personal use it&rsquo;s more than enough. For professional use? You will be missing some key features (eg. funnels) - which are accessible in enterprise edition tho. But the open-source version gives you a <em>general</em> overview about how your site is performing and which sections are most interesting for your readers.</p>
<p>So, let&rsquo;s try to use traefik 2.0 to install our analytics then! We will use the suggested mongodb image for our database and configure routing rules for frontend and api <a href="https://github.com/Countly/countly-server/blob/master/bin/docker/nginx.server.conf">based on provided nginx example</a>.</p>
<p>Note: if you&rsquo;re not familiar with new Traefik syntax checkout <a href="/blog/2019/10/14/load-balancing-docker-services-with-traefik-2.0/">my previous blog post about spinning up Traefik 2.x in general</a>.</p>
<p>Below is ansible snippet for creating our database container. For sake of simplicity we will spin single one and point frontend &amp; api to it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create a named volume</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_volume</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">countly</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create countly db container</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_container</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">started</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">countly-db</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart_policy</span>: <span style="color:#ae81ff">always</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#e6db74">&#39;bitnami/mongodb:3.6.14&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># aadjust this to your needs, I personally bind port on a</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># private interface as db is kept on a different node</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;27017:27017&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">MONGODB_USERNAME</span>: <span style="color:#ae81ff">username</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">MONGODB_PASSWORD</span>: <span style="color:#ae81ff">password</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">MONGODB_DATABASE</span>: <span style="color:#ae81ff">dbname</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">MONGODB_ROOT_PASSWORD</span>: <span style="color:#ae81ff">rootpassword</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;countly:/bitnami&#39;</span>
</span></span></code></pre></div><p>Now let&rsquo;s start with the API part.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create countly api container</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_container</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">countly-api</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">countly/api:19.08.1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">started</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart_policy</span>: <span style="color:#ae81ff">always</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">expose</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">3001</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">etc_hosts</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &#34;db-host&#34;: &#34;{{ I keep mongodb container on a different host and I grab private ip of that host here }}&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      }</span>      
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># what is &amp;w: https://docs.mongodb.com/manual/reference/write-concern/#wc-w</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># change DB-USER, DB-PASSWORD, DB-NAME, DB-PORT according to your mongo credentials</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">COUNTLY_CONFIG_API_MONGODB</span>: <span style="color:#e6db74">&#34;mongodb://DB-USER:DB-PASSWORD@db-host:DB-PORT/DB-NAME?w=1&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">COUNTLY_CONFIG_FRONTEND_MONGODB</span>: <span style="color:#e6db74">&#34;mongodb://DB-HUSER:DB-PASSWORD@db-host:DB-PORT/DB-NAME?w=1&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.enable</span>: <span style="color:#e6db74">&#39;true&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.docker.network</span>: <span style="color:#e6db74">&#39;traefik&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-api.rule</span>: <span style="color:#e6db74">&#39;Host(`countly.mydomain`) &amp;&amp; (PathPrefix(`/i/`, `/o/`) || Path(`/i`, `/o`))&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-api.entrypoints</span>: <span style="color:#e6db74">&#39;websecure&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># we set a higher priority here due to PathPrefix &amp; Path matchers</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-api.priority</span>: <span style="color:#e6db74">&#39;2&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-api.tls.certResolver</span>: <span style="color:#e6db74">&#39;letsencrypt&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.services.balancer-countly-api.loadbalancer.server.port</span>: <span style="color:#e6db74">&#39;3001&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">purge_networks</span>: <span style="color:#66d9ef">yes</span>
</span></span></code></pre></div><p>And here goes our final frontend container.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create countly frontend container</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_container</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">countly-frontend</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">countly/frontend:19.08.1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">started</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart_policy</span>: <span style="color:#ae81ff">always</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">expose</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">6001</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">etc_hosts</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &#34;db-host&#34;: &#34;{{ same here thing; change / remove this }}&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      }</span>      
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">COUNTLY_CONFIG_API_MONGODB</span>: <span style="color:#e6db74">&#34;mongodb://DB-USER:DB-PASSWORD@db-host:DB-PORT/DB-NAME?w=1&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">COUNTLY_CONFIG_FRONTEND_MONGODB</span>: <span style="color:#e6db74">&#34;mongodb://DB-HUSER:DB-PASSWORD@db-host:DB-PORT/DB-NAME?w=1&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.enable</span>: <span style="color:#e6db74">&#39;true&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.docker.network</span>: <span style="color:#e6db74">&#39;traefik&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-http-countly-frontend.rule</span>: <span style="color:#e6db74">&#39;Host(`countly.mydomain`)&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-http-countly-frontend.entrypoints</span>: <span style="color:#e6db74">&#39;web&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-http-countly-frontend.middlewares</span>: <span style="color:#e6db74">&#39;redirect-https@docker&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-frontend.rule</span>: <span style="color:#e6db74">&#39;Host(`countly.mydomain`)&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-frontend.entrypoints</span>: <span style="color:#e6db74">&#39;websecure&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-frontend.priority</span>: <span style="color:#e6db74">&#39;1&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-frontend.tls.certResolver</span>: <span style="color:#e6db74">&#39;letsencrypt&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># If you want to throw some basic auth for your front-end in the mix:</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># traefik.http.middlewares.countly-auth.basicauth.users: &#39;/generate with htpasswd/&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># traefik.http.routers.router-https-countly-frontend.middlewares: &#39;countly-auth@docker&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.services.balancer-countly-frontend.loadbalancer.server.port</span>: <span style="color:#e6db74">&#39;6001&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">purge_networks</span>: <span style="color:#66d9ef">yes</span>
</span></span></code></pre></div><p>And that&rsquo;s it - once you visit <code>countly.mydomain</code> you should be prompted to create an admin account and add your first website.</p>
<figure><img src="/images/countly.png"/><figcaption>
            <h4>Part of countly&#39;s dashboard. Data from one of my projects.</h4>
        </figcaption>
</figure>]]></description><content:encoded><![CDATA[<p>I have been trying to tune down the usage of Google services in my life in the last couple of months.</p>
<p>The monopoly google has regarding the global Internet is simply not healthy and we should be more aware of whom we gave our data. Over the years we very easily traded privacy for convenience without even noticing. At least I did.</p>
<p>So first I switched to <a href="https://protonmail.com/">protonmail</a>, I started to keep more of my data locally / on my Synology NAS, I tend to use <a href="https://duckduckgo.com/">duckduckgo</a> more often. Just add a little bit of diversity to keep things healthy. Unfortunately, I noticed <em>by default</em> I add Google Analytics to all my sites - even tho I have <a href="https://pi-hole.net/">pihole</a> setup locally. Oh, the irony. I talk about privacy and I give the data about my visitors to Google without hesitation. And don&rsquo;t get me wrong - there is nothing wrong with analytics in general - I mean as an author/contributor you want to know how your content is performing. You have to measure it somehow.</p>
<p>So I started to look for an alternative. There is obviously <a href="https://matomo.org/">matomo</a> (former piwik). There is simple, but promising <a href="https://github.com/usefathom/fathom">phantom analytics</a>. After some more research, I finally stumbled upon <a href="https://count.ly/">countly</a>.</p>
<p>Matomo seems like a beast that collects waaay too much information about visitors by default, phantom seems too limiting so I went with countly. For personal use it&rsquo;s more than enough. For professional use? You will be missing some key features (eg. funnels) - which are accessible in enterprise edition tho. But the open-source version gives you a <em>general</em> overview about how your site is performing and which sections are most interesting for your readers.</p>
<p>So, let&rsquo;s try to use traefik 2.0 to install our analytics then! We will use the suggested mongodb image for our database and configure routing rules for frontend and api <a href="https://github.com/Countly/countly-server/blob/master/bin/docker/nginx.server.conf">based on provided nginx example</a>.</p>
<p>Note: if you&rsquo;re not familiar with new Traefik syntax checkout <a href="/blog/2019/10/14/load-balancing-docker-services-with-traefik-2.0/">my previous blog post about spinning up Traefik 2.x in general</a>.</p>
<p>Below is ansible snippet for creating our database container. For sake of simplicity we will spin single one and point frontend &amp; api to it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create a named volume</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_volume</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">countly</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create countly db container</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_container</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">started</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">countly-db</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart_policy</span>: <span style="color:#ae81ff">always</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#e6db74">&#39;bitnami/mongodb:3.6.14&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># aadjust this to your needs, I personally bind port on a</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># private interface as db is kept on a different node</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;27017:27017&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">MONGODB_USERNAME</span>: <span style="color:#ae81ff">username</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">MONGODB_PASSWORD</span>: <span style="color:#ae81ff">password</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">MONGODB_DATABASE</span>: <span style="color:#ae81ff">dbname</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">MONGODB_ROOT_PASSWORD</span>: <span style="color:#ae81ff">rootpassword</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;countly:/bitnami&#39;</span>
</span></span></code></pre></div><p>Now let&rsquo;s start with the API part.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create countly api container</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_container</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">countly-api</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">countly/api:19.08.1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">started</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart_policy</span>: <span style="color:#ae81ff">always</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">expose</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">3001</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">etc_hosts</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &#34;db-host&#34;: &#34;{{ I keep mongodb container on a different host and I grab private ip of that host here }}&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      }</span>      
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># what is &amp;w: https://docs.mongodb.com/manual/reference/write-concern/#wc-w</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># change DB-USER, DB-PASSWORD, DB-NAME, DB-PORT according to your mongo credentials</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">COUNTLY_CONFIG_API_MONGODB</span>: <span style="color:#e6db74">&#34;mongodb://DB-USER:DB-PASSWORD@db-host:DB-PORT/DB-NAME?w=1&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">COUNTLY_CONFIG_FRONTEND_MONGODB</span>: <span style="color:#e6db74">&#34;mongodb://DB-HUSER:DB-PASSWORD@db-host:DB-PORT/DB-NAME?w=1&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.enable</span>: <span style="color:#e6db74">&#39;true&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.docker.network</span>: <span style="color:#e6db74">&#39;traefik&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-api.rule</span>: <span style="color:#e6db74">&#39;Host(`countly.mydomain`) &amp;&amp; (PathPrefix(`/i/`, `/o/`) || Path(`/i`, `/o`))&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-api.entrypoints</span>: <span style="color:#e6db74">&#39;websecure&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># we set a higher priority here due to PathPrefix &amp; Path matchers</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-api.priority</span>: <span style="color:#e6db74">&#39;2&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-api.tls.certResolver</span>: <span style="color:#e6db74">&#39;letsencrypt&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.services.balancer-countly-api.loadbalancer.server.port</span>: <span style="color:#e6db74">&#39;3001&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">purge_networks</span>: <span style="color:#66d9ef">yes</span>
</span></span></code></pre></div><p>And here goes our final frontend container.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create countly frontend container</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_container</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">countly-frontend</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">countly/frontend:19.08.1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">started</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart_policy</span>: <span style="color:#ae81ff">always</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">expose</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">6001</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">etc_hosts</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &#34;db-host&#34;: &#34;{{ same here thing; change / remove this }}&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      }</span>      
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">COUNTLY_CONFIG_API_MONGODB</span>: <span style="color:#e6db74">&#34;mongodb://DB-USER:DB-PASSWORD@db-host:DB-PORT/DB-NAME?w=1&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">COUNTLY_CONFIG_FRONTEND_MONGODB</span>: <span style="color:#e6db74">&#34;mongodb://DB-HUSER:DB-PASSWORD@db-host:DB-PORT/DB-NAME?w=1&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.enable</span>: <span style="color:#e6db74">&#39;true&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.docker.network</span>: <span style="color:#e6db74">&#39;traefik&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-http-countly-frontend.rule</span>: <span style="color:#e6db74">&#39;Host(`countly.mydomain`)&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-http-countly-frontend.entrypoints</span>: <span style="color:#e6db74">&#39;web&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-http-countly-frontend.middlewares</span>: <span style="color:#e6db74">&#39;redirect-https@docker&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-frontend.rule</span>: <span style="color:#e6db74">&#39;Host(`countly.mydomain`)&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-frontend.entrypoints</span>: <span style="color:#e6db74">&#39;websecure&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-frontend.priority</span>: <span style="color:#e6db74">&#39;1&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-countly-frontend.tls.certResolver</span>: <span style="color:#e6db74">&#39;letsencrypt&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># If you want to throw some basic auth for your front-end in the mix:</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># traefik.http.middlewares.countly-auth.basicauth.users: &#39;/generate with htpasswd/&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># traefik.http.routers.router-https-countly-frontend.middlewares: &#39;countly-auth@docker&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.services.balancer-countly-frontend.loadbalancer.server.port</span>: <span style="color:#e6db74">&#39;6001&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">purge_networks</span>: <span style="color:#66d9ef">yes</span>
</span></span></code></pre></div><p>And that&rsquo;s it - once you visit <code>countly.mydomain</code> you should be prompted to create an admin account and add your first website.</p>
<figure><img src="/images/countly.png"/><figcaption>
            <h4>Part of countly&#39;s dashboard. Data from one of my projects.</h4>
        </figcaption>
</figure>]]></content:encoded></item><item><title>Terraform, Ansible or both?</title><link>https://awesomeprogrammer.com/blog/2019/10/18/terraform-ansible-or-both/</link><pubDate>Fri, 18 Oct 2019 09:11:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2019/10/18/terraform-ansible-or-both/</guid><description><![CDATA[<p>A few weeks ago I gave <a href="https://www.hashicorp.com/products/terraform">Terraform</a> a try. Ansible was my go-to tool when coming to managing servers for quite some time ago and I loved it for its simplicity and flexibility. So what about Terraform?</p>
<p>Well&hellip; It depends (I wish I could copyright that response :P). I have a feeling that Terraform along with <a href="https://www.packer.io/">Packer</a> (because once you in you have to marry HashiCorp ecosystem) can shine when you have to provision hundreds and hundreds of servers on a single cloud provider. You prepare your base image with Packer, provision it with Ansible, Chef or whatever and use that image to spin more server as you need.</p>
<p>Terraform relies on a state you have to store somewhere (locally or remotely). So if you have the existing infrastructure you have to <em>import</em> the state into terraform which can be cumbersome. AFAIK there is some work in progress that should automate this step, so that might not be an obstacle soon/anymore.</p>
<p>Because it keeps state it can <em>plan</em> changes in your infra. This is a great feature because you can see what will be created/changed/removed <em>globally</em> before deciding to apply the change. Additionally, it allows having some fancy things like cost estimation for AWS services you spin up. At the moment it&rsquo;s a paid feature tho and it gets pricey quite fast ($70/user).</p>
<figure><img src="/images/terraform-io.png"/><figcaption>
            <h4>Terraform.io web UI panel (running jobs view)</h4>
        </figcaption>
</figure>

<p>On the other hand if you use <em>some</em> tool for provisioning / configuring servers later on it gets kinda shady. Suddenly you have two tools that can do <em>almost</em> the same thing. You can use Terraform for managing &lsquo;hard&rsquo; infra - spinning up servers, configuring DNSes, setting up alerting and so on. You will introduce <em>some</em> duplication sooner or later tho or you will end up doing some rather weird templates generation based on terraform run (which gets even more complicated if you rely on official Terraform&rsquo;s remote backend/UI). Because you know - <em>I need to grab that private ip address from the network to configure iptables somewhere</em> etc.</p>
<p>So my advice if you have a handful of servers and your infrastructure doesn&rsquo;t change much and is rather straightforward (old &amp; boring 😉) - just go with Ansible (or whatever tool of your choice), I don&rsquo;t see much of a benefit introducing more complexity in the mix.</p>]]></description><content:encoded><![CDATA[<p>A few weeks ago I gave <a href="https://www.hashicorp.com/products/terraform">Terraform</a> a try. Ansible was my go-to tool when coming to managing servers for quite some time ago and I loved it for its simplicity and flexibility. So what about Terraform?</p>
<p>Well&hellip; It depends (I wish I could copyright that response :P). I have a feeling that Terraform along with <a href="https://www.packer.io/">Packer</a> (because once you in you have to marry HashiCorp ecosystem) can shine when you have to provision hundreds and hundreds of servers on a single cloud provider. You prepare your base image with Packer, provision it with Ansible, Chef or whatever and use that image to spin more server as you need.</p>
<p>Terraform relies on a state you have to store somewhere (locally or remotely). So if you have the existing infrastructure you have to <em>import</em> the state into terraform which can be cumbersome. AFAIK there is some work in progress that should automate this step, so that might not be an obstacle soon/anymore.</p>
<p>Because it keeps state it can <em>plan</em> changes in your infra. This is a great feature because you can see what will be created/changed/removed <em>globally</em> before deciding to apply the change. Additionally, it allows having some fancy things like cost estimation for AWS services you spin up. At the moment it&rsquo;s a paid feature tho and it gets pricey quite fast ($70/user).</p>
<figure><img src="/images/terraform-io.png"/><figcaption>
            <h4>Terraform.io web UI panel (running jobs view)</h4>
        </figcaption>
</figure>

<p>On the other hand if you use <em>some</em> tool for provisioning / configuring servers later on it gets kinda shady. Suddenly you have two tools that can do <em>almost</em> the same thing. You can use Terraform for managing &lsquo;hard&rsquo; infra - spinning up servers, configuring DNSes, setting up alerting and so on. You will introduce <em>some</em> duplication sooner or later tho or you will end up doing some rather weird templates generation based on terraform run (which gets even more complicated if you rely on official Terraform&rsquo;s remote backend/UI). Because you know - <em>I need to grab that private ip address from the network to configure iptables somewhere</em> etc.</p>
<p>So my advice if you have a handful of servers and your infrastructure doesn&rsquo;t change much and is rather straightforward (old &amp; boring 😉) - just go with Ansible (or whatever tool of your choice), I don&rsquo;t see much of a benefit introducing more complexity in the mix.</p>]]></content:encoded></item><item><title>Demystifying Legacy (Rails) Project</title><link>https://awesomeprogrammer.com/blog/2019/10/15/demystifying-legacy-rails-project/</link><pubDate>Tue, 15 Oct 2019 20:37:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2019/10/15/demystifying-legacy-rails-project/</guid><description><![CDATA[<p>I wanted to write this post for a long time. I have first drafts from Q3 of 2018, yet I never had the chance to crystallize my thoughts on that matter. Because how you demystify a legacy project? And not only in Rails but in general? I guess the answer is the same as for many other similar questions - it depends.</p>
<p>Technical debt is unavoidable. One split there, another over there. Your team has a worse day, requirements are unclear, you have no better idea, you&rsquo;re annoyed, frustrated, business requirements changed, you pivoted. Bam, a few months later you wonder who wrote this pile of crap? Oh yeah - it was you. Or blame the other guy/gal. <a href="http://www.commitstrip.com/en/2019/09/02/its-tradition/">Because it&rsquo;s a tradition</a>.</p>
<p>Instead of trying to form my thoughts into nice words I will put a list of bullet points for future references - hoping that at least some of them will be still useful and valid.</p>
<ul>
<li>
<p>are there any long-running pull requests? Is there a chance those will be finished at all? If not close, delete, move on</p>
</li>
<li>
<p>delete old unmerged branches, old issues from a few years back - at this point it&rsquo;s probably just noise. I&rsquo;m not saying some of them are not <em>valid</em>, but the chance is it&rsquo;s just too much information at this point - you will have a chance to get back to it if it&rsquo;s truly important</p>
</li>
<li>
<p>how the process looks like on Trello/Jira/other issue tracking software - does it make sense?</p>
</li>
<li>
<p>do you receive any notifications about deployments/exceptions that are happening in the system?</p>
</li>
<li>
<p>is there some sort of monitoring? At least will you be informed if your app(s) is(are) down?</p>
</li>
<li>
<p>is infrastructure written as code? Is it fragmented? (different PaaS services or even different hosting providers on top) Do you have some documentation? If not start documenting moving parts - it will come in handy later</p>
</li>
<li>
<p>having a test suite is the key; otherwise, it&rsquo;s really hard to move forward. I would start with adding integrations specs (assuming there are none) and take it from there - you will have chance to ask a lot of business-behavior related questions while you&rsquo;re at it</p>
</li>
<li>
<p>at the same time I wouldn&rsquo;t be afraid to remove old, poorly written tests - especially when those tests are testing the implementation instead of the outcome - the only way to deal with it is to rewrite the test itself - and again at the beginning I would favor integration specs; if you&rsquo;re unsure what is safe to delete you can at least separate <em>legacy tests</em> from new one</p>
</li>
<li>
<p>on of the worse thing that you might encounter is some undocumented behavior deeply hidden in the system - some process glued together between two different apps, or some cryptic cron job running in the background that exports excel sheets once a month that was set up 5 years ago - and usually you will find out about it once it breaks; but check crontabs, Heroku schedulers and similar stuff for your own sanity</p>
</li>
<li>
<p>refactor while introducing new features or bugfixes only - in my experience refactoring for the sake of refactoring never works as you will lack clear vision where you want to end up as you have no idea how the whole system is running and is connected at this point</p>
</li>
<li>
<p>accept the fact it will most likely take a freaking long time to get it straight 🤷‍♂️</p>
</li>
</ul>]]></description><content:encoded><![CDATA[<p>I wanted to write this post for a long time. I have first drafts from Q3 of 2018, yet I never had the chance to crystallize my thoughts on that matter. Because how you demystify a legacy project? And not only in Rails but in general? I guess the answer is the same as for many other similar questions - it depends.</p>
<p>Technical debt is unavoidable. One split there, another over there. Your team has a worse day, requirements are unclear, you have no better idea, you&rsquo;re annoyed, frustrated, business requirements changed, you pivoted. Bam, a few months later you wonder who wrote this pile of crap? Oh yeah - it was you. Or blame the other guy/gal. <a href="http://www.commitstrip.com/en/2019/09/02/its-tradition/">Because it&rsquo;s a tradition</a>.</p>
<p>Instead of trying to form my thoughts into nice words I will put a list of bullet points for future references - hoping that at least some of them will be still useful and valid.</p>
<ul>
<li>
<p>are there any long-running pull requests? Is there a chance those will be finished at all? If not close, delete, move on</p>
</li>
<li>
<p>delete old unmerged branches, old issues from a few years back - at this point it&rsquo;s probably just noise. I&rsquo;m not saying some of them are not <em>valid</em>, but the chance is it&rsquo;s just too much information at this point - you will have a chance to get back to it if it&rsquo;s truly important</p>
</li>
<li>
<p>how the process looks like on Trello/Jira/other issue tracking software - does it make sense?</p>
</li>
<li>
<p>do you receive any notifications about deployments/exceptions that are happening in the system?</p>
</li>
<li>
<p>is there some sort of monitoring? At least will you be informed if your app(s) is(are) down?</p>
</li>
<li>
<p>is infrastructure written as code? Is it fragmented? (different PaaS services or even different hosting providers on top) Do you have some documentation? If not start documenting moving parts - it will come in handy later</p>
</li>
<li>
<p>having a test suite is the key; otherwise, it&rsquo;s really hard to move forward. I would start with adding integrations specs (assuming there are none) and take it from there - you will have chance to ask a lot of business-behavior related questions while you&rsquo;re at it</p>
</li>
<li>
<p>at the same time I wouldn&rsquo;t be afraid to remove old, poorly written tests - especially when those tests are testing the implementation instead of the outcome - the only way to deal with it is to rewrite the test itself - and again at the beginning I would favor integration specs; if you&rsquo;re unsure what is safe to delete you can at least separate <em>legacy tests</em> from new one</p>
</li>
<li>
<p>on of the worse thing that you might encounter is some undocumented behavior deeply hidden in the system - some process glued together between two different apps, or some cryptic cron job running in the background that exports excel sheets once a month that was set up 5 years ago - and usually you will find out about it once it breaks; but check crontabs, Heroku schedulers and similar stuff for your own sanity</p>
</li>
<li>
<p>refactor while introducing new features or bugfixes only - in my experience refactoring for the sake of refactoring never works as you will lack clear vision where you want to end up as you have no idea how the whole system is running and is connected at this point</p>
</li>
<li>
<p>accept the fact it will most likely take a freaking long time to get it straight 🤷‍♂️</p>
</li>
</ul>]]></content:encoded></item><item><title>Load balancing docker services with Traefik 2.0</title><link>https://awesomeprogrammer.com/blog/2019/10/14/load-balancing-docker-services-with-traefik-2.0/</link><pubDate>Mon, 14 Oct 2019 17:15:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2019/10/14/load-balancing-docker-services-with-traefik-2.0/</guid><description><![CDATA[<p>I had overall good experience with Traefik 1.x used on Rancher 1.x (before they switched to Kubernetes, rest in peace Cattle container orchestration) and recently I have been trying to migrate my tiny playground infrastructure to Traefik 2.0. Unfortunately, the API is completely different and you have to wrap your head around the new concept how to handle load balancing from scratch.</p>
<p>It seems official documentation is still not there yet so I&rsquo;m gonna post some general examples specifically for load balancing docker services using docker labels alone (without using file configuration at all) - hopefully somebody will find it helpful.</p>
<h3 id="spinning-up-traefik-with-web-ui-with-http-to-https-redirect-letsencrypt-and-basic-auth">Spinning up Traefik with web UI with http to https redirect, letsencrypt and basic auth</h3>
<p>Here is ansible config that you can use almost out of the box (docker compose syntax is very similar tho):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create a named volume</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_volume</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create traefik network</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_network</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create traefik container</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_container</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">traefik:v2.0.2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart_policy</span>: <span style="color:#ae81ff">always</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">api.insecure=true</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">providers.docker</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">providers.docker.exposedbydefault=false</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">entrypoints.web.address=:80</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">entrypoints.websecure.address=:443</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">certificatesresolvers.letsencrypt.acme.httpchallenge=true</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">certificatesresolvers.letsencrypt.acme.email=myemail@email.com</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">accesslog=true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.enable</span>: <span style="color:#e6db74">&#39;true&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.docker.network</span>: <span style="color:#e6db74">&#39;traefik&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.middlewares.redirect-https.redirectscheme.scheme</span>: <span style="color:#e6db74">&#39;https&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.middlewares.redirect-https.redirectscheme.permanent</span>: <span style="color:#e6db74">&#39;true&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-http-traefik.rule</span>: <span style="color:#e6db74">&#39;Host(`traefik.mydomain.com`)&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-http-traefik.entrypoints</span>: <span style="color:#e6db74">&#39;web&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-http-traefik.middlewares</span>: <span style="color:#e6db74">&#39;redirect-https@docker&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.middlewares.auth-traefik.basicauth.users</span>: <span style="color:#e6db74">&#39;test:$apr1$EBRw4S.r$avwoNeITj198OBI8BOsI61&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-traefik.rule</span>: <span style="color:#e6db74">&#39;Host(`traefik.mydomain.com`)&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-traefik.middlewares</span>: <span style="color:#e6db74">&#39;auth-traefik@docker&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-traefik.entrypoints</span>: <span style="color:#e6db74">&#39;websecure&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-traefik.tls.certResolver</span>: <span style="color:#e6db74">&#39;letsencrypt&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.services.balancer-traefik.loadbalancer.server.port</span>: <span style="color:#e6db74">&#39;8080&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/var/run/docker.sock:/var/run/docker.sock</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">traefik:/traefik</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">80</span>:<span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">443</span>:<span style="color:#ae81ff">443</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">purge_networks</span>: <span style="color:#66d9ef">yes</span>
</span></span></code></pre></div><p>So we&rsquo;re putting traefik container using <code>traefik</code> docker network - we will use that network for all of our containers that need to <em>speak</em> with traefik. We also mount traefik <em>volume</em> where traefik will store certificates (so we assume we&rsquo;re running this on a single server, HA setup is a little bit more complex).</p>
<p>Then we spin up the traefik itself:</p>
<ul>
<li>
<p>with api enabled and with ACME provider declared (that is: letsencrypt) - <code>certificatesresolvers</code> labels are responsible for that and <code>letsencrypt</code> is our custom name (it can be anything)</p>
</li>
<li>
<p>with two default <code>entrypoints</code> (we name them <code>web</code> and <code>websecure</code> and assign corresponding ports <code>80</code> and <code>443</code>)</p>
</li>
<li>
<p>we also enable <code>accesslog</code> (in case you don&rsquo;t need it feel free to disable it, but might be handy when trying to debug some potential issues)</p>
</li>
</ul>
<p>The interesting parts are the labels - you notice a lot of <code>routers</code>, <code>middlewares</code> and <code>services</code> - and if you look closely it all comes in play together.</p>
<p>We define <code>redirect-https</code> middleware and then use it in our <code>router-http-traefik</code> router - we redirect all traffic to <code>https</code>.</p>
<p><code>auth-traefik</code> middleware (<code>test</code> user &amp; password) is being used our <code>router-https-traefik</code> router. Loadbalancer - <code>balancer-traefik</code> (again this is a custom name and it needs to be unique across your stack - you can re-use middleware names tho) points traffic to traefik&rsquo;s <code>8080</code> port (web dashboard).</p>
<p>For sake of completion of this example let&rsquo;s add another service:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create Docker Container</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_container</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">other-app</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">image-of-my-app</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">started</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart_policy</span>: <span style="color:#ae81ff">always</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pull</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.enable</span>: <span style="color:#e6db74">&#39;true&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.docker.network</span>: <span style="color:#e6db74">&#39;traefik&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.myother-router.rule</span>: <span style="color:#e6db74">&#39;Host(`mydomain.com`, `subdomain.mydomain.com`, `otherdomain.com`)&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.myother-router.entrypoints</span>: <span style="color:#e6db74">&#39;web&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.myother-router.middlewares</span>: <span style="color:#e6db74">&#39;redirect-https@docker&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.myother-https-router.rule</span>: <span style="color:#e6db74">&#39;Host(`mydomain.com`, `subdomain.mydomain.com`, `otherdomain.com`)&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.myother-https-router.entrypoints</span>: <span style="color:#e6db74">&#39;websecure&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.myother-https-router.tls.certResolver</span>: <span style="color:#e6db74">&#39;letsencrypt&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.services.myother-balancer.loadbalancer.server.port</span>: <span style="color:#e6db74">&#39;8080&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">purge_networks</span>: <span style="color:#66d9ef">yes</span>
</span></span></code></pre></div><p>Here we configure another simple service that should be accessible on 3 different addresses - and whole traffic should be redirected to https. As you see we re-used <code>redirect-https@docker</code> middleware declaration that was defined on traefik container itself and it should just work. The rest is very similar - we specify we want to use cert resolver and balance traffic to port <code>8080</code>.</p>
<p>Assuming everything is configured as it should you should be able to access traefik&rsquo;s panel via <code>traefik.mydomain.com</code> and see your services.</p>
<figure><img src="/images/traefik.png"/><figcaption>
            <h4>Traefik 2.x Dashboard</h4>
        </figcaption>
</figure>

<p>Overall I suggest taking a deep dive <a href="https://docs.traefik.io">into official documentation</a> and happy traefiking! If you notice any issues with instructions above please lemme know as I&rsquo;m still in process of migrating my stuff as well ;).</p>]]></description><content:encoded><![CDATA[<p>I had overall good experience with Traefik 1.x used on Rancher 1.x (before they switched to Kubernetes, rest in peace Cattle container orchestration) and recently I have been trying to migrate my tiny playground infrastructure to Traefik 2.0. Unfortunately, the API is completely different and you have to wrap your head around the new concept how to handle load balancing from scratch.</p>
<p>It seems official documentation is still not there yet so I&rsquo;m gonna post some general examples specifically for load balancing docker services using docker labels alone (without using file configuration at all) - hopefully somebody will find it helpful.</p>
<h3 id="spinning-up-traefik-with-web-ui-with-http-to-https-redirect-letsencrypt-and-basic-auth">Spinning up Traefik with web UI with http to https redirect, letsencrypt and basic auth</h3>
<p>Here is ansible config that you can use almost out of the box (docker compose syntax is very similar tho):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create a named volume</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_volume</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create traefik network</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_network</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create traefik container</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_container</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">traefik:v2.0.2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart_policy</span>: <span style="color:#ae81ff">always</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">api.insecure=true</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">providers.docker</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">providers.docker.exposedbydefault=false</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">entrypoints.web.address=:80</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">entrypoints.websecure.address=:443</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">certificatesresolvers.letsencrypt.acme.httpchallenge=true</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">certificatesresolvers.letsencrypt.acme.email=myemail@email.com</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">accesslog=true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.enable</span>: <span style="color:#e6db74">&#39;true&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.docker.network</span>: <span style="color:#e6db74">&#39;traefik&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.middlewares.redirect-https.redirectscheme.scheme</span>: <span style="color:#e6db74">&#39;https&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.middlewares.redirect-https.redirectscheme.permanent</span>: <span style="color:#e6db74">&#39;true&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-http-traefik.rule</span>: <span style="color:#e6db74">&#39;Host(`traefik.mydomain.com`)&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-http-traefik.entrypoints</span>: <span style="color:#e6db74">&#39;web&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-http-traefik.middlewares</span>: <span style="color:#e6db74">&#39;redirect-https@docker&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.middlewares.auth-traefik.basicauth.users</span>: <span style="color:#e6db74">&#39;test:$apr1$EBRw4S.r$avwoNeITj198OBI8BOsI61&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-traefik.rule</span>: <span style="color:#e6db74">&#39;Host(`traefik.mydomain.com`)&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-traefik.middlewares</span>: <span style="color:#e6db74">&#39;auth-traefik@docker&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-traefik.entrypoints</span>: <span style="color:#e6db74">&#39;websecure&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.router-https-traefik.tls.certResolver</span>: <span style="color:#e6db74">&#39;letsencrypt&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.services.balancer-traefik.loadbalancer.server.port</span>: <span style="color:#e6db74">&#39;8080&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/var/run/docker.sock:/var/run/docker.sock</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">traefik:/traefik</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">80</span>:<span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">443</span>:<span style="color:#ae81ff">443</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">purge_networks</span>: <span style="color:#66d9ef">yes</span>
</span></span></code></pre></div><p>So we&rsquo;re putting traefik container using <code>traefik</code> docker network - we will use that network for all of our containers that need to <em>speak</em> with traefik. We also mount traefik <em>volume</em> where traefik will store certificates (so we assume we&rsquo;re running this on a single server, HA setup is a little bit more complex).</p>
<p>Then we spin up the traefik itself:</p>
<ul>
<li>
<p>with api enabled and with ACME provider declared (that is: letsencrypt) - <code>certificatesresolvers</code> labels are responsible for that and <code>letsencrypt</code> is our custom name (it can be anything)</p>
</li>
<li>
<p>with two default <code>entrypoints</code> (we name them <code>web</code> and <code>websecure</code> and assign corresponding ports <code>80</code> and <code>443</code>)</p>
</li>
<li>
<p>we also enable <code>accesslog</code> (in case you don&rsquo;t need it feel free to disable it, but might be handy when trying to debug some potential issues)</p>
</li>
</ul>
<p>The interesting parts are the labels - you notice a lot of <code>routers</code>, <code>middlewares</code> and <code>services</code> - and if you look closely it all comes in play together.</p>
<p>We define <code>redirect-https</code> middleware and then use it in our <code>router-http-traefik</code> router - we redirect all traffic to <code>https</code>.</p>
<p><code>auth-traefik</code> middleware (<code>test</code> user &amp; password) is being used our <code>router-https-traefik</code> router. Loadbalancer - <code>balancer-traefik</code> (again this is a custom name and it needs to be unique across your stack - you can re-use middleware names tho) points traffic to traefik&rsquo;s <code>8080</code> port (web dashboard).</p>
<p>For sake of completion of this example let&rsquo;s add another service:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create Docker Container</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">docker_container</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">other-app</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">image-of-my-app</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">started</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart_policy</span>: <span style="color:#ae81ff">always</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pull</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.enable</span>: <span style="color:#e6db74">&#39;true&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.docker.network</span>: <span style="color:#e6db74">&#39;traefik&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.myother-router.rule</span>: <span style="color:#e6db74">&#39;Host(`mydomain.com`, `subdomain.mydomain.com`, `otherdomain.com`)&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.myother-router.entrypoints</span>: <span style="color:#e6db74">&#39;web&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.myother-router.middlewares</span>: <span style="color:#e6db74">&#39;redirect-https@docker&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.myother-https-router.rule</span>: <span style="color:#e6db74">&#39;Host(`mydomain.com`, `subdomain.mydomain.com`, `otherdomain.com`)&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.myother-https-router.entrypoints</span>: <span style="color:#e6db74">&#39;websecure&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.routers.myother-https-router.tls.certResolver</span>: <span style="color:#e6db74">&#39;letsencrypt&#39;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.http.services.myother-balancer.loadbalancer.server.port</span>: <span style="color:#e6db74">&#39;8080&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">traefik</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">purge_networks</span>: <span style="color:#66d9ef">yes</span>
</span></span></code></pre></div><p>Here we configure another simple service that should be accessible on 3 different addresses - and whole traffic should be redirected to https. As you see we re-used <code>redirect-https@docker</code> middleware declaration that was defined on traefik container itself and it should just work. The rest is very similar - we specify we want to use cert resolver and balance traffic to port <code>8080</code>.</p>
<p>Assuming everything is configured as it should you should be able to access traefik&rsquo;s panel via <code>traefik.mydomain.com</code> and see your services.</p>
<figure><img src="/images/traefik.png"/><figcaption>
            <h4>Traefik 2.x Dashboard</h4>
        </figcaption>
</figure>

<p>Overall I suggest taking a deep dive <a href="https://docs.traefik.io">into official documentation</a> and happy traefiking! If you notice any issues with instructions above please lemme know as I&rsquo;m still in process of migrating my stuff as well ;).</p>]]></content:encoded></item><item><title>How to become Linkedin specialist in two simple steps - check out ready to use template</title><link>https://awesomeprogrammer.com/blog/2019/09/30/how-to-become-linkedin-specialist-in-two-simple-steps-check-out-ready-to-use-template/</link><pubDate>Mon, 30 Sep 2019 19:54:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2019/09/30/how-to-become-linkedin-specialist-in-two-simple-steps-check-out-ready-to-use-template/</guid><description><![CDATA[<p>The template below can be used by all experts, technology advocates, visionaries, founders, owners, trainers, CEOs and speakers that are trying to build up their audience on Linkedin. Feel free to use it and share it with your colleagues and coworkers.</p>
<hr>
<p>/some obvious statement goes here; alternatively semi-motivational quote if you&rsquo;re feeling like it/</p>
<p>Agree? /this is the crucial part - you need to end your post with a question!/</p>
<hr>
<p>Success almost guaranteed. No warranty. MIT license.</p>]]></description><content:encoded><![CDATA[<p>The template below can be used by all experts, technology advocates, visionaries, founders, owners, trainers, CEOs and speakers that are trying to build up their audience on Linkedin. Feel free to use it and share it with your colleagues and coworkers.</p>
<hr>
<p>/some obvious statement goes here; alternatively semi-motivational quote if you&rsquo;re feeling like it/</p>
<p>Agree? /this is the crucial part - you need to end your post with a question!/</p>
<hr>
<p>Success almost guaranteed. No warranty. MIT license.</p>]]></content:encoded></item><item><title>Resolving - there is not enough disk space available to install the product issue (XCode/MacOS)</title><link>https://awesomeprogrammer.com/blog/2019/09/22/resolving-there-is-not-enough-disk-space-available-to-install-the-product-issue-xcode/macos/</link><pubDate>Sun, 22 Sep 2019 07:16:45 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2019/09/22/resolving-there-is-not-enough-disk-space-available-to-install-the-product-issue-xcode/macos/</guid><description><![CDATA[<p>Apparently MacOS doesn&rsquo;t free up disk space right away and overall it&rsquo;s more complicated process. <a href="https://apple.stackexchange.com/questions/254676/how-do-i-clear-the-purgeable-area-on-my-disk">Reference</a>. This causes error in AppStore when trying to upgrade bigger in size apps like XCode.</p>
<p>If you go to <strong>Disk Utility</strong> you should see <strong>Available space</strong> and amount of purgeable space just near it.</p>
<p>To force disk space purge you need to fill up your free disk space first. Here is tldr version of <a href="http://osxdaily.com/2013/05/31/create-large-file-mac-os-x/">this tutorial</a>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># go to any directory with write access</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 10g = 10GB file; adjust according to your needs</span>
</span></span><span style="display:flex;"><span>mkfile -n 10g largefile
</span></span><span style="display:flex;"><span>rm largefile
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># check out disk utility</span>
</span></span></code></pre></div>]]></description><content:encoded><![CDATA[<p>Apparently MacOS doesn&rsquo;t free up disk space right away and overall it&rsquo;s more complicated process. <a href="https://apple.stackexchange.com/questions/254676/how-do-i-clear-the-purgeable-area-on-my-disk">Reference</a>. This causes error in AppStore when trying to upgrade bigger in size apps like XCode.</p>
<p>If you go to <strong>Disk Utility</strong> you should see <strong>Available space</strong> and amount of purgeable space just near it.</p>
<p>To force disk space purge you need to fill up your free disk space first. Here is tldr version of <a href="http://osxdaily.com/2013/05/31/create-large-file-mac-os-x/">this tutorial</a>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># go to any directory with write access</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 10g = 10GB file; adjust according to your needs</span>
</span></span><span style="display:flex;"><span>mkfile -n 10g largefile
</span></span><span style="display:flex;"><span>rm largefile
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># check out disk utility</span>
</span></span></code></pre></div>]]></content:encoded></item><item><title>Publish-subscribe pattern on Rails with wisper</title><link>https://awesomeprogrammer.com/blog/2019/09/21/publish-subscribe-pattern-on-rails-with-wisper/</link><pubDate>Sat, 21 Sep 2019 12:13:05 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2019/09/21/publish-subscribe-pattern-on-rails-with-wisper/</guid><description><![CDATA[<p>I&rsquo;m gonna skip introduction about <a href="https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern">what is publish-subcribe patter</a>, <a href="https://github.com/krisleech/wisper/wiki">what wisper is</a> or <a href="https://www.sitepoint.com/using-wisper-to-decompose-applications/">how to use it</a> and get straight to the real-life example.</p>
<p>Let&rsquo;s say you have a controller action that for some business-related reasons have to handle 7 different use-cases - for example we have to support different kind of logic for our legacy users that have some sort old(er) subscription model, on top of which we have some new subscription models and of course we need to handle some generic failures.</p>
<p>The complexity has to go somewhere - you might decide to split it on <em>entrypoint</em> (maybe split your controller action altogether, but that also have some downsides) - but let&rsquo;s say you ended up with one bulky action where you have some sort of service class (workflow, action or whatever the modern term for those are nowadays) that does the business logic.</p>
<p>The tricky part begins when you have to add some business checks on top of your service class. So your <em>main</em> service class is doing some business logic but you probably don&rsquo;t want to break single responsibility principle and put a bunch of if-checks on top of that. In such cases I like to introduce <em>bouncer</em> class that requires <em>filters</em>, here is an example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># This is simplified example, in real-life project I ended  up with some</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># very simple inheritance as I needed two different kind of bouncers with different</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># set of predefined filters (use dependency injection at will)</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Bouncer</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">Wisper</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Publisher</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">DEFAULT_FILTERS</span> <span style="color:#f92672">=</span> <span style="color:#f92672">[</span><span style="color:#66d9ef">Bouncer</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Filters</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Subscription</span><span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># don&#39;t worry what user and job is, this is just an example</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">initialize</span>(user, job, <span style="color:#f92672">&amp;</span>block)
</span></span><span style="display:flex;"><span>    @user  <span style="color:#f92672">=</span> user
</span></span><span style="display:flex;"><span>    @job   <span style="color:#f92672">=</span> job
</span></span><span style="display:flex;"><span>    @block <span style="color:#f92672">=</span> block
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">call</span>(filters <span style="color:#f92672">=</span> <span style="color:#66d9ef">DEFAULT_FILTERS</span>)
</span></span><span style="display:flex;"><span>    filters<span style="color:#f92672">.</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>klass<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>      filter <span style="color:#f92672">=</span> klass<span style="color:#f92672">.</span>new(user, job)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># here our nice early return, looks clean &amp; elegant</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> broadcast(<span style="color:#f92672">*</span>filter<span style="color:#f92672">.</span>event) <span style="color:#66d9ef">if</span> filter<span style="color:#f92672">.</span>apply?
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    block<span style="color:#f92672">.</span>call
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">private</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">attr_reader</span> <span style="color:#e6db74">:user</span>, <span style="color:#e6db74">:job</span>, <span style="color:#e6db74">:block</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># And this is base class for the filter itself - as stated above</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#  I ended up with very simple inheritance</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Bouncer</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Filters</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">initialize</span>(user, job)
</span></span><span style="display:flex;"><span>    @user <span style="color:#f92672">=</span> user
</span></span><span style="display:flex;"><span>    @job  <span style="color:#f92672">=</span> job
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">apply?</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">raise</span> <span style="color:#66d9ef">NotImplementedError</span>, <span style="color:#f92672">&lt;&lt;~</span><span style="color:#66d9ef">INFO</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">Please</span> implement method that will <span style="color:#66d9ef">return</span> boolean
</span></span><span style="display:flex;"><span>      regarding <span style="color:#66d9ef">if</span> that particular filter should be applied
</span></span><span style="display:flex;"><span>      once it<span style="color:#960050;background-color:#1e0010">&#39;</span>s applies bouncer will use <span style="color:#e6db74">`event`</span> method to broadcast
</span></span><span style="display:flex;"><span>      information up to controller
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">INFO</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">event</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">raise</span> <span style="color:#66d9ef">NotImplementedError</span>, <span style="color:#f92672">&lt;&lt;~</span><span style="color:#66d9ef">INFO</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">Please</span> implement method that will be used <span style="color:#66d9ef">for</span>
</span></span><span style="display:flex;"><span>      broadcasting information up to controller
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">INFO</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">private</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">attr_reader</span> <span style="color:#e6db74">:user</span>, <span style="color:#e6db74">:job</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># Here is sample implementation of the filter class</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># By itself it doesn&#39;t do much, but publishing event</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># On failure gives us nice flexibility in terms how</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># we want o react for given even - read further</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Bouncer</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Filters</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Subscription</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">Bouncer</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Filters</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">apply?</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">!</span>subscription <span style="color:#f92672">||</span> subscription<span style="color:#f92672">.</span>expired?
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">event</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">[</span><span style="color:#e6db74">:invalid_subscription</span><span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">private</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ... get subscription and possibly more logic</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>By following this pattern we have a possibility to extend it further and further without polluting our existing classes - we can easily add new filters <em>and</em> react to business events. We can even hook in different subscribers for both (service class and bouncer) classes. Let&rsquo;s take a look.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">create</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># let&#39;s not dig into business details here, assume service</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># is doing our &#39;main&#39; business thing and subscribers are</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># handling potential side effects (on success or failure)</span>
</span></span><span style="display:flex;"><span>  service <span style="color:#f92672">=</span> <span style="color:#66d9ef">ContactRequest</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Submit</span><span style="color:#f92672">.</span>new(current_user, job)
</span></span><span style="display:flex;"><span>  service<span style="color:#f92672">.</span>subscribe(<span style="color:#66d9ef">Subscribers</span><span style="color:#f92672">::</span><span style="color:#66d9ef">NewContactRequestNotification</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># we can attach subscribers to our main service class and</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># attach subscribers to our subscribers even further</span>
</span></span><span style="display:flex;"><span>  credit_reducer <span style="color:#f92672">=</span> <span style="color:#66d9ef">Subscribers</span><span style="color:#f92672">::</span><span style="color:#66d9ef">CreditReduction</span><span style="color:#f92672">.</span>new(current_user, job)
</span></span><span style="display:flex;"><span>  credit_reducer<span style="color:#f92672">.</span>subscribe(<span style="color:#66d9ef">Subscribers</span><span style="color:#f92672">::</span><span style="color:#66d9ef">CreditNotificationChecker</span>)
</span></span><span style="display:flex;"><span>  service<span style="color:#f92672">.</span>subscribe(credit_reducer)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># we wrap our service inside our &#39;bouncer&#39; class, so basically mimic</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># before_action behavior without polluting controller</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># In real-life scenario I ended up with extra factory class here</span>
</span></span><span style="display:flex;"><span>  bouncer <span style="color:#f92672">=</span> <span style="color:#66d9ef">Bouncer</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span><span style="color:#f92672">.</span>new(current_user, job) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    service<span style="color:#f92672">.</span>call(contact_request_params)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># we want to react to bouncer event for some reason?</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Hook in analytics or marketing events? No problem there</span>
</span></span><span style="display:flex;"><span>  bouncer<span style="color:#f92672">.</span>subscribe(<span style="color:#66d9ef">Subscribers</span><span style="color:#f92672">::</span><span style="color:#66d9ef">ParticipationDeclinedEvent</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  service<span style="color:#f92672">.</span>on(<span style="color:#e6db74">:success</span>) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    redirect_back <span style="color:#e6db74">fallback_location</span>: <span style="color:#e6db74">notice</span>: <span style="color:#e6db74">&#39;Handle success&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  service<span style="color:#f92672">.</span>on(<span style="color:#e6db74">:failure</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>contact_request, job<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>    redirect_to job_path(job), <span style="color:#e6db74">flash</span>: { <span style="color:#e6db74">error</span>: <span style="color:#e6db74">&#39;Respond with some errors maybe?&#39;</span> }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Handle our &#39;bounced&#39; actions</span>
</span></span><span style="display:flex;"><span>  bouncer<span style="color:#f92672">.</span>on(<span style="color:#e6db74">:invalid_subscription</span>) { redirect_to subscription_path, <span style="color:#e6db74">notice</span>: <span style="color:#e6db74">&#39;Add some meaningful message?&#39;</span> }
</span></span><span style="display:flex;"><span>  bouncer<span style="color:#f92672">.</span>on(<span style="color:#e6db74">:restricted_participation</span>) { redirect_to job_limited_participation_path(job) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  bouncer<span style="color:#f92672">.</span>call
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>This way we end up with loosely coupled code, the main downside, on the other hand, is that you need pretty good code coverage when following such pattern, otherwise you might end up with pretty green unit tests, yet broken business logic. <a href="https://github.com/krisleech/wisper/issues/137">See github discussion here</a>.</p>
<p>Either way, I recommend giving it a try, maybe such approach will suit your needs!</p>]]></description><content:encoded><![CDATA[<p>I&rsquo;m gonna skip introduction about <a href="https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern">what is publish-subcribe patter</a>, <a href="https://github.com/krisleech/wisper/wiki">what wisper is</a> or <a href="https://www.sitepoint.com/using-wisper-to-decompose-applications/">how to use it</a> and get straight to the real-life example.</p>
<p>Let&rsquo;s say you have a controller action that for some business-related reasons have to handle 7 different use-cases - for example we have to support different kind of logic for our legacy users that have some sort old(er) subscription model, on top of which we have some new subscription models and of course we need to handle some generic failures.</p>
<p>The complexity has to go somewhere - you might decide to split it on <em>entrypoint</em> (maybe split your controller action altogether, but that also have some downsides) - but let&rsquo;s say you ended up with one bulky action where you have some sort of service class (workflow, action or whatever the modern term for those are nowadays) that does the business logic.</p>
<p>The tricky part begins when you have to add some business checks on top of your service class. So your <em>main</em> service class is doing some business logic but you probably don&rsquo;t want to break single responsibility principle and put a bunch of if-checks on top of that. In such cases I like to introduce <em>bouncer</em> class that requires <em>filters</em>, here is an example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># This is simplified example, in real-life project I ended  up with some</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># very simple inheritance as I needed two different kind of bouncers with different</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># set of predefined filters (use dependency injection at will)</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Bouncer</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">Wisper</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Publisher</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">DEFAULT_FILTERS</span> <span style="color:#f92672">=</span> <span style="color:#f92672">[</span><span style="color:#66d9ef">Bouncer</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Filters</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Subscription</span><span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># don&#39;t worry what user and job is, this is just an example</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">initialize</span>(user, job, <span style="color:#f92672">&amp;</span>block)
</span></span><span style="display:flex;"><span>    @user  <span style="color:#f92672">=</span> user
</span></span><span style="display:flex;"><span>    @job   <span style="color:#f92672">=</span> job
</span></span><span style="display:flex;"><span>    @block <span style="color:#f92672">=</span> block
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">call</span>(filters <span style="color:#f92672">=</span> <span style="color:#66d9ef">DEFAULT_FILTERS</span>)
</span></span><span style="display:flex;"><span>    filters<span style="color:#f92672">.</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>klass<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>      filter <span style="color:#f92672">=</span> klass<span style="color:#f92672">.</span>new(user, job)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># here our nice early return, looks clean &amp; elegant</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> broadcast(<span style="color:#f92672">*</span>filter<span style="color:#f92672">.</span>event) <span style="color:#66d9ef">if</span> filter<span style="color:#f92672">.</span>apply?
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    block<span style="color:#f92672">.</span>call
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">private</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">attr_reader</span> <span style="color:#e6db74">:user</span>, <span style="color:#e6db74">:job</span>, <span style="color:#e6db74">:block</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># And this is base class for the filter itself - as stated above</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#  I ended up with very simple inheritance</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Bouncer</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Filters</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">initialize</span>(user, job)
</span></span><span style="display:flex;"><span>    @user <span style="color:#f92672">=</span> user
</span></span><span style="display:flex;"><span>    @job  <span style="color:#f92672">=</span> job
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">apply?</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">raise</span> <span style="color:#66d9ef">NotImplementedError</span>, <span style="color:#f92672">&lt;&lt;~</span><span style="color:#66d9ef">INFO</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">Please</span> implement method that will <span style="color:#66d9ef">return</span> boolean
</span></span><span style="display:flex;"><span>      regarding <span style="color:#66d9ef">if</span> that particular filter should be applied
</span></span><span style="display:flex;"><span>      once it<span style="color:#960050;background-color:#1e0010">&#39;</span>s applies bouncer will use <span style="color:#e6db74">`event`</span> method to broadcast
</span></span><span style="display:flex;"><span>      information up to controller
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">INFO</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">event</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">raise</span> <span style="color:#66d9ef">NotImplementedError</span>, <span style="color:#f92672">&lt;&lt;~</span><span style="color:#66d9ef">INFO</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">Please</span> implement method that will be used <span style="color:#66d9ef">for</span>
</span></span><span style="display:flex;"><span>      broadcasting information up to controller
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">INFO</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">private</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">attr_reader</span> <span style="color:#e6db74">:user</span>, <span style="color:#e6db74">:job</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># Here is sample implementation of the filter class</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># By itself it doesn&#39;t do much, but publishing event</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># On failure gives us nice flexibility in terms how</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># we want o react for given even - read further</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Bouncer</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Filters</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Subscription</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">Bouncer</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Filters</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">apply?</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">!</span>subscription <span style="color:#f92672">||</span> subscription<span style="color:#f92672">.</span>expired?
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">event</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">[</span><span style="color:#e6db74">:invalid_subscription</span><span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">private</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ... get subscription and possibly more logic</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>By following this pattern we have a possibility to extend it further and further without polluting our existing classes - we can easily add new filters <em>and</em> react to business events. We can even hook in different subscribers for both (service class and bouncer) classes. Let&rsquo;s take a look.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">create</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># let&#39;s not dig into business details here, assume service</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># is doing our &#39;main&#39; business thing and subscribers are</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># handling potential side effects (on success or failure)</span>
</span></span><span style="display:flex;"><span>  service <span style="color:#f92672">=</span> <span style="color:#66d9ef">ContactRequest</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Submit</span><span style="color:#f92672">.</span>new(current_user, job)
</span></span><span style="display:flex;"><span>  service<span style="color:#f92672">.</span>subscribe(<span style="color:#66d9ef">Subscribers</span><span style="color:#f92672">::</span><span style="color:#66d9ef">NewContactRequestNotification</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># we can attach subscribers to our main service class and</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># attach subscribers to our subscribers even further</span>
</span></span><span style="display:flex;"><span>  credit_reducer <span style="color:#f92672">=</span> <span style="color:#66d9ef">Subscribers</span><span style="color:#f92672">::</span><span style="color:#66d9ef">CreditReduction</span><span style="color:#f92672">.</span>new(current_user, job)
</span></span><span style="display:flex;"><span>  credit_reducer<span style="color:#f92672">.</span>subscribe(<span style="color:#66d9ef">Subscribers</span><span style="color:#f92672">::</span><span style="color:#66d9ef">CreditNotificationChecker</span>)
</span></span><span style="display:flex;"><span>  service<span style="color:#f92672">.</span>subscribe(credit_reducer)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># we wrap our service inside our &#39;bouncer&#39; class, so basically mimic</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># before_action behavior without polluting controller</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># In real-life scenario I ended up with extra factory class here</span>
</span></span><span style="display:flex;"><span>  bouncer <span style="color:#f92672">=</span> <span style="color:#66d9ef">Bouncer</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span><span style="color:#f92672">.</span>new(current_user, job) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    service<span style="color:#f92672">.</span>call(contact_request_params)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># we want to react to bouncer event for some reason?</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Hook in analytics or marketing events? No problem there</span>
</span></span><span style="display:flex;"><span>  bouncer<span style="color:#f92672">.</span>subscribe(<span style="color:#66d9ef">Subscribers</span><span style="color:#f92672">::</span><span style="color:#66d9ef">ParticipationDeclinedEvent</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  service<span style="color:#f92672">.</span>on(<span style="color:#e6db74">:success</span>) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    redirect_back <span style="color:#e6db74">fallback_location</span>: <span style="color:#e6db74">notice</span>: <span style="color:#e6db74">&#39;Handle success&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  service<span style="color:#f92672">.</span>on(<span style="color:#e6db74">:failure</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>contact_request, job<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>    redirect_to job_path(job), <span style="color:#e6db74">flash</span>: { <span style="color:#e6db74">error</span>: <span style="color:#e6db74">&#39;Respond with some errors maybe?&#39;</span> }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Handle our &#39;bounced&#39; actions</span>
</span></span><span style="display:flex;"><span>  bouncer<span style="color:#f92672">.</span>on(<span style="color:#e6db74">:invalid_subscription</span>) { redirect_to subscription_path, <span style="color:#e6db74">notice</span>: <span style="color:#e6db74">&#39;Add some meaningful message?&#39;</span> }
</span></span><span style="display:flex;"><span>  bouncer<span style="color:#f92672">.</span>on(<span style="color:#e6db74">:restricted_participation</span>) { redirect_to job_limited_participation_path(job) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  bouncer<span style="color:#f92672">.</span>call
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>This way we end up with loosely coupled code, the main downside, on the other hand, is that you need pretty good code coverage when following such pattern, otherwise you might end up with pretty green unit tests, yet broken business logic. <a href="https://github.com/krisleech/wisper/issues/137">See github discussion here</a>.</p>
<p>Either way, I recommend giving it a try, maybe such approach will suit your needs!</p>]]></content:encoded></item><item><title>How to migrate your old Synology to new Synology</title><link>https://awesomeprogrammer.com/blog/2019/03/09/how-to-migrate-your-old-synology-to-new-synology/</link><pubDate>Sat, 09 Mar 2019 15:54:01 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2019/03/09/how-to-migrate-your-old-synology-to-new-synology/</guid><description><![CDATA[<p>Recently I have upgraded my good old Synology DS411slim to newer, bigger model - DS918+. I kinda started to run out of free space (1TB 2.5&quot; x4) and decided to invest some money into a new box and new drives.</p>
<p><a href="https://www.synology.com/en-us/knowledgebase/DSM/tutorial/General_Setup/How_to_migrate_between_Synology_NAS_DSM_6_0_and_later">There is an official guide</a> how you can migrate with disk swipe, but as I wanted to move my data to new drives I just decided I would try to <code>rsync</code> everything. And BTW there is a <code>screen</code> command if you would like to run in the background that can be found on <a href="https://synocommunity.com/package/synocli-net">synocommunity</a>.</p>
<p>Long story short I had a poor experience with rsync - tweaking settings on both ends (disabling compression, tinkering with used crypto algo and so on) yielded rather disappointing results - transfer up to 10MB/s and almost 100% CPU utilization on my old NAS.</p>
<p>Weirdly enough I had much better luck with simply mounting CIFS resources in my new nas from my old nas directly in DistStation File Manager (<code>Tools</code> -&gt; <code>Mount Remote Folder</code> -&gt; <code>CIFS shared folder</code>). That doubled transfer speed and lowered CPU usage ~2x times. So far so good.</p>
<p>Aside from standard packages like PhotoStation (and btw if you move your <code>photos</code> directory it should <em>just work</em>) I also had <a href="https://github.com/sickchill/sickchill">SickChill</a> (previously SickRage and before that SickBeard as apparently there was some fight over the project) and <a href="https://transmissionbt.com/">transmission</a> that I wanted to move.</p>
<p>My goto solution were packages from synocommunity, but I noticed my new box have great support for Docker - Synology provides an official Docker package. So I decided to give it a try.</p>
<h3 id="new-stack">New stack</h3>
<p>After some research I ended up with <a href="https://github.com/pymedusa/Medusa/">Medusa</a> (as replacement for SickChill), <a href="https://github.com/Radarr/Radarr">Radarr</a> (for managing movies collection), <a href="https://github.com/Jackett/Jackett">Jackett</a> (that acts as proxy between medusa and radarr and various private torrent trackers) and <a href="https://github.com/Novik/ruTorrent">rutorrent</a> (as replacement for transmission as radarr don&rsquo;t have unpack rar archives capabilities).</p>
<p>Hopefully for me there is a <a href="https://www.linuxserver.io">linuxserver</a> community that released all the images that I needed, so basically half the work as done. Opensource ftw 💪.</p>
<p>I&rsquo;m using three virtual volumes on my synology:</p>
<ul>
<li><code>volume1</code> for various stuff - there I will create a docker directory to keep all the configs, dbs and so on</li>
<li><code>volume2</code> where I have my torrent (downloads) directory</li>
<li><code>volume3</code> where I have my tv/movies collection</li>
</ul>
<p>Sample docker compose will look something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#f92672">version</span>: <span style="color:#e6db74">&#34;2&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">radarr</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">linuxserver/radarr</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">radarr</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PUID=1024</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PGID=100</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">TZ=Europe/Warsaw</span> <span style="color:#75715e"># or whatever your timezone is</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume1/docker/radarr:/config</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume3/video/movies:/movies</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume2/torrent:/downloads</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">7878</span>:<span style="color:#ae81ff">7878</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">links</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">jackett</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">rutorrent</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mem_limit</span>: <span style="color:#ae81ff">1g</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">jackett</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">linuxserver/jackett</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">jackett</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PUID=1024</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PGID=100</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">TZ=Europe/Warsaw</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume1/docker/jackett:/config</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">9117</span>:<span style="color:#ae81ff">9117</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mem_limit</span>: <span style="color:#ae81ff">512mb</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rutorrent</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">linuxserver/rutorrent</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">rutorrent</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PUID=1024</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PGID=100</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume1/docker/rutorrent:/config</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume2/torrent:/downloads</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">9091</span>:<span style="color:#ae81ff">80</span> <span style="color:#75715e"># so you can access rutorrent on port 9091 on your synology</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">5050</span>:<span style="color:#ae81ff">5000</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">51413</span>:<span style="color:#ae81ff">51413</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">6881</span>:<span style="color:#ae81ff">6881</span><span style="color:#ae81ff">/udp</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mem_limit</span>: <span style="color:#ae81ff">512mb</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">medusa</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">linuxserver/medusa</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">medusa</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">links</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">jackett</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">rutorrent</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PUID=1024</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PGID=100</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">TZ=Europe/Warsaw</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume1/docker/medusa:/config</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume2/torrent:/downloads</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume3/video/tv:/tv</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">8081</span>:<span style="color:#ae81ff">8081</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mem_limit</span>: <span style="color:#ae81ff">1g</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span></code></pre></div><p>Notes:</p>
<ul>
<li>
<p><code>PUID</code> is user ID and <code>PGID</code> is group ID <em>from</em> synology system (just ssh into you machine and check <code>id</code> of your designated user), otherwise mounted directories will be owned by <code>root</code> and this is not what you usually want</p>
</li>
<li>
<p>if you notice <code>NotImplementedError</code> errors in logs in Medusa try to set schema (and optionally port) for rutorrent</p>
</li>
<li>
<p>synology docker package ships with <code>docker-compose</code> (run it via <code>sudo</code>) so you can simply use such yaml or go other way and eg. configure ansible (requires some extra configuration on client side, but nothing heavy effort-wise)</p>
</li>
</ul>]]></description><content:encoded><![CDATA[<p>Recently I have upgraded my good old Synology DS411slim to newer, bigger model - DS918+. I kinda started to run out of free space (1TB 2.5&quot; x4) and decided to invest some money into a new box and new drives.</p>
<p><a href="https://www.synology.com/en-us/knowledgebase/DSM/tutorial/General_Setup/How_to_migrate_between_Synology_NAS_DSM_6_0_and_later">There is an official guide</a> how you can migrate with disk swipe, but as I wanted to move my data to new drives I just decided I would try to <code>rsync</code> everything. And BTW there is a <code>screen</code> command if you would like to run in the background that can be found on <a href="https://synocommunity.com/package/synocli-net">synocommunity</a>.</p>
<p>Long story short I had a poor experience with rsync - tweaking settings on both ends (disabling compression, tinkering with used crypto algo and so on) yielded rather disappointing results - transfer up to 10MB/s and almost 100% CPU utilization on my old NAS.</p>
<p>Weirdly enough I had much better luck with simply mounting CIFS resources in my new nas from my old nas directly in DistStation File Manager (<code>Tools</code> -&gt; <code>Mount Remote Folder</code> -&gt; <code>CIFS shared folder</code>). That doubled transfer speed and lowered CPU usage ~2x times. So far so good.</p>
<p>Aside from standard packages like PhotoStation (and btw if you move your <code>photos</code> directory it should <em>just work</em>) I also had <a href="https://github.com/sickchill/sickchill">SickChill</a> (previously SickRage and before that SickBeard as apparently there was some fight over the project) and <a href="https://transmissionbt.com/">transmission</a> that I wanted to move.</p>
<p>My goto solution were packages from synocommunity, but I noticed my new box have great support for Docker - Synology provides an official Docker package. So I decided to give it a try.</p>
<h3 id="new-stack">New stack</h3>
<p>After some research I ended up with <a href="https://github.com/pymedusa/Medusa/">Medusa</a> (as replacement for SickChill), <a href="https://github.com/Radarr/Radarr">Radarr</a> (for managing movies collection), <a href="https://github.com/Jackett/Jackett">Jackett</a> (that acts as proxy between medusa and radarr and various private torrent trackers) and <a href="https://github.com/Novik/ruTorrent">rutorrent</a> (as replacement for transmission as radarr don&rsquo;t have unpack rar archives capabilities).</p>
<p>Hopefully for me there is a <a href="https://www.linuxserver.io">linuxserver</a> community that released all the images that I needed, so basically half the work as done. Opensource ftw 💪.</p>
<p>I&rsquo;m using three virtual volumes on my synology:</p>
<ul>
<li><code>volume1</code> for various stuff - there I will create a docker directory to keep all the configs, dbs and so on</li>
<li><code>volume2</code> where I have my torrent (downloads) directory</li>
<li><code>volume3</code> where I have my tv/movies collection</li>
</ul>
<p>Sample docker compose will look something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#f92672">version</span>: <span style="color:#e6db74">&#34;2&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">radarr</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">linuxserver/radarr</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">radarr</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PUID=1024</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PGID=100</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">TZ=Europe/Warsaw</span> <span style="color:#75715e"># or whatever your timezone is</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume1/docker/radarr:/config</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume3/video/movies:/movies</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume2/torrent:/downloads</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">7878</span>:<span style="color:#ae81ff">7878</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">links</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">jackett</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">rutorrent</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mem_limit</span>: <span style="color:#ae81ff">1g</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">jackett</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">linuxserver/jackett</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">jackett</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PUID=1024</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PGID=100</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">TZ=Europe/Warsaw</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume1/docker/jackett:/config</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">9117</span>:<span style="color:#ae81ff">9117</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mem_limit</span>: <span style="color:#ae81ff">512mb</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rutorrent</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">linuxserver/rutorrent</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">rutorrent</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PUID=1024</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PGID=100</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume1/docker/rutorrent:/config</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume2/torrent:/downloads</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">9091</span>:<span style="color:#ae81ff">80</span> <span style="color:#75715e"># so you can access rutorrent on port 9091 on your synology</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">5050</span>:<span style="color:#ae81ff">5000</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">51413</span>:<span style="color:#ae81ff">51413</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">6881</span>:<span style="color:#ae81ff">6881</span><span style="color:#ae81ff">/udp</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mem_limit</span>: <span style="color:#ae81ff">512mb</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">medusa</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">linuxserver/medusa</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">medusa</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">links</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">jackett</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">rutorrent</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PUID=1024</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">PGID=100</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">TZ=Europe/Warsaw</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume1/docker/medusa:/config</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume2/torrent:/downloads</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/volume3/video/tv:/tv</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">8081</span>:<span style="color:#ae81ff">8081</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mem_limit</span>: <span style="color:#ae81ff">1g</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span></code></pre></div><p>Notes:</p>
<ul>
<li>
<p><code>PUID</code> is user ID and <code>PGID</code> is group ID <em>from</em> synology system (just ssh into you machine and check <code>id</code> of your designated user), otherwise mounted directories will be owned by <code>root</code> and this is not what you usually want</p>
</li>
<li>
<p>if you notice <code>NotImplementedError</code> errors in logs in Medusa try to set schema (and optionally port) for rutorrent</p>
</li>
<li>
<p>synology docker package ships with <code>docker-compose</code> (run it via <code>sudo</code>) so you can simply use such yaml or go other way and eg. configure ansible (requires some extra configuration on client side, but nothing heavy effort-wise)</p>
</li>
</ul>]]></content:encoded></item><item><title>PhotoSwipe gallery with StimulusJS (on Rails)</title><link>https://awesomeprogrammer.com/blog/2019/02/16/photoswipe-gallery-with-stimulusjs-on-rails/</link><pubDate>Sat, 16 Feb 2019 11:10:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2019/02/16/photoswipe-gallery-with-stimulusjs-on-rails/</guid><description><![CDATA[<p><a href="https://github.com/stimulusjs/stimulus">StimulusJS</a>, born at Basecamp, was officially released around a year ago - and I kind of ignored it&rsquo;s existence as I simply didn&rsquo;t need it anywhere (I worked on React-based project for a while). But right now I&rsquo;m working with an app that relies heavily on backend-rendering and Rails itself - in such scenario dropping the whole ecosystem of any javascript framework with client-side rendering makes no sense business-wise. But <em>gluing</em> random js snippets can get you far until some point.</p>
<p>I think the biggest advantage of stimulus is that it gives you some sane <em>structure</em>. It <a href="https://stimulusjs.org/handbook/installing">integrates nicely</a> with webpack/er and introduces idea of <a href="https://stimulusjs.org/reference/controllers">controllers</a>. So let&rsquo;s take a practical example of what we can do with it.</p>
<p><a href="https://github.com/dimsemenov/PhotoSwipe">PhotoSwipe</a> is pretty neat vanilla-js gallery library. But it requires some time to set up, and docs to be honest <a href="https://photoswipe.com/documentation/getting-started.html">looks pretty intimidating</a>. So let&rsquo;s break it down into the most simple example, so we can quickly see some results.</p>
<h3 id="add-required-html-markup">Add required HTML markup</h3>
<p><em>Note: I assume you already added photoswipe, its styles and you have stimulusjs ready to go.</em></p>
<p>We need to explicitly add required markup somewhere in our document, so let&rsquo;s slap this partial just before closing <code>&lt;/body&gt;</code> tag. You will notice we can already incorporate <code>I18n</code> for translating captions/navigation - which is pretty great as you can stick with ruby/rails for that.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp&#34;</span> <span style="color:#a6e22e">tabindex</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;-1&#34;</span> <span style="color:#a6e22e">role</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;dialog&#34;</span> <span style="color:#a6e22e">aria-hidden</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;true&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__bg&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__scroll-wrap&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__container&#34;</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__item&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__item&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__item&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__ui pswp__ui--hidden&#34;</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__top-bar&#34;</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__counter&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">&lt;!-- You can replace those t() calls with static text for the sake of example --&gt;</span>
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__button pswp__button--close&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;%= t(&#39;gallery.close&#39;) %&gt;&#34;</span>&gt;&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__button pswp__button--share&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;%= t(&#39;gallery.share&#39;) %&gt;&#34;</span>&gt;&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__button pswp__button--fs&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;%= t(&#39;gallery.fullscreen&#39;) %&gt;&#34;</span>&gt;&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__button pswp__button--zoom&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;%= t(&#39;gallery.zoom&#39;) %&gt;&#34;</span>&gt;&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__preloader&#34;</span>&gt;
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__preloader__icn&#34;</span>&gt;
</span></span><span style="display:flex;"><span>            &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__preloader__cut&#34;</span>&gt;
</span></span><span style="display:flex;"><span>              &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__preloader__donut&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>            &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>          &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__share-modal pswp__share-modal--hidden pswp__single-tap&#34;</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__share-tooltip&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__button pswp__button--arrow--left&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;%= t(&#39;gallery.previous&#39;) %&gt;&#34;</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__button pswp__button--arrow--right&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;%= t(&#39;gallery.next&#39;) %&gt;&#34;</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__caption&#34;</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__caption__center&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">div</span>&gt;
</span></span></code></pre></div><h3 id="render-some-gallery-markup">Render some gallery markup</h3>
<p>I think the most common scenario is that you already render some markup on the backend and you want to initialize a gallery based on that, so at this point we will utilize stimulus for that. Our simple markup might look something like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span><span style="color:#75715e">&lt;!-- we point this element to gallery controller --&gt;</span>
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">data-controller</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;gallery&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">&lt;!-- and within the gallery we want to render our &lt;image&gt; collection/record/whatever --&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">&lt;!-- let&#39;s connect click action &lt;a&gt; to onImageClick method in our controller --&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">&lt;!-- and specify target that we can nicely access in controller --&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">&lt;</span>%= link_to image.big_file_destination, title: image.title, data: { action: &#39;gallery#onImageClick&#39;, target: &#39;gallery.picture&#39; } do %&gt;
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">&lt;</span>%= image_tag image.small_thumb %&gt;
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">&lt;</span>% end %&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">div</span>&gt;
</span></span></code></pre></div><h3 id="add-gallery-controller">Add gallery controller</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#75715e">// gallery_controller.js
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> {<span style="color:#a6e22e">Controller</span>} <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;stimulus&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">as</span> <span style="color:#a6e22e">PhotoSwipe</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;photoswipe&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">as</span> <span style="color:#a6e22e">PhotoSwipeUI_Default</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;photoswipe/dist/photoswipe-ui-default&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">class</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Controller</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">static</span> <span style="color:#a6e22e">targets</span> <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#39;picture&#39;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">onImageClick</span>(<span style="color:#a6e22e">event</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">preventDefault</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// as our gallery markup lives outside of our controller
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// unfortunately we need to query for it, for the simplicity of example
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// let&#39;s assume we have single gallery controller in the app and we can call
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// query selector directly by it&#39;s class and we don&#39;t need to extract it into
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// configurable data-attribute
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">galleryWrapper</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;.pswp&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">options</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// we don&#39;t want browser history for or example for the sake of simplicity
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#a6e22e">history</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// and I&#39;m assuming we have unique links in each gallery
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#a6e22e">index</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">items</span>.<span style="color:#a6e22e">findIndex</span>(<span style="color:#a6e22e">item</span> =&gt; <span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">src</span> <span style="color:#f92672">===</span> <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">currentTarget</span>.<span style="color:#a6e22e">getAttribute</span>(<span style="color:#e6db74">&#39;href&#39;</span>))
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">gallery</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">PhotoSwipe</span>(<span style="color:#a6e22e">galleryWrapper</span>, <span style="color:#a6e22e">PhotoSwipeUI_Default</span>, <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">items</span>, <span style="color:#a6e22e">options</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// PhotoSwipe requires width and height do be declared up-front
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// let&#39;s work around that limitation, references:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// https://github.com/dimsemenov/PhotoSwipe/issues/741#issuecomment-430725838
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">listen</span>(<span style="color:#e6db74">&#39;beforeChange&#39;</span>, <span style="color:#66d9ef">function</span>() {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">src</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">src</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">image</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Image</span>()
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">src</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">src</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">onload</span> <span style="color:#f92672">=</span> () =&gt; {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">w</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">width</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">h</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">height</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">updateSize</span>()
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">init</span>()
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">get</span> <span style="color:#a6e22e">items</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">pictureTargets</span>.<span style="color:#a6e22e">map</span>(<span style="color:#66d9ef">function</span>(<span style="color:#a6e22e">item</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">src</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">getAttribute</span>(<span style="color:#e6db74">&#39;href&#39;</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">title</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">getAttribute</span>(<span style="color:#e6db74">&#39;title&#39;</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">w</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">h</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>So why this is better than simply dropping some js code on <code>DOMContentLoaded</code> <em>somewhere</em> within your app? As said before - it gives you a nice structure, our gallery logic is nicely encapsulated within javascript controller. We had to add a minimal amount of extra html markup to our code. Let&rsquo;s say we would like to handle async removal of image elements - combining stimulus with rails ujs events (binding action like <code>ajax:success-&gt;gallery#imageRemoved</code>) is pretty powerful and effortless.</p>
<p>I have been using stimulus for over 2 months now and if you&rsquo;re not planning on doing any intense client-side rendering / very dynamic UI I think it&rsquo;s a pretty solid tiny framework that is worth checking out.</p>
<hr>
<h4 id="update-dec-18-2019">Update: Dec 18, 2019</h4>
<p>As this post got some traction let&rsquo;s also briefly discuss how to properly attach stylesheets to your wepack bundle - as I received few questions regarding that matter as well.</p>
<p>I assume you already have some sort of <em>application pack</em> file that is compiled by webpack. Simply put there:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-scss" data-lang="scss"><span style="display:flex;"><span><span style="color:#75715e">// packs/application.scss
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Specifying pswp__assets-path might not be needed - it depends on your webpack config
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// Please read: https://github.com/rails/webpacker/blob/master/docs/css.md#resolve-url-loader
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// for references regarding url resolver
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>$pswp__assets-path<span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;~photoswipe/src/css/default-skin/&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">@import</span> <span style="color:#e6db74">&#39;~photoswipe/src/css/main&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">@import</span> <span style="color:#e6db74">&#39;~photoswipe/src/css/default-skin/default-skin&#39;</span>;
</span></span></code></pre></div><p>And that should do it!</p>
<h4 id="update-may-21-2020">Update: May 21, 2020</h4>
<p>If you&rsquo;re wondering <a href="/blog/2020/05/21/photoswipe-video-player-again-with-stimulusjs/">how to add video support to PhotoSwipe</a> wonder no more ;).</p>]]></description><content:encoded><![CDATA[<p><a href="https://github.com/stimulusjs/stimulus">StimulusJS</a>, born at Basecamp, was officially released around a year ago - and I kind of ignored it&rsquo;s existence as I simply didn&rsquo;t need it anywhere (I worked on React-based project for a while). But right now I&rsquo;m working with an app that relies heavily on backend-rendering and Rails itself - in such scenario dropping the whole ecosystem of any javascript framework with client-side rendering makes no sense business-wise. But <em>gluing</em> random js snippets can get you far until some point.</p>
<p>I think the biggest advantage of stimulus is that it gives you some sane <em>structure</em>. It <a href="https://stimulusjs.org/handbook/installing">integrates nicely</a> with webpack/er and introduces idea of <a href="https://stimulusjs.org/reference/controllers">controllers</a>. So let&rsquo;s take a practical example of what we can do with it.</p>
<p><a href="https://github.com/dimsemenov/PhotoSwipe">PhotoSwipe</a> is pretty neat vanilla-js gallery library. But it requires some time to set up, and docs to be honest <a href="https://photoswipe.com/documentation/getting-started.html">looks pretty intimidating</a>. So let&rsquo;s break it down into the most simple example, so we can quickly see some results.</p>
<h3 id="add-required-html-markup">Add required HTML markup</h3>
<p><em>Note: I assume you already added photoswipe, its styles and you have stimulusjs ready to go.</em></p>
<p>We need to explicitly add required markup somewhere in our document, so let&rsquo;s slap this partial just before closing <code>&lt;/body&gt;</code> tag. You will notice we can already incorporate <code>I18n</code> for translating captions/navigation - which is pretty great as you can stick with ruby/rails for that.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp&#34;</span> <span style="color:#a6e22e">tabindex</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;-1&#34;</span> <span style="color:#a6e22e">role</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;dialog&#34;</span> <span style="color:#a6e22e">aria-hidden</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;true&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__bg&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__scroll-wrap&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__container&#34;</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__item&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__item&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__item&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__ui pswp__ui--hidden&#34;</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__top-bar&#34;</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__counter&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">&lt;!-- You can replace those t() calls with static text for the sake of example --&gt;</span>
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__button pswp__button--close&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;%= t(&#39;gallery.close&#39;) %&gt;&#34;</span>&gt;&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__button pswp__button--share&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;%= t(&#39;gallery.share&#39;) %&gt;&#34;</span>&gt;&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__button pswp__button--fs&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;%= t(&#39;gallery.fullscreen&#39;) %&gt;&#34;</span>&gt;&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__button pswp__button--zoom&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;%= t(&#39;gallery.zoom&#39;) %&gt;&#34;</span>&gt;&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__preloader&#34;</span>&gt;
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__preloader__icn&#34;</span>&gt;
</span></span><span style="display:flex;"><span>            &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__preloader__cut&#34;</span>&gt;
</span></span><span style="display:flex;"><span>              &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__preloader__donut&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>            &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>          &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__share-modal pswp__share-modal--hidden pswp__single-tap&#34;</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__share-tooltip&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__button pswp__button--arrow--left&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;%= t(&#39;gallery.previous&#39;) %&gt;&#34;</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__button pswp__button--arrow--right&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;%= t(&#39;gallery.next&#39;) %&gt;&#34;</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__caption&#34;</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pswp__caption__center&#34;</span>&gt;&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">div</span>&gt;
</span></span></code></pre></div><h3 id="render-some-gallery-markup">Render some gallery markup</h3>
<p>I think the most common scenario is that you already render some markup on the backend and you want to initialize a gallery based on that, so at this point we will utilize stimulus for that. Our simple markup might look something like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span><span style="color:#75715e">&lt;!-- we point this element to gallery controller --&gt;</span>
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">data-controller</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;gallery&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">&lt;!-- and within the gallery we want to render our &lt;image&gt; collection/record/whatever --&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">&lt;!-- let&#39;s connect click action &lt;a&gt; to onImageClick method in our controller --&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">&lt;!-- and specify target that we can nicely access in controller --&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">&lt;</span>%= link_to image.big_file_destination, title: image.title, data: { action: &#39;gallery#onImageClick&#39;, target: &#39;gallery.picture&#39; } do %&gt;
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">&lt;</span>%= image_tag image.small_thumb %&gt;
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">&lt;</span>% end %&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">div</span>&gt;
</span></span></code></pre></div><h3 id="add-gallery-controller">Add gallery controller</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#75715e">// gallery_controller.js
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> {<span style="color:#a6e22e">Controller</span>} <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;stimulus&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">as</span> <span style="color:#a6e22e">PhotoSwipe</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;photoswipe&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">as</span> <span style="color:#a6e22e">PhotoSwipeUI_Default</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;photoswipe/dist/photoswipe-ui-default&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">class</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">Controller</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">static</span> <span style="color:#a6e22e">targets</span> <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#39;picture&#39;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">onImageClick</span>(<span style="color:#a6e22e">event</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">preventDefault</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// as our gallery markup lives outside of our controller
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// unfortunately we need to query for it, for the simplicity of example
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// let&#39;s assume we have single gallery controller in the app and we can call
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// query selector directly by it&#39;s class and we don&#39;t need to extract it into
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// configurable data-attribute
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">galleryWrapper</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;.pswp&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">options</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// we don&#39;t want browser history for or example for the sake of simplicity
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#a6e22e">history</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// and I&#39;m assuming we have unique links in each gallery
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#a6e22e">index</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">items</span>.<span style="color:#a6e22e">findIndex</span>(<span style="color:#a6e22e">item</span> =&gt; <span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">src</span> <span style="color:#f92672">===</span> <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">currentTarget</span>.<span style="color:#a6e22e">getAttribute</span>(<span style="color:#e6db74">&#39;href&#39;</span>))
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">gallery</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">PhotoSwipe</span>(<span style="color:#a6e22e">galleryWrapper</span>, <span style="color:#a6e22e">PhotoSwipeUI_Default</span>, <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">items</span>, <span style="color:#a6e22e">options</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// PhotoSwipe requires width and height do be declared up-front
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// let&#39;s work around that limitation, references:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// https://github.com/dimsemenov/PhotoSwipe/issues/741#issuecomment-430725838
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">listen</span>(<span style="color:#e6db74">&#39;beforeChange&#39;</span>, <span style="color:#66d9ef">function</span>() {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">src</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">src</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">image</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Image</span>()
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">src</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">src</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">onload</span> <span style="color:#f92672">=</span> () =&gt; {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">w</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">width</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">currItem</span>.<span style="color:#a6e22e">h</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">image</span>.<span style="color:#a6e22e">height</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">updateSize</span>()
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">gallery</span>.<span style="color:#a6e22e">init</span>()
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">get</span> <span style="color:#a6e22e">items</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">pictureTargets</span>.<span style="color:#a6e22e">map</span>(<span style="color:#66d9ef">function</span>(<span style="color:#a6e22e">item</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">src</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">getAttribute</span>(<span style="color:#e6db74">&#39;href&#39;</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">title</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">getAttribute</span>(<span style="color:#e6db74">&#39;title&#39;</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">w</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">h</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>So why this is better than simply dropping some js code on <code>DOMContentLoaded</code> <em>somewhere</em> within your app? As said before - it gives you a nice structure, our gallery logic is nicely encapsulated within javascript controller. We had to add a minimal amount of extra html markup to our code. Let&rsquo;s say we would like to handle async removal of image elements - combining stimulus with rails ujs events (binding action like <code>ajax:success-&gt;gallery#imageRemoved</code>) is pretty powerful and effortless.</p>
<p>I have been using stimulus for over 2 months now and if you&rsquo;re not planning on doing any intense client-side rendering / very dynamic UI I think it&rsquo;s a pretty solid tiny framework that is worth checking out.</p>
<hr>
<h4 id="update-dec-18-2019">Update: Dec 18, 2019</h4>
<p>As this post got some traction let&rsquo;s also briefly discuss how to properly attach stylesheets to your wepack bundle - as I received few questions regarding that matter as well.</p>
<p>I assume you already have some sort of <em>application pack</em> file that is compiled by webpack. Simply put there:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-scss" data-lang="scss"><span style="display:flex;"><span><span style="color:#75715e">// packs/application.scss
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Specifying pswp__assets-path might not be needed - it depends on your webpack config
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// Please read: https://github.com/rails/webpacker/blob/master/docs/css.md#resolve-url-loader
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// for references regarding url resolver
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>$pswp__assets-path<span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;~photoswipe/src/css/default-skin/&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">@import</span> <span style="color:#e6db74">&#39;~photoswipe/src/css/main&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">@import</span> <span style="color:#e6db74">&#39;~photoswipe/src/css/default-skin/default-skin&#39;</span>;
</span></span></code></pre></div><p>And that should do it!</p>
<h4 id="update-may-21-2020">Update: May 21, 2020</h4>
<p>If you&rsquo;re wondering <a href="/blog/2020/05/21/photoswipe-video-player-again-with-stimulusjs/">how to add video support to PhotoSwipe</a> wonder no more ;).</p>]]></content:encoded></item><item><title>Crystal app: Resurrection</title><link>https://awesomeprogrammer.com/blog/2019/02/10/crystal-app-resurrection/</link><pubDate>Sun, 10 Feb 2019 13:46:54 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2019/02/10/crystal-app-resurrection/</guid><description><![CDATA[<p>So <a href="/blog/2018/05/18/back-to-the-crystal-thoughts-on-my-first-app/">it&rsquo;s been a while</a> since I finished prototyping my very first app in Crystal. I decided to dust it off a little bit around a week ago.</p>
<p>It&rsquo;s not available under <a href="https://moviestowatch.app/">moviestowatch.app</a> domain, but aside that I got yet another idea based on the original concept.</p>
<p>After watching <em>Travelers</em>, then almost right after that <em>12 Monkeys</em> (goddamn I love that show), followed by <em>Schitt&rsquo;s Creek</em> (I needed more of Emily Hampshire, don&rsquo;t judge me) I started to wonder how much life I wasted on television. So I quickly glued (that&rsquo;s not an overstatement) estimator for that - based off what I already had.</p>
<p>It simply multiplies the number of shows episodes reported by IMDB with a length of the episode and gives you an estimate, no rocket science really - but at least now I know how big part of my life was TV shows.</p>
<p>Example &ldquo;Starting sets&rdquo;:</p>
<p><a href="https://moviestowatch.app/tv/hours/mine/iOgNwk61">Top 50 Rated TV shows</a> &amp; <a href="https://moviestowatch.app/tv/hours/mine/7msX8THy">Most Rated TV Shows</a>. And <a href="https://moviestowatch.app/tv/hours/mine/pb8RSNX6">here is mine set</a> (most likely incomplete)</p>
<p>Technically it still based on crystal backend with postgres as database and just slaps some of javascript inline (yup) on top of everything (<a href="https://semantic-ui.com">semantic ui</a> components + <a href="https://www.chartjs.org/">Chart.js</a> with <a href="https://github.com/nagix/chartjs-plugin-colorschemes/">color schemes plugin</a> + <a href="https://inorganik.github.io/countUp.js/">countUp.js</a>).</p>]]></description><content:encoded><![CDATA[<p>So <a href="/blog/2018/05/18/back-to-the-crystal-thoughts-on-my-first-app/">it&rsquo;s been a while</a> since I finished prototyping my very first app in Crystal. I decided to dust it off a little bit around a week ago.</p>
<p>It&rsquo;s not available under <a href="https://moviestowatch.app/">moviestowatch.app</a> domain, but aside that I got yet another idea based on the original concept.</p>
<p>After watching <em>Travelers</em>, then almost right after that <em>12 Monkeys</em> (goddamn I love that show), followed by <em>Schitt&rsquo;s Creek</em> (I needed more of Emily Hampshire, don&rsquo;t judge me) I started to wonder how much life I wasted on television. So I quickly glued (that&rsquo;s not an overstatement) estimator for that - based off what I already had.</p>
<p>It simply multiplies the number of shows episodes reported by IMDB with a length of the episode and gives you an estimate, no rocket science really - but at least now I know how big part of my life was TV shows.</p>
<p>Example &ldquo;Starting sets&rdquo;:</p>
<p><a href="https://moviestowatch.app/tv/hours/mine/iOgNwk61">Top 50 Rated TV shows</a> &amp; <a href="https://moviestowatch.app/tv/hours/mine/7msX8THy">Most Rated TV Shows</a>. And <a href="https://moviestowatch.app/tv/hours/mine/pb8RSNX6">here is mine set</a> (most likely incomplete)</p>
<p>Technically it still based on crystal backend with postgres as database and just slaps some of javascript inline (yup) on top of everything (<a href="https://semantic-ui.com">semantic ui</a> components + <a href="https://www.chartjs.org/">Chart.js</a> with <a href="https://github.com/nagix/chartjs-plugin-colorschemes/">color schemes plugin</a> + <a href="https://inorganik.github.io/countUp.js/">countUp.js</a>).</p>]]></content:encoded></item><item><title>Surviving technological hype with a peace of mind</title><link>https://awesomeprogrammer.com/blog/2019/02/10/surviving-technological-hype-with-a-peace-of-mind/</link><pubDate>Sun, 10 Feb 2019 09:54:54 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2019/02/10/surviving-technological-hype-with-a-peace-of-mind/</guid><description><![CDATA[<p>How can you sleep when people are building serverless applications in the cloud with blockchain while sitting in a self-driving car powered by AI and machine learning?</p>
<p>Don&rsquo;t get me wrong - behind some of those words are hidden really interesting things and problems, but as an average Joe that is doing mostly web-related stuff how do you deal with such noise?</p>
<p>I think the answer to this is the same principle you might apply to different problems - keep it simple.</p>
<p>Simple is often small, often boring. Because <em>new</em> is exciting. <em>New</em> is cool. A new gadget is exciting, a new car is fun, <a href="https://github.com/emq/documentWrite.JS">a new javascript framework that will solve all your issues</a> sounds reassuring.</p>
<p><em>New</em> promises a better way of doing things, a way that will solve all your problems and worries. Unfortunately, this is usually not the case.</p>
<figure><img src="/images/hype_cycle.svg"/><figcaption>
            <h4>You mostly seen that before. Source: wikipedia</h4>
        </figcaption>
</figure>

<p>Simple means it solves problems at your scale and is manageable with resources you were given. While some approach might help bigger teams to move faster (despite various shortcomings), it might cripple smaller teams due to maintenance/complexity overhead it brings.</p>
<p>So often you don&rsquo;t need to spin up Kubernetes cluster (cool) - instead, you can bootstrap a few vpses with the latest Ubuntu LTS using Ansible (boring). You might want to use - god-forbid - jQuery to solve a small task (old &amp; boring) instead of dropping the whole ecosystem of React/Vue.js (new &amp; hip) on your shoulders.</p>
<p>In the end, it&rsquo;s always a bargain and there is no silver bullet. So choose your fights wisely.</p>]]></description><content:encoded><![CDATA[<p>How can you sleep when people are building serverless applications in the cloud with blockchain while sitting in a self-driving car powered by AI and machine learning?</p>
<p>Don&rsquo;t get me wrong - behind some of those words are hidden really interesting things and problems, but as an average Joe that is doing mostly web-related stuff how do you deal with such noise?</p>
<p>I think the answer to this is the same principle you might apply to different problems - keep it simple.</p>
<p>Simple is often small, often boring. Because <em>new</em> is exciting. <em>New</em> is cool. A new gadget is exciting, a new car is fun, <a href="https://github.com/emq/documentWrite.JS">a new javascript framework that will solve all your issues</a> sounds reassuring.</p>
<p><em>New</em> promises a better way of doing things, a way that will solve all your problems and worries. Unfortunately, this is usually not the case.</p>
<figure><img src="/images/hype_cycle.svg"/><figcaption>
            <h4>You mostly seen that before. Source: wikipedia</h4>
        </figcaption>
</figure>

<p>Simple means it solves problems at your scale and is manageable with resources you were given. While some approach might help bigger teams to move faster (despite various shortcomings), it might cripple smaller teams due to maintenance/complexity overhead it brings.</p>
<p>So often you don&rsquo;t need to spin up Kubernetes cluster (cool) - instead, you can bootstrap a few vpses with the latest Ubuntu LTS using Ansible (boring). You might want to use - god-forbid - jQuery to solve a small task (old &amp; boring) instead of dropping the whole ecosystem of React/Vue.js (new &amp; hip) on your shoulders.</p>
<p>In the end, it&rsquo;s always a bargain and there is no silver bullet. So choose your fights wisely.</p>]]></content:encoded></item><item><title>User systemd and capistrano</title><link>https://awesomeprogrammer.com/blog/2018/11/18/user-systemd-and-capistrano/</link><pubDate>Sun, 18 Nov 2018 19:53:55 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2018/11/18/user-systemd-and-capistrano/</guid><description><![CDATA[<p>A few weeks ago I was upgrading legacy infrastructure for small-sized project and decided to go with <code>systemd</code> for service management as I had quite good experience with it while tinkering with my <em>toy</em> apps written in Crystal.</p>
<p>If you want to use systemd for rails/ruby project - there is a <em>great news everyone!</em> <a href="https://github.com/groovenauts/capistrano-systemd-multiservice">capistrano-systemd-multiservice</a> will do the work for you and I must say - it works just great.</p>
<p>There are some caveats worth mentioning tho:</p>
<ol>
<li>
<p><code>WantedBy = multi-user.target</code> doesn&rsquo;t work on ubuntu for user systemd services - you will want to use <code>WantedBy = default.target</code> instead.</p>
</li>
<li>
<p>You will need to <a href="https://www.freedesktop.org/software/systemd/man/loginctl.html#enable-linger%20USER%E2%80%A6">enable linger</a> with <code>loginctl enable-linger</code> for your particular user(s) that are running the app on your production system.</p>
</li>
<li>
<p>If you encounter quirks regarding unicorn hot reloading try to set <code>WorkingDirectory</code> to <em>current</em> directory in your service declaration.</p>
</li>
<li>
<p>When using <code>rvm1-capistrano3</code> you can invoke <code>rvm1:alias:create</code> to generate nice aliases under <code>~/.rvm/wrappers/&lt;application&gt;</code> - this is very handy in services declaration as you don&rsquo;t have to reference ruby version directly (less hassle during ruby upgrade - you can simply invoke alias creation on every deploy).</p>
</li>
<li>
<p>Make sure <code>~/.config/systemd/user</code> directory exists on the first deploy - this probably should we incorporated in <code>capistrano-systemd-multiservice</code> itself.</p>
</li>
</ol>
]]></description><content:encoded><![CDATA[<p>A few weeks ago I was upgrading legacy infrastructure for small-sized project and decided to go with <code>systemd</code> for service management as I had quite good experience with it while tinkering with my <em>toy</em> apps written in Crystal.</p>
<p>If you want to use systemd for rails/ruby project - there is a <em>great news everyone!</em> <a href="https://github.com/groovenauts/capistrano-systemd-multiservice">capistrano-systemd-multiservice</a> will do the work for you and I must say - it works just great.</p>
<p>There are some caveats worth mentioning tho:</p>
<ol>
<li>
<p><code>WantedBy = multi-user.target</code> doesn&rsquo;t work on ubuntu for user systemd services - you will want to use <code>WantedBy = default.target</code> instead.</p>
</li>
<li>
<p>You will need to <a href="https://www.freedesktop.org/software/systemd/man/loginctl.html#enable-linger%20USER%E2%80%A6">enable linger</a> with <code>loginctl enable-linger</code> for your particular user(s) that are running the app on your production system.</p>
</li>
<li>
<p>If you encounter quirks regarding unicorn hot reloading try to set <code>WorkingDirectory</code> to <em>current</em> directory in your service declaration.</p>
</li>
<li>
<p>When using <code>rvm1-capistrano3</code> you can invoke <code>rvm1:alias:create</code> to generate nice aliases under <code>~/.rvm/wrappers/&lt;application&gt;</code> - this is very handy in services declaration as you don&rsquo;t have to reference ruby version directly (less hassle during ruby upgrade - you can simply invoke alias creation on every deploy).</p>
</li>
<li>
<p>Make sure <code>~/.config/systemd/user</code> directory exists on the first deploy - this probably should we incorporated in <code>capistrano-systemd-multiservice</code> itself.</p>
</li>
</ol>
]]></content:encoded></item><item><title>Randomly failing RSpec test suite - is there a hope?</title><link>https://awesomeprogrammer.com/blog/2018/07/24/randomly-failing-rspec-test-suite-is-there-a-hope/</link><pubDate>Tue, 24 Jul 2018 10:40:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2018/07/24/randomly-failing-rspec-test-suite-is-there-a-hope/</guid><description><![CDATA[<p>Fortunately thanks to very smart people in the community - yes there is! So worry not, warrior of legacy code, protector of good practices. Now you have tools that will help you lead the way into the clean and solid code, where tests are always green and developers are happy. Ok, maybe I got carried away a little bit here, let&rsquo;s get down to the business ;).</p>
<p>Rspec 3.3 introduced <a href="https://relishapp.com/rspec/rspec-core/docs/command-line/bisect">bisect command</a> which is truly awesome, from docs:</p>
<blockquote>
<p>Pass the &ndash;bisect option (in addition to &ndash;seed and any other options) and RSpec will repeatedly run subsets of your suite to isolate the minimal set of examples that reproduce the same failures.</p>
</blockquote>
<p>I recommend checkout out <a href="https://github.com/rspec/rspec-core/pull/1917">this pull request</a> for more references regarding internals/implementation.</p>
<p>Side note: still on Rspec 2.x? <a href="https://github.com/yujinakayama/transpec">Checkout transpec</a> that will help you with migration to 3.x.</p>
<p>So let&rsquo;s say you have some sort of test suite that is passing. Unless you add <code>config.order = :random</code> to your rspec config (or run it with <code>--order rand</code> flag), then it&rsquo;s not. Test suite should fail with a seed of some sort (eg. <em>Randomized with seed 45379</em>), pass that seed to bisect command (<code>rspec --seed 45379 --bisect</code>) and you should end up with a very narrow subset of tests that should help you <em>a lot</em> with the process of investigating what went wrong - of course, this often won&rsquo;t solve everything magically, but should help you guide into the right place.</p>
<p>Random tips / notes:</p>
<ul>
<li>
<p>depending on test suite size - it might take some time to compute, I suggest disabling Rails logging (via <code>Rails.logger.level = 4</code>) for some free performance gain and keeping database in memory if possible - if you&rsquo;re using docker you can mount database directory as <a href="https://docs.docker.com/compose/compose-file/#tmpfs">tmpfs</a></p>
</li>
<li>
<p>if you&rsquo;re having still troubles or rspec can&rsquo;t bisect your test suite try with different seed</p>
</li>
<li>
<p>generally, watch out for <code>before</code> / <code>after</code> - <code>(:all)</code> hooks, unclear/magical fixtures that might mess up global database state (and other code that touches globals/opens classes)</p>
</li>
<li>
<p>additionally, you can run each spec individually which might be a good approach in some cases</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>find spec -name <span style="color:#e6db74">&#39;*_spec.rb&#39;</span> -exec rspec <span style="color:#f92672">{}</span> <span style="color:#ae81ff">\;</span>
</span></span></code></pre></div><p>(you might want to quote <code>'{}'</code> under fish shell)</p>
<hr>
<p>Good luck! ⚔️</p>
]]></description><content:encoded><![CDATA[<p>Fortunately thanks to very smart people in the community - yes there is! So worry not, warrior of legacy code, protector of good practices. Now you have tools that will help you lead the way into the clean and solid code, where tests are always green and developers are happy. Ok, maybe I got carried away a little bit here, let&rsquo;s get down to the business ;).</p>
<p>Rspec 3.3 introduced <a href="https://relishapp.com/rspec/rspec-core/docs/command-line/bisect">bisect command</a> which is truly awesome, from docs:</p>
<blockquote>
<p>Pass the &ndash;bisect option (in addition to &ndash;seed and any other options) and RSpec will repeatedly run subsets of your suite to isolate the minimal set of examples that reproduce the same failures.</p>
</blockquote>
<p>I recommend checkout out <a href="https://github.com/rspec/rspec-core/pull/1917">this pull request</a> for more references regarding internals/implementation.</p>
<p>Side note: still on Rspec 2.x? <a href="https://github.com/yujinakayama/transpec">Checkout transpec</a> that will help you with migration to 3.x.</p>
<p>So let&rsquo;s say you have some sort of test suite that is passing. Unless you add <code>config.order = :random</code> to your rspec config (or run it with <code>--order rand</code> flag), then it&rsquo;s not. Test suite should fail with a seed of some sort (eg. <em>Randomized with seed 45379</em>), pass that seed to bisect command (<code>rspec --seed 45379 --bisect</code>) and you should end up with a very narrow subset of tests that should help you <em>a lot</em> with the process of investigating what went wrong - of course, this often won&rsquo;t solve everything magically, but should help you guide into the right place.</p>
<p>Random tips / notes:</p>
<ul>
<li>
<p>depending on test suite size - it might take some time to compute, I suggest disabling Rails logging (via <code>Rails.logger.level = 4</code>) for some free performance gain and keeping database in memory if possible - if you&rsquo;re using docker you can mount database directory as <a href="https://docs.docker.com/compose/compose-file/#tmpfs">tmpfs</a></p>
</li>
<li>
<p>if you&rsquo;re having still troubles or rspec can&rsquo;t bisect your test suite try with different seed</p>
</li>
<li>
<p>generally, watch out for <code>before</code> / <code>after</code> - <code>(:all)</code> hooks, unclear/magical fixtures that might mess up global database state (and other code that touches globals/opens classes)</p>
</li>
<li>
<p>additionally, you can run each spec individually which might be a good approach in some cases</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>find spec -name <span style="color:#e6db74">&#39;*_spec.rb&#39;</span> -exec rspec <span style="color:#f92672">{}</span> <span style="color:#ae81ff">\;</span>
</span></span></code></pre></div><p>(you might want to quote <code>'{}'</code> under fish shell)</p>
<hr>
<p>Good luck! ⚔️</p>
]]></content:encoded></item><item><title>How to POST multipart form in Crystal and receive in it Express.js (Node.js framework)</title><link>https://awesomeprogrammer.com/blog/2018/06/21/how-to-post-multipart-form-in-crystal-and-receive-in-it-express.js-node.js-framework/</link><pubDate>Thu, 21 Jun 2018 12:40:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2018/06/21/how-to-post-multipart-form-in-crystal-and-receive-in-it-express.js-node.js-framework/</guid><description><![CDATA[<p>I was still tinkering with my pet project a little bit - connecting it to social media was an interesting learning experience as I had to create tiny service in Node.js due to fact Crystal lacked proper libraries (and that is ok, you don&rsquo;t have to duplicate everything in every language). Below are some short notes that might help you a little bit if you&rsquo;re trying to do a similar thing.</p>
<h3 id="post-multipart-form-in-crystal">Post multipart form in Crystal</h3>
<p>This is an easy part - crystal comes with very convenient <a href="https://crystal-lang.org/api/0.25.0/HTTP/FormData/Builder.html">FormData::Builder</a>. So you don&rsquo;t have to go crazy end base64 encode your file, put it in JSON payload and such - you can simply use native http way. Here is how you can use it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-crystal" data-lang="crystal"><span style="display:flex;"><span>io <span style="color:#f92672">=</span> <span style="color:#66d9ef">IO</span><span style="color:#f92672">::</span>Memory<span style="color:#f92672">.</span>new
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># boundary is optional - will be generated randomly if not specified</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># unfortunately, some frameworks in some languages</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># have hardcoded boundary specified below in their multipart parsers!</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># (they ignore Content-Type bounday=&lt;boundary string&gt; header) - worth keeping in mind</span>
</span></span><span style="display:flex;"><span>builder <span style="color:#f92672">=</span> <span style="color:#66d9ef">HTTP</span><span style="color:#f92672">::</span>FormData<span style="color:#f92672">::</span>Builder<span style="color:#f92672">.</span>new(io, <span style="color:#e6db74">&#34;AaB03x&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>builder<span style="color:#f92672">.</span>field(<span style="color:#e6db74">&#34;myfield&#34;</span>, <span style="color:#e6db74">&#34;content of my field&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># optional headers for the file part</span>
</span></span><span style="display:flex;"><span>file_headers <span style="color:#f92672">=</span> <span style="color:#66d9ef">HTTP</span><span style="color:#f92672">::</span>Headers{<span style="color:#e6db74">&#34;Content-Type&#34;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;image/jpeg&#34;</span>}
</span></span><span style="display:flex;"><span><span style="color:#75715e"># `file` can be an IO; optional metadata</span>
</span></span><span style="display:flex;"><span>file_metadata <span style="color:#f92672">=</span> <span style="color:#66d9ef">HTTP</span><span style="color:#f92672">::</span>FormData<span style="color:#f92672">::</span>FileMetadata<span style="color:#f92672">.</span>new(<span style="color:#e6db74">filename</span>: File<span style="color:#f92672">.</span>basename(file<span style="color:#f92672">.</span>path))
</span></span><span style="display:flex;"><span>builder<span style="color:#f92672">.</span>file(<span style="color:#e6db74">&#34;myimage&#34;</span>, file, file_metadata, file_headers)
</span></span><span style="display:flex;"><span><span style="color:#75715e"># you need to wrap everything up</span>
</span></span><span style="display:flex;"><span>builder<span style="color:#f92672">.</span>finish
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># also don&#39;t forget to set proper request headers</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># otherwise receiver will most likely fail to parse whole request</span>
</span></span><span style="display:flex;"><span>headers <span style="color:#f92672">=</span> <span style="color:#66d9ef">HTTP</span><span style="color:#f92672">::</span>Headers{
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;Content-Type&#34;</span> <span style="color:#f92672">=&gt;</span> builder<span style="color:#f92672">.</span>content_type,
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">HTTP</span><span style="color:#f92672">::</span>Client<span style="color:#f92672">.</span>post(<span style="color:#e6db74">&#34;http://my.endpoint&#34;</span>, <span style="color:#e6db74">headers</span>: headers, <span style="color:#e6db74">body</span>: io<span style="color:#f92672">.</span>to_s) <span style="color:#75715e"># =&gt; HTTP::Client::Response</span>
</span></span></code></pre></div><h3 id="handle-multipart-in-expressjs">Handle multipart in Express.js</h3>
<p>Welcome to the world of javascript, a world of deprecation.</p>
<p>Ok, jokes aside. It took me a while to select proper middleware, as Expressjs doesn&rsquo;t ship with anything you can use for multipart by default. There is a <a href="https://github.com/expressjs/body-parser">bodyparser</a> that does not support handle multipart bodies. <a href="https://andrewkelley.me/post/do-not-use-bodyparser-with-express-js.html">There is also an article</a> that suggests not using it, but I think it&rsquo;s quite outdated as some of the facts are inconsistent with the current state of things.</p>
<p>I ended up with <a href="https://github.com/expressjs/multer">multer</a> that does the job just right and I think <em>right now</em> is <em>the right choice</em> for express. Anyhow, assuming you want to handle request submitted above you can do in your express app:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">express</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#39;express&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">multer</span>  <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#39;multer&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">upload</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">multer</span>({ <span style="color:#a6e22e">dest</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;uploads/&#39;</span> })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">app</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">express</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">post</span>(<span style="color:#e6db74">&#39;/endpoint&#39;</span>, <span style="color:#a6e22e">upload</span>.<span style="color:#a6e22e">single</span>(<span style="color:#e6db74">&#39;myimage&#39;</span>), <span style="color:#66d9ef">function</span> (<span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">res</span>, <span style="color:#a6e22e">next</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// req.file will hold `myimage` file (to access absolute path use req.file.path)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// req.body will hold fields, so in our case you want to access req.body.myfield
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>})
</span></span></code></pre></div><p>And just that will do the job.</p>
<hr>
<p>Note: Using <code>Crystal 0.25.0</code> and <code>expressjs 4.16.3</code>.</p>
]]></description><content:encoded><![CDATA[<p>I was still tinkering with my pet project a little bit - connecting it to social media was an interesting learning experience as I had to create tiny service in Node.js due to fact Crystal lacked proper libraries (and that is ok, you don&rsquo;t have to duplicate everything in every language). Below are some short notes that might help you a little bit if you&rsquo;re trying to do a similar thing.</p>
<h3 id="post-multipart-form-in-crystal">Post multipart form in Crystal</h3>
<p>This is an easy part - crystal comes with very convenient <a href="https://crystal-lang.org/api/0.25.0/HTTP/FormData/Builder.html">FormData::Builder</a>. So you don&rsquo;t have to go crazy end base64 encode your file, put it in JSON payload and such - you can simply use native http way. Here is how you can use it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-crystal" data-lang="crystal"><span style="display:flex;"><span>io <span style="color:#f92672">=</span> <span style="color:#66d9ef">IO</span><span style="color:#f92672">::</span>Memory<span style="color:#f92672">.</span>new
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># boundary is optional - will be generated randomly if not specified</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># unfortunately, some frameworks in some languages</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># have hardcoded boundary specified below in their multipart parsers!</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># (they ignore Content-Type bounday=&lt;boundary string&gt; header) - worth keeping in mind</span>
</span></span><span style="display:flex;"><span>builder <span style="color:#f92672">=</span> <span style="color:#66d9ef">HTTP</span><span style="color:#f92672">::</span>FormData<span style="color:#f92672">::</span>Builder<span style="color:#f92672">.</span>new(io, <span style="color:#e6db74">&#34;AaB03x&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>builder<span style="color:#f92672">.</span>field(<span style="color:#e6db74">&#34;myfield&#34;</span>, <span style="color:#e6db74">&#34;content of my field&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># optional headers for the file part</span>
</span></span><span style="display:flex;"><span>file_headers <span style="color:#f92672">=</span> <span style="color:#66d9ef">HTTP</span><span style="color:#f92672">::</span>Headers{<span style="color:#e6db74">&#34;Content-Type&#34;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;image/jpeg&#34;</span>}
</span></span><span style="display:flex;"><span><span style="color:#75715e"># `file` can be an IO; optional metadata</span>
</span></span><span style="display:flex;"><span>file_metadata <span style="color:#f92672">=</span> <span style="color:#66d9ef">HTTP</span><span style="color:#f92672">::</span>FormData<span style="color:#f92672">::</span>FileMetadata<span style="color:#f92672">.</span>new(<span style="color:#e6db74">filename</span>: File<span style="color:#f92672">.</span>basename(file<span style="color:#f92672">.</span>path))
</span></span><span style="display:flex;"><span>builder<span style="color:#f92672">.</span>file(<span style="color:#e6db74">&#34;myimage&#34;</span>, file, file_metadata, file_headers)
</span></span><span style="display:flex;"><span><span style="color:#75715e"># you need to wrap everything up</span>
</span></span><span style="display:flex;"><span>builder<span style="color:#f92672">.</span>finish
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># also don&#39;t forget to set proper request headers</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># otherwise receiver will most likely fail to parse whole request</span>
</span></span><span style="display:flex;"><span>headers <span style="color:#f92672">=</span> <span style="color:#66d9ef">HTTP</span><span style="color:#f92672">::</span>Headers{
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;Content-Type&#34;</span> <span style="color:#f92672">=&gt;</span> builder<span style="color:#f92672">.</span>content_type,
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">HTTP</span><span style="color:#f92672">::</span>Client<span style="color:#f92672">.</span>post(<span style="color:#e6db74">&#34;http://my.endpoint&#34;</span>, <span style="color:#e6db74">headers</span>: headers, <span style="color:#e6db74">body</span>: io<span style="color:#f92672">.</span>to_s) <span style="color:#75715e"># =&gt; HTTP::Client::Response</span>
</span></span></code></pre></div><h3 id="handle-multipart-in-expressjs">Handle multipart in Express.js</h3>
<p>Welcome to the world of javascript, a world of deprecation.</p>
<p>Ok, jokes aside. It took me a while to select proper middleware, as Expressjs doesn&rsquo;t ship with anything you can use for multipart by default. There is a <a href="https://github.com/expressjs/body-parser">bodyparser</a> that does not support handle multipart bodies. <a href="https://andrewkelley.me/post/do-not-use-bodyparser-with-express-js.html">There is also an article</a> that suggests not using it, but I think it&rsquo;s quite outdated as some of the facts are inconsistent with the current state of things.</p>
<p>I ended up with <a href="https://github.com/expressjs/multer">multer</a> that does the job just right and I think <em>right now</em> is <em>the right choice</em> for express. Anyhow, assuming you want to handle request submitted above you can do in your express app:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">express</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#39;express&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">multer</span>  <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#39;multer&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">upload</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">multer</span>({ <span style="color:#a6e22e">dest</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;uploads/&#39;</span> })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">app</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">express</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">post</span>(<span style="color:#e6db74">&#39;/endpoint&#39;</span>, <span style="color:#a6e22e">upload</span>.<span style="color:#a6e22e">single</span>(<span style="color:#e6db74">&#39;myimage&#39;</span>), <span style="color:#66d9ef">function</span> (<span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">res</span>, <span style="color:#a6e22e">next</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// req.file will hold `myimage` file (to access absolute path use req.file.path)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// req.body will hold fields, so in our case you want to access req.body.myfield
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>})
</span></span></code></pre></div><p>And just that will do the job.</p>
<hr>
<p>Note: Using <code>Crystal 0.25.0</code> and <code>expressjs 4.16.3</code>.</p>
]]></content:encoded></item><item><title>Writing Chip-8 Emulator (interpreter) in Crystal</title><link>https://awesomeprogrammer.com/blog/2018/06/16/writing-chip-8-emulator-interpreter-in-crystal/</link><pubDate>Sat, 16 Jun 2018 09:30:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2018/06/16/writing-chip-8-emulator-interpreter-in-crystal/</guid><description><![CDATA[<p>It was the middle of a week, around noon I think.</p>
<p>I thought to myself <em>&ldquo;I should write a yet-another NES emulator, that would be awesome!&rdquo;</em>.</p>
<p>A few minutes later I am reading blog posts, articles, browsing repositories.</p>
<p>And I am like <em>&ldquo;Damn it looks complicated&rdquo;</em>.</p>
<figure><img src="/images/2012-05-02-beartato-hackthedatabase.png"/><figcaption>
            <h4>Me about to write NES emulator (Source: nedroid.com)</h4>
        </figcaption>
</figure>

<p>So I decided to start with something much, much simpler - <a href="https://en.wikipedia.org/wiki/CHIP-8">Chip-8</a> that was developed in mid-1970s. Because it&rsquo;s architecture was pretty straightforward it was a project I was able to finish within few days without a rush - and that included whole day+ of debugging why the hell drawing on screen doesn&rsquo;t work as expected (reason: Crystal type interference gave me unsigned int8 on one calculation, but it somehow awkwardly handled overflow of int8 - I didn&rsquo;t have chance to dig into it yet).</p>
<p>I am not going to write any step-by-step guide, because I have found two just fabulous blog posts related to the task - one is about <a href="http://blog.alexanderdickson.com/javascript-chip-8-emulator">writing Chip-8 emulator in Javascript</a>, and the other one is complete guide with detailed explanations <a href="http://www.multigesture.net/articles/how-to-write-an-emulator-chip-8-interpreter/">how to do it in C++</a> - big kudos for the authors. I stole a lot of code from the latter after my debugging frustration :P.</p>
<figure><img src="/images/invaders_chip8.png"/><figcaption>
            <h4>Space Invaders welcome screen running on Crystal</h4>
        </figcaption>
</figure>

<p>You can <a href="https://github.com/emq/chip8.cr">check out my result here</a>. It is very messy, but it should give you a basic idea of how to do it in Crystal and how to use <a href="https://github.com/ysbaddaden/sdl.cr">SDL2 bindings</a> (that I have never had chance to use before in my life).</p>
<hr>
<p><em>Note: <a href="http://nedroid.com/2012/05/honk-the-databus/">Direct source of the comic above</a></em></p>]]></description><content:encoded><![CDATA[<p>It was the middle of a week, around noon I think.</p>
<p>I thought to myself <em>&ldquo;I should write a yet-another NES emulator, that would be awesome!&rdquo;</em>.</p>
<p>A few minutes later I am reading blog posts, articles, browsing repositories.</p>
<p>And I am like <em>&ldquo;Damn it looks complicated&rdquo;</em>.</p>
<figure><img src="/images/2012-05-02-beartato-hackthedatabase.png"/><figcaption>
            <h4>Me about to write NES emulator (Source: nedroid.com)</h4>
        </figcaption>
</figure>

<p>So I decided to start with something much, much simpler - <a href="https://en.wikipedia.org/wiki/CHIP-8">Chip-8</a> that was developed in mid-1970s. Because it&rsquo;s architecture was pretty straightforward it was a project I was able to finish within few days without a rush - and that included whole day+ of debugging why the hell drawing on screen doesn&rsquo;t work as expected (reason: Crystal type interference gave me unsigned int8 on one calculation, but it somehow awkwardly handled overflow of int8 - I didn&rsquo;t have chance to dig into it yet).</p>
<p>I am not going to write any step-by-step guide, because I have found two just fabulous blog posts related to the task - one is about <a href="http://blog.alexanderdickson.com/javascript-chip-8-emulator">writing Chip-8 emulator in Javascript</a>, and the other one is complete guide with detailed explanations <a href="http://www.multigesture.net/articles/how-to-write-an-emulator-chip-8-interpreter/">how to do it in C++</a> - big kudos for the authors. I stole a lot of code from the latter after my debugging frustration :P.</p>
<figure><img src="/images/invaders_chip8.png"/><figcaption>
            <h4>Space Invaders welcome screen running on Crystal</h4>
        </figcaption>
</figure>

<p>You can <a href="https://github.com/emq/chip8.cr">check out my result here</a>. It is very messy, but it should give you a basic idea of how to do it in Crystal and how to use <a href="https://github.com/ysbaddaden/sdl.cr">SDL2 bindings</a> (that I have never had chance to use before in my life).</p>
<hr>
<p><em>Note: <a href="http://nedroid.com/2012/05/honk-the-databus/">Direct source of the comic above</a></em></p>]]></content:encoded></item><item><title>(Kind-of) Gotcha In Postgres Upsert Query</title><link>https://awesomeprogrammer.com/blog/2018/05/26/kind-of-gotcha-in-postgres-upsert-query/</link><pubDate>Sat, 26 May 2018 16:50:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2018/05/26/kind-of-gotcha-in-postgres-upsert-query/</guid><description><![CDATA[<p>I noticed a funny thing happening in my most recent pet project. All of the sudden (auto-incremented) primary key was <em>out of range</em> for my <code>smallint</code> column.</p>
<p>But how come? I had like 15 records in that particular table. It turns out it was all by design and completely makes sense when you think about it, but I managed to miss that anyway.</p>
<p>The problem is when you do an upsert sequence on your primary key will be incremented anyway - it had to be obtained before actual insert so when you take a step back it&rsquo;s like pretty obvious ;). You can read some discussion more about it <a href="https://www.postgresql.org/message-id/CA%2BbJJbz7Gydp%2BDRRe_sgV1d9hP_6PtqxzLBwxCAb8DyZxYmcrQ%40mail.gmail.com">here</a>.</p>
<p>So just if you are doing a lot of inserts with <em>on conflict do nothing</em> maybe you can filter out duplicates before the actual insert? (Which is exactly what I did) Or maybe you can get rid of the autoincrement key in favor of <em>different</em> unique key / use <a href="https://www.postgresql.org/docs/10/static/ddl-constraints.html#DDL-CONSTRAINTS-PRIMARY-KEYS">composite key</a>?</p>
]]></description><content:encoded><![CDATA[<p>I noticed a funny thing happening in my most recent pet project. All of the sudden (auto-incremented) primary key was <em>out of range</em> for my <code>smallint</code> column.</p>
<p>But how come? I had like 15 records in that particular table. It turns out it was all by design and completely makes sense when you think about it, but I managed to miss that anyway.</p>
<p>The problem is when you do an upsert sequence on your primary key will be incremented anyway - it had to be obtained before actual insert so when you take a step back it&rsquo;s like pretty obvious ;). You can read some discussion more about it <a href="https://www.postgresql.org/message-id/CA%2BbJJbz7Gydp%2BDRRe_sgV1d9hP_6PtqxzLBwxCAb8DyZxYmcrQ%40mail.gmail.com">here</a>.</p>
<p>So just if you are doing a lot of inserts with <em>on conflict do nothing</em> maybe you can filter out duplicates before the actual insert? (Which is exactly what I did) Or maybe you can get rid of the autoincrement key in favor of <em>different</em> unique key / use <a href="https://www.postgresql.org/docs/10/static/ddl-constraints.html#DDL-CONSTRAINTS-PRIMARY-KEYS">composite key</a>?</p>
]]></content:encoded></item><item><title>Back To The Crystal - Thoughts on my first app</title><link>https://awesomeprogrammer.com/blog/2018/05/18/back-to-the-crystal-thoughts-on-my-first-app/</link><pubDate>Fri, 18 May 2018 13:30:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2018/05/18/back-to-the-crystal-thoughts-on-my-first-app/</guid><description><![CDATA[<p>So I wrote my first smallish web app in Crystal and it&rsquo;s been running for over two months now without much of supervision (thanks to monit and systemd). It basically, um, <em>collects</em> some metadata from IMDB every week or so. Based on that data I&rsquo;m trying to find underrated movies worth watching. Some general thoughts for future-me:</p>
<ul>
<li>
<p>I like Crystal&rsquo;s HTTP lib, it seems cleaner compared to Ruby (where I immediately switch to excon, faraday or some other gem)</p>
</li>
<li>
<p>Sidekiq port works like a charm and memory usage is sweet. I&rsquo;m not concerned much about speed itself as I need to <em>collect</em> data <em>gently</em> anyway. Pushing jobs from rails app after switching redis connection in Sidekiq is also a painless process - it opens some interesting opportunities for mixing those two languages in production</p>
</li>
<li>
<p>Composing SQL queries get clunky pretty fast as expected (I had some problems in Go once my first app grew). <a href="https://github.com/luckyframework/lucky_record">lucky_record</a> from <a href="https://github.com/luckyframework/lucky">lucky framework</a> looks promising tho. Seems I can&rsquo;t live with some sort of ORM no more.</p>
</li>
<li>
<p>I miss some build-in auto-reload/file watcher and probably I should incorporate one into my editor for Crystal projects</p>
</li>
<li>
<p>I should pay more attention to return types, those are inferred by the compiler, but avoiding nil union types is probably a good idea</p>
</li>
<li>
<p><a href="https://semantic-ui.com/">Semantic UI</a> is really cool for prototyping :P out of the box you get all the building blocks you can use, neat - as a mostly backend developer I found it very handy</p>
</li>
</ul>
<h3 id="what-about-the-app-itself">What about the app itself?</h3>
<p><strong>Update 2019-02-10:</strong> <a href="/blog/2019/02/10/crystal-app-resurrection/">You can find follow-up on the app here</a></p>
<p>You can find it <a href="https://imdb.wojsznis.eu">here</a>. You need to use basic auth: <code>&lt;REDACTED&gt;</code> / <code>&lt;REDACTED&gt;</code> (update VII.2017): have to remove those for now). Because of various <em>reasons</em> I won&rsquo;t be able to publicly release it as Amazon will probably try to shut it down. <a href="https://github.com/ariejan/imdb">Sample reference</a>.</p>
<p>But we can look at the data/fun facts (made easier thanks to <a href="https://www.metabase.com/">metabase</a>). Keep in mind the <em>data</em> is just a small portion of what IMDB is; also I will refer to reviews with ratings only.</p>
<ol>
<li>
<p>So far I have over 126.000 movies in 27 different categories and 2.230.000 reviews (metadata only) for those movies - and those are reviews only with rating included - most of pretty old reviews does not include rating, if I&rsquo;m not mistaken that feature was added later on around year 1998.</p>
</li>
<li>
<p>You can&rsquo;t get past 200th page in categories listing, probably due to Elasticsearch pagination limitations (thanks @sztos for this precious knowledge).</p>
</li>
<li>
<p><a href="https://www.imdb.com/title/tt5174640/">There is a movie</a> written by John Malkovich scheduled to be released in 2115.</p>
</li>
<li>
<p>Seems that IMDB users tend to write reviews when they really like given movie. A little over 20% of collected reviews have 10-stars rating (on the other hand 1-star reviews are 9% of total).</p>
</li>
<li>
<p>In year 2006 around 180.000 new reviews were posted - that&rsquo;s almost 500 reviews a day!</p>
</li>
<li>
<p>The oldest review is for a movie <a href="https://www.imdb.com/title/tt0119237/">Gummo</a>, it was added on July 27, 1998.</p>
</li>
<li>
<p>According to current formula in the app most overrated movie is <a href="https://www.imdb.com/title/tt1520498/">The Oogieloves in the Big Balloon Adventure</a> with a rating of 6.1 and weighted rating based on reviews of 1.8. And the second one is <a href="https://www.imdb.com/title/tt2527336/">Star Wars: Episode VIII - The Last Jedi</a>, which I actually liked ;).</p>
</li>
<li>
<p>Most underrated movie is <a href="https://www.imdb.com/title/tt0270846/">Superbabies: Baby Geniuses 2</a> probably due to troll reviews :P. The second one is <a href="https://www.imdb.com/title/tt4776998/">The Promise</a>.</p>
</li>
<li>
<p>Most up-voted review (2974 of 2994 <em>helpfulness score</em>) is for a movie <a href="https://www.imdb.com/title/tt5537140/">The Red Maple Leaf</a>. Note: at least so far, had major hiccup regarding parsing reviews and data is still being updated.</p>
</li>
<li>
<p>Movies have on average 35 reviews, 7 reviews median. Most reviewed movie is <a href="https://www.imdb.com/title/tt0468569/">The Dark Knight</a> with almost 5100 reviews!</p>
</li>
</ol>
<p>That&rsquo;s it for now, if you found that app somewhat useful let me know :).</p>]]></description><content:encoded><![CDATA[<p>So I wrote my first smallish web app in Crystal and it&rsquo;s been running for over two months now without much of supervision (thanks to monit and systemd). It basically, um, <em>collects</em> some metadata from IMDB every week or so. Based on that data I&rsquo;m trying to find underrated movies worth watching. Some general thoughts for future-me:</p>
<ul>
<li>
<p>I like Crystal&rsquo;s HTTP lib, it seems cleaner compared to Ruby (where I immediately switch to excon, faraday or some other gem)</p>
</li>
<li>
<p>Sidekiq port works like a charm and memory usage is sweet. I&rsquo;m not concerned much about speed itself as I need to <em>collect</em> data <em>gently</em> anyway. Pushing jobs from rails app after switching redis connection in Sidekiq is also a painless process - it opens some interesting opportunities for mixing those two languages in production</p>
</li>
<li>
<p>Composing SQL queries get clunky pretty fast as expected (I had some problems in Go once my first app grew). <a href="https://github.com/luckyframework/lucky_record">lucky_record</a> from <a href="https://github.com/luckyframework/lucky">lucky framework</a> looks promising tho. Seems I can&rsquo;t live with some sort of ORM no more.</p>
</li>
<li>
<p>I miss some build-in auto-reload/file watcher and probably I should incorporate one into my editor for Crystal projects</p>
</li>
<li>
<p>I should pay more attention to return types, those are inferred by the compiler, but avoiding nil union types is probably a good idea</p>
</li>
<li>
<p><a href="https://semantic-ui.com/">Semantic UI</a> is really cool for prototyping :P out of the box you get all the building blocks you can use, neat - as a mostly backend developer I found it very handy</p>
</li>
</ul>
<h3 id="what-about-the-app-itself">What about the app itself?</h3>
<p><strong>Update 2019-02-10:</strong> <a href="/blog/2019/02/10/crystal-app-resurrection/">You can find follow-up on the app here</a></p>
<p>You can find it <a href="https://imdb.wojsznis.eu">here</a>. You need to use basic auth: <code>&lt;REDACTED&gt;</code> / <code>&lt;REDACTED&gt;</code> (update VII.2017): have to remove those for now). Because of various <em>reasons</em> I won&rsquo;t be able to publicly release it as Amazon will probably try to shut it down. <a href="https://github.com/ariejan/imdb">Sample reference</a>.</p>
<p>But we can look at the data/fun facts (made easier thanks to <a href="https://www.metabase.com/">metabase</a>). Keep in mind the <em>data</em> is just a small portion of what IMDB is; also I will refer to reviews with ratings only.</p>
<ol>
<li>
<p>So far I have over 126.000 movies in 27 different categories and 2.230.000 reviews (metadata only) for those movies - and those are reviews only with rating included - most of pretty old reviews does not include rating, if I&rsquo;m not mistaken that feature was added later on around year 1998.</p>
</li>
<li>
<p>You can&rsquo;t get past 200th page in categories listing, probably due to Elasticsearch pagination limitations (thanks @sztos for this precious knowledge).</p>
</li>
<li>
<p><a href="https://www.imdb.com/title/tt5174640/">There is a movie</a> written by John Malkovich scheduled to be released in 2115.</p>
</li>
<li>
<p>Seems that IMDB users tend to write reviews when they really like given movie. A little over 20% of collected reviews have 10-stars rating (on the other hand 1-star reviews are 9% of total).</p>
</li>
<li>
<p>In year 2006 around 180.000 new reviews were posted - that&rsquo;s almost 500 reviews a day!</p>
</li>
<li>
<p>The oldest review is for a movie <a href="https://www.imdb.com/title/tt0119237/">Gummo</a>, it was added on July 27, 1998.</p>
</li>
<li>
<p>According to current formula in the app most overrated movie is <a href="https://www.imdb.com/title/tt1520498/">The Oogieloves in the Big Balloon Adventure</a> with a rating of 6.1 and weighted rating based on reviews of 1.8. And the second one is <a href="https://www.imdb.com/title/tt2527336/">Star Wars: Episode VIII - The Last Jedi</a>, which I actually liked ;).</p>
</li>
<li>
<p>Most underrated movie is <a href="https://www.imdb.com/title/tt0270846/">Superbabies: Baby Geniuses 2</a> probably due to troll reviews :P. The second one is <a href="https://www.imdb.com/title/tt4776998/">The Promise</a>.</p>
</li>
<li>
<p>Most up-voted review (2974 of 2994 <em>helpfulness score</em>) is for a movie <a href="https://www.imdb.com/title/tt5537140/">The Red Maple Leaf</a>. Note: at least so far, had major hiccup regarding parsing reviews and data is still being updated.</p>
</li>
<li>
<p>Movies have on average 35 reviews, 7 reviews median. Most reviewed movie is <a href="https://www.imdb.com/title/tt0468569/">The Dark Knight</a> with almost 5100 reviews!</p>
</li>
</ol>
<p>That&rsquo;s it for now, if you found that app somewhat useful let me know :).</p>]]></content:encoded></item><item><title>Cleanup MacOS disk and get that space back</title><link>https://awesomeprogrammer.com/blog/2018/05/18/cleanup-macos-disk-and-get-that-space-back/</link><pubDate>Fri, 18 May 2018 11:30:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2018/05/18/cleanup-macos-disk-and-get-that-space-back/</guid><description><![CDATA[<p>I looked at my free disk space and I was like <em>What the f*ck?!</em></p>
<p>Quickly I decided to <em>Erase them all and reclaim my space.</em> 🤟</p>
<p>Here is how.</p>
<h5 id="1-homebrew-users">1. Homebrew users</h5>
<p>Get rid of old homebrew packages by running <code>brew cleanup</code>. You might want to update homebrew itself and your packages via
<code>brew update &amp;&amp; brew upgrade</code> while you&rsquo;re at it.</p>
<h5 id="2-rvm-users">2. RVM users</h5>
<p>Uninstall old rubies you no longer use via <code>rvm uninstall &lt;ruby&gt;</code> and cleanup all the junk by running <code>rvm cleanup all</code>.</p>
<h5 id="3-docker-users">3. Docker users</h5>
<p>List your docker images via <code>docker images</code>, remove the one you don&rsquo;t use by running <code>docker image rm &lt;image:tag&gt;</code> and finally nuke all the rest with <code>docker system prune</code>.</p>
<h5 id="4-logs">4. Logs</h5>
<p>You can delete logs from given directory and subdirectories with <code>find . -type f -name '*.log' -delete -print</code> (print added for verbosity). I recommend running it in <code>/usr/local/var/log</code> as well.</p>
<h5 id="5-js-developers">5. JS developers</h5>
<p>You might want to delete node modules from your old projects as well - <code>find . -name 'node_modules' -delete -print</code>. Bye bye modules.</p>
<h5 id="6-git-users">6. Git users</h5>
<p>To aggressively garbage collect all repos (recursively), you can try running this in your <em>projects</em> directory:
<code>find . -name '.git' -print -execdir sh -c 'cd {} &amp;&amp; git gc --aggressive --prune' \;</code></p>
<p>Note: with <code>aggressive</code> flag it might take a while if you have a lot of repositories (or few really big ones).</p>
<h5 id="7-ruby-developers">7. Ruby developers</h5>
<p>You might want to nuke gems bundled to <code>vendor/</code> in your old projects by:</p>
<pre tabindex="0"><code>find . -wholename &#39;*/vendor/bundle&#39; -exec rm -rf &#34;{}&#34; \;
find . -wholename &#39;*/vendor/ruby&#39; -exec rm -rf &#34;{}&#34; \;
</code></pre><h5 id="8-going-further">8. Going further</h5>
<p>You can use tools like <a href="https://www.cleverfiles.com">Disk Drill</a> to visualize disk usage and track down more useless stuff that occupies your disk.</p>
<hr>
<p>I&rsquo;m happy to say to I managed to reclaim almost 30GB of disk space after going through all the process.</p>]]></description><content:encoded><![CDATA[<p>I looked at my free disk space and I was like <em>What the f*ck?!</em></p>
<p>Quickly I decided to <em>Erase them all and reclaim my space.</em> 🤟</p>
<p>Here is how.</p>
<h5 id="1-homebrew-users">1. Homebrew users</h5>
<p>Get rid of old homebrew packages by running <code>brew cleanup</code>. You might want to update homebrew itself and your packages via
<code>brew update &amp;&amp; brew upgrade</code> while you&rsquo;re at it.</p>
<h5 id="2-rvm-users">2. RVM users</h5>
<p>Uninstall old rubies you no longer use via <code>rvm uninstall &lt;ruby&gt;</code> and cleanup all the junk by running <code>rvm cleanup all</code>.</p>
<h5 id="3-docker-users">3. Docker users</h5>
<p>List your docker images via <code>docker images</code>, remove the one you don&rsquo;t use by running <code>docker image rm &lt;image:tag&gt;</code> and finally nuke all the rest with <code>docker system prune</code>.</p>
<h5 id="4-logs">4. Logs</h5>
<p>You can delete logs from given directory and subdirectories with <code>find . -type f -name '*.log' -delete -print</code> (print added for verbosity). I recommend running it in <code>/usr/local/var/log</code> as well.</p>
<h5 id="5-js-developers">5. JS developers</h5>
<p>You might want to delete node modules from your old projects as well - <code>find . -name 'node_modules' -delete -print</code>. Bye bye modules.</p>
<h5 id="6-git-users">6. Git users</h5>
<p>To aggressively garbage collect all repos (recursively), you can try running this in your <em>projects</em> directory:
<code>find . -name '.git' -print -execdir sh -c 'cd {} &amp;&amp; git gc --aggressive --prune' \;</code></p>
<p>Note: with <code>aggressive</code> flag it might take a while if you have a lot of repositories (or few really big ones).</p>
<h5 id="7-ruby-developers">7. Ruby developers</h5>
<p>You might want to nuke gems bundled to <code>vendor/</code> in your old projects by:</p>
<pre tabindex="0"><code>find . -wholename &#39;*/vendor/bundle&#39; -exec rm -rf &#34;{}&#34; \;
find . -wholename &#39;*/vendor/ruby&#39; -exec rm -rf &#34;{}&#34; \;
</code></pre><h5 id="8-going-further">8. Going further</h5>
<p>You can use tools like <a href="https://www.cleverfiles.com">Disk Drill</a> to visualize disk usage and track down more useless stuff that occupies your disk.</p>
<hr>
<p>I&rsquo;m happy to say to I managed to reclaim almost 30GB of disk space after going through all the process.</p>]]></content:encoded></item><item><title>How to install BasicTeX on MacOS via homebrew</title><link>https://awesomeprogrammer.com/blog/2018/05/18/how-to-install-basictex-on-macos-via-homebrew/</link><pubDate>Fri, 18 May 2018 09:50:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2018/05/18/how-to-install-basictex-on-macos-via-homebrew/</guid><description><![CDATA[<p>If you want to install latex on your Mac you have two most common options - install Full MacTeX or smaller distro - Basic TeX. <a href="https://tug.org/mactex/morepackages.html">See package listing here</a>. The first option is easy, but package weights few gigabytes of data you probably won&rsquo;t use anyway. The second one might be problematic for latex newbies like me, so here are some scribbles that hopefully might help someone.</p>
<ul>
<li>
<p>You can install BasicTeX directly from the console if you have homebrew installed via <code>brew cask install basictex</code>.</p>
</li>
<li>
<p>it comes with <code>tlmgr</code> - <em>the native TeX Live Manager</em> and probably you will need to install some missing packages via <code>sudo tlmgr install &lt;package&gt;</code>. I suggest installing <code>collection-fontsrecommended</code> right away. Extra tip: if you will encounter something like <code>Package fontenc Error: Encoding file 'ly1enc.def' not found. (fontenc)</code> just install <code>ly1</code> package to solve it - it wasn&rsquo;t obvious to me at first. You can look up packages online <a href="https://ctan.org/pkg/ly1">here</a>.</p>
</li>
<li>
<p>I had to manually modify <code>PATH</code> on my <a href="https://fishshell.com/">fish shell</a> after installation by adding <code>set PATH /Library/TeX/texbin/ $PATH</code> in <code>~/.config/fish/config.fish</code> (reload shell and check if you can use <code>pdflatex</code> for example).</p>
</li>
<li>
<p>You can compress generated pdfs further via Ghostscript (code stolen from stackoverflow):</p>
</li>
</ul>
<pre tabindex="0"><code>gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dNOPAUSE -dQUIET -dBATCH -sOutputFile=output.pdf input.pdf
</code></pre><p>or you can check out <a href="https://github.com/pts/pdfsizeopt">pdfsizeopt</a>.</p>]]></description><content:encoded><![CDATA[<p>If you want to install latex on your Mac you have two most common options - install Full MacTeX or smaller distro - Basic TeX. <a href="https://tug.org/mactex/morepackages.html">See package listing here</a>. The first option is easy, but package weights few gigabytes of data you probably won&rsquo;t use anyway. The second one might be problematic for latex newbies like me, so here are some scribbles that hopefully might help someone.</p>
<ul>
<li>
<p>You can install BasicTeX directly from the console if you have homebrew installed via <code>brew cask install basictex</code>.</p>
</li>
<li>
<p>it comes with <code>tlmgr</code> - <em>the native TeX Live Manager</em> and probably you will need to install some missing packages via <code>sudo tlmgr install &lt;package&gt;</code>. I suggest installing <code>collection-fontsrecommended</code> right away. Extra tip: if you will encounter something like <code>Package fontenc Error: Encoding file 'ly1enc.def' not found. (fontenc)</code> just install <code>ly1</code> package to solve it - it wasn&rsquo;t obvious to me at first. You can look up packages online <a href="https://ctan.org/pkg/ly1">here</a>.</p>
</li>
<li>
<p>I had to manually modify <code>PATH</code> on my <a href="https://fishshell.com/">fish shell</a> after installation by adding <code>set PATH /Library/TeX/texbin/ $PATH</code> in <code>~/.config/fish/config.fish</code> (reload shell and check if you can use <code>pdflatex</code> for example).</p>
</li>
<li>
<p>You can compress generated pdfs further via Ghostscript (code stolen from stackoverflow):</p>
</li>
</ul>
<pre tabindex="0"><code>gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dNOPAUSE -dQUIET -dBATCH -sOutputFile=output.pdf input.pdf
</code></pre><p>or you can check out <a href="https://github.com/pts/pdfsizeopt">pdfsizeopt</a>.</p>]]></content:encoded></item><item><title>Back To The Crystal - Decorators and presenter ideas for Kemal</title><link>https://awesomeprogrammer.com/blog/2018/04/01/back-to-the-crystal-decorators-and-presenter-ideas-for-kemal/</link><pubDate>Sun, 01 Apr 2018 08:24:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2018/04/01/back-to-the-crystal-decorators-and-presenter-ideas-for-kemal/</guid><description><![CDATA[<p>Note: <a href="/blog/2018/03/21/back-to-the-crystal---choosing-web-framework/">Checkout part one</a> about web frameworks options in Crystal.</p>
<p>Once your Kemal app grows a little bit it seems obnoxious to declare tons of variables that are automatically passed down to the view.</p>
<p>Personally what I like to do when I&rsquo;m feeling that the amount of variables is too damn high and the logic used inside the Kemal router is getting hard to reuse - is to introduce a <em>presenter</em> (simple class, no magic there) that basically wraps whole logic responsible for initializing variables I will need. And usually I drop-in some convenient helper methods.</p>
<p>To decouple it even further and make it a little bit more flexible I tend to implement decorators. If you use <a href="https://crystal-lang.org/api/0.24.1/ECR.html">ecr</a>
there is handy <code>def_to_s</code> class method that allows you to specify template file to use when <code>to_s</code> is called on your object. And it seems to me that it&rsquo;s a perfect combination. The use case is as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span><span style="color:#75715e">&lt;!-- somewhere in static view we can just call --&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">&lt;</span>%- @movies.each_with_index do |movie, idx| -%&gt;
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">&lt;</span>%= Decorators::Movie.new(@presenter.category, movie, idx + 1) %&gt;
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">&lt;</span>% end %&gt;
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-crystal" data-lang="crystal"><span style="display:flex;"><span><span style="color:#75715e"># src/decorators/movie.cr</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">module</span> Decorators
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Movie</span>
</span></span><span style="display:flex;"><span>    @percentage : Int32
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">initialize</span>(@category : String, @movie : Mappings<span style="color:#f92672">::</span>Movie, @number : Int32)
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># here I calculate @percentage that I will later use as width of a progress bar</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># inside that partial you will have access to all instance variables</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">ECR</span><span style="color:#f92672">.</span>def_to_s <span style="color:#e6db74">&#34;src/views/movies/_movie.ecr&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>I think it&rsquo;s a neat and convenient approach for building small static sites with Kemal.</p>
]]></description><content:encoded><![CDATA[<p>Note: <a href="/blog/2018/03/21/back-to-the-crystal---choosing-web-framework/">Checkout part one</a> about web frameworks options in Crystal.</p>
<p>Once your Kemal app grows a little bit it seems obnoxious to declare tons of variables that are automatically passed down to the view.</p>
<p>Personally what I like to do when I&rsquo;m feeling that the amount of variables is too damn high and the logic used inside the Kemal router is getting hard to reuse - is to introduce a <em>presenter</em> (simple class, no magic there) that basically wraps whole logic responsible for initializing variables I will need. And usually I drop-in some convenient helper methods.</p>
<p>To decouple it even further and make it a little bit more flexible I tend to implement decorators. If you use <a href="https://crystal-lang.org/api/0.24.1/ECR.html">ecr</a>
there is handy <code>def_to_s</code> class method that allows you to specify template file to use when <code>to_s</code> is called on your object. And it seems to me that it&rsquo;s a perfect combination. The use case is as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span><span style="color:#75715e">&lt;!-- somewhere in static view we can just call --&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">&lt;</span>%- @movies.each_with_index do |movie, idx| -%&gt;
</span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">&lt;</span>%= Decorators::Movie.new(@presenter.category, movie, idx + 1) %&gt;
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">&lt;</span>% end %&gt;
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-crystal" data-lang="crystal"><span style="display:flex;"><span><span style="color:#75715e"># src/decorators/movie.cr</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">module</span> Decorators
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Movie</span>
</span></span><span style="display:flex;"><span>    @percentage : Int32
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">initialize</span>(@category : String, @movie : Mappings<span style="color:#f92672">::</span>Movie, @number : Int32)
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># here I calculate @percentage that I will later use as width of a progress bar</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># inside that partial you will have access to all instance variables</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">ECR</span><span style="color:#f92672">.</span>def_to_s <span style="color:#e6db74">&#34;src/views/movies/_movie.ecr&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>I think it&rsquo;s a neat and convenient approach for building small static sites with Kemal.</p>
]]></content:encoded></item><item><title>Back To The Crystal - Background workers and deployment</title><link>https://awesomeprogrammer.com/blog/2018/03/22/back-to-the-crystal-background-workers-and-deployment/</link><pubDate>Thu, 22 Mar 2018 17:24:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2018/03/22/back-to-the-crystal-background-workers-and-deployment/</guid><description><![CDATA[<p><a href="/blog/2018/03/21/back-to-the-crystal---choosing-web-framework/">In previous post</a> I briefly discussed web frameworks options for Crystal. Once you have your main web application up and running usually pretty soon you need some sort of background worker - to do some heavy lifting, fetch some data and whatnot.</p>
<p>In Ruby world you would probably choose between two most popular and straightforward redis-based solutions - <a href="https://github.com/resque/resque">resque</a> and <a href="https://github.com/mperham/sidekiq">sidekiq</a> (obviously there is <a href="https://github.com/markets/awesome-ruby#queues-and-messaging">much more to choose from</a>).</p>
<p>Thanks to <a href="https://www.mikeperham.com">Mike Perham</a> there is a sweet Ruby-compatible Crystal port of Sidekiq - called, <a href="https://github.com/mperham/sidekiq.cr/">Sidekiq.cr</a> (who would guess).</p>
<p>I will skip here getting started guide as everything is nicely described in a <a href="https://github.com/mperham/sidekiq.cr/wiki/Getting-Started">official wiki</a>. Basically for a very simple project in the end you should have up to three different binaries:</p>
<ul>
<li>your main web application</li>
<li>sidekiq binary for your worker(s) (<a href="https://github.com/mperham/sidekiq.cr/wiki/Getting-Started#the-sidekiq-process">references</a>)</li>
<li>optional sidekiq web ui panel /that is also served by Kemal BTW/ (<a href="https://github.com/mperham/sidekiq.cr/wiki/Getting-Started#the-web-ui">references</a>)</li>
</ul>
<p>Seems like a much hassle, but actually this is pretty nice - you can compile minimal binaries and you will have more granular control over deployment. Maybe you will need to change something in your web application that doesn&rsquo;t necessary affects workers, so you can modify only web-part, recompile only web app and re-deploy it without even touching the rest - and in Crystal it&rsquo;s super easy to <a href="https://crystal-lang.org/docs/syntax_and_semantics/requiring_files.html">require needed modules</a> (I&rsquo;m looking at you go import) so you have that separation out of the box.</p>
<p>You have many options to chose from when it comes to managing your system processes - I used <a href="http://supervisord.org/">supervisor</a> for a while, <a href="https://wiki.debian.org/LSBInitScripts">init.d scripts</a> few times along with <a href="https://packages.debian.org/jessie/monit">monit</a>, you can pack your binary inside a mini docker image and use that (as docker have handy <a href="https://docs.docker.com/engine/reference/run/#restart-policies---restart">restart policies</a>). This time I went with <a href="https://github.com/systemd/systemd">systemd</a> as it kinda seems like nowadays standard in devops world - and it turned out to be quite easy to use after all.</p>
<p>What&rsquo;s even better sidekiq.cr comes with <a href="https://github.com/mperham/sidekiq.cr/blob/master/examples/sidekiq.service">two</a> different <a href="https://github.com/mperham/sidekiq.cr/blob/master/examples/sidekiqweb.service">examples</a> that you can use for daemon-izing your processes.</p>
<p>But let&rsquo;s take few steps back first.</p>
<h3 id="prerequisites">Prerequisites</h3>
<ul>
<li>Bare metal/VPS/whatever Linux box with a systemd (most major Linux distros have it) and Redis configured - I&rsquo;m using Ubuntu 16.04</li>
<li>Dedicated non-root user for your crystal app (<a href="https://www.tecmint.com/add-users-in-linux/">you can read this guide</a>), let&rsquo;s say that user is called <code>crystalapp</code> and that user have it&rsquo;s own group and home directory, as we will be serving everything from <code>/home/crystalapp/app</code> folder</li>
<li>installed <a href="http://docs.ansible.com/ansible/latest/intro_installation.html#installation">Ansible</a> (I&rsquo;m running 2.4.3.0) and some basic Ansible knowledge</li>
<li>some basic linux/shell knowledge, there is a lot of topic to cover here, but I will try to keep it very short and not go into details</li>
</ul>
<h3 id="assumptions--simplifications">Assumptions &amp; simplifications</h3>
<ul>
<li>We will deploy three binaries to a single machine and spawn three processes - this is important because you can increase concurrency for a single sidekiq process, but for Kemal you would have to spawn extra processes and do some load balancing (<a href="https://github.com/kemalcr/kemal/issues/282">references/discussion</a>)</li>
<li>Downtime during deployment is acceptable</li>
<li>Too keep it simple we will always override old binary (no rollback) and execute (compile &amp; deploy) everything locally</li>
<li>Once again - this is single machine setup for your pet project, you don&rsquo;t need a fleet of virtual servers running Mesos, Kubernetes or whatever and three load balancers in two physical locations in front of all that. Basic linux tooling will be more than enough :).</li>
</ul>
<h3 id="configuring-systemd-services">Configuring systemd services</h3>
<p><strong>Note</strong>: In general you should use Ansible or other automation tool for configuring your server and try not to touch any configuration manually.</p>
<p>Configure three services by putting those three <code>.service</code> files under <code>/etc/systemd/system/</code></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># /etc/systemd/system/crystalapp-sidekiq.service</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Unit<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>Description<span style="color:#f92672">=</span>crystalapp-sidekiq
</span></span><span style="display:flex;"><span><span style="color:#75715e"># start only once the network and logging subsystems are available</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># we should probably wait for redis here as well, but that depends</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># how you run redis - eg. redis-server.service or maybe it&#39;s running</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># in docker so wait for docker.service (in case of docker you would</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># probably have to make sure the actual container exists) - feel free</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># to tweak it further</span>
</span></span><span style="display:flex;"><span>After<span style="color:#f92672">=</span>syslog.target network.target
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Service<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>Type<span style="color:#f92672">=</span>simple
</span></span><span style="display:flex;"><span><span style="color:#75715e"># You can provide some ENVs here, eg. for redis</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Environment=&#34;REDIS_PROVIDER=REDIS_URL&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Environment=&#34;REDIS_URL=redis://127.0.0.1:2104/0&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Or you can load it via something like cr-dotenv</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># during application boot</span>
</span></span><span style="display:flex;"><span>WorkingDirectory<span style="color:#f92672">=</span>/home/crystalapp/app
</span></span><span style="display:flex;"><span><span style="color:#75715e"># You can provide more sidekiq options here, try running it with -h parameter</span>
</span></span><span style="display:flex;"><span>ExecStart<span style="color:#f92672">=</span>/home/crystalapp/app/sidekiq -e production
</span></span><span style="display:flex;"><span>User<span style="color:#f92672">=</span>crystalapp
</span></span><span style="display:flex;"><span>Group<span style="color:#f92672">=</span>crystalapp
</span></span><span style="display:flex;"><span>UMask<span style="color:#f92672">=</span><span style="color:#ae81ff">0002</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># restart on failure</span>
</span></span><span style="display:flex;"><span>RestartSec<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>Restart<span style="color:#f92672">=</span>on-failure
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># and log to /var/log/syslog</span>
</span></span><span style="display:flex;"><span>StandardOutput<span style="color:#f92672">=</span>syslog
</span></span><span style="display:flex;"><span>StandardError<span style="color:#f92672">=</span>syslog
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Install<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>WantedBy<span style="color:#f92672">=</span>multi-user.target
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># /etc/systemd/system/crystalapp-web.service</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Unit<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>Description<span style="color:#f92672">=</span>crystalapp-web
</span></span><span style="display:flex;"><span>After<span style="color:#f92672">=</span>syslog.target network.target
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Service<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>Type<span style="color:#f92672">=</span>simple
</span></span><span style="display:flex;"><span>Environment<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;KEMAL_ENV=production&#34;</span>
</span></span><span style="display:flex;"><span>WorkingDirectory<span style="color:#f92672">=</span>/home/crystalapp/app
</span></span><span style="display:flex;"><span>ExecStart<span style="color:#f92672">=</span>/home/crystalapp/app/web -p <span style="color:#ae81ff">3000</span>
</span></span><span style="display:flex;"><span>User<span style="color:#f92672">=</span>crystalapp
</span></span><span style="display:flex;"><span>Group<span style="color:#f92672">=</span>crystalapp
</span></span><span style="display:flex;"><span>UMask<span style="color:#f92672">=</span><span style="color:#ae81ff">0002</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>RestartSec<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>Restart<span style="color:#f92672">=</span>on-failure
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>StandardOutput<span style="color:#f92672">=</span>syslog
</span></span><span style="display:flex;"><span>StandardError<span style="color:#f92672">=</span>syslog
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Install<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>WantedBy<span style="color:#f92672">=</span>multi-user.target
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># /etc/systemd/system/crystalapp-kiqweb.service</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This is our sidekiq web ui that we will run on port 3001</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Unit<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>Description<span style="color:#f92672">=</span>crystalapp-kiqweb
</span></span><span style="display:flex;"><span>After<span style="color:#f92672">=</span>syslog.target network.target
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Service<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>Type<span style="color:#f92672">=</span>simple
</span></span><span style="display:flex;"><span>Environment<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;KEMAL_ENV=production&#34;</span>
</span></span><span style="display:flex;"><span>WorkingDirectory<span style="color:#f92672">=</span>/home/crystalapp/app
</span></span><span style="display:flex;"><span>ExecStart<span style="color:#f92672">=</span>/home/crystalapp/app/kiqweb -p <span style="color:#ae81ff">3001</span>
</span></span><span style="display:flex;"><span>User<span style="color:#f92672">=</span>crystalapp
</span></span><span style="display:flex;"><span>Group<span style="color:#f92672">=</span>crystalapp
</span></span><span style="display:flex;"><span>UMask<span style="color:#f92672">=</span><span style="color:#ae81ff">0002</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>RestartSec<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>Restart<span style="color:#f92672">=</span>on-failure
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>StandardOutput<span style="color:#f92672">=</span>syslog
</span></span><span style="display:flex;"><span>StandardError<span style="color:#f92672">=</span>syslog
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Install<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>WantedBy<span style="color:#f92672">=</span>multi-user.target
</span></span></code></pre></div><p>Ok that seems like quite a lot, but you can notice it&rsquo;s mostly a boilerplate and configuration itself is very minimal.</p>
<p>So now you can enable your three services via:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>systemctl enable <span style="color:#f92672">{</span>crystalapp-sidekiq,crystalapp-web,crystalapp-kiqweb<span style="color:#f92672">}</span>
</span></span></code></pre></div><p>and have control over them via:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>systemctl <span style="color:#f92672">{</span>start,stop,restart<span style="color:#f92672">}</span> <span style="color:#f92672">{</span>crystalapp-sidekiq,crystalapp-web,crystalapp-kiqweb<span style="color:#f92672">}</span>
</span></span></code></pre></div><p>Output will be logged to syslog - for convenience you can tail logs from given service by <code>journalctl -f -u {service_name}</code> command.</p>
<p>So we have web app running on port <code>3000</code>, siekiq panel running on port <code>3001</code> and sidekiq process running on the system - normally you would proxy those two ports via nginx/haproxy/caddy/traefik or whatever, but this should do the job as a proof of concept.</p>
<h3 id="binaries-for-linux-64-bit">Binaries for Linux 64-Bit</h3>
<p>Next we should automate the deployment process. Ideally we should just compile binaries <em>somewhere</em> and put them on the system. So if you have a CI environment you can setup it there and basically automate everything, let&rsquo;s assume we will do everything locally.</p>
<p>This is where it gets a little bit tricky if you&rsquo;re running a different architecture than your server. So I&rsquo;m on Mac and I need to build Linux 64bit binaries, cross compilation in Crystal <a href="https://crystal-lang.org/docs/syntax_and_semantics/cross-compilation.html">is possible</a>, but not really that straightforward and convenient. Also I don&rsquo;t want Crystal with all dependencies on my production server.</p>
<p>What I decided to do is simply create a bash script and use <a href="https://hub.docker.com/r/crystallang/crystal/">official Crystal docker image</a>. There is another problem, because I&rsquo;m simply mounting whole project directory <code>lib/</code> folder is mounted as well, aaand those packages are for different architecture (there will be problem when you&rsquo;re using some Crystal C-Bindings), so what I did - I just moved the folder and called it a day :P. Lame? Most definitely. It does the job here? Yup.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e"># put it bin/compile in root of your project and chmod +x it</span>
</span></span><span style="display:flex;"><span>mv lib/ lib_darwin/
</span></span><span style="display:flex;"><span>mv lib_linux lib/
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker run --rm --volume <span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>:/app --workdir /app crystallang/crystal:0.24.2 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  /bin/bash -c <span style="color:#e6db74">&#34;crystal deps; crystal build --progress --release --stats src/</span>$1<span style="color:#e6db74">.cr&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>mv lib/ lib_linux/
</span></span><span style="display:flex;"><span>mv lib_darwin lib/
</span></span></code></pre></div><p>With this simple script I can compile anything by calling <code>bin/compile {crystal_file}</code> and get linux binary I can then put on my server. Because I move directories during the process it doesn&rsquo;t mess up my development (MacOS) environment.</p>
<h3 id="deployment-automation-with-ansible">Deployment automation with Ansible</h3>
<p>Ok so now it would be nice to automate the whole process, we have our services, we have our binaries. There is still one problem tho, our <code>crystalapp</code> user cannot stop/start those services due to insufficient system permissions!</p>
<p>But there is a simple workaround for that with <code>sudoers</code> configuration. Let&rsquo;s create new file under <code>/etc/sudoers.d/</code> directory (it&rsquo;s normally auto loaded - please take a look at your <code>/etc/sudoers</code> file for more useful information)</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># /etc/sudoers.d/crystalapp (don&#39;t put any dots in the filename because it won&#39;t be loaded)</span>
</span></span><span style="display:flex;"><span>Cmnd_Alias MANAGE_APP_CMDS <span style="color:#f92672">=</span> /bin/systemctl start crystalapp-*, /bin/systemctl stop crystalapp-*, /bin/systemctl restart crystalapp-*
</span></span><span style="display:flex;"><span>crystalapp ALL<span style="color:#f92672">=(</span>ALL<span style="color:#f92672">)</span> NOPASSWD: MANAGE_APP_CMDS
</span></span></code></pre></div><p>With such simple modification <code>crystalapp</code> user will be able to run <code>sudo systemctl stop crystalapp-web</code> without password prompt and it won&rsquo;t affect any other services/commands, so we&rsquo;re pretty safe here. You can read <a href="https://askubuntu.com/questions/692701/allowing-user-to-run-systemctl-systemd-services-without-password">this stackoverflow question</a> for more references.</p>
<p>Now back to ansible configuration.</p>
<p>My <code>hosts</code> file looks like this. I specify <code>ansible_user</code> as I want to run playbook with this particular user.</p>
<pre tabindex="0"><code class="language-ansible" data-lang="ansible">ip-of-the-server ansible_user=crystalapp app_directory=~/web
</code></pre><p>Because my setup for workers is quite specific I will only show you basic example how you can deploy your <code>web</code> app, but you can get the general idea from there, nothing fancy is happening. It&rsquo;s worth noticing that I&rsquo;m running this from a <code>deploy/</code> directory - thus those <code>../</code> directory changes you see below.</p>
<p>We&rsquo;re simply compiling binary locally, stopping web service on the remote, copying new binary and starting service back up.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># web.yml</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">hosts</span>: <span style="color:#ae81ff">all</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">vars</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">binary_name</span>: <span style="color:#ae81ff">web</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tasks</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Compile binary</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">local_action</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">module</span>: <span style="color:#ae81ff">command</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">args</span>: <span style="color:#e6db74">&#34;bin/compile {{ binary_name }}&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">chdir</span>: <span style="color:#ae81ff">../</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># we need to user shell module here because of limitations mentioned here:</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># https://docs.ansible.com/ansible/latest/become.html#can-t-limit-escalation-to-certain-commands</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Stop web service</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">shell</span>: <span style="color:#ae81ff">sudo systemctl stop crystalapp-web</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Copy new binary</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">copy</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">src</span>: <span style="color:#e6db74">&#34;../{{ binary_name }}&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">dest</span>: <span style="color:#e6db74">&#34;{{ app_directory }}/{{ binary_name }}&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Start web service</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">shell</span>: <span style="color:#ae81ff">sudo systemctl start crystalapp-web</span>
</span></span></code></pre></div><p>Run <code>ansible-playbook -i hosts web.yml</code> and hopefully it will just work ;).</p>
<p>Now go and build some Crystal apps, cheers!</p>
]]></description><content:encoded><![CDATA[<p><a href="/blog/2018/03/21/back-to-the-crystal---choosing-web-framework/">In previous post</a> I briefly discussed web frameworks options for Crystal. Once you have your main web application up and running usually pretty soon you need some sort of background worker - to do some heavy lifting, fetch some data and whatnot.</p>
<p>In Ruby world you would probably choose between two most popular and straightforward redis-based solutions - <a href="https://github.com/resque/resque">resque</a> and <a href="https://github.com/mperham/sidekiq">sidekiq</a> (obviously there is <a href="https://github.com/markets/awesome-ruby#queues-and-messaging">much more to choose from</a>).</p>
<p>Thanks to <a href="https://www.mikeperham.com">Mike Perham</a> there is a sweet Ruby-compatible Crystal port of Sidekiq - called, <a href="https://github.com/mperham/sidekiq.cr/">Sidekiq.cr</a> (who would guess).</p>
<p>I will skip here getting started guide as everything is nicely described in a <a href="https://github.com/mperham/sidekiq.cr/wiki/Getting-Started">official wiki</a>. Basically for a very simple project in the end you should have up to three different binaries:</p>
<ul>
<li>your main web application</li>
<li>sidekiq binary for your worker(s) (<a href="https://github.com/mperham/sidekiq.cr/wiki/Getting-Started#the-sidekiq-process">references</a>)</li>
<li>optional sidekiq web ui panel /that is also served by Kemal BTW/ (<a href="https://github.com/mperham/sidekiq.cr/wiki/Getting-Started#the-web-ui">references</a>)</li>
</ul>
<p>Seems like a much hassle, but actually this is pretty nice - you can compile minimal binaries and you will have more granular control over deployment. Maybe you will need to change something in your web application that doesn&rsquo;t necessary affects workers, so you can modify only web-part, recompile only web app and re-deploy it without even touching the rest - and in Crystal it&rsquo;s super easy to <a href="https://crystal-lang.org/docs/syntax_and_semantics/requiring_files.html">require needed modules</a> (I&rsquo;m looking at you go import) so you have that separation out of the box.</p>
<p>You have many options to chose from when it comes to managing your system processes - I used <a href="http://supervisord.org/">supervisor</a> for a while, <a href="https://wiki.debian.org/LSBInitScripts">init.d scripts</a> few times along with <a href="https://packages.debian.org/jessie/monit">monit</a>, you can pack your binary inside a mini docker image and use that (as docker have handy <a href="https://docs.docker.com/engine/reference/run/#restart-policies---restart">restart policies</a>). This time I went with <a href="https://github.com/systemd/systemd">systemd</a> as it kinda seems like nowadays standard in devops world - and it turned out to be quite easy to use after all.</p>
<p>What&rsquo;s even better sidekiq.cr comes with <a href="https://github.com/mperham/sidekiq.cr/blob/master/examples/sidekiq.service">two</a> different <a href="https://github.com/mperham/sidekiq.cr/blob/master/examples/sidekiqweb.service">examples</a> that you can use for daemon-izing your processes.</p>
<p>But let&rsquo;s take few steps back first.</p>
<h3 id="prerequisites">Prerequisites</h3>
<ul>
<li>Bare metal/VPS/whatever Linux box with a systemd (most major Linux distros have it) and Redis configured - I&rsquo;m using Ubuntu 16.04</li>
<li>Dedicated non-root user for your crystal app (<a href="https://www.tecmint.com/add-users-in-linux/">you can read this guide</a>), let&rsquo;s say that user is called <code>crystalapp</code> and that user have it&rsquo;s own group and home directory, as we will be serving everything from <code>/home/crystalapp/app</code> folder</li>
<li>installed <a href="http://docs.ansible.com/ansible/latest/intro_installation.html#installation">Ansible</a> (I&rsquo;m running 2.4.3.0) and some basic Ansible knowledge</li>
<li>some basic linux/shell knowledge, there is a lot of topic to cover here, but I will try to keep it very short and not go into details</li>
</ul>
<h3 id="assumptions--simplifications">Assumptions &amp; simplifications</h3>
<ul>
<li>We will deploy three binaries to a single machine and spawn three processes - this is important because you can increase concurrency for a single sidekiq process, but for Kemal you would have to spawn extra processes and do some load balancing (<a href="https://github.com/kemalcr/kemal/issues/282">references/discussion</a>)</li>
<li>Downtime during deployment is acceptable</li>
<li>Too keep it simple we will always override old binary (no rollback) and execute (compile &amp; deploy) everything locally</li>
<li>Once again - this is single machine setup for your pet project, you don&rsquo;t need a fleet of virtual servers running Mesos, Kubernetes or whatever and three load balancers in two physical locations in front of all that. Basic linux tooling will be more than enough :).</li>
</ul>
<h3 id="configuring-systemd-services">Configuring systemd services</h3>
<p><strong>Note</strong>: In general you should use Ansible or other automation tool for configuring your server and try not to touch any configuration manually.</p>
<p>Configure three services by putting those three <code>.service</code> files under <code>/etc/systemd/system/</code></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># /etc/systemd/system/crystalapp-sidekiq.service</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Unit<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>Description<span style="color:#f92672">=</span>crystalapp-sidekiq
</span></span><span style="display:flex;"><span><span style="color:#75715e"># start only once the network and logging subsystems are available</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># we should probably wait for redis here as well, but that depends</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># how you run redis - eg. redis-server.service or maybe it&#39;s running</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># in docker so wait for docker.service (in case of docker you would</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># probably have to make sure the actual container exists) - feel free</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># to tweak it further</span>
</span></span><span style="display:flex;"><span>After<span style="color:#f92672">=</span>syslog.target network.target
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Service<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>Type<span style="color:#f92672">=</span>simple
</span></span><span style="display:flex;"><span><span style="color:#75715e"># You can provide some ENVs here, eg. for redis</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Environment=&#34;REDIS_PROVIDER=REDIS_URL&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Environment=&#34;REDIS_URL=redis://127.0.0.1:2104/0&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Or you can load it via something like cr-dotenv</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># during application boot</span>
</span></span><span style="display:flex;"><span>WorkingDirectory<span style="color:#f92672">=</span>/home/crystalapp/app
</span></span><span style="display:flex;"><span><span style="color:#75715e"># You can provide more sidekiq options here, try running it with -h parameter</span>
</span></span><span style="display:flex;"><span>ExecStart<span style="color:#f92672">=</span>/home/crystalapp/app/sidekiq -e production
</span></span><span style="display:flex;"><span>User<span style="color:#f92672">=</span>crystalapp
</span></span><span style="display:flex;"><span>Group<span style="color:#f92672">=</span>crystalapp
</span></span><span style="display:flex;"><span>UMask<span style="color:#f92672">=</span><span style="color:#ae81ff">0002</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># restart on failure</span>
</span></span><span style="display:flex;"><span>RestartSec<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>Restart<span style="color:#f92672">=</span>on-failure
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># and log to /var/log/syslog</span>
</span></span><span style="display:flex;"><span>StandardOutput<span style="color:#f92672">=</span>syslog
</span></span><span style="display:flex;"><span>StandardError<span style="color:#f92672">=</span>syslog
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Install<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>WantedBy<span style="color:#f92672">=</span>multi-user.target
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># /etc/systemd/system/crystalapp-web.service</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Unit<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>Description<span style="color:#f92672">=</span>crystalapp-web
</span></span><span style="display:flex;"><span>After<span style="color:#f92672">=</span>syslog.target network.target
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Service<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>Type<span style="color:#f92672">=</span>simple
</span></span><span style="display:flex;"><span>Environment<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;KEMAL_ENV=production&#34;</span>
</span></span><span style="display:flex;"><span>WorkingDirectory<span style="color:#f92672">=</span>/home/crystalapp/app
</span></span><span style="display:flex;"><span>ExecStart<span style="color:#f92672">=</span>/home/crystalapp/app/web -p <span style="color:#ae81ff">3000</span>
</span></span><span style="display:flex;"><span>User<span style="color:#f92672">=</span>crystalapp
</span></span><span style="display:flex;"><span>Group<span style="color:#f92672">=</span>crystalapp
</span></span><span style="display:flex;"><span>UMask<span style="color:#f92672">=</span><span style="color:#ae81ff">0002</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>RestartSec<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>Restart<span style="color:#f92672">=</span>on-failure
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>StandardOutput<span style="color:#f92672">=</span>syslog
</span></span><span style="display:flex;"><span>StandardError<span style="color:#f92672">=</span>syslog
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Install<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>WantedBy<span style="color:#f92672">=</span>multi-user.target
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># /etc/systemd/system/crystalapp-kiqweb.service</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This is our sidekiq web ui that we will run on port 3001</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Unit<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>Description<span style="color:#f92672">=</span>crystalapp-kiqweb
</span></span><span style="display:flex;"><span>After<span style="color:#f92672">=</span>syslog.target network.target
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Service<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>Type<span style="color:#f92672">=</span>simple
</span></span><span style="display:flex;"><span>Environment<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;KEMAL_ENV=production&#34;</span>
</span></span><span style="display:flex;"><span>WorkingDirectory<span style="color:#f92672">=</span>/home/crystalapp/app
</span></span><span style="display:flex;"><span>ExecStart<span style="color:#f92672">=</span>/home/crystalapp/app/kiqweb -p <span style="color:#ae81ff">3001</span>
</span></span><span style="display:flex;"><span>User<span style="color:#f92672">=</span>crystalapp
</span></span><span style="display:flex;"><span>Group<span style="color:#f92672">=</span>crystalapp
</span></span><span style="display:flex;"><span>UMask<span style="color:#f92672">=</span><span style="color:#ae81ff">0002</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>RestartSec<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>Restart<span style="color:#f92672">=</span>on-failure
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>StandardOutput<span style="color:#f92672">=</span>syslog
</span></span><span style="display:flex;"><span>StandardError<span style="color:#f92672">=</span>syslog
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Install<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>WantedBy<span style="color:#f92672">=</span>multi-user.target
</span></span></code></pre></div><p>Ok that seems like quite a lot, but you can notice it&rsquo;s mostly a boilerplate and configuration itself is very minimal.</p>
<p>So now you can enable your three services via:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>systemctl enable <span style="color:#f92672">{</span>crystalapp-sidekiq,crystalapp-web,crystalapp-kiqweb<span style="color:#f92672">}</span>
</span></span></code></pre></div><p>and have control over them via:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>systemctl <span style="color:#f92672">{</span>start,stop,restart<span style="color:#f92672">}</span> <span style="color:#f92672">{</span>crystalapp-sidekiq,crystalapp-web,crystalapp-kiqweb<span style="color:#f92672">}</span>
</span></span></code></pre></div><p>Output will be logged to syslog - for convenience you can tail logs from given service by <code>journalctl -f -u {service_name}</code> command.</p>
<p>So we have web app running on port <code>3000</code>, siekiq panel running on port <code>3001</code> and sidekiq process running on the system - normally you would proxy those two ports via nginx/haproxy/caddy/traefik or whatever, but this should do the job as a proof of concept.</p>
<h3 id="binaries-for-linux-64-bit">Binaries for Linux 64-Bit</h3>
<p>Next we should automate the deployment process. Ideally we should just compile binaries <em>somewhere</em> and put them on the system. So if you have a CI environment you can setup it there and basically automate everything, let&rsquo;s assume we will do everything locally.</p>
<p>This is where it gets a little bit tricky if you&rsquo;re running a different architecture than your server. So I&rsquo;m on Mac and I need to build Linux 64bit binaries, cross compilation in Crystal <a href="https://crystal-lang.org/docs/syntax_and_semantics/cross-compilation.html">is possible</a>, but not really that straightforward and convenient. Also I don&rsquo;t want Crystal with all dependencies on my production server.</p>
<p>What I decided to do is simply create a bash script and use <a href="https://hub.docker.com/r/crystallang/crystal/">official Crystal docker image</a>. There is another problem, because I&rsquo;m simply mounting whole project directory <code>lib/</code> folder is mounted as well, aaand those packages are for different architecture (there will be problem when you&rsquo;re using some Crystal C-Bindings), so what I did - I just moved the folder and called it a day :P. Lame? Most definitely. It does the job here? Yup.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e"># put it bin/compile in root of your project and chmod +x it</span>
</span></span><span style="display:flex;"><span>mv lib/ lib_darwin/
</span></span><span style="display:flex;"><span>mv lib_linux lib/
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker run --rm --volume <span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>:/app --workdir /app crystallang/crystal:0.24.2 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  /bin/bash -c <span style="color:#e6db74">&#34;crystal deps; crystal build --progress --release --stats src/</span>$1<span style="color:#e6db74">.cr&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>mv lib/ lib_linux/
</span></span><span style="display:flex;"><span>mv lib_darwin lib/
</span></span></code></pre></div><p>With this simple script I can compile anything by calling <code>bin/compile {crystal_file}</code> and get linux binary I can then put on my server. Because I move directories during the process it doesn&rsquo;t mess up my development (MacOS) environment.</p>
<h3 id="deployment-automation-with-ansible">Deployment automation with Ansible</h3>
<p>Ok so now it would be nice to automate the whole process, we have our services, we have our binaries. There is still one problem tho, our <code>crystalapp</code> user cannot stop/start those services due to insufficient system permissions!</p>
<p>But there is a simple workaround for that with <code>sudoers</code> configuration. Let&rsquo;s create new file under <code>/etc/sudoers.d/</code> directory (it&rsquo;s normally auto loaded - please take a look at your <code>/etc/sudoers</code> file for more useful information)</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># /etc/sudoers.d/crystalapp (don&#39;t put any dots in the filename because it won&#39;t be loaded)</span>
</span></span><span style="display:flex;"><span>Cmnd_Alias MANAGE_APP_CMDS <span style="color:#f92672">=</span> /bin/systemctl start crystalapp-*, /bin/systemctl stop crystalapp-*, /bin/systemctl restart crystalapp-*
</span></span><span style="display:flex;"><span>crystalapp ALL<span style="color:#f92672">=(</span>ALL<span style="color:#f92672">)</span> NOPASSWD: MANAGE_APP_CMDS
</span></span></code></pre></div><p>With such simple modification <code>crystalapp</code> user will be able to run <code>sudo systemctl stop crystalapp-web</code> without password prompt and it won&rsquo;t affect any other services/commands, so we&rsquo;re pretty safe here. You can read <a href="https://askubuntu.com/questions/692701/allowing-user-to-run-systemctl-systemd-services-without-password">this stackoverflow question</a> for more references.</p>
<p>Now back to ansible configuration.</p>
<p>My <code>hosts</code> file looks like this. I specify <code>ansible_user</code> as I want to run playbook with this particular user.</p>
<pre tabindex="0"><code class="language-ansible" data-lang="ansible">ip-of-the-server ansible_user=crystalapp app_directory=~/web
</code></pre><p>Because my setup for workers is quite specific I will only show you basic example how you can deploy your <code>web</code> app, but you can get the general idea from there, nothing fancy is happening. It&rsquo;s worth noticing that I&rsquo;m running this from a <code>deploy/</code> directory - thus those <code>../</code> directory changes you see below.</p>
<p>We&rsquo;re simply compiling binary locally, stopping web service on the remote, copying new binary and starting service back up.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># web.yml</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">hosts</span>: <span style="color:#ae81ff">all</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">vars</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">binary_name</span>: <span style="color:#ae81ff">web</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tasks</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Compile binary</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">local_action</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">module</span>: <span style="color:#ae81ff">command</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">args</span>: <span style="color:#e6db74">&#34;bin/compile {{ binary_name }}&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">chdir</span>: <span style="color:#ae81ff">../</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># we need to user shell module here because of limitations mentioned here:</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># https://docs.ansible.com/ansible/latest/become.html#can-t-limit-escalation-to-certain-commands</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Stop web service</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">shell</span>: <span style="color:#ae81ff">sudo systemctl stop crystalapp-web</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Copy new binary</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">copy</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">src</span>: <span style="color:#e6db74">&#34;../{{ binary_name }}&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">dest</span>: <span style="color:#e6db74">&#34;{{ app_directory }}/{{ binary_name }}&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Start web service</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">shell</span>: <span style="color:#ae81ff">sudo systemctl start crystalapp-web</span>
</span></span></code></pre></div><p>Run <code>ansible-playbook -i hosts web.yml</code> and hopefully it will just work ;).</p>
<p>Now go and build some Crystal apps, cheers!</p>
]]></content:encoded></item><item><title>Back To The Crystal - Choosing web framework</title><link>https://awesomeprogrammer.com/blog/2018/03/21/back-to-the-crystal-choosing-web-framework/</link><pubDate>Wed, 21 Mar 2018 12:16:00 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2018/03/21/back-to-the-crystal-choosing-web-framework/</guid><description><![CDATA[<p>After <a href="/blog/2016/01/02/put-some-go-in-your-ruby-so-you-dont-rust/">over two years</a> (dang that was quick 😳) I have decided to give <a href="https://crystal-lang.org/">Crystal</a> another go. During those two years some nice libraries appeared and language seems quite promising - I hope it will gain some traction in the future. I will try to make a series of short posts regarding Crystal within this month as I think it lacks documentation/references and attention it deserves - and as I&rsquo;m taking some longer time off from IT in general very next month I have to hurry :P.</p>
<p><em>Note 1</em>: I&rsquo;m assuming you&rsquo;re coming from Ruby/Rails world, thus references to other frameworks.</p>
<p><em>Note 2</em>: Those posts are backed by my first crystal web app that is at this point soft-released, hopefully I will be able to show it to the public this month, after it collects some data from 3rd party service (it&rsquo;s time consuming process and I can&rsquo;t really speed it up because of <em>reasons</em>)</p>
<h3 id="web-framework-options-for-crystal">Web framework options for Crystal</h3>
<p>First of all - <a href="https://github.com/veelenga/awesome-crystal">awesome-crystal</a> is, well, awesome starting point when you&rsquo;re trying to build your first app in this language - and not only web app, but app in general.</p>
<p>At this moment it seems we have two major players here - <a href="https://github.com/amberframework/amber">amber</a> and <a href="https://github.com/kemalcr/kemal">kemal</a>.</p>
<p>I think <code>Amber</code> is trying to be a Rails-for-Crystal kind of thing - it have ton of stuff build-in, app generator, support for various databases, migrations, build-in watcher for your app (so binary is being constantly recompiled during development, same for your assets), support for two popular ORMs, routes, you name it - basically all the goodies you are probably used to when working with mature web framework.</p>
<p><code>Kemal</code> on the other hand is basically a Sinatra-for-Crystal, dead simple, you will be up&amp;running within a minute.</p>
<p>I tried Amber, but right away I had problems with <a href="https://github.com/amberframework/granite-orm">granite-orm</a>, it seems last known stable version vs version from <code>master</code> branch produced different kind of errors. I was impatient to start so I quickly gave up on idea about having ORM as I kinda knew it would probably limit me anyway - as I was planning to do some batch upserts and CTE queries in my lovely Postgres. It&rsquo;s still all fresh, so you have to applicate the effort either way &lt;3.</p>
<p>So I went with <strong>Kemal</strong>.</p>
<p><strong>Update 26.03.2018:</strong> It seems <a href="https://github.com/luckyframework/lucky">lucky</a> is getting some traction recently as well and it seems it&rsquo;s under heavy development. It looks really promising and I encourage you to take a look at it as well!</p>
<h3 id="no-orm-so-what-about-database">No ORM, so what about database?</h3>
<p>There is a <a href="https://github.com/crystal-lang/crystal-db/">common db api</a> for Crystal for which you can select specific driver - in my case I went <a href="https://github.com/will/crystal-pg">postgres</a>.</p>
<p>Because Kemal is so minimal you have to take care of the migrations process - you can use Amber&rsquo;s <a href="https://github.com/amberframework/micrate">micrate</a> that is easily pluggable into any kind of application and does the job quite right. You can create <a href="https://github.com/amberframework/micrate#api">simple shell script</a> for convince and call it a day.</p>
<p>Known limitation of crystal-db is <a href="https://github.com/crystal-lang/crystal-db/issues/43">bulk insert</a>, also it seems <code>WHERE IN (?)</code> doesn&rsquo;t quite work for arrays. But if you&rsquo;re in a rush like me you can work around this problem simply by building proper prepared query - you just need to pass corresponding arguments (<code>$1, $2 ... $n</code>) wrapped in parenthesis as needed and in the query and as an <a href="https://crystal-lang.org/docs/database/#exec"><code>exec</code> argument</a> pass flat array - so you end up with prepared query that postgres can understand and arguments that will align nicely with that query.</p>
<p>And if you&rsquo;re not in a hurry you can maybe prepare a pull request that fixes it ;).</p>
<p>Here is my small db class I&rsquo;m using</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-crystal" data-lang="crystal"><span style="display:flex;"><span><span style="color:#66d9ef">require</span> <span style="color:#e6db74">&#34;pg&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">module</span> Models
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Db</span>
</span></span><span style="display:flex;"><span>    @@instance : <span style="color:#66d9ef">DB</span><span style="color:#f92672">::</span>Database
</span></span><span style="display:flex;"><span>    @@instance <span style="color:#f92672">=</span> <span style="color:#66d9ef">DB</span><span style="color:#f92672">.</span>open(<span style="color:#e6db74">&#34;postgresql://</span><span style="color:#e6db74">#{</span><span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#34;DB_USER&#34;</span><span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74">:</span><span style="color:#e6db74">#{</span><span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#34;DB_PASS&#34;</span><span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74">@</span><span style="color:#e6db74">#{</span><span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#34;DB_HOST&#34;</span><span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">#{</span><span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#34;DB_NAME&#34;</span><span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">.</span><span style="color:#a6e22e">instance</span>
</span></span><span style="display:flex;"><span>      @@instance
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">.</span><span style="color:#a6e22e">close</span>
</span></span><span style="display:flex;"><span>      @@instance<span style="color:#f92672">.</span>close
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># this is for my bulk inserts / upserts</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">.</span><span style="color:#a6e22e">preapre_nested</span>(rows, nested_count)
</span></span><span style="display:flex;"><span>      rows<span style="color:#f92672">.</span>times<span style="color:#f92672">.</span>map <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;(&#34;</span> <span style="color:#f92672">+</span> Array<span style="color:#f92672">.</span>new(nested_count) { <span style="color:#f92672">|</span>j<span style="color:#f92672">|</span> <span style="color:#e6db74">&#34;$</span><span style="color:#e6db74">#{</span>(j <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>) <span style="color:#f92672">+</span> (i <span style="color:#f92672">*</span> nested_count)<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> }<span style="color:#f92672">.</span>join(<span style="color:#e6db74">&#34;,&#34;</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;)&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">end</span><span style="color:#f92672">.</span>join(<span style="color:#e6db74">&#34;,&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Next we will take a look how we can hook in <a href="https://github.com/mperham/sidekiq.cr">sidekiq.cr</a> as a background worker and how to deal with deploying binaries to the server.</p>
<p><strong>Update 22.03.2017:</strong></p>
<p>I forgot to mention one kinda controversial/interesting (?) thing in Kemal - it adds it&rsquo;s own custom <code>X-Powered-By</code> header to each response, you can remove it via middleware or by <code>before</code> filter - <a href="https://github.com/kemalcr/kemal/issues/439">click here for more references</a>.</p>]]></description><content:encoded><![CDATA[<p>After <a href="/blog/2016/01/02/put-some-go-in-your-ruby-so-you-dont-rust/">over two years</a> (dang that was quick 😳) I have decided to give <a href="https://crystal-lang.org/">Crystal</a> another go. During those two years some nice libraries appeared and language seems quite promising - I hope it will gain some traction in the future. I will try to make a series of short posts regarding Crystal within this month as I think it lacks documentation/references and attention it deserves - and as I&rsquo;m taking some longer time off from IT in general very next month I have to hurry :P.</p>
<p><em>Note 1</em>: I&rsquo;m assuming you&rsquo;re coming from Ruby/Rails world, thus references to other frameworks.</p>
<p><em>Note 2</em>: Those posts are backed by my first crystal web app that is at this point soft-released, hopefully I will be able to show it to the public this month, after it collects some data from 3rd party service (it&rsquo;s time consuming process and I can&rsquo;t really speed it up because of <em>reasons</em>)</p>
<h3 id="web-framework-options-for-crystal">Web framework options for Crystal</h3>
<p>First of all - <a href="https://github.com/veelenga/awesome-crystal">awesome-crystal</a> is, well, awesome starting point when you&rsquo;re trying to build your first app in this language - and not only web app, but app in general.</p>
<p>At this moment it seems we have two major players here - <a href="https://github.com/amberframework/amber">amber</a> and <a href="https://github.com/kemalcr/kemal">kemal</a>.</p>
<p>I think <code>Amber</code> is trying to be a Rails-for-Crystal kind of thing - it have ton of stuff build-in, app generator, support for various databases, migrations, build-in watcher for your app (so binary is being constantly recompiled during development, same for your assets), support for two popular ORMs, routes, you name it - basically all the goodies you are probably used to when working with mature web framework.</p>
<p><code>Kemal</code> on the other hand is basically a Sinatra-for-Crystal, dead simple, you will be up&amp;running within a minute.</p>
<p>I tried Amber, but right away I had problems with <a href="https://github.com/amberframework/granite-orm">granite-orm</a>, it seems last known stable version vs version from <code>master</code> branch produced different kind of errors. I was impatient to start so I quickly gave up on idea about having ORM as I kinda knew it would probably limit me anyway - as I was planning to do some batch upserts and CTE queries in my lovely Postgres. It&rsquo;s still all fresh, so you have to applicate the effort either way &lt;3.</p>
<p>So I went with <strong>Kemal</strong>.</p>
<p><strong>Update 26.03.2018:</strong> It seems <a href="https://github.com/luckyframework/lucky">lucky</a> is getting some traction recently as well and it seems it&rsquo;s under heavy development. It looks really promising and I encourage you to take a look at it as well!</p>
<h3 id="no-orm-so-what-about-database">No ORM, so what about database?</h3>
<p>There is a <a href="https://github.com/crystal-lang/crystal-db/">common db api</a> for Crystal for which you can select specific driver - in my case I went <a href="https://github.com/will/crystal-pg">postgres</a>.</p>
<p>Because Kemal is so minimal you have to take care of the migrations process - you can use Amber&rsquo;s <a href="https://github.com/amberframework/micrate">micrate</a> that is easily pluggable into any kind of application and does the job quite right. You can create <a href="https://github.com/amberframework/micrate#api">simple shell script</a> for convince and call it a day.</p>
<p>Known limitation of crystal-db is <a href="https://github.com/crystal-lang/crystal-db/issues/43">bulk insert</a>, also it seems <code>WHERE IN (?)</code> doesn&rsquo;t quite work for arrays. But if you&rsquo;re in a rush like me you can work around this problem simply by building proper prepared query - you just need to pass corresponding arguments (<code>$1, $2 ... $n</code>) wrapped in parenthesis as needed and in the query and as an <a href="https://crystal-lang.org/docs/database/#exec"><code>exec</code> argument</a> pass flat array - so you end up with prepared query that postgres can understand and arguments that will align nicely with that query.</p>
<p>And if you&rsquo;re not in a hurry you can maybe prepare a pull request that fixes it ;).</p>
<p>Here is my small db class I&rsquo;m using</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-crystal" data-lang="crystal"><span style="display:flex;"><span><span style="color:#66d9ef">require</span> <span style="color:#e6db74">&#34;pg&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">module</span> Models
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Db</span>
</span></span><span style="display:flex;"><span>    @@instance : <span style="color:#66d9ef">DB</span><span style="color:#f92672">::</span>Database
</span></span><span style="display:flex;"><span>    @@instance <span style="color:#f92672">=</span> <span style="color:#66d9ef">DB</span><span style="color:#f92672">.</span>open(<span style="color:#e6db74">&#34;postgresql://</span><span style="color:#e6db74">#{</span><span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#34;DB_USER&#34;</span><span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74">:</span><span style="color:#e6db74">#{</span><span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#34;DB_PASS&#34;</span><span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74">@</span><span style="color:#e6db74">#{</span><span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#34;DB_HOST&#34;</span><span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">#{</span><span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#34;DB_NAME&#34;</span><span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">.</span><span style="color:#a6e22e">instance</span>
</span></span><span style="display:flex;"><span>      @@instance
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">.</span><span style="color:#a6e22e">close</span>
</span></span><span style="display:flex;"><span>      @@instance<span style="color:#f92672">.</span>close
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># this is for my bulk inserts / upserts</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">protected</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">.</span><span style="color:#a6e22e">preapre_nested</span>(rows, nested_count)
</span></span><span style="display:flex;"><span>      rows<span style="color:#f92672">.</span>times<span style="color:#f92672">.</span>map <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;(&#34;</span> <span style="color:#f92672">+</span> Array<span style="color:#f92672">.</span>new(nested_count) { <span style="color:#f92672">|</span>j<span style="color:#f92672">|</span> <span style="color:#e6db74">&#34;$</span><span style="color:#e6db74">#{</span>(j <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>) <span style="color:#f92672">+</span> (i <span style="color:#f92672">*</span> nested_count)<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> }<span style="color:#f92672">.</span>join(<span style="color:#e6db74">&#34;,&#34;</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;)&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">end</span><span style="color:#f92672">.</span>join(<span style="color:#e6db74">&#34;,&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Next we will take a look how we can hook in <a href="https://github.com/mperham/sidekiq.cr">sidekiq.cr</a> as a background worker and how to deal with deploying binaries to the server.</p>
<p><strong>Update 22.03.2017:</strong></p>
<p>I forgot to mention one kinda controversial/interesting (?) thing in Kemal - it adds it&rsquo;s own custom <code>X-Powered-By</code> header to each response, you can remove it via middleware or by <code>before</code> filter - <a href="https://github.com/kemalcr/kemal/issues/439">click here for more references</a>.</p>]]></content:encoded></item><item><title>5 Short Reflections From 5 Years</title><link>https://awesomeprogrammer.com/blog/2018/02/26/5-short-reflections-from-5-years/</link><pubDate>Mon, 26 Feb 2018 14:14:37 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2018/02/26/5-short-reflections-from-5-years/</guid><description><![CDATA[<p>Last month I decided to quit, to stop being a manager/developer. 5 years ago I was looking for a place where I can improve my rails skills and suddenly I stayed at that place for 5 full years :).</p>
<p>Below are the scribbles, I hope to read them again in few years (dang) and see how far I /hopefully!/ grew from there.</p>
<h5 id="1-seniority--ability-to-deliver">1. Seniority != ability to deliver</h5>
<p>Vast knowledge about the technology, coding paradigms, years of experience doesn&rsquo;t mean given person can actually ship good quality code (or even working solutions) in practice. Nowadays I don&rsquo;t find it strange why in some companies there are obligatory demo days involved during recruitment process - on the other hand market can be so hard it can scare off potential candidates. I guess it depends how big your company is - the marketing/public relation aspects can be important here - you can pull of more if you have good hr-marketing I suppose.</p>
<p>And also let&rsquo;s be honest here - especially in web development you don&rsquo;t need to know all graph algorithms (or whatever) off top of your head, it&rsquo;s <em>usually</em> not what clients are paying for - and at the end of a day often database or even network itself will be your bottleneck anyway.</p>
<h5 id="2-building-teams-is-hard-maintaining-teams-is-next-to-impossible">2. Building teams is hard, maintaining teams is next to impossible</h5>
<p>I consider myself lucky. I have managed to find awesome people that decided to work with me for 1-2 years+. In my calender 1 year is a long-term now. Market is very dynamic, people come and go - and that&rsquo;s a normal thing; if you find some folks that are willing to sail on the same boat with you for 5 days a week - appreciate that.</p>
<p>Look for people that approve your values. I believe sometimes you have to trust your guts - not everything can be quantized. Not every technologically strong person will be a good it, and a bad hire costs a lot. A lot of time, resources, money, and let&rsquo;s not get into collateral cultural damage.</p>
<h5 id="3-nobody-can-tell-you-how-to-be-a-leader">3. Nobody can tell you how to be a leader</h5>
<p>Sadly the &lsquo;general flow&rsquo; in companies is that the person with most experience within the company is promoted to be some kind of manager, team leader etc. And I think is is pretty common that (unfortunately) people just don&rsquo;t get a proper training. So you have to train yourself - and that&rsquo;s what I exactly did. Did some research, read some books, sucked at being a manager but kept doing it as there was no-one around that was willing too. Started an remote R&amp;D office, starting hiring people (I had no idea how to do it, probably I still don&rsquo;t), in 2 years grew from 0 to 6, hired 8 total. It was exhausting and I learned quite a lot.</p>
<h5 id="4-automation-is-a-blessing">4. Automation is a blessing</h5>
<p>Automate tech and automate the process itself. If you don&rsquo;t have CI - set it up, add static code analyzers, build your own toolkit of apps that will help you and your team ship the code. Configure continuous deployment, build&amp;share scripts or whatever can minimize the manual works that is required on daily basis.</p>
<h5 id="5-pat-yourself-on-the-back">5. Pat yourself on the back</h5>
<p>Being some sort of manager&amp;tech-person-at-the-same time is in my opinion very ungrateful job. Coding is easy. Your code works or it does not. If it does not you can usually relatively easy debug and fix it, feel good about it and move to the next thing. Teams on the other hand are quite different. Dealing with global problems, personal problems, unhappy team members becomes a chore. Then you have your managers (and keep in mind - just to keep the big picture in front of you - those managers usually have their own managers), budget strains, product owner(s), requests from teams from different departments, deadlines, overall technology stack (and technology debt) and juggling all of this while trying to avoid bigger problems ahead that only you and few people in the company might be aware of. I had no idea how draining this might be to be honest. So sometimes just pat yourself on the back at the of a day, don&rsquo;t expect anyone else will.</p>]]></description><content:encoded><![CDATA[<p>Last month I decided to quit, to stop being a manager/developer. 5 years ago I was looking for a place where I can improve my rails skills and suddenly I stayed at that place for 5 full years :).</p>
<p>Below are the scribbles, I hope to read them again in few years (dang) and see how far I /hopefully!/ grew from there.</p>
<h5 id="1-seniority--ability-to-deliver">1. Seniority != ability to deliver</h5>
<p>Vast knowledge about the technology, coding paradigms, years of experience doesn&rsquo;t mean given person can actually ship good quality code (or even working solutions) in practice. Nowadays I don&rsquo;t find it strange why in some companies there are obligatory demo days involved during recruitment process - on the other hand market can be so hard it can scare off potential candidates. I guess it depends how big your company is - the marketing/public relation aspects can be important here - you can pull of more if you have good hr-marketing I suppose.</p>
<p>And also let&rsquo;s be honest here - especially in web development you don&rsquo;t need to know all graph algorithms (or whatever) off top of your head, it&rsquo;s <em>usually</em> not what clients are paying for - and at the end of a day often database or even network itself will be your bottleneck anyway.</p>
<h5 id="2-building-teams-is-hard-maintaining-teams-is-next-to-impossible">2. Building teams is hard, maintaining teams is next to impossible</h5>
<p>I consider myself lucky. I have managed to find awesome people that decided to work with me for 1-2 years+. In my calender 1 year is a long-term now. Market is very dynamic, people come and go - and that&rsquo;s a normal thing; if you find some folks that are willing to sail on the same boat with you for 5 days a week - appreciate that.</p>
<p>Look for people that approve your values. I believe sometimes you have to trust your guts - not everything can be quantized. Not every technologically strong person will be a good it, and a bad hire costs a lot. A lot of time, resources, money, and let&rsquo;s not get into collateral cultural damage.</p>
<h5 id="3-nobody-can-tell-you-how-to-be-a-leader">3. Nobody can tell you how to be a leader</h5>
<p>Sadly the &lsquo;general flow&rsquo; in companies is that the person with most experience within the company is promoted to be some kind of manager, team leader etc. And I think is is pretty common that (unfortunately) people just don&rsquo;t get a proper training. So you have to train yourself - and that&rsquo;s what I exactly did. Did some research, read some books, sucked at being a manager but kept doing it as there was no-one around that was willing too. Started an remote R&amp;D office, starting hiring people (I had no idea how to do it, probably I still don&rsquo;t), in 2 years grew from 0 to 6, hired 8 total. It was exhausting and I learned quite a lot.</p>
<h5 id="4-automation-is-a-blessing">4. Automation is a blessing</h5>
<p>Automate tech and automate the process itself. If you don&rsquo;t have CI - set it up, add static code analyzers, build your own toolkit of apps that will help you and your team ship the code. Configure continuous deployment, build&amp;share scripts or whatever can minimize the manual works that is required on daily basis.</p>
<h5 id="5-pat-yourself-on-the-back">5. Pat yourself on the back</h5>
<p>Being some sort of manager&amp;tech-person-at-the-same time is in my opinion very ungrateful job. Coding is easy. Your code works or it does not. If it does not you can usually relatively easy debug and fix it, feel good about it and move to the next thing. Teams on the other hand are quite different. Dealing with global problems, personal problems, unhappy team members becomes a chore. Then you have your managers (and keep in mind - just to keep the big picture in front of you - those managers usually have their own managers), budget strains, product owner(s), requests from teams from different departments, deadlines, overall technology stack (and technology debt) and juggling all of this while trying to avoid bigger problems ahead that only you and few people in the company might be aware of. I had no idea how draining this might be to be honest. So sometimes just pat yourself on the back at the of a day, don&rsquo;t expect anyone else will.</p>]]></content:encoded></item><item><title>Setup and Tooling for small project as a single developer</title><link>https://awesomeprogrammer.com/blog/2018/02/25/setup-and-tooling-for-small-project-as-a-single-developer/</link><pubDate>Sun, 25 Feb 2018 13:59:50 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2018/02/25/setup-and-tooling-for-small-project-as-a-single-developer/</guid><description><![CDATA[<p>As I&rsquo;m still fooling around with my old pet project I decided to sum up current setup I&rsquo;m on - from the basic things like where I host my stuff to what kind of tools/products I use, and how much does it cost me in the end (spoiler: I&rsquo;m cheap as hell :P) So let&rsquo;s see it.</p>
<h4 id="1-hosting">1. Hosting</h4>
<p>Bare-metal, I&rsquo;m an old folk and I change vendors whenever I feel I need to ;-).</p>
<p>I was on two different shared hosting for a while, then moved to <a href="https://www.soyoustart.com/">SYS</a> (OVH dedicated), then to <a href="https://www.ovh.pl/vps/vps-ssd.xml">OVH VPS</a> (Note: PL link) and currently I&rsquo;m on <a href="https://www.hetzner.com/cloud">Hetzner Cloud</a> - basically downscaled everything x8 within a last year and a half. I keep there three static sites served via Caddy, Rails app (unicorn + sidekiq), docker with mariadb, elasticsearch and redis containers and a single static Go binary.</p>
<p>Plus I still have plenty of room for experiments when needed. I killed my servers many times in the past (due to misconfiguration), but that&rsquo;s the great opportunity to learn (hello netboot rescue) and I don&rsquo;t really care about 100% uptime.</p>
<p><strong>Cost: around €5/mo ($6.2/mo) with few snapshots</strong> (Note: I use that box as storage box as well)</p>
<h4 id="2-version-controlgit">2. Version Control/Git</h4>
<p><a href="https://bitbucket.org">Bitbucket</a> by Atlassian. Free private repos ftw - which I use 7 at this point. That would cost me $7 on github - $84 annually.</p>
<p><strong>Cost: $0</strong></p>
<h4 id="3-server-configuration--management">3. Server configuration / management</h4>
<p><a href="https://www.ansible.com/">Ansible</a> - it&rsquo;s simple and it allows to keep my infrastructure as code (git repo). I try no to touch any configuration manually. Give it and hour and you&rsquo;re up &amp; running, DSL is simple, configuration structure is very self-explanatory and clear. If you don&rsquo;t need to constantly reconfigure thousands of servers Ansible is IMO the way to go.</p>
<p><strong>Cost: Free</strong> (Open-source)</p>
<h4 id="4-domains">4. Domains</h4>
<p><a href="https://www.ovh.com/">OVH</a>. Didn&rsquo;t had chance to research cheaper alternatives, the prices seemed reasonable few years ago (remember about price of the renewal!). For now - works for me.</p>
<p><strong>Cost: Vary (depending on domain)</strong>, from like $1.5 to ~$15/year</p>
<h4 id="5-dns">5. DNS</h4>
<p>Mostly <a href="https://www.cloudflare.com/">cloudflare</a>, free plan is more than enough for your daily needs.</p>
<p><strong>Cost: $0 for basic plan</strong></p>
<h4 id="6-monitoring">6. Monitoring</h4>
<p>Again I go with very simple solutions:</p>
<ul>
<li>monit - for internal services and all my websites; it&rsquo;s a single package you can install directly from most system package managers, it have simple web ui, simple syntax and just works - perfect for a single-server solution</li>
<li><a href="https://uptimerobot.com/">uptimerobot</a> - pinging server itself and a few projects just so I&rsquo;m confident it&rsquo;s up</li>
</ul>
<p>In both cases I have email alerts enabled for each service I monitor.</p>
<p><strong>Cost: Free</strong> / (<a href="https://mmonit.com/">M/Monit</a> is paid version), <strong>$0 (uptimerobot is free up to 50 checks)</strong></p>
<h4 id="7-ssl">7. SSL</h4>
<p><a href="https://letsencrypt.org/">Let&rsquo;s encrypt</a>. No more paying for ssl certs, yay! Modern webservers usually provide support for let&rsquo;s encrypt out of the box - <a href="https://traefik.io/">Traefik</a> or <a href="https://caddyserver.com/">Caddy</a> (waning: they changed the license a while back) for example.</p>
<p><strong>Cost: $0</strong></p>
<h4 id="8-email">8. Email</h4>
<p>I don&rsquo;t use much of an email and I don&rsquo;t want to configure mailserver on my bare-metal box - did it few times and it&rsquo;s just too much work (at least for me). That&rsquo;s why I use <a href="https://www.mailgun.com/">Mailgun</a>, just generate smtp credentials and you&rsquo;re good to go, hassle-free!</p>
<p><strong>Cost: $0 (up to 10000 emails/month)</strong></p>
<h4 id="9-error-tracking">9. Error tracking</h4>
<p><a href="https://sentry.io">Sentry</a>. I was mentioning <a href="https://github.com/errbit/errbit">errbit</a> as open source alternative for <a href="https://airbrake.io/">Airbrake</a> here <a href="/blog/2012/10/03/open-source-airbrake-alternative/">few years ago</a>, but forget about it, Sentry is the new king in town. Slick UI, works across every modern language nicely and you can even self-host it in minutes via docker image.</p>
<p><strong>Cost: $0 (up to 10k events/month) / open-source</strong> (paid plan options)</p>
<h4 id="10-ci--cd">10. CI / CD</h4>
<p><a href="https://circleci.com/">CircleCI</a>; if needed I can always self-host <a href="https://drone.io/">Drone</a> (which I simply love) - and to be honest I prefer Drone&rsquo;s syntax/way of configuring things, it seems CircleCI can be sometimes kinda limiting, eg. I think you still can&rsquo;t fully purge cache in their 2.0 version, you have to change cache key as a workaround. But for now CircleCI does the job right and I appreciate that they offer free plan in the first place.</p>
<p><strong>Cost: $0 (1500 &ldquo;build minutes&rdquo; per month included)</strong></p>
<h4 id="11-analytics">11. Analytics</h4>
<p>Basic free stuff: Google Analytics (wich Search Console integrated) and <a href="https://www.fullstory.com/">Fullstory</a> on top of that - because sometimes I like to check user&rsquo;s behavior and realize how bad my UX skills are :P.</p>
<p><strong>Cost: $0 (up to 1000 monthly sessions)</strong></p>
<hr>
<p>So as you see I use a quite a few services that I don&rsquo;t pay a dime for - and I truly appreciate that those platforms offer free stuff for folks like me 😘 (very low usage &amp; single dev).</p>
<p>Summing it all app - hosting with domains costs me around <strong>$130 a year</strong> and I should probably drop some of my old unused domains and scale down to less than a hundred bucks a year #oniondev.</p>
]]></description><content:encoded><![CDATA[<p>As I&rsquo;m still fooling around with my old pet project I decided to sum up current setup I&rsquo;m on - from the basic things like where I host my stuff to what kind of tools/products I use, and how much does it cost me in the end (spoiler: I&rsquo;m cheap as hell :P) So let&rsquo;s see it.</p>
<h4 id="1-hosting">1. Hosting</h4>
<p>Bare-metal, I&rsquo;m an old folk and I change vendors whenever I feel I need to ;-).</p>
<p>I was on two different shared hosting for a while, then moved to <a href="https://www.soyoustart.com/">SYS</a> (OVH dedicated), then to <a href="https://www.ovh.pl/vps/vps-ssd.xml">OVH VPS</a> (Note: PL link) and currently I&rsquo;m on <a href="https://www.hetzner.com/cloud">Hetzner Cloud</a> - basically downscaled everything x8 within a last year and a half. I keep there three static sites served via Caddy, Rails app (unicorn + sidekiq), docker with mariadb, elasticsearch and redis containers and a single static Go binary.</p>
<p>Plus I still have plenty of room for experiments when needed. I killed my servers many times in the past (due to misconfiguration), but that&rsquo;s the great opportunity to learn (hello netboot rescue) and I don&rsquo;t really care about 100% uptime.</p>
<p><strong>Cost: around €5/mo ($6.2/mo) with few snapshots</strong> (Note: I use that box as storage box as well)</p>
<h4 id="2-version-controlgit">2. Version Control/Git</h4>
<p><a href="https://bitbucket.org">Bitbucket</a> by Atlassian. Free private repos ftw - which I use 7 at this point. That would cost me $7 on github - $84 annually.</p>
<p><strong>Cost: $0</strong></p>
<h4 id="3-server-configuration--management">3. Server configuration / management</h4>
<p><a href="https://www.ansible.com/">Ansible</a> - it&rsquo;s simple and it allows to keep my infrastructure as code (git repo). I try no to touch any configuration manually. Give it and hour and you&rsquo;re up &amp; running, DSL is simple, configuration structure is very self-explanatory and clear. If you don&rsquo;t need to constantly reconfigure thousands of servers Ansible is IMO the way to go.</p>
<p><strong>Cost: Free</strong> (Open-source)</p>
<h4 id="4-domains">4. Domains</h4>
<p><a href="https://www.ovh.com/">OVH</a>. Didn&rsquo;t had chance to research cheaper alternatives, the prices seemed reasonable few years ago (remember about price of the renewal!). For now - works for me.</p>
<p><strong>Cost: Vary (depending on domain)</strong>, from like $1.5 to ~$15/year</p>
<h4 id="5-dns">5. DNS</h4>
<p>Mostly <a href="https://www.cloudflare.com/">cloudflare</a>, free plan is more than enough for your daily needs.</p>
<p><strong>Cost: $0 for basic plan</strong></p>
<h4 id="6-monitoring">6. Monitoring</h4>
<p>Again I go with very simple solutions:</p>
<ul>
<li>monit - for internal services and all my websites; it&rsquo;s a single package you can install directly from most system package managers, it have simple web ui, simple syntax and just works - perfect for a single-server solution</li>
<li><a href="https://uptimerobot.com/">uptimerobot</a> - pinging server itself and a few projects just so I&rsquo;m confident it&rsquo;s up</li>
</ul>
<p>In both cases I have email alerts enabled for each service I monitor.</p>
<p><strong>Cost: Free</strong> / (<a href="https://mmonit.com/">M/Monit</a> is paid version), <strong>$0 (uptimerobot is free up to 50 checks)</strong></p>
<h4 id="7-ssl">7. SSL</h4>
<p><a href="https://letsencrypt.org/">Let&rsquo;s encrypt</a>. No more paying for ssl certs, yay! Modern webservers usually provide support for let&rsquo;s encrypt out of the box - <a href="https://traefik.io/">Traefik</a> or <a href="https://caddyserver.com/">Caddy</a> (waning: they changed the license a while back) for example.</p>
<p><strong>Cost: $0</strong></p>
<h4 id="8-email">8. Email</h4>
<p>I don&rsquo;t use much of an email and I don&rsquo;t want to configure mailserver on my bare-metal box - did it few times and it&rsquo;s just too much work (at least for me). That&rsquo;s why I use <a href="https://www.mailgun.com/">Mailgun</a>, just generate smtp credentials and you&rsquo;re good to go, hassle-free!</p>
<p><strong>Cost: $0 (up to 10000 emails/month)</strong></p>
<h4 id="9-error-tracking">9. Error tracking</h4>
<p><a href="https://sentry.io">Sentry</a>. I was mentioning <a href="https://github.com/errbit/errbit">errbit</a> as open source alternative for <a href="https://airbrake.io/">Airbrake</a> here <a href="/blog/2012/10/03/open-source-airbrake-alternative/">few years ago</a>, but forget about it, Sentry is the new king in town. Slick UI, works across every modern language nicely and you can even self-host it in minutes via docker image.</p>
<p><strong>Cost: $0 (up to 10k events/month) / open-source</strong> (paid plan options)</p>
<h4 id="10-ci--cd">10. CI / CD</h4>
<p><a href="https://circleci.com/">CircleCI</a>; if needed I can always self-host <a href="https://drone.io/">Drone</a> (which I simply love) - and to be honest I prefer Drone&rsquo;s syntax/way of configuring things, it seems CircleCI can be sometimes kinda limiting, eg. I think you still can&rsquo;t fully purge cache in their 2.0 version, you have to change cache key as a workaround. But for now CircleCI does the job right and I appreciate that they offer free plan in the first place.</p>
<p><strong>Cost: $0 (1500 &ldquo;build minutes&rdquo; per month included)</strong></p>
<h4 id="11-analytics">11. Analytics</h4>
<p>Basic free stuff: Google Analytics (wich Search Console integrated) and <a href="https://www.fullstory.com/">Fullstory</a> on top of that - because sometimes I like to check user&rsquo;s behavior and realize how bad my UX skills are :P.</p>
<p><strong>Cost: $0 (up to 1000 monthly sessions)</strong></p>
<hr>
<p>So as you see I use a quite a few services that I don&rsquo;t pay a dime for - and I truly appreciate that those platforms offer free stuff for folks like me 😘 (very low usage &amp; single dev).</p>
<p>Summing it all app - hosting with domains costs me around <strong>$130 a year</strong> and I should probably drop some of my old unused domains and scale down to less than a hundred bucks a year #oniondev.</p>
]]></content:encoded></item><item><title>Upgrading to Rails 5 - bunch of random tips</title><link>https://awesomeprogrammer.com/blog/2018/02/24/upgrading-to-rails-5-bunch-of-random-tips/</link><pubDate>Sat, 24 Feb 2018 13:34:37 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2018/02/24/upgrading-to-rails-5-bunch-of-random-tips/</guid><description><![CDATA[<p>After like 3 weeks of not touching code I decided to refresh my pet project a little bit, below are random issues you might encounter while trying to upgrade your old Rails 4 app to Rails 5.</p>
<ol>
<li><code>uninitialized constant ActionController::RedirectBackError</code></li>
</ol>
<!-- raw HTML omitted -->
<ol start="2">
<li>Returning <code>false</code> in <code>before_save</code> callback doesn&rsquo;t halt it (old kinda specific Rails gotcha/behavior).</li>
</ol>
<!-- raw HTML omitted -->
<ol start="3">
<li><code>NoMethodError: super: no superclass method xhr for RSpec::ExampleGroups::&lt;controller&gt;</code></li>
</ol>
<!-- raw HTML omitted -->
<ol start="4">
<li><code>render nothing: true</code> tries to render template</li>
</ol>
<!-- raw HTML omitted -->
<ol start="5">
<li><code>ArgumentError: unknown keyword: &lt;key&gt;</code></li>
</ol>
<!-- raw HTML omitted -->
<ol start="6">
<li>No method error on <code>object.errors.get(:field)</code></li>
</ol>
<!-- raw HTML omitted -->]]></description><content:encoded><![CDATA[<p>After like 3 weeks of not touching code I decided to refresh my pet project a little bit, below are random issues you might encounter while trying to upgrade your old Rails 4 app to Rails 5.</p>
<ol>
<li><code>uninitialized constant ActionController::RedirectBackError</code></li>
</ol>
<!-- raw HTML omitted -->
<ol start="2">
<li>Returning <code>false</code> in <code>before_save</code> callback doesn&rsquo;t halt it (old kinda specific Rails gotcha/behavior).</li>
</ol>
<!-- raw HTML omitted -->
<ol start="3">
<li><code>NoMethodError: super: no superclass method xhr for RSpec::ExampleGroups::&lt;controller&gt;</code></li>
</ol>
<!-- raw HTML omitted -->
<ol start="4">
<li><code>render nothing: true</code> tries to render template</li>
</ol>
<!-- raw HTML omitted -->
<ol start="5">
<li><code>ArgumentError: unknown keyword: &lt;key&gt;</code></li>
</ol>
<!-- raw HTML omitted -->
<ol start="6">
<li>No method error on <code>object.errors.get(:field)</code></li>
</ol>
<!-- raw HTML omitted -->]]></content:encoded></item><item><title>How to merge PDFs to a single page</title><link>https://awesomeprogrammer.com/blog/2018/01/31/how-to-merge-pdfs-to-a-single-page/</link><pubDate>Wed, 31 Jan 2018 08:53:11 +0100</pubDate><guid>https://awesomeprogrammer.com/blog/2018/01/31/how-to-merge-pdfs-to-a-single-page/</guid><description><![CDATA[<p>Recently I have been selling a lot of my stuff. Simply getting rid of unused or unneeded things - and mostly I&rsquo;m packing &amp; sending it to other parts of the country by using a package carrier that requires printing labels. The standard drill. But those labels are kinda smallish, you can fit like 4 on single A4 page (it&rsquo;s like a horizontal strip kid of thing).</p>
<p>Ok so I&rsquo;m sending 3 packages and I get 3 pdfs to print with huge amount of white space. I don&rsquo;t own a printer anymore (sold it as well), I need to go photocopy point, technically speaking I could simply go and ask them to print 3 separate documents. But the environment <em>and</em> extra 20 cents - I just couldn&rsquo;t stand it :P.</p>
<p>None of the free software I have tried (PDF Studio 12, PDF Editor 6, PDFSam and other) offered me option to do what I really wanted - I could crop single page and merge those pages to a single document with few pages (and you can do that as well in build-in MacOS Preview btw) but I couldn&rsquo;t merge those pages in the end. So after an hour or so I went back to the basics - imagemagick. Here is the final recipe based on various StackOverflow questions :P.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>convert -density <span style="color:#ae81ff">300</span> -trim *.pdf -quality <span style="color:#ae81ff">100</span> -append output.jpg <span style="color:#f92672">&amp;&amp;</span> convert output.jpg output.pdf <span style="color:#f92672">&amp;&amp;</span> rm output.jpg
</span></span></code></pre></div><p>What is happening here is: we&rsquo;re converting all pdfs within given directory to jpgs on the way we&rsquo;re trimming all the surrounding blank space and stacking each one on top of another. Then simply we convert back stacked jpg to pdf and removing temporary file - not using unix pipes as it will mess up result - I&rsquo;m guessing it&rsquo;s related to how imagemagick is handling convert+append for many files - but I&rsquo;m not really into details here as it does the job ;-).</p>
<p>It seems the simples approach won again 😉</p>]]></description><content:encoded><![CDATA[<p>Recently I have been selling a lot of my stuff. Simply getting rid of unused or unneeded things - and mostly I&rsquo;m packing &amp; sending it to other parts of the country by using a package carrier that requires printing labels. The standard drill. But those labels are kinda smallish, you can fit like 4 on single A4 page (it&rsquo;s like a horizontal strip kid of thing).</p>
<p>Ok so I&rsquo;m sending 3 packages and I get 3 pdfs to print with huge amount of white space. I don&rsquo;t own a printer anymore (sold it as well), I need to go photocopy point, technically speaking I could simply go and ask them to print 3 separate documents. But the environment <em>and</em> extra 20 cents - I just couldn&rsquo;t stand it :P.</p>
<p>None of the free software I have tried (PDF Studio 12, PDF Editor 6, PDFSam and other) offered me option to do what I really wanted - I could crop single page and merge those pages to a single document with few pages (and you can do that as well in build-in MacOS Preview btw) but I couldn&rsquo;t merge those pages in the end. So after an hour or so I went back to the basics - imagemagick. Here is the final recipe based on various StackOverflow questions :P.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>convert -density <span style="color:#ae81ff">300</span> -trim *.pdf -quality <span style="color:#ae81ff">100</span> -append output.jpg <span style="color:#f92672">&amp;&amp;</span> convert output.jpg output.pdf <span style="color:#f92672">&amp;&amp;</span> rm output.jpg
</span></span></code></pre></div><p>What is happening here is: we&rsquo;re converting all pdfs within given directory to jpgs on the way we&rsquo;re trimming all the surrounding blank space and stacking each one on top of another. Then simply we convert back stacked jpg to pdf and removing temporary file - not using unix pipes as it will mess up result - I&rsquo;m guessing it&rsquo;s related to how imagemagick is handling convert+append for many files - but I&rsquo;m not really into details here as it does the job ;-).</p>
<p>It seems the simples approach won again 😉</p>]]></content:encoded></item><item><title>(Personal) API Clients (Anit-)Patterns</title><link>https://awesomeprogrammer.com/blog/2017/12/28/personal-api-clients-anit-patterns/</link><pubDate>Thu, 28 Dec 2017 08:30:27 +0200</pubDate><guid>https://awesomeprogrammer.com/blog/2017/12/28/personal-api-clients-anit-patterns/</guid><description><![CDATA[<p>You&rsquo;re building open source API client? That&rsquo;s great ❤️ I like you already. But please keep in mind:</p>
<h5 id="1-its-always-cool-to-have-a-changelog">1. It&rsquo;s always cool to have a changelog</h5>
<p>Changelogs are super-awesome, they allows other developers to quickly check what cool feature you hacked and what breaking changes they might face. It takes a few minutes to create a changelog and it can save hours of somebody else&rsquo;s time.</p>
<h5 id="2-try-to-keep-runtime-dependencies-at-minimum">2. Try to keep runtime dependencies at minimum</h5>
<p>Do you really need that popular-fancy-library in order to execute few http requests? Or that uber-quick parser to parse few lines of JSON? Can you use soft-dependencies - or better yet - no dependencies at all? Give it a try, keep it low level if possible, who knows - maybe you will have a chance to know core library of language-of-your-choice a little bit better?</p>
<h5 id="3-please-do-no-parse-_my_-parameters-in-a-automagical-fashion">3. Please do no parse <em>my</em> parameters in a automagical fashion</h5>
<p>If I try to push timestamp param with nano-seconds precision please do not parse it back to datetime under the hood or at least explicitly mention in in the readme so I don&rsquo;t have dive into the source code in order to debug the problem. Imagine how frustrating it might be for other developers, assuming something they will know, when they have no chance of knowing that. Transparency is the key and other folks will love you for that.</p>
<h5 id="4-do-not-swallow-exceptions-its-just-easier-that-way">4. Do not swallow exceptions, it&rsquo;s just easier that way!</h5>
<p>Just throw it at my face! I can handle those exceptions - even better I want to handle it and react accordingly. Treat fellow developers like adults!</p>]]></description><content:encoded><![CDATA[<p>You&rsquo;re building open source API client? That&rsquo;s great ❤️ I like you already. But please keep in mind:</p>
<h5 id="1-its-always-cool-to-have-a-changelog">1. It&rsquo;s always cool to have a changelog</h5>
<p>Changelogs are super-awesome, they allows other developers to quickly check what cool feature you hacked and what breaking changes they might face. It takes a few minutes to create a changelog and it can save hours of somebody else&rsquo;s time.</p>
<h5 id="2-try-to-keep-runtime-dependencies-at-minimum">2. Try to keep runtime dependencies at minimum</h5>
<p>Do you really need that popular-fancy-library in order to execute few http requests? Or that uber-quick parser to parse few lines of JSON? Can you use soft-dependencies - or better yet - no dependencies at all? Give it a try, keep it low level if possible, who knows - maybe you will have a chance to know core library of language-of-your-choice a little bit better?</p>
<h5 id="3-please-do-no-parse-_my_-parameters-in-a-automagical-fashion">3. Please do no parse <em>my</em> parameters in a automagical fashion</h5>
<p>If I try to push timestamp param with nano-seconds precision please do not parse it back to datetime under the hood or at least explicitly mention in in the readme so I don&rsquo;t have dive into the source code in order to debug the problem. Imagine how frustrating it might be for other developers, assuming something they will know, when they have no chance of knowing that. Transparency is the key and other folks will love you for that.</p>
<h5 id="4-do-not-swallow-exceptions-its-just-easier-that-way">4. Do not swallow exceptions, it&rsquo;s just easier that way!</h5>
<p>Just throw it at my face! I can handle those exceptions - even better I want to handle it and react accordingly. Treat fellow developers like adults!</p>]]></content:encoded></item><item><title>Goodbye Jekyll, Welcome Hugo</title><link>https://awesomeprogrammer.com/blog/2017/10/15/goodbye-jekyll-welcome-hugo/</link><pubDate>Sun, 15 Oct 2017 19:30:27 +0200</pubDate><guid>https://awesomeprogrammer.com/blog/2017/10/15/goodbye-jekyll-welcome-hugo/</guid><description><![CDATA[<p>Pfew, after over a year of inactivity it&rsquo;s time to refresh this site a little bit. It was on my todo list for over a while now.</p>
<p>Actually to be honest only updating logo was on my list 😜 but after researching alternatives to <a href="http://octopress.org/">Octopress</a> (that is in fact Jekyll fork) I stumbled upon <a href="https://gohugo.io">Hugo</a> and it instantly got my attention as it&rsquo;s written in Go - and that is is distributed as a single binary file (no more installing gems and such).</p>
<p>Migration was quite painless <a href="https://gohugo.io/commands/hugo_import_jekyll/">thanks to this</a>, template I&rsquo;m using here right now is called <a href="https://themes.gohugo.io/hugo-nuo/">hugo-nuo</a> and deployment is a bascially a <a href="https://gohugo.io/hosting-and-deployment/deployment-with-rsync/">one-liner script</a> as I have my own server.</p>]]></description><content:encoded><![CDATA[<p>Pfew, after over a year of inactivity it&rsquo;s time to refresh this site a little bit. It was on my todo list for over a while now.</p>
<p>Actually to be honest only updating logo was on my list 😜 but after researching alternatives to <a href="http://octopress.org/">Octopress</a> (that is in fact Jekyll fork) I stumbled upon <a href="https://gohugo.io">Hugo</a> and it instantly got my attention as it&rsquo;s written in Go - and that is is distributed as a single binary file (no more installing gems and such).</p>
<p>Migration was quite painless <a href="https://gohugo.io/commands/hugo_import_jekyll/">thanks to this</a>, template I&rsquo;m using here right now is called <a href="https://themes.gohugo.io/hugo-nuo/">hugo-nuo</a> and deployment is a bascially a <a href="https://gohugo.io/hosting-and-deployment/deployment-with-rsync/">one-liner script</a> as I have my own server.</p>]]></content:encoded></item><item><title>Power to the masses</title><link>https://awesomeprogrammer.com/blog/2016/07/03/power-to-the-masses/</link><pubDate>Sun, 03 Jul 2016 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2016/07/03/power-to-the-masses/</guid><description><![CDATA[<p>You can&rsquo;t deny docker hype isn&rsquo;t going anywhere any time soon. For a software that is barely 3 years (!) old it have already made quite impressive impact in the widely known <em>IT ecosystem</em>.</p>
<p>I was fiddling with docker <a href="http://awesomeprogrammer.com/blog/2015/01/31/vagrant-for-ruby-on-rails-development/">around a year ago</a> for the first time and I wasn&rsquo;t impressed (I won&rsquo;t deny I&rsquo;m mostly a consumer of open source world) - things did break, setup was awkward and tooling immature. But that changed. Oh boy it changed.</p>
<p>With software like <a href="http://rancher.com/">rancher</a> that basically allows you to setup containers via web ui (or it&rsquo;s <a href="https://github.com/rancher/rancher-compose">rancher-compose</a> that consumes <a href="https://docs.docker.com/compose/">docker-compose</a> /v1 atm/ config) you can spin enterprise-quality software within minutes - and it <em>just works</em>. Crazy how far it got within such short period of time.</p>
<p>It truly blows mind my mind that you can setup <a href="https://getsentry.com/welcome/">sentry</a> or <a href="https://drone.io/">your own CI</a> with just few clicks. And I guess that is were all the hype lies - as a independent developer, given the right configuration, docker gives you power to single-handed developer &amp; ship to production piece of code with all necessary dependencies (obviously there might be existing architecture limitation involved, but that&rsquo;s a different story).</p>
<p>Sure there are some security-related concerns to be taken into an account (I recommend checking <a href="https://github.com/docker/docker-bench-security">best practices</a>), but overall I think times are bright.</p>
]]></description><content:encoded><![CDATA[<p>You can&rsquo;t deny docker hype isn&rsquo;t going anywhere any time soon. For a software that is barely 3 years (!) old it have already made quite impressive impact in the widely known <em>IT ecosystem</em>.</p>
<p>I was fiddling with docker <a href="http://awesomeprogrammer.com/blog/2015/01/31/vagrant-for-ruby-on-rails-development/">around a year ago</a> for the first time and I wasn&rsquo;t impressed (I won&rsquo;t deny I&rsquo;m mostly a consumer of open source world) - things did break, setup was awkward and tooling immature. But that changed. Oh boy it changed.</p>
<p>With software like <a href="http://rancher.com/">rancher</a> that basically allows you to setup containers via web ui (or it&rsquo;s <a href="https://github.com/rancher/rancher-compose">rancher-compose</a> that consumes <a href="https://docs.docker.com/compose/">docker-compose</a> /v1 atm/ config) you can spin enterprise-quality software within minutes - and it <em>just works</em>. Crazy how far it got within such short period of time.</p>
<p>It truly blows mind my mind that you can setup <a href="https://getsentry.com/welcome/">sentry</a> or <a href="https://drone.io/">your own CI</a> with just few clicks. And I guess that is were all the hype lies - as a independent developer, given the right configuration, docker gives you power to single-handed developer &amp; ship to production piece of code with all necessary dependencies (obviously there might be existing architecture limitation involved, but that&rsquo;s a different story).</p>
<p>Sure there are some security-related concerns to be taken into an account (I recommend checking <a href="https://github.com/docker/docker-bench-security">best practices</a>), but overall I think times are bright.</p>
]]></content:encoded></item><item><title>Put some Go in your Ruby so you don't Rust</title><link>https://awesomeprogrammer.com/blog/2016/01/02/put-some-go-in-your-ruby-so-you-dont-rust/</link><pubDate>Sat, 02 Jan 2016 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2016/01/02/put-some-go-in-your-ruby-so-you-dont-rust/</guid><description><![CDATA[<p>Lately I have been looking for some solid alternatives to Ruby, and I must admit up-front - Rails spoiled me, badly.</p>
<p>I was hungry for some performance boosts, and let&rsquo;s face it - dynamically typed languages can get you until some point.</p>
<p>I played with few popular modern languages (manually dealing with memory management? No thank you sir) and tried to hook them into Rails application.</p>
<h3 id="crystal">Crystal</h3>
<p>So first, Crystal - it felt great (look at me, I&rsquo;m writing <em>compiled Ruby</em> that is magnitude faster!), but the language itself is so immature at this point that basically each <a href="https://github.com/manastech/crystal/blob/master/CHANGELOG.md">release breaks something</a> (in a major way). There are some external libraries in active development, you can find some basic tools for postgres, redis, http lib etc. I really like <a href="https://github.com/ysbaddaden/shards">shards</a> - dependency manager, so you can plug in some goodies into your project - after all, it&rsquo;s all about ecosystem of the language.</p>
<p>Overall I really like it, I hope the crystal team will manage to ship some stable version this year (the <a href="http://crystal-lang.org/2015/12/24/the-future-of-crystal.html">most recent news</a> was kinda a turn off), but I would rather wait a little but more. So I ended my journey with Crystal pretty quick.</p>
<h3 id="rust">Rust</h3>
<p>Started by Mozilla in around 2010. API at this point seems quite stable, and the language itself feels pretty great, mostly because <em>zero-cost abstractions</em>. I recommend <a href="https://www.youtube.com/watch?v=2BdJeSC4FFI">this awesome full stack fest talk</a> about writing Ruby extensions in Rust where Yehuda Katz goes into some details about language specifics. Rust have <a href="https://doc.rust-lang.org/stable/book/">great official documentation</a>, and pretty sweet dependency manager called <a href="http://doc.crates.io/guide.html">cargo</a> - something like Rubygems / npmjs, but for Rust.</p>
<p>I tried to write some more meaningful that <em>Hello world</em> web app in Rust, and at this point it felt a little bit awkward. I think mostly because tools are still in the works (check out <a href="http://arewewebyet.com/"><em>are we web yet webpage</em></a>). But the development activity in rust community is so active I wouldn&rsquo;t be surprised that <em>we will be web</em> in next few months.</p>
<h3 id="go">Go</h3>
<p>Accounted in 2009 by Google, <a href="https://golang.org/doc/devel/release.html">pretty damn stable</a> as major version is released like twice a year. Pretty awkward dependency management (you have to choose something from <a href="https://github.com/golang/go/wiki/PackageManagementTools">this list</a> yourself), very verbose error checking might seem like a big turn-off at first, but after giving it some time I kinda liked it.</p>
<p>Starting from zero you can relatively quickly build some tiny app that spits out JSON that can be consumed by your Rails app. If you have to crunch and aggregate some bigger amounts of data in-memory even badly written go code should run much faster than your carefully profiled ruby code (well, GC is sooo costly in Ruby) - even considering network latency between two applications.</p>
<p>I took some extra time to do some proof of concept in Go and extracted part of the Rails logic into separate go lang app, then just hooked them two together via JSON, here are my thoughts:</p>
<ul>
<li>
<p>building complex sql queries in Go - super awkward, there are some sql query builders, but I don&rsquo;t think any of them can handle like complex nested joins with select right, I ended up <em>gluing</em> query inside custom struct with a help of <a href="https://github.com/jmoiron/sqlx">sqlx</a>. There are so ORMs out there I didn&rsquo;t tried as I didn&rsquo;t really needed one.</p>
</li>
<li>
<p>you can get some ruby/rails goodies for free thanks to awesome go community, good examples: <a href="https://github.com/joho/godotenv">godotenv</a> and <a href="https://github.com/airbrake/gobrake">gobrake</a></p>
</li>
<li>
<p>gotta love <a href="https://golang.org/cmd/gofmt/">gofmt</a>, same goes for go compiler that does a lot of code validation for you (where in ruby you would have to write tests in go it just doesn&rsquo;t compile in the first place) - if you&rsquo;re using sublimetext <a href="https://github.com/DisposaBoy/GoSublime">it has quite neat package for go</a></p>
</li>
<li>
<p><a href="http://www.golangbootcamp.com/book/concurrency">concurrency in go</a> is quite cheap - you will be amazed how easy you can speed things up even further</p>
</li>
</ul>
<p>Next step: some sane production deployment pipeline (docker maybe?) and real-world benchmarks.</p>]]></description><content:encoded><![CDATA[<p>Lately I have been looking for some solid alternatives to Ruby, and I must admit up-front - Rails spoiled me, badly.</p>
<p>I was hungry for some performance boosts, and let&rsquo;s face it - dynamically typed languages can get you until some point.</p>
<p>I played with few popular modern languages (manually dealing with memory management? No thank you sir) and tried to hook them into Rails application.</p>
<h3 id="crystal">Crystal</h3>
<p>So first, Crystal - it felt great (look at me, I&rsquo;m writing <em>compiled Ruby</em> that is magnitude faster!), but the language itself is so immature at this point that basically each <a href="https://github.com/manastech/crystal/blob/master/CHANGELOG.md">release breaks something</a> (in a major way). There are some external libraries in active development, you can find some basic tools for postgres, redis, http lib etc. I really like <a href="https://github.com/ysbaddaden/shards">shards</a> - dependency manager, so you can plug in some goodies into your project - after all, it&rsquo;s all about ecosystem of the language.</p>
<p>Overall I really like it, I hope the crystal team will manage to ship some stable version this year (the <a href="http://crystal-lang.org/2015/12/24/the-future-of-crystal.html">most recent news</a> was kinda a turn off), but I would rather wait a little but more. So I ended my journey with Crystal pretty quick.</p>
<h3 id="rust">Rust</h3>
<p>Started by Mozilla in around 2010. API at this point seems quite stable, and the language itself feels pretty great, mostly because <em>zero-cost abstractions</em>. I recommend <a href="https://www.youtube.com/watch?v=2BdJeSC4FFI">this awesome full stack fest talk</a> about writing Ruby extensions in Rust where Yehuda Katz goes into some details about language specifics. Rust have <a href="https://doc.rust-lang.org/stable/book/">great official documentation</a>, and pretty sweet dependency manager called <a href="http://doc.crates.io/guide.html">cargo</a> - something like Rubygems / npmjs, but for Rust.</p>
<p>I tried to write some more meaningful that <em>Hello world</em> web app in Rust, and at this point it felt a little bit awkward. I think mostly because tools are still in the works (check out <a href="http://arewewebyet.com/"><em>are we web yet webpage</em></a>). But the development activity in rust community is so active I wouldn&rsquo;t be surprised that <em>we will be web</em> in next few months.</p>
<h3 id="go">Go</h3>
<p>Accounted in 2009 by Google, <a href="https://golang.org/doc/devel/release.html">pretty damn stable</a> as major version is released like twice a year. Pretty awkward dependency management (you have to choose something from <a href="https://github.com/golang/go/wiki/PackageManagementTools">this list</a> yourself), very verbose error checking might seem like a big turn-off at first, but after giving it some time I kinda liked it.</p>
<p>Starting from zero you can relatively quickly build some tiny app that spits out JSON that can be consumed by your Rails app. If you have to crunch and aggregate some bigger amounts of data in-memory even badly written go code should run much faster than your carefully profiled ruby code (well, GC is sooo costly in Ruby) - even considering network latency between two applications.</p>
<p>I took some extra time to do some proof of concept in Go and extracted part of the Rails logic into separate go lang app, then just hooked them two together via JSON, here are my thoughts:</p>
<ul>
<li>
<p>building complex sql queries in Go - super awkward, there are some sql query builders, but I don&rsquo;t think any of them can handle like complex nested joins with select right, I ended up <em>gluing</em> query inside custom struct with a help of <a href="https://github.com/jmoiron/sqlx">sqlx</a>. There are so ORMs out there I didn&rsquo;t tried as I didn&rsquo;t really needed one.</p>
</li>
<li>
<p>you can get some ruby/rails goodies for free thanks to awesome go community, good examples: <a href="https://github.com/joho/godotenv">godotenv</a> and <a href="https://github.com/airbrake/gobrake">gobrake</a></p>
</li>
<li>
<p>gotta love <a href="https://golang.org/cmd/gofmt/">gofmt</a>, same goes for go compiler that does a lot of code validation for you (where in ruby you would have to write tests in go it just doesn&rsquo;t compile in the first place) - if you&rsquo;re using sublimetext <a href="https://github.com/DisposaBoy/GoSublime">it has quite neat package for go</a></p>
</li>
<li>
<p><a href="http://www.golangbootcamp.com/book/concurrency">concurrency in go</a> is quite cheap - you will be amazed how easy you can speed things up even further</p>
</li>
</ul>
<p>Next step: some sane production deployment pipeline (docker maybe?) and real-world benchmarks.</p>]]></content:encoded></item><item><title>How to alter big postgres table</title><link>https://awesomeprogrammer.com/blog/2015/11/25/how-to-alter-big-postgres-table/</link><pubDate>Wed, 25 Nov 2015 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2015/11/25/how-to-alter-big-postgres-table/</guid><description><![CDATA[<p>Lately I had to migrate pretty fat (300GB+ data and 150GB+ of index data) postgres table - application grew, we had to get rid of one pretty big column that was redundant now, change main composite index, change one column type etc. etc. The problem was - do it without significant amount of downtime.</p>
<p>You can google up tons of solutions, but let me walk through approach I chose - hopefully you will find it somewhat useful.</p>
<p>I decided to create another table on the side, using <code>CREATE TABLE ... LIKE INCLUDING ALL</code> - then I dropped all the indexes on new table (you don&rsquo;t needed extra overhead for the process) and just started copying the data I really needed to new table using dead simple loop in ruby, something more or less like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">insert</span> <span style="color:#66d9ef">into</span> new_table (columns...) <span style="color:#66d9ef">select</span> columns... <span style="color:#66d9ef">from</span> old_table
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">where</span> id <span style="color:#66d9ef">between</span> <span style="color:#f92672">&lt;</span>batch <span style="color:#66d9ef">of</span> <span style="color:#f92672">~</span><span style="color:#ae81ff">10</span>_000_000 <span style="color:#66d9ef">rows</span><span style="color:#f92672">&gt;</span> <span style="color:#75715e">-- do this until insert results 0 rows
</span></span></span></code></pre></div><p>The whole process was actually pretty quick (you gotta love SSDs) and took just few hours - without indexes on new table I ended up with 185 GB of fresh &amp; clean data. So far so good.</p>
<p>Then I needed to apply new indexes, the problem was I wanted to use unique index and I knew the current dataset had duplicates, fortunately postgres wiki have a very <a href="https://wiki.postgresql.org/wiki/Deleting_duplicates">clever short article</a> how to deal with such problem. And that took another 3.5 hours.</p>
<p>Adding unique index took another 2 hours, then after removing all duplicates and adding index I did full vacuum &amp; analyze on the table and that took yet another hour.</p>
<p>The result was brand new table - with 185GB data &amp; 61GB index - pretty sweet. I prepared the application up-front for upcoming structural changes (that obviously sometimes cannot be done and you have to do some workarounds) and simply renamed both tables within single transaction.</p>
<p>And that&rsquo;s it. Sounds kinda unimpressive, but I was pretty happy with the result, also performance-wise it was a huge win - by simplifying just few things we lowered our response times by almost twice.</p>]]></description><content:encoded><![CDATA[<p>Lately I had to migrate pretty fat (300GB+ data and 150GB+ of index data) postgres table - application grew, we had to get rid of one pretty big column that was redundant now, change main composite index, change one column type etc. etc. The problem was - do it without significant amount of downtime.</p>
<p>You can google up tons of solutions, but let me walk through approach I chose - hopefully you will find it somewhat useful.</p>
<p>I decided to create another table on the side, using <code>CREATE TABLE ... LIKE INCLUDING ALL</code> - then I dropped all the indexes on new table (you don&rsquo;t needed extra overhead for the process) and just started copying the data I really needed to new table using dead simple loop in ruby, something more or less like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">insert</span> <span style="color:#66d9ef">into</span> new_table (columns...) <span style="color:#66d9ef">select</span> columns... <span style="color:#66d9ef">from</span> old_table
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">where</span> id <span style="color:#66d9ef">between</span> <span style="color:#f92672">&lt;</span>batch <span style="color:#66d9ef">of</span> <span style="color:#f92672">~</span><span style="color:#ae81ff">10</span>_000_000 <span style="color:#66d9ef">rows</span><span style="color:#f92672">&gt;</span> <span style="color:#75715e">-- do this until insert results 0 rows
</span></span></span></code></pre></div><p>The whole process was actually pretty quick (you gotta love SSDs) and took just few hours - without indexes on new table I ended up with 185 GB of fresh &amp; clean data. So far so good.</p>
<p>Then I needed to apply new indexes, the problem was I wanted to use unique index and I knew the current dataset had duplicates, fortunately postgres wiki have a very <a href="https://wiki.postgresql.org/wiki/Deleting_duplicates">clever short article</a> how to deal with such problem. And that took another 3.5 hours.</p>
<p>Adding unique index took another 2 hours, then after removing all duplicates and adding index I did full vacuum &amp; analyze on the table and that took yet another hour.</p>
<p>The result was brand new table - with 185GB data &amp; 61GB index - pretty sweet. I prepared the application up-front for upcoming structural changes (that obviously sometimes cannot be done and you have to do some workarounds) and simply renamed both tables within single transaction.</p>
<p>And that&rsquo;s it. Sounds kinda unimpressive, but I was pretty happy with the result, also performance-wise it was a huge win - by simplifying just few things we lowered our response times by almost twice.</p>]]></content:encoded></item><item><title>Getting the best of your Rails continuous integration</title><link>https://awesomeprogrammer.com/blog/2015/11/15/getting-the-best-of-your-rails-continuous-integration/</link><pubDate>Sun, 15 Nov 2015 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2015/11/15/getting-the-best-of-your-rails-continuous-integration/</guid><description><![CDATA[<p><em>You&rsquo;re using CI? Great! You are not? It&rsquo;s perfect time to start ;-).</em></p>
<p>Continuous integration (along with continuous deployment) is pretty simple and pretty awesome idea. Can basic setup where you simply run tests, and assuming tests passed whole thing gets deployed into production server(s) be done better?</p>
<p>Lately I gave some thought to the whole deployment process in our company. The whole development process in fact. I think todays web development got soooo complicated when compared what it was few years ago. The scale is just much bigger, everything is bigger, you operate on much more data, you use smarter tools that allows you to do more with less, but at the same time the amount of moving parts is higher and it seems easier to break.</p>
<p>That&rsquo;s why you automate. Here are some tools that we added to out CI pipeline few weeks ago:</p>
<ul>
<li>
<p><a href="https://github.com/rubysec/bundler-audit/">bundler-audit</a> - to keep you informed about vulnerable gems used in your app. There is also interesting project/github repo called <a href="https://github.com/ASoftCo/leaky-gems/issues/2">leaky-gems</a></p>
</li>
<li>
<p><a href="https://github.com/katgironpe/rails-erb-lint">erb-linter</a> - assuming you don&rsquo;t have 100% feature/view spec coverage it might be a good idea to simply check your erb files for possible typos</p>
</li>
<li>
<p><a href="https://github.com/sds/slim-lint">slim-lint</a> - same thing for slim, even integrates with rubocop nicely</p>
</li>
</ul>
<p>Speaking of rubocop - setting custom instance of <a href="https://github.com/thoughtbot/hound">hound</a> worked great for us (with keeping coding style consistent and catching little things like unused arguments and such).</p>
<p>Those small additions can make your live a little bit better ;).</p>
]]></description><content:encoded><![CDATA[<p><em>You&rsquo;re using CI? Great! You are not? It&rsquo;s perfect time to start ;-).</em></p>
<p>Continuous integration (along with continuous deployment) is pretty simple and pretty awesome idea. Can basic setup where you simply run tests, and assuming tests passed whole thing gets deployed into production server(s) be done better?</p>
<p>Lately I gave some thought to the whole deployment process in our company. The whole development process in fact. I think todays web development got soooo complicated when compared what it was few years ago. The scale is just much bigger, everything is bigger, you operate on much more data, you use smarter tools that allows you to do more with less, but at the same time the amount of moving parts is higher and it seems easier to break.</p>
<p>That&rsquo;s why you automate. Here are some tools that we added to out CI pipeline few weeks ago:</p>
<ul>
<li>
<p><a href="https://github.com/rubysec/bundler-audit/">bundler-audit</a> - to keep you informed about vulnerable gems used in your app. There is also interesting project/github repo called <a href="https://github.com/ASoftCo/leaky-gems/issues/2">leaky-gems</a></p>
</li>
<li>
<p><a href="https://github.com/katgironpe/rails-erb-lint">erb-linter</a> - assuming you don&rsquo;t have 100% feature/view spec coverage it might be a good idea to simply check your erb files for possible typos</p>
</li>
<li>
<p><a href="https://github.com/sds/slim-lint">slim-lint</a> - same thing for slim, even integrates with rubocop nicely</p>
</li>
</ul>
<p>Speaking of rubocop - setting custom instance of <a href="https://github.com/thoughtbot/hound">hound</a> worked great for us (with keeping coding style consistent and catching little things like unused arguments and such).</p>
<p>Those small additions can make your live a little bit better ;).</p>
]]></content:encoded></item><item><title>Graceful unauthorized pdf redirect with devise (custom failure app)</title><link>https://awesomeprogrammer.com/blog/2015/05/26/graceful-unauthorized-pdf-redirect-with-devise-custom-failure-app/</link><pubDate>Tue, 26 May 2015 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2015/05/26/graceful-unauthorized-pdf-redirect-with-devise-custom-failure-app/</guid><description><![CDATA[<p>Let&rsquo;s say your users are trying to access some pdf files that you&rsquo;re serving inline with <a href="https://github.com/mileszs/wicked_pdf">wicked_pdf</a> or any other gem of you choice that is a wrapper for <code>wkhtmltopdf</code>. But they have to sign in first.</p>
<p>What happens when you use devise and you hit <em>pdf</em> url as unauthorized user? By default your app will respond with <code>401 Unauthorized</code> which will get converted into invalid pdf file and this kinda not what you want. Probably ;).</p>
<p>This can be fairly easy resolved with the help of custom <a href="https://github.com/plataformatec/devise/blob/master/lib/devise/failure_app.rb">failure_app</a>. Let the code speak for itself:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># config/initializers/devise.rb</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># you should probably put this somewhere in lib/ ;-)</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MyFailureApp</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">Devise</span><span style="color:#f92672">::</span><span style="color:#66d9ef">FailureApp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">respond</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># let&#39;s say we want to handle that pdf request nicely</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> request<span style="color:#f92672">.</span>format <span style="color:#f92672">==</span> <span style="color:#e6db74">:pdf</span>
</span></span><span style="display:flex;"><span>      pdf_redirect
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">super</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">protected</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Assuming you have new_user_session_path in your routes</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Devise is doing much much more magic, see `scope_url`</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># for more insight!</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">pdf_redirect</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># after signing-in we want to redirect user back to requested pdf file</span>
</span></span><span style="display:flex;"><span>    session<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span>scope<span style="color:#e6db74">}</span><span style="color:#e6db74">_return_to&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> attempted_path <span style="color:#66d9ef">if</span> request<span style="color:#f92672">.</span>get?
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># nothing fancy here, i18n_message &amp; scope are provided by parent class</span>
</span></span><span style="display:flex;"><span>    redirect_to <span style="color:#e6db74">:&#34;new_</span><span style="color:#e6db74">#{</span>scope<span style="color:#e6db74">}</span><span style="color:#e6db74">_session&#34;</span>, format: <span style="color:#e6db74">:html</span>, <span style="color:#e6db74">alert</span>: i18n_message
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Devise</span><span style="color:#f92672">.</span>setup <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>config<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span>  config<span style="color:#f92672">.</span>warden <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>manager<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>    manager<span style="color:#f92672">.</span>failure_app <span style="color:#f92672">=</span> <span style="color:#66d9ef">MyFailureApp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>And that&rsquo;s the simple remedy for our pain ;-).</p>
<p>You can also try playing with <code>Devise.navigational_formats</code>, unfortunately I regret that I didn&rsquo;t wrote that blog-post few weeks ago, because I recall that I stumbled upon some issue while trying to solve it that way. I ended up with custom failure app after all, but right now I simply don&rsquo;t remember exact reasoning :P.</p>]]></description><content:encoded><![CDATA[<p>Let&rsquo;s say your users are trying to access some pdf files that you&rsquo;re serving inline with <a href="https://github.com/mileszs/wicked_pdf">wicked_pdf</a> or any other gem of you choice that is a wrapper for <code>wkhtmltopdf</code>. But they have to sign in first.</p>
<p>What happens when you use devise and you hit <em>pdf</em> url as unauthorized user? By default your app will respond with <code>401 Unauthorized</code> which will get converted into invalid pdf file and this kinda not what you want. Probably ;).</p>
<p>This can be fairly easy resolved with the help of custom <a href="https://github.com/plataformatec/devise/blob/master/lib/devise/failure_app.rb">failure_app</a>. Let the code speak for itself:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># config/initializers/devise.rb</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># you should probably put this somewhere in lib/ ;-)</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MyFailureApp</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">Devise</span><span style="color:#f92672">::</span><span style="color:#66d9ef">FailureApp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">respond</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># let&#39;s say we want to handle that pdf request nicely</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> request<span style="color:#f92672">.</span>format <span style="color:#f92672">==</span> <span style="color:#e6db74">:pdf</span>
</span></span><span style="display:flex;"><span>      pdf_redirect
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">super</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">protected</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Assuming you have new_user_session_path in your routes</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Devise is doing much much more magic, see `scope_url`</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># for more insight!</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">pdf_redirect</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># after signing-in we want to redirect user back to requested pdf file</span>
</span></span><span style="display:flex;"><span>    session<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span>scope<span style="color:#e6db74">}</span><span style="color:#e6db74">_return_to&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> attempted_path <span style="color:#66d9ef">if</span> request<span style="color:#f92672">.</span>get?
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># nothing fancy here, i18n_message &amp; scope are provided by parent class</span>
</span></span><span style="display:flex;"><span>    redirect_to <span style="color:#e6db74">:&#34;new_</span><span style="color:#e6db74">#{</span>scope<span style="color:#e6db74">}</span><span style="color:#e6db74">_session&#34;</span>, format: <span style="color:#e6db74">:html</span>, <span style="color:#e6db74">alert</span>: i18n_message
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Devise</span><span style="color:#f92672">.</span>setup <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>config<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span>  config<span style="color:#f92672">.</span>warden <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>manager<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>    manager<span style="color:#f92672">.</span>failure_app <span style="color:#f92672">=</span> <span style="color:#66d9ef">MyFailureApp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>And that&rsquo;s the simple remedy for our pain ;-).</p>
<p>You can also try playing with <code>Devise.navigational_formats</code>, unfortunately I regret that I didn&rsquo;t wrote that blog-post few weeks ago, because I recall that I stumbled upon some issue while trying to solve it that way. I ended up with custom failure app after all, but right now I simply don&rsquo;t remember exact reasoning :P.</p>]]></content:encoded></item><item><title>Migrating from MacOS to Ubuntu</title><link>https://awesomeprogrammer.com/blog/2015/05/05/migrating-from-macos-to-ubuntu/</link><pubDate>Tue, 05 May 2015 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2015/05/05/migrating-from-macos-to-ubuntu/</guid><description><![CDATA[<h3 id="reasoning">Reasoning</h3>
<p>I&rsquo;m not a huge fan of Apple products, been using mostly linux during my <em>professional career</em> (d&rsquo;oh). I can appreciate the overall hardware build quality (well - at least when it lasts), easy of use (<em>it just works</em>) and all that jazz, but after working on MBA for two years I decided to look around for new machine that would serve me for another few years (hopefully).</p>
<h3 id="hardware">Hardware</h3>
<p>I was a little bit astounded how hard it&rsquo;s to find good Macbook Air replacement in reasonable price range. And for me &lsquo;reasonable&rsquo; = cheaper than buying new Macbook Air/Pro with similar specification.</p>
<p>After googling a lot I ended up with <a href="http://www.asus.com/Notebooks_Ultrabooks/ASUS_ZENBOOK_UX303LN/">Zenbook UX303LN</a> that I paid for around $1200 total - 13&quot; laptop (FHD, glare, non-touch version), packed with i7-5500U processor, 12GB of RAM (4GB soldered + 8GB stick), 256GB SSD drive (replaceable) and GT840 graphic card - I would say it&rsquo;s a good bargain, considering it&rsquo;s specs and build quality. One con worth mentioning: way too loud touchpad (if you use tap this obviously won&rsquo;t be an issue for you), could be a little bit more precise - you can say whatever you want, but Apple simply nailed their touchpad technology and that&rsquo;s one thing I&rsquo;m missing :P.</p>
<h3 id="software">Software</h3>
<p>My laptop came without OS, I decided to go with Ubuntu 15.04 - <a href="http://ubuntugnome.org/">Gnome 3 flavor</a> (after trying out <a href="http://blog.linuxmint.com/?p=2714">new Mint</a>). To &lsquo;fix&rsquo; focaltech touchpad (two finger scroll was no worky) I installed <a href="http://www.omgubuntu.co.uk/2015/04/linux-kernel-4-0-new-features">Kernel 4.0</a>, later decided to upgrade <a href="http://www.webupd8.org/2015/03/gnome-316-released-with-new.html">Gnome to 3.16</a> (staging ppa) - and overall system seems stable, I think it crashed like once for no apparent reason so far :P.</p>
<h4 id="known-issues">Known issues:</h4>
<ul>
<li>
<p>bluetooth seems to be breaking each few updates (lol) - it seems like issue I was solving <a href="/blog/2013/06/22/fixing-bluetooth-audio-issue-on-ubuntu-13-dot-04-based-distors/">almost two years ago</a> still persists - not a huge deal breaker for me atm</p>
</li>
<li>
<p>native screen brightness keyword shortcut does nothing - had to remap it to alt + up/down (seems to do the job) (setting -&gt; keyword -&gt; shortcuts -&gt; add: <code>xbacklight -dec X</code> and similar for <code>xbacklight -inc Y</code>)</p>
</li>
<li>
<p>still didn&rsquo;t find time to properly configure bumblebee &amp; nvidia drivers (still on nouveau driver) - it seems that after gnome upgrade it doesn&rsquo;t play well anymore</p>
</li>
</ul>
<h4 id="my-apps-replacement">My apps replacement:</h4>
<ul>
<li>
<p><a href="http://airmailapp.com/">AirMail</a> -&gt; <a href="https://wiki.gnome.org/Apps/Geary">Geary</a></p>
</li>
<li>
<p><a href="https://itunes.apple.com/app/slack/id803453959?ls=1&amp;mt=12">Slack for Mac</a> -&gt; <a href="https://github.com/wlaurance/slack-for-linux">slack-for-linux</a> / <a href="https://github.com/raelgc/scudcloud">ScudCloud</a></p>
</li>
<li>
<p><a href="http://pow.cx/">pow</a> -&gt; <a href="https://github.com/ysbaddaden/prax">prax</a> (works well with RVM)</p>
</li>
<li>
<p><a href="https://evernote.com/skitch/">skitch</a> -&gt; <a href="http://shutter-project.org/">Shutter</a></p>
</li>
<li>
<p><a href="http://totalterminal.binaryage.com/">TotalTerminal</a> -&gt; <a href="https://github.com/Guake/guake">guake</a></p>
</li>
</ul>
<p>And that&rsquo;s more or less it - the rest of the apps I&rsquo;m using are available for all popular systems (SublimeText, VLC, Clementine, Skype, Telegram etc.). Additionally I installed Windows to utilize that GT840 power - I&rsquo;m not really a huge gamer (anymore), but I must was amazed how smoothly this little piece of hardware can run even newer titles. So from time to time I dual-boot into windows, log into steam and unleash my inner-child ;).</p>
<h3 id="tweaking-gnome">Tweaking Gnome</h3>
<p>Decided to go with icon-less dektop, some useful links and preview below.</p>
<ol>
<li>
<p>Some pretty cool extensions I recommend checking out - huge kudos to the original authors!</p>
<ul>
<li><a href="https://extensions.gnome.org/extension/55/media-player-indicator/">https://extensions.gnome.org/extension/55/media-player-indicator/</a></li>
<li><a href="https://extensions.gnome.org/extension/8/places-status-indicator/">https://extensions.gnome.org/extension/8/places-status-indicator/</a></li>
<li><a href="https://extensions.gnome.org/extension/905/refresh-wifi-connections/">https://extensions.gnome.org/extension/905/refresh-wifi-connections/</a></li>
<li><a href="https://extensions.gnome.org/extension/815/simple-dock/">https://extensions.gnome.org/extension/815/simple-dock/</a></li>
<li><a href="https://extensions.gnome.org/extension/906/sound-output-device-chooser/">https://extensions.gnome.org/extension/906/sound-output-device-chooser/</a></li>
<li><a href="https://extensions.gnome.org/extension/584/taskbar/">https://extensions.gnome.org/extension/584/taskbar/</a></li>
<li><a href="https://extensions.gnome.org/extension/613/weather/">https://extensions.gnome.org/extension/613/weather/</a></li>
<li><a href="https://extensions.gnome.org/extension/549/web-search-dialog/">https://extensions.gnome.org/extension/549/web-search-dialog/</a></li>
<li><a href="https://extensions.gnome.org/extension/800/remove-dropdown-arrows/">https://extensions.gnome.org/extension/800/remove-dropdown-arrows/</a></li>
<li><a href="https://extensions.gnome.org/extension/495/topicons/">https://extensions.gnome.org/extension/495/topicons/</a></li>
<li><a href="https://extensions.gnome.org/extension/818/battery-percentage/">https://extensions.gnome.org/extension/818/battery-percentage/</a></li>
</ul>
</li>
<li>
<p><a href="http://conky.sourceforge.net/">conky</a> with customized <a href="http://psyjunta.deviantart.com/art/Gotham-Conky-config-205465419">Gotham theme</a>. While we&rsquo;re at it, some tips for conky:</p>
</li>
</ol>
<p>To get transparent background:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>own_window yes
</span></span><span style="display:flex;"><span>own_window_type normal
</span></span><span style="display:flex;"><span>own_window_transparent yes
</span></span><span style="display:flex;"><span>own_window_hints undecorated,below,sticky,skip_taskbar,skip_pager
</span></span><span style="display:flex;"><span>own_window_argb_visual yes
</span></span><span style="display:flex;"><span>own_window_colour <span style="color:#ae81ff">000000</span>
</span></span><span style="display:flex;"><span>own_window_argb_value <span style="color:#ae81ff">100</span>
</span></span></code></pre></div><p>Also if you&rsquo;re having height issues (eg. conky is cut off at the bottom) you can try playing with <code>minimum_size X Y</code> setting.</p>
<p>Didn&rsquo;t even tried custom themes yet, Gnome 3.16 out of the box looks pretty damn slick. Here&rsquo;s the result:</p>
<p><a href="/images/workspace.jpg"><img src="/images/workspace.jpg" alt="End result"></a></p>
<p>So that&rsquo;s it, so far system runs smoothly and I&rsquo;m happy with usability as well with overall look&amp;feel :).</p>]]></description><content:encoded><![CDATA[<h3 id="reasoning">Reasoning</h3>
<p>I&rsquo;m not a huge fan of Apple products, been using mostly linux during my <em>professional career</em> (d&rsquo;oh). I can appreciate the overall hardware build quality (well - at least when it lasts), easy of use (<em>it just works</em>) and all that jazz, but after working on MBA for two years I decided to look around for new machine that would serve me for another few years (hopefully).</p>
<h3 id="hardware">Hardware</h3>
<p>I was a little bit astounded how hard it&rsquo;s to find good Macbook Air replacement in reasonable price range. And for me &lsquo;reasonable&rsquo; = cheaper than buying new Macbook Air/Pro with similar specification.</p>
<p>After googling a lot I ended up with <a href="http://www.asus.com/Notebooks_Ultrabooks/ASUS_ZENBOOK_UX303LN/">Zenbook UX303LN</a> that I paid for around $1200 total - 13&quot; laptop (FHD, glare, non-touch version), packed with i7-5500U processor, 12GB of RAM (4GB soldered + 8GB stick), 256GB SSD drive (replaceable) and GT840 graphic card - I would say it&rsquo;s a good bargain, considering it&rsquo;s specs and build quality. One con worth mentioning: way too loud touchpad (if you use tap this obviously won&rsquo;t be an issue for you), could be a little bit more precise - you can say whatever you want, but Apple simply nailed their touchpad technology and that&rsquo;s one thing I&rsquo;m missing :P.</p>
<h3 id="software">Software</h3>
<p>My laptop came without OS, I decided to go with Ubuntu 15.04 - <a href="http://ubuntugnome.org/">Gnome 3 flavor</a> (after trying out <a href="http://blog.linuxmint.com/?p=2714">new Mint</a>). To &lsquo;fix&rsquo; focaltech touchpad (two finger scroll was no worky) I installed <a href="http://www.omgubuntu.co.uk/2015/04/linux-kernel-4-0-new-features">Kernel 4.0</a>, later decided to upgrade <a href="http://www.webupd8.org/2015/03/gnome-316-released-with-new.html">Gnome to 3.16</a> (staging ppa) - and overall system seems stable, I think it crashed like once for no apparent reason so far :P.</p>
<h4 id="known-issues">Known issues:</h4>
<ul>
<li>
<p>bluetooth seems to be breaking each few updates (lol) - it seems like issue I was solving <a href="/blog/2013/06/22/fixing-bluetooth-audio-issue-on-ubuntu-13-dot-04-based-distors/">almost two years ago</a> still persists - not a huge deal breaker for me atm</p>
</li>
<li>
<p>native screen brightness keyword shortcut does nothing - had to remap it to alt + up/down (seems to do the job) (setting -&gt; keyword -&gt; shortcuts -&gt; add: <code>xbacklight -dec X</code> and similar for <code>xbacklight -inc Y</code>)</p>
</li>
<li>
<p>still didn&rsquo;t find time to properly configure bumblebee &amp; nvidia drivers (still on nouveau driver) - it seems that after gnome upgrade it doesn&rsquo;t play well anymore</p>
</li>
</ul>
<h4 id="my-apps-replacement">My apps replacement:</h4>
<ul>
<li>
<p><a href="http://airmailapp.com/">AirMail</a> -&gt; <a href="https://wiki.gnome.org/Apps/Geary">Geary</a></p>
</li>
<li>
<p><a href="https://itunes.apple.com/app/slack/id803453959?ls=1&amp;mt=12">Slack for Mac</a> -&gt; <a href="https://github.com/wlaurance/slack-for-linux">slack-for-linux</a> / <a href="https://github.com/raelgc/scudcloud">ScudCloud</a></p>
</li>
<li>
<p><a href="http://pow.cx/">pow</a> -&gt; <a href="https://github.com/ysbaddaden/prax">prax</a> (works well with RVM)</p>
</li>
<li>
<p><a href="https://evernote.com/skitch/">skitch</a> -&gt; <a href="http://shutter-project.org/">Shutter</a></p>
</li>
<li>
<p><a href="http://totalterminal.binaryage.com/">TotalTerminal</a> -&gt; <a href="https://github.com/Guake/guake">guake</a></p>
</li>
</ul>
<p>And that&rsquo;s more or less it - the rest of the apps I&rsquo;m using are available for all popular systems (SublimeText, VLC, Clementine, Skype, Telegram etc.). Additionally I installed Windows to utilize that GT840 power - I&rsquo;m not really a huge gamer (anymore), but I must was amazed how smoothly this little piece of hardware can run even newer titles. So from time to time I dual-boot into windows, log into steam and unleash my inner-child ;).</p>
<h3 id="tweaking-gnome">Tweaking Gnome</h3>
<p>Decided to go with icon-less dektop, some useful links and preview below.</p>
<ol>
<li>
<p>Some pretty cool extensions I recommend checking out - huge kudos to the original authors!</p>
<ul>
<li><a href="https://extensions.gnome.org/extension/55/media-player-indicator/">https://extensions.gnome.org/extension/55/media-player-indicator/</a></li>
<li><a href="https://extensions.gnome.org/extension/8/places-status-indicator/">https://extensions.gnome.org/extension/8/places-status-indicator/</a></li>
<li><a href="https://extensions.gnome.org/extension/905/refresh-wifi-connections/">https://extensions.gnome.org/extension/905/refresh-wifi-connections/</a></li>
<li><a href="https://extensions.gnome.org/extension/815/simple-dock/">https://extensions.gnome.org/extension/815/simple-dock/</a></li>
<li><a href="https://extensions.gnome.org/extension/906/sound-output-device-chooser/">https://extensions.gnome.org/extension/906/sound-output-device-chooser/</a></li>
<li><a href="https://extensions.gnome.org/extension/584/taskbar/">https://extensions.gnome.org/extension/584/taskbar/</a></li>
<li><a href="https://extensions.gnome.org/extension/613/weather/">https://extensions.gnome.org/extension/613/weather/</a></li>
<li><a href="https://extensions.gnome.org/extension/549/web-search-dialog/">https://extensions.gnome.org/extension/549/web-search-dialog/</a></li>
<li><a href="https://extensions.gnome.org/extension/800/remove-dropdown-arrows/">https://extensions.gnome.org/extension/800/remove-dropdown-arrows/</a></li>
<li><a href="https://extensions.gnome.org/extension/495/topicons/">https://extensions.gnome.org/extension/495/topicons/</a></li>
<li><a href="https://extensions.gnome.org/extension/818/battery-percentage/">https://extensions.gnome.org/extension/818/battery-percentage/</a></li>
</ul>
</li>
<li>
<p><a href="http://conky.sourceforge.net/">conky</a> with customized <a href="http://psyjunta.deviantart.com/art/Gotham-Conky-config-205465419">Gotham theme</a>. While we&rsquo;re at it, some tips for conky:</p>
</li>
</ol>
<p>To get transparent background:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>own_window yes
</span></span><span style="display:flex;"><span>own_window_type normal
</span></span><span style="display:flex;"><span>own_window_transparent yes
</span></span><span style="display:flex;"><span>own_window_hints undecorated,below,sticky,skip_taskbar,skip_pager
</span></span><span style="display:flex;"><span>own_window_argb_visual yes
</span></span><span style="display:flex;"><span>own_window_colour <span style="color:#ae81ff">000000</span>
</span></span><span style="display:flex;"><span>own_window_argb_value <span style="color:#ae81ff">100</span>
</span></span></code></pre></div><p>Also if you&rsquo;re having height issues (eg. conky is cut off at the bottom) you can try playing with <code>minimum_size X Y</code> setting.</p>
<p>Didn&rsquo;t even tried custom themes yet, Gnome 3.16 out of the box looks pretty damn slick. Here&rsquo;s the result:</p>
<p><a href="/images/workspace.jpg"><img src="/images/workspace.jpg" alt="End result"></a></p>
<p>So that&rsquo;s it, so far system runs smoothly and I&rsquo;m happy with usability as well with overall look&amp;feel :).</p>]]></content:encoded></item><item><title>WAN DHCP mystery on Tomato software</title><link>https://awesomeprogrammer.com/blog/2015/04/23/wan-dhcp-mystery-on-tomato-software/</link><pubDate>Thu, 23 Apr 2015 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2015/04/23/wan-dhcp-mystery-on-tomato-software/</guid><description><![CDATA[<p>Lately I&rsquo;ve been messing with my home network - long story short I ended up buying new router because my old Buffalo WZR-HP-G300NH was starting to having problems with wifi signal strength out of the blue. So I ended up with NetGear N300 v2 running Tomato by Shibby and a smaller one TP-LINK WR710N for the other room. And here is the kicker - none of routers was able to obtain IP address on WAN port via DHCP (after cloning mac address of wan port, as mac was filtered by my Internet provided).</p>
<p>I tried different configurations, different firmwares, but it seems like only my Buffalo (running OpenWRT) was working fine. After wasting hours and reading many forums I decided to check default <code>udhcpc</code> settings on OpenWRT very next morning and there I had epiphany - Tomato used different configuration, and to be exact two options were missing:</p>
<ul>
<li><code>-t 0</code> - <em>Send up to N discover packets</em></li>
<li><code>-C</code> - <em>Don&rsquo;t send MAC as client identifier</em></li>
</ul>
<p>My solution:</p>
<ul>
<li>go to Settings -&gt; Advanced -&gt; DHCP Client (WAN)</li>
<li>In <em>DHCPC Options</em> simply add <code>-t 0 -C</code>, click save and enjoy obtained IP address. I&rsquo;m guessing this is required by <em>some</em> routers on the other end (didn&rsquo;t bothered to dig deeper into this)</li>
</ul>
<p>I hope this tip will save some time for some lost souls like me ;).</p>
]]></description><content:encoded><![CDATA[<p>Lately I&rsquo;ve been messing with my home network - long story short I ended up buying new router because my old Buffalo WZR-HP-G300NH was starting to having problems with wifi signal strength out of the blue. So I ended up with NetGear N300 v2 running Tomato by Shibby and a smaller one TP-LINK WR710N for the other room. And here is the kicker - none of routers was able to obtain IP address on WAN port via DHCP (after cloning mac address of wan port, as mac was filtered by my Internet provided).</p>
<p>I tried different configurations, different firmwares, but it seems like only my Buffalo (running OpenWRT) was working fine. After wasting hours and reading many forums I decided to check default <code>udhcpc</code> settings on OpenWRT very next morning and there I had epiphany - Tomato used different configuration, and to be exact two options were missing:</p>
<ul>
<li><code>-t 0</code> - <em>Send up to N discover packets</em></li>
<li><code>-C</code> - <em>Don&rsquo;t send MAC as client identifier</em></li>
</ul>
<p>My solution:</p>
<ul>
<li>go to Settings -&gt; Advanced -&gt; DHCP Client (WAN)</li>
<li>In <em>DHCPC Options</em> simply add <code>-t 0 -C</code>, click save and enjoy obtained IP address. I&rsquo;m guessing this is required by <em>some</em> routers on the other end (didn&rsquo;t bothered to dig deeper into this)</li>
</ul>
<p>I hope this tip will save some time for some lost souls like me ;).</p>
]]></content:encoded></item><item><title>How I failed to create Vagrant-based Ruby On Rails environment</title><link>https://awesomeprogrammer.com/blog/2015/01/31/vagrant-for-ruby-on-rails-development/</link><pubDate>Sat, 31 Jan 2015 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2015/01/31/vagrant-for-ruby-on-rails-development/</guid><description><![CDATA[<p>Did you ever tried to solve a problem of unified development environment? It&rsquo;s hard. I mean - seriously. &lsquo;It works on my machine&rsquo; - how many times did you heard that?</p>
<p>So you have <a href="https://www.vagrantup.com/">Vagrant</a> or <a href="https://www.docker.com/">Docker</a> or maybe some other brand-new-solution. With Vagrant you basically build a whole VM environment, with Docker it&rsquo;s another story (assuming you&rsquo;re running linux).</p>
<p>Personally I prefer using Vagrant, because with Docker - well, you still need Vagrant to run it on mac/windows. Of course you can be a hipster, and run ubuntu using docker on your <a href="https://coreos.com/">CoreOs</a> within Vagrant on MacOs (comment I read somewhere on the Internet that pretty sums it up). And that may have it&rsquo;s pros that&rsquo;s I&rsquo;m refusing to see.</p>
<p>Recently I have build pretty neat (IMO :P) box that consists of:</p>
<ul>
<li><a href="http://www.elastichq.org/">ElasticHQ</a> - for monitoring elasticsearch status</li>
<li><a href="http://mailcatcher.me/">mailcatcher</a> (later replaced by <a href="https://mailtrap.io/">mailtrap.io</a>) - for catching all emails from your dev environment</li>
<li><a href="http://mmonit.com/monit/">monit</a> - for monitoring processes, memory &amp; cpu usage</li>
<li><a href="http://code.macournoyer.com/thin/">thin</a> (alternatively <a href="http://puma.io/">puma</a>) - a rack server that is listening on unix socket (served by nginx)</li>
<li><a href="http://upstart.ubuntu.com/">upstart</a> - for automatically starting processes</li>
<li><a href="https://rvm.io/">rvm</a> - for your ruby &amp; gemsets</li>
</ul>
<p>Of course you can&rsquo;t pack too much into one VM, because obviously it will became too bulky and slow, but with right tuning you can safely run few (two-three) minor-sized rails app within one machine. As a rule of thumb tune logging to minimum, tweak configs (especially if you&rsquo;re using elasticsearch that is memory heavy), don&rsquo;t be afraid to experiment.</p>
<p>So where is the failure you might ask? Unfortunately I traded <em>it works on my machine</em> to <em>it works in my Vagrant</em>. I s..t you not - this happened to me. Despite the whole Vagrant awesomeness at some point I had to revert to local development env, because Vagrant was not longer reliable. So - what happened? This happened:</p>
<ul>
<li><a href="http://fsainz.com/2014/07/25/vagrant-not-updating-files.html">Vagrant not updating Files</a></li>
<li><a href="https://groups.google.com/forum/#!topic/vagrant-up/1WlgDhCKPk4">Synced folders not always updating</a></li>
<li><a href="https://stackoverflow.com/questions/25451327/rails-4-1-doesnt-autoreload-on-vagrant-vm">Rails 4.1 doesn&rsquo;t autoreload on Vagrant VM</a></li>
</ul>
<p>And many more issues &amp; possible solutions related to same thing. What&rsquo;s funny I was never able to reproduce the issues with not updating/reloading code that my colleagues had. I tried both Mac &amp; Linux, different editors and configurations (both NFS &amp; rsync), different versions of software itself, etc. etc. We used same packaged box and yet the basic foundation, the idea of having a dev box didn&rsquo;t worked. At it&rsquo;s core it was somehow broken.</p>
<p>Ultimately I didn&rsquo;t had time to track down the cause of it, my &lsquo;vagrant project&rsquo; became a burden to myself and to my team and i decided to axe it. Hopefully some day I will get back to it (and find a solution, because it&rsquo;s bugging me as hell :P).</p>]]></description><content:encoded><![CDATA[<p>Did you ever tried to solve a problem of unified development environment? It&rsquo;s hard. I mean - seriously. &lsquo;It works on my machine&rsquo; - how many times did you heard that?</p>
<p>So you have <a href="https://www.vagrantup.com/">Vagrant</a> or <a href="https://www.docker.com/">Docker</a> or maybe some other brand-new-solution. With Vagrant you basically build a whole VM environment, with Docker it&rsquo;s another story (assuming you&rsquo;re running linux).</p>
<p>Personally I prefer using Vagrant, because with Docker - well, you still need Vagrant to run it on mac/windows. Of course you can be a hipster, and run ubuntu using docker on your <a href="https://coreos.com/">CoreOs</a> within Vagrant on MacOs (comment I read somewhere on the Internet that pretty sums it up). And that may have it&rsquo;s pros that&rsquo;s I&rsquo;m refusing to see.</p>
<p>Recently I have build pretty neat (IMO :P) box that consists of:</p>
<ul>
<li><a href="http://www.elastichq.org/">ElasticHQ</a> - for monitoring elasticsearch status</li>
<li><a href="http://mailcatcher.me/">mailcatcher</a> (later replaced by <a href="https://mailtrap.io/">mailtrap.io</a>) - for catching all emails from your dev environment</li>
<li><a href="http://mmonit.com/monit/">monit</a> - for monitoring processes, memory &amp; cpu usage</li>
<li><a href="http://code.macournoyer.com/thin/">thin</a> (alternatively <a href="http://puma.io/">puma</a>) - a rack server that is listening on unix socket (served by nginx)</li>
<li><a href="http://upstart.ubuntu.com/">upstart</a> - for automatically starting processes</li>
<li><a href="https://rvm.io/">rvm</a> - for your ruby &amp; gemsets</li>
</ul>
<p>Of course you can&rsquo;t pack too much into one VM, because obviously it will became too bulky and slow, but with right tuning you can safely run few (two-three) minor-sized rails app within one machine. As a rule of thumb tune logging to minimum, tweak configs (especially if you&rsquo;re using elasticsearch that is memory heavy), don&rsquo;t be afraid to experiment.</p>
<p>So where is the failure you might ask? Unfortunately I traded <em>it works on my machine</em> to <em>it works in my Vagrant</em>. I s..t you not - this happened to me. Despite the whole Vagrant awesomeness at some point I had to revert to local development env, because Vagrant was not longer reliable. So - what happened? This happened:</p>
<ul>
<li><a href="http://fsainz.com/2014/07/25/vagrant-not-updating-files.html">Vagrant not updating Files</a></li>
<li><a href="https://groups.google.com/forum/#!topic/vagrant-up/1WlgDhCKPk4">Synced folders not always updating</a></li>
<li><a href="https://stackoverflow.com/questions/25451327/rails-4-1-doesnt-autoreload-on-vagrant-vm">Rails 4.1 doesn&rsquo;t autoreload on Vagrant VM</a></li>
</ul>
<p>And many more issues &amp; possible solutions related to same thing. What&rsquo;s funny I was never able to reproduce the issues with not updating/reloading code that my colleagues had. I tried both Mac &amp; Linux, different editors and configurations (both NFS &amp; rsync), different versions of software itself, etc. etc. We used same packaged box and yet the basic foundation, the idea of having a dev box didn&rsquo;t worked. At it&rsquo;s core it was somehow broken.</p>
<p>Ultimately I didn&rsquo;t had time to track down the cause of it, my &lsquo;vagrant project&rsquo; became a burden to myself and to my team and i decided to axe it. Hopefully some day I will get back to it (and find a solution, because it&rsquo;s bugging me as hell :P).</p>]]></content:encoded></item><item><title>Extracting form objects in practice</title><link>https://awesomeprogrammer.com/blog/2015/01/14/extracting-form-objects-in-practice/</link><pubDate>Wed, 14 Jan 2015 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2015/01/14/extracting-form-objects-in-practice/</guid><description><![CDATA[<p>Did you get to the point where your model is over 1k lines longs and moreover stuffed with various concerns? If so - maybe it&rsquo;s time to think about extracting so called <em>form models</em> (Ruby/Rails community always have fancy names for most simple things :p).</p>
<p>What is a <em>form model</em>? It&rsquo;s simply a Ruby class (<em>d&rsquo;oh</em>) that encapsulates logic related to a single <em>operation</em>. Dead simple example that comes to mind is some kind of a sign up process that exists in almost every web application.</p>
<h3 id="how-to-do-it">How to do it?</h3>
<p>If you are using Rails 4 you can simply include <code>ActiveModel::Model</code> in your ruby class to benefit from validations, callbacks and all that useful stuff. There are also alternatives like <a href="https://github.com/apotonick/reform">reform</a> or <a href="https://github.com/makandra/active_type">active_type</a> - personally I really like the second one as it&rsquo;s really small with no extra dependencies.</p>
<p>So let&rsquo;s say I have a profile form in my app in which user can change bunch of different stuff. It&rsquo;s quite a big form, it allows changing many things, it accepts nested attributes for one or more associated models - in general my <code>User</code> model have a lot of logic related to that <em>form</em>. Let&rsquo;s create new form object using <code>active_type</code> and simply move some logic from one class to another.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># app/models/user/profile_form.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Inherit from User class - as we still want underlying activerecord object</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">User</span><span style="color:#f92672">::</span><span style="color:#66d9ef">ProfileForm</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveType</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Record</span><span style="color:#f92672">[</span><span style="color:#66d9ef">User</span><span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># instead of attr_accessor you can now declare an &#39;attribute&#39; and typecast it if needed</span>
</span></span><span style="display:flex;"><span>  attribute <span style="color:#e6db74">:some_dynamic_attribute</span>, <span style="color:#e6db74">:string</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># active_type have it&#39;s own nested-attributes a-like method, but we&#39;re inheriting from our User clas</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># so we can simply completely move those declarations without breaking anything</span>
</span></span><span style="display:flex;"><span>  accepts_nested_attributes_for <span style="color:#e6db74">:something_1</span>, <span style="color:#e6db74">reject_if</span>: proc { <span style="color:#f92672">|</span>a<span style="color:#f92672">|</span> a<span style="color:#f92672">[</span><span style="color:#e6db74">:config</span><span style="color:#f92672">].</span>blank? }, <span style="color:#e6db74">allow_destroy</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  accepts_nested_attributes_for <span style="color:#e6db74">:something_1</span>, <span style="color:#e6db74">reject_if</span>: proc { <span style="color:#f92672">|</span>a<span style="color:#f92672">|</span> a<span style="color:#f92672">[</span><span style="color:#e6db74">:name</span><span style="color:#f92672">].</span>blank? },   <span style="color:#e6db74">allow_destroy</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># no monkey business during profile update</span>
</span></span><span style="display:flex;"><span>  validate <span style="color:#e6db74">:custom_validation</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># callback related to profile update</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># let&#39;s save we&#39;re stripping some html tags from user&#39;s signature</span>
</span></span><span style="display:flex;"><span>  before_validation <span style="color:#e6db74">:some_callback</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># maybe rebuild cache, because user changed email?</span>
</span></span><span style="display:flex;"><span>  after_update <span style="color:#e6db74">:other_callback</span>, <span style="color:#66d9ef">if</span>: <span style="color:#e6db74">:email_changed?</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>And now in where you used <code>User.find(x)</code> to set a <code>@user</code> for your <code>form_for</code> you can simply use <code>User::ProfileForm.find(x)</code> and it will still work (<code>model_name</code> still points to <code>User</code> so there is need to change params format in your controller).</p>
<h3 id="how-is-that-better">How is that better?</h3>
<p>You moved some code from one class to another one - it this even worth the trouble? I think it is - our <em>main</em> <code>User</code> model in now thinner, we have a class that is responsible for updating user&rsquo;s profile, we&rsquo;re no longer polluting user with bunch of logic that happens only in single place in the application. This logic in not carried away every time you fetch a user from your database.</p>
<p>This can be especially useful if you&rsquo;re doing things like sending emails after updating <em>some important column</em>. Now you have a callback disaster waiting to happen. One day you run a rake task and without even knowing you send a bunch of emails to your users (been there, done that).</p>
<h3 id="downsides">Downsides</h3>
<p>In given example you moved some validation, it&rsquo;s cool you there is only one place in application where user can update it&rsquo;s profile. But if suddenly user will be allowed to update profile in another place and you forget to use proper form object you might end up with inconsistent data. If you&rsquo;re taking logic from <code>User</code> class you need to make sure if it&rsquo;s not used in other places in your application.</p>
<h3 id="gotchas">Gotchas</h3>
<p>Watch our for gems that are included in your parent class (of course if you&rsquo;re using underlying activerecord object). <code>paper_clip</code> for example relies on class name (not model name) when saving attachments so in given example your would have to explicitly set <code>path</code> and <code>url</code> in <code>has_attached_file</code> declaration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># app/models/user.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">User</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  has_attached_file <span style="color:#e6db74">:avatar</span>, <span style="color:#e6db74">styles</span>: { <span style="color:#e6db74">thumb</span>: <span style="color:#e6db74">&#34;50x50&gt;&#34;</span> },
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">path</span>: <span style="color:#e6db74">&#34;:rails_root/public/system/users/:attachment/:id_partition/:style/:filename&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">url</span>: <span style="color:#e6db74">&#34;/system/users/:attachment/:id_partition/:style/:filename&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div>]]></description><content:encoded><![CDATA[<p>Did you get to the point where your model is over 1k lines longs and moreover stuffed with various concerns? If so - maybe it&rsquo;s time to think about extracting so called <em>form models</em> (Ruby/Rails community always have fancy names for most simple things :p).</p>
<p>What is a <em>form model</em>? It&rsquo;s simply a Ruby class (<em>d&rsquo;oh</em>) that encapsulates logic related to a single <em>operation</em>. Dead simple example that comes to mind is some kind of a sign up process that exists in almost every web application.</p>
<h3 id="how-to-do-it">How to do it?</h3>
<p>If you are using Rails 4 you can simply include <code>ActiveModel::Model</code> in your ruby class to benefit from validations, callbacks and all that useful stuff. There are also alternatives like <a href="https://github.com/apotonick/reform">reform</a> or <a href="https://github.com/makandra/active_type">active_type</a> - personally I really like the second one as it&rsquo;s really small with no extra dependencies.</p>
<p>So let&rsquo;s say I have a profile form in my app in which user can change bunch of different stuff. It&rsquo;s quite a big form, it allows changing many things, it accepts nested attributes for one or more associated models - in general my <code>User</code> model have a lot of logic related to that <em>form</em>. Let&rsquo;s create new form object using <code>active_type</code> and simply move some logic from one class to another.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># app/models/user/profile_form.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Inherit from User class - as we still want underlying activerecord object</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">User</span><span style="color:#f92672">::</span><span style="color:#66d9ef">ProfileForm</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveType</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Record</span><span style="color:#f92672">[</span><span style="color:#66d9ef">User</span><span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># instead of attr_accessor you can now declare an &#39;attribute&#39; and typecast it if needed</span>
</span></span><span style="display:flex;"><span>  attribute <span style="color:#e6db74">:some_dynamic_attribute</span>, <span style="color:#e6db74">:string</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># active_type have it&#39;s own nested-attributes a-like method, but we&#39;re inheriting from our User clas</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># so we can simply completely move those declarations without breaking anything</span>
</span></span><span style="display:flex;"><span>  accepts_nested_attributes_for <span style="color:#e6db74">:something_1</span>, <span style="color:#e6db74">reject_if</span>: proc { <span style="color:#f92672">|</span>a<span style="color:#f92672">|</span> a<span style="color:#f92672">[</span><span style="color:#e6db74">:config</span><span style="color:#f92672">].</span>blank? }, <span style="color:#e6db74">allow_destroy</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  accepts_nested_attributes_for <span style="color:#e6db74">:something_1</span>, <span style="color:#e6db74">reject_if</span>: proc { <span style="color:#f92672">|</span>a<span style="color:#f92672">|</span> a<span style="color:#f92672">[</span><span style="color:#e6db74">:name</span><span style="color:#f92672">].</span>blank? },   <span style="color:#e6db74">allow_destroy</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># no monkey business during profile update</span>
</span></span><span style="display:flex;"><span>  validate <span style="color:#e6db74">:custom_validation</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># callback related to profile update</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># let&#39;s save we&#39;re stripping some html tags from user&#39;s signature</span>
</span></span><span style="display:flex;"><span>  before_validation <span style="color:#e6db74">:some_callback</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># maybe rebuild cache, because user changed email?</span>
</span></span><span style="display:flex;"><span>  after_update <span style="color:#e6db74">:other_callback</span>, <span style="color:#66d9ef">if</span>: <span style="color:#e6db74">:email_changed?</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>And now in where you used <code>User.find(x)</code> to set a <code>@user</code> for your <code>form_for</code> you can simply use <code>User::ProfileForm.find(x)</code> and it will still work (<code>model_name</code> still points to <code>User</code> so there is need to change params format in your controller).</p>
<h3 id="how-is-that-better">How is that better?</h3>
<p>You moved some code from one class to another one - it this even worth the trouble? I think it is - our <em>main</em> <code>User</code> model in now thinner, we have a class that is responsible for updating user&rsquo;s profile, we&rsquo;re no longer polluting user with bunch of logic that happens only in single place in the application. This logic in not carried away every time you fetch a user from your database.</p>
<p>This can be especially useful if you&rsquo;re doing things like sending emails after updating <em>some important column</em>. Now you have a callback disaster waiting to happen. One day you run a rake task and without even knowing you send a bunch of emails to your users (been there, done that).</p>
<h3 id="downsides">Downsides</h3>
<p>In given example you moved some validation, it&rsquo;s cool you there is only one place in application where user can update it&rsquo;s profile. But if suddenly user will be allowed to update profile in another place and you forget to use proper form object you might end up with inconsistent data. If you&rsquo;re taking logic from <code>User</code> class you need to make sure if it&rsquo;s not used in other places in your application.</p>
<h3 id="gotchas">Gotchas</h3>
<p>Watch our for gems that are included in your parent class (of course if you&rsquo;re using underlying activerecord object). <code>paper_clip</code> for example relies on class name (not model name) when saving attachments so in given example your would have to explicitly set <code>path</code> and <code>url</code> in <code>has_attached_file</code> declaration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># app/models/user.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">User</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  has_attached_file <span style="color:#e6db74">:avatar</span>, <span style="color:#e6db74">styles</span>: { <span style="color:#e6db74">thumb</span>: <span style="color:#e6db74">&#34;50x50&gt;&#34;</span> },
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">path</span>: <span style="color:#e6db74">&#34;:rails_root/public/system/users/:attachment/:id_partition/:style/:filename&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">url</span>: <span style="color:#e6db74">&#34;/system/users/:attachment/:id_partition/:style/:filename&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div>]]></content:encoded></item><item><title>Vagrant setup for multiple subdomains application (dev box)</title><link>https://awesomeprogrammer.com/blog/2014/12/07/vagrant-setup-for-multiple-subdomains-application/</link><pubDate>Sun, 07 Dec 2014 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2014/12/07/vagrant-setup-for-multiple-subdomains-application/</guid><description><![CDATA[<h3 id="problem">Problem</h3>
<p>Build a vagrant development environment that will serve an rails application that works on multiple subdomains. Moreover that application consists of multiple smaller apps that all are running with a single domain.</p>
<ul>
<li><code>example.dev</code> -&gt; Application #1</li>
<li><code>app.example.dev</code> -&gt; Application #2</li>
<li><code>&lt;any other subdomain&gt;.example.dev</code> -&gt; Application #3</li>
</ul>
<p>Let&rsquo;s assume we&rsquo;re using <code>.dev</code> domains, because we&rsquo;re used to <a href="http://pow.cx/">pow</a>. Meaning those routing should work within vagrant machine and same subdomains should work exactly same way on our local machine.</p>
<h3 id="solution---nginx--dnsmasq">Solution - nginx &amp; dnsmasq</h3>
<p><em>(I assume you&rsquo;re running some kind of ubuntu-based distro, paths may vary between distros)</em></p>
<p>Install nginx &amp; dnsmasq. Edit file <code>/etc/dnsmasq.d/dev-tld</code> as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># /etc/dnsmasq.d/dev-tld</span>
</span></span><span style="display:flex;"><span>local<span style="color:#f92672">=</span>/dev/
</span></span><span style="display:flex;"><span>address<span style="color:#f92672">=</span>/dev/127.0.0.1
</span></span></code></pre></div><p>Restart <code>dnsmasq</code> service. This rule will forward traffic from .dev domains to your localhost - and this is exactly what we want.</p>
<p>Now let&rsquo;s configure nginx, following the scenario above we will need configuration for three apps. We will need some kind of server (thin, puma, unicorn, whatever) that will listen on unix socket. In our nginx config / sites-enabled we need:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>upstream app1 <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  server unix:/apps/app1/tmp/some_rack_server.sock;
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>server <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  listen 80;
</span></span><span style="display:flex;"><span>  server_name example.dev;
</span></span><span style="display:flex;"><span>  root /apps/app1/public;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  location / <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    proxy_pass http://app1;
</span></span><span style="display:flex;"><span>    proxy_set_header Host $host;
</span></span><span style="display:flex;"><span>    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>upstream app2 <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  server unix:/apps/app2/tmp/some_rack_server.sock;
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>server <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  listen 80;
</span></span><span style="display:flex;"><span>  server_name app.example.dev;
</span></span><span style="display:flex;"><span>  root /apps/app2/public;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  location / <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    proxy_pass http://app2;
</span></span><span style="display:flex;"><span>    proxy_set_header Host $host;
</span></span><span style="display:flex;"><span>    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>upstream app3 <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  server unix:/apps/app3/tmp/some_rack_server.sock;
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>server <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  listen 80;
</span></span><span style="display:flex;"><span>  server_name *.example.dev;
</span></span><span style="display:flex;"><span>  root /apps/app3/public;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  location / <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    proxy_pass http://app3;
</span></span><span style="display:flex;"><span>    proxy_set_header Host $host;
</span></span><span style="display:flex;"><span>    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>Nging will listen on port 80 and forward request according to our needs. Traffic from <code>example.dev</code> will be served by our first app (app1), <code>app.example.dev</code> will be served by app2 and any other traffic (<code>*.example.dev</code>) will go through app3.</p>
<p>So this works, nice &amp; easy. We have 3 rack servers listing on UNIX sockets backed by nginx that routes everything. From vagrant box you can access all <code>.dev</code> domains, so far - very cool.</p>
<h3 id="next-step---accessing-dev-subdomains-from-local-machine">Next step - accessing .dev subdomains from local machine</h3>
<p>It&rsquo;s nice, but it&rsquo;s not very useful at this point really - we have no way for accessing <code>example.dev</code> (or any of it&rsquo;s subdomains) from our local machine. But there is already solution for that - some smart folks already created tools for us that solves this problem.</p>
<p>First we need to route traffic from ninx in vagrant (port 80) to our machine, in Vagrantfile you need:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">Vagrant</span><span style="color:#f92672">.</span>configure(<span style="color:#e6db74">&#34;2&#34;</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>config<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span>  config<span style="color:#f92672">.</span>vm<span style="color:#f92672">.</span>network <span style="color:#e6db74">:forwarded_port</span>, <span style="color:#e6db74">guest</span>: <span style="color:#ae81ff">80</span>, <span style="color:#e6db74">host</span>: <span style="color:#ae81ff">8080</span> <span style="color:#75715e"># or any other port available on your machine</span>
</span></span></code></pre></div><p>Note: don&rsquo;t forget to restart your vagrant vm after making changes to Vagrantfile.</p>
<p>Now, depending on what system you are using:</p>
<ul>
<li>on Mac you have already mentioned <a href="http://pow.cx/">pow</a>, after installation you go to <code>~/.pow</code> and type:</li>
</ul>
<pre tabindex="0"><code>echo 8080 &gt; example
</code></pre><ul>
<li>on Linux you have <a href="https://github.com/ysbaddaden/prax">prax</a>, very similarly - after installation you to to <code>~/.prax</code> and type the same thing</li>
</ul>
<p>pow/prax will now forward traffic from <code>*.example.dev</code> on your local machine to port 8080 - so basically this will be handled by nginx in vagrant and it <em>just works</em> with our subdomains setup. Magic.</p>
<p>References: <a href="http://pow.cx/manual.html#section_2.1.4">Port Proxying (pow)</a>, <a href="https://github.com/ysbaddaden/prax/issues/11">Per-app port forwarding (prax)</a>, <a href="https://docs.vagrantup.com/v2/networking/forwarded_ports.html">Vagrant - port forwarding</a>, <a href="http://ruby-journal.com/how-to-setup-rails-app-with-puma-and-nginx/">Rails App With Puma and NGINX</a></p>]]></description><content:encoded><![CDATA[<h3 id="problem">Problem</h3>
<p>Build a vagrant development environment that will serve an rails application that works on multiple subdomains. Moreover that application consists of multiple smaller apps that all are running with a single domain.</p>
<ul>
<li><code>example.dev</code> -&gt; Application #1</li>
<li><code>app.example.dev</code> -&gt; Application #2</li>
<li><code>&lt;any other subdomain&gt;.example.dev</code> -&gt; Application #3</li>
</ul>
<p>Let&rsquo;s assume we&rsquo;re using <code>.dev</code> domains, because we&rsquo;re used to <a href="http://pow.cx/">pow</a>. Meaning those routing should work within vagrant machine and same subdomains should work exactly same way on our local machine.</p>
<h3 id="solution---nginx--dnsmasq">Solution - nginx &amp; dnsmasq</h3>
<p><em>(I assume you&rsquo;re running some kind of ubuntu-based distro, paths may vary between distros)</em></p>
<p>Install nginx &amp; dnsmasq. Edit file <code>/etc/dnsmasq.d/dev-tld</code> as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># /etc/dnsmasq.d/dev-tld</span>
</span></span><span style="display:flex;"><span>local<span style="color:#f92672">=</span>/dev/
</span></span><span style="display:flex;"><span>address<span style="color:#f92672">=</span>/dev/127.0.0.1
</span></span></code></pre></div><p>Restart <code>dnsmasq</code> service. This rule will forward traffic from .dev domains to your localhost - and this is exactly what we want.</p>
<p>Now let&rsquo;s configure nginx, following the scenario above we will need configuration for three apps. We will need some kind of server (thin, puma, unicorn, whatever) that will listen on unix socket. In our nginx config / sites-enabled we need:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>upstream app1 <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  server unix:/apps/app1/tmp/some_rack_server.sock;
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>server <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  listen 80;
</span></span><span style="display:flex;"><span>  server_name example.dev;
</span></span><span style="display:flex;"><span>  root /apps/app1/public;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  location / <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    proxy_pass http://app1;
</span></span><span style="display:flex;"><span>    proxy_set_header Host $host;
</span></span><span style="display:flex;"><span>    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>upstream app2 <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  server unix:/apps/app2/tmp/some_rack_server.sock;
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>server <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  listen 80;
</span></span><span style="display:flex;"><span>  server_name app.example.dev;
</span></span><span style="display:flex;"><span>  root /apps/app2/public;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  location / <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    proxy_pass http://app2;
</span></span><span style="display:flex;"><span>    proxy_set_header Host $host;
</span></span><span style="display:flex;"><span>    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>upstream app3 <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  server unix:/apps/app3/tmp/some_rack_server.sock;
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>server <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  listen 80;
</span></span><span style="display:flex;"><span>  server_name *.example.dev;
</span></span><span style="display:flex;"><span>  root /apps/app3/public;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  location / <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    proxy_pass http://app3;
</span></span><span style="display:flex;"><span>    proxy_set_header Host $host;
</span></span><span style="display:flex;"><span>    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>Nging will listen on port 80 and forward request according to our needs. Traffic from <code>example.dev</code> will be served by our first app (app1), <code>app.example.dev</code> will be served by app2 and any other traffic (<code>*.example.dev</code>) will go through app3.</p>
<p>So this works, nice &amp; easy. We have 3 rack servers listing on UNIX sockets backed by nginx that routes everything. From vagrant box you can access all <code>.dev</code> domains, so far - very cool.</p>
<h3 id="next-step---accessing-dev-subdomains-from-local-machine">Next step - accessing .dev subdomains from local machine</h3>
<p>It&rsquo;s nice, but it&rsquo;s not very useful at this point really - we have no way for accessing <code>example.dev</code> (or any of it&rsquo;s subdomains) from our local machine. But there is already solution for that - some smart folks already created tools for us that solves this problem.</p>
<p>First we need to route traffic from ninx in vagrant (port 80) to our machine, in Vagrantfile you need:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">Vagrant</span><span style="color:#f92672">.</span>configure(<span style="color:#e6db74">&#34;2&#34;</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>config<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span>  config<span style="color:#f92672">.</span>vm<span style="color:#f92672">.</span>network <span style="color:#e6db74">:forwarded_port</span>, <span style="color:#e6db74">guest</span>: <span style="color:#ae81ff">80</span>, <span style="color:#e6db74">host</span>: <span style="color:#ae81ff">8080</span> <span style="color:#75715e"># or any other port available on your machine</span>
</span></span></code></pre></div><p>Note: don&rsquo;t forget to restart your vagrant vm after making changes to Vagrantfile.</p>
<p>Now, depending on what system you are using:</p>
<ul>
<li>on Mac you have already mentioned <a href="http://pow.cx/">pow</a>, after installation you go to <code>~/.pow</code> and type:</li>
</ul>
<pre tabindex="0"><code>echo 8080 &gt; example
</code></pre><ul>
<li>on Linux you have <a href="https://github.com/ysbaddaden/prax">prax</a>, very similarly - after installation you to to <code>~/.prax</code> and type the same thing</li>
</ul>
<p>pow/prax will now forward traffic from <code>*.example.dev</code> on your local machine to port 8080 - so basically this will be handled by nginx in vagrant and it <em>just works</em> with our subdomains setup. Magic.</p>
<p>References: <a href="http://pow.cx/manual.html#section_2.1.4">Port Proxying (pow)</a>, <a href="https://github.com/ysbaddaden/prax/issues/11">Per-app port forwarding (prax)</a>, <a href="https://docs.vagrantup.com/v2/networking/forwarded_ports.html">Vagrant - port forwarding</a>, <a href="http://ruby-journal.com/how-to-setup-rails-app-with-puma-and-nginx/">Rails App With Puma and NGINX</a></p>]]></content:encoded></item><item><title>Rails 3.2 to Rails 4.1 upgrade</title><link>https://awesomeprogrammer.com/blog/2014/09/03/rails-3-dot-2-to-rails-4-dot-1-upgrade/</link><pubDate>Wed, 03 Sep 2014 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2014/09/03/rails-3-dot-2-to-rails-4-dot-1-upgrade/</guid><description><![CDATA[<p>Let&rsquo;s talk about pretty tedious task - upgrading rails, of course it can be interesting experience, but if you have relatively large application to upgrade it can be cumbersome, here are my thoughts after doing through process of switching Rails 3.2 to Rails 4.1.</p>
<ul>
<li>
<p>the basic thing - test suite. Few integration specs are better than none, obviously. Make sure it&rsquo;s nice &amp; green before even starting the process</p>
</li>
<li>
<p><a href="http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html">rails upgrade guide</a> will give you some tips &amp; hints about what changes, what may brake etc. Must read.</p>
</li>
<li>
<p>if you&rsquo;re using observers you will need to include <code>rails-observers</code> gem and if you&rsquo;re not ready to implement strong attributes - go with <code>protected_attributes</code></p>
</li>
<li>
<p>Dependencies. I&rsquo;m guessing you are not running vanilla rails application, but you rather packed your Gemfile with some open source goodies. That&rsquo;s cool, but keep in mind that most of the gems will probably need upgrading as well. <code>bundle outdated</code> command will help you track down what gems needs upgrading. I recommend checking out Changelog/History in gem&rsquo;s repository (if provided) - sometimes you will need to take some additional steps during the upgrade process, but fortunately most of the popular gems are pretty well documented.</p>
</li>
<li>
<p>if you are using rspec, there is a big chance that your test suite still uses old <code>should</code> syntax. There is a really cool tool called <a href="https://github.com/yujinakayama/transpec">Transpec</a> that will help you automatically upgrade your whole test suite (even from rspec2 to rspec3)</p>
</li>
<li>
<p>some more complex SQL queries (joins with custom select etc.) won&rsquo;t allow you to do <code>count</code> - to reproduce old (Rails 3) behavior you have to call it with <code>:all</code> argument - <code>count(:all)</code></p>
</li>
<li>
<p>watch our if you&rsquo;re using <code>EXIST</code> query - I noticed that rails 4 returns boolean, where in 3.2 you got &rsquo;t&rsquo; / &lsquo;f&rsquo; string (postgresql)</p>
</li>
<li>
<p>using cancan? Cool - but check out it&rsquo;s successor called <a href="https://github.com/CanCanCommunity/cancancan">cancancan</a> - it&rsquo;s very actively maintained by open source community</p>
</li>
<li>
<p>Rails 4 comes with <a href="http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html">default headers</a> that will block running your app from iframe - if you would like to disable this behavior put this in your application config:</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>config<span style="color:#f92672">.</span>action_dispatch<span style="color:#f92672">.</span>default_headers <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># documentation specifies ALLOWALL, but empty string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># should work across all common browsers</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#39;X-Frame-Options&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;&#39;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The list goes on, but every app is different, so I will stop here and wish you best luck with your upgrade process ;-). Let the 200 response status be with you, always.</p>]]></description><content:encoded><![CDATA[<p>Let&rsquo;s talk about pretty tedious task - upgrading rails, of course it can be interesting experience, but if you have relatively large application to upgrade it can be cumbersome, here are my thoughts after doing through process of switching Rails 3.2 to Rails 4.1.</p>
<ul>
<li>
<p>the basic thing - test suite. Few integration specs are better than none, obviously. Make sure it&rsquo;s nice &amp; green before even starting the process</p>
</li>
<li>
<p><a href="http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html">rails upgrade guide</a> will give you some tips &amp; hints about what changes, what may brake etc. Must read.</p>
</li>
<li>
<p>if you&rsquo;re using observers you will need to include <code>rails-observers</code> gem and if you&rsquo;re not ready to implement strong attributes - go with <code>protected_attributes</code></p>
</li>
<li>
<p>Dependencies. I&rsquo;m guessing you are not running vanilla rails application, but you rather packed your Gemfile with some open source goodies. That&rsquo;s cool, but keep in mind that most of the gems will probably need upgrading as well. <code>bundle outdated</code> command will help you track down what gems needs upgrading. I recommend checking out Changelog/History in gem&rsquo;s repository (if provided) - sometimes you will need to take some additional steps during the upgrade process, but fortunately most of the popular gems are pretty well documented.</p>
</li>
<li>
<p>if you are using rspec, there is a big chance that your test suite still uses old <code>should</code> syntax. There is a really cool tool called <a href="https://github.com/yujinakayama/transpec">Transpec</a> that will help you automatically upgrade your whole test suite (even from rspec2 to rspec3)</p>
</li>
<li>
<p>some more complex SQL queries (joins with custom select etc.) won&rsquo;t allow you to do <code>count</code> - to reproduce old (Rails 3) behavior you have to call it with <code>:all</code> argument - <code>count(:all)</code></p>
</li>
<li>
<p>watch our if you&rsquo;re using <code>EXIST</code> query - I noticed that rails 4 returns boolean, where in 3.2 you got &rsquo;t&rsquo; / &lsquo;f&rsquo; string (postgresql)</p>
</li>
<li>
<p>using cancan? Cool - but check out it&rsquo;s successor called <a href="https://github.com/CanCanCommunity/cancancan">cancancan</a> - it&rsquo;s very actively maintained by open source community</p>
</li>
<li>
<p>Rails 4 comes with <a href="http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html">default headers</a> that will block running your app from iframe - if you would like to disable this behavior put this in your application config:</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>config<span style="color:#f92672">.</span>action_dispatch<span style="color:#f92672">.</span>default_headers <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># documentation specifies ALLOWALL, but empty string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># should work across all common browsers</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#39;X-Frame-Options&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;&#39;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The list goes on, but every app is different, so I will stop here and wish you best luck with your upgrade process ;-). Let the 200 response status be with you, always.</p>]]></content:encoded></item><item><title>Why my code is so darn slow?!</title><link>https://awesomeprogrammer.com/blog/2014/07/06/why-my-code-is-so-darn-slow/</link><pubDate>Sun, 06 Jul 2014 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2014/07/06/why-my-code-is-so-darn-slow/</guid><description><![CDATA[<p>-&gt; <img src="/images/slow_code.jpg" alt="Slow code?" title="Fry is confused about all this"> &lt;-</p>
<p>Ruby is very pretty, and unfortunattely it can be very slow. Beautiful syntax and flexebility of this language has its price (well, it&rsquo;s not so bad compared to other interpreted languages anyway).</p>
<p>Let&rsquo;s go over few (totally random) things that might cause performance issues.</p>
<h3 id="abusing-rails-view-helpers">Abusing rails view helpers</h3>
<p>Rails helpers are very useful, but if you&rsquo;re calling various helpers hundreds of times on a single page you might be surprised how much overhead it might produce. Do you really need that <code>number_with_delimiter</code> everywhere? Maybe you can use something simpler than <code>number_with_precision</code>?</p>
<p>Take a look at those two benchmarks:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;action_view&#34;</span> <span style="color:#75715e">#rails 4.1.4</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;benchmark&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Helper</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">extend</span> <span style="color:#66d9ef">ActionView</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Helpers</span><span style="color:#f92672">::</span><span style="color:#66d9ef">NumberHelper</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>iterations <span style="color:#f92672">=</span> (<span style="color:#ae81ff">1</span><span style="color:#f92672">..</span><span style="color:#ae81ff">100_000</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Benchmark</span><span style="color:#f92672">.</span>bm(<span style="color:#ae81ff">20</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>x<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;number_with_delimiter&#34;</span>) { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> <span style="color:#66d9ef">Helper</span><span style="color:#f92672">.</span>number_with_delimiter(<span style="color:#ae81ff">100</span>) } }
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;without helper&#34;</span>)        { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> i } }
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Benchmark</span><span style="color:#f92672">.</span>bm(<span style="color:#ae81ff">20</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>x<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;number_with_precision&#34;</span>) { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> <span style="color:#66d9ef">Helper</span><span style="color:#f92672">.</span>number_with_precision(i <span style="color:#f92672">*</span> <span style="color:#ae81ff">1</span><span style="color:#f92672">.</span><span style="color:#ae81ff">1123</span>, <span style="color:#e6db74">precision</span>: <span style="color:#ae81ff">2</span>) } }
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;without helper&#34;</span>)        { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> <span style="color:#e6db74">&#39;%.2f&#39;</span> <span style="color:#f92672">%</span> (i<span style="color:#f92672">*</span><span style="color:#ae81ff">1</span><span style="color:#f92672">.</span><span style="color:#ae81ff">1123</span>) } }
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#                            user     system      total        real</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># number_with_delimiter  3.350000   0.010000   3.360000 (  3.360141)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># without helper         0.000000   0.000000   0.000000 (  0.004517)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#                            user     system      total        real</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># number_with_precision 10.220000   0.010000  10.230000 ( 10.221791)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># without helper         0.120000   0.000000   0.120000 (  0.121396)</span>
</span></span></code></pre></div><p>If you&rsquo;re generating huge lists, summaries or reports - every ms matters.</p>
<h3 id="hashes---to-fetch-or-not-to-fetch">Hashes - to .fetch or not to .fetch?</h3>
<p>Stop and thing about very simple thing as retrieving value from a hash. Let&rsquo;s run some benchmarks:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;benchmark&#34;</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;ostruct&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>iterations <span style="color:#f92672">=</span> (<span style="color:#ae81ff">1</span><span style="color:#f92672">..</span><span style="color:#ae81ff">100_000</span>)
</span></span><span style="display:flex;"><span>hash <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> hash<span style="color:#f92672">[</span>i<span style="color:#f92672">]</span> <span style="color:#f92672">=</span> i }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Benchmark</span><span style="color:#f92672">.</span>bm(<span style="color:#ae81ff">20</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>x<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;fetch, proc&#34;</span>)     { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> hash<span style="color:#f92672">.</span>fetch(i) { <span style="color:#66d9ef">OpenStruct</span><span style="color:#f92672">.</span>new(<span style="color:#e6db74">a</span>: <span style="color:#e6db74">&#34;one&#34;</span>, <span style="color:#e6db74">b</span>: <span style="color:#e6db74">&#34;two&#34;</span>) } } }
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;fetch, argument&#34;</span>) { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> hash<span style="color:#f92672">.</span>fetch(i, <span style="color:#66d9ef">OpenStruct</span><span style="color:#f92672">.</span>new(<span style="color:#e6db74">a</span>: <span style="color:#e6db74">&#34;one&#34;</span>, <span style="color:#e6db74">b</span>: <span style="color:#e6db74">&#34;two&#34;</span>))} }
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;[]&#34;</span>)              { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> hash<span style="color:#f92672">[</span>i<span style="color:#f92672">]</span> } }
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#                            user     system      total        real</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># fetch, proc            0.020000   0.000000   0.020000 (  0.024753)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># fetch, argument        1.080000   0.010000   1.090000 (  1.085491)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># []                     0.020000   0.000000   0.020000 (  0.019607)</span>
</span></span></code></pre></div><p><code>[]</code> seems like the fastest way (by tiny margin) to get the value, but what the hell happened in <code>fetch</code> with provided default argument?! Let me tell you that - it&rsquo;s a trap ;-). If you provide argument to a fetch method it will be <em>always</em> evaluated, <code>proc</code> on the other hand is only called when given key is missing.</p>
<p>Another gotcha related to hashes that is worth mentioning here:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>puts({ <span style="color:#e6db74">&#34;false&#34;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">false</span>}<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;false&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">||</span> <span style="color:#66d9ef">true</span>)         <span style="color:#75715e"># -&gt; true</span>
</span></span><span style="display:flex;"><span>puts({ <span style="color:#e6db74">&#34;false&#34;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">false</span> }<span style="color:#f92672">.</span>fetch(<span style="color:#e6db74">&#34;false&#34;</span>) { <span style="color:#66d9ef">true</span> }) <span style="color:#75715e"># -&gt; false</span>
</span></span></code></pre></div><h3 id="use-rubys-native-methods">Use Ruby&rsquo;s native methods</h3>
<p>Even simple things like checking if given date is a weekend can be optimized. In example below first method uses naive approach, when second method refers to methods provided by <code>Date</code> class.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;benchmark&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>iterations <span style="color:#f92672">=</span> (<span style="color:#ae81ff">1</span><span style="color:#f92672">..</span><span style="color:#ae81ff">100_000</span>)
</span></span><span style="display:flex;"><span>days <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> days<span style="color:#f92672">[</span>i<span style="color:#f92672">]</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">Time</span><span style="color:#f92672">.</span>at(i <span style="color:#f92672">*</span> <span style="color:#ae81ff">3600</span>) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Weekend</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">.</span><span style="color:#a6e22e">include?</span>(date)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">[</span><span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">6</span><span style="color:#f92672">].</span>include?(date<span style="color:#f92672">.</span>wday)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">.</span><span style="color:#a6e22e">ruby</span>(date)
</span></span><span style="display:flex;"><span>     date<span style="color:#f92672">.</span>sunday? <span style="color:#f92672">||</span> date<span style="color:#f92672">.</span>saturday?
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Benchmark</span><span style="color:#f92672">.</span>bm(<span style="color:#ae81ff">20</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>x<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;check array&#34;</span>)  { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> <span style="color:#66d9ef">Weekend</span><span style="color:#f92672">.</span>include?(days<span style="color:#f92672">[</span>i<span style="color:#f92672">]</span>) } }
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;use c method&#34;</span>) { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> <span style="color:#66d9ef">Weekend</span><span style="color:#f92672">.</span>ruby(days<span style="color:#f92672">[</span>i<span style="color:#f92672">]</span>) } }
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#                            user     system      total        real</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># check array            0.160000   0.020000   0.180000 (  0.184166)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># use c method           0.040000   0.000000   0.040000 (  0.039794)</span>
</span></span></code></pre></div><h3 id="memoize-gotcha">Memoize gotcha</h3>
<p>Often you want to cache some method&rsquo;s result into instance variable (simply by using <code>||=</code>), but let&rsquo;s say it&rsquo;s a complex SQL query/algorithm that returns <code>false</code> - if this is the case it will be executed over and over again, you might even not notice it at the beginning. The solution to this problem is quite simple, but worth mentioning.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;benchmark&#34;</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;ostruct&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">VariableCache</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">method1</span>
</span></span><span style="display:flex;"><span>    @method1 <span style="color:#f92672">||=</span> <span style="color:#66d9ef">begin</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">OpenStruct</span><span style="color:#f92672">.</span>new(<span style="color:#e6db74">a</span>: <span style="color:#e6db74">&#34;b&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">method2</span>
</span></span><span style="display:flex;"><span>    @method2 <span style="color:#f92672">=</span> <span style="color:#66d9ef">begin</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">OpenStruct</span><span style="color:#f92672">.</span>new(<span style="color:#e6db74">a</span>: <span style="color:#e6db74">&#34;b&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span> <span style="color:#66d9ef">unless</span> defined?(@method2)
</span></span><span style="display:flex;"><span>    @method2
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>iterations <span style="color:#f92672">=</span> (<span style="color:#ae81ff">1</span><span style="color:#f92672">..</span><span style="color:#ae81ff">100_000</span>)
</span></span><span style="display:flex;"><span>cache_test <span style="color:#f92672">=</span> <span style="color:#66d9ef">VariableCache</span><span style="color:#f92672">.</span>new
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Benchmark</span><span style="color:#f92672">.</span>bm(<span style="color:#ae81ff">20</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>x<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;method1&#34;</span>) { iterations<span style="color:#f92672">.</span>each { cache_test<span style="color:#f92672">.</span>method1 }}
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;method2&#34;</span>) { iterations<span style="color:#f92672">.</span>each { cache_test<span style="color:#f92672">.</span>method2 }}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#                            user     system      total        real</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># method1                0.620000   0.000000   0.620000 (  0.629754)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># method2                0.010000   0.000000   0.010000 (  0.013680)</span>
</span></span></code></pre></div><p>Those tiny things can add up and affect usability of your app. If you ever encounter performance issues stop for a minute, browse your code, benchmark it. You can help yourself by using <a href="https://github.com/ruby-prof/ruby-prof">ruby-prof</a> or <a href="https://github.com/MiniProfiler/rack-mini-profiler">rack-mini-profiler</a> - those two tools are quite powerful, but that&rsquo;s a topic for yet another blog post ;).</p>
<p><em>Note: Benchmarks executed on ruby 2.1.2p95</em></p>]]></description><content:encoded><![CDATA[<p>-&gt; <img src="/images/slow_code.jpg" alt="Slow code?" title="Fry is confused about all this"> &lt;-</p>
<p>Ruby is very pretty, and unfortunattely it can be very slow. Beautiful syntax and flexebility of this language has its price (well, it&rsquo;s not so bad compared to other interpreted languages anyway).</p>
<p>Let&rsquo;s go over few (totally random) things that might cause performance issues.</p>
<h3 id="abusing-rails-view-helpers">Abusing rails view helpers</h3>
<p>Rails helpers are very useful, but if you&rsquo;re calling various helpers hundreds of times on a single page you might be surprised how much overhead it might produce. Do you really need that <code>number_with_delimiter</code> everywhere? Maybe you can use something simpler than <code>number_with_precision</code>?</p>
<p>Take a look at those two benchmarks:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;action_view&#34;</span> <span style="color:#75715e">#rails 4.1.4</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;benchmark&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Helper</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">extend</span> <span style="color:#66d9ef">ActionView</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Helpers</span><span style="color:#f92672">::</span><span style="color:#66d9ef">NumberHelper</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>iterations <span style="color:#f92672">=</span> (<span style="color:#ae81ff">1</span><span style="color:#f92672">..</span><span style="color:#ae81ff">100_000</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Benchmark</span><span style="color:#f92672">.</span>bm(<span style="color:#ae81ff">20</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>x<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;number_with_delimiter&#34;</span>) { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> <span style="color:#66d9ef">Helper</span><span style="color:#f92672">.</span>number_with_delimiter(<span style="color:#ae81ff">100</span>) } }
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;without helper&#34;</span>)        { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> i } }
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Benchmark</span><span style="color:#f92672">.</span>bm(<span style="color:#ae81ff">20</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>x<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;number_with_precision&#34;</span>) { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> <span style="color:#66d9ef">Helper</span><span style="color:#f92672">.</span>number_with_precision(i <span style="color:#f92672">*</span> <span style="color:#ae81ff">1</span><span style="color:#f92672">.</span><span style="color:#ae81ff">1123</span>, <span style="color:#e6db74">precision</span>: <span style="color:#ae81ff">2</span>) } }
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;without helper&#34;</span>)        { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> <span style="color:#e6db74">&#39;%.2f&#39;</span> <span style="color:#f92672">%</span> (i<span style="color:#f92672">*</span><span style="color:#ae81ff">1</span><span style="color:#f92672">.</span><span style="color:#ae81ff">1123</span>) } }
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#                            user     system      total        real</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># number_with_delimiter  3.350000   0.010000   3.360000 (  3.360141)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># without helper         0.000000   0.000000   0.000000 (  0.004517)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#                            user     system      total        real</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># number_with_precision 10.220000   0.010000  10.230000 ( 10.221791)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># without helper         0.120000   0.000000   0.120000 (  0.121396)</span>
</span></span></code></pre></div><p>If you&rsquo;re generating huge lists, summaries or reports - every ms matters.</p>
<h3 id="hashes---to-fetch-or-not-to-fetch">Hashes - to .fetch or not to .fetch?</h3>
<p>Stop and thing about very simple thing as retrieving value from a hash. Let&rsquo;s run some benchmarks:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;benchmark&#34;</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;ostruct&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>iterations <span style="color:#f92672">=</span> (<span style="color:#ae81ff">1</span><span style="color:#f92672">..</span><span style="color:#ae81ff">100_000</span>)
</span></span><span style="display:flex;"><span>hash <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> hash<span style="color:#f92672">[</span>i<span style="color:#f92672">]</span> <span style="color:#f92672">=</span> i }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Benchmark</span><span style="color:#f92672">.</span>bm(<span style="color:#ae81ff">20</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>x<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;fetch, proc&#34;</span>)     { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> hash<span style="color:#f92672">.</span>fetch(i) { <span style="color:#66d9ef">OpenStruct</span><span style="color:#f92672">.</span>new(<span style="color:#e6db74">a</span>: <span style="color:#e6db74">&#34;one&#34;</span>, <span style="color:#e6db74">b</span>: <span style="color:#e6db74">&#34;two&#34;</span>) } } }
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;fetch, argument&#34;</span>) { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> hash<span style="color:#f92672">.</span>fetch(i, <span style="color:#66d9ef">OpenStruct</span><span style="color:#f92672">.</span>new(<span style="color:#e6db74">a</span>: <span style="color:#e6db74">&#34;one&#34;</span>, <span style="color:#e6db74">b</span>: <span style="color:#e6db74">&#34;two&#34;</span>))} }
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;[]&#34;</span>)              { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> hash<span style="color:#f92672">[</span>i<span style="color:#f92672">]</span> } }
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#                            user     system      total        real</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># fetch, proc            0.020000   0.000000   0.020000 (  0.024753)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># fetch, argument        1.080000   0.010000   1.090000 (  1.085491)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># []                     0.020000   0.000000   0.020000 (  0.019607)</span>
</span></span></code></pre></div><p><code>[]</code> seems like the fastest way (by tiny margin) to get the value, but what the hell happened in <code>fetch</code> with provided default argument?! Let me tell you that - it&rsquo;s a trap ;-). If you provide argument to a fetch method it will be <em>always</em> evaluated, <code>proc</code> on the other hand is only called when given key is missing.</p>
<p>Another gotcha related to hashes that is worth mentioning here:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>puts({ <span style="color:#e6db74">&#34;false&#34;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">false</span>}<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;false&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">||</span> <span style="color:#66d9ef">true</span>)         <span style="color:#75715e"># -&gt; true</span>
</span></span><span style="display:flex;"><span>puts({ <span style="color:#e6db74">&#34;false&#34;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">false</span> }<span style="color:#f92672">.</span>fetch(<span style="color:#e6db74">&#34;false&#34;</span>) { <span style="color:#66d9ef">true</span> }) <span style="color:#75715e"># -&gt; false</span>
</span></span></code></pre></div><h3 id="use-rubys-native-methods">Use Ruby&rsquo;s native methods</h3>
<p>Even simple things like checking if given date is a weekend can be optimized. In example below first method uses naive approach, when second method refers to methods provided by <code>Date</code> class.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;benchmark&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>iterations <span style="color:#f92672">=</span> (<span style="color:#ae81ff">1</span><span style="color:#f92672">..</span><span style="color:#ae81ff">100_000</span>)
</span></span><span style="display:flex;"><span>days <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> days<span style="color:#f92672">[</span>i<span style="color:#f92672">]</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">Time</span><span style="color:#f92672">.</span>at(i <span style="color:#f92672">*</span> <span style="color:#ae81ff">3600</span>) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Weekend</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">.</span><span style="color:#a6e22e">include?</span>(date)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">[</span><span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">6</span><span style="color:#f92672">].</span>include?(date<span style="color:#f92672">.</span>wday)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">.</span><span style="color:#a6e22e">ruby</span>(date)
</span></span><span style="display:flex;"><span>     date<span style="color:#f92672">.</span>sunday? <span style="color:#f92672">||</span> date<span style="color:#f92672">.</span>saturday?
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Benchmark</span><span style="color:#f92672">.</span>bm(<span style="color:#ae81ff">20</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>x<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;check array&#34;</span>)  { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> <span style="color:#66d9ef">Weekend</span><span style="color:#f92672">.</span>include?(days<span style="color:#f92672">[</span>i<span style="color:#f92672">]</span>) } }
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;use c method&#34;</span>) { iterations<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>i<span style="color:#f92672">|</span> <span style="color:#66d9ef">Weekend</span><span style="color:#f92672">.</span>ruby(days<span style="color:#f92672">[</span>i<span style="color:#f92672">]</span>) } }
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#                            user     system      total        real</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># check array            0.160000   0.020000   0.180000 (  0.184166)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># use c method           0.040000   0.000000   0.040000 (  0.039794)</span>
</span></span></code></pre></div><h3 id="memoize-gotcha">Memoize gotcha</h3>
<p>Often you want to cache some method&rsquo;s result into instance variable (simply by using <code>||=</code>), but let&rsquo;s say it&rsquo;s a complex SQL query/algorithm that returns <code>false</code> - if this is the case it will be executed over and over again, you might even not notice it at the beginning. The solution to this problem is quite simple, but worth mentioning.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;benchmark&#34;</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;ostruct&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">VariableCache</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">method1</span>
</span></span><span style="display:flex;"><span>    @method1 <span style="color:#f92672">||=</span> <span style="color:#66d9ef">begin</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">OpenStruct</span><span style="color:#f92672">.</span>new(<span style="color:#e6db74">a</span>: <span style="color:#e6db74">&#34;b&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">method2</span>
</span></span><span style="display:flex;"><span>    @method2 <span style="color:#f92672">=</span> <span style="color:#66d9ef">begin</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">OpenStruct</span><span style="color:#f92672">.</span>new(<span style="color:#e6db74">a</span>: <span style="color:#e6db74">&#34;b&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span> <span style="color:#66d9ef">unless</span> defined?(@method2)
</span></span><span style="display:flex;"><span>    @method2
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>iterations <span style="color:#f92672">=</span> (<span style="color:#ae81ff">1</span><span style="color:#f92672">..</span><span style="color:#ae81ff">100_000</span>)
</span></span><span style="display:flex;"><span>cache_test <span style="color:#f92672">=</span> <span style="color:#66d9ef">VariableCache</span><span style="color:#f92672">.</span>new
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Benchmark</span><span style="color:#f92672">.</span>bm(<span style="color:#ae81ff">20</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>x<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;method1&#34;</span>) { iterations<span style="color:#f92672">.</span>each { cache_test<span style="color:#f92672">.</span>method1 }}
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;method2&#34;</span>) { iterations<span style="color:#f92672">.</span>each { cache_test<span style="color:#f92672">.</span>method2 }}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#                            user     system      total        real</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># method1                0.620000   0.000000   0.620000 (  0.629754)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># method2                0.010000   0.000000   0.010000 (  0.013680)</span>
</span></span></code></pre></div><p>Those tiny things can add up and affect usability of your app. If you ever encounter performance issues stop for a minute, browse your code, benchmark it. You can help yourself by using <a href="https://github.com/ruby-prof/ruby-prof">ruby-prof</a> or <a href="https://github.com/MiniProfiler/rack-mini-profiler">rack-mini-profiler</a> - those two tools are quite powerful, but that&rsquo;s a topic for yet another blog post ;).</p>
<p><em>Note: Benchmarks executed on ruby 2.1.2p95</em></p>]]></content:encoded></item><item><title>How to protect your website against spammers</title><link>https://awesomeprogrammer.com/blog/2014/06/25/how-to-protect-your-website-against-spammers/</link><pubDate>Wed, 25 Jun 2014 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2014/06/25/how-to-protect-your-website-against-spammers/</guid><description><![CDATA[<p>-&gt; <img src="/images/spam.png" alt="Spam" title="We don't want your spam"> &lt;-</p>
<p>When you&rsquo;re running community-driven website sooner or later you will have to fight back spammers. There can be different kind of spammers, but the main question here is - how do we stop them from flooding your precious site?</p>
<p>Few days ago I noticed huge spike in users registration count, apparently some &lsquo;seo&rsquo; agency (quote indented) decided that website I&rsquo;m running is a great place for new backlinks directory ;-).</p>
<p>I&rsquo;m running devise with obligatory email confirmation, so that&rsquo;s the first gate you have to pass. Let&rsquo;s say you don&rsquo;t have to confirm your account, what are the other options here? Obviously captcha comes to mind. You can go and try good (?) old <a href="https://github.com/ambethia/recaptcha/">reCAPTCHA</a>, but personally I&rsquo;m not a big fan of it. You can get some great results with something similar to <a href="https://github.com/subwindow/negative-captcha">Negative Captcha</a> that is 100% transparent to the user.</p>
<p>If that won&rsquo;t help I recommend using some heavy artillery - <a href="https://github.com/kickstarter/rack-attack">rack-attack</a>. It&rsquo;s a rack middleware that can be a great help in those situations. Let&rsquo;s go over few common cases here:</p>
<ul>
<li>spam is coming from few IP addresses, maybe a IP range or something, it&rsquo;s should be quite easy to block with this formula (first see <a href="https://github.com/kickstarter/rack-attack#usage">usage notes</a>):</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">Rack</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Attack</span><span style="color:#f92672">.</span>blacklist(<span style="color:#e6db74">&#39;block spammers&#39;</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>req<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">[</span><span style="color:#e6db74">&#34;1.2.3.4&#34;</span>, <span style="color:#e6db74">&#34;5.6.7.9&#34;</span>, <span style="color:#e6db74">&#34;and-so-on&#34;</span><span style="color:#f92672">].</span>include?(req<span style="color:#f92672">.</span>ip)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><ul>
<li>you can&rsquo;t filter traffic based on IP, but spammers are hitting your sign up page pretty hard - let&rsquo;s blacklist too many sign up attempts</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">Rack</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Attack</span><span style="color:#f92672">.</span>blacklist(<span style="color:#e6db74">&#39;allow2ban signup attempts&#39;</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>req<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># After 5 requests in 6 hours, block all requests from that IP for 24 hours</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">Rack</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Attack</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Allow2Ban</span><span style="color:#f92672">.</span>filter(req<span style="color:#f92672">.</span>ip, <span style="color:#e6db74">maxretry</span>: <span style="color:#ae81ff">5</span>, <span style="color:#e6db74">findtime</span>: <span style="color:#ae81ff">6</span><span style="color:#f92672">.</span>hours, <span style="color:#e6db74">bantime</span>: <span style="color:#ae81ff">24</span><span style="color:#f92672">.</span>hours) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    req<span style="color:#f92672">.</span>post? <span style="color:#f92672">&amp;&amp;</span> req<span style="color:#f92672">.</span>path <span style="color:#f92672">=~</span> <span style="color:#e6db74">/sign_up/</span> <span style="color:#75715e"># POST request pointed to your sign_up url</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><ul>
<li>still not good? Try limiting requests per seconds - just be sure to set this accordingly to your app (traffic you&rsquo;re getting, amount of ajax requests etc.)</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># Throttle requests to 5 requests per second per ip</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Rack</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Attack</span><span style="color:#f92672">.</span>throttle(<span style="color:#e6db74">&#34;throttle req per ip&#34;</span>, <span style="color:#e6db74">limit</span>: <span style="color:#ae81ff">5</span>, <span style="color:#e6db74">period</span>: <span style="color:#ae81ff">1</span><span style="color:#f92672">.</span>second) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>req<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  req<span style="color:#f92672">.</span>ip
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Good luck and may the Force be with you ;-).</p>]]></description><content:encoded><![CDATA[<p>-&gt; <img src="/images/spam.png" alt="Spam" title="We don't want your spam"> &lt;-</p>
<p>When you&rsquo;re running community-driven website sooner or later you will have to fight back spammers. There can be different kind of spammers, but the main question here is - how do we stop them from flooding your precious site?</p>
<p>Few days ago I noticed huge spike in users registration count, apparently some &lsquo;seo&rsquo; agency (quote indented) decided that website I&rsquo;m running is a great place for new backlinks directory ;-).</p>
<p>I&rsquo;m running devise with obligatory email confirmation, so that&rsquo;s the first gate you have to pass. Let&rsquo;s say you don&rsquo;t have to confirm your account, what are the other options here? Obviously captcha comes to mind. You can go and try good (?) old <a href="https://github.com/ambethia/recaptcha/">reCAPTCHA</a>, but personally I&rsquo;m not a big fan of it. You can get some great results with something similar to <a href="https://github.com/subwindow/negative-captcha">Negative Captcha</a> that is 100% transparent to the user.</p>
<p>If that won&rsquo;t help I recommend using some heavy artillery - <a href="https://github.com/kickstarter/rack-attack">rack-attack</a>. It&rsquo;s a rack middleware that can be a great help in those situations. Let&rsquo;s go over few common cases here:</p>
<ul>
<li>spam is coming from few IP addresses, maybe a IP range or something, it&rsquo;s should be quite easy to block with this formula (first see <a href="https://github.com/kickstarter/rack-attack#usage">usage notes</a>):</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">Rack</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Attack</span><span style="color:#f92672">.</span>blacklist(<span style="color:#e6db74">&#39;block spammers&#39;</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>req<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">[</span><span style="color:#e6db74">&#34;1.2.3.4&#34;</span>, <span style="color:#e6db74">&#34;5.6.7.9&#34;</span>, <span style="color:#e6db74">&#34;and-so-on&#34;</span><span style="color:#f92672">].</span>include?(req<span style="color:#f92672">.</span>ip)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><ul>
<li>you can&rsquo;t filter traffic based on IP, but spammers are hitting your sign up page pretty hard - let&rsquo;s blacklist too many sign up attempts</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">Rack</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Attack</span><span style="color:#f92672">.</span>blacklist(<span style="color:#e6db74">&#39;allow2ban signup attempts&#39;</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>req<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># After 5 requests in 6 hours, block all requests from that IP for 24 hours</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">Rack</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Attack</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Allow2Ban</span><span style="color:#f92672">.</span>filter(req<span style="color:#f92672">.</span>ip, <span style="color:#e6db74">maxretry</span>: <span style="color:#ae81ff">5</span>, <span style="color:#e6db74">findtime</span>: <span style="color:#ae81ff">6</span><span style="color:#f92672">.</span>hours, <span style="color:#e6db74">bantime</span>: <span style="color:#ae81ff">24</span><span style="color:#f92672">.</span>hours) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    req<span style="color:#f92672">.</span>post? <span style="color:#f92672">&amp;&amp;</span> req<span style="color:#f92672">.</span>path <span style="color:#f92672">=~</span> <span style="color:#e6db74">/sign_up/</span> <span style="color:#75715e"># POST request pointed to your sign_up url</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><ul>
<li>still not good? Try limiting requests per seconds - just be sure to set this accordingly to your app (traffic you&rsquo;re getting, amount of ajax requests etc.)</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># Throttle requests to 5 requests per second per ip</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Rack</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Attack</span><span style="color:#f92672">.</span>throttle(<span style="color:#e6db74">&#34;throttle req per ip&#34;</span>, <span style="color:#e6db74">limit</span>: <span style="color:#ae81ff">5</span>, <span style="color:#e6db74">period</span>: <span style="color:#ae81ff">1</span><span style="color:#f92672">.</span>second) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>req<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  req<span style="color:#f92672">.</span>ip
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Good luck and may the Force be with you ;-).</p>]]></content:encoded></item><item><title>Working remotely</title><link>https://awesomeprogrammer.com/blog/2014/05/04/working-remotely/</link><pubDate>Sun, 04 May 2014 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2014/05/04/working-remotely/</guid><description><![CDATA[<p>I must say time flies <strong>really</strong> fast. It&rsquo;s been a while since I started my journey with working remotely full time. After moving from Warsaw to Cracow I&rsquo;m working <em>in</em> Warsaw after all. I&rsquo;m really happy that remote working (in general) is getting more and more attention (I guess you already read <a href="http://www.amazon.com/Remote-Office-Required-Jason-Fried/dp/0804137501">Remote</a>?) and that companies start to see that huge office space isn&rsquo;t always what makes employees happy ;).</p>
<p>Obviously everything have it pros and cons, but you won&rsquo;t know if it works for you (no matter if you are a employee or employer) if you don&rsquo;t try, right? I know some companies are still requires strict on-site presence, I guess sometimes it comes down to very basic thing - trust and (false) feel of control. In the end <a href="https://netguru.co/blog/posts/is-remote-work-for-you-5-minutes-test">it&rsquo;s not for everyone</a>, but with the technology, tools and global Internet access we have nowadays it would be a shame not to give it a shot.</p>
]]></description><content:encoded><![CDATA[<p>I must say time flies <strong>really</strong> fast. It&rsquo;s been a while since I started my journey with working remotely full time. After moving from Warsaw to Cracow I&rsquo;m working <em>in</em> Warsaw after all. I&rsquo;m really happy that remote working (in general) is getting more and more attention (I guess you already read <a href="http://www.amazon.com/Remote-Office-Required-Jason-Fried/dp/0804137501">Remote</a>?) and that companies start to see that huge office space isn&rsquo;t always what makes employees happy ;).</p>
<p>Obviously everything have it pros and cons, but you won&rsquo;t know if it works for you (no matter if you are a employee or employer) if you don&rsquo;t try, right? I know some companies are still requires strict on-site presence, I guess sometimes it comes down to very basic thing - trust and (false) feel of control. In the end <a href="https://netguru.co/blog/posts/is-remote-work-for-you-5-minutes-test">it&rsquo;s not for everyone</a>, but with the technology, tools and global Internet access we have nowadays it would be a shame not to give it a shot.</p>
]]></content:encoded></item><item><title>My Gems of 2013</title><link>https://awesomeprogrammer.com/blog/2014/02/08/my-gems-of-2013/</link><pubDate>Sat, 08 Feb 2014 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2014/02/08/my-gems-of-2013/</guid><description><![CDATA[<p>I noticed that I &lsquo;starred&rsquo; over 320 gems on rubygems.org. So I decided to do some cleanups there and while I&rsquo;m at it - share with you few real <em>gems</em> that I like to use. So here it is - my tiny list that I called &ldquo;My Gems of 2013&rdquo;, mainly because it sounds catchy; a little bit of marketing didn&rsquo;t killed anybody yet, I think.</p>
<p>I tried to avoid some, well, really obvious choices (like rspec, mocha, sidekiq, devise, resque, carrierwave, etc. - you get the idea) so maybe you fill find that one precious, very special thing that you will love ;-). Here we go.</p>
<ul>
<li>
<p><a href="https://github.com/twg/active_link_to">active_link_to</a> - one simple method for your views that will handle all the logic.</p>
</li>
<li>
<p><a href="https://github.com/amatsuda/active_decorator">active_decorator</a> - object-oriented view helper for Rails 3 and Rails 4. VERY useful when working with complex views.</p>
</li>
<li>
<p><a href="https://rubygems.org/gems/annotate">annotate</a> - annotate your models and specs, display columns&amp;indexes right within your ruby class.</p>
</li>
<li>
<p><a href="https://github.com/norman/ambry">ambry</a> - database and ORM replacement for static models / small datasets. I like to use it from time to time.</p>
</li>
<li>
<p><a href="https://github.com/charliesome/better_errors">better_errors</a> - essential dev gem for your rails environment.</p>
</li>
<li>
<p><a href="https://github.com/presidentbeef/brakeman">brakeman</a> - scan you Rails app for potential vulnerabilities.</p>
</li>
<li>
<p><a href="https://github.com/rubysec/bundler-audit">bundler-audit</a> - patch-level verification for Bundler, make sure your app&rsquo;s dependencies are secure.</p>
</li>
<li>
<p><a href="https://github.com/brianmario/charlock_holmes">charlock_holmes</a> - ask Holmes which encoding is used.</p>
</li>
<li>
<p><a href="https://github.com/magnusvk/counter_culture">counter_culture</a> - make some complex counters with ease.</p>
</li>
<li>
<p><a href="https://github.com/mhfs/devise-async">devise-async</a> - universal plug&amp;play async backend for devise mailer.</p>
</li>
<li>
<p><a href="https://github.com/bmabey/email-spec/">email-spec</a> - just a great addition to rspec for testing emails.</p>
</li>
<li>
<p><a href="https://github.com/geemus/excon">excon</a> - my favorite HTTP library for Ruby. Fast &amp; reliable, simply love it.</p>
</li>
<li>
<p><a href="https://github.com/emmanueloga/ffaker">ffaker</a> - fake your test data, but faster.</p>
</li>
<li>
<p><a href="https://github.com/Shopify/identity_cache">identity_cache</a> - cache your Rails models with ease (but look out for gotchas, caching ain&rsquo;t easy)</p>
</li>
<li>
<p><a href="https://github.com/plentz/lol_dba">lol_dba</a> - can I haz db indexes? Scan &amp; see if you&rsquo;re missing some indexes maybe?</p>
</li>
<li>
<p><a href="https://github.com/tute/merit">merit</a> - swiss army knife when it comes to points, badges &amp; rankings. Gamify your whole app with just one gem.</p>
</li>
<li>
<p><a href="http://rubygems.org/gems/paranoia">paranoia</a> - <em>soft delete</em> solution for Rails. Tiny &amp; reliable.</p>
</li>
<li>
<p><a href="https://github.com/vmg/redcarpet">redcarpet</a> - one of the best Markdown parser I know.</p>
</li>
<li>
<p><a href="https://github.com/EppO/rolify">rolify</a> - roles solution for Rails. Plug &amp; Play.</p>
</li>
<li>
<p><a href="https://github.com/jonleighton/spring">spring</a> - shipped with Rails 4.1, basically no more need for guard, zeus or spork.</p>
</li>
<li>
<p><a href="https://github.com/andrew/split">split</a> - A/B testing? No problem.</p>
</li>
<li>
<p><a href="https://github.com/slim-template/slim">slim</a> - you probably know it. You may love or you may hate it. I chose &lt;3.</p>
</li>
<li>
<p><a href="https://github.com/pat/thinking-sphinx">thinking-sphinx</a> - integrate Sphinx right way.</p>
</li>
<li>
<p><a href="https://github.com/travisjeffery/timecop">timecop</a> - because testing time can be tricky, make it less tricky.</p>
</li>
<li>
<p><a href="https://github.com/websocket-rails/websocket-rails">websocket-rails</a> - use the power of HTML5 websockets! If you need it, of course.</p>
</li>
</ul>]]></description><content:encoded><![CDATA[<p>I noticed that I &lsquo;starred&rsquo; over 320 gems on rubygems.org. So I decided to do some cleanups there and while I&rsquo;m at it - share with you few real <em>gems</em> that I like to use. So here it is - my tiny list that I called &ldquo;My Gems of 2013&rdquo;, mainly because it sounds catchy; a little bit of marketing didn&rsquo;t killed anybody yet, I think.</p>
<p>I tried to avoid some, well, really obvious choices (like rspec, mocha, sidekiq, devise, resque, carrierwave, etc. - you get the idea) so maybe you fill find that one precious, very special thing that you will love ;-). Here we go.</p>
<ul>
<li>
<p><a href="https://github.com/twg/active_link_to">active_link_to</a> - one simple method for your views that will handle all the logic.</p>
</li>
<li>
<p><a href="https://github.com/amatsuda/active_decorator">active_decorator</a> - object-oriented view helper for Rails 3 and Rails 4. VERY useful when working with complex views.</p>
</li>
<li>
<p><a href="https://rubygems.org/gems/annotate">annotate</a> - annotate your models and specs, display columns&amp;indexes right within your ruby class.</p>
</li>
<li>
<p><a href="https://github.com/norman/ambry">ambry</a> - database and ORM replacement for static models / small datasets. I like to use it from time to time.</p>
</li>
<li>
<p><a href="https://github.com/charliesome/better_errors">better_errors</a> - essential dev gem for your rails environment.</p>
</li>
<li>
<p><a href="https://github.com/presidentbeef/brakeman">brakeman</a> - scan you Rails app for potential vulnerabilities.</p>
</li>
<li>
<p><a href="https://github.com/rubysec/bundler-audit">bundler-audit</a> - patch-level verification for Bundler, make sure your app&rsquo;s dependencies are secure.</p>
</li>
<li>
<p><a href="https://github.com/brianmario/charlock_holmes">charlock_holmes</a> - ask Holmes which encoding is used.</p>
</li>
<li>
<p><a href="https://github.com/magnusvk/counter_culture">counter_culture</a> - make some complex counters with ease.</p>
</li>
<li>
<p><a href="https://github.com/mhfs/devise-async">devise-async</a> - universal plug&amp;play async backend for devise mailer.</p>
</li>
<li>
<p><a href="https://github.com/bmabey/email-spec/">email-spec</a> - just a great addition to rspec for testing emails.</p>
</li>
<li>
<p><a href="https://github.com/geemus/excon">excon</a> - my favorite HTTP library for Ruby. Fast &amp; reliable, simply love it.</p>
</li>
<li>
<p><a href="https://github.com/emmanueloga/ffaker">ffaker</a> - fake your test data, but faster.</p>
</li>
<li>
<p><a href="https://github.com/Shopify/identity_cache">identity_cache</a> - cache your Rails models with ease (but look out for gotchas, caching ain&rsquo;t easy)</p>
</li>
<li>
<p><a href="https://github.com/plentz/lol_dba">lol_dba</a> - can I haz db indexes? Scan &amp; see if you&rsquo;re missing some indexes maybe?</p>
</li>
<li>
<p><a href="https://github.com/tute/merit">merit</a> - swiss army knife when it comes to points, badges &amp; rankings. Gamify your whole app with just one gem.</p>
</li>
<li>
<p><a href="http://rubygems.org/gems/paranoia">paranoia</a> - <em>soft delete</em> solution for Rails. Tiny &amp; reliable.</p>
</li>
<li>
<p><a href="https://github.com/vmg/redcarpet">redcarpet</a> - one of the best Markdown parser I know.</p>
</li>
<li>
<p><a href="https://github.com/EppO/rolify">rolify</a> - roles solution for Rails. Plug &amp; Play.</p>
</li>
<li>
<p><a href="https://github.com/jonleighton/spring">spring</a> - shipped with Rails 4.1, basically no more need for guard, zeus or spork.</p>
</li>
<li>
<p><a href="https://github.com/andrew/split">split</a> - A/B testing? No problem.</p>
</li>
<li>
<p><a href="https://github.com/slim-template/slim">slim</a> - you probably know it. You may love or you may hate it. I chose &lt;3.</p>
</li>
<li>
<p><a href="https://github.com/pat/thinking-sphinx">thinking-sphinx</a> - integrate Sphinx right way.</p>
</li>
<li>
<p><a href="https://github.com/travisjeffery/timecop">timecop</a> - because testing time can be tricky, make it less tricky.</p>
</li>
<li>
<p><a href="https://github.com/websocket-rails/websocket-rails">websocket-rails</a> - use the power of HTML5 websockets! If you need it, of course.</p>
</li>
</ul>]]></content:encoded></item><item><title>Yet another chruby user</title><link>https://awesomeprogrammer.com/blog/2014/01/04/yet-another-chruby-user/</link><pubDate>Sat, 04 Jan 2014 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2014/01/04/yet-another-chruby-user/</guid><description><![CDATA[<p>Few days ago I notice that RVM took over 8GB of my disk space for various gemsets/rubies. RVM in general is a beast (20k lines of shell script, you kidding me?!) and I used it since I started my journey with RoR, it was great (hey, you can <a href="https://www.bountysource.com/fundraisers/489-rvm-2-0">support rvm 2.0</a> if you want/can) but I decided it was time to look for an alternative. It was nice meeting you RVM, but it&rsquo;s time to move on. So to speak.</p>
<p>Didn&rsquo;t take much to find a real beauty. Thanks to <a href="https://github.com/postmodern">postmodern</a> I found just the perfect combination: <a href="https://github.com/postmodern/chruby">chruby</a> - to easily switch mri/rbx/jruby and <a href="https://github.com/postmodern/ruby-install">ruby-install</a> to install binaries with just one command. And all of this in like ~500 lines of code, total. Go to github, checkout docs and just try it out. You can integrate it with capistrano wit no problem, if you want to keep gemset-like functionality on your dev machine you can always bundle with <code>--path</code> option (<code>./vendor/bundle</code> will work just fine, similar to how <a href="http://bundler.io/v1.5/deploying.html">&ndash;deployment</a> option works).</p>
]]></description><content:encoded><![CDATA[<p>Few days ago I notice that RVM took over 8GB of my disk space for various gemsets/rubies. RVM in general is a beast (20k lines of shell script, you kidding me?!) and I used it since I started my journey with RoR, it was great (hey, you can <a href="https://www.bountysource.com/fundraisers/489-rvm-2-0">support rvm 2.0</a> if you want/can) but I decided it was time to look for an alternative. It was nice meeting you RVM, but it&rsquo;s time to move on. So to speak.</p>
<p>Didn&rsquo;t take much to find a real beauty. Thanks to <a href="https://github.com/postmodern">postmodern</a> I found just the perfect combination: <a href="https://github.com/postmodern/chruby">chruby</a> - to easily switch mri/rbx/jruby and <a href="https://github.com/postmodern/ruby-install">ruby-install</a> to install binaries with just one command. And all of this in like ~500 lines of code, total. Go to github, checkout docs and just try it out. You can integrate it with capistrano wit no problem, if you want to keep gemset-like functionality on your dev machine you can always bundle with <code>--path</code> option (<code>./vendor/bundle</code> will work just fine, similar to how <a href="http://bundler.io/v1.5/deploying.html">&ndash;deployment</a> option works).</p>
]]></content:encoded></item><item><title>REST API Client Server with Rails</title><link>https://awesomeprogrammer.com/blog/2013/12/10/rest-api-client-server-with-rails/</link><pubDate>Tue, 10 Dec 2013 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2013/12/10/rest-api-client-server-with-rails/</guid><description><![CDATA[<p>Few days ago a new project was thrown in my face ;). And it was interesting I must say - extracting some heavy internals of existing application into internal API + external client (app), that could be developed further with some additional features.</p>
<p>First I spend few hours do to some research an just to experiment a little, I found <a href="http://stackoverflow.com/questions/10941249/separate-rest-json-api-server-and-client">this topic</a> on stackoverflow (d&rsquo;oh) that was quite useful. Obviously first I though of some MV* Javascript framework at first, <a href="http://angularjs.org/">AngularJS</a> seemed like a great choice, but after messing with it for a while I concluded I won&rsquo;t be able to learn it in given time-frame I had (sorry Angular, maybe next time!). I decided to go more ruby (backend) way - my weapon of choice was: <a href="https://github.com/rails/activeresource/">Active Resource</a>.</p>
<p>ActiveResource was extracted into separate gem in Rails 4, and it basically allows you to route actions on you model to REST API. So you get almost all the benefits as using regular ActiveRecord model like associations, query methods etc. Sounds cool, right? So how to do it in few general steps:</p>
<ul>
<li>
<p>first you need to build your API, you may have some kind of existing API already in your app, you may even want to build even external API that will serve data on behalf of you app, there are so many different cases you wouldn&rsquo;t guess :). You can go with rails, with <a href="https://github.com/rails-api/rails-api">rails-api</a> or even something like [grape] (grape sound really cool if you want to build something light-weight from scratch).</p>
</li>
<li>
<p>you have to serialize your data at some point into pretty JSON - it depends on what you use, two popular choices are <a href="https://github.com/rails-api/active_model_serializers">active_model_serializers</a> and <a href="https://github.com/nesquena/rabl">rabl</a></p>
</li>
</ul>
<p>Now that you have basic API setup it&rsquo;s time to communicate with it. ActiveResource have very poor readme, but great RDoc (that&rsquo;s a shame, to be honest I prefer just having rock-solid readme file, but maybe that&rsquo;s just me :P). Few tips you might find useful:</p>
<h3 id="how-to-modify-activeresource-headers">How to modify ActiveResource headers?</h3>
<p>Let&rsquo;s say you want to authenticate by checking <code>HTTP_SECRET</code> header, so let&rsquo;s create a base class that all of your ActiveResource models will inherit from.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Base</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveResource</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  self<span style="color:#f92672">.</span>headers<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;SECRET&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;wow, so secret, wow&#39;</span> <span style="color:#75715e"># notice no HTTP_ here</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>See why this works - <a href="https://github.com/rails/activeresource/blob/master/lib/active_resource/base.rb#L606">source</a>.</p>
<h3 id="how-to-paginate-with-activeresource">How to paginate with ActiveResource?</h3>
<p>You can go with not so restful way by using <code>ActiveResource::Collection</code>. See <a href="https://gist.github.com/emq/7838240">this gist</a> I stolen <a href="http://yetimedia.tumblr.com/post/35233051627/activeresource-is-dead-long-live-activeresource">this blogpost</a> (check this out, it&rsquo;s very useful &lt;3). Or you can use <a href="https://github.com/Fivell/activeresource-response">activeresource-response</a> that have <a href="https://github.com/Fivell/activeresource-response#full-example-of-usage-with-kaminari-gem">full working example</a> how to implement REST pagination with kaminari. It&rsquo;s that simple. In case you are using will_paginate on your API side, this should help a little:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>  <span style="color:#75715e"># Kaminari</span>
</span></span><span style="display:flex;"><span>  response<span style="color:#f92672">.</span>headers<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;X-total&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> @orders<span style="color:#f92672">.</span>total_count<span style="color:#f92672">.</span>to_s
</span></span><span style="display:flex;"><span>  response<span style="color:#f92672">.</span>headers<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;X-offset&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> @orders<span style="color:#f92672">.</span>offset_value<span style="color:#f92672">.</span>to_s
</span></span><span style="display:flex;"><span>  response<span style="color:#f92672">.</span>headers<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;X-limit&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> @orders<span style="color:#f92672">.</span>limit_value<span style="color:#f92672">.</span>to_s
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Will_paginate</span>
</span></span><span style="display:flex;"><span>  response<span style="color:#f92672">.</span>headers<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;X-total&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> @orders<span style="color:#f92672">.</span>total_entries<span style="color:#f92672">.</span>to_s
</span></span><span style="display:flex;"><span>  response<span style="color:#f92672">.</span>headers<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;X-offset&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> @orders<span style="color:#f92672">.</span>offset<span style="color:#f92672">.</span>to_s
</span></span><span style="display:flex;"><span>  response<span style="color:#f92672">.</span>headers<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;X-limit&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> @orders<span style="color:#f92672">.</span>per_page<span style="color:#f92672">.</span>to_s
</span></span></code></pre></div><p>On the API side you might also use will_paginate, in that case you can just require <code>will_paginate/array</code> (in initializer for example) and paginate array directly by calling <code>.paginate(options)</code> with proper total/page/per_page values - <a href="https://github.com/mislav/will_paginate/blob/master/lib/will_paginate/array.rb">see source</a>.</p>
<h3 id="how-to-handle-remote-validation">How to handle remote validation?</h3>
<p>You probably don&rsquo;t want to duplicate validation logic in both API server and the client. So it makes sense to just return validation errors to ActiveResource in a way it can understand it. It should be enough to respond with status 422 (ActiveResource::ResourceInvalid) and pass validation errors:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># Inside API try to save/update_attributes and on failure return this:</span>
</span></span><span style="display:flex;"><span>render <span style="color:#e6db74">json</span>: { <span style="color:#e6db74">errors</span>: object<span style="color:#f92672">.</span>errors }, <span style="color:#e6db74">status</span>: <span style="color:#ae81ff">422</span>
</span></span></code></pre></div><h3 id="testing--mocks">Testing &amp; mocks</h3>
<p>ActiveResource provides HttpMock class, that allows you to register mock responses. Taken from docs:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># unfortunately can&#39;t register regex path at the moment (4.0.0)</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ActiveResource</span><span style="color:#f92672">::</span><span style="color:#66d9ef">HttpMock</span><span style="color:#f92672">.</span>respond_to <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>mock<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  mock<span style="color:#f92672">.</span>post <span style="color:#e6db74">&#34;/people.json&#34;</span>, {}, @matz, <span style="color:#ae81ff">201</span>, <span style="color:#e6db74">&#34;Location&#34;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;/people/1.json&#34;</span>
</span></span><span style="display:flex;"><span>  mock<span style="color:#f92672">.</span>get <span style="color:#e6db74">&#34;/people/1.json&#34;</span>, {}, @matz
</span></span><span style="display:flex;"><span>  mock<span style="color:#f92672">.</span>put <span style="color:#e6db74">&#34;/people/1.json&#34;</span>, {}, <span style="color:#66d9ef">nil</span>, <span style="color:#ae81ff">204</span>
</span></span><span style="display:flex;"><span>  mock<span style="color:#f92672">.</span>delete <span style="color:#e6db74">&#34;/people/1.json&#34;</span>, {}, <span style="color:#66d9ef">nil</span>, <span style="color:#ae81ff">200</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>You can clear defined request by calling <code>ActiveResource::HttpMock.reset!</code>. If you want to raise an error, you can trigger it by setting proper response code. Once again, taken from docs:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>  <span style="color:#75715e"># * 200..399 - Valid response. No exceptions, other than these redirects:</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 301, 302, 303, 307 - ActiveResource::Redirection</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 400 - ActiveResource::BadRequest</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 401 - ActiveResource::UnauthorizedAccess</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 403 - ActiveResource::ForbiddenAccess</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 404 - ActiveResource::ResourceNotFound</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 405 - ActiveResource::MethodNotAllowed</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 409 - ActiveResource::ResourceConflict</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 410 - ActiveResource::ResourceGone</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 422 - ActiveResource::ResourceInvalid (rescued by save as validation errors)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 401..499 - ActiveResource::ClientError</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 500..599 - ActiveResource::ServerError</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * Other - ActiveResource::ConnectionError</span>
</span></span></code></pre></div><p>Happy coding!</p>]]></description><content:encoded><![CDATA[<p>Few days ago a new project was thrown in my face ;). And it was interesting I must say - extracting some heavy internals of existing application into internal API + external client (app), that could be developed further with some additional features.</p>
<p>First I spend few hours do to some research an just to experiment a little, I found <a href="http://stackoverflow.com/questions/10941249/separate-rest-json-api-server-and-client">this topic</a> on stackoverflow (d&rsquo;oh) that was quite useful. Obviously first I though of some MV* Javascript framework at first, <a href="http://angularjs.org/">AngularJS</a> seemed like a great choice, but after messing with it for a while I concluded I won&rsquo;t be able to learn it in given time-frame I had (sorry Angular, maybe next time!). I decided to go more ruby (backend) way - my weapon of choice was: <a href="https://github.com/rails/activeresource/">Active Resource</a>.</p>
<p>ActiveResource was extracted into separate gem in Rails 4, and it basically allows you to route actions on you model to REST API. So you get almost all the benefits as using regular ActiveRecord model like associations, query methods etc. Sounds cool, right? So how to do it in few general steps:</p>
<ul>
<li>
<p>first you need to build your API, you may have some kind of existing API already in your app, you may even want to build even external API that will serve data on behalf of you app, there are so many different cases you wouldn&rsquo;t guess :). You can go with rails, with <a href="https://github.com/rails-api/rails-api">rails-api</a> or even something like [grape] (grape sound really cool if you want to build something light-weight from scratch).</p>
</li>
<li>
<p>you have to serialize your data at some point into pretty JSON - it depends on what you use, two popular choices are <a href="https://github.com/rails-api/active_model_serializers">active_model_serializers</a> and <a href="https://github.com/nesquena/rabl">rabl</a></p>
</li>
</ul>
<p>Now that you have basic API setup it&rsquo;s time to communicate with it. ActiveResource have very poor readme, but great RDoc (that&rsquo;s a shame, to be honest I prefer just having rock-solid readme file, but maybe that&rsquo;s just me :P). Few tips you might find useful:</p>
<h3 id="how-to-modify-activeresource-headers">How to modify ActiveResource headers?</h3>
<p>Let&rsquo;s say you want to authenticate by checking <code>HTTP_SECRET</code> header, so let&rsquo;s create a base class that all of your ActiveResource models will inherit from.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Base</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveResource</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  self<span style="color:#f92672">.</span>headers<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;SECRET&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;wow, so secret, wow&#39;</span> <span style="color:#75715e"># notice no HTTP_ here</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>See why this works - <a href="https://github.com/rails/activeresource/blob/master/lib/active_resource/base.rb#L606">source</a>.</p>
<h3 id="how-to-paginate-with-activeresource">How to paginate with ActiveResource?</h3>
<p>You can go with not so restful way by using <code>ActiveResource::Collection</code>. See <a href="https://gist.github.com/emq/7838240">this gist</a> I stolen <a href="http://yetimedia.tumblr.com/post/35233051627/activeresource-is-dead-long-live-activeresource">this blogpost</a> (check this out, it&rsquo;s very useful &lt;3). Or you can use <a href="https://github.com/Fivell/activeresource-response">activeresource-response</a> that have <a href="https://github.com/Fivell/activeresource-response#full-example-of-usage-with-kaminari-gem">full working example</a> how to implement REST pagination with kaminari. It&rsquo;s that simple. In case you are using will_paginate on your API side, this should help a little:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>  <span style="color:#75715e"># Kaminari</span>
</span></span><span style="display:flex;"><span>  response<span style="color:#f92672">.</span>headers<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;X-total&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> @orders<span style="color:#f92672">.</span>total_count<span style="color:#f92672">.</span>to_s
</span></span><span style="display:flex;"><span>  response<span style="color:#f92672">.</span>headers<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;X-offset&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> @orders<span style="color:#f92672">.</span>offset_value<span style="color:#f92672">.</span>to_s
</span></span><span style="display:flex;"><span>  response<span style="color:#f92672">.</span>headers<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;X-limit&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> @orders<span style="color:#f92672">.</span>limit_value<span style="color:#f92672">.</span>to_s
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Will_paginate</span>
</span></span><span style="display:flex;"><span>  response<span style="color:#f92672">.</span>headers<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;X-total&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> @orders<span style="color:#f92672">.</span>total_entries<span style="color:#f92672">.</span>to_s
</span></span><span style="display:flex;"><span>  response<span style="color:#f92672">.</span>headers<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;X-offset&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> @orders<span style="color:#f92672">.</span>offset<span style="color:#f92672">.</span>to_s
</span></span><span style="display:flex;"><span>  response<span style="color:#f92672">.</span>headers<span style="color:#f92672">[</span><span style="color:#e6db74">&#34;X-limit&#34;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> @orders<span style="color:#f92672">.</span>per_page<span style="color:#f92672">.</span>to_s
</span></span></code></pre></div><p>On the API side you might also use will_paginate, in that case you can just require <code>will_paginate/array</code> (in initializer for example) and paginate array directly by calling <code>.paginate(options)</code> with proper total/page/per_page values - <a href="https://github.com/mislav/will_paginate/blob/master/lib/will_paginate/array.rb">see source</a>.</p>
<h3 id="how-to-handle-remote-validation">How to handle remote validation?</h3>
<p>You probably don&rsquo;t want to duplicate validation logic in both API server and the client. So it makes sense to just return validation errors to ActiveResource in a way it can understand it. It should be enough to respond with status 422 (ActiveResource::ResourceInvalid) and pass validation errors:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># Inside API try to save/update_attributes and on failure return this:</span>
</span></span><span style="display:flex;"><span>render <span style="color:#e6db74">json</span>: { <span style="color:#e6db74">errors</span>: object<span style="color:#f92672">.</span>errors }, <span style="color:#e6db74">status</span>: <span style="color:#ae81ff">422</span>
</span></span></code></pre></div><h3 id="testing--mocks">Testing &amp; mocks</h3>
<p>ActiveResource provides HttpMock class, that allows you to register mock responses. Taken from docs:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># unfortunately can&#39;t register regex path at the moment (4.0.0)</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ActiveResource</span><span style="color:#f92672">::</span><span style="color:#66d9ef">HttpMock</span><span style="color:#f92672">.</span>respond_to <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>mock<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  mock<span style="color:#f92672">.</span>post <span style="color:#e6db74">&#34;/people.json&#34;</span>, {}, @matz, <span style="color:#ae81ff">201</span>, <span style="color:#e6db74">&#34;Location&#34;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;/people/1.json&#34;</span>
</span></span><span style="display:flex;"><span>  mock<span style="color:#f92672">.</span>get <span style="color:#e6db74">&#34;/people/1.json&#34;</span>, {}, @matz
</span></span><span style="display:flex;"><span>  mock<span style="color:#f92672">.</span>put <span style="color:#e6db74">&#34;/people/1.json&#34;</span>, {}, <span style="color:#66d9ef">nil</span>, <span style="color:#ae81ff">204</span>
</span></span><span style="display:flex;"><span>  mock<span style="color:#f92672">.</span>delete <span style="color:#e6db74">&#34;/people/1.json&#34;</span>, {}, <span style="color:#66d9ef">nil</span>, <span style="color:#ae81ff">200</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>You can clear defined request by calling <code>ActiveResource::HttpMock.reset!</code>. If you want to raise an error, you can trigger it by setting proper response code. Once again, taken from docs:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>  <span style="color:#75715e"># * 200..399 - Valid response. No exceptions, other than these redirects:</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 301, 302, 303, 307 - ActiveResource::Redirection</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 400 - ActiveResource::BadRequest</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 401 - ActiveResource::UnauthorizedAccess</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 403 - ActiveResource::ForbiddenAccess</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 404 - ActiveResource::ResourceNotFound</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 405 - ActiveResource::MethodNotAllowed</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 409 - ActiveResource::ResourceConflict</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 410 - ActiveResource::ResourceGone</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 422 - ActiveResource::ResourceInvalid (rescued by save as validation errors)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 401..499 - ActiveResource::ClientError</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * 500..599 - ActiveResource::ServerError</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># * Other - ActiveResource::ConnectionError</span>
</span></span></code></pre></div><p>Happy coding!</p>]]></content:encoded></item><item><title>Debricking Buffalo WZR-HP-G300NH (linux)</title><link>https://awesomeprogrammer.com/blog/2013/11/17/debricking-buffalo-wzr-hp-g300nh_linux/</link><pubDate>Sun, 17 Nov 2013 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2013/11/17/debricking-buffalo-wzr-hp-g300nh_linux/</guid><description><![CDATA[<p>If you&rsquo;re reading this you&rsquo;re that means you don&rsquo;t need further introduction to the problem, let go straight to solution then ;).</p>
<ul>
<li>
<p>You will need some kind of firmware, apparently you can use <a href="http://www.buffalotech.com/support-and-downloads/downloads">official Buffalo firmware</a>, official buffalo PRO (dd-wrt branded) firmware or just <a href="ftp://ftp.dd-wrt.com/others/eko/BrainSlayer-V24-preSP2/2013/">regular dd-wrt release</a>. I used the first one - in my case <em>wzrhpg300nh-176</em> (then you will be able to upgrade as usual, but for now let just get this one working again). Also you will need some kind of switch/another router, so you could connect both your PC and your bricked Buffalo to the same network.</p>
</li>
<li>
<p>Install the enhanced version of TFTP (<code>sudo apt-get install tftp-hpa</code>). Turn off you router. Setup static IP on your PC to of 192.168.11.2 / 255.255.255.0 Gateway: 192.168.11.1.</p>
</li>
<li>
<p>In terminal <code>cd</code> to the directory where you downloaded&amp;unpacked new firmware. Type <code>sudo arp -s 192.168.11.1 02-AA-BB-CC-DD-1A</code> (you can use real MAC address of your router, but fake one should work just fine).</p>
</li>
<li>
<p>Now it&rsquo;s time for TFTP:</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>tftp 192.168.11.1
</span></span><span style="display:flex;"><span>tftp&gt; verbose
</span></span><span style="display:flex;"><span>tftp&gt; binary
</span></span><span style="display:flex;"><span>tftp&gt; trace
</span></span><span style="display:flex;"><span>tftp&gt; rexmt <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>tftp&gt; timeout <span style="color:#ae81ff">60</span>
</span></span><span style="display:flex;"><span>tftp&gt; put wzrhpg300nh-176 <span style="color:#75715e"># this is the filename of downloaded firmware</span>
</span></span></code></pre></div><ul>
<li>
<p>Power up your router and wait for tftp to do it&rsquo;s magic. After a while you should start seeing some progress and shortly after it should be transferred to the router.</p>
</li>
<li>
<p>Give it about 6 minutes to boot up. With a little bit of luck your router should be alive again.</p>
</li>
</ul>
<p>More references: <a href="http://www.dd-wrt.com/wiki/index.php/TFTP_flash">1</a>, <a href="http://dd-wrt.com/wiki/index.php/Buffalo_WZR-HP-G300NH#Debricking_Info_using_TFTP">2</a>. Good luck!</p>]]></description><content:encoded><![CDATA[<p>If you&rsquo;re reading this you&rsquo;re that means you don&rsquo;t need further introduction to the problem, let go straight to solution then ;).</p>
<ul>
<li>
<p>You will need some kind of firmware, apparently you can use <a href="http://www.buffalotech.com/support-and-downloads/downloads">official Buffalo firmware</a>, official buffalo PRO (dd-wrt branded) firmware or just <a href="ftp://ftp.dd-wrt.com/others/eko/BrainSlayer-V24-preSP2/2013/">regular dd-wrt release</a>. I used the first one - in my case <em>wzrhpg300nh-176</em> (then you will be able to upgrade as usual, but for now let just get this one working again). Also you will need some kind of switch/another router, so you could connect both your PC and your bricked Buffalo to the same network.</p>
</li>
<li>
<p>Install the enhanced version of TFTP (<code>sudo apt-get install tftp-hpa</code>). Turn off you router. Setup static IP on your PC to of 192.168.11.2 / 255.255.255.0 Gateway: 192.168.11.1.</p>
</li>
<li>
<p>In terminal <code>cd</code> to the directory where you downloaded&amp;unpacked new firmware. Type <code>sudo arp -s 192.168.11.1 02-AA-BB-CC-DD-1A</code> (you can use real MAC address of your router, but fake one should work just fine).</p>
</li>
<li>
<p>Now it&rsquo;s time for TFTP:</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>tftp 192.168.11.1
</span></span><span style="display:flex;"><span>tftp&gt; verbose
</span></span><span style="display:flex;"><span>tftp&gt; binary
</span></span><span style="display:flex;"><span>tftp&gt; trace
</span></span><span style="display:flex;"><span>tftp&gt; rexmt <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>tftp&gt; timeout <span style="color:#ae81ff">60</span>
</span></span><span style="display:flex;"><span>tftp&gt; put wzrhpg300nh-176 <span style="color:#75715e"># this is the filename of downloaded firmware</span>
</span></span></code></pre></div><ul>
<li>
<p>Power up your router and wait for tftp to do it&rsquo;s magic. After a while you should start seeing some progress and shortly after it should be transferred to the router.</p>
</li>
<li>
<p>Give it about 6 minutes to boot up. With a little bit of luck your router should be alive again.</p>
</li>
</ul>
<p>More references: <a href="http://www.dd-wrt.com/wiki/index.php/TFTP_flash">1</a>, <a href="http://dd-wrt.com/wiki/index.php/Buffalo_WZR-HP-G300NH#Debricking_Info_using_TFTP">2</a>. Good luck!</p>]]></content:encoded></item><item><title>Migrating from resque to sidekiq</title><link>https://awesomeprogrammer.com/blog/2013/10/03/migrating-from-resque-to-sidekiq/</link><pubDate>Thu, 03 Oct 2013 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2013/10/03/migrating-from-resque-to-sidekiq/</guid><description><![CDATA[<p>In a company I&rsquo;m currently working we have been using <a href="https://github.com/resque/resque">resque</a> to do some heavy asynchronous work. And well, that&rsquo;s like obvious choice in Rails world - just go with resque, fork some processes and relax ;). But when you get to a point, when you need few hundred workers and few servers, you start to ask yourself - can&rsquo;t you do it better?</p>
<p>You probably can. There are some pretty smart folks out there, who were so kindly to share their knowledge. And that&rsquo;s how you can find <a href="https://github.com/mperham/sidekiq">sidekiq</a> build on top of <a href="https://github.com/celluloid/celluloid/">celluloid</a>.</p>
<p>Sidekiq uses multi-threading, so you can leverage that even if you use MRI (that have un-famous <a href="http://en.wikipedia.org/wiki/Global_Interpreter_Lock">GIL</a>) - if you have a lot of I/O bound work you can still benefit from this great piece of software (even without need to migrate to <a href="https://github.com/jruby/jruby">JRuby</a> or <a href="https://github.com/rubinius/rubinius">Rubinius</a>).</p>
<p>I ran some simple benchmarks and said to myself - hell yeah, let&rsquo;s just do it! About 70 commits later I completely integrated sidekiq, updated test suite and created some custom sidekiq middleware that I needed (man it&rsquo;s great, just <a href="https://github.com/mperham/sidekiq/wiki/Middleware">take a look at the docs</a>). Within 24 hours it processed over 1000000 jobs using 20x less amount of workers. How cool is that? I would say - pretty cool.</p>
<p>Few general tips and notes to all of you and to future-myself ;-)</p>
<ul>
<li>
<p>if you&rsquo;re coming from resque - <a href="https://github.com/mhfs/sidekiq-failures">sidekiq-failures</a> is like the second thing you should checkout, this will make you feel like home</p>
</li>
<li>
<p>when using mongoid - be sure to install <a href="https://rubygems.org/gems/kiqstand">kiqstand</a> to properly disconnect workers from db</p>
</li>
<li>
<p>remember about thread-safety, that also includes gems you are using, be aware of what you are putting in your Gemfile</p>
</li>
<li>
<p>if you need just write your of middleware, it&rsquo;s easy to test and easy to extract into separate gem and reuse in your other projects</p>
</li>
<li>
<p>I wouldn&rsquo;t recommend using resque with sidekiq within the same redis namespace. Theoretically you can, but I just wouldn&rsquo;t go for it, it&rsquo;s too messy. Yeah, you will need to web backends to monitor your jobs (if you are using resque+sidekiq combination somehow), but personally I think it&rsquo;s even better, especially if you are doing a lot of asynch work - it&rsquo;s easier to see what&rsquo;s going on.</p>
</li>
</ul>]]></description><content:encoded><![CDATA[<p>In a company I&rsquo;m currently working we have been using <a href="https://github.com/resque/resque">resque</a> to do some heavy asynchronous work. And well, that&rsquo;s like obvious choice in Rails world - just go with resque, fork some processes and relax ;). But when you get to a point, when you need few hundred workers and few servers, you start to ask yourself - can&rsquo;t you do it better?</p>
<p>You probably can. There are some pretty smart folks out there, who were so kindly to share their knowledge. And that&rsquo;s how you can find <a href="https://github.com/mperham/sidekiq">sidekiq</a> build on top of <a href="https://github.com/celluloid/celluloid/">celluloid</a>.</p>
<p>Sidekiq uses multi-threading, so you can leverage that even if you use MRI (that have un-famous <a href="http://en.wikipedia.org/wiki/Global_Interpreter_Lock">GIL</a>) - if you have a lot of I/O bound work you can still benefit from this great piece of software (even without need to migrate to <a href="https://github.com/jruby/jruby">JRuby</a> or <a href="https://github.com/rubinius/rubinius">Rubinius</a>).</p>
<p>I ran some simple benchmarks and said to myself - hell yeah, let&rsquo;s just do it! About 70 commits later I completely integrated sidekiq, updated test suite and created some custom sidekiq middleware that I needed (man it&rsquo;s great, just <a href="https://github.com/mperham/sidekiq/wiki/Middleware">take a look at the docs</a>). Within 24 hours it processed over 1000000 jobs using 20x less amount of workers. How cool is that? I would say - pretty cool.</p>
<p>Few general tips and notes to all of you and to future-myself ;-)</p>
<ul>
<li>
<p>if you&rsquo;re coming from resque - <a href="https://github.com/mhfs/sidekiq-failures">sidekiq-failures</a> is like the second thing you should checkout, this will make you feel like home</p>
</li>
<li>
<p>when using mongoid - be sure to install <a href="https://rubygems.org/gems/kiqstand">kiqstand</a> to properly disconnect workers from db</p>
</li>
<li>
<p>remember about thread-safety, that also includes gems you are using, be aware of what you are putting in your Gemfile</p>
</li>
<li>
<p>if you need just write your of middleware, it&rsquo;s easy to test and easy to extract into separate gem and reuse in your other projects</p>
</li>
<li>
<p>I wouldn&rsquo;t recommend using resque with sidekiq within the same redis namespace. Theoretically you can, but I just wouldn&rsquo;t go for it, it&rsquo;s too messy. Yeah, you will need to web backends to monitor your jobs (if you are using resque+sidekiq combination somehow), but personally I think it&rsquo;s even better, especially if you are doing a lot of asynch work - it&rsquo;s easier to see what&rsquo;s going on.</p>
</li>
</ul>]]></content:encoded></item><item><title>API-like gem for fetching data from quakelive.com</title><link>https://awesomeprogrammer.com/blog/2013/09/25/api-like-gem-for-fetching-data-from-quakelive-dot-com/</link><pubDate>Wed, 25 Sep 2013 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2013/09/25/api-like-gem-for-fetching-data-from-quakelive-dot-com/</guid><description><![CDATA[<p>It has been a busy time for me lately, but I finally managed to release a small gem, that some of you might find useful.</p>
<p>You can <a href="https://github.com/emq/quakelive_api">download it from github</a> or <a href="https://rubygems.org/gems/quakelive_api">rubygems</a>, I attached readme, that should be sufficient enough to start hacking. As you may already know ID Software unfortunately didn&rsquo;t provided official API, so if you want to get some data from them, your only option is to crawl their site. And that&rsquo;s what this gem basically is - it&rsquo;s a small crawler with parser, packed nicely into nice ready-to-use gem.</p>
<p>I currently use it in production on <a href="http://quake.net.pl">quake.net.pl</a>, and so far it worked really nicely :-).</p>
]]></description><content:encoded><![CDATA[<p>It has been a busy time for me lately, but I finally managed to release a small gem, that some of you might find useful.</p>
<p>You can <a href="https://github.com/emq/quakelive_api">download it from github</a> or <a href="https://rubygems.org/gems/quakelive_api">rubygems</a>, I attached readme, that should be sufficient enough to start hacking. As you may already know ID Software unfortunately didn&rsquo;t provided official API, so if you want to get some data from them, your only option is to crawl their site. And that&rsquo;s what this gem basically is - it&rsquo;s a small crawler with parser, packed nicely into nice ready-to-use gem.</p>
<p>I currently use it in production on <a href="http://quake.net.pl">quake.net.pl</a>, and so far it worked really nicely :-).</p>
]]></content:encoded></item><item><title>Rails after_commit create mindf*ck</title><link>https://awesomeprogrammer.com/blog/2013/09/25/rails-after-commit-create-mindf-star-ck/</link><pubDate>Wed, 25 Sep 2013 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2013/09/25/rails-after-commit-create-mindf-star-ck/</guid><description><![CDATA[<p>Rails 3 observers can be tricky. You probably already heard a thousand times how to deal with <em>undefined</em> id issue and how transactions are handled, so let&rsquo;s take a quick example that can be google&rsquo;d in just a couple of seconds.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">UserObserver</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Observer</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">after_commit</span>(user)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">unless</span> user<span style="color:#f92672">.</span>send(<span style="color:#e6db74">:transaction_include_action?</span>, <span style="color:#e6db74">:create</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># some code that should be executed only on create, after transaction is committed</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>So what&rsquo;s wrong with this example? Nothing. But let say <code>SuperUser</code> <em>has_many</em> <code>users</code>. Some example code:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SuperUser</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  has_many <span style="color:#e6db74">:users</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">User</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  attr_accessible <span style="color:#e6db74">:name</span>
</span></span><span style="display:flex;"><span>  validates <span style="color:#e6db74">:name</span>, <span style="color:#e6db74">presence</span>: <span style="color:#66d9ef">true</span>, <span style="color:#e6db74">inclusion</span>: { <span style="color:#66d9ef">in</span>: <span style="color:#e6db74">%(foo bar)</span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">UserObserver</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Observer</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">after_commit</span>(user)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">unless</span> user<span style="color:#f92672">.</span>send(<span style="color:#e6db74">:transaction_include_action?</span>, <span style="color:#e6db74">:create</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">raise</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span>user<span style="color:#f92672">.</span>inspect<span style="color:#e6db74">}</span><span style="color:#e6db74"> | Persisted: </span><span style="color:#e6db74">#{</span>user<span style="color:#f92672">.</span>persisted?<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>And try it out:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># obviously we need super user first</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SuperUser</span><span style="color:#f92672">.</span>first<span style="color:#f92672">.</span>users<span style="color:#f92672">.</span>create(name: <span style="color:#e6db74">&#34;boo&#34;</span>) <span style="color:#75715e"># this won&#39;t create record, due validation</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#&lt;RuntimeError: #&lt;User id: nil, name: &#34;boo&#34;, owner_id: 1, created_at: nil, updated_at: nil&gt; | Persisted: false&gt;</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">=&gt;</span> <span style="color:#75715e">#&lt;User id: nil, name: &#34;boo&#34;, owner_id: 1, created_at: nil, updated_at: nil&gt;</span>
</span></span></code></pre></div><p>Hell yeah, observer code executed perfectly, even this is not what we would normally expect. I was quite surprised with this behavior, I recommend checking out <a href="https://github.com/rails/rails/blob/3-2-stable/activerecord/lib/active_record/transactions.rb">ActiveRecord::Transactions</a> internals, if I find some time I will write a follow-up to this, cheers.</p>]]></description><content:encoded><![CDATA[<p>Rails 3 observers can be tricky. You probably already heard a thousand times how to deal with <em>undefined</em> id issue and how transactions are handled, so let&rsquo;s take a quick example that can be google&rsquo;d in just a couple of seconds.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">UserObserver</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Observer</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">after_commit</span>(user)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">unless</span> user<span style="color:#f92672">.</span>send(<span style="color:#e6db74">:transaction_include_action?</span>, <span style="color:#e6db74">:create</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># some code that should be executed only on create, after transaction is committed</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>So what&rsquo;s wrong with this example? Nothing. But let say <code>SuperUser</code> <em>has_many</em> <code>users</code>. Some example code:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SuperUser</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  has_many <span style="color:#e6db74">:users</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">User</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  attr_accessible <span style="color:#e6db74">:name</span>
</span></span><span style="display:flex;"><span>  validates <span style="color:#e6db74">:name</span>, <span style="color:#e6db74">presence</span>: <span style="color:#66d9ef">true</span>, <span style="color:#e6db74">inclusion</span>: { <span style="color:#66d9ef">in</span>: <span style="color:#e6db74">%(foo bar)</span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">UserObserver</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Observer</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">after_commit</span>(user)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">unless</span> user<span style="color:#f92672">.</span>send(<span style="color:#e6db74">:transaction_include_action?</span>, <span style="color:#e6db74">:create</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">raise</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span>user<span style="color:#f92672">.</span>inspect<span style="color:#e6db74">}</span><span style="color:#e6db74"> | Persisted: </span><span style="color:#e6db74">#{</span>user<span style="color:#f92672">.</span>persisted?<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>And try it out:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># obviously we need super user first</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SuperUser</span><span style="color:#f92672">.</span>first<span style="color:#f92672">.</span>users<span style="color:#f92672">.</span>create(name: <span style="color:#e6db74">&#34;boo&#34;</span>) <span style="color:#75715e"># this won&#39;t create record, due validation</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#&lt;RuntimeError: #&lt;User id: nil, name: &#34;boo&#34;, owner_id: 1, created_at: nil, updated_at: nil&gt; | Persisted: false&gt;</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">=&gt;</span> <span style="color:#75715e">#&lt;User id: nil, name: &#34;boo&#34;, owner_id: 1, created_at: nil, updated_at: nil&gt;</span>
</span></span></code></pre></div><p>Hell yeah, observer code executed perfectly, even this is not what we would normally expect. I was quite surprised with this behavior, I recommend checking out <a href="https://github.com/rails/rails/blob/3-2-stable/activerecord/lib/active_record/transactions.rb">ActiveRecord::Transactions</a> internals, if I find some time I will write a follow-up to this, cheers.</p>]]></content:encoded></item><item><title>Windows-1250 to utf-8 bash one-liner</title><link>https://awesomeprogrammer.com/blog/2013/07/25/windows-1250-to-utf-8-bash-one-liner/</link><pubDate>Thu, 25 Jul 2013 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2013/07/25/windows-1250-to-utf-8-bash-one-liner/</guid><description><![CDATA[<p>There was a time when I worked with non utf-8 coded files quite a lot. They were usually saved using Windows-1250 encoding and obviously I wanted them in utf-8. I think I don&rsquo;t need to tell you that you should probably use <code>iconv</code> for that, but there is also quite interesting tool called <code>sponge</code> that will allow you to pipe output of iconv to the very same file. In ubuntu it comes with packed called <code>moreutils</code> (also check out other apps it provides!). Install it with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt-get install moreutils
</span></span></code></pre></div><p>Then in your <code>.bashrc</code> you can define something like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> windows2utf<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  iconv -f Windows-1250 -t utf-8 $1 | sponge $1
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>Reload the file with <code>source .bashrc</code> and try out your new, handy function :).</p>
]]></description><content:encoded><![CDATA[<p>There was a time when I worked with non utf-8 coded files quite a lot. They were usually saved using Windows-1250 encoding and obviously I wanted them in utf-8. I think I don&rsquo;t need to tell you that you should probably use <code>iconv</code> for that, but there is also quite interesting tool called <code>sponge</code> that will allow you to pipe output of iconv to the very same file. In ubuntu it comes with packed called <code>moreutils</code> (also check out other apps it provides!). Install it with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt-get install moreutils
</span></span></code></pre></div><p>Then in your <code>.bashrc</code> you can define something like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> windows2utf<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  iconv -f Windows-1250 -t utf-8 $1 | sponge $1
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>Reload the file with <code>source .bashrc</code> and try out your new, handy function :).</p>
]]></content:encoded></item><item><title>Delayed::Job to Resque migration</title><link>https://awesomeprogrammer.com/blog/2013/06/30/delayed-job-to-resque-migration/</link><pubDate>Sun, 30 Jun 2013 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2013/06/30/delayed-job-to-resque-migration/</guid><description><![CDATA[<p>When it comes to doing some asynchronous work, Ruby on Rails community have some great ready to use solutions to choose from. I think most popular is <a href="https://github.com/collectiveidea/delayed_job">delayed_job</a> and <a href="https://github.com/resque/resque">resque</a> (and probably sidekiq, but a story for another blog post).</p>
<p>Delayed Job is great for small apps, it&rsquo;s work out-of-the-box with ActiveRecord, you can also plug it into mongoid and it&rsquo;s probably your first choice if you don&rsquo;t want to setup redis. But when your traffic gets higher and higher, and you have to do more background jobs it&rsquo;s becoming clear it&rsquo;s not the perfect solution - the database-lock mechanism just don&rsquo;t play very well with high amount of tasks.</p>
<p>Migrating to resque shouldn&rsquo;t be a big problem (well, I guess it&rsquo;s vary greatly, depending of you app specifics), I will provide here some quick tips, as usual:</p>
<!-- raw HTML omitted -->
<ul>
<li>
<p>configuring <a href="http://redis.io/">redis</a> isn&rsquo;t so complicated as it seems, <a href="https://raw.github.com/antirez/redis/2.6/redis.conf">provided example</a> (I recommend sticking to current branch 2.6) is very well documented and should be self-explanatory</p>
</li>
<li>
<p>keep in mind that job arguments are JSON encoded, so all hashes will became strings</p>
</li>
<li>
<p>converting jobs structure and enqueue calls should also be straight-forward, if you have some heavily customized jobs take a look at <a href="https://github.com/resque/resque/blob/master/docs/HOOKS.md">hooks that resque has to offer</a> - they are quite similar to the ones from delayed_job, so you shouldn&rsquo;t have much of a problem converting those</p>
</li>
<li>
<p>you can replace <a href="https://github.com/ejschmitt/delayed_job_web">delayed_job_web</a> with built-in resque back-end. Just add to your routes:</p>
</li>
</ul>
<p>{% codeblock config/routes.rb %}
mount Resque::Server, :at =&gt; &ldquo;/resque&rdquo;
{% endcodeblock %}</p>
<p>You will probably want to protect it with some kind of password using initializer:</p>
<p>{% codeblock config/initializers/resque.rb %}
unless Rails.env.development?
Resque::Server.use Rack::Auth::Basic do |user, password|
user == &rsquo;login&rsquo; &amp;&amp; password == &lsquo;password&rsquo;
end
end
{% endcodeblock %}</p>
<ul>
<li>
<p>if you need some simple capistrano integration - take a look at <a href="https://github.com/sshingler/capistrano-resque">capistrano-resque</a></p>
</li>
<li>
<p>I also recommend checking out <a href="https://github.com/bvandenbos/resque-scheduler">resque-scheduler</a>, especially if you have a lot of tasks scheduled in you crontab - moving them to your app repository should make it a little bit more clear what&rsquo;s really going on</p>
</li>
</ul>
<p>And one final note - resque 2.0 is currently heavy development, you you might want to stick to <a href="https://github.com/resque/resque/tree/1-x-stable">1-x-stable</a> branch.</p>]]></description><content:encoded><![CDATA[<p>When it comes to doing some asynchronous work, Ruby on Rails community have some great ready to use solutions to choose from. I think most popular is <a href="https://github.com/collectiveidea/delayed_job">delayed_job</a> and <a href="https://github.com/resque/resque">resque</a> (and probably sidekiq, but a story for another blog post).</p>
<p>Delayed Job is great for small apps, it&rsquo;s work out-of-the-box with ActiveRecord, you can also plug it into mongoid and it&rsquo;s probably your first choice if you don&rsquo;t want to setup redis. But when your traffic gets higher and higher, and you have to do more background jobs it&rsquo;s becoming clear it&rsquo;s not the perfect solution - the database-lock mechanism just don&rsquo;t play very well with high amount of tasks.</p>
<p>Migrating to resque shouldn&rsquo;t be a big problem (well, I guess it&rsquo;s vary greatly, depending of you app specifics), I will provide here some quick tips, as usual:</p>
<!-- raw HTML omitted -->
<ul>
<li>
<p>configuring <a href="http://redis.io/">redis</a> isn&rsquo;t so complicated as it seems, <a href="https://raw.github.com/antirez/redis/2.6/redis.conf">provided example</a> (I recommend sticking to current branch 2.6) is very well documented and should be self-explanatory</p>
</li>
<li>
<p>keep in mind that job arguments are JSON encoded, so all hashes will became strings</p>
</li>
<li>
<p>converting jobs structure and enqueue calls should also be straight-forward, if you have some heavily customized jobs take a look at <a href="https://github.com/resque/resque/blob/master/docs/HOOKS.md">hooks that resque has to offer</a> - they are quite similar to the ones from delayed_job, so you shouldn&rsquo;t have much of a problem converting those</p>
</li>
<li>
<p>you can replace <a href="https://github.com/ejschmitt/delayed_job_web">delayed_job_web</a> with built-in resque back-end. Just add to your routes:</p>
</li>
</ul>
<p>{% codeblock config/routes.rb %}
mount Resque::Server, :at =&gt; &ldquo;/resque&rdquo;
{% endcodeblock %}</p>
<p>You will probably want to protect it with some kind of password using initializer:</p>
<p>{% codeblock config/initializers/resque.rb %}
unless Rails.env.development?
Resque::Server.use Rack::Auth::Basic do |user, password|
user == &rsquo;login&rsquo; &amp;&amp; password == &lsquo;password&rsquo;
end
end
{% endcodeblock %}</p>
<ul>
<li>
<p>if you need some simple capistrano integration - take a look at <a href="https://github.com/sshingler/capistrano-resque">capistrano-resque</a></p>
</li>
<li>
<p>I also recommend checking out <a href="https://github.com/bvandenbos/resque-scheduler">resque-scheduler</a>, especially if you have a lot of tasks scheduled in you crontab - moving them to your app repository should make it a little bit more clear what&rsquo;s really going on</p>
</li>
</ul>
<p>And one final note - resque 2.0 is currently heavy development, you you might want to stick to <a href="https://github.com/resque/resque/tree/1-x-stable">1-x-stable</a> branch.</p>]]></content:encoded></item><item><title>Fixing Bluetooth audio issue on Ubuntu 13.04 based distors</title><link>https://awesomeprogrammer.com/blog/2013/06/22/fixing-bluetooth-audio-issue-on-ubuntu-13-dot-04-based-distors/</link><pubDate>Sat, 22 Jun 2013 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2013/06/22/fixing-bluetooth-audio-issue-on-ubuntu-13-dot-04-based-distors/</guid><description><![CDATA[<p>I&rsquo;ve noticed that Ubuntu 13.04 (and all distors based on it, eg. Linux Mint 15) have some weird audio issue that basically makes impossible to connect to some audio speakers through audio sink (I had problems with mine Creative T12). After googling around and trying all possible solutions I still couldn&rsquo;t get it working.</p>
<p>Syslog showed various errors, for example: <code>Unable to select SEP</code> or <code>Endpoint replied with an error: org.freedesktop.DBus.Error.UnknownMethod</code></p>
<p>Messing with <code>/etc/bluetooth/audio.conf</code> didn&rsquo;t helped. After using option <code>Enable=Socket</code> I could connect to the speakers but I couldn&rsquo;t select proper audio profile. I tried upgrading to different, most recent versions - still without results.</p>
<p>So in the end I decided to downgrade the damn <code>bluez</code> along with <code>pulseaudio</code>.</p>
<p>I recommend getting .deb packages - get it <a href="http://archive.ubuntu.com/ubuntu/pool/main/b/bluez/">from here for bluez</a> (I used version 4.96) and <a href="http://archive.ubuntu.com/ubuntu/pool/main/p/pulseaudio/">from here for pulseaudio</a> (I used version 2.1-0) - and installing them with <code>dpkg</code>. After downgrade process everything worked like a charm. I have no idea how they could broke something like that is such a major release - hopefully they will fix it in the next one ;-).</p>
<p><strong>UPDATE 23.11.2013</strong></p>
<p>I see that this post got some attention, I guess I&rsquo;m kind-of glad that not only I had the same problem and that I could help at least some of you folks :). <strong>krisek</strong> in comments section mentioned that it would be wise to lock version using <code>apt-mark</code> command (so packaged won&rsquo;t get automatically upgraded or removed). It&rsquo;s a very good point - and it can be done with command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt-mark hold bluez bluez-alsa bluez-gstreamer libbluetooth3 libpulse-mainloop-glib0 libpulse0 libpulse0:i386 libpulsedsp pulseaudio pulseaudio-module-bluetooth pulseaudio-module-x11 pulseaudio-utils
</span></span></code></pre></div>]]></description><content:encoded><![CDATA[<p>I&rsquo;ve noticed that Ubuntu 13.04 (and all distors based on it, eg. Linux Mint 15) have some weird audio issue that basically makes impossible to connect to some audio speakers through audio sink (I had problems with mine Creative T12). After googling around and trying all possible solutions I still couldn&rsquo;t get it working.</p>
<p>Syslog showed various errors, for example: <code>Unable to select SEP</code> or <code>Endpoint replied with an error: org.freedesktop.DBus.Error.UnknownMethod</code></p>
<p>Messing with <code>/etc/bluetooth/audio.conf</code> didn&rsquo;t helped. After using option <code>Enable=Socket</code> I could connect to the speakers but I couldn&rsquo;t select proper audio profile. I tried upgrading to different, most recent versions - still without results.</p>
<p>So in the end I decided to downgrade the damn <code>bluez</code> along with <code>pulseaudio</code>.</p>
<p>I recommend getting .deb packages - get it <a href="http://archive.ubuntu.com/ubuntu/pool/main/b/bluez/">from here for bluez</a> (I used version 4.96) and <a href="http://archive.ubuntu.com/ubuntu/pool/main/p/pulseaudio/">from here for pulseaudio</a> (I used version 2.1-0) - and installing them with <code>dpkg</code>. After downgrade process everything worked like a charm. I have no idea how they could broke something like that is such a major release - hopefully they will fix it in the next one ;-).</p>
<p><strong>UPDATE 23.11.2013</strong></p>
<p>I see that this post got some attention, I guess I&rsquo;m kind-of glad that not only I had the same problem and that I could help at least some of you folks :). <strong>krisek</strong> in comments section mentioned that it would be wise to lock version using <code>apt-mark</code> command (so packaged won&rsquo;t get automatically upgraded or removed). It&rsquo;s a very good point - and it can be done with command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt-mark hold bluez bluez-alsa bluez-gstreamer libbluetooth3 libpulse-mainloop-glib0 libpulse0 libpulse0:i386 libpulsedsp pulseaudio pulseaudio-module-bluetooth pulseaudio-module-x11 pulseaudio-utils
</span></span></code></pre></div>]]></content:encoded></item><item><title>How to migrate from Mina to Capistrano</title><link>https://awesomeprogrammer.com/blog/2013/05/19/how-to-migrate-from-mina-to-capistrano/</link><pubDate>Sun, 19 May 2013 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2013/05/19/how-to-migrate-from-mina-to-capistrano/</guid><description><![CDATA[<p>It&rsquo;s been a while since I last posted here. I&rsquo;ve been pretty busy with work, I&rsquo;ve also moved to Cracov in the meantime - so yeah, I can&rsquo;t really complaint about boredom ;).</p>
<p>Anyhow, some time ago I posted <a href="/blog/2012/10/27/super-fast-deployment-with-mina-capistrano-alternative/">about Mina</a> - neat little gem that allows super fast deployments. Unfortunately when your project grows you start adding additional bash scripts, you add more and more commands to your deploy script and it grows into something not-so-neat anymore.</p>
<p>That&rsquo;s why tempted with Capistrano&rsquo;s ready-to-use recipes I decided it&rsquo;s time to say hello to Capistrano. I&rsquo;ll provide here step-by-step migration process from Mina. I will be using here <a href="https://github.com/collectiveidea/delayed_job">delayed_job</a>, <a href="http://unicorn.bogomips.org/">unicorn</a> (with <a href="https://github.com/sosedoff/capistrano-unicorn">capistrano-unicorn</a> gem) and <a href="https://github.com/pat/thinking-sphinx">thinking-sphinx</a>.</p>
<p>First, install <a href="https://github.com/capistrano/capistrano">Capistrano</a> and capify your project with command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>capify .
</span></span></code></pre></div><p>(I assume you need only one deployment environment) Go to <code>config/deploy.rb</code> and add some recipes that are provided by thinking-sphinx, delayed_job and capistrano-unicorn:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;delayed/recipes&#34;</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;thinking_sphinx/capistrano&#34;</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;capistrano-unicorn&#34;</span>
</span></span></code></pre></div><p>When you run <code>cap -T</code> in your console you will get the list of available commands that should include newly added toys, isn&rsquo;t that nice? As always I recommend reading provided manual(s) before proceeding further (especially if you&rsquo;re using thinking-sphinx, a lot of stuff is happening after simply requiring its recipe).</p>
<p>Next setup your application repository:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>set <span style="color:#e6db74">:application</span>, <span style="color:#e6db74">&#34;app name&#34;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:repository</span>,  <span style="color:#e6db74">&#34;repo path&#34;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:scm</span>, <span style="color:#e6db74">:git</span> <span style="color:#75715e"># or whatever you use, this can be guessed by Capistrano if not provided</span>
</span></span></code></pre></div><p>Now we will setup a destination machine. I&rsquo;m running my app on shared host and I need to connect to my destination (web) server through shell server first. But in my case that is not enough - I&rsquo;ll need also provide separate ssh key and set proper PATH and RUBY_VER values so it all works nicely.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>server <span style="color:#e6db74">&#39;destination server&#39;</span>, <span style="color:#e6db74">:app</span>, <span style="color:#e6db74">:web</span>, <span style="color:#e6db74">:db</span>, <span style="color:#e6db74">:primary</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:gateway</span>, <span style="color:#e6db74">&#39;shell server&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:deploy_to</span>, <span style="color:#e6db74">&#39;my deploy path&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:user</span>, <span style="color:#e6db74">&#39;my login&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:use_sudo</span>, <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:default_environment</span>, {
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#39;PATH&#39;</span>     <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;path dependent values&#39;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#39;RUBY_VER&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;1.9&#39;</span> <span style="color:#75715e"># This is required by my shared host provider</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:rake</span>, <span style="color:#e6db74">&#39;bundle exec rake&#39;</span> <span style="color:#75715e"># and I need to run everything through bundler,</span>
</span></span><span style="display:flex;"><span>                              <span style="color:#75715e"># because I&#39;m installing gem to my home directory</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>ssh_options<span style="color:#f92672">[</span><span style="color:#e6db74">:keys</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">%w(~/.ssh/my_private_key ~/.ssh/my_web_server_key)</span> <span style="color:#75715e"># Capistrano will try each key</span>
</span></span><span style="display:flex;"><span>                                                                        <span style="color:#75715e"># and I need a different one when</span>
</span></span><span style="display:flex;"><span>                                                                        <span style="color:#75715e"># connecting to destination server</span>
</span></span><span style="display:flex;"><span>                                                                        <span style="color:#75715e"># through shell server</span>
</span></span></code></pre></div><p>Now let&rsquo;s set some Rails env variables:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>set <span style="color:#e6db74">:default_env</span>, <span style="color:#e6db74">&#39;production&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:rails_env</span>, <span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#39;RAILS_ENV&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">||</span> default_env
</span></span></code></pre></div><p>I liked that Mina provided <code>shared_paths</code> variable and just linked everything there as you would normally expect. Unfortunately in Capistrano it works a little bit different. You can override <code>shared_children</code> (with is set default to <code>%w(public/system log tmp/pids)</code>), so it would look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>set <span style="color:#e6db74">:shared_content</span>, <span style="color:#e6db74">%w(config/database.yml config/something.yml)</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:shared_children</span>, shared_children <span style="color:#f92672">+</span> shared_content
</span></span></code></pre></div><p>But what I really don&rsquo;t like about this solution is that you have to keep your <code>shared/</code> directory structure flat - I would normally expect to put <em>database.yml</em> in <em>shared/config/database.yml</em>, not inside <em>shared/database.yml</em>, same goes for assets and everything else. So instead I decided to set <code>shared_children</code> to an empty array and create custom task that link all my files and compile assets in a way that I&rsquo;m used to.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>set <span style="color:#e6db74">:shared_children</span>, <span style="color:#f92672">[]</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:shared_content</span>, <span style="color:#f92672">[</span><span style="color:#e6db74">&#39;public/system&#39;</span>, <span style="color:#e6db74">&#39;public/assets&#39;</span>, <span style="color:#e6db74">&#39;tmp&#39;</span><span style="color:#f92672">]</span> <span style="color:#75715e"># and it goes on</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>namespace <span style="color:#e6db74">:deploy</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  task <span style="color:#e6db74">:precompile_assets</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    run <span style="color:#e6db74">&#34;cd -- </span><span style="color:#e6db74">#{</span>latest_release<span style="color:#e6db74">}</span><span style="color:#e6db74"> &amp;&amp; </span><span style="color:#e6db74">#{</span>rake<span style="color:#e6db74">}</span><span style="color:#e6db74"> RAILS_ENV=</span><span style="color:#e6db74">#{</span>rails_env<span style="color:#f92672">.</span>to_s<span style="color:#f92672">.</span>shellescape<span style="color:#e6db74">}</span><span style="color:#e6db74"> assets:precompile&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  task <span style="color:#e6db74">:symlink_directories</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    shared_content<span style="color:#f92672">.</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>file<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>      run <span style="color:#e6db74">&#34;ln -nfs </span><span style="color:#e6db74">#{</span>deploy_to<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">#{</span>shared_dir<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">#{</span>file<span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">#{</span>release_path<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">#{</span>file<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#75715e"># you can pack it into once huge command if you want - execution will be much faster</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>after <span style="color:#e6db74">&#39;deploy:update_code&#39;</span>, <span style="color:#e6db74">&#39;deploy:symlink_directories&#39;</span>, <span style="color:#e6db74">&#39;deploy:precompile_assets&#39;</span>
</span></span></code></pre></div><p>Now - I like it much better. What&rsquo;s let to to is to setup some tasks for delayed_job and for restarting our unicorns:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>after <span style="color:#e6db74">&#39;deploy:restart&#39;</span>,     <span style="color:#e6db74">&#39;unicorn:duplicate&#39;</span>
</span></span><span style="display:flex;"><span>after <span style="color:#e6db74">&#39;deploy:stop&#39;</span>,        <span style="color:#e6db74">&#39;delayed_job:stop&#39;</span>
</span></span><span style="display:flex;"><span>after <span style="color:#e6db74">&#39;deploy:start&#39;</span>,       <span style="color:#e6db74">&#39;delayed_job:start&#39;</span>
</span></span><span style="display:flex;"><span>after <span style="color:#e6db74">&#39;deploy:restart&#39;</span>,     <span style="color:#e6db74">&#39;delayed_job:restart&#39;</span>
</span></span></code></pre></div><p>Why <code>unicorn:duplicate</code> you ask? If you are using unicorn configuration provided by <a href="https://github.com/sosedoff/capistrano-unicorn/blob/master/examples/rails3.rb">this sample file</a> after sending USR2 signal to master process unicorn should restart itself smoothly and you will get zero downtime deployment.</p>
<p>That&rsquo;s more or less it when it comes to basic configuration, happy deploying!</p>]]></description><content:encoded><![CDATA[<p>It&rsquo;s been a while since I last posted here. I&rsquo;ve been pretty busy with work, I&rsquo;ve also moved to Cracov in the meantime - so yeah, I can&rsquo;t really complaint about boredom ;).</p>
<p>Anyhow, some time ago I posted <a href="/blog/2012/10/27/super-fast-deployment-with-mina-capistrano-alternative/">about Mina</a> - neat little gem that allows super fast deployments. Unfortunately when your project grows you start adding additional bash scripts, you add more and more commands to your deploy script and it grows into something not-so-neat anymore.</p>
<p>That&rsquo;s why tempted with Capistrano&rsquo;s ready-to-use recipes I decided it&rsquo;s time to say hello to Capistrano. I&rsquo;ll provide here step-by-step migration process from Mina. I will be using here <a href="https://github.com/collectiveidea/delayed_job">delayed_job</a>, <a href="http://unicorn.bogomips.org/">unicorn</a> (with <a href="https://github.com/sosedoff/capistrano-unicorn">capistrano-unicorn</a> gem) and <a href="https://github.com/pat/thinking-sphinx">thinking-sphinx</a>.</p>
<p>First, install <a href="https://github.com/capistrano/capistrano">Capistrano</a> and capify your project with command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>capify .
</span></span></code></pre></div><p>(I assume you need only one deployment environment) Go to <code>config/deploy.rb</code> and add some recipes that are provided by thinking-sphinx, delayed_job and capistrano-unicorn:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;delayed/recipes&#34;</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;thinking_sphinx/capistrano&#34;</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;capistrano-unicorn&#34;</span>
</span></span></code></pre></div><p>When you run <code>cap -T</code> in your console you will get the list of available commands that should include newly added toys, isn&rsquo;t that nice? As always I recommend reading provided manual(s) before proceeding further (especially if you&rsquo;re using thinking-sphinx, a lot of stuff is happening after simply requiring its recipe).</p>
<p>Next setup your application repository:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>set <span style="color:#e6db74">:application</span>, <span style="color:#e6db74">&#34;app name&#34;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:repository</span>,  <span style="color:#e6db74">&#34;repo path&#34;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:scm</span>, <span style="color:#e6db74">:git</span> <span style="color:#75715e"># or whatever you use, this can be guessed by Capistrano if not provided</span>
</span></span></code></pre></div><p>Now we will setup a destination machine. I&rsquo;m running my app on shared host and I need to connect to my destination (web) server through shell server first. But in my case that is not enough - I&rsquo;ll need also provide separate ssh key and set proper PATH and RUBY_VER values so it all works nicely.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>server <span style="color:#e6db74">&#39;destination server&#39;</span>, <span style="color:#e6db74">:app</span>, <span style="color:#e6db74">:web</span>, <span style="color:#e6db74">:db</span>, <span style="color:#e6db74">:primary</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:gateway</span>, <span style="color:#e6db74">&#39;shell server&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:deploy_to</span>, <span style="color:#e6db74">&#39;my deploy path&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:user</span>, <span style="color:#e6db74">&#39;my login&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:use_sudo</span>, <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:default_environment</span>, {
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#39;PATH&#39;</span>     <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;path dependent values&#39;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#39;RUBY_VER&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;1.9&#39;</span> <span style="color:#75715e"># This is required by my shared host provider</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:rake</span>, <span style="color:#e6db74">&#39;bundle exec rake&#39;</span> <span style="color:#75715e"># and I need to run everything through bundler,</span>
</span></span><span style="display:flex;"><span>                              <span style="color:#75715e"># because I&#39;m installing gem to my home directory</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>ssh_options<span style="color:#f92672">[</span><span style="color:#e6db74">:keys</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">%w(~/.ssh/my_private_key ~/.ssh/my_web_server_key)</span> <span style="color:#75715e"># Capistrano will try each key</span>
</span></span><span style="display:flex;"><span>                                                                        <span style="color:#75715e"># and I need a different one when</span>
</span></span><span style="display:flex;"><span>                                                                        <span style="color:#75715e"># connecting to destination server</span>
</span></span><span style="display:flex;"><span>                                                                        <span style="color:#75715e"># through shell server</span>
</span></span></code></pre></div><p>Now let&rsquo;s set some Rails env variables:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>set <span style="color:#e6db74">:default_env</span>, <span style="color:#e6db74">&#39;production&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:rails_env</span>, <span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#39;RAILS_ENV&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">||</span> default_env
</span></span></code></pre></div><p>I liked that Mina provided <code>shared_paths</code> variable and just linked everything there as you would normally expect. Unfortunately in Capistrano it works a little bit different. You can override <code>shared_children</code> (with is set default to <code>%w(public/system log tmp/pids)</code>), so it would look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>set <span style="color:#e6db74">:shared_content</span>, <span style="color:#e6db74">%w(config/database.yml config/something.yml)</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:shared_children</span>, shared_children <span style="color:#f92672">+</span> shared_content
</span></span></code></pre></div><p>But what I really don&rsquo;t like about this solution is that you have to keep your <code>shared/</code> directory structure flat - I would normally expect to put <em>database.yml</em> in <em>shared/config/database.yml</em>, not inside <em>shared/database.yml</em>, same goes for assets and everything else. So instead I decided to set <code>shared_children</code> to an empty array and create custom task that link all my files and compile assets in a way that I&rsquo;m used to.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>set <span style="color:#e6db74">:shared_children</span>, <span style="color:#f92672">[]</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:shared_content</span>, <span style="color:#f92672">[</span><span style="color:#e6db74">&#39;public/system&#39;</span>, <span style="color:#e6db74">&#39;public/assets&#39;</span>, <span style="color:#e6db74">&#39;tmp&#39;</span><span style="color:#f92672">]</span> <span style="color:#75715e"># and it goes on</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>namespace <span style="color:#e6db74">:deploy</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  task <span style="color:#e6db74">:precompile_assets</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    run <span style="color:#e6db74">&#34;cd -- </span><span style="color:#e6db74">#{</span>latest_release<span style="color:#e6db74">}</span><span style="color:#e6db74"> &amp;&amp; </span><span style="color:#e6db74">#{</span>rake<span style="color:#e6db74">}</span><span style="color:#e6db74"> RAILS_ENV=</span><span style="color:#e6db74">#{</span>rails_env<span style="color:#f92672">.</span>to_s<span style="color:#f92672">.</span>shellescape<span style="color:#e6db74">}</span><span style="color:#e6db74"> assets:precompile&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  task <span style="color:#e6db74">:symlink_directories</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    shared_content<span style="color:#f92672">.</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>file<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>      run <span style="color:#e6db74">&#34;ln -nfs </span><span style="color:#e6db74">#{</span>deploy_to<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">#{</span>shared_dir<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">#{</span>file<span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">#{</span>release_path<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">#{</span>file<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#75715e"># you can pack it into once huge command if you want - execution will be much faster</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>after <span style="color:#e6db74">&#39;deploy:update_code&#39;</span>, <span style="color:#e6db74">&#39;deploy:symlink_directories&#39;</span>, <span style="color:#e6db74">&#39;deploy:precompile_assets&#39;</span>
</span></span></code></pre></div><p>Now - I like it much better. What&rsquo;s let to to is to setup some tasks for delayed_job and for restarting our unicorns:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>after <span style="color:#e6db74">&#39;deploy:restart&#39;</span>,     <span style="color:#e6db74">&#39;unicorn:duplicate&#39;</span>
</span></span><span style="display:flex;"><span>after <span style="color:#e6db74">&#39;deploy:stop&#39;</span>,        <span style="color:#e6db74">&#39;delayed_job:stop&#39;</span>
</span></span><span style="display:flex;"><span>after <span style="color:#e6db74">&#39;deploy:start&#39;</span>,       <span style="color:#e6db74">&#39;delayed_job:start&#39;</span>
</span></span><span style="display:flex;"><span>after <span style="color:#e6db74">&#39;deploy:restart&#39;</span>,     <span style="color:#e6db74">&#39;delayed_job:restart&#39;</span>
</span></span></code></pre></div><p>Why <code>unicorn:duplicate</code> you ask? If you are using unicorn configuration provided by <a href="https://github.com/sosedoff/capistrano-unicorn/blob/master/examples/rails3.rb">this sample file</a> after sending USR2 signal to master process unicorn should restart itself smoothly and you will get zero downtime deployment.</p>
<p>That&rsquo;s more or less it when it comes to basic configuration, happy deploying!</p>]]></content:encoded></item><item><title>Bootstrap 2.3.0 and lightbox/fancybox crash issue</title><link>https://awesomeprogrammer.com/blog/2013/02/17/bootstrap-2-dot-3-0-and-lightbox-slash-fancybox-crash-issue/</link><pubDate>Sun, 17 Feb 2013 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2013/02/17/bootstrap-2-dot-3-0-and-lightbox-slash-fancybox-crash-issue/</guid><description><![CDATA[<p>After upgrading <a href="http://twitter.github.com/bootstrap/">bootstrap</a> to version 2.3.0 I spent an evening trying to figure out what&rsquo;s causing <em>too much recursion</em> error that was in the end crashing my entire browser when using lightbox or fancybox. Then I said - frak it, it&rsquo;s time to get some rest and in the morning I decided to visit github issues section first - and let me say I should start there, because there it was - waiting for me - exactly the same problem with possible solution :P. <a href="https://github.com/twitter/bootstrap/issues/6862">Issue #6862</a> is the right place for you. Thanks to the fellow programmer called <a href="https://github.com/Yohn">Yohn</a> I didn&rsquo;t had to waste no more time, so here we go:</p>
<p>You need to edit <code>dropdown.js</code> file and replace:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span>.<span style="color:#a6e22e">on</span>(<span style="color:#e6db74">&#39;.dropdown-menu&#39;</span>, <span style="color:#66d9ef">function</span> (<span style="color:#a6e22e">e</span>) { <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">stopPropagation</span>() })
</span></span></code></pre></div><p>with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span>.<span style="color:#a6e22e">on</span>(<span style="color:#e6db74">&#39;click.dropdown-menu&#39;</span>, <span style="color:#66d9ef">function</span> (<span style="color:#a6e22e">e</span>) { <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">stopPropagation</span>() })
</span></span></code></pre></div><p>And that&rsquo;s it. If you are using Rails and <a href="https://github.com/thomas-mcdonald/bootstrap-sass">bootstrap-sass</a> gem - load only required js files in you <code>applicaion.js</code> and then clone <a href="https://github.com/thomas-mcdonald/bootstrap-sass/blob/master/vendor/assets/javascripts/bootstrap-dropdown.js">bootstrap-dropdown.js</a> to <code>vendor/assets/javascripts</code> directory, patch the file and load that patched one instead of original. This will to the trick until it is fixed (if it ever will, apparently third party tools are not supported by Boostrap, but I don&rsquo;t see the reason why should they break it on the way).</p>
]]></description><content:encoded><![CDATA[<p>After upgrading <a href="http://twitter.github.com/bootstrap/">bootstrap</a> to version 2.3.0 I spent an evening trying to figure out what&rsquo;s causing <em>too much recursion</em> error that was in the end crashing my entire browser when using lightbox or fancybox. Then I said - frak it, it&rsquo;s time to get some rest and in the morning I decided to visit github issues section first - and let me say I should start there, because there it was - waiting for me - exactly the same problem with possible solution :P. <a href="https://github.com/twitter/bootstrap/issues/6862">Issue #6862</a> is the right place for you. Thanks to the fellow programmer called <a href="https://github.com/Yohn">Yohn</a> I didn&rsquo;t had to waste no more time, so here we go:</p>
<p>You need to edit <code>dropdown.js</code> file and replace:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span>.<span style="color:#a6e22e">on</span>(<span style="color:#e6db74">&#39;.dropdown-menu&#39;</span>, <span style="color:#66d9ef">function</span> (<span style="color:#a6e22e">e</span>) { <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">stopPropagation</span>() })
</span></span></code></pre></div><p>with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span>.<span style="color:#a6e22e">on</span>(<span style="color:#e6db74">&#39;click.dropdown-menu&#39;</span>, <span style="color:#66d9ef">function</span> (<span style="color:#a6e22e">e</span>) { <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">stopPropagation</span>() })
</span></span></code></pre></div><p>And that&rsquo;s it. If you are using Rails and <a href="https://github.com/thomas-mcdonald/bootstrap-sass">bootstrap-sass</a> gem - load only required js files in you <code>applicaion.js</code> and then clone <a href="https://github.com/thomas-mcdonald/bootstrap-sass/blob/master/vendor/assets/javascripts/bootstrap-dropdown.js">bootstrap-dropdown.js</a> to <code>vendor/assets/javascripts</code> directory, patch the file and load that patched one instead of original. This will to the trick until it is fixed (if it ever will, apparently third party tools are not supported by Boostrap, but I don&rsquo;t see the reason why should they break it on the way).</p>
]]></content:encoded></item><item><title>PayPal IPN madness with Ruby on Rails</title><link>https://awesomeprogrammer.com/blog/2013/02/04/paypal-ipn-madness-with-ruby-on-rails/</link><pubDate>Mon, 04 Feb 2013 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2013/02/04/paypal-ipn-madness-with-ruby-on-rails/</guid><description><![CDATA[<p>Lately I&rsquo;ve been implementing <a href="https://www.paypal.com/ipn">Instant Payment Notification</a> - <em>great</em> feature provided by PayPal. To be honest it cost me quite some time and a little bit of my dignity. I mean you can <a href="https://www.x.com/sites/default/files/ipnguide.pdf">read the official guide</a> and play with <a href="https://developer.paypal.com/cgi-bin/devscr?cmd=_ipn-link-session">Instant Payment Notification (IPN) simulator</a> - but in the end it&rsquo;s back to trials and errors.</p>
<p>So here we go, here are some tips for you, I hope you will find them useful and it will save you some time.</p>
<ol>
<li>
<p>When you receive an IPN notification you obviously have to send it back to paypal for verification (with additional <code>cmd=_notify-validate</code> option), don&rsquo;t forget to escape parameters while you do that (it may depend how you send it, but <code>URI.escape</code> may be your friend).</p>
</li>
<li>
<p>You may receive <code>txn_type</code> you didn&rsquo;t expect, here a (incomplete) list of possible variables:
<code>subscr_signup</code>,
<code>subscr_modify</code>,
<code>subscr_failed</code>,
<code>subscr_cancel</code>,
<code>subscr_payment</code>,
<code>subscr_eot</code>,
<code>recurring_payment_profile_created</code>,
<code>recurring_payment_profile_cancel</code>,
<code>recurring_payment_failed</code>,
<code>recurring_payment_outstanding_payment_failed</code>,
<code>recurring_payment</code>,
<code>recurring_payment_skipped</code>,
<code>recurring_payment_expired</code>,
<code>recurring_payment_suspended_due_to_max_failed_payment</code></p>
</li>
<li>
<p>What&rsquo;s more interesting (so to speak) is that you may receive different parameters depending on txn_type - for example if you get <code>subscr_</code> you can identify your buyer with <code>subscr_id</code>. On the other hand if you get a <code>recurring_payment_</code> you will have to use <code>recurring_payment_id</code>. There are more differences but you will have to debug it yourself - this is just a heads up.</p>
</li>
<li>
<p>PayPal tends to return a time in some messed up format (like <code>01:00:00 Jan 11, 2013 PST</code>) that can&rsquo;t be parsed using regular <code>Time.parse</code> method, instead you have to write your own parser:</p>
</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">paypal_time</span>(time)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">DateTime</span><span style="color:#f92672">.</span>strptime(time, <span style="color:#e6db74">&#34;%H:%M:%S %b %e, %Y %Z&#34;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># you may also want to convert it back to UTC using .utc on in</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>And thats four simple tips that can save A LOT of time.</p>]]></description><content:encoded><![CDATA[<p>Lately I&rsquo;ve been implementing <a href="https://www.paypal.com/ipn">Instant Payment Notification</a> - <em>great</em> feature provided by PayPal. To be honest it cost me quite some time and a little bit of my dignity. I mean you can <a href="https://www.x.com/sites/default/files/ipnguide.pdf">read the official guide</a> and play with <a href="https://developer.paypal.com/cgi-bin/devscr?cmd=_ipn-link-session">Instant Payment Notification (IPN) simulator</a> - but in the end it&rsquo;s back to trials and errors.</p>
<p>So here we go, here are some tips for you, I hope you will find them useful and it will save you some time.</p>
<ol>
<li>
<p>When you receive an IPN notification you obviously have to send it back to paypal for verification (with additional <code>cmd=_notify-validate</code> option), don&rsquo;t forget to escape parameters while you do that (it may depend how you send it, but <code>URI.escape</code> may be your friend).</p>
</li>
<li>
<p>You may receive <code>txn_type</code> you didn&rsquo;t expect, here a (incomplete) list of possible variables:
<code>subscr_signup</code>,
<code>subscr_modify</code>,
<code>subscr_failed</code>,
<code>subscr_cancel</code>,
<code>subscr_payment</code>,
<code>subscr_eot</code>,
<code>recurring_payment_profile_created</code>,
<code>recurring_payment_profile_cancel</code>,
<code>recurring_payment_failed</code>,
<code>recurring_payment_outstanding_payment_failed</code>,
<code>recurring_payment</code>,
<code>recurring_payment_skipped</code>,
<code>recurring_payment_expired</code>,
<code>recurring_payment_suspended_due_to_max_failed_payment</code></p>
</li>
<li>
<p>What&rsquo;s more interesting (so to speak) is that you may receive different parameters depending on txn_type - for example if you get <code>subscr_</code> you can identify your buyer with <code>subscr_id</code>. On the other hand if you get a <code>recurring_payment_</code> you will have to use <code>recurring_payment_id</code>. There are more differences but you will have to debug it yourself - this is just a heads up.</p>
</li>
<li>
<p>PayPal tends to return a time in some messed up format (like <code>01:00:00 Jan 11, 2013 PST</code>) that can&rsquo;t be parsed using regular <code>Time.parse</code> method, instead you have to write your own parser:</p>
</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">paypal_time</span>(time)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">DateTime</span><span style="color:#f92672">.</span>strptime(time, <span style="color:#e6db74">&#34;%H:%M:%S %b %e, %Y %Z&#34;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># you may also want to convert it back to UTC using .utc on in</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>And thats four simple tips that can save A LOT of time.</p>]]></content:encoded></item><item><title>Morris.js display:none workaround</title><link>https://awesomeprogrammer.com/blog/2013/01/20/morris-dot-js-display-none-workaround/</link><pubDate>Sun, 20 Jan 2013 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2013/01/20/morris-dot-js-display-none-workaround/</guid><description><![CDATA[<p><a href="http://www.oesmith.co.uk/morris.js/">Morris.js</a> is great, simple and lightweight charts library powered by jQuery and Raphael. Unfortunately you may encounter an issue when you will try to plot a chart on element with <code>display:none</code> property - for example when using Twitter&rsquo;s Bootstrap tabbed component - it just doesn&rsquo;t render properly. So as a quick workaround you can fire-up <em>after</em> showing that element.</p>
<p>Bootstrap provides some neat javascript events that are fired after some specific actions. So when you dive in into <a href="http://twitter.github.com/bootstrap/javascript.html#tabs">documentation</a> you will find out that you can catch <code>shown</code> even that is fired after the tab has been shown. Just like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#a6e22e">$</span>(<span style="color:#e6db74">&#39;a[data-toggle=&#34;tab&#34;]&#39;</span>).<span style="color:#a6e22e">on</span>(<span style="color:#e6db74">&#39;shown&#39;</span>, <span style="color:#66d9ef">function</span> (<span style="color:#a6e22e">e</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">target</span> <span style="color:#75715e">// activated tab
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">relatedTarget</span> <span style="color:#75715e">// previous tab
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>})
</span></span></code></pre></div><p>Just create a simple snippet and you are good to go.</p>
]]></description><content:encoded><![CDATA[<p><a href="http://www.oesmith.co.uk/morris.js/">Morris.js</a> is great, simple and lightweight charts library powered by jQuery and Raphael. Unfortunately you may encounter an issue when you will try to plot a chart on element with <code>display:none</code> property - for example when using Twitter&rsquo;s Bootstrap tabbed component - it just doesn&rsquo;t render properly. So as a quick workaround you can fire-up <em>after</em> showing that element.</p>
<p>Bootstrap provides some neat javascript events that are fired after some specific actions. So when you dive in into <a href="http://twitter.github.com/bootstrap/javascript.html#tabs">documentation</a> you will find out that you can catch <code>shown</code> even that is fired after the tab has been shown. Just like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#a6e22e">$</span>(<span style="color:#e6db74">&#39;a[data-toggle=&#34;tab&#34;]&#39;</span>).<span style="color:#a6e22e">on</span>(<span style="color:#e6db74">&#39;shown&#39;</span>, <span style="color:#66d9ef">function</span> (<span style="color:#a6e22e">e</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">target</span> <span style="color:#75715e">// activated tab
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">relatedTarget</span> <span style="color:#75715e">// previous tab
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>})
</span></span></code></pre></div><p>Just create a simple snippet and you are good to go.</p>
]]></content:encoded></item><item><title>How fast your tests are? - Working with rspec outside rails</title><link>https://awesomeprogrammer.com/blog/2012/12/30/how-fast-your-tests-are-working-with-rspec-outside-rails/</link><pubDate>Sun, 30 Dec 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/12/30/how-fast-your-tests-are-working-with-rspec-outside-rails/</guid><description><![CDATA[<p>Rails is a great framework, isn&rsquo;t it? It&rsquo;s ships with all those great tools witch are supposed to help you automatically test your application. So you write specs, you practice TDD, BDD or maybe DDD, but over time you may notice something - it&rsquo;s getting painfully slow. You app grows, your test suite grows and suddenly testing isn&rsquo;t anymore so much fun as it supposed to be. You feel like it&rsquo;s slowing you down.</p>
<p>So you you start using <em>spork</em>, you setup all the <em>guards</em> - but that are just a bandages for a bigger problem. And the problem is - Rails is just slow. I mean not like it&rsquo;s a slow in production - when you just need to boot up your app once and that&rsquo;s it - it&rsquo;s not such a big deal - but you do it constantly in development, and more gems you add (because gems are cool, right?) it takes more and more time. Sooner or later you will start skipping on your tests and you may end up in very bad place.</p>
<p>What I started to do lately with my legacy app I was talking before is - I started testing outside rails. Rails is slow, so let&rsquo;s take everything you can outside rails and work with pure ruby and rspec. What I mean is you should try and extract some logic into separate classes/modules and test them without database and without framework. And you just need a place to start, let me give you a very simple example where I started.</p>
<p>I have heavily customized two redcarpet render engines - basically I was interested in two helper methods in my <em>ApplicationHelper</em> module. But god how I was frustrated with loading times - I have there some very <em>annoying</em> tests, because I&rsquo;m asserting html output of markdown input - as I needed absolute certainty everything worked as it supposed to the last tag.</p>
<p>So here&rsquo;s what my spec look like, I added some comments for clarity.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># spec/modules/redcarpet/markdown_spec.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># obviously we will need that</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;redcarpet&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I&#39;m using cattr_accessor in one of my classes</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;active_support/all&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I&#39;m also using strip_tags methods from action view helpers</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;action_view/helpers/sanitize_helper&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># so I&#39;m just including here all the methods of sanitize helper</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">include</span> <span style="color:#66d9ef">ActionView</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Helpers</span><span style="color:#f92672">::</span><span style="color:#66d9ef">SanitizeHelper</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I want my app/ directory</span>
</span></span><span style="display:flex;"><span>app_dir <span style="color:#f92672">=</span> <span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>expand_path(<span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>join(<span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>dirname(__FILE__), <span style="color:#e6db74">&#39;..&#39;</span>, <span style="color:#e6db74">&#39;..&#39;</span>, <span style="color:#e6db74">&#39;..&#39;</span>, <span style="color:#e6db74">&#39;app&#39;</span>))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I want my application helper (even tho I&#39;m interested in two methods here)</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span>app_dir<span style="color:#e6db74">}</span><span style="color:#e6db74">/helpers/application_helper.rb&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># here I also included my custom redcarpet classes,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># but I will omit that in this example</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I will a class that I will test</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># and include all helper methods</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MarkdownTest</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">extend</span> <span style="color:#66d9ef">ApplicationHelper</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># and later or it&#39;s just a &#39;regular&#39; rspec as you know it</span>
</span></span><span style="display:flex;"><span>describe <span style="color:#e6db74">&#34;Sugar Markdown&#34;</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  subject { <span style="color:#66d9ef">MarkdownTest</span><span style="color:#f92672">.</span>markdown(text) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  context <span style="color:#e6db74">&#34;handles flags&#34;</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    let(<span style="color:#e6db74">:text</span>){ <span style="color:#e6db74">&#34;Titta :se: pa mig!&#34;</span> }
</span></span><span style="display:flex;"><span>    it { should <span style="color:#f92672">=~</span> <span style="color:#e6db74">/Titta &lt;img src=&#39;\/img\/flags\/se.gif&#39; title=&#39;Szwecja&#39; \/&gt; pa mig!/</span> }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>And how fast is it? Pretty damn fast.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>$ time rspec spec/modules/redcarpet/
</span></span><span style="display:flex;"><span>Finished in 0.02648 seconds
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">16</span> examples, <span style="color:#ae81ff">0</span> failures
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>real  0m0.424s
</span></span><span style="display:flex;"><span>user  0m0.388s
</span></span><span style="display:flex;"><span>sys 0m0.032s
</span></span></code></pre></div><p>Now you don&rsquo;t have to wait for rails to load just to test this one part of you app and you can do true TDD - your tests won&rsquo;t slow you down anymore - they will drive you and your design. Of course you will still need to test models, controllers, do integrational tests, but I hope you see the point of testing without rails, and if you don&rsquo;t - even so I encourage you to try it for yourself.</p>]]></description><content:encoded><![CDATA[<p>Rails is a great framework, isn&rsquo;t it? It&rsquo;s ships with all those great tools witch are supposed to help you automatically test your application. So you write specs, you practice TDD, BDD or maybe DDD, but over time you may notice something - it&rsquo;s getting painfully slow. You app grows, your test suite grows and suddenly testing isn&rsquo;t anymore so much fun as it supposed to be. You feel like it&rsquo;s slowing you down.</p>
<p>So you you start using <em>spork</em>, you setup all the <em>guards</em> - but that are just a bandages for a bigger problem. And the problem is - Rails is just slow. I mean not like it&rsquo;s a slow in production - when you just need to boot up your app once and that&rsquo;s it - it&rsquo;s not such a big deal - but you do it constantly in development, and more gems you add (because gems are cool, right?) it takes more and more time. Sooner or later you will start skipping on your tests and you may end up in very bad place.</p>
<p>What I started to do lately with my legacy app I was talking before is - I started testing outside rails. Rails is slow, so let&rsquo;s take everything you can outside rails and work with pure ruby and rspec. What I mean is you should try and extract some logic into separate classes/modules and test them without database and without framework. And you just need a place to start, let me give you a very simple example where I started.</p>
<p>I have heavily customized two redcarpet render engines - basically I was interested in two helper methods in my <em>ApplicationHelper</em> module. But god how I was frustrated with loading times - I have there some very <em>annoying</em> tests, because I&rsquo;m asserting html output of markdown input - as I needed absolute certainty everything worked as it supposed to the last tag.</p>
<p>So here&rsquo;s what my spec look like, I added some comments for clarity.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># spec/modules/redcarpet/markdown_spec.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># obviously we will need that</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;redcarpet&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I&#39;m using cattr_accessor in one of my classes</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;active_support/all&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I&#39;m also using strip_tags methods from action view helpers</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;action_view/helpers/sanitize_helper&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># so I&#39;m just including here all the methods of sanitize helper</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">include</span> <span style="color:#66d9ef">ActionView</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Helpers</span><span style="color:#f92672">::</span><span style="color:#66d9ef">SanitizeHelper</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I want my app/ directory</span>
</span></span><span style="display:flex;"><span>app_dir <span style="color:#f92672">=</span> <span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>expand_path(<span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>join(<span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>dirname(__FILE__), <span style="color:#e6db74">&#39;..&#39;</span>, <span style="color:#e6db74">&#39;..&#39;</span>, <span style="color:#e6db74">&#39;..&#39;</span>, <span style="color:#e6db74">&#39;app&#39;</span>))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I want my application helper (even tho I&#39;m interested in two methods here)</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span>app_dir<span style="color:#e6db74">}</span><span style="color:#e6db74">/helpers/application_helper.rb&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># here I also included my custom redcarpet classes,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># but I will omit that in this example</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I will a class that I will test</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># and include all helper methods</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MarkdownTest</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">extend</span> <span style="color:#66d9ef">ApplicationHelper</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># and later or it&#39;s just a &#39;regular&#39; rspec as you know it</span>
</span></span><span style="display:flex;"><span>describe <span style="color:#e6db74">&#34;Sugar Markdown&#34;</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  subject { <span style="color:#66d9ef">MarkdownTest</span><span style="color:#f92672">.</span>markdown(text) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  context <span style="color:#e6db74">&#34;handles flags&#34;</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    let(<span style="color:#e6db74">:text</span>){ <span style="color:#e6db74">&#34;Titta :se: pa mig!&#34;</span> }
</span></span><span style="display:flex;"><span>    it { should <span style="color:#f92672">=~</span> <span style="color:#e6db74">/Titta &lt;img src=&#39;\/img\/flags\/se.gif&#39; title=&#39;Szwecja&#39; \/&gt; pa mig!/</span> }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ...</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>And how fast is it? Pretty damn fast.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>$ time rspec spec/modules/redcarpet/
</span></span><span style="display:flex;"><span>Finished in 0.02648 seconds
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">16</span> examples, <span style="color:#ae81ff">0</span> failures
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>real  0m0.424s
</span></span><span style="display:flex;"><span>user  0m0.388s
</span></span><span style="display:flex;"><span>sys 0m0.032s
</span></span></code></pre></div><p>Now you don&rsquo;t have to wait for rails to load just to test this one part of you app and you can do true TDD - your tests won&rsquo;t slow you down anymore - they will drive you and your design. Of course you will still need to test models, controllers, do integrational tests, but I hope you see the point of testing without rails, and if you don&rsquo;t - even so I encourage you to try it for yourself.</p>]]></content:encoded></item><item><title>Project released!</title><link>https://awesomeprogrammer.com/blog/2012/12/24/project-released/</link><pubDate>Mon, 24 Dec 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/12/24/project-released/</guid><description><![CDATA[<p>Finally, after initial commit_ I did almost year ago (30.12.2011) I decided it&rsquo;s time to release this app I&rsquo;ve been working on. I spent many, many hours after work and I don&rsquo;t regret it - I learned A LOT of rails and now I can say I&rsquo;m pretty confident when it comes working with RoR.</p>
<p>Yet I&rsquo;m aware I kinda rushed it a little bit, but to be honest - year is a long time and I felt, I don&rsquo;t know, personal pressure - I just had to release it before 2013 :P. It&rsquo;s way better comparing to previous (legacy PHP) version and yeah - I&rsquo;m proud of it - it will probably take a week or two for some various tweaks and fixes and later on I&rsquo;m planning on adding some new cool features and maybe integrating another (already dead tho) quake-like project of mine into it? Time will tell, for now I feel I need to step back a little bit and take a break and just relax. Almost forgot - you can see the result <a href="http://quake.net.pl">quake.net.pl</a>.</p>
<p>As I mentioned before on my blog I&rsquo;ll try to find some time and talk about some common issues I&rsquo;ve encountered. What can I tell now, just after release is:</p>
<ul>
<li>
<p>something will probably break anyway, especially when it&rsquo;s not-so-tiny one-man project, so don&rsquo;t worry and be prepared to add some specs you might missed :P</p>
</li>
<li>
<p>when you doing migration like I recommend logging all 404 errors (at least for first month or two) - <a href="https://github.com/jtrupiano/rack-rewrite">rack-rewrite</a> will be your friend; you might have no idea about some weird old urls that may point to your site - and you don&rsquo;t want to lose traffic, don&rsquo;t you? Be kind and prepare some nice redirects for your users that may became regular members</p>
</li>
</ul>
<p>I would also thank to Rails community, gem maintainers and all programmers who&rsquo;re sharing their knowledge - without you I would probably spend another year (or three) building this app from scratch. Many thanks!</p>
<p>And of course&hellip; Merry Christmas everyone!</p>
]]></description><content:encoded><![CDATA[<p>Finally, after initial commit_ I did almost year ago (30.12.2011) I decided it&rsquo;s time to release this app I&rsquo;ve been working on. I spent many, many hours after work and I don&rsquo;t regret it - I learned A LOT of rails and now I can say I&rsquo;m pretty confident when it comes working with RoR.</p>
<p>Yet I&rsquo;m aware I kinda rushed it a little bit, but to be honest - year is a long time and I felt, I don&rsquo;t know, personal pressure - I just had to release it before 2013 :P. It&rsquo;s way better comparing to previous (legacy PHP) version and yeah - I&rsquo;m proud of it - it will probably take a week or two for some various tweaks and fixes and later on I&rsquo;m planning on adding some new cool features and maybe integrating another (already dead tho) quake-like project of mine into it? Time will tell, for now I feel I need to step back a little bit and take a break and just relax. Almost forgot - you can see the result <a href="http://quake.net.pl">quake.net.pl</a>.</p>
<p>As I mentioned before on my blog I&rsquo;ll try to find some time and talk about some common issues I&rsquo;ve encountered. What can I tell now, just after release is:</p>
<ul>
<li>
<p>something will probably break anyway, especially when it&rsquo;s not-so-tiny one-man project, so don&rsquo;t worry and be prepared to add some specs you might missed :P</p>
</li>
<li>
<p>when you doing migration like I recommend logging all 404 errors (at least for first month or two) - <a href="https://github.com/jtrupiano/rack-rewrite">rack-rewrite</a> will be your friend; you might have no idea about some weird old urls that may point to your site - and you don&rsquo;t want to lose traffic, don&rsquo;t you? Be kind and prepare some nice redirects for your users that may became regular members</p>
</li>
</ul>
<p>I would also thank to Rails community, gem maintainers and all programmers who&rsquo;re sharing their knowledge - without you I would probably spend another year (or three) building this app from scratch. Many thanks!</p>
<p>And of course&hellip; Merry Christmas everyone!</p>
]]></content:encoded></item><item><title>Diary of Legacy Application</title><link>https://awesomeprogrammer.com/blog/2012/12/16/diary-of-legacy-application/</link><pubDate>Sun, 16 Dec 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/12/16/diary-of-legacy-application/</guid><description><![CDATA[<p>Some time ago I wrote two short stories (<a href="/blog/2012/07/28/rails-legacy-database-migration/">1</a>, <a href="/blog/2012/08/08/rails-legacy-database-migration-part-2/">2</a>) about my journey with migrating legacy php application to rails. And yes - that&rsquo;s topic is till on my plate :P. I didn&rsquo;t had much time to push it till the finish (not mention posting on my blog), but I&rsquo;m getting there.</p>
<p>Since I&rsquo;ve started my small <em>after-hours</em> project quite a few things have changed, for example I:</p>
<ul>
<li>
<p>said goodbye to <a href="http://haml-lang.com/">HAML</a> and welcomed <a href="http://slim-lang.com/">SLIM</a></p>
</li>
<li>
<p>started using <a href="https://github.com/thoughtbot/capybara-webkit">capybara-webkit</a> instead of slow <a href="http://seleniumhq.org/">Selenium</a> for integration tests with javascript enabled</p>
</li>
<li>
<p>cleaned up my helpers and moved some logic to decorators (I recommend checking out <a href="https://github.com/amatsuda/active_decorator">active_decorator</a>)</p>
</li>
<li>
<p>hooked up <a href="http://pat.github.com/ts/en/">thinking-sphinx</a> for creating global (site-wide) search engine (works amazing and I truly recommend it for everyone)</p>
</li>
<li>
<p>created a devise&amp;omniauth combo and allowed users to sign-in via google/facebook account</p>
</li>
<li>
<p>build simple xmmp (jabber) bot as a way of informing users about new replies/posts in observed topics on forum (using quite outdated <a href="https://github.com/ln/xmpp4r">xmpp4r</a> library - I will came back to the topic after official release for sure, for now I&rsquo;ll have to be be enough)</p>
</li>
<li>
<p>after a discussion with the editors of current site we have decided it&rsquo;s time to evolve and choose markdown instead of bbcode after all. I&rsquo;ve chosen <a href="https://github.com/vmg/redcarpet">redcarpet</a> - with if bloody fast and really flexible - I&rsquo;ve added some custom tags and support for <a href="http://www.emoji-cheat-sheet.com/">emoji</a>, created a neat editor based on <a href="http://markitup.jaysalvat.com/">markItUp</a>, spiced it a little with some github like preview style and some <a href="https://github.com/ichord/At.js">at.js</a> goodness</p>
</li>
</ul>
<p>So yeah - quite a few things changed indeed and there is more to come. I hope when I will finish this project (and I mean here official release) I will find some time to write about <em>gotchas</em> I&rsquo;ve encountered and some general tips when dealing with legacy application complete upgrade from my points of view :).</p>]]></description><content:encoded><![CDATA[<p>Some time ago I wrote two short stories (<a href="/blog/2012/07/28/rails-legacy-database-migration/">1</a>, <a href="/blog/2012/08/08/rails-legacy-database-migration-part-2/">2</a>) about my journey with migrating legacy php application to rails. And yes - that&rsquo;s topic is till on my plate :P. I didn&rsquo;t had much time to push it till the finish (not mention posting on my blog), but I&rsquo;m getting there.</p>
<p>Since I&rsquo;ve started my small <em>after-hours</em> project quite a few things have changed, for example I:</p>
<ul>
<li>
<p>said goodbye to <a href="http://haml-lang.com/">HAML</a> and welcomed <a href="http://slim-lang.com/">SLIM</a></p>
</li>
<li>
<p>started using <a href="https://github.com/thoughtbot/capybara-webkit">capybara-webkit</a> instead of slow <a href="http://seleniumhq.org/">Selenium</a> for integration tests with javascript enabled</p>
</li>
<li>
<p>cleaned up my helpers and moved some logic to decorators (I recommend checking out <a href="https://github.com/amatsuda/active_decorator">active_decorator</a>)</p>
</li>
<li>
<p>hooked up <a href="http://pat.github.com/ts/en/">thinking-sphinx</a> for creating global (site-wide) search engine (works amazing and I truly recommend it for everyone)</p>
</li>
<li>
<p>created a devise&amp;omniauth combo and allowed users to sign-in via google/facebook account</p>
</li>
<li>
<p>build simple xmmp (jabber) bot as a way of informing users about new replies/posts in observed topics on forum (using quite outdated <a href="https://github.com/ln/xmpp4r">xmpp4r</a> library - I will came back to the topic after official release for sure, for now I&rsquo;ll have to be be enough)</p>
</li>
<li>
<p>after a discussion with the editors of current site we have decided it&rsquo;s time to evolve and choose markdown instead of bbcode after all. I&rsquo;ve chosen <a href="https://github.com/vmg/redcarpet">redcarpet</a> - with if bloody fast and really flexible - I&rsquo;ve added some custom tags and support for <a href="http://www.emoji-cheat-sheet.com/">emoji</a>, created a neat editor based on <a href="http://markitup.jaysalvat.com/">markItUp</a>, spiced it a little with some github like preview style and some <a href="https://github.com/ichord/At.js">at.js</a> goodness</p>
</li>
</ul>
<p>So yeah - quite a few things changed indeed and there is more to come. I hope when I will finish this project (and I mean here official release) I will find some time to write about <em>gotchas</em> I&rsquo;ve encountered and some general tips when dealing with legacy application complete upgrade from my points of view :).</p>]]></content:encoded></item><item><title>Dealing with Omniauth redirect_uri_mismatch (invalid port number gotcha)</title><link>https://awesomeprogrammer.com/blog/2012/12/09/dealing-with-omniauth-redirect-uri-mismatch-invalid-port-number-gotcha/</link><pubDate>Sun, 09 Dec 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/12/09/dealing-with-omniauth-redirect-uri-mismatch-invalid-port-number-gotcha/</guid><description><![CDATA[<p>Lately I had some problems with invalid callback url I was getting on my production server (shared host). Omniauth in response included port used internally by nginx (eg. this could be the case eg. when you have varnish in front of you web server) - after browsing the source code of omniauth gem I have found out that you can easily fix if without even touching webserver configuration (and I didn&rsquo;t had access to it anyway :P).</p>
<p>So take a look at <code>strategy.rb</code>, you have there method <code>full_host</code> and as you see below in the first place it tries to fetch it&rsquo;s value from user&rsquo;s configuration.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># omniauth/lib/omniauth/strategy.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">full_host</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">case</span> <span style="color:#66d9ef">OmniAuth</span><span style="color:#f92672">.</span>config<span style="color:#f92672">.</span>full_host
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">when</span> String
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">OmniAuth</span><span style="color:#f92672">.</span>config<span style="color:#f92672">.</span>full_host
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">when</span> <span style="color:#66d9ef">Proc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">OmniAuth</span><span style="color:#f92672">.</span>config<span style="color:#f92672">.</span>full_host<span style="color:#f92672">.</span>call(env)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>      uri <span style="color:#f92672">=</span> <span style="color:#66d9ef">URI</span><span style="color:#f92672">.</span>parse(request<span style="color:#f92672">.</span>url<span style="color:#f92672">.</span>gsub(<span style="color:#e6db74">/\?.*$/</span>,<span style="color:#e6db74">&#39;&#39;</span>))
</span></span><span style="display:flex;"><span>      uri<span style="color:#f92672">.</span>path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;</span>
</span></span><span style="display:flex;"><span>      uri<span style="color:#f92672">.</span>query <span style="color:#f92672">=</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">#sometimes the url is actually showing http inside rails because the other layers (like nginx) have handled the ssl termination.</span>
</span></span><span style="display:flex;"><span>      uri<span style="color:#f92672">.</span>scheme <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;https&#39;</span> <span style="color:#66d9ef">if</span> request<span style="color:#f92672">.</span>ssl?
</span></span><span style="display:flex;"><span>      uri<span style="color:#f92672">.</span>to_s
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>So to fix it you can add to your production environment something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># config/environments/production.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">OmniAuth</span><span style="color:#f92672">.</span>config<span style="color:#f92672">.</span>full_host <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;http://example.com&#34;</span>
</span></span></code></pre></div><p>Yep - that&rsquo;s all :).</p>
]]></description><content:encoded><![CDATA[<p>Lately I had some problems with invalid callback url I was getting on my production server (shared host). Omniauth in response included port used internally by nginx (eg. this could be the case eg. when you have varnish in front of you web server) - after browsing the source code of omniauth gem I have found out that you can easily fix if without even touching webserver configuration (and I didn&rsquo;t had access to it anyway :P).</p>
<p>So take a look at <code>strategy.rb</code>, you have there method <code>full_host</code> and as you see below in the first place it tries to fetch it&rsquo;s value from user&rsquo;s configuration.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># omniauth/lib/omniauth/strategy.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">full_host</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">case</span> <span style="color:#66d9ef">OmniAuth</span><span style="color:#f92672">.</span>config<span style="color:#f92672">.</span>full_host
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">when</span> String
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">OmniAuth</span><span style="color:#f92672">.</span>config<span style="color:#f92672">.</span>full_host
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">when</span> <span style="color:#66d9ef">Proc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">OmniAuth</span><span style="color:#f92672">.</span>config<span style="color:#f92672">.</span>full_host<span style="color:#f92672">.</span>call(env)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>      uri <span style="color:#f92672">=</span> <span style="color:#66d9ef">URI</span><span style="color:#f92672">.</span>parse(request<span style="color:#f92672">.</span>url<span style="color:#f92672">.</span>gsub(<span style="color:#e6db74">/\?.*$/</span>,<span style="color:#e6db74">&#39;&#39;</span>))
</span></span><span style="display:flex;"><span>      uri<span style="color:#f92672">.</span>path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;</span>
</span></span><span style="display:flex;"><span>      uri<span style="color:#f92672">.</span>query <span style="color:#f92672">=</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">#sometimes the url is actually showing http inside rails because the other layers (like nginx) have handled the ssl termination.</span>
</span></span><span style="display:flex;"><span>      uri<span style="color:#f92672">.</span>scheme <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;https&#39;</span> <span style="color:#66d9ef">if</span> request<span style="color:#f92672">.</span>ssl?
</span></span><span style="display:flex;"><span>      uri<span style="color:#f92672">.</span>to_s
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>So to fix it you can add to your production environment something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># config/environments/production.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">OmniAuth</span><span style="color:#f92672">.</span>config<span style="color:#f92672">.</span>full_host <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;http://example.com&#34;</span>
</span></span></code></pre></div><p>Yep - that&rsquo;s all :).</p>
]]></content:encoded></item><item><title>Hook up aspell dictionary into your rails app</title><link>https://awesomeprogrammer.com/blog/2012/11/26/hook-up-aspell-dictionary-into-your-rails-app/</link><pubDate>Mon, 26 Nov 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/11/26/hook-up-aspell-dictionary-into-your-rails-app/</guid><description><![CDATA[<p>How to check spelling in your rails app in three quick steps.</p>
<h4 id="1-install-aspell-doh">1. Install aspell (d&rsquo;oh)</h4>
<p>with desired dictionary/ies.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt-get install aspell aspell-en aspell-pl
</span></span></code></pre></div><h4 id="2-install-ffi-aspell1">2. Install <a href="https://rubygems.org/gems/ffi-aspell">ffi-aspell</a></h4>
<p>(I can&rsquo;t recommend <a href="https://rubygems.org/gems/raspell">respell</a> as it&rsquo;s quite dated and you probably will encounter some annoying utf-8 encoding problems)</p>
<h4 id="3-wake-up">3. Wake up</h4>
<p>your inner grammar-nazi!</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>  speller <span style="color:#f92672">=</span> <span style="color:#66d9ef">FFI</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Aspell</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Speller</span><span style="color:#f92672">.</span>new(<span style="color:#e6db74">&#39;en&#39;</span>, <span style="color:#e6db74">encoding</span>: <span style="color:#e6db74">&#39;utf-8&#39;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># you may be also interested in those two options for starter:</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># speller.set(&#39;ignore-case&#39;, &#39;true&#39;)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># speller.suggestion_mode = &#39;normal&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;I have no ideaz wut&#39;s goign on&#34;</span><span style="color:#f92672">.</span>gsub(<span style="color:#e6db74">/[\w\&#39;]+/</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>word<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>    puts <span style="color:#e6db74">&#34;&#39;</span><span style="color:#e6db74">#{</span>word<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39; seems incorrect, did you mean: &#39;</span><span style="color:#e6db74">#{</span>speller<span style="color:#f92672">.</span>suggestions(word)<span style="color:#f92672">.</span>first<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;?&#34;</span> <span style="color:#66d9ef">unless</span> speller<span style="color:#f92672">.</span>correct?(word)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span></code></pre></div><pre tabindex="0"><code>ideaz&#39; seems incorrect, did you mean: &#39;ideas&#39;?
&#39;wut&#39;s&#39; seems incorrect, did you mean: &#39;Wit&#39;s&#39;?
&#39;goign&#39; seems incorrect, did you mean: &#39;going&#39;?
</code></pre><p>It&rsquo;s a nice feature that you can use eg. for suggesting/validating user&rsquo;s search queries.</p>]]></description><content:encoded><![CDATA[<p>How to check spelling in your rails app in three quick steps.</p>
<h4 id="1-install-aspell-doh">1. Install aspell (d&rsquo;oh)</h4>
<p>with desired dictionary/ies.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt-get install aspell aspell-en aspell-pl
</span></span></code></pre></div><h4 id="2-install-ffi-aspell1">2. Install <a href="https://rubygems.org/gems/ffi-aspell">ffi-aspell</a></h4>
<p>(I can&rsquo;t recommend <a href="https://rubygems.org/gems/raspell">respell</a> as it&rsquo;s quite dated and you probably will encounter some annoying utf-8 encoding problems)</p>
<h4 id="3-wake-up">3. Wake up</h4>
<p>your inner grammar-nazi!</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>  speller <span style="color:#f92672">=</span> <span style="color:#66d9ef">FFI</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Aspell</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Speller</span><span style="color:#f92672">.</span>new(<span style="color:#e6db74">&#39;en&#39;</span>, <span style="color:#e6db74">encoding</span>: <span style="color:#e6db74">&#39;utf-8&#39;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># you may be also interested in those two options for starter:</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># speller.set(&#39;ignore-case&#39;, &#39;true&#39;)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># speller.suggestion_mode = &#39;normal&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;I have no ideaz wut&#39;s goign on&#34;</span><span style="color:#f92672">.</span>gsub(<span style="color:#e6db74">/[\w\&#39;]+/</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>word<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>    puts <span style="color:#e6db74">&#34;&#39;</span><span style="color:#e6db74">#{</span>word<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39; seems incorrect, did you mean: &#39;</span><span style="color:#e6db74">#{</span>speller<span style="color:#f92672">.</span>suggestions(word)<span style="color:#f92672">.</span>first<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;?&#34;</span> <span style="color:#66d9ef">unless</span> speller<span style="color:#f92672">.</span>correct?(word)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span></code></pre></div><pre tabindex="0"><code>ideaz&#39; seems incorrect, did you mean: &#39;ideas&#39;?
&#39;wut&#39;s&#39; seems incorrect, did you mean: &#39;Wit&#39;s&#39;?
&#39;goign&#39; seems incorrect, did you mean: &#39;going&#39;?
</code></pre><p>It&rsquo;s a nice feature that you can use eg. for suggesting/validating user&rsquo;s search queries.</p>]]></content:encoded></item><item><title>Create build system for PHP under Sublime Text 2</title><link>https://awesomeprogrammer.com/blog/2012/11/05/create-build-system-for-php-under-sublime-text-2/</link><pubDate>Mon, 05 Nov 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/11/05/create-build-system-for-php-under-sublime-text-2/</guid><description><![CDATA[<p>Lately I&rsquo;ve been working quite a lot with unit test using <a href="http://www.simpletest.org/">SimpleTest</a> - so I often had to run scripts directly from command line. But wouldn&rsquo;t it be cool if I just could write a test and run it directly under my code editor without even leaving it? And if you using <a href="http://www.sublimetext.com">Sublime Text 2</a> (and I really recommend checking it out - it&rsquo;s gorgeous, it&rsquo;s slim, it&rsquo;s ultra fast and you can do basically everything with it) - you easily can.</p>
<p>How to do it: from main menu choose: <strong>Tools</strong> -&gt; <strong>Build System</strong> -&gt; <strong>New Build System</strong>, new tab should pop up, now paste:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;cmd&#34;</span>: [<span style="color:#e6db74">&#34;php&#34;</span>, <span style="color:#e6db74">&#34;$file&#34;</span>],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;selector&#34;</span>: <span style="color:#e6db74">&#34;source.php&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Here&rsquo;s quick explanation: it will run command <code>php</code> (I assume it&rsquo;s available globally from your system) and pass full path for currently opened file as an argument. <em>Selector</em> on the other hand is useful when you have setup your build system as <em>automatic</em> - it will just choose your new build when you are working with php files.</p>
<p>Now save it in your <em>User</em> directory (I recommend creating subdirectory called <em>build</em>) as <em>php.sublime-build</em>. Open some php file that can be run from command line, hit <em>CTRL+B</em> (default shortcut) and enjoy ;).</p>]]></description><content:encoded><![CDATA[<p>Lately I&rsquo;ve been working quite a lot with unit test using <a href="http://www.simpletest.org/">SimpleTest</a> - so I often had to run scripts directly from command line. But wouldn&rsquo;t it be cool if I just could write a test and run it directly under my code editor without even leaving it? And if you using <a href="http://www.sublimetext.com">Sublime Text 2</a> (and I really recommend checking it out - it&rsquo;s gorgeous, it&rsquo;s slim, it&rsquo;s ultra fast and you can do basically everything with it) - you easily can.</p>
<p>How to do it: from main menu choose: <strong>Tools</strong> -&gt; <strong>Build System</strong> -&gt; <strong>New Build System</strong>, new tab should pop up, now paste:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;cmd&#34;</span>: [<span style="color:#e6db74">&#34;php&#34;</span>, <span style="color:#e6db74">&#34;$file&#34;</span>],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;selector&#34;</span>: <span style="color:#e6db74">&#34;source.php&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Here&rsquo;s quick explanation: it will run command <code>php</code> (I assume it&rsquo;s available globally from your system) and pass full path for currently opened file as an argument. <em>Selector</em> on the other hand is useful when you have setup your build system as <em>automatic</em> - it will just choose your new build when you are working with php files.</p>
<p>Now save it in your <em>User</em> directory (I recommend creating subdirectory called <em>build</em>) as <em>php.sublime-build</em>. Open some php file that can be run from command line, hit <em>CTRL+B</em> (default shortcut) and enjoy ;).</p>]]></content:encoded></item><item><title>Testing private methods with SimpleTest - example</title><link>https://awesomeprogrammer.com/blog/2012/10/30/testing-private-methods-with-simpletest-example/</link><pubDate>Tue, 30 Oct 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/10/30/testing-private-methods-with-simpletest-example/</guid><description><![CDATA[<p><strong>2017 Update note:</strong> before proceeding think if you <em>really</em> want to test underlying implementation of a class in the first place ;-)</p>
<p>Here&rsquo;s quick example how to test private class method with <a href="http://www.simpletest.org/">SimpleTest</a> using <a href="http://www.php.net/manual/en/class.reflectionclass.php">RelectionClass</a> (while you are reading about reflection class I will also point you directly to <a href="http://www.php.net/manual/en/class.reflectionmethod.php">reflection method</a> and what it&rsquo;s all about).</p>
<p>Our class that we want to test with some private method:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">// sample_class.php
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SampleClass</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">myPrivateMethod</span>($arg1) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> $arg1 <span style="color:#f92672">==</span> <span style="color:#ae81ff">7</span>;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#75715e">?&gt;</span><span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>And here&rsquo;s how you test that <code>myPrivateMethod</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">// sample_class_spec.php
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SampleClassTest</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">UnitTestCase</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">testPrivateMethod</span>() {
</span></span><span style="display:flex;"><span>    $reflection_class <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">ReflectionClass</span>(<span style="color:#e6db74">&#34;SampleClass&#34;</span>);
</span></span><span style="display:flex;"><span>    $method <span style="color:#f92672">=</span> $reflection_class<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getMethod</span>(<span style="color:#e6db74">&#34;myPrivateMethod&#34;</span>);
</span></span><span style="display:flex;"><span>    $method<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">setAccessible</span>(<span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    $sample_class <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">SampleClass</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    $result <span style="color:#f92672">=</span> $method<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">invoke</span>($sample_class, <span style="color:#ae81ff">7</span>); <span style="color:#75715e"># here you invoke method with you class object and pass arguments
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">assertEqual</span>($result, <span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#75715e">?&gt;</span><span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div>]]></description><content:encoded><![CDATA[<p><strong>2017 Update note:</strong> before proceeding think if you <em>really</em> want to test underlying implementation of a class in the first place ;-)</p>
<p>Here&rsquo;s quick example how to test private class method with <a href="http://www.simpletest.org/">SimpleTest</a> using <a href="http://www.php.net/manual/en/class.reflectionclass.php">RelectionClass</a> (while you are reading about reflection class I will also point you directly to <a href="http://www.php.net/manual/en/class.reflectionmethod.php">reflection method</a> and what it&rsquo;s all about).</p>
<p>Our class that we want to test with some private method:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">// sample_class.php
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SampleClass</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">myPrivateMethod</span>($arg1) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> $arg1 <span style="color:#f92672">==</span> <span style="color:#ae81ff">7</span>;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#75715e">?&gt;</span><span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>And here&rsquo;s how you test that <code>myPrivateMethod</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">// sample_class_spec.php
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SampleClassTest</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">UnitTestCase</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">testPrivateMethod</span>() {
</span></span><span style="display:flex;"><span>    $reflection_class <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">ReflectionClass</span>(<span style="color:#e6db74">&#34;SampleClass&#34;</span>);
</span></span><span style="display:flex;"><span>    $method <span style="color:#f92672">=</span> $reflection_class<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getMethod</span>(<span style="color:#e6db74">&#34;myPrivateMethod&#34;</span>);
</span></span><span style="display:flex;"><span>    $method<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">setAccessible</span>(<span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    $sample_class <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">SampleClass</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    $result <span style="color:#f92672">=</span> $method<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">invoke</span>($sample_class, <span style="color:#ae81ff">7</span>); <span style="color:#75715e"># here you invoke method with you class object and pass arguments
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">assertEqual</span>($result, <span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#75715e">?&gt;</span><span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div>]]></content:encoded></item><item><title>Super fast deployment with mina - capistrano alternative</title><link>https://awesomeprogrammer.com/blog/2012/10/27/super-fast-deployment-with-mina-capistrano-alternative/</link><pubDate>Sat, 27 Oct 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/10/27/super-fast-deployment-with-mina-capistrano-alternative/</guid><description><![CDATA[<p><a href="https://github.com/nadarei/mina">Mina</a> is an interesting alternative for <a href="https://github.com/capistrano/capistrano/">capistrano</a>. It&rsquo;s really tiny, easy to configure and fast as hell. Overall usage is <a href="http://nadarei.co/mina/">very well documented</a>, so to avoid repetition I&rsquo;ll just describe very specific case of deploying rails apps on a shared server through use of a gateway (or whatever you may call it).</p>
<p>So it happens I keep my personal project on ones of those shared servers - it&rsquo;s just cheaper and I don&rsquo;t have to bother with upgrades, system security etc. - assuming my provider knows what he&rsquo;s doing of course ;). I can normally log through ssh on my <em>shell account</em> and then from that machine I can ssh to yet another machine that hosts my sites.</p>
<h3 id="preparing">Preparing</h3>
<p>I assume you already setup password-less login from your machine to your primary server. Now it depends how you login to your secondary (www) server. In my case I had an system alias that logged me into that server using one command, and that alias was just an ssh command pointing to some ip with use of different public key. So first I had to allow simple <em>ssh www-server</em> to work. And you can easily automatically provide key for some host in your ssh configuration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># ~/.ssh/config</span>
</span></span><span style="display:flex;"><span>Host web-server
</span></span><span style="display:flex;"><span>  IdentityFile /path/to/specific/key
</span></span></code></pre></div><p>After very little configuration you should be able to do: <em>ssh primary-server</em> and from that server <em>ssh web-server</em> without a password.</p>
<h3 id="configuring-mina">Configuring mina</h3>
<p>As I said before - mina has great documentation, so I will just copy my deployment configuration and give you some tips what could go wrong.</p>
<p>Be honest I had some hard time at the beginning, what may seems weird considering what I just said before :P. Ssh&rsquo;ing from one server to another just didn&rsquo;t work as expected and after further investigation it appeared that I got disconnected from web-server after executing first command (the rest of script were executing on shell server or not executing at all - it just hanged)
. What you have to do to fix it is wrap whole executed script in <a href="http://www.techrepublic.com/article/use-heredoc-in-shell-scripts/5733460">heredoc</a>. And that&rsquo;s basically it, except there is a small catch while you are deploying, because there is external script that is loaded just for deployment. It is in <code>mina/data/deploy.sh.erb</code>, so don&rsquo;t bother patching ssh_helper as I did ;-). Script itself loaded in <code>deploy_helpers.rb</code>, so you may want just to modify and include this updated file in your deploy.rb, or you may want fork the project and made your own modifications. Whatever solution you choose it the end you should wrap it all within something like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#e6db74">&lt;&lt;&#39;ENDSSH&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># you custom taks(s), commands or the whole deploy script
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">ENDSSH</span>
</span></span></code></pre></div><p>And here is my (still very fresh - so expect update in the future) deploy file configuration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># config/deploy.rb</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;mina/git&#39;</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;mina/rails&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:domain</span>, <span style="color:#e6db74">&#39;mydomain.net&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:user</span>, <span style="color:#e6db74">&#39;user&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:deploy_to</span>, <span style="color:#e6db74">&#39;/my/path&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:repository</span>, <span style="color:#e6db74">&#39;git@myrepo.git&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:branch</span>, <span style="color:#e6db74">&#39;master&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:current_path</span>, <span style="color:#e6db74">&#39;custom-current-directory&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:shared_paths</span>, <span style="color:#f92672">[</span><span style="color:#e6db74">&#39;.bundle&#39;</span>, <span style="color:#e6db74">&#39;config/email.yml&#39;</span>, <span style="color:#e6db74">&#39;config/database.yml&#39;</span>, <span style="color:#e6db74">&#39;public/system&#39;</span>, <span style="color:#e6db74">&#39;log&#39;</span>, <span style="color:#e6db74">&#39;tmp&#39;</span><span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:ssh_options</span>, <span style="color:#e6db74">&#39;-A -t ssh -A user@www-server.net&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:term_mode</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>task <span style="color:#e6db74">:deploy</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  deploy <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    invoke <span style="color:#e6db74">:&#39;git:clone&#39;</span>
</span></span><span style="display:flex;"><span>    invoke <span style="color:#e6db74">:&#39;deploy:link_shared_paths&#39;</span>
</span></span><span style="display:flex;"><span>    queue <span style="color:#e6db74">&#34;bundle install&#34;</span>
</span></span><span style="display:flex;"><span>    invoke <span style="color:#e6db74">:&#39;rails:db_migrate&#39;</span>
</span></span><span style="display:flex;"><span>    invoke <span style="color:#e6db74">:&#39;rails:assets_precompile&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    to <span style="color:#e6db74">:launch</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>      queue <span style="color:#e6db74">&#39;touch tmp/restart.txt&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Note that I don&rsquo;t use provided bundle:install command – instead I&rsquo;m linking my custom bundler configuration. I&rsquo;m aware this is very specific configuration with some weird workarounds, moreover if you&rsquo;re planning of using whole tasks I just recommend forking the project on github and adjusting it to your needs.</p>]]></description><content:encoded><![CDATA[<p><a href="https://github.com/nadarei/mina">Mina</a> is an interesting alternative for <a href="https://github.com/capistrano/capistrano/">capistrano</a>. It&rsquo;s really tiny, easy to configure and fast as hell. Overall usage is <a href="http://nadarei.co/mina/">very well documented</a>, so to avoid repetition I&rsquo;ll just describe very specific case of deploying rails apps on a shared server through use of a gateway (or whatever you may call it).</p>
<p>So it happens I keep my personal project on ones of those shared servers - it&rsquo;s just cheaper and I don&rsquo;t have to bother with upgrades, system security etc. - assuming my provider knows what he&rsquo;s doing of course ;). I can normally log through ssh on my <em>shell account</em> and then from that machine I can ssh to yet another machine that hosts my sites.</p>
<h3 id="preparing">Preparing</h3>
<p>I assume you already setup password-less login from your machine to your primary server. Now it depends how you login to your secondary (www) server. In my case I had an system alias that logged me into that server using one command, and that alias was just an ssh command pointing to some ip with use of different public key. So first I had to allow simple <em>ssh www-server</em> to work. And you can easily automatically provide key for some host in your ssh configuration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># ~/.ssh/config</span>
</span></span><span style="display:flex;"><span>Host web-server
</span></span><span style="display:flex;"><span>  IdentityFile /path/to/specific/key
</span></span></code></pre></div><p>After very little configuration you should be able to do: <em>ssh primary-server</em> and from that server <em>ssh web-server</em> without a password.</p>
<h3 id="configuring-mina">Configuring mina</h3>
<p>As I said before - mina has great documentation, so I will just copy my deployment configuration and give you some tips what could go wrong.</p>
<p>Be honest I had some hard time at the beginning, what may seems weird considering what I just said before :P. Ssh&rsquo;ing from one server to another just didn&rsquo;t work as expected and after further investigation it appeared that I got disconnected from web-server after executing first command (the rest of script were executing on shell server or not executing at all - it just hanged)
. What you have to do to fix it is wrap whole executed script in <a href="http://www.techrepublic.com/article/use-heredoc-in-shell-scripts/5733460">heredoc</a>. And that&rsquo;s basically it, except there is a small catch while you are deploying, because there is external script that is loaded just for deployment. It is in <code>mina/data/deploy.sh.erb</code>, so don&rsquo;t bother patching ssh_helper as I did ;-). Script itself loaded in <code>deploy_helpers.rb</code>, so you may want just to modify and include this updated file in your deploy.rb, or you may want fork the project and made your own modifications. Whatever solution you choose it the end you should wrap it all within something like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#e6db74">&lt;&lt;&#39;ENDSSH&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># you custom taks(s), commands or the whole deploy script
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">ENDSSH</span>
</span></span></code></pre></div><p>And here is my (still very fresh - so expect update in the future) deploy file configuration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># config/deploy.rb</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;mina/git&#39;</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;mina/rails&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:domain</span>, <span style="color:#e6db74">&#39;mydomain.net&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:user</span>, <span style="color:#e6db74">&#39;user&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:deploy_to</span>, <span style="color:#e6db74">&#39;/my/path&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:repository</span>, <span style="color:#e6db74">&#39;git@myrepo.git&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:branch</span>, <span style="color:#e6db74">&#39;master&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:current_path</span>, <span style="color:#e6db74">&#39;custom-current-directory&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:shared_paths</span>, <span style="color:#f92672">[</span><span style="color:#e6db74">&#39;.bundle&#39;</span>, <span style="color:#e6db74">&#39;config/email.yml&#39;</span>, <span style="color:#e6db74">&#39;config/database.yml&#39;</span>, <span style="color:#e6db74">&#39;public/system&#39;</span>, <span style="color:#e6db74">&#39;log&#39;</span>, <span style="color:#e6db74">&#39;tmp&#39;</span><span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:ssh_options</span>, <span style="color:#e6db74">&#39;-A -t ssh -A user@www-server.net&#39;</span>
</span></span><span style="display:flex;"><span>set <span style="color:#e6db74">:term_mode</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>task <span style="color:#e6db74">:deploy</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  deploy <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    invoke <span style="color:#e6db74">:&#39;git:clone&#39;</span>
</span></span><span style="display:flex;"><span>    invoke <span style="color:#e6db74">:&#39;deploy:link_shared_paths&#39;</span>
</span></span><span style="display:flex;"><span>    queue <span style="color:#e6db74">&#34;bundle install&#34;</span>
</span></span><span style="display:flex;"><span>    invoke <span style="color:#e6db74">:&#39;rails:db_migrate&#39;</span>
</span></span><span style="display:flex;"><span>    invoke <span style="color:#e6db74">:&#39;rails:assets_precompile&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    to <span style="color:#e6db74">:launch</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>      queue <span style="color:#e6db74">&#39;touch tmp/restart.txt&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Note that I don&rsquo;t use provided bundle:install command – instead I&rsquo;m linking my custom bundler configuration. I&rsquo;m aware this is very specific configuration with some weird workarounds, moreover if you&rsquo;re planning of using whole tasks I just recommend forking the project on github and adjusting it to your needs.</p>]]></content:encoded></item><item><title>Winter is coming... so protect your eyes with Redshift</title><link>https://awesomeprogrammer.com/blog/2012/10/23/winter-is-coming-so-protect-your-eyes-with-redshift/</link><pubDate>Tue, 23 Oct 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/10/23/winter-is-coming-so-protect-your-eyes-with-redshift/</guid><description><![CDATA[<p>As they say - winter is coming - and that&rsquo;s mean the day is getting shorter and shorter. Nowadays it&rsquo;s getting dark quite fast out here so maybe it&rsquo;s time to adjust the color temperature and the brightness of your screen accordingly to the time of the day/night? There are two alternatives that I know of that can help you with that:</p>
<ul>
<li>
<p>first one is <a href="http://stereopsis.com/flux/">f.lux</a> - I have used it while I was working under windows for most of the time, unfortunately linux distribution is a little behind and it still didn&rsquo;t get the support for Ubuntu 12.10 and it has some problems with supporting multiple monitors (it may be not true while you read it tho)</p>
</li>
<li>
<p>the second one (that I&rsquo;m currently using) is <a href="http://jonls.dk/redshift/">Redshift</a> - and that piece of software you can install directly from ubuntu repositories with no fuss at all
Just run <code>sudo apt-get install gtk-redshift</code> to install it with simple GTK integration. Here&rsquo;s configuration that I use:</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># ~/.config/redshift.conf</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>redshift<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>location-provider<span style="color:#f92672">=</span>manual
</span></span><span style="display:flex;"><span>adjustment-method<span style="color:#f92672">=</span>vidmode
</span></span><span style="display:flex;"><span>brightness<span style="color:#f92672">=</span>0.88
</span></span><span style="display:flex;"><span>temp-day<span style="color:#f92672">=</span><span style="color:#ae81ff">6500</span>
</span></span><span style="display:flex;"><span>temp-night<span style="color:#f92672">=</span><span style="color:#ae81ff">5000</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>manual<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>lat<span style="color:#f92672">=</span>here.goes
</span></span><span style="display:flex;"><span>lon<span style="color:#f92672">=</span>your.location
</span></span></code></pre></div><p>Other example <a href="http://jonls.dk/2010/10/redshift-1-6-released/">can be found here</a>, feel free to adjust the settings with trial and error method and give your eyes some rest :).</p>]]></description><content:encoded><![CDATA[<p>As they say - winter is coming - and that&rsquo;s mean the day is getting shorter and shorter. Nowadays it&rsquo;s getting dark quite fast out here so maybe it&rsquo;s time to adjust the color temperature and the brightness of your screen accordingly to the time of the day/night? There are two alternatives that I know of that can help you with that:</p>
<ul>
<li>
<p>first one is <a href="http://stereopsis.com/flux/">f.lux</a> - I have used it while I was working under windows for most of the time, unfortunately linux distribution is a little behind and it still didn&rsquo;t get the support for Ubuntu 12.10 and it has some problems with supporting multiple monitors (it may be not true while you read it tho)</p>
</li>
<li>
<p>the second one (that I&rsquo;m currently using) is <a href="http://jonls.dk/redshift/">Redshift</a> - and that piece of software you can install directly from ubuntu repositories with no fuss at all
Just run <code>sudo apt-get install gtk-redshift</code> to install it with simple GTK integration. Here&rsquo;s configuration that I use:</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># ~/.config/redshift.conf</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>redshift<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>location-provider<span style="color:#f92672">=</span>manual
</span></span><span style="display:flex;"><span>adjustment-method<span style="color:#f92672">=</span>vidmode
</span></span><span style="display:flex;"><span>brightness<span style="color:#f92672">=</span>0.88
</span></span><span style="display:flex;"><span>temp-day<span style="color:#f92672">=</span><span style="color:#ae81ff">6500</span>
</span></span><span style="display:flex;"><span>temp-night<span style="color:#f92672">=</span><span style="color:#ae81ff">5000</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>manual<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>lat<span style="color:#f92672">=</span>here.goes
</span></span><span style="display:flex;"><span>lon<span style="color:#f92672">=</span>your.location
</span></span></code></pre></div><p>Other example <a href="http://jonls.dk/2010/10/redshift-1-6-released/">can be found here</a>, feel free to adjust the settings with trial and error method and give your eyes some rest :).</p>]]></content:encoded></item><item><title>Free github alternative with private repos - meet Bitbucket</title><link>https://awesomeprogrammer.com/blog/2012/10/15/free-github-alternative-with-private-repos-meet-bitbucket/</link><pubDate>Mon, 15 Oct 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/10/15/free-github-alternative-with-private-repos-meet-bitbucket/</guid><description><![CDATA[<p>Everyone knows github, right? The github community - that the main power of that site. The amount of open-source projects, drafts, ideas is just overwhelming and nobody can&rsquo;t neglect that.</p>
<p>But what if you have a small team that need some <em>github-like</em> platform for hosting your new top-secret project in private and right now you just don&rsquo;t have few bucks to spare? Well, you can take a look at <a href="https://bitbucket.org/">bitbucket</a> that got totally redesigned last few days, you can read all about all cool features and tweaks <a href="http://blog.bitbucket.org/2012/10/09/introducing-the-redesigned-bitbucket/">here</a>.</p>
<p>You can work there in a team up to 5 people (8, if you invite some friends to register) and I think it&rsquo;s a perfect place to start developing some non open-source great apps. Give it a try!</p>
]]></description><content:encoded><![CDATA[<p>Everyone knows github, right? The github community - that the main power of that site. The amount of open-source projects, drafts, ideas is just overwhelming and nobody can&rsquo;t neglect that.</p>
<p>But what if you have a small team that need some <em>github-like</em> platform for hosting your new top-secret project in private and right now you just don&rsquo;t have few bucks to spare? Well, you can take a look at <a href="https://bitbucket.org/">bitbucket</a> that got totally redesigned last few days, you can read all about all cool features and tweaks <a href="http://blog.bitbucket.org/2012/10/09/introducing-the-redesigned-bitbucket/">here</a>.</p>
<p>You can work there in a team up to 5 people (8, if you invite some friends to register) and I think it&rsquo;s a perfect place to start developing some non open-source great apps. Give it a try!</p>
]]></content:encoded></item><item><title>Breaking Rails, part 2, putting your RoR inside your PHP</title><link>https://awesomeprogrammer.com/blog/2012/10/10/breaking-rails-putting-your-ror-inside-your-php/</link><pubDate>Wed, 10 Oct 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/10/10/breaking-rails-putting-your-ror-inside-your-php/</guid><description><![CDATA[<p><em>Yo dawg, I heard you like&hellip;</em></p>
<p>Purely theoretical (obviously) case: you have existing php app and some legacy rails code that you refreshed a little bit and now someone asks you one simple question - <em>why can&rsquo;t you just put <strong>that</strong> code into <strong>that</strong> code and run it as one app</em>? Sometimes you can reason, sometimes not. But let&rsquo;s do it - I mean seriously - let&rsquo;s create some kind of hybrid that will work transparently and silently for the user. Unleash your imagination and get ready for breaking every programming paradigm; treat it like a sport and get ready :).</p>
<p>Let&rsquo;s ever get a little bit further, let&rsquo;s create one rails app that will distribute its content for two different domains. Where do we start?</p>
<p>I assume you have some kind of working application already, in the end we want to have:</p>
<ul>
<li>domain.com - our main application written in php</li>
<li>domain.com/superapp1 - rails app that is serving some part of content</li>
<li>domain.com/superapp2 - same rails app that is serving another part of content</li>
</ul>
<p>Moreover we want to create this whole structure in a way that it can be ran as separate apps in the future <em>just in case</em>.</p>
<p>First - get familiar with <a href="http://httpd.apache.org/docs/2.2/mod/mod_proxy.html">mod proxy</a> as I&rsquo;ll be using Apache here with with <a href="http://www.modrails.com">phusion passanger</a>. I think the most reasonable approach is to handle two different sub-domains for serving content (plus admin namespace for backend).</p>
<p>So looking at our application it would look like:</p>
<ul>
<li>superapp1.rails.app - for serving content for &lsquo;app1&rsquo;. This url will be not directly accessible on production.</li>
<li>superapp2.rails.app - same as above, for &lsquo;app2&rsquo;</li>
<li>rails.app/admin - handling backend with restricted access</li>
</ul>
<p>Be aware that I can&rsquo;t provide where working example as solutions may vary quite a lot depending on structure of your existing applications and need, but I will give you some ideas if you will need to (theoretically) create that kind of hybrid someday.</p>
<p>Configure phussion passanger for your rails app, don&rsquo;t forget to set <code>PassengerFriendlyErrorPages on</code>  and <code>RailsEnv &quot;development&quot;</code> for now.</p>
<p>Now it&rsquo;s time to start some proxy-action in your php app, setup in your virtual host config:</p>
<pre tabindex="0"><code>ProxyPass /superapp1 http://superapp1.rails.app
ProxyPassReverse /superapp1 http://superapp1.rails.app

ProxyPass /superapp1 http://superapp2.rails.app
ProxyPassReverse /superapp1 http://superapp2.rails.app
</code></pre><p>In your rails routes on the other hand you have to handle those sub-domains, after reading/watching <a href="http://railscasts.com/episodes/221-subdomains-in-rails-3?view=asciicast">this railscasts</a> I ended up with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># config/routes.rb</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">...</span>
</span></span><span style="display:flex;"><span>  constraints(<span style="color:#66d9ef">Subdomain</span>) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># some resources and stuff</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">...</span>
</span></span><span style="display:flex;"><span>  namespace <span style="color:#e6db74">:admin</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># and here some controllers for handling backend</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>and:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># app/lib/subdomain.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Subdomain</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">.</span><span style="color:#a6e22e">matches?</span>(request)
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">%w(superapp1 superapp2)</span><span style="color:#f92672">.</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>subdomain<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span> <span style="color:#66d9ef">if</span> request<span style="color:#f92672">.</span>env<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;SERVER_NAME&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=~</span> <span style="color:#e6db74">/</span><span style="color:#e6db74">#{</span>subdomain<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>After reloading apache cofiguration (<code>service apache2 reload</code>) loading address /superapp1 from php app should bring on your brand new ror app.</p>
<p>Its not especially exciting and you may noticed some serious problems here:</p>
<ul>
<li>
<p>you ended up with broken urls when trying to move deeper inside your proxy-ed app - as you need to provide proper urls</p>
</li>
<li>
<p>your assets are broken because you are working really on subdomain, but that can be easily fixed by adding to your apache conf:
<code>ProxyPass /assets http://rails.app/assets</code></p>
</li>
<li>
<p>how can you serve diffirent part of content with same rails application depeding on those domains?</p>
</li>
<li>
<p>what about log in feature for your users?</p>
</li>
</ul>
<p>Lets stard from fixing those urls, one way is to overload <code>url_for</code> method from <a href="http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html">UrlHelper</a>. I ended up with something like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># app/helpers/url_helper.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">module</span> UrlHelper
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">url_for</span>(options <span style="color:#f92672">=</span> <span style="color:#66d9ef">nil</span>)
</span></span><span style="display:flex;"><span>    options<span style="color:#f92672">[</span><span style="color:#e6db74">:host</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span>request<span style="color:#f92672">.</span>domain<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">#{</span>subdomain_from_server<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#66d9ef">if</span> options<span style="color:#f92672">.</span>kind_of?(<span style="color:#66d9ef">Hash</span>) <span style="color:#f92672">&amp;&amp;</span> <span style="color:#f92672">!</span>subdomain_from_server<span style="color:#f92672">.</span>empty?
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">super</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># app/controllers/application_controllers.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ApplicationController</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActionController</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">UrlHelper</span>
</span></span><span style="display:flex;"><span>  helper_method <span style="color:#e6db74">:subdomain_from_server</span> <span style="color:#75715e"># allow with method for views (UrlHelper)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">protected</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">subdomain_from_server</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">%w(superapp1 superapp2)</span><span style="color:#f92672">.</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>subdomain<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> subdomain <span style="color:#66d9ef">if</span> request<span style="color:#f92672">.</span>env<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;SERVER_NAME&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=~</span> <span style="color:#e6db74">/</span><span style="color:#e6db74">#{</span>subdomain<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span> <span style="color:#75715e"># as it&#39;s proxied we have to get subdomain from server</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Now when you call eg. articles_path or whatever you should end up with pretty proxy-ready url (that will be broken when you run your app as-it obviously).</p>
<p>I decided to filter content just by using <a href="https://github.com/ryanb/cancan">CanCan</a> (once again thank to Ryan) and setup access depending on current sub-domain in <code>ApplicationController</code> (you can use <code>can</code> and <code>cannot</code> on <code>current_ability</code>).</p>
<p>What&rsquo;s left is authentication - how to setup backend access, log-in curret users - and that&rsquo;s whole different subject. I will point three things here:</p>
<ul>
<li>you can work your way with <a href="http://php.net/manual/en/book.memcached.php">php-memcached</a>, <a href="https://github.com/mperham/dalli">dalli</a> and cookies that are shared across your applications</li>
<li>you can share existing database by setting up additional adapter in config/database.yml (then use <a href="http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/ConnectionHandler.html#method-i-establish_connection">establish_connection</a> in your model and build up proper relation(s) onto existing database structure)</li>
<li>in the end you probably would want to lock your domains they are accessible only from internal network(s), rewrite below should do it</li>
</ul>
<pre tabindex="0"><code>RewriteEngine On
RewriteCond %{HTTP_HOST} ^superapp1\.rails\.app$
RewriteCond %{REMOTE_ADDR} !^((1\.2\.3\.4)|(127\.0\.0\.1))$ # allowed address(es)
RewriteRule ^.*$ http://domain.com [R]
</code></pre><p>Well, that&rsquo;s it - very generall idea how it can be done, hope you liked this little piece of dark hackery ;-).</p>]]></description><content:encoded><![CDATA[<p><em>Yo dawg, I heard you like&hellip;</em></p>
<p>Purely theoretical (obviously) case: you have existing php app and some legacy rails code that you refreshed a little bit and now someone asks you one simple question - <em>why can&rsquo;t you just put <strong>that</strong> code into <strong>that</strong> code and run it as one app</em>? Sometimes you can reason, sometimes not. But let&rsquo;s do it - I mean seriously - let&rsquo;s create some kind of hybrid that will work transparently and silently for the user. Unleash your imagination and get ready for breaking every programming paradigm; treat it like a sport and get ready :).</p>
<p>Let&rsquo;s ever get a little bit further, let&rsquo;s create one rails app that will distribute its content for two different domains. Where do we start?</p>
<p>I assume you have some kind of working application already, in the end we want to have:</p>
<ul>
<li>domain.com - our main application written in php</li>
<li>domain.com/superapp1 - rails app that is serving some part of content</li>
<li>domain.com/superapp2 - same rails app that is serving another part of content</li>
</ul>
<p>Moreover we want to create this whole structure in a way that it can be ran as separate apps in the future <em>just in case</em>.</p>
<p>First - get familiar with <a href="http://httpd.apache.org/docs/2.2/mod/mod_proxy.html">mod proxy</a> as I&rsquo;ll be using Apache here with with <a href="http://www.modrails.com">phusion passanger</a>. I think the most reasonable approach is to handle two different sub-domains for serving content (plus admin namespace for backend).</p>
<p>So looking at our application it would look like:</p>
<ul>
<li>superapp1.rails.app - for serving content for &lsquo;app1&rsquo;. This url will be not directly accessible on production.</li>
<li>superapp2.rails.app - same as above, for &lsquo;app2&rsquo;</li>
<li>rails.app/admin - handling backend with restricted access</li>
</ul>
<p>Be aware that I can&rsquo;t provide where working example as solutions may vary quite a lot depending on structure of your existing applications and need, but I will give you some ideas if you will need to (theoretically) create that kind of hybrid someday.</p>
<p>Configure phussion passanger for your rails app, don&rsquo;t forget to set <code>PassengerFriendlyErrorPages on</code>  and <code>RailsEnv &quot;development&quot;</code> for now.</p>
<p>Now it&rsquo;s time to start some proxy-action in your php app, setup in your virtual host config:</p>
<pre tabindex="0"><code>ProxyPass /superapp1 http://superapp1.rails.app
ProxyPassReverse /superapp1 http://superapp1.rails.app

ProxyPass /superapp1 http://superapp2.rails.app
ProxyPassReverse /superapp1 http://superapp2.rails.app
</code></pre><p>In your rails routes on the other hand you have to handle those sub-domains, after reading/watching <a href="http://railscasts.com/episodes/221-subdomains-in-rails-3?view=asciicast">this railscasts</a> I ended up with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># config/routes.rb</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">...</span>
</span></span><span style="display:flex;"><span>  constraints(<span style="color:#66d9ef">Subdomain</span>) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># some resources and stuff</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">...</span>
</span></span><span style="display:flex;"><span>  namespace <span style="color:#e6db74">:admin</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># and here some controllers for handling backend</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>and:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># app/lib/subdomain.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Subdomain</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">.</span><span style="color:#a6e22e">matches?</span>(request)
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">%w(superapp1 superapp2)</span><span style="color:#f92672">.</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>subdomain<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span> <span style="color:#66d9ef">if</span> request<span style="color:#f92672">.</span>env<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;SERVER_NAME&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=~</span> <span style="color:#e6db74">/</span><span style="color:#e6db74">#{</span>subdomain<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>After reloading apache cofiguration (<code>service apache2 reload</code>) loading address /superapp1 from php app should bring on your brand new ror app.</p>
<p>Its not especially exciting and you may noticed some serious problems here:</p>
<ul>
<li>
<p>you ended up with broken urls when trying to move deeper inside your proxy-ed app - as you need to provide proper urls</p>
</li>
<li>
<p>your assets are broken because you are working really on subdomain, but that can be easily fixed by adding to your apache conf:
<code>ProxyPass /assets http://rails.app/assets</code></p>
</li>
<li>
<p>how can you serve diffirent part of content with same rails application depeding on those domains?</p>
</li>
<li>
<p>what about log in feature for your users?</p>
</li>
</ul>
<p>Lets stard from fixing those urls, one way is to overload <code>url_for</code> method from <a href="http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html">UrlHelper</a>. I ended up with something like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># app/helpers/url_helper.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">module</span> UrlHelper
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">url_for</span>(options <span style="color:#f92672">=</span> <span style="color:#66d9ef">nil</span>)
</span></span><span style="display:flex;"><span>    options<span style="color:#f92672">[</span><span style="color:#e6db74">:host</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span>request<span style="color:#f92672">.</span>domain<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">#{</span>subdomain_from_server<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#66d9ef">if</span> options<span style="color:#f92672">.</span>kind_of?(<span style="color:#66d9ef">Hash</span>) <span style="color:#f92672">&amp;&amp;</span> <span style="color:#f92672">!</span>subdomain_from_server<span style="color:#f92672">.</span>empty?
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">super</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># app/controllers/application_controllers.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ApplicationController</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActionController</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">UrlHelper</span>
</span></span><span style="display:flex;"><span>  helper_method <span style="color:#e6db74">:subdomain_from_server</span> <span style="color:#75715e"># allow with method for views (UrlHelper)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">protected</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">subdomain_from_server</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">%w(superapp1 superapp2)</span><span style="color:#f92672">.</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>subdomain<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> subdomain <span style="color:#66d9ef">if</span> request<span style="color:#f92672">.</span>env<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;SERVER_NAME&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=~</span> <span style="color:#e6db74">/</span><span style="color:#e6db74">#{</span>subdomain<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span> <span style="color:#75715e"># as it&#39;s proxied we have to get subdomain from server</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Now when you call eg. articles_path or whatever you should end up with pretty proxy-ready url (that will be broken when you run your app as-it obviously).</p>
<p>I decided to filter content just by using <a href="https://github.com/ryanb/cancan">CanCan</a> (once again thank to Ryan) and setup access depending on current sub-domain in <code>ApplicationController</code> (you can use <code>can</code> and <code>cannot</code> on <code>current_ability</code>).</p>
<p>What&rsquo;s left is authentication - how to setup backend access, log-in curret users - and that&rsquo;s whole different subject. I will point three things here:</p>
<ul>
<li>you can work your way with <a href="http://php.net/manual/en/book.memcached.php">php-memcached</a>, <a href="https://github.com/mperham/dalli">dalli</a> and cookies that are shared across your applications</li>
<li>you can share existing database by setting up additional adapter in config/database.yml (then use <a href="http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/ConnectionHandler.html#method-i-establish_connection">establish_connection</a> in your model and build up proper relation(s) onto existing database structure)</li>
<li>in the end you probably would want to lock your domains they are accessible only from internal network(s), rewrite below should do it</li>
</ul>
<pre tabindex="0"><code>RewriteEngine On
RewriteCond %{HTTP_HOST} ^superapp1\.rails\.app$
RewriteCond %{REMOTE_ADDR} !^((1\.2\.3\.4)|(127\.0\.0\.1))$ # allowed address(es)
RewriteRule ^.*$ http://domain.com [R]
</code></pre><p>Well, that&rsquo;s it - very generall idea how it can be done, hope you liked this little piece of dark hackery ;-).</p>]]></content:encoded></item><item><title>Breaking Rails, hackery that you probably shouldn't do - rendering views in model</title><link>https://awesomeprogrammer.com/blog/2012/10/07/breaking-rails-rendering-views-in-model/</link><pubDate>Sun, 07 Oct 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/10/07/breaking-rails-rendering-views-in-model/</guid><description><![CDATA[<p>You may have some reason to break outside MVC model and I won&rsquo;t give you lecture why you shoudn&rsquo;t really do it, neither I won&rsquo;t describe how you will burn in hell for commiting that kind of sin. Instead I will provide a working solution how it can be done. Just as a example of bad pratice :P.</p>
<p>So – how to render view inside model in Rails 3.2?</p>
<p>Let&rsquo;s start by creating an class that will inherit from <a href="http://api.rubyonrails.org/classes/AbstractController/Base.html">AbstractController::Base</a>. You will also need to include bunch of other classes that are required for rendering views, include url_helper, application helper and any other helpers that you may want to use, don&rsquo;t forget to setup your views path.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># lib/render_abstract.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">RenderAbstract</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">AbstractController</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">AbstractController</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Rendering</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">AbstractController</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Layouts</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">AbstractController</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Helpers</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">AbstractController</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Translation</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">AbstractController</span><span style="color:#f92672">::</span><span style="color:#66d9ef">AssetPaths</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">ActionDispatch</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Routing</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">Rails</span><span style="color:#f92672">.</span>application<span style="color:#f92672">.</span>routes<span style="color:#f92672">.</span>url_helpers
</span></span><span style="display:flex;"><span>  helper <span style="color:#66d9ef">ApplicationHelper</span>
</span></span><span style="display:flex;"><span>  self<span style="color:#f92672">.</span>view_paths <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;app/views&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">show</span>(article)
</span></span><span style="display:flex;"><span>    @article <span style="color:#f92672">=</span> article
</span></span><span style="display:flex;"><span>    render <span style="color:#e6db74">:partial</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;articles/show&#39;</span>, <span style="color:#e6db74">:layout</span><span style="color:#f92672">=&gt;</span><span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p><code>show</code> method will accept object of class <code>Article</code> and setup variable accessible to view in standard fashion. Here&rsquo;s how <code>Article</code> model looks:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># app/models/article.rb</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;render_abstract.rb&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Article</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">render</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">RenderAbstract</span><span style="color:#f92672">.</span>new<span style="color:#f92672">.</span>show(self)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Calling render on article object should now render your view. How about that. Just don&rsquo;t tell anyone ;-).</p>
<p>In next part I will give you some ideas how can you proxy your Rails application inside existing PHP project. The hunt for witches begins now.</p>]]></description><content:encoded><![CDATA[<p>You may have some reason to break outside MVC model and I won&rsquo;t give you lecture why you shoudn&rsquo;t really do it, neither I won&rsquo;t describe how you will burn in hell for commiting that kind of sin. Instead I will provide a working solution how it can be done. Just as a example of bad pratice :P.</p>
<p>So – how to render view inside model in Rails 3.2?</p>
<p>Let&rsquo;s start by creating an class that will inherit from <a href="http://api.rubyonrails.org/classes/AbstractController/Base.html">AbstractController::Base</a>. You will also need to include bunch of other classes that are required for rendering views, include url_helper, application helper and any other helpers that you may want to use, don&rsquo;t forget to setup your views path.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># lib/render_abstract.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">RenderAbstract</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">AbstractController</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">AbstractController</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Rendering</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">AbstractController</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Layouts</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">AbstractController</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Helpers</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">AbstractController</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Translation</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">AbstractController</span><span style="color:#f92672">::</span><span style="color:#66d9ef">AssetPaths</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">ActionDispatch</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Routing</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">include</span> <span style="color:#66d9ef">Rails</span><span style="color:#f92672">.</span>application<span style="color:#f92672">.</span>routes<span style="color:#f92672">.</span>url_helpers
</span></span><span style="display:flex;"><span>  helper <span style="color:#66d9ef">ApplicationHelper</span>
</span></span><span style="display:flex;"><span>  self<span style="color:#f92672">.</span>view_paths <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;app/views&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">show</span>(article)
</span></span><span style="display:flex;"><span>    @article <span style="color:#f92672">=</span> article
</span></span><span style="display:flex;"><span>    render <span style="color:#e6db74">:partial</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;articles/show&#39;</span>, <span style="color:#e6db74">:layout</span><span style="color:#f92672">=&gt;</span><span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p><code>show</code> method will accept object of class <code>Article</code> and setup variable accessible to view in standard fashion. Here&rsquo;s how <code>Article</code> model looks:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># app/models/article.rb</span>
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;render_abstract.rb&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Article</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">render</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">RenderAbstract</span><span style="color:#f92672">.</span>new<span style="color:#f92672">.</span>show(self)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Calling render on article object should now render your view. How about that. Just don&rsquo;t tell anyone ;-).</p>
<p>In next part I will give you some ideas how can you proxy your Rails application inside existing PHP project. The hunt for witches begins now.</p>]]></content:encoded></item><item><title>Open source airbrake alternative</title><link>https://awesomeprogrammer.com/blog/2012/10/03/open-source-airbrake-alternative/</link><pubDate>Wed, 03 Oct 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/10/03/open-source-airbrake-alternative/</guid><description><![CDATA[<p>Monitoring your applications can be tricky, especially when you have to maintain more and more of them, that uses various technologies. So that&rsquo;s why programmers helps other programmers with those tasks and nowadays you can choose from various monitoring services. I was mostly focused on <a href="https://airbrake.io/pages/home">airbrake</a>, but overall it seems a little bit pricey for me. I mean it&rsquo;s a great piece of software that fully supports Ruby on Rails and iOS and there are bunch of addons for PHP, .Net, Java, Python - you name it. But I needed something relatively simple that could allow me to monitor my apps build with RoR and PHP only, and I found shiny piece of code named <a href="https://github.com/errbit/errbit">errbit</a>.</p>
<p>Errbit uses official airbrake gems for contacting with your Rails apps, you can also hook it up with PHP if you want.</p>
<p>It runs on mongo, and I will provide here a short (very overall) quick-start guide for all interested (tested under Ubuntu 12.04 Server).</p>
<p>Let&rsquo;s start by installing latests stable release of MongoDB, as official Ubuntu repositories are a little bit behind:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo su -
</span></span><span style="display:flex;"><span>apt-key adv --keyserver keyserver.ubuntu.com --recv 7F0CEB10
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen&#34;</span> &gt; /etc/apt/sources.list.d/10gen.list
</span></span><span style="display:flex;"><span>apt-get update <span style="color:#f92672">&amp;&amp;</span> apt-get install mongodb-10gen
</span></span></code></pre></div><p>It will add new source list for aptitude, you may want to <a href="http://docs.mongodb.org/manual/tutorial/install-mongodb-on-ubuntu/#configure-mongodb">configure mongo</a> for your needs after installing. Also don&rsquo;t forget to create db and user on your production server - take a peak in the <a href="http://www.mongodb.org/display/DOCS/Security+and+Authentication">manual</a> or simply login to mongo with <code>mongo</code> command and type:</p>
<pre tabindex="0"><code>// select db
use errbit
// give joe read/write access to the errbit database
db.addUser(&#34;joe&#34;, &#34;PasswordOfYourChoice&#34;)
</code></pre><p>Now lets install required packages (if you&rsquo;re not planning using errbit with php you can skip php5-curl).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt-get install libxml2 libxml2-dev libxslt-dev libcurl4-openssl-dev php5-curl
</span></span></code></pre></div><p>I assume you&rsquo;ve already cloned errbit repo, personally I&rsquo;m using <a href="https://rvm.io/integration/passenger/">RVM with passenger</a> for running my apps, so don&rsquo;t forget to setup <em>.ruby-version</em> or <em>.rvmrc</em> file, eg:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>rvm use 1.9.3
</span></span><span style="display:flex;"><span>rvm gemset create errbit
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;1.9.3@errbit&#34;</span> &gt; .ruby-version
</span></span></code></pre></div><p>and don&rsquo;t forget about <em>setup_load_paths.rb</em> file (I&rsquo;ve spent 1 hour debugging passenger and wondering why the hell it&rsquo;s not picking up proper gemset, my bad :P).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># config/setup_load_paths.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#39;MY_RUBY_HOME&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#39;MY_RUBY_HOME&#39;</span><span style="color:#f92672">].</span>include?(<span style="color:#e6db74">&#39;rvm&#39;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">begin</span>
</span></span><span style="display:flex;"><span>    gems_path <span style="color:#f92672">=</span> <span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#39;MY_RUBY_HOME&#39;</span><span style="color:#f92672">].</span>split(<span style="color:#e6db74">/@/</span>)<span style="color:#f92672">[</span><span style="color:#ae81ff">0</span><span style="color:#f92672">].</span>sub(<span style="color:#e6db74">/rubies/</span>,<span style="color:#e6db74">&#39;gems&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#39;GEM_PATH&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span>gems_path<span style="color:#e6db74">}</span><span style="color:#e6db74">:</span><span style="color:#e6db74">#{</span>gems_path<span style="color:#e6db74">}</span><span style="color:#e6db74">@global&#34;</span>
</span></span><span style="display:flex;"><span>    require <span style="color:#e6db74">&#39;rvm&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">RVM</span><span style="color:#f92672">.</span>use_from_path! <span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>dirname(<span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>dirname(__FILE__))
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">rescue</span> <span style="color:#66d9ef">LoadError</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">raise</span> <span style="color:#e6db74">&#34;RVM gem is currently unavailable.&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># If you&#39;re not using Bundler at all, remove lines bellow</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#39;BUNDLE_GEMFILE&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>expand_path(<span style="color:#e6db74">&#39;../Gemfile&#39;</span>, <span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>dirname(__FILE__))
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;bundler/setup&#39;</span>
</span></span></code></pre></div><p>If you are good to go you can <code>bundle install</code> and <code>rake errbit:bootstrap</code>. After that take a look at <code>config.yml</code> and <code>mongoid.yml</code> and configure it for you environment. Basically that&rsquo;s it - you will need to also configure proper deployment, but that&rsquo;s all covered in official documentation.</p>
<p>In you Rails application that you want to monitor add <a href="https://github.com/airbrake/airbrake">airbrake gem</a> to <code>Gemfile</code>, bundle install and create <code>config/initializers/errbit.rb</code> that is provided after adding application in Errbit itself.</p>
<p>You can also hook up PHP apps using <a href="https://github.com/flippa/errbit-php">errbit-php</a>. It&rsquo;s simple as cloning the repo, and including code below in some file that is included every request.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">require_once</span> <span style="color:#e6db74">&#39;errbit-php/lib/Errbit.php&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Errbit</span><span style="color:#f92672">::</span><span style="color:#a6e22e">instance</span>()
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">configure</span>(<span style="color:#66d9ef">array</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;api_key&#39;</span>           <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;YOUR API KEY&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;host&#39;</span>              <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;host&#39;</span>,                               <span style="color:#75715e">// don&#39;t try to specify port here
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#39;port&#39;</span>              <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">80</span>,                                   <span style="color:#75715e">// optional
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#39;secure&#39;</span>            <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">false</span>,                                <span style="color:#75715e">// optional
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#39;project_root&#39;</span>      <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;/your/project/root&#39;</span>,                 <span style="color:#75715e">// optional
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#39;environment_name&#39;</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;production&#39;</span>,                         <span style="color:#75715e">// optional
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#39;params_filters&#39;</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">array</span>(<span style="color:#e6db74">&#39;/password/&#39;</span>, <span style="color:#e6db74">&#39;/card_number/&#39;</span>), <span style="color:#75715e">// optional
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#39;backtrace_filters&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">array</span>(<span style="color:#e6db74">&#39;#/some/long/path#&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;&#39;</span>)      <span style="color:#75715e">// optional
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  ))
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">start</span>();
</span></span></code></pre></div><p>It will handle any exceptions and errors (depending on your php.ini configuration) and curl your errbit instance with proper params if anything bad happens :).</p>]]></description><content:encoded><![CDATA[<p>Monitoring your applications can be tricky, especially when you have to maintain more and more of them, that uses various technologies. So that&rsquo;s why programmers helps other programmers with those tasks and nowadays you can choose from various monitoring services. I was mostly focused on <a href="https://airbrake.io/pages/home">airbrake</a>, but overall it seems a little bit pricey for me. I mean it&rsquo;s a great piece of software that fully supports Ruby on Rails and iOS and there are bunch of addons for PHP, .Net, Java, Python - you name it. But I needed something relatively simple that could allow me to monitor my apps build with RoR and PHP only, and I found shiny piece of code named <a href="https://github.com/errbit/errbit">errbit</a>.</p>
<p>Errbit uses official airbrake gems for contacting with your Rails apps, you can also hook it up with PHP if you want.</p>
<p>It runs on mongo, and I will provide here a short (very overall) quick-start guide for all interested (tested under Ubuntu 12.04 Server).</p>
<p>Let&rsquo;s start by installing latests stable release of MongoDB, as official Ubuntu repositories are a little bit behind:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo su -
</span></span><span style="display:flex;"><span>apt-key adv --keyserver keyserver.ubuntu.com --recv 7F0CEB10
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen&#34;</span> &gt; /etc/apt/sources.list.d/10gen.list
</span></span><span style="display:flex;"><span>apt-get update <span style="color:#f92672">&amp;&amp;</span> apt-get install mongodb-10gen
</span></span></code></pre></div><p>It will add new source list for aptitude, you may want to <a href="http://docs.mongodb.org/manual/tutorial/install-mongodb-on-ubuntu/#configure-mongodb">configure mongo</a> for your needs after installing. Also don&rsquo;t forget to create db and user on your production server - take a peak in the <a href="http://www.mongodb.org/display/DOCS/Security+and+Authentication">manual</a> or simply login to mongo with <code>mongo</code> command and type:</p>
<pre tabindex="0"><code>// select db
use errbit
// give joe read/write access to the errbit database
db.addUser(&#34;joe&#34;, &#34;PasswordOfYourChoice&#34;)
</code></pre><p>Now lets install required packages (if you&rsquo;re not planning using errbit with php you can skip php5-curl).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt-get install libxml2 libxml2-dev libxslt-dev libcurl4-openssl-dev php5-curl
</span></span></code></pre></div><p>I assume you&rsquo;ve already cloned errbit repo, personally I&rsquo;m using <a href="https://rvm.io/integration/passenger/">RVM with passenger</a> for running my apps, so don&rsquo;t forget to setup <em>.ruby-version</em> or <em>.rvmrc</em> file, eg:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>rvm use 1.9.3
</span></span><span style="display:flex;"><span>rvm gemset create errbit
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;1.9.3@errbit&#34;</span> &gt; .ruby-version
</span></span></code></pre></div><p>and don&rsquo;t forget about <em>setup_load_paths.rb</em> file (I&rsquo;ve spent 1 hour debugging passenger and wondering why the hell it&rsquo;s not picking up proper gemset, my bad :P).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># config/setup_load_paths.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#39;MY_RUBY_HOME&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#39;MY_RUBY_HOME&#39;</span><span style="color:#f92672">].</span>include?(<span style="color:#e6db74">&#39;rvm&#39;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">begin</span>
</span></span><span style="display:flex;"><span>    gems_path <span style="color:#f92672">=</span> <span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#39;MY_RUBY_HOME&#39;</span><span style="color:#f92672">].</span>split(<span style="color:#e6db74">/@/</span>)<span style="color:#f92672">[</span><span style="color:#ae81ff">0</span><span style="color:#f92672">].</span>sub(<span style="color:#e6db74">/rubies/</span>,<span style="color:#e6db74">&#39;gems&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#39;GEM_PATH&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span>gems_path<span style="color:#e6db74">}</span><span style="color:#e6db74">:</span><span style="color:#e6db74">#{</span>gems_path<span style="color:#e6db74">}</span><span style="color:#e6db74">@global&#34;</span>
</span></span><span style="display:flex;"><span>    require <span style="color:#e6db74">&#39;rvm&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">RVM</span><span style="color:#f92672">.</span>use_from_path! <span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>dirname(<span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>dirname(__FILE__))
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">rescue</span> <span style="color:#66d9ef">LoadError</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">raise</span> <span style="color:#e6db74">&#34;RVM gem is currently unavailable.&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># If you&#39;re not using Bundler at all, remove lines bellow</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ENV</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#39;BUNDLE_GEMFILE&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>expand_path(<span style="color:#e6db74">&#39;../Gemfile&#39;</span>, <span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>dirname(__FILE__))
</span></span><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;bundler/setup&#39;</span>
</span></span></code></pre></div><p>If you are good to go you can <code>bundle install</code> and <code>rake errbit:bootstrap</code>. After that take a look at <code>config.yml</code> and <code>mongoid.yml</code> and configure it for you environment. Basically that&rsquo;s it - you will need to also configure proper deployment, but that&rsquo;s all covered in official documentation.</p>
<p>In you Rails application that you want to monitor add <a href="https://github.com/airbrake/airbrake">airbrake gem</a> to <code>Gemfile</code>, bundle install and create <code>config/initializers/errbit.rb</code> that is provided after adding application in Errbit itself.</p>
<p>You can also hook up PHP apps using <a href="https://github.com/flippa/errbit-php">errbit-php</a>. It&rsquo;s simple as cloning the repo, and including code below in some file that is included every request.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">require_once</span> <span style="color:#e6db74">&#39;errbit-php/lib/Errbit.php&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Errbit</span><span style="color:#f92672">::</span><span style="color:#a6e22e">instance</span>()
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">configure</span>(<span style="color:#66d9ef">array</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;api_key&#39;</span>           <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;YOUR API KEY&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;host&#39;</span>              <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;host&#39;</span>,                               <span style="color:#75715e">// don&#39;t try to specify port here
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#39;port&#39;</span>              <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">80</span>,                                   <span style="color:#75715e">// optional
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#39;secure&#39;</span>            <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">false</span>,                                <span style="color:#75715e">// optional
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#39;project_root&#39;</span>      <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;/your/project/root&#39;</span>,                 <span style="color:#75715e">// optional
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#39;environment_name&#39;</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;production&#39;</span>,                         <span style="color:#75715e">// optional
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#39;params_filters&#39;</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">array</span>(<span style="color:#e6db74">&#39;/password/&#39;</span>, <span style="color:#e6db74">&#39;/card_number/&#39;</span>), <span style="color:#75715e">// optional
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#39;backtrace_filters&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">array</span>(<span style="color:#e6db74">&#39;#/some/long/path#&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;&#39;</span>)      <span style="color:#75715e">// optional
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  ))
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">start</span>();
</span></span></code></pre></div><p>It will handle any exceptions and errors (depending on your php.ini configuration) and curl your errbit instance with proper params if anything bad happens :).</p>]]></content:encoded></item><item><title>Using Factory Girl in development</title><link>https://awesomeprogrammer.com/blog/2012/09/25/using-factory-girl-in-development/</link><pubDate>Tue, 25 Sep 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/09/25/using-factory-girl-in-development/</guid><description><![CDATA[<p>There is a time when you need to put some random data in your database. Maybe build up pretty frontend or manually test some features. I was using rake tasks that with a little help from <a href="http://faker.rubyforge.org/">Faker</a>, at least until last few days - I needed to upgrade my tasks, but I mean - I had my test factories that were up-to-date, so I thought - why not use them? And so I did ;). Below is sample rake tasks that build a forum structure:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;factory_girl&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>namespace <span style="color:#e6db74">:db</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  desc <span style="color:#e6db74">&#34;Build me a forum!&#34;</span>
</span></span><span style="display:flex;"><span>  task <span style="color:#e6db74">:populate</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">:environment</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># here maybe you want to delete stuff?</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># User.destroy_all or whatever suits your needs</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  require <span style="color:#66d9ef">Rails</span><span style="color:#f92672">.</span>root<span style="color:#f92672">.</span>join(<span style="color:#e6db74">&#39;spec/factories.rb&#39;</span>) <span style="color:#75715e"># Here I keep my factories, it also loads Faker gem</span>
</span></span><span style="display:flex;"><span>  user_pool <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># I&#39;m overriding some FactoryGirl sequences so it doesn&#39;t mess with my validation</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ae81ff">10</span><span style="color:#f92672">.</span>times { user_pool <span style="color:#f92672">&lt;&lt;</span> <span style="color:#66d9ef">FactoryGirl</span><span style="color:#f92672">.</span>create(<span style="color:#e6db74">:user</span>, <span style="color:#e6db74">:login</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">Faker</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Internet</span><span style="color:#f92672">.</span>user_name,<span style="color:#e6db74">:email</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">Faker</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Internet</span><span style="color:#f92672">.</span>email)<span style="color:#f92672">.</span>id }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># This factory for example will also create forum and sections for the forums that I use</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">User</span><span style="color:#f92672">.</span>all<span style="color:#f92672">.</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>user<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">10</span><span style="color:#f92672">.</span>times { <span style="color:#66d9ef">FactoryGirl</span><span style="color:#f92672">.</span>create(<span style="color:#e6db74">:topic</span>, <span style="color:#e6db74">:user</span> <span style="color:#f92672">=&gt;</span> user, <span style="color:#e6db74">:title</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">Faker</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Lorem</span><span style="color:#f92672">.</span>sentence) }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># And this factory just pusts posts for users from pool that I declared before</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">Topic</span><span style="color:#f92672">.</span>all<span style="color:#f92672">.</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>topic<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">5</span><span style="color:#f92672">.</span>times { <span style="color:#66d9ef">FactoryGirl</span><span style="color:#f92672">.</span>create(<span style="color:#e6db74">:post</span>, <span style="color:#e6db74">:postable</span> <span style="color:#f92672">=&gt;</span> topic, <span style="color:#e6db74">:user_id</span> <span style="color:#f92672">=&gt;</span> user_pool<span style="color:#f92672">.</span>sample) }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>I think it&rsquo;s much better approach that building new tasks from scratch. Just keep in mind that you may ovrride some attributes (eg. if you are validating uniqness) and you should be just fine.</p>
]]></description><content:encoded><![CDATA[<p>There is a time when you need to put some random data in your database. Maybe build up pretty frontend or manually test some features. I was using rake tasks that with a little help from <a href="http://faker.rubyforge.org/">Faker</a>, at least until last few days - I needed to upgrade my tasks, but I mean - I had my test factories that were up-to-date, so I thought - why not use them? And so I did ;). Below is sample rake tasks that build a forum structure:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>require <span style="color:#e6db74">&#39;factory_girl&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>namespace <span style="color:#e6db74">:db</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  desc <span style="color:#e6db74">&#34;Build me a forum!&#34;</span>
</span></span><span style="display:flex;"><span>  task <span style="color:#e6db74">:populate</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">:environment</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># here maybe you want to delete stuff?</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># User.destroy_all or whatever suits your needs</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  require <span style="color:#66d9ef">Rails</span><span style="color:#f92672">.</span>root<span style="color:#f92672">.</span>join(<span style="color:#e6db74">&#39;spec/factories.rb&#39;</span>) <span style="color:#75715e"># Here I keep my factories, it also loads Faker gem</span>
</span></span><span style="display:flex;"><span>  user_pool <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># I&#39;m overriding some FactoryGirl sequences so it doesn&#39;t mess with my validation</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ae81ff">10</span><span style="color:#f92672">.</span>times { user_pool <span style="color:#f92672">&lt;&lt;</span> <span style="color:#66d9ef">FactoryGirl</span><span style="color:#f92672">.</span>create(<span style="color:#e6db74">:user</span>, <span style="color:#e6db74">:login</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">Faker</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Internet</span><span style="color:#f92672">.</span>user_name,<span style="color:#e6db74">:email</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">Faker</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Internet</span><span style="color:#f92672">.</span>email)<span style="color:#f92672">.</span>id }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># This factory for example will also create forum and sections for the forums that I use</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">User</span><span style="color:#f92672">.</span>all<span style="color:#f92672">.</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>user<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">10</span><span style="color:#f92672">.</span>times { <span style="color:#66d9ef">FactoryGirl</span><span style="color:#f92672">.</span>create(<span style="color:#e6db74">:topic</span>, <span style="color:#e6db74">:user</span> <span style="color:#f92672">=&gt;</span> user, <span style="color:#e6db74">:title</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">Faker</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Lorem</span><span style="color:#f92672">.</span>sentence) }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># And this factory just pusts posts for users from pool that I declared before</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">Topic</span><span style="color:#f92672">.</span>all<span style="color:#f92672">.</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>topic<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">5</span><span style="color:#f92672">.</span>times { <span style="color:#66d9ef">FactoryGirl</span><span style="color:#f92672">.</span>create(<span style="color:#e6db74">:post</span>, <span style="color:#e6db74">:postable</span> <span style="color:#f92672">=&gt;</span> topic, <span style="color:#e6db74">:user_id</span> <span style="color:#f92672">=&gt;</span> user_pool<span style="color:#f92672">.</span>sample) }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>I think it&rsquo;s much better approach that building new tasks from scratch. Just keep in mind that you may ovrride some attributes (eg. if you are validating uniqness) and you should be just fine.</p>
]]></content:encoded></item><item><title>Fixing GIT https certificate problem (insecure workaround)</title><link>https://awesomeprogrammer.com/blog/2012/09/18/fixing-git-https-certificate-problem/</link><pubDate>Tue, 18 Sep 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/09/18/fixing-git-https-certificate-problem/</guid><description><![CDATA[<p>If you are using a git repository over https protocol you may encounter error:</p>
<p><code>server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none while accessing https://your-git-repo fatal: HTTP request failed</code></p>
<p>What it mean is that probably your hosting provider (that you are hosting your repo on) upgraded their SSL certificate (go and check it out first). Here&rsquo;s recipe for a quick fix:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>export GIT_SSL_NO_VERIFY<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>git push
</span></span><span style="display:flex;"><span>git pull
</span></span><span style="display:flex;"><span>export GIT_SSL_NO_VERIFY<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
</span></span></code></pre></div>]]></description><content:encoded><![CDATA[<p>If you are using a git repository over https protocol you may encounter error:</p>
<p><code>server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none while accessing https://your-git-repo fatal: HTTP request failed</code></p>
<p>What it mean is that probably your hosting provider (that you are hosting your repo on) upgraded their SSL certificate (go and check it out first). Here&rsquo;s recipe for a quick fix:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>export GIT_SSL_NO_VERIFY<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>git push
</span></span><span style="display:flex;"><span>git pull
</span></span><span style="display:flex;"><span>export GIT_SSL_NO_VERIFY<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
</span></span></code></pre></div>]]></content:encoded></item><item><title> Efficient use of PostgreSQL full text search with highlighting using pg_search</title><link>https://awesomeprogrammer.com/blog/2012/09/11/efficient-use-of-postgresql-full-text-search-with-highlighting-using-pg-search/</link><pubDate>Tue, 11 Sep 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/09/11/efficient-use-of-postgresql-full-text-search-with-highlighting-using-pg-search/</guid><description><![CDATA[<p>For some time I was struggling with efficient of <a href="http://www.postgresql.org/docs/9.1/static/textsearch-controls.html">ts_highligh</a> function of postgres using Rails and <a href="https://github.com/Casecommons/pg_search">pg_search</a>. I had bunch of really long articles and I wanted to highlight search results in the content as well in the title with the power of tsquery and polish dictionary that we feed our database.</p>
<p>In the end solutions was quite simple really, maybe not the most elegant one but does the job just right and in the end your will end up still with ActiveRecord::Relation, so you can easily paginate your results with will_paginate or kaminari gem.</p>
<p>So, let&rsquo;s break it down.</p>
<p>First - I was using custom polish <a href="http://www.postgresql.org/docs/current/static/textsearch-dictionaries.html">dictionary</a>, and I already had a trigger in database that recreated my tsvector column every update/insert. So in that case you can declare PgSearch scope like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>pg_search_scope <span style="color:#e6db74">:search_full_text</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">:against</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">:body</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">:using</span>  <span style="color:#f92672">=&gt;</span> { <span style="color:#e6db74">:tsearch</span> <span style="color:#f92672">=&gt;</span>
</span></span><span style="display:flex;"><span>              {
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">:dictionary</span>       <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;polish&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">:tsvector_column</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;fts&#39;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">:prefix</span>           <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>              }
</span></span><span style="display:flex;"><span>            }
</span></span></code></pre></div><p>And basically that works great, except when your try to select ts_highligh with can be painfully slow. So what I did there:</p>
<ul>
<li>
<p>first I monkey patched pg_search for my application - I didn&rsquo;t wanted to select all the columns, just few ones. So in <code>pg_search/lib/pg_search/scope_options.rb</code> I customized <code>apply</code> method</p>
</li>
<li>
<p>then I just used select from select in a simple manner, take a look at example below</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">.</span><span style="color:#a6e22e">search</span>(search_query, page)
</span></span><span style="display:flex;"><span>  body <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ts_headline(&#39;polish&#39;, body, plainto_tsquery(&#39;polish&#39;,&#39;</span><span style="color:#e6db74">#{</span>search_query<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;), &#39;startsel=&#39;&#39;&lt;em class=</span><span style="color:#ae81ff">\&#34;</span><span style="color:#e6db74">headline</span><span style="color:#ae81ff">\&#34;</span><span style="color:#e6db74">&gt;&#39;&#39; stopsel=&lt;/em&gt;, MaxFragments=2, maxwords=15, minwords=5&#39;) as body&#34;</span>
</span></span><span style="display:flex;"><span>  self<span style="color:#f92672">.</span>select<span style="color:#f92672">[</span><span style="color:#e6db74">:id</span>, body<span style="color:#f92672">].</span>from(<span style="color:#e6db74">&#34;(</span><span style="color:#e6db74">#{</span>self<span style="color:#f92672">.</span>search_full_text(search_query)<span style="color:#f92672">.</span>paginate(<span style="color:#e6db74">:page</span> <span style="color:#f92672">=&gt;</span> page)<span style="color:#f92672">.</span>to_sql<span style="color:#e6db74">}</span><span style="color:#e6db74">) as result&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>As you see I&rsquo;m selecting id column and my highlighted body from paginated results (using will_paginate here). In the view you can display pagination over <code>search_full_text(search_query).paginate(:page =&gt; page)</code>. Refactor it as you please. Right now it seems like the best solution I could think of - if you have better idea let me know!</p>]]></description><content:encoded><![CDATA[<p>For some time I was struggling with efficient of <a href="http://www.postgresql.org/docs/9.1/static/textsearch-controls.html">ts_highligh</a> function of postgres using Rails and <a href="https://github.com/Casecommons/pg_search">pg_search</a>. I had bunch of really long articles and I wanted to highlight search results in the content as well in the title with the power of tsquery and polish dictionary that we feed our database.</p>
<p>In the end solutions was quite simple really, maybe not the most elegant one but does the job just right and in the end your will end up still with ActiveRecord::Relation, so you can easily paginate your results with will_paginate or kaminari gem.</p>
<p>So, let&rsquo;s break it down.</p>
<p>First - I was using custom polish <a href="http://www.postgresql.org/docs/current/static/textsearch-dictionaries.html">dictionary</a>, and I already had a trigger in database that recreated my tsvector column every update/insert. So in that case you can declare PgSearch scope like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>pg_search_scope <span style="color:#e6db74">:search_full_text</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">:against</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">:body</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">:using</span>  <span style="color:#f92672">=&gt;</span> { <span style="color:#e6db74">:tsearch</span> <span style="color:#f92672">=&gt;</span>
</span></span><span style="display:flex;"><span>              {
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">:dictionary</span>       <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;polish&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">:tsvector_column</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;fts&#39;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">:prefix</span>           <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>              }
</span></span><span style="display:flex;"><span>            }
</span></span></code></pre></div><p>And basically that works great, except when your try to select ts_highligh with can be painfully slow. So what I did there:</p>
<ul>
<li>
<p>first I monkey patched pg_search for my application - I didn&rsquo;t wanted to select all the columns, just few ones. So in <code>pg_search/lib/pg_search/scope_options.rb</code> I customized <code>apply</code> method</p>
</li>
<li>
<p>then I just used select from select in a simple manner, take a look at example below</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">.</span><span style="color:#a6e22e">search</span>(search_query, page)
</span></span><span style="display:flex;"><span>  body <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ts_headline(&#39;polish&#39;, body, plainto_tsquery(&#39;polish&#39;,&#39;</span><span style="color:#e6db74">#{</span>search_query<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;), &#39;startsel=&#39;&#39;&lt;em class=</span><span style="color:#ae81ff">\&#34;</span><span style="color:#e6db74">headline</span><span style="color:#ae81ff">\&#34;</span><span style="color:#e6db74">&gt;&#39;&#39; stopsel=&lt;/em&gt;, MaxFragments=2, maxwords=15, minwords=5&#39;) as body&#34;</span>
</span></span><span style="display:flex;"><span>  self<span style="color:#f92672">.</span>select<span style="color:#f92672">[</span><span style="color:#e6db74">:id</span>, body<span style="color:#f92672">].</span>from(<span style="color:#e6db74">&#34;(</span><span style="color:#e6db74">#{</span>self<span style="color:#f92672">.</span>search_full_text(search_query)<span style="color:#f92672">.</span>paginate(<span style="color:#e6db74">:page</span> <span style="color:#f92672">=&gt;</span> page)<span style="color:#f92672">.</span>to_sql<span style="color:#e6db74">}</span><span style="color:#e6db74">) as result&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>As you see I&rsquo;m selecting id column and my highlighted body from paginated results (using will_paginate here). In the view you can display pagination over <code>search_full_text(search_query).paginate(:page =&gt; page)</code>. Refactor it as you please. Right now it seems like the best solution I could think of - if you have better idea let me know!</p>]]></content:encoded></item><item><title>Batch ERB to HAML conversion</title><link>https://awesomeprogrammer.com/blog/2012/09/03/batch-erb-to-haml-conversion/</link><pubDate>Mon, 03 Sep 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/09/03/batch-erb-to-haml-conversion/</guid><description><![CDATA[<p>Quick and useful code snippet that you may want to use eg. while upgrading some old Rails projects. That HAML awesomeness you should use, Luke!</p>
<p>Depending on your configuration (I&rsquo;m using RVM for example) you may want to add those gems to your Gemfile:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>gem <span style="color:#e6db74">&#39;haml&#39;</span> <span style="color:#75715e"># obviously</span>
</span></span><span style="display:flex;"><span>gem <span style="color:#e6db74">&#39;hpricot&#39;</span>
</span></span><span style="display:flex;"><span>gem <span style="color:#e6db74">&#39;ruby_parser&#39;</span>
</span></span></code></pre></div><p>Here&rsquo;s the code:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># erb_to_haml.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Dir</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span><span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>join(<span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>dirname(__FILE__), <span style="color:#e6db74">&#39;app&#39;</span>, <span style="color:#e6db74">&#39;views&#39;</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">/**/*.erb&#34;</span><span style="color:#f92672">].</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>file<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  puts <span style="color:#e6db74">&#34;Working on: </span><span style="color:#e6db74">#{</span>file<span style="color:#e6db74">}</span><span style="color:#e6db74"> ...&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">`html2haml -rx </span><span style="color:#e6db74">#{</span>file<span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">#{</span>file<span style="color:#f92672">.</span>gsub(<span style="color:#e6db74">/\.erb$/</span>, <span style="color:#e6db74">&#39;.haml&#39;</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Description of html2haml options:</p>
<p><code>-e, --erb</code>
Parse ERb tags.</p>
<p><code>-x, --xhtml</code>
Parse the input using the more strict XHTML parser.</p>
<p>Drop this file into your rails root directory and just run it. You may want to review the code of you new HAML files and delete those old nasty erbs. Have fun!</p>]]></description><content:encoded><![CDATA[<p>Quick and useful code snippet that you may want to use eg. while upgrading some old Rails projects. That HAML awesomeness you should use, Luke!</p>
<p>Depending on your configuration (I&rsquo;m using RVM for example) you may want to add those gems to your Gemfile:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>gem <span style="color:#e6db74">&#39;haml&#39;</span> <span style="color:#75715e"># obviously</span>
</span></span><span style="display:flex;"><span>gem <span style="color:#e6db74">&#39;hpricot&#39;</span>
</span></span><span style="display:flex;"><span>gem <span style="color:#e6db74">&#39;ruby_parser&#39;</span>
</span></span></code></pre></div><p>Here&rsquo;s the code:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#75715e"># erb_to_haml.rb</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Dir</span><span style="color:#f92672">[</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">#{</span><span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>join(<span style="color:#66d9ef">File</span><span style="color:#f92672">.</span>dirname(__FILE__), <span style="color:#e6db74">&#39;app&#39;</span>, <span style="color:#e6db74">&#39;views&#39;</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">/**/*.erb&#34;</span><span style="color:#f92672">].</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>file<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  puts <span style="color:#e6db74">&#34;Working on: </span><span style="color:#e6db74">#{</span>file<span style="color:#e6db74">}</span><span style="color:#e6db74"> ...&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">`html2haml -rx </span><span style="color:#e6db74">#{</span>file<span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">#{</span>file<span style="color:#f92672">.</span>gsub(<span style="color:#e6db74">/\.erb$/</span>, <span style="color:#e6db74">&#39;.haml&#39;</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Description of html2haml options:</p>
<p><code>-e, --erb</code>
Parse ERb tags.</p>
<p><code>-x, --xhtml</code>
Parse the input using the more strict XHTML parser.</p>
<p>Drop this file into your rails root directory and just run it. You may want to review the code of you new HAML files and delete those old nasty erbs. Have fun!</p>]]></content:encoded></item><item><title>Setting up Ubuntu 12.04 Web Server</title><link>https://awesomeprogrammer.com/blog/2012/09/01/setting-up-ubuntu-12-dot-04-web-server/</link><pubDate>Sat, 01 Sep 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/09/01/setting-up-ubuntu-12-dot-04-web-server/</guid><description><![CDATA[<p>Last week I&rsquo;ve got a brand new server to play with ;). The mission was clear (<em>I&rsquo;m going over there, I&rsquo;m going to do the mission&hellip;</em>) - upgrade, install all essential stuff, prepare and migrate the data from the old machine. So here&rsquo;s what I&rsquo;ve done so far (except setting up system accounts, ssh access and all that basic stuff).</p>
<ul>
<li>
<p>installed fresh Apache &amp; PHP from ubuntu repositories - I&rsquo;m aware it&rsquo;s not 100% up to date, but - it&rsquo;s stable, and I didn&rsquo;t want to go as war as PHP 5.4 as it&rsquo;s breaks a lot of stuff with existing applications (and I didn&rsquo;t had time to fix it all on my own), also lack of Suhosin patch for 5.4 is a little bit disappointing (well - it&rsquo;s available but it&rsquo;s still a development version)</p>
</li>
<li>
<p><a href="http://httpd.apache.org/docs/2.2/programs/rotatelogs.html">rotated logs in apache</a> for most sites, as they can get out of control sometimes and cause slowdowns</p>
</li>
<li>
<p>moved some of simple cronjobs tasks that involved only some mysql deletes/updates into <a href="http://dev.mysql.com/doc/refman/5.1/en/events.html">MySQL events</a></p>
</li>
<li>
<p>installed RVM with Ruby 1.9.3 (with standalone passanger that handled legacy 1.8.7/Rails2 apps - with weird dependencies, hacked gems and lack of tests - trust me, it wasn&rsquo;t pleasant ride). Tip: remember to enable and configure all essential proxy modules under apache, and keep an eye on your gemsets so you don&rsquo;t mix it during the process.</p>
</li>
<li>
<p>updated some database backup scripts</p>
</li>
<li>
<p>configured monit to make sure that everything is running smoothly and without interruptions. Monit is a great (and free!) tool by the way - be sure to <a href="http://mmonit.com/monit/">check it out</a>. You can also buy a license for M/Monit which is monit on steroids.</p>
</li>
<li>
<p>and here quick way to dump MySQL tables that you need from old server to the new one:</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mysqldump --add-drop-database --add-drop-table --extended-insert --quick --create-options --log-error<span style="color:#f92672">=</span>dump_error.log --user<span style="color:#f92672">=</span>USER --password<span style="color:#f92672">=</span>PASSWORD --databases database1 database2 database3 | pv | ssh -C login@newserver <span style="color:#e6db74">&#34;mysql --user=USER --password=PASSWORD&#34;</span>
</span></span></code></pre></div><p>You execute this command on old server, pipe it to <code>pv</code> (for some neat progress bar) and then to the new server. Here description of options I decided to go with (tweak it as you please for your needs):</p>
<p><code>--add-drop-table</code>
Add a DROP TABLE statement before each CREATE TABLE statement.</p>
<p><code>--add-drop-database</code>
Add a DROP DATABASE statement before each CREATE DATABASE statement. This option is typically used in conjunction with the &ndash;all-databases or &ndash;databases option because no CREATE DATABASE statements are written unless one of those options is specified.</p>
<p><code>--databases</code>
Dump several databases. Normally, mysqldump treats the first name argument on the command line as a database name and following names as table names. With this option, it treats all name arguments as database names. CREATE DATABASE and USE statements are included in the output before each new database.</p>
<p><code>--extended-insert</code>
Use multiple-row INSERT syntax that include several VALUES lists. This results in a smaller dump file and speeds up inserts when the file is reloaded.</p>
<p><code>--log-error=file_name</code>
Log warnings and errors by appending them to the named file. The default is to do no logging.</p>
<p><code>--quick</code>
This option is useful for dumping large tables. It forces mysqldump to retrieve rows for a table from the server a row at a time rather than retrieving the entire row set and buffering it in memory before writing it out (this may be already set as default in your my.cfg file).</p>
<p><code>--create-options</code>
Include all MySQL-specific table options in the CREATE TABLE statements.</p>]]></description><content:encoded><![CDATA[<p>Last week I&rsquo;ve got a brand new server to play with ;). The mission was clear (<em>I&rsquo;m going over there, I&rsquo;m going to do the mission&hellip;</em>) - upgrade, install all essential stuff, prepare and migrate the data from the old machine. So here&rsquo;s what I&rsquo;ve done so far (except setting up system accounts, ssh access and all that basic stuff).</p>
<ul>
<li>
<p>installed fresh Apache &amp; PHP from ubuntu repositories - I&rsquo;m aware it&rsquo;s not 100% up to date, but - it&rsquo;s stable, and I didn&rsquo;t want to go as war as PHP 5.4 as it&rsquo;s breaks a lot of stuff with existing applications (and I didn&rsquo;t had time to fix it all on my own), also lack of Suhosin patch for 5.4 is a little bit disappointing (well - it&rsquo;s available but it&rsquo;s still a development version)</p>
</li>
<li>
<p><a href="http://httpd.apache.org/docs/2.2/programs/rotatelogs.html">rotated logs in apache</a> for most sites, as they can get out of control sometimes and cause slowdowns</p>
</li>
<li>
<p>moved some of simple cronjobs tasks that involved only some mysql deletes/updates into <a href="http://dev.mysql.com/doc/refman/5.1/en/events.html">MySQL events</a></p>
</li>
<li>
<p>installed RVM with Ruby 1.9.3 (with standalone passanger that handled legacy 1.8.7/Rails2 apps - with weird dependencies, hacked gems and lack of tests - trust me, it wasn&rsquo;t pleasant ride). Tip: remember to enable and configure all essential proxy modules under apache, and keep an eye on your gemsets so you don&rsquo;t mix it during the process.</p>
</li>
<li>
<p>updated some database backup scripts</p>
</li>
<li>
<p>configured monit to make sure that everything is running smoothly and without interruptions. Monit is a great (and free!) tool by the way - be sure to <a href="http://mmonit.com/monit/">check it out</a>. You can also buy a license for M/Monit which is monit on steroids.</p>
</li>
<li>
<p>and here quick way to dump MySQL tables that you need from old server to the new one:</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mysqldump --add-drop-database --add-drop-table --extended-insert --quick --create-options --log-error<span style="color:#f92672">=</span>dump_error.log --user<span style="color:#f92672">=</span>USER --password<span style="color:#f92672">=</span>PASSWORD --databases database1 database2 database3 | pv | ssh -C login@newserver <span style="color:#e6db74">&#34;mysql --user=USER --password=PASSWORD&#34;</span>
</span></span></code></pre></div><p>You execute this command on old server, pipe it to <code>pv</code> (for some neat progress bar) and then to the new server. Here description of options I decided to go with (tweak it as you please for your needs):</p>
<p><code>--add-drop-table</code>
Add a DROP TABLE statement before each CREATE TABLE statement.</p>
<p><code>--add-drop-database</code>
Add a DROP DATABASE statement before each CREATE DATABASE statement. This option is typically used in conjunction with the &ndash;all-databases or &ndash;databases option because no CREATE DATABASE statements are written unless one of those options is specified.</p>
<p><code>--databases</code>
Dump several databases. Normally, mysqldump treats the first name argument on the command line as a database name and following names as table names. With this option, it treats all name arguments as database names. CREATE DATABASE and USE statements are included in the output before each new database.</p>
<p><code>--extended-insert</code>
Use multiple-row INSERT syntax that include several VALUES lists. This results in a smaller dump file and speeds up inserts when the file is reloaded.</p>
<p><code>--log-error=file_name</code>
Log warnings and errors by appending them to the named file. The default is to do no logging.</p>
<p><code>--quick</code>
This option is useful for dumping large tables. It forces mysqldump to retrieve rows for a table from the server a row at a time rather than retrieving the entire row set and buffering it in memory before writing it out (this may be already set as default in your my.cfg file).</p>
<p><code>--create-options</code>
Include all MySQL-specific table options in the CREATE TABLE statements.</p>]]></content:encoded></item><item><title>Stubbing paperclip file upload with rspec</title><link>https://awesomeprogrammer.com/blog/2012/08/24/stubbing-paperclip-file-upload-with-rspec/</link><pubDate>Fri, 24 Aug 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/08/24/stubbing-paperclip-file-upload-with-rspec/</guid><description><![CDATA[<p>Here&rsquo;s small chunk of code that will help you out with testing paperclip. Basically it will stub any fie upload of any instance for given model. So now you won&rsquo;t be left with ridiculous amount of files created after each test run. Yay!</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">module</span> PaperclipMacros
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">stub_paperclip</span>(model)
</span></span><span style="display:flex;"><span>    before <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>      model<span style="color:#f92672">.</span>any_instance<span style="color:#f92672">.</span>stub(<span style="color:#e6db74">:save_attached_files</span>)<span style="color:#f92672">.</span>and_return(<span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>      model<span style="color:#f92672">.</span>any_instance<span style="color:#f92672">.</span>stub(<span style="color:#e6db74">:delete_attached_files</span>)<span style="color:#f92672">.</span>and_return(<span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">Paperclip</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Attachment</span><span style="color:#f92672">.</span>any_instance<span style="color:#f92672">.</span>stub(<span style="color:#e6db74">:post_process</span>)<span style="color:#f92672">.</span>and_return(<span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Put that file somehere under <code>spec/support/</code> and in your <code>spec_helper.rb</code> include it like using <code>config.extend PaperclipMacros</code>. Now later in your test (of models or controllers, whatever) you can call it like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>describe <span style="color:#66d9ef">Download</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  stub_paperclip(<span style="color:#66d9ef">Download</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">...</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div>]]></description><content:encoded><![CDATA[<p>Here&rsquo;s small chunk of code that will help you out with testing paperclip. Basically it will stub any fie upload of any instance for given model. So now you won&rsquo;t be left with ridiculous amount of files created after each test run. Yay!</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">module</span> PaperclipMacros
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">stub_paperclip</span>(model)
</span></span><span style="display:flex;"><span>    before <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>      model<span style="color:#f92672">.</span>any_instance<span style="color:#f92672">.</span>stub(<span style="color:#e6db74">:save_attached_files</span>)<span style="color:#f92672">.</span>and_return(<span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>      model<span style="color:#f92672">.</span>any_instance<span style="color:#f92672">.</span>stub(<span style="color:#e6db74">:delete_attached_files</span>)<span style="color:#f92672">.</span>and_return(<span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">Paperclip</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Attachment</span><span style="color:#f92672">.</span>any_instance<span style="color:#f92672">.</span>stub(<span style="color:#e6db74">:post_process</span>)<span style="color:#f92672">.</span>and_return(<span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Put that file somehere under <code>spec/support/</code> and in your <code>spec_helper.rb</code> include it like using <code>config.extend PaperclipMacros</code>. Now later in your test (of models or controllers, whatever) you can call it like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>describe <span style="color:#66d9ef">Download</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  stub_paperclip(<span style="color:#66d9ef">Download</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">...</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div>]]></content:encoded></item><item><title>PHP Code Quality / refactoring messy code tips</title><link>https://awesomeprogrammer.com/blog/2012/08/11/php-code-quality-slash-refactoring-messy-code-tips/</link><pubDate>Sat, 11 Aug 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/08/11/php-code-quality-slash-refactoring-messy-code-tips/</guid><description><![CDATA[<p>Lately I have been adding new and new features to existing application and now came time when I have to play with some seriously old code. I mean - really old, forgotten and never refactored code. It&rsquo;s just there, it&rsquo;s working and nobody dares touching it. So before I started I need to prepare myself for the battle.</p>
<ul>
<li>
<p>if you are in need of refactoring some critical part of the system the big chance is that this critical part doesn&rsquo;t came with any tests (life, so to say). There is also a big chance that the code is basically impossible to test with unit testing - write some integration tests instead. As they say - any kind of test are better that none tests. You can use anything that works for you (for example BDD tool like <a href="http://behat.org/">behat</a>).</p>
</li>
<li>
<p>install <a href="http://pear.php.net/package/PHP_CodeSniffer">PHP_CodeSniffer</a>, <a href="http://phpmd.org/">PHP Mess Detector</a>, <a href="https://github.com/sebastianbergmann/phpcpd/">PHP Copy/Paste Detector</a> and <a href="http://pdepend.org/">PHP_Depend</a> (phpmd requires it so probably at this point you&rsquo;ll have it installed anyway) - these tools will be very useful - trust me</p>
</li>
<li>
<p>you can integrate your tests/code checks with <a href="http://phpundercontrol.org/">phpUnderControl</a> that is extending <a href="http://cruisecontrol.sourceforge.net/">CruiseControl</a> - prepare your <em>build</em> file according to manual (you may want to exclude some directories for each tool, also if you code base is old I would stick with PSR-1 for the start as the code standard because PSR-2 check will likely to kill you on sight)</p>
</li>
</ul>
<p>Bonus: some interesting results of <em>phpmd</em> ran just for two files :) (methods/class names blanked).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">...</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">The</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">some_class</span> <span style="color:#a6e22e">has</span> <span style="color:#ae81ff">41</span> <span style="color:#a6e22e">methods</span><span style="color:#f92672">.</span> <span style="color:#a6e22e">Consider</span> <span style="color:#a6e22e">refactoring</span> <span style="color:#a6e22e">user_art</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">keep</span> <span style="color:#a6e22e">number</span> <span style="color:#a6e22e">of</span> <span style="color:#a6e22e">methods</span> <span style="color:#a6e22e">under</span> <span style="color:#ae81ff">10.</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">The</span> <span style="color:#a6e22e">method</span> <span style="color:#a6e22e">someMethod</span>() <span style="color:#a6e22e">has</span> <span style="color:#a6e22e">an</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">of</span> <span style="color:#ae81ff">15211861552761400491758084218783915899329999864762835394560.</span> <span style="color:#a6e22e">The</span> <span style="color:#a6e22e">configured</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">threshold</span> <span style="color:#a6e22e">is</span> <span style="color:#ae81ff">200.</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">The</span> <span style="color:#a6e22e">method</span> <span style="color:#a6e22e">someMethod2</span>() <span style="color:#a6e22e">has</span> <span style="color:#a6e22e">an</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">of</span> <span style="color:#ae81ff">900000.</span> <span style="color:#a6e22e">The</span> <span style="color:#a6e22e">configured</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">threshold</span> <span style="color:#a6e22e">is</span> <span style="color:#ae81ff">200.</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">The</span> <span style="color:#a6e22e">method</span> <span style="color:#a6e22e">someMethod3</span>() <span style="color:#a6e22e">has</span> <span style="color:#a6e22e">an</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">of</span> <span style="color:#ae81ff">119430531454390553552.</span> <span style="color:#a6e22e">The</span> <span style="color:#a6e22e">configured</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">threshold</span> <span style="color:#a6e22e">is</span> <span style="color:#ae81ff">200.</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">...</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">The</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">some_class2</span> <span style="color:#a6e22e">has</span> <span style="color:#ae81ff">3442</span> <span style="color:#a6e22e">lines</span> <span style="color:#a6e22e">of</span> <span style="color:#a6e22e">code</span><span style="color:#f92672">.</span> <span style="color:#a6e22e">Current</span> <span style="color:#a6e22e">threshold</span> <span style="color:#a6e22e">is</span> <span style="color:#ae81ff">1000.</span> <span style="color:#a6e22e">Avoid</span> <span style="color:#a6e22e">really</span> <span style="color:#a6e22e">long</span> <span style="color:#a6e22e">classes</span><span style="color:#f92672">.</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">The</span> <span style="color:#a6e22e">method</span> <span style="color:#a6e22e">someMethod</span>() <span style="color:#a6e22e">has</span> <span style="color:#a6e22e">an</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">of</span> <span style="color:#ae81ff">151619238808128.</span> <span style="color:#a6e22e">The</span> <span style="color:#a6e22e">configured</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">threshold</span> <span style="color:#a6e22e">is</span> <span style="color:#ae81ff">200.</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">...</span>
</span></span></code></pre></div><p>Happy refactoring!</p>]]></description><content:encoded><![CDATA[<p>Lately I have been adding new and new features to existing application and now came time when I have to play with some seriously old code. I mean - really old, forgotten and never refactored code. It&rsquo;s just there, it&rsquo;s working and nobody dares touching it. So before I started I need to prepare myself for the battle.</p>
<ul>
<li>
<p>if you are in need of refactoring some critical part of the system the big chance is that this critical part doesn&rsquo;t came with any tests (life, so to say). There is also a big chance that the code is basically impossible to test with unit testing - write some integration tests instead. As they say - any kind of test are better that none tests. You can use anything that works for you (for example BDD tool like <a href="http://behat.org/">behat</a>).</p>
</li>
<li>
<p>install <a href="http://pear.php.net/package/PHP_CodeSniffer">PHP_CodeSniffer</a>, <a href="http://phpmd.org/">PHP Mess Detector</a>, <a href="https://github.com/sebastianbergmann/phpcpd/">PHP Copy/Paste Detector</a> and <a href="http://pdepend.org/">PHP_Depend</a> (phpmd requires it so probably at this point you&rsquo;ll have it installed anyway) - these tools will be very useful - trust me</p>
</li>
<li>
<p>you can integrate your tests/code checks with <a href="http://phpundercontrol.org/">phpUnderControl</a> that is extending <a href="http://cruisecontrol.sourceforge.net/">CruiseControl</a> - prepare your <em>build</em> file according to manual (you may want to exclude some directories for each tool, also if you code base is old I would stick with PSR-1 for the start as the code standard because PSR-2 check will likely to kill you on sight)</p>
</li>
</ul>
<p>Bonus: some interesting results of <em>phpmd</em> ran just for two files :) (methods/class names blanked).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">...</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">The</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">some_class</span> <span style="color:#a6e22e">has</span> <span style="color:#ae81ff">41</span> <span style="color:#a6e22e">methods</span><span style="color:#f92672">.</span> <span style="color:#a6e22e">Consider</span> <span style="color:#a6e22e">refactoring</span> <span style="color:#a6e22e">user_art</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">keep</span> <span style="color:#a6e22e">number</span> <span style="color:#a6e22e">of</span> <span style="color:#a6e22e">methods</span> <span style="color:#a6e22e">under</span> <span style="color:#ae81ff">10.</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">The</span> <span style="color:#a6e22e">method</span> <span style="color:#a6e22e">someMethod</span>() <span style="color:#a6e22e">has</span> <span style="color:#a6e22e">an</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">of</span> <span style="color:#ae81ff">15211861552761400491758084218783915899329999864762835394560.</span> <span style="color:#a6e22e">The</span> <span style="color:#a6e22e">configured</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">threshold</span> <span style="color:#a6e22e">is</span> <span style="color:#ae81ff">200.</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">The</span> <span style="color:#a6e22e">method</span> <span style="color:#a6e22e">someMethod2</span>() <span style="color:#a6e22e">has</span> <span style="color:#a6e22e">an</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">of</span> <span style="color:#ae81ff">900000.</span> <span style="color:#a6e22e">The</span> <span style="color:#a6e22e">configured</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">threshold</span> <span style="color:#a6e22e">is</span> <span style="color:#ae81ff">200.</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">The</span> <span style="color:#a6e22e">method</span> <span style="color:#a6e22e">someMethod3</span>() <span style="color:#a6e22e">has</span> <span style="color:#a6e22e">an</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">of</span> <span style="color:#ae81ff">119430531454390553552.</span> <span style="color:#a6e22e">The</span> <span style="color:#a6e22e">configured</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">threshold</span> <span style="color:#a6e22e">is</span> <span style="color:#ae81ff">200.</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">...</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">The</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">some_class2</span> <span style="color:#a6e22e">has</span> <span style="color:#ae81ff">3442</span> <span style="color:#a6e22e">lines</span> <span style="color:#a6e22e">of</span> <span style="color:#a6e22e">code</span><span style="color:#f92672">.</span> <span style="color:#a6e22e">Current</span> <span style="color:#a6e22e">threshold</span> <span style="color:#a6e22e">is</span> <span style="color:#ae81ff">1000.</span> <span style="color:#a6e22e">Avoid</span> <span style="color:#a6e22e">really</span> <span style="color:#a6e22e">long</span> <span style="color:#a6e22e">classes</span><span style="color:#f92672">.</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">The</span> <span style="color:#a6e22e">method</span> <span style="color:#a6e22e">someMethod</span>() <span style="color:#a6e22e">has</span> <span style="color:#a6e22e">an</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">of</span> <span style="color:#ae81ff">151619238808128.</span> <span style="color:#a6e22e">The</span> <span style="color:#a6e22e">configured</span> <span style="color:#a6e22e">NPath</span> <span style="color:#a6e22e">complexity</span> <span style="color:#a6e22e">threshold</span> <span style="color:#a6e22e">is</span> <span style="color:#ae81ff">200.</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">...</span>
</span></span></code></pre></div><p>Happy refactoring!</p>]]></content:encoded></item><item><title>Rails legacy database migration, Part 2</title><link>https://awesomeprogrammer.com/blog/2012/08/08/rails-legacy-database-migration-part-2/</link><pubDate>Wed, 08 Aug 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/08/08/rails-legacy-database-migration-part-2/</guid><description><![CDATA[<p><a href="/blog/2012/07/28/rails-legacy-database-migration/">(Link to Part 1)</a> Now let&rsquo;s look at some tools that can help you migrating messy html into some more easy to maintain format. Obvious choice for many would be <a href="http://daringfireball.net/projects/markdown/">markdown</a>, but as markdown is quite geeky I decided to go with well known by average user - and hated by many programmers - <a href="http://en.wikipedia.org/wiki/BBCode">BBCode</a>. BBCode would also allow me to implement some custom tags that I needed anyway - so from now we will stick with it.</p>
<h3 id="how-to-parse--html-to-bbcode-in-ruby--rails">How to parse  HTML to BBCode in Ruby / Rails?</h3>
<p>After some unsuccessful tries with regular expressions that aren&rsquo;t really solutions to the problem. Here are my weapons of choice:</p>
<ul>
<li><a href="https://github.com/flavorjones/loofah">Loofah</a> - powered with <a href="http://nokogiri.org">Nokogiri</a> and libxml2 is a excellent choice</li>
<li><a href="https://github.com/asceth/bbcoder">BBCoder</a> - easy to configure and clean BBCode to HTML parser</li>
<li><a href="https://github.com/carld/tidy">Tidy</a> - Ruby interface for TidyHTML (if you have really messy content as I do you can tidy-it-up a little bit before processing)</li>
</ul>
<p>Loofah allows to define custom scrubbers that we will later apply to our dirty, dirty html. I won&rsquo;t repeat here the documentation and/or manual, because you can easily read it yourself - and I&rsquo;m really recommending getting to know how nokogiri works and why it&rsquo;s so awesome. Let&rsquo;s just cut straight to the problem.</p>
<p>Here code for my Loofah&rsquo;s scrubber that converts HTML to BBCode:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">HTMLBBCoder</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">Loofah</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Scrubber</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">initialize</span>
</span></span><span style="display:flex;"><span>    @direction <span style="color:#f92672">=</span> <span style="color:#e6db74">:bottom_up</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">scrub</span>(node)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">CONTINUE</span> <span style="color:#66d9ef">if</span> node<span style="color:#f92672">.</span>text?
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">case</span> node<span style="color:#f92672">.</span>name
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;i&#34;</span>, <span style="color:#e6db74">&#34;em&#34;</span>
</span></span><span style="display:flex;"><span>        node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[i]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/i]&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;br&#34;</span>
</span></span><span style="display:flex;"><span>        node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;strong&#34;</span>
</span></span><span style="display:flex;"><span>        node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[b]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/b]&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;a&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> node<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;href&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">&amp;&amp;</span> node<span style="color:#f92672">.</span>inner_html
</span></span><span style="display:flex;"><span>          node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[url=</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;href&#39;</span><span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74">]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/url]&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>          node<span style="color:#f92672">.</span>remove <span style="color:#75715e"># remove urls without href or title</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;img&#34;</span>
</span></span><span style="display:flex;"><span>        node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[img]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;src&#39;</span><span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74">[/img]&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;table&#34;</span>, <span style="color:#e6db74">&#34;td&#34;</span> ,<span style="color:#e6db74">&#34;tr&#34;</span>, <span style="color:#e6db74">&#34;li&#34;</span>, <span style="color:#e6db74">&#34;b&#34;</span>, <span style="color:#e6db74">&#34;ol&#34;</span>, <span style="color:#e6db74">&#34;ul&#34;</span>, <span style="color:#e6db74">&#34;u&#34;</span>
</span></span><span style="display:flex;"><span>        node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74">]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74">]&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;span&#34;</span>, <span style="color:#e6db74">&#34;div&#34;</span>, <span style="color:#e6db74">&#34;p&#34;</span> <span style="color:#75715e"># handle text aligment, font-weight etc.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> node<span style="color:#f92672">.</span>attributes<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;style&#39;</span><span style="color:#f92672">]</span> 
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">if</span> node<span style="color:#f92672">.</span>attributes<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;style&#39;</span><span style="color:#f92672">].</span>to_s<span style="color:#f92672">.</span>match(<span style="color:#e6db74">/bold/</span>)
</span></span><span style="display:flex;"><span>             node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[b]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/b]&#34;</span>)
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">elsif</span> node<span style="color:#f92672">.</span>attributes<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;style&#39;</span><span style="color:#f92672">].</span>to_s<span style="color:#f92672">.</span>match(<span style="color:#e6db74">/italic/</span>)
</span></span><span style="display:flex;"><span>            node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[i]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/i]&#34;</span>)
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">elsif</span> node<span style="color:#f92672">.</span>attributes<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;style&#39;</span><span style="color:#f92672">].</span>to_s<span style="color:#f92672">.</span>match(<span style="color:#e6db74">/underline/</span>)
</span></span><span style="display:flex;"><span>            node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[u]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/u]&#34;</span>)
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">elsif</span> node<span style="color:#f92672">.</span>attributes<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;style&#39;</span><span style="color:#f92672">].</span>to_s<span style="color:#f92672">.</span>match(<span style="color:#e6db74">/center/</span>) <span style="color:#f92672">||</span> (node<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;align&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">&amp;&amp;</span> node<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;align&#39;</span><span style="color:#f92672">].</span>match(<span style="color:#e6db74">/center/</span>))
</span></span><span style="display:flex;"><span>             node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[center]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/center]&#34;</span>)
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>            node<span style="color:#f92672">.</span>replace(node<span style="color:#f92672">.</span>inner_html)  
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>          node<span style="color:#f92672">.</span>replace(node<span style="color:#f92672">.</span>inner_html)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;font&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> node<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;color&#39;</span><span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>          node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[color=</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;color&#39;</span><span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74">]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/color]&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>          node<span style="color:#f92672">.</span>inner_html<span style="color:#f92672">.</span>empty? ? node<span style="color:#f92672">.</span>remove : node<span style="color:#f92672">.</span>replace(node<span style="color:#f92672">.</span>inner_html)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">STOP</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span> <span style="color:#75715e"># case</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span> <span style="color:#75715e"># scrub</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span> <span style="color:#75715e"># class</span>
</span></span></code></pre></div><p>It goes from bottom, rewrites the html tags into BBCode tags and on the way fixes some common problems I have encountered. You will probably need to tweak it for your needs, but at least now you have a base. It&rsquo;s not pretties thing in the world, but it gets the job done.</p>
<p>Now let&rsquo;s use some Tidy help before calling the scrubber. You can tweak settings after <a href="http://tidy.sourceforge.net/docs/tidy_man.html">reading the manual</a>. I decided to go with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>tidy <span style="color:#f92672">=</span> <span style="color:#66d9ef">Tidy</span><span style="color:#f92672">.</span>open({<span style="color:#e6db74">:show_warnings</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">:wrap</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;char-encoding&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;utf8&#39;</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;output-bom&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;show-body-only&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;word2000&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;merge-spans&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;merge-divs&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;drop-empty-paras&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;tidy-mark&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;drop-proprietary-attributes&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;quote-ampersand&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;force-output&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>}) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>tidy<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  clean <span style="color:#f92672">=</span> tidy<span style="color:#f92672">.</span>clean(text)
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># maybe here you&#39;ll want to check for errors/warnings</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>After tidy&rsquo;ing it up you probably will have to force encoding like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">Loofah</span><span style="color:#f92672">.</span>fragment(clean<span style="color:#f92672">.</span>force_encoding(<span style="color:#e6db74">&#39;UTF-8&#39;</span>))<span style="color:#f92672">.</span>scrub!(bbcoder)<span style="color:#f92672">.</span>to_s
</span></span></code></pre></div><p>Also - depending of your chunks of html you may need to wrap your text inside some proper html tags like <em>&lt;p&gt;</em> or <em>&lt;div&gt;</em></p>]]></description><content:encoded><![CDATA[<p><a href="/blog/2012/07/28/rails-legacy-database-migration/">(Link to Part 1)</a> Now let&rsquo;s look at some tools that can help you migrating messy html into some more easy to maintain format. Obvious choice for many would be <a href="http://daringfireball.net/projects/markdown/">markdown</a>, but as markdown is quite geeky I decided to go with well known by average user - and hated by many programmers - <a href="http://en.wikipedia.org/wiki/BBCode">BBCode</a>. BBCode would also allow me to implement some custom tags that I needed anyway - so from now we will stick with it.</p>
<h3 id="how-to-parse--html-to-bbcode-in-ruby--rails">How to parse  HTML to BBCode in Ruby / Rails?</h3>
<p>After some unsuccessful tries with regular expressions that aren&rsquo;t really solutions to the problem. Here are my weapons of choice:</p>
<ul>
<li><a href="https://github.com/flavorjones/loofah">Loofah</a> - powered with <a href="http://nokogiri.org">Nokogiri</a> and libxml2 is a excellent choice</li>
<li><a href="https://github.com/asceth/bbcoder">BBCoder</a> - easy to configure and clean BBCode to HTML parser</li>
<li><a href="https://github.com/carld/tidy">Tidy</a> - Ruby interface for TidyHTML (if you have really messy content as I do you can tidy-it-up a little bit before processing)</li>
</ul>
<p>Loofah allows to define custom scrubbers that we will later apply to our dirty, dirty html. I won&rsquo;t repeat here the documentation and/or manual, because you can easily read it yourself - and I&rsquo;m really recommending getting to know how nokogiri works and why it&rsquo;s so awesome. Let&rsquo;s just cut straight to the problem.</p>
<p>Here code for my Loofah&rsquo;s scrubber that converts HTML to BBCode:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">HTMLBBCoder</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">Loofah</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Scrubber</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">initialize</span>
</span></span><span style="display:flex;"><span>    @direction <span style="color:#f92672">=</span> <span style="color:#e6db74">:bottom_up</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">scrub</span>(node)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">CONTINUE</span> <span style="color:#66d9ef">if</span> node<span style="color:#f92672">.</span>text?
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">case</span> node<span style="color:#f92672">.</span>name
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;i&#34;</span>, <span style="color:#e6db74">&#34;em&#34;</span>
</span></span><span style="display:flex;"><span>        node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[i]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/i]&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;br&#34;</span>
</span></span><span style="display:flex;"><span>        node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;strong&#34;</span>
</span></span><span style="display:flex;"><span>        node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[b]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/b]&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;a&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> node<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;href&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">&amp;&amp;</span> node<span style="color:#f92672">.</span>inner_html
</span></span><span style="display:flex;"><span>          node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[url=</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;href&#39;</span><span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74">]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/url]&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>          node<span style="color:#f92672">.</span>remove <span style="color:#75715e"># remove urls without href or title</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;img&#34;</span>
</span></span><span style="display:flex;"><span>        node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[img]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;src&#39;</span><span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74">[/img]&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;table&#34;</span>, <span style="color:#e6db74">&#34;td&#34;</span> ,<span style="color:#e6db74">&#34;tr&#34;</span>, <span style="color:#e6db74">&#34;li&#34;</span>, <span style="color:#e6db74">&#34;b&#34;</span>, <span style="color:#e6db74">&#34;ol&#34;</span>, <span style="color:#e6db74">&#34;ul&#34;</span>, <span style="color:#e6db74">&#34;u&#34;</span>
</span></span><span style="display:flex;"><span>        node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74">]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74">]&#34;</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;span&#34;</span>, <span style="color:#e6db74">&#34;div&#34;</span>, <span style="color:#e6db74">&#34;p&#34;</span> <span style="color:#75715e"># handle text aligment, font-weight etc.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> node<span style="color:#f92672">.</span>attributes<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;style&#39;</span><span style="color:#f92672">]</span> 
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">if</span> node<span style="color:#f92672">.</span>attributes<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;style&#39;</span><span style="color:#f92672">].</span>to_s<span style="color:#f92672">.</span>match(<span style="color:#e6db74">/bold/</span>)
</span></span><span style="display:flex;"><span>             node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[b]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/b]&#34;</span>)
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">elsif</span> node<span style="color:#f92672">.</span>attributes<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;style&#39;</span><span style="color:#f92672">].</span>to_s<span style="color:#f92672">.</span>match(<span style="color:#e6db74">/italic/</span>)
</span></span><span style="display:flex;"><span>            node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[i]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/i]&#34;</span>)
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">elsif</span> node<span style="color:#f92672">.</span>attributes<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;style&#39;</span><span style="color:#f92672">].</span>to_s<span style="color:#f92672">.</span>match(<span style="color:#e6db74">/underline/</span>)
</span></span><span style="display:flex;"><span>            node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[u]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/u]&#34;</span>)
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">elsif</span> node<span style="color:#f92672">.</span>attributes<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;style&#39;</span><span style="color:#f92672">].</span>to_s<span style="color:#f92672">.</span>match(<span style="color:#e6db74">/center/</span>) <span style="color:#f92672">||</span> (node<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;align&#39;</span><span style="color:#f92672">]</span> <span style="color:#f92672">&amp;&amp;</span> node<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;align&#39;</span><span style="color:#f92672">].</span>match(<span style="color:#e6db74">/center/</span>))
</span></span><span style="display:flex;"><span>             node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[center]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/center]&#34;</span>)
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>            node<span style="color:#f92672">.</span>replace(node<span style="color:#f92672">.</span>inner_html)  
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>          node<span style="color:#f92672">.</span>replace(node<span style="color:#f92672">.</span>inner_html)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">when</span> <span style="color:#e6db74">&#34;font&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> node<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;color&#39;</span><span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>          node<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;[color=</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">[</span><span style="color:#e6db74">&#39;color&#39;</span><span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74">]</span><span style="color:#e6db74">#{</span>node<span style="color:#f92672">.</span>inner_html<span style="color:#e6db74">}</span><span style="color:#e6db74">[/color]&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>          node<span style="color:#f92672">.</span>inner_html<span style="color:#f92672">.</span>empty? ? node<span style="color:#f92672">.</span>remove : node<span style="color:#f92672">.</span>replace(node<span style="color:#f92672">.</span>inner_html)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">STOP</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span> <span style="color:#75715e"># case</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span> <span style="color:#75715e"># scrub</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span> <span style="color:#75715e"># class</span>
</span></span></code></pre></div><p>It goes from bottom, rewrites the html tags into BBCode tags and on the way fixes some common problems I have encountered. You will probably need to tweak it for your needs, but at least now you have a base. It&rsquo;s not pretties thing in the world, but it gets the job done.</p>
<p>Now let&rsquo;s use some Tidy help before calling the scrubber. You can tweak settings after <a href="http://tidy.sourceforge.net/docs/tidy_man.html">reading the manual</a>. I decided to go with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>tidy <span style="color:#f92672">=</span> <span style="color:#66d9ef">Tidy</span><span style="color:#f92672">.</span>open({<span style="color:#e6db74">:show_warnings</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">:wrap</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;char-encoding&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;utf8&#39;</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;output-bom&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;show-body-only&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;word2000&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;merge-spans&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;merge-divs&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;drop-empty-paras&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;tidy-mark&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;drop-proprietary-attributes&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;quote-ampersand&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>                  <span style="color:#e6db74">&#39;force-output&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>}) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>tidy<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  clean <span style="color:#f92672">=</span> tidy<span style="color:#f92672">.</span>clean(text)
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># maybe here you&#39;ll want to check for errors/warnings</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>After tidy&rsquo;ing it up you probably will have to force encoding like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">Loofah</span><span style="color:#f92672">.</span>fragment(clean<span style="color:#f92672">.</span>force_encoding(<span style="color:#e6db74">&#39;UTF-8&#39;</span>))<span style="color:#f92672">.</span>scrub!(bbcoder)<span style="color:#f92672">.</span>to_s
</span></span></code></pre></div><p>Also - depending of your chunks of html you may need to wrap your text inside some proper html tags like <em>&lt;p&gt;</em> or <em>&lt;div&gt;</em></p>]]></content:encoded></item><item><title>Exporting SMS messages and contacts into Android phone</title><link>https://awesomeprogrammer.com/blog/2012/08/03/exporting-sms-messages-and-contacts-into-android-phone/</link><pubDate>Fri, 03 Aug 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/08/03/exporting-sms-messages-and-contacts-into-android-phone/</guid><description><![CDATA[<p>Whoa, that was a busy week, anyhow, here&rsquo;s a quick reference for all new android phone users. If you own an old WindowsMobile 5/6 phone and you want to import your contacts and text messages here is a quick receipt (sources: <a href="http://forum.xda-developers.com/showthread.php?t=734044">1</a>, <a href="http://android.riteshsahu.com/tips/importexportmove-sms-messages-from-windows-mobile-to-android">2</a>). Tools you will need:</p>
<p>For your windows phone:</p>
<ul>
<li><a href="/files/SmsExporterSetup0.5.cab">SMSExporter</a></li>
<li><a href="/files/PPCPimBackupV2.8_wm2005.zip">Dotfred&rsquo;s PimBackup</a></li>
</ul>
<p>For your android phone:</p>
<ul>
<li>Gmail account</li>
<li><a href="https://play.google.com/store/apps/details?id=com.riteshsahu.SMSBackupRestore">SMS Backup &amp; Restore</a></li>
</ul>
<h3 id="windows-mobile-exporting-contacts">[Windows Mobile] Exporting contacts</h3>
<p>Install and run PimBackup, choose <em>Backup</em> and uncheck everything except <em>Contacts</em>, click next, then uncheck everything except <em>Create temp files in memory</em> and click next again, wait for it and exit the app.</p>
<p>You will find a file called <em>PIMBackup_xxxxxxxx.pib</em> (name depends of the time of the backup) on your SD Card - download it to your computer. It&rsquo;s a normal zip archive, so extract a file called <em>contacts_xxxxxxxx.csc</em> from it and rename it to .csv. Now you can clean it up a little bit (if you want) in LibreCalc, Excel or whatever you use.</p>
<h3 id="android-importing-contacts">[Android] Importing contacts</h3>
<p>Visit gmail, go to <em>Contacts</em> and choose <em>Import</em>, select .csv file and your are done here. Now setup an google account on your android phone, go to contacts and choose <em>sync</em> from menu - now you should have a full list of your beloved people on your brand new phone.</p>
<h3 id="windows-mobile-exporting-sms">[Windows Mobile] Exporting SMS</h3>
<p>It&rsquo;s really straightforward here - just install and run SMS exporter on your phone, choose to export messages and you will end on with file called SMS.xml.</p>
<h3 id="android-importing-sms">[Android] Importing SMS</h3>
<p>Install SMS Backup &amp; Restore. From there choose <em>Restore</em> and select that xml file - wait for the import and you are done :).</p>]]></description><content:encoded><![CDATA[<p>Whoa, that was a busy week, anyhow, here&rsquo;s a quick reference for all new android phone users. If you own an old WindowsMobile 5/6 phone and you want to import your contacts and text messages here is a quick receipt (sources: <a href="http://forum.xda-developers.com/showthread.php?t=734044">1</a>, <a href="http://android.riteshsahu.com/tips/importexportmove-sms-messages-from-windows-mobile-to-android">2</a>). Tools you will need:</p>
<p>For your windows phone:</p>
<ul>
<li><a href="/files/SmsExporterSetup0.5.cab">SMSExporter</a></li>
<li><a href="/files/PPCPimBackupV2.8_wm2005.zip">Dotfred&rsquo;s PimBackup</a></li>
</ul>
<p>For your android phone:</p>
<ul>
<li>Gmail account</li>
<li><a href="https://play.google.com/store/apps/details?id=com.riteshsahu.SMSBackupRestore">SMS Backup &amp; Restore</a></li>
</ul>
<h3 id="windows-mobile-exporting-contacts">[Windows Mobile] Exporting contacts</h3>
<p>Install and run PimBackup, choose <em>Backup</em> and uncheck everything except <em>Contacts</em>, click next, then uncheck everything except <em>Create temp files in memory</em> and click next again, wait for it and exit the app.</p>
<p>You will find a file called <em>PIMBackup_xxxxxxxx.pib</em> (name depends of the time of the backup) on your SD Card - download it to your computer. It&rsquo;s a normal zip archive, so extract a file called <em>contacts_xxxxxxxx.csc</em> from it and rename it to .csv. Now you can clean it up a little bit (if you want) in LibreCalc, Excel or whatever you use.</p>
<h3 id="android-importing-contacts">[Android] Importing contacts</h3>
<p>Visit gmail, go to <em>Contacts</em> and choose <em>Import</em>, select .csv file and your are done here. Now setup an google account on your android phone, go to contacts and choose <em>sync</em> from menu - now you should have a full list of your beloved people on your brand new phone.</p>
<h3 id="windows-mobile-exporting-sms">[Windows Mobile] Exporting SMS</h3>
<p>It&rsquo;s really straightforward here - just install and run SMS exporter on your phone, choose to export messages and you will end on with file called SMS.xml.</p>
<h3 id="android-importing-sms">[Android] Importing SMS</h3>
<p>Install SMS Backup &amp; Restore. From there choose <em>Restore</em> and select that xml file - wait for the import and you are done :).</p>]]></content:encoded></item><item><title>Rails legacy database migration</title><link>https://awesomeprogrammer.com/blog/2012/07/28/rails-legacy-database-migration/</link><pubDate>Sat, 28 Jul 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/07/28/rails-legacy-database-migration/</guid><description><![CDATA[<p>I will give you some of my thoughts about <strong>migrating legacy database</strong> to Ruby On Rails framework. Welcome to part one.</p>
<p>It&rsquo;s 32°C outside (and apparently inside), I&rsquo;m trying to focus and finish my old project that I started after-hours. Being a programmer ain&rsquo;t easy :P. But let&rsquo;s get back on track. So to happens I have this legacy, really old, not maintained for years database. It comes from some old custom php cms, that is you might say - far from perfect. It doesn&rsquo;t have any logical constraints (no data integrity) and validation is a mess.</p>
<p>I have started from rough PHP script that tries to fix what can be fixed in a one way or another, purges database from spam (hello there web bots), applies some &lsquo;static&rsquo; fixes (like rewriting some custom prepared data) and leaves the rest as-it. I won&rsquo;t post the code here, because it&rsquo;s just written Rambo style - I didn&rsquo;t payed attention to the optimization or anything as I had to run it basically once.</p>
<p>After that it was time to cook <strong>rake tasks</strong> that will migrate table by table this data into my new app. I have setup a legacy database in <code>database.yml</code> and created bunch of legacy classes in external file like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LegacyPost</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  establish_connection <span style="color:#e6db74">:legacy</span>
</span></span><span style="display:flex;"><span>  self<span style="color:#f92672">.</span>table_name  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;board_posty&#39;</span>
</span></span><span style="display:flex;"><span>  self<span style="color:#f92672">.</span>primary_key <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;odpid&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Then was the time to start working or rake task, by trial and error I ended up with a function that could be applied for most of my cases.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run_migrate_task</span>(new_model, legacy_model, rewrite_hash, broken <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  error_count <span style="color:#f92672">=</span> migrate_count <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>  p_key <span style="color:#f92672">=</span> legacy_model<span style="color:#f92672">.</span>primary_key<span style="color:#f92672">.</span>to_sym <span style="color:#66d9ef">unless</span> legacy_model<span style="color:#f92672">.</span>primary_key<span style="color:#f92672">.</span>nil? <span style="color:#75715e"># get primary key of legacy table</span>
</span></span><span style="display:flex;"><span>  id_important <span style="color:#f92672">=</span> rewrite_hash<span style="color:#f92672">.</span>keys<span style="color:#f92672">.</span>include?(<span style="color:#e6db74">:id</span>) <span style="color:#75715e"># we are rewriting old ids</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  legacy_model<span style="color:#f92672">.</span>find_in_batches(<span style="color:#e6db74">:batch_size</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">2000</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>old_objects<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    existing_ids <span style="color:#f92672">=</span> id_important ? new_model<span style="color:#f92672">.</span>select(<span style="color:#e6db74">:id</span>)<span style="color:#f92672">.</span>where(<span style="color:#e6db74">:id</span> <span style="color:#f92672">=&gt;</span> old_objects<span style="color:#f92672">.</span>map(<span style="color:#f92672">&amp;</span>p_key))<span style="color:#f92672">.</span>map(<span style="color:#f92672">&amp;</span><span style="color:#e6db74">:id</span>)<span style="color:#f92672">.</span>to_set : {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span><span style="color:#f92672">.</span>transaction <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>      old_objects<span style="color:#f92672">.</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>old_object<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">begin</span>
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span>id_important <span style="color:#f92672">||</span> <span style="color:#f92672">!</span>existing_ids<span style="color:#f92672">.</span>include?(old_object<span style="color:#f92672">[</span>p_key<span style="color:#f92672">]</span>)) <span style="color:#f92672">&amp;&amp;</span> <span style="color:#f92672">!</span>broken<span style="color:#f92672">.</span>include?(old_object<span style="color:#f92672">[</span>p_key<span style="color:#f92672">]</span>)
</span></span><span style="display:flex;"><span>            new_object <span style="color:#f92672">=</span> new_model<span style="color:#f92672">.</span>new
</span></span><span style="display:flex;"><span>            rewrite_hash<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>key, value<span style="color:#f92672">|</span> eval(<span style="color:#e6db74">&#34;new_object.</span><span style="color:#e6db74">#{</span>key<span style="color:#e6db74">}</span><span style="color:#e6db74"> = </span><span style="color:#e6db74">#{</span>value<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>) } <span style="color:#75715e"># setup new values by eval&#39;ing passed hash</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> new_object<span style="color:#f92672">.</span>save
</span></span><span style="display:flex;"><span>              migrate_count<span style="color:#f92672">+=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>              puts <span style="color:#e6db74">&#34;ERROR | ID: </span><span style="color:#e6db74">#{</span>old_object<span style="color:#f92672">[</span>p_key<span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> | MSG: </span><span style="color:#e6db74">#{</span>new_object<span style="color:#f92672">.</span>errors<span style="color:#f92672">.</span>full_messages<span style="color:#f92672">.</span>to_sentence<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>              error_count<span style="color:#f92672">+=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">end</span> <span style="color:#75715e"># if save</span>
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">end</span> <span style="color:#75715e"># if important</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">rescue</span> <span style="color:#66d9ef">Exception</span> <span style="color:#f92672">=&gt;</span> exception
</span></span><span style="display:flex;"><span>          puts <span style="color:#e6db74">&#34;EXCEPTION | ID: </span><span style="color:#e6db74">#{</span>old_object<span style="color:#f92672">[</span>p_key<span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> | </span><span style="color:#e6db74">#{</span>exception<span style="color:#f92672">.</span>message<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>          error_count<span style="color:#f92672">+=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">end</span> <span style="color:#75715e"># begin</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span> <span style="color:#75715e"># transaction commit</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span> <span style="color:#75715e"># each old objects</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span> <span style="color:#75715e"># each batch</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  puts <span style="color:#e6db74">&#34;DONE | Migrated: </span><span style="color:#e6db74">#{</span>migrate_count<span style="color:#e6db74">}</span><span style="color:#e6db74"> | Error: </span><span style="color:#e6db74">#{</span>error_count<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>What this function does is takes new and old model and hash that consists of keys corresponding to the new model and raw code to apply for that value, optional I&rsquo;m passing array if IDs of totally broken objects or object that I decided that I don&rsquo;t want to import for some reason. I&rsquo;m checking if object was properly saved (validation message) and rescuing any exceptions as I just let it fail if I couldn&rsquo;t fix it in php script. Few notices:</p>
<ul>
<li>
<p>if I&rsquo;m rewriting old IDs (and most of the time I am) first I&rsquo;m fetching stuff I have already imported (maybe in previous run before some additional tweaks) - I don&rsquo;t want to import 200k records just because two were broken</p>
</li>
<li>
<p>using <code>find_in_batches</code> is quite useful here, apparently <code>all</code> worked fine until I got to table that had half a million rows and then ran out of memory, my bad :P</p>
</li>
<li>
<p>wrap it inside a transaction for some speed improvement, you may want to disable validation or go with raw SQL generation instead - it all depends of your needs</p>
</li>
</ul>
<p>So I can call it like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>  task <span style="color:#e6db74">:posts</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">:environment</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    setup_env <span style="color:#75715e"># setup configuration, load classes and stuff</span>
</span></span><span style="display:flex;"><span>    rewrite <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:id</span>         <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;old_object.odpid&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:topic_id</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;old_object.tematid&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:user_id</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;old_object.autor&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:content</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;html_to_bbc(old_object.tresc, true)&#34;</span>, <span style="color:#75715e"># call custom function here</span>
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:updated_at</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;old_object.edytowano &gt; 0 ? Time.at(old_object.edytowano) : Time.at(old_object.dopisano)&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:updated_by</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;old_object.edytowal &gt; 0 ? Time.at(old_object.edytowal) : nil&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:created_at</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;Time.at(old_object.dopisano)&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    run_migrate_task(<span style="color:#66d9ef">Post</span>, <span style="color:#66d9ef">LegacyPost</span>, rewrite)  
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Maybe it&rsquo;s not the perfect solution, not the smartest and quickest - but in my opinion migrations are supposed to be dirty, doable in reasonable time and just should work. You won&rsquo;t maintain this code in the future anyway, so it&rsquo;s up to you how much effort you are willing to put it. As it&rsquo;s my side project I decided to be cheap ;).</p>
<p>In next chapter I will give you some hints how to convert pretty messed up HTML to BBCode (don&rsquo;t give me that look!) without using regular expressions (that don&rsquo;t cut it anyway in the end). Cheers!</p>
<h3 id="updated-14082012">Updated 14.08.2012</h3>
<p>After some investigation of my migration I realized that using <code>include?</code> on huge <code>Array</code> in this case is not the way to go. Instead convert array to <code>Set</code>. Why you ask? <a href="http://www.ruby-doc.org/stdlib-1.9.3/libdoc/set/rdoc/Set.html">Here you can find</a> some useful information. Simple benchmark below.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>array <span style="color:#f92672">=</span> (<span style="color:#ae81ff">1</span><span style="color:#f92672">..</span><span style="color:#ae81ff">1_000_000</span>)<span style="color:#f92672">.</span>to_a
</span></span><span style="display:flex;"><span>set <span style="color:#f92672">=</span> array<span style="color:#f92672">.</span>to_set
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Benchmark</span><span style="color:#f92672">.</span>bm(<span style="color:#ae81ff">15</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>x<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;Array.include?&#34;</span>) { <span style="color:#ae81ff">1000</span><span style="color:#f92672">.</span>times { array<span style="color:#f92672">.</span>include?(<span style="color:#ae81ff">500_000</span>) } }
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;Set.include?&#34;</span>)   { <span style="color:#ae81ff">1000</span><span style="color:#f92672">.</span>times { set<span style="color:#f92672">.</span>include?(<span style="color:#ae81ff">500_000</span>) } }
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                      user     system      total        real
</span></span><span style="display:flex;"><span>Array<span style="color:#f92672">.</span>include?   <span style="color:#ae81ff">20</span><span style="color:#f92672">.</span><span style="color:#ae81ff">940000</span>   <span style="color:#ae81ff">0</span><span style="color:#f92672">.</span><span style="color:#ae81ff">000000</span>  <span style="color:#ae81ff">20</span><span style="color:#f92672">.</span><span style="color:#ae81ff">940000</span> ( <span style="color:#ae81ff">20</span><span style="color:#f92672">.</span><span style="color:#ae81ff">991647</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Set</span><span style="color:#f92672">.</span>include?      <span style="color:#ae81ff">0</span><span style="color:#f92672">.</span><span style="color:#ae81ff">000000</span>   <span style="color:#ae81ff">0</span><span style="color:#f92672">.</span><span style="color:#ae81ff">000000</span>   <span style="color:#ae81ff">0</span><span style="color:#f92672">.</span><span style="color:#ae81ff">000000</span> (  <span style="color:#ae81ff">0</span><span style="color:#f92672">.</span><span style="color:#ae81ff">0001</span><span style="color:#ae81ff">99</span>)
</span></span></code></pre></div>]]></description><content:encoded><![CDATA[<p>I will give you some of my thoughts about <strong>migrating legacy database</strong> to Ruby On Rails framework. Welcome to part one.</p>
<p>It&rsquo;s 32°C outside (and apparently inside), I&rsquo;m trying to focus and finish my old project that I started after-hours. Being a programmer ain&rsquo;t easy :P. But let&rsquo;s get back on track. So to happens I have this legacy, really old, not maintained for years database. It comes from some old custom php cms, that is you might say - far from perfect. It doesn&rsquo;t have any logical constraints (no data integrity) and validation is a mess.</p>
<p>I have started from rough PHP script that tries to fix what can be fixed in a one way or another, purges database from spam (hello there web bots), applies some &lsquo;static&rsquo; fixes (like rewriting some custom prepared data) and leaves the rest as-it. I won&rsquo;t post the code here, because it&rsquo;s just written Rambo style - I didn&rsquo;t payed attention to the optimization or anything as I had to run it basically once.</p>
<p>After that it was time to cook <strong>rake tasks</strong> that will migrate table by table this data into my new app. I have setup a legacy database in <code>database.yml</code> and created bunch of legacy classes in external file like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LegacyPost</span> <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span>
</span></span><span style="display:flex;"><span>  establish_connection <span style="color:#e6db74">:legacy</span>
</span></span><span style="display:flex;"><span>  self<span style="color:#f92672">.</span>table_name  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;board_posty&#39;</span>
</span></span><span style="display:flex;"><span>  self<span style="color:#f92672">.</span>primary_key <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;odpid&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Then was the time to start working or rake task, by trial and error I ended up with a function that could be applied for most of my cases.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run_migrate_task</span>(new_model, legacy_model, rewrite_hash, broken <span style="color:#f92672">=</span> <span style="color:#f92672">[]</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  error_count <span style="color:#f92672">=</span> migrate_count <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>  p_key <span style="color:#f92672">=</span> legacy_model<span style="color:#f92672">.</span>primary_key<span style="color:#f92672">.</span>to_sym <span style="color:#66d9ef">unless</span> legacy_model<span style="color:#f92672">.</span>primary_key<span style="color:#f92672">.</span>nil? <span style="color:#75715e"># get primary key of legacy table</span>
</span></span><span style="display:flex;"><span>  id_important <span style="color:#f92672">=</span> rewrite_hash<span style="color:#f92672">.</span>keys<span style="color:#f92672">.</span>include?(<span style="color:#e6db74">:id</span>) <span style="color:#75715e"># we are rewriting old ids</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  legacy_model<span style="color:#f92672">.</span>find_in_batches(<span style="color:#e6db74">:batch_size</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">2000</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>old_objects<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    existing_ids <span style="color:#f92672">=</span> id_important ? new_model<span style="color:#f92672">.</span>select(<span style="color:#e6db74">:id</span>)<span style="color:#f92672">.</span>where(<span style="color:#e6db74">:id</span> <span style="color:#f92672">=&gt;</span> old_objects<span style="color:#f92672">.</span>map(<span style="color:#f92672">&amp;</span>p_key))<span style="color:#f92672">.</span>map(<span style="color:#f92672">&amp;</span><span style="color:#e6db74">:id</span>)<span style="color:#f92672">.</span>to_set : {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">ActiveRecord</span><span style="color:#f92672">::</span><span style="color:#66d9ef">Base</span><span style="color:#f92672">.</span>transaction <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>      old_objects<span style="color:#f92672">.</span>each <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>old_object<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">begin</span>
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span>id_important <span style="color:#f92672">||</span> <span style="color:#f92672">!</span>existing_ids<span style="color:#f92672">.</span>include?(old_object<span style="color:#f92672">[</span>p_key<span style="color:#f92672">]</span>)) <span style="color:#f92672">&amp;&amp;</span> <span style="color:#f92672">!</span>broken<span style="color:#f92672">.</span>include?(old_object<span style="color:#f92672">[</span>p_key<span style="color:#f92672">]</span>)
</span></span><span style="display:flex;"><span>            new_object <span style="color:#f92672">=</span> new_model<span style="color:#f92672">.</span>new
</span></span><span style="display:flex;"><span>            rewrite_hash<span style="color:#f92672">.</span>each { <span style="color:#f92672">|</span>key, value<span style="color:#f92672">|</span> eval(<span style="color:#e6db74">&#34;new_object.</span><span style="color:#e6db74">#{</span>key<span style="color:#e6db74">}</span><span style="color:#e6db74"> = </span><span style="color:#e6db74">#{</span>value<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>) } <span style="color:#75715e"># setup new values by eval&#39;ing passed hash</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> new_object<span style="color:#f92672">.</span>save
</span></span><span style="display:flex;"><span>              migrate_count<span style="color:#f92672">+=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>              puts <span style="color:#e6db74">&#34;ERROR | ID: </span><span style="color:#e6db74">#{</span>old_object<span style="color:#f92672">[</span>p_key<span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> | MSG: </span><span style="color:#e6db74">#{</span>new_object<span style="color:#f92672">.</span>errors<span style="color:#f92672">.</span>full_messages<span style="color:#f92672">.</span>to_sentence<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>              error_count<span style="color:#f92672">+=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">end</span> <span style="color:#75715e"># if save</span>
</span></span><span style="display:flex;"><span>          <span style="color:#66d9ef">end</span> <span style="color:#75715e"># if important</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">rescue</span> <span style="color:#66d9ef">Exception</span> <span style="color:#f92672">=&gt;</span> exception
</span></span><span style="display:flex;"><span>          puts <span style="color:#e6db74">&#34;EXCEPTION | ID: </span><span style="color:#e6db74">#{</span>old_object<span style="color:#f92672">[</span>p_key<span style="color:#f92672">]</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> | </span><span style="color:#e6db74">#{</span>exception<span style="color:#f92672">.</span>message<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>          error_count<span style="color:#f92672">+=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">end</span> <span style="color:#75715e"># begin</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span> <span style="color:#75715e"># transaction commit</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span> <span style="color:#75715e"># each old objects</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span> <span style="color:#75715e"># each batch</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  puts <span style="color:#e6db74">&#34;DONE | Migrated: </span><span style="color:#e6db74">#{</span>migrate_count<span style="color:#e6db74">}</span><span style="color:#e6db74"> | Error: </span><span style="color:#e6db74">#{</span>error_count<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>What this function does is takes new and old model and hash that consists of keys corresponding to the new model and raw code to apply for that value, optional I&rsquo;m passing array if IDs of totally broken objects or object that I decided that I don&rsquo;t want to import for some reason. I&rsquo;m checking if object was properly saved (validation message) and rescuing any exceptions as I just let it fail if I couldn&rsquo;t fix it in php script. Few notices:</p>
<ul>
<li>
<p>if I&rsquo;m rewriting old IDs (and most of the time I am) first I&rsquo;m fetching stuff I have already imported (maybe in previous run before some additional tweaks) - I don&rsquo;t want to import 200k records just because two were broken</p>
</li>
<li>
<p>using <code>find_in_batches</code> is quite useful here, apparently <code>all</code> worked fine until I got to table that had half a million rows and then ran out of memory, my bad :P</p>
</li>
<li>
<p>wrap it inside a transaction for some speed improvement, you may want to disable validation or go with raw SQL generation instead - it all depends of your needs</p>
</li>
</ul>
<p>So I can call it like that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>  task <span style="color:#e6db74">:posts</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">:environment</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    setup_env <span style="color:#75715e"># setup configuration, load classes and stuff</span>
</span></span><span style="display:flex;"><span>    rewrite <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:id</span>         <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;old_object.odpid&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:topic_id</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;old_object.tematid&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:user_id</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;old_object.autor&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:content</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;html_to_bbc(old_object.tresc, true)&#34;</span>, <span style="color:#75715e"># call custom function here</span>
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:updated_at</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;old_object.edytowano &gt; 0 ? Time.at(old_object.edytowano) : Time.at(old_object.dopisano)&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:updated_by</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;old_object.edytowal &gt; 0 ? Time.at(old_object.edytowal) : nil&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">:created_at</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#34;Time.at(old_object.dopisano)&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    run_migrate_task(<span style="color:#66d9ef">Post</span>, <span style="color:#66d9ef">LegacyPost</span>, rewrite)  
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Maybe it&rsquo;s not the perfect solution, not the smartest and quickest - but in my opinion migrations are supposed to be dirty, doable in reasonable time and just should work. You won&rsquo;t maintain this code in the future anyway, so it&rsquo;s up to you how much effort you are willing to put it. As it&rsquo;s my side project I decided to be cheap ;).</p>
<p>In next chapter I will give you some hints how to convert pretty messed up HTML to BBCode (don&rsquo;t give me that look!) without using regular expressions (that don&rsquo;t cut it anyway in the end). Cheers!</p>
<h3 id="updated-14082012">Updated 14.08.2012</h3>
<p>After some investigation of my migration I realized that using <code>include?</code> on huge <code>Array</code> in this case is not the way to go. Instead convert array to <code>Set</code>. Why you ask? <a href="http://www.ruby-doc.org/stdlib-1.9.3/libdoc/set/rdoc/Set.html">Here you can find</a> some useful information. Simple benchmark below.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>array <span style="color:#f92672">=</span> (<span style="color:#ae81ff">1</span><span style="color:#f92672">..</span><span style="color:#ae81ff">1_000_000</span>)<span style="color:#f92672">.</span>to_a
</span></span><span style="display:flex;"><span>set <span style="color:#f92672">=</span> array<span style="color:#f92672">.</span>to_set
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Benchmark</span><span style="color:#f92672">.</span>bm(<span style="color:#ae81ff">15</span>) <span style="color:#66d9ef">do</span> <span style="color:#f92672">|</span>x<span style="color:#f92672">|</span>
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;Array.include?&#34;</span>) { <span style="color:#ae81ff">1000</span><span style="color:#f92672">.</span>times { array<span style="color:#f92672">.</span>include?(<span style="color:#ae81ff">500_000</span>) } }
</span></span><span style="display:flex;"><span>  x<span style="color:#f92672">.</span>report(<span style="color:#e6db74">&#34;Set.include?&#34;</span>)   { <span style="color:#ae81ff">1000</span><span style="color:#f92672">.</span>times { set<span style="color:#f92672">.</span>include?(<span style="color:#ae81ff">500_000</span>) } }
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                      user     system      total        real
</span></span><span style="display:flex;"><span>Array<span style="color:#f92672">.</span>include?   <span style="color:#ae81ff">20</span><span style="color:#f92672">.</span><span style="color:#ae81ff">940000</span>   <span style="color:#ae81ff">0</span><span style="color:#f92672">.</span><span style="color:#ae81ff">000000</span>  <span style="color:#ae81ff">20</span><span style="color:#f92672">.</span><span style="color:#ae81ff">940000</span> ( <span style="color:#ae81ff">20</span><span style="color:#f92672">.</span><span style="color:#ae81ff">991647</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">Set</span><span style="color:#f92672">.</span>include?      <span style="color:#ae81ff">0</span><span style="color:#f92672">.</span><span style="color:#ae81ff">000000</span>   <span style="color:#ae81ff">0</span><span style="color:#f92672">.</span><span style="color:#ae81ff">000000</span>   <span style="color:#ae81ff">0</span><span style="color:#f92672">.</span><span style="color:#ae81ff">000000</span> (  <span style="color:#ae81ff">0</span><span style="color:#f92672">.</span><span style="color:#ae81ff">0001</span><span style="color:#ae81ff">99</span>)
</span></span></code></pre></div>]]></content:encoded></item><item><title>Working with Typo3</title><link>https://awesomeprogrammer.com/blog/2012/07/28/working-with-typo3/</link><pubDate>Sat, 28 Jul 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/07/28/working-with-typo3/</guid><description><![CDATA[<p>At my current job I have to devote a lot of time to deal with <em>enterprise Open Source CMS</em> called <a href="http://typo3.org/">Typo3</a>. If you didn&rsquo;t worked with it before it may seems like it&rsquo;s just another CMS of choice, but let me warn you - if you still considering platform for you site or whatever I strongly recommend checking it out first very carefully, you have to see for yourself how it&rsquo;s flexibility works for you. On the other hand I can&rsquo;t be really objective about it. Let&rsquo;s be honest - I&rsquo;m not a big fan of it. And apparently there is a <a href="http://hatetypo3.blogspot.com/2008/04/welcome-to-i-hate-typo3-list.html">bunch of angry programmers out there</a> who, well, are not really <em>lovin&rsquo; it</em>.</p>
<p>The three main problems with Typo that I noticed during my work experience are:</p>
<ul>
<li>
<p>chaotic documentation or <strong>lack of documentation</strong> - you have to google your way through official typo3 site, bugtrackers, some German forums etc. In the end you can find yourself browsing through the millions of lines of source code (yes - it&rsquo;s that big) trying to figure out what authors had in mind. Because of it you can spend hours changing thing that should take no more than few minutes.</p>
</li>
<li>
<p>it&rsquo;s bulky, it&rsquo;s big, <strong>it&rsquo;s messy</strong>. The whole API is just like slap in a face - it&rsquo;s a stock of sometimes random functions and weird wrappers (some basically do the same thing that calling normal PHP function would do anyway, so why not just use what already exists?), starting to write a custom extension without an&hellip; extension that allows you to generate base code is generally near impossible. But I guess that&rsquo;s maybe a way of making money - let&rsquo;s create CMS so complex that every-day use will basically need technical assistance sooner or later :).</p>
</li>
<li>
<p><strong>TypoScript</strong> - one of the things you will have to learn and master just to begin using Typo3. Why reinvent the wheel and add ever more complexity? Don&rsquo;t ask me.</p>
</li>
</ul>
<p>For me - Typo3 just doesn&rsquo;t feel right. It&rsquo;s hard on a regular backend users and it can be tricky while implementing new features. It&rsquo;s supposed to speed up development process but instead it&rsquo;s often slowing it down.</p>
]]></description><content:encoded><![CDATA[<p>At my current job I have to devote a lot of time to deal with <em>enterprise Open Source CMS</em> called <a href="http://typo3.org/">Typo3</a>. If you didn&rsquo;t worked with it before it may seems like it&rsquo;s just another CMS of choice, but let me warn you - if you still considering platform for you site or whatever I strongly recommend checking it out first very carefully, you have to see for yourself how it&rsquo;s flexibility works for you. On the other hand I can&rsquo;t be really objective about it. Let&rsquo;s be honest - I&rsquo;m not a big fan of it. And apparently there is a <a href="http://hatetypo3.blogspot.com/2008/04/welcome-to-i-hate-typo3-list.html">bunch of angry programmers out there</a> who, well, are not really <em>lovin&rsquo; it</em>.</p>
<p>The three main problems with Typo that I noticed during my work experience are:</p>
<ul>
<li>
<p>chaotic documentation or <strong>lack of documentation</strong> - you have to google your way through official typo3 site, bugtrackers, some German forums etc. In the end you can find yourself browsing through the millions of lines of source code (yes - it&rsquo;s that big) trying to figure out what authors had in mind. Because of it you can spend hours changing thing that should take no more than few minutes.</p>
</li>
<li>
<p>it&rsquo;s bulky, it&rsquo;s big, <strong>it&rsquo;s messy</strong>. The whole API is just like slap in a face - it&rsquo;s a stock of sometimes random functions and weird wrappers (some basically do the same thing that calling normal PHP function would do anyway, so why not just use what already exists?), starting to write a custom extension without an&hellip; extension that allows you to generate base code is generally near impossible. But I guess that&rsquo;s maybe a way of making money - let&rsquo;s create CMS so complex that every-day use will basically need technical assistance sooner or later :).</p>
</li>
<li>
<p><strong>TypoScript</strong> - one of the things you will have to learn and master just to begin using Typo3. Why reinvent the wheel and add ever more complexity? Don&rsquo;t ask me.</p>
</li>
</ul>
<p>For me - Typo3 just doesn&rsquo;t feel right. It&rsquo;s hard on a regular backend users and it can be tricky while implementing new features. It&rsquo;s supposed to speed up development process but instead it&rsquo;s often slowing it down.</p>
]]></content:encoded></item><item><title>Installing Debian Squeeze on Buffalo Linkstation Mini (LS-WSXL)</title><link>https://awesomeprogrammer.com/blog/2012/07/23/installing-debian-squeeze-on-buffalo-linkstation-mini-ls-wsxl/</link><pubDate>Mon, 23 Jul 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/07/23/installing-debian-squeeze-on-buffalo-linkstation-mini-ls-wsxl/</guid><description><![CDATA[<p>I own a little pretty Buffalo Linkstation Mini that - I bought it over a year ago and I decided it&rsquo;s about time to get this puppy for a spin, so to say. Tempted with idea of installing Debian on it I armed myself with a bunch of tutorials and just went for it.</p>
<p>Bricket my NAS twice during this operation. Took about half a day to fully restore it, try again, fail again and finally succeed.</p>
<p>So here are few tips for all of you, so you can learn from my mistakes :P</p>
<ul>
<li>
<p><a href="http://buffalo.nas-central.org">buffalo.nas-central.org</a> has a huge amount of information, don&rsquo;t over look the forum for some more additional solutions</p>
</li>
<li>
<p>for the love of god <strong>don&rsquo;t use</strong> <a href="http://buffalo.nas-central.org/wiki/Install_Debian_6.0_%28Squeeze%29_On_Buffalo_Linkstation_Mini">this</a> instruction if you own a LS-WSXL version of linkstation, <a href="http://buffalo.nas-central.org/wiki/Debian_Squeeze_on_LS-WXL">instead use this one!</a></p>
</li>
<li>
<p>if you don&rsquo;t want to cook your own firmware to gain ssh/telnet on newer firmware access you can try:</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>java -jar acp_commander.jar -t <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -ip <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -pw password -c <span style="color:#e6db74">&#34;sed -i &#39;/PermitRootLogin/s/no/yes/&#39; /etc/sshd_config&#34;</span>
</span></span><span style="display:flex;"><span>java -jar acp_commander.jar -t <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -ip <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -pw password -c <span style="color:#e6db74">&#34;sed -i &#39;/PermitEmptyPasswords/s/^#//&#39; /etc/sshd_config&#34;</span>
</span></span><span style="display:flex;"><span>java -jar acp_commander.jar -t <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -ip <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -pw password -c <span style="color:#e6db74">&#34;sed -i &#39;/PermitEmptyPasswords/s/no/yes/&#39; /etc/sshd_config&#34;</span>
</span></span><span style="display:flex;"><span>java -jar acp_commander.jar -t <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -ip <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -pw password -c <span style="color:#e6db74">&#34;usermod -p \&#34;\&#34; root&#34;</span>
</span></span><span style="display:flex;"><span>java -jar acp_commander.jar -t <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -ip <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -pw password -c <span style="color:#e6db74">&#34;/etc/init.d/sshd.sh restart&#34;</span>
</span></span></code></pre></div><p>Worked great for me</p>
<ul>
<li>
<p>current version of deboostrap is 1.0.42 (<a href="http://ftp.us.debian.org/debian/pool/main/d/debootstrap/debootstrap_1.0.42_all.deb">get it here</a>)</p>
</li>
<li>
<p>if you will encounter problems with <em>volatile.debian.org/debian-volatile</em> in your <code>/etc/apt/sources.list</code> file - just comment it out</p>
</li>
<li>
<p>if your NAS won&rsquo;t boot after first restart - <a href="http://forum.buffalo.nas-central.org/viewtopic.php?f=61&amp;t=21954&amp;p=141663#p141663">check out this solution</a></p>
</li>
<li>
<p>if you would like to install newer packaged you can try adding: <code>deb http://ftp.us.debian.org/debian wheezy main</code> to your source list. Installing fresh transmission demon goes then like that (as root): <code>apt-get -t wheezy install transmission-daemon</code></p>
</li>
</ul>
<h3 id="2012-07-26---where-to-go-from-there">2012-07-26 - Where to go from there?</h3>
<p>After messing for a while with my brand new NAS I&rsquo;ve successfully updated old packaged using <em>wheezy</em> source lists (almost broke dependencies while doing that), didn&rsquo;t had much luck compiling kernel tho. I&rsquo;ve setup account I needed, configured samba shares and installed latest <a href="http://webmin.com">webmin</a>. So far - very happy with old-new Buffalo Mini :-).</p>
<h3 id="2012-07-28---some-screens">2012-07-28 - Some screens</h3>
<p>Some screens of my working unit.</p>
<p><img src="/images/buffalo/buffalo_mini.jpg" alt="Buffalo Mini" title="Working dissambled LS-WSXL"></p>
<p>Dissambled working unit with two Samsung 640GB drives.</p>
<p><img src="/images/buffalo/webmin.png" alt="Webmin" title="Up-to-date webmin"></p>
<p>Up-to-date system with webmin installed.</p>]]></description><content:encoded><![CDATA[<p>I own a little pretty Buffalo Linkstation Mini that - I bought it over a year ago and I decided it&rsquo;s about time to get this puppy for a spin, so to say. Tempted with idea of installing Debian on it I armed myself with a bunch of tutorials and just went for it.</p>
<p>Bricket my NAS twice during this operation. Took about half a day to fully restore it, try again, fail again and finally succeed.</p>
<p>So here are few tips for all of you, so you can learn from my mistakes :P</p>
<ul>
<li>
<p><a href="http://buffalo.nas-central.org">buffalo.nas-central.org</a> has a huge amount of information, don&rsquo;t over look the forum for some more additional solutions</p>
</li>
<li>
<p>for the love of god <strong>don&rsquo;t use</strong> <a href="http://buffalo.nas-central.org/wiki/Install_Debian_6.0_%28Squeeze%29_On_Buffalo_Linkstation_Mini">this</a> instruction if you own a LS-WSXL version of linkstation, <a href="http://buffalo.nas-central.org/wiki/Debian_Squeeze_on_LS-WXL">instead use this one!</a></p>
</li>
<li>
<p>if you don&rsquo;t want to cook your own firmware to gain ssh/telnet on newer firmware access you can try:</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>java -jar acp_commander.jar -t <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -ip <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -pw password -c <span style="color:#e6db74">&#34;sed -i &#39;/PermitRootLogin/s/no/yes/&#39; /etc/sshd_config&#34;</span>
</span></span><span style="display:flex;"><span>java -jar acp_commander.jar -t <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -ip <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -pw password -c <span style="color:#e6db74">&#34;sed -i &#39;/PermitEmptyPasswords/s/^#//&#39; /etc/sshd_config&#34;</span>
</span></span><span style="display:flex;"><span>java -jar acp_commander.jar -t <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -ip <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -pw password -c <span style="color:#e6db74">&#34;sed -i &#39;/PermitEmptyPasswords/s/no/yes/&#39; /etc/sshd_config&#34;</span>
</span></span><span style="display:flex;"><span>java -jar acp_commander.jar -t <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -ip <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -pw password -c <span style="color:#e6db74">&#34;usermod -p \&#34;\&#34; root&#34;</span>
</span></span><span style="display:flex;"><span>java -jar acp_commander.jar -t <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -ip <span style="color:#f92672">{</span>NAS_IP<span style="color:#f92672">}</span> -pw password -c <span style="color:#e6db74">&#34;/etc/init.d/sshd.sh restart&#34;</span>
</span></span></code></pre></div><p>Worked great for me</p>
<ul>
<li>
<p>current version of deboostrap is 1.0.42 (<a href="http://ftp.us.debian.org/debian/pool/main/d/debootstrap/debootstrap_1.0.42_all.deb">get it here</a>)</p>
</li>
<li>
<p>if you will encounter problems with <em>volatile.debian.org/debian-volatile</em> in your <code>/etc/apt/sources.list</code> file - just comment it out</p>
</li>
<li>
<p>if your NAS won&rsquo;t boot after first restart - <a href="http://forum.buffalo.nas-central.org/viewtopic.php?f=61&amp;t=21954&amp;p=141663#p141663">check out this solution</a></p>
</li>
<li>
<p>if you would like to install newer packaged you can try adding: <code>deb http://ftp.us.debian.org/debian wheezy main</code> to your source list. Installing fresh transmission demon goes then like that (as root): <code>apt-get -t wheezy install transmission-daemon</code></p>
</li>
</ul>
<h3 id="2012-07-26---where-to-go-from-there">2012-07-26 - Where to go from there?</h3>
<p>After messing for a while with my brand new NAS I&rsquo;ve successfully updated old packaged using <em>wheezy</em> source lists (almost broke dependencies while doing that), didn&rsquo;t had much luck compiling kernel tho. I&rsquo;ve setup account I needed, configured samba shares and installed latest <a href="http://webmin.com">webmin</a>. So far - very happy with old-new Buffalo Mini :-).</p>
<h3 id="2012-07-28---some-screens">2012-07-28 - Some screens</h3>
<p>Some screens of my working unit.</p>
<p><img src="/images/buffalo/buffalo_mini.jpg" alt="Buffalo Mini" title="Working dissambled LS-WSXL"></p>
<p>Dissambled working unit with two Samsung 640GB drives.</p>
<p><img src="/images/buffalo/webmin.png" alt="Webmin" title="Up-to-date webmin"></p>
<p>Up-to-date system with webmin installed.</p>]]></content:encoded></item><item><title>Git deployment using post-receive hook</title><link>https://awesomeprogrammer.com/blog/2012/07/20/git-deployment-using-post-receive-hook/</link><pubDate>Fri, 20 Jul 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/07/20/git-deployment-using-post-receive-hook/</guid><description><![CDATA[<p>Let&rsquo;s assume you have configured your brand new git repository on a brand new server and now you want to setup it in a way that every push done on a master branch update your production directory. How can you achieve that?</p>
<p>You can use <a href="http://git-scm.com/book/en/Customizing-Git-Git-Hooks">git hooks</a> feature. In <em>hooks/</em> directory in your git repo you can find a file called <em>post-receive</em>, take a look at quick example below:</p>
<p>{% codeblock lang:bash %}
#!/bin/bash</p>
<p>while read oldrev newrev ref
do
branch=<code>echo $ref | cut -d/ -f3</code></p>
<p>if [ &ldquo;master&rdquo; == &ldquo;$branch&rdquo; ]; then
git &ndash;work-tree=/YOUR-PRODUCTION-DIRECTORY-HERE/ checkout -f $branch
echo &lsquo;Changed pushed to production&rsquo;
fi
done
{% endcodeblock %}</p>
<p>What this script does is checking out repository to a specific directory (<em>&ndash;work-tree</em>) if this push was done on master branch. Of course you can add more stuff there, it&rsquo;s just simple example but it works great (tested with git 1.5). Unfortunately I can&rsquo;t remember where I found out about this solution, but the credit goes to the original author.</p>
<p>Also - don&rsquo;t forget to add execute permission for that file (<em>chmod +x post-receive</em>).</p>
]]></description><content:encoded><![CDATA[<p>Let&rsquo;s assume you have configured your brand new git repository on a brand new server and now you want to setup it in a way that every push done on a master branch update your production directory. How can you achieve that?</p>
<p>You can use <a href="http://git-scm.com/book/en/Customizing-Git-Git-Hooks">git hooks</a> feature. In <em>hooks/</em> directory in your git repo you can find a file called <em>post-receive</em>, take a look at quick example below:</p>
<p>{% codeblock lang:bash %}
#!/bin/bash</p>
<p>while read oldrev newrev ref
do
branch=<code>echo $ref | cut -d/ -f3</code></p>
<p>if [ &ldquo;master&rdquo; == &ldquo;$branch&rdquo; ]; then
git &ndash;work-tree=/YOUR-PRODUCTION-DIRECTORY-HERE/ checkout -f $branch
echo &lsquo;Changed pushed to production&rsquo;
fi
done
{% endcodeblock %}</p>
<p>What this script does is checking out repository to a specific directory (<em>&ndash;work-tree</em>) if this push was done on master branch. Of course you can add more stuff there, it&rsquo;s just simple example but it works great (tested with git 1.5). Unfortunately I can&rsquo;t remember where I found out about this solution, but the credit goes to the original author.</p>
<p>Also - don&rsquo;t forget to add execute permission for that file (<em>chmod +x post-receive</em>).</p>
]]></content:encoded></item><item><title>How to deal with MySQL Error 2013</title><link>https://awesomeprogrammer.com/blog/2012/07/19/how-to-deal-with-mysql-error-2013/</link><pubDate>Thu, 19 Jul 2012 00:00:00 +0000</pubDate><guid>https://awesomeprogrammer.com/blog/2012/07/19/how-to-deal-with-mysql-error-2013/</guid><description><![CDATA[<p>Some time ago I was having problem with my script that backups all the databases on the server - one day it just began to produce empty sql files. When I ran script manually I saw unpleasant:
Error 2013: Lost connection to MySQL server
while dumping one of the tables - hence empty backups.</p>
<p>First I tried to check table, repair it and optimize it, but with no luck. So I took a look at syslog (I guess should have done that first) and saw to my surprise:
InnoDB: Database page corruption on disk (&hellip;)
I will spare you the googling and give you solution that might help you too. After reading <a href="http://dev.mysql.com/doc/refman/5.0/en/forcing-innodb-recovery.html">this reference</a> I decided to set value of
innodb_force_recovery
to 1 (in <em>/etc/my.cnf</em>), restart server, quickly dump the problematic table (with this option on database is locked so if you&rsquo;re doing in on production server maybe prepare some bash scripts first), revert changes in config and restart mysql once again.</p>
<p>After that I dropped that table and without any problems imported it again - since then everything works smooth.</p>
]]></description><content:encoded><![CDATA[<p>Some time ago I was having problem with my script that backups all the databases on the server - one day it just began to produce empty sql files. When I ran script manually I saw unpleasant:
Error 2013: Lost connection to MySQL server
while dumping one of the tables - hence empty backups.</p>
<p>First I tried to check table, repair it and optimize it, but with no luck. So I took a look at syslog (I guess should have done that first) and saw to my surprise:
InnoDB: Database page corruption on disk (&hellip;)
I will spare you the googling and give you solution that might help you too. After reading <a href="http://dev.mysql.com/doc/refman/5.0/en/forcing-innodb-recovery.html">this reference</a> I decided to set value of
innodb_force_recovery
to 1 (in <em>/etc/my.cnf</em>), restart server, quickly dump the problematic table (with this option on database is locked so if you&rsquo;re doing in on production server maybe prepare some bash scripts first), revert changes in config and restart mysql once again.</p>
<p>After that I dropped that table and without any problems imported it again - since then everything works smooth.</p>
]]></content:encoded></item></channel></rss>