chore(#16): Remove jekyll files

This commit is contained in:
Josh Creek
2022-10-20 22:15:01 +01:00
parent 38b094f99e
commit a931890dcd
28 changed files with 0 additions and 2931 deletions
-5
View File
@@ -1,5 +0,0 @@
_site
.sass-cache
.jekyll-cache
.jekyll-metadata
vendor
-25
View File
@@ -1,25 +0,0 @@
---
permalink: /404.html
layout: default
---
<style type="text/css" media="screen">
.container {
margin: 10px auto;
max-width: 600px;
text-align: center;
}
h1 {
margin: 30px 0;
font-size: 4em;
line-height: 1;
letter-spacing: -1px;
}
</style>
<div class="container">
<h1>404</h1>
<p><strong>Page not found :(</strong></p>
<p>The requested page could not be found.</p>
</div>
-1
View File
@@ -1 +0,0 @@
www.jcreek.co.uk
-35
View File
@@ -1,35 +0,0 @@
source "https://rubygems.org"
# Hello! This is where you manage which Jekyll version is used to run.
# When you want to use a different version, change it below, save the
# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
#
# bundle exec jekyll serve
#
# This will help ensure the proper Jekyll version is running.
# Happy Jekylling!
gem "jekyll", "~> 3.9.0"
# This is the default theme for new Jekyll sites. You may change this to anything you like.
gem "minima", "~> 2.5"
# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
# uncomment the line below. To upgrade, run `bundle update github-pages`.
gem "github-pages", "~> 215", group: :jekyll_plugins
# If you have any plugins, put them here!
group :jekyll_plugins do
gem "jekyll-feed", "~> 0.12"
end
# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
# and associated library.
platforms :mingw, :x64_mingw, :mswin, :jruby do
gem "tzinfo", "~> 1.2"
gem "tzinfo-data"
end
# Performance-booster for watching directories on Windows
gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
# No longer bundled file, required for serving the blog locally
gem "webrick"
# Add the theme I like
gem "just-the-docs"
-298
View File
@@ -1,298 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (6.0.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2, >= 2.2.2)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.11.1)
colorator (1.1.0)
commonmarker (0.17.13)
ruby-enum (~> 0.5)
concurrent-ruby (1.1.9)
dnsruby (1.61.7)
simpleidn (~> 0.1)
em-websocket (0.5.2)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
ethon (0.14.0)
ffi (>= 1.15.0)
eventmachine (1.2.7)
eventmachine (1.2.7-x64-mingw32)
execjs (2.8.1)
faraday (1.5.1)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0.1)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.1)
faraday-patron (~> 1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.1.0)
faraday-patron (1.0.0)
ffi (1.15.3)
ffi (1.15.3-x64-mingw32)
forwardable-extended (2.6.0)
gemoji (3.0.1)
github-pages (215)
github-pages-health-check (= 1.17.2)
jekyll (= 3.9.0)
jekyll-avatar (= 0.7.0)
jekyll-coffeescript (= 1.1.1)
jekyll-commonmark-ghpages (= 0.1.6)
jekyll-default-layout (= 0.1.4)
jekyll-feed (= 0.15.1)
jekyll-gist (= 1.5.0)
jekyll-github-metadata (= 2.13.0)
jekyll-mentions (= 1.6.0)
jekyll-optional-front-matter (= 0.3.2)
jekyll-paginate (= 1.1.0)
jekyll-readme-index (= 0.3.0)
jekyll-redirect-from (= 0.16.0)
jekyll-relative-links (= 0.6.1)
jekyll-remote-theme (= 0.4.3)
jekyll-sass-converter (= 1.5.2)
jekyll-seo-tag (= 2.7.1)
jekyll-sitemap (= 1.4.0)
jekyll-swiss (= 1.0.0)
jekyll-theme-architect (= 0.1.1)
jekyll-theme-cayman (= 0.1.1)
jekyll-theme-dinky (= 0.1.1)
jekyll-theme-hacker (= 0.1.2)
jekyll-theme-leap-day (= 0.1.1)
jekyll-theme-merlot (= 0.1.1)
jekyll-theme-midnight (= 0.1.1)
jekyll-theme-minimal (= 0.1.1)
jekyll-theme-modernist (= 0.1.1)
jekyll-theme-primer (= 0.5.4)
jekyll-theme-slate (= 0.1.1)
jekyll-theme-tactile (= 0.1.1)
jekyll-theme-time-machine (= 0.1.1)
jekyll-titles-from-headings (= 0.5.3)
jemoji (= 0.12.0)
kramdown (= 2.3.1)
kramdown-parser-gfm (= 1.1.0)
liquid (= 4.0.3)
mercenary (~> 0.3)
minima (= 2.5.1)
nokogiri (>= 1.10.4, < 2.0)
rouge (= 3.26.0)
terminal-table (~> 1.4)
github-pages-health-check (1.17.2)
addressable (~> 2.3)
dnsruby (~> 1.60)
octokit (~> 4.0)
public_suffix (>= 2.0.2, < 5.0)
typhoeus (~> 1.3)
html-pipeline (2.14.0)
activesupport (>= 2)
nokogiri (>= 1.4)
http_parser.rb (0.6.0)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
jekyll (3.9.0)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
i18n (~> 0.7)
jekyll-sass-converter (~> 1.0)
jekyll-watch (~> 2.0)
kramdown (>= 1.17, < 3)
liquid (~> 4.0)
mercenary (~> 0.3.3)
pathutil (~> 0.9)
rouge (>= 1.7, < 4)
safe_yaml (~> 1.0)
jekyll-avatar (0.7.0)
jekyll (>= 3.0, < 5.0)
jekyll-coffeescript (1.1.1)
coffee-script (~> 2.2)
coffee-script-source (~> 1.11.1)
jekyll-commonmark (1.3.1)
commonmarker (~> 0.14)
jekyll (>= 3.7, < 5.0)
jekyll-commonmark-ghpages (0.1.6)
commonmarker (~> 0.17.6)
jekyll-commonmark (~> 1.2)
rouge (>= 2.0, < 4.0)
jekyll-default-layout (0.1.4)
jekyll (~> 3.0)
jekyll-feed (0.15.1)
jekyll (>= 3.7, < 5.0)
jekyll-gist (1.5.0)
octokit (~> 4.2)
jekyll-github-metadata (2.13.0)
jekyll (>= 3.4, < 5.0)
octokit (~> 4.0, != 4.4.0)
jekyll-mentions (1.6.0)
html-pipeline (~> 2.3)
jekyll (>= 3.7, < 5.0)
jekyll-optional-front-matter (0.3.2)
jekyll (>= 3.0, < 5.0)
jekyll-paginate (1.1.0)
jekyll-readme-index (0.3.0)
jekyll (>= 3.0, < 5.0)
jekyll-redirect-from (0.16.0)
jekyll (>= 3.3, < 5.0)
jekyll-relative-links (0.6.1)
jekyll (>= 3.3, < 5.0)
jekyll-remote-theme (0.4.3)
addressable (~> 2.0)
jekyll (>= 3.5, < 5.0)
jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
rubyzip (>= 1.3.0, < 3.0)
jekyll-sass-converter (1.5.2)
sass (~> 3.4)
jekyll-seo-tag (2.7.1)
jekyll (>= 3.8, < 5.0)
jekyll-sitemap (1.4.0)
jekyll (>= 3.7, < 5.0)
jekyll-swiss (1.0.0)
jekyll-theme-architect (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-cayman (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-dinky (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-hacker (0.1.2)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-leap-day (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-merlot (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-midnight (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-minimal (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-modernist (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-primer (0.5.4)
jekyll (> 3.5, < 5.0)
jekyll-github-metadata (~> 2.9)
jekyll-seo-tag (~> 2.0)
jekyll-theme-slate (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-tactile (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-time-machine (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-titles-from-headings (0.5.3)
jekyll (>= 3.3, < 5.0)
jekyll-watch (2.2.1)
listen (~> 3.0)
jemoji (0.12.0)
gemoji (~> 3.0)
html-pipeline (~> 2.2)
jekyll (>= 3.0, < 5.0)
just-the-docs (0.3.3)
jekyll (>= 3.8.5)
jekyll-seo-tag (~> 2.0)
rake (>= 12.3.1, < 13.1.0)
kramdown (2.3.1)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.3)
listen (3.5.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.3.6)
minima (2.5.1)
jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
minitest (5.14.4)
multipart-post (2.1.1)
nokogiri (1.13.6-x64-mingw32)
racc (~> 1.4)
nokogiri (1.13.6-x86_64-linux)
racc (~> 1.4)
octokit (4.21.0)
faraday (>= 0.9)
sawyer (~> 0.8.0, >= 0.5.3)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (4.0.6)
racc (1.6.0)
rake (13.0.6)
rb-fsevent (0.11.0)
rb-inotify (0.10.1)
ffi (~> 1.0)
rexml (3.2.5)
rouge (3.26.0)
ruby-enum (0.9.0)
i18n
ruby2_keywords (0.0.4)
rubyzip (2.3.2)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sawyer (0.8.2)
addressable (>= 2.3.5)
faraday (> 0.8, < 2.0)
simpleidn (0.2.1)
unf (~> 0.1.4)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (1.2.10)
thread_safe (~> 0.1)
tzinfo-data (1.2021.1)
tzinfo (>= 1.0.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
unf_ext (0.0.7.7-x64-mingw32)
unicode-display_width (1.7.0)
wdm (0.1.1)
webrick (1.7.0)
zeitwerk (2.4.2)
PLATFORMS
x64-mingw32
x86_64-linux
DEPENDENCIES
github-pages (~> 215)
jekyll (~> 3.9.0)
jekyll-feed (~> 0.12)
just-the-docs
minima (~> 2.5)
tzinfo (~> 1.2)
tzinfo-data
wdm (~> 0.1.1)
webrick
BUNDLED WITH
2.2.23
-120
View File
@@ -1,120 +0,0 @@
# Welcome to Jekyll!
#
# This config file is meant for settings that affect your whole blog, values
# which you are expected to set up once and rarely edit after that. If you find
# yourself editing this file very often, consider using Jekyll's data files
# feature for the data you need to update frequently.
#
# For technical reasons, this file is *NOT* reloaded automatically when you use
# 'bundle exec jekyll serve'. If you change this file, please restart the server process.
#
# If you need help with YAML syntax, here are some quick references for you:
# https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml
# https://learnxinyminutes.com/docs/yaml/
#
# Site settings
# These are used to personalize your new site. If you look in the HTML files,
# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
# You can create any custom variable you would like, and they will be accessible
# in the templates via {{ site.myvariable }}.
title: Josh Creek's Blog
email: your-email@example.com
description: >- # this means to ignore newlines until "baseurl:"
A blog covering anything I found interesting or useful in the worlds of technology and code.
baseurl: "" # the subpath of your site, e.g. /blog
url: "https://www.jcreek.co.uk" # the base hostname & protocol for your site, e.g. http://example.com
twitter_username: jcreek23
github_username: jcreek
# Build settings
#theme: just-the-docs
remote_theme: pmarsceill/just-the-docs
plugins:
- jekyll-feed
# Exclude from processing.
# The following items will not be processed, by default.
# Any item listed under the `exclude:` key here will be automatically added to
# the internal "default list".
#
# Excluded items can be processed by explicitly listing the directories or
# their entries' file path in the `include:` list.
#
# exclude:
# - .sass-cache/
# - .jekyll-cache/
# - gemfiles/
# - Gemfile
# - Gemfile.lock
# - node_modules/
# - vendor/bundle/
# - vendor/cache/
# - vendor/gems/
# - vendor/ruby/
# Set a path/url to a logo that will be displayed instead of the title
# logo: "profile.png"
# Aux links for the upper right navigation
aux_links:
"GitHub":
- "//github.com/jcreek"
"Twitter":
- "//twitter.com/jcreek23"
# Makes Aux links open in a new tab. Default is false
aux_links_new_tab: true
# Enable or disable the site search
# Supports true (default) or false
search_enabled: true
search:
# Split pages into sections that can be searched individually
# Supports 1 - 6, default: 2
heading_level: 2
# Maximum amount of previews per search result
# Default: 3
previews: 3
# Maximum amount of words to display before a matched word in the preview
# Default: 5
preview_words_before: 5
# Maximum amount of words to display after a matched word in the preview
# Default: 10
preview_words_after: 10
# Set the search token separator
# Default: /[\s\-/]+/
# Example: enable support for hyphenated search words
tokenizer_separator: /[\s/]+/
# Display the relative url in search results
# Supports true (default) or false
rel_url: true
# Enable or disable the search button that appears in the bottom right corner of every page
# Supports true or false (default)
button: false
# Footer content
# appears at the bottom of every page's main content
# Note: The footer_content option is deprecated and will be removed in a future major release. Please use `_includes/footer_custom.html` for more robust markup / liquid-based content.
footer_content: "Copyright &copy; 2017-2020 Josh Creek. No reproduction allowed without permission."
# Footer last edited timestamp
last_edit_timestamp: true # show or hide edit time - page must have `last_modified_date` defined in the frontmatter
last_edit_time_format: "%b %e %Y at %I:%M %p" # uses ruby's time format: https://ruby-doc.org/stdlib-2.7.0/libdoc/time/rdoc/Time.html
# Footer "Edit this page on GitHub" link text
gh_edit_link: false # show or hide edit this page link
gh_edit_link_text: "Edit this page on GitHub."
gh_edit_repository: "https://github.com/pmarsceill/just-the-docs" # the github URL for your repo
gh_edit_branch: "master" # the branch that your docs is served from
# gh_edit_source: docs # the source that your files originate from
gh_edit_view_mode: "tree" # "tree" or "edit" if you want the user to jump into the editor immediately
# Color scheme supports "light" (default) and "dark"
color_scheme: light
# Google Analytics Tracking (optional)
# e.g, UA-1234567-89
ga_tracking: UA-201885349-1
ga_tracking_anonymize_ip: true # Use GDPR compliant Google Analytics settings (true by default)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 611 KiB

@@ -1,321 +0,0 @@
---
layout: post
parent: Containers & Deploying
nav_order: 1
title: "Docker Compose for ASP.Net Core with Postgres + S3 backups"
date: 2021-07-12 21:01:45 +0100
categories: deploying docker
---
# {{page.title}}
_{{page.date}}_
In this post, I will cover how I set up the following application structure, run from a single `docker-compose` file:
- ASP.Net Core MVC & Razor Web Application
- ASP.Net Core Entity Framework Migrations
- PostgreSQL
- SMTP Server
- S3 Backups for PostgreSQL
- S3 Restores for PostgreSQL
The directory structure, including pertinent files, looks like this.
```
Repository Root Folder
│ .dockerignore
│ database.env
│ docker-compose.yml
└───ASP.Net Core Project Folder
│ │ Dockerfile
│ │ Migrations.Dockerfile
│ │ Setup.sh
└───postgres-backup-s3
│ │ Dockerfile
└───postgres-restore-s3
│ │ Dockerfile
```
Starting with an almost blank `docker-compose.yml` file, over the course of this post we'll add each service so that the entire infrastructure can be brought up with one single `docker-compose up` command.
```docker
version: '3.4'
services:
```
N.B. One optional thing not covered here is to include `restart: always` for each service in the `docker-compose.yml` file to ensure they come back online after the host machine reboots. I wouldn't recommend using this for anything but the core web app, database and mail server.
## ASP.Net Core Web Application
For this we want to specify a few key things:
- the name to give the container, to make it easier to work with than the docker auto-generated names
- the internal port to expose on the host machine's external port
- the Dockerfile to use to build the application
- the folder to map to make log files accessible from the host machine without having to use the terminal
- the environment variable to set the app into production mode instead of the default
- the other containers this one will depend on
```docker
version: '3.4'
services:
aspprojectname:
container_name: myaspprojectname
ports:
- "80:80"
build:
context: .
dockerfile: MyAspProjectName/Dockerfile
volumes:
- ./MyAspProjectName/logs:/app/logs
environment:
- ASPNETCORE_ENVIRONMENT=Production
depends_on:
- db
# - migrations
```
You will notice here that I am commenting out the migrations dependency. This is because with the S3 backup and restore images it's not really needed, and it requires more powerful hardware to run than any of the other images and is a large docker image, so worth avoiding if possible, or only using to set up the database initially, then deleting.
The Dockerfile itself is pretty standard for ASP.Net Core apps. In this instance, the app is running on dotnet 5.
```docker
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["MyAspProjectName/MyAspProjectName.csproj", "MyAspProjectName/"]
RUN dotnet restore "MyAspProjectName/MyAspProjectName.csproj" --disable-parallel
COPY . .
WORKDIR "/src/MyAspProjectName"
RUN dotnet build "MyAspProjectName.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "MyAspProjectName.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyAspProjectName.dll"]
```
## ASP.Net Core Entity Framework Migrations
This one is pretty similar to the main web app in terms of what needs adding to add the service to the `docker-compose.yml` file.
```docker
migrations:
container_name: dbmigrations
build:
context: .
dockerfile: MyAspProjectName/Migrations.Dockerfile
environment:
- ASPNETCORE_ENVIRONMENT=Production
depends_on:
- db
```
The Dockerfile itself is where the magic happens, building the web app, installing the global dotnet-ef tools needed and then running the migrations.
```docker
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["MyAspProjectName/MyAspProjectName.csproj", "MyAspProjectName/"]
COPY ["MyAspProjectName/Setup.sh", "MyAspProjectName/"]
ENV PATH="${PATH}:/root/.dotnet/tools"
RUN dotnet tool install --global dotnet-ef
RUN dotnet restore "MyAspProjectName/MyAspProjectName.csproj" --disable-parallel
COPY . .
WORKDIR "/src/MyAspProjectName/."
RUN /root/.dotnet/tools/dotnet-ef migrations add InitialMigrations
RUN chmod +x ./Setup.sh
CMD /bin/bash ./Setup.**sh**
```
Personally, I don't use this unless I have to, for the reasons previously stated, but it's worth sharing in case it's useful to anyone else.
## PostgreSQL
There's two key parts to add to the `docker-compose.yml` file for this one. The service itself, with the port to expose and an `env` file to store sensitive information, and the volume mapped to a directory within the container.
```docker
services:
db:
container_name: myappdb
image: "postgres"
ports:
- "5432:5432"
env_file:
- database.env # configure postgres
volumes:
- database-data:/var/lib/postgresql/data/ # persist data even if container shuts down
volumes:
database-data: # named volumes can be managed easier using docker-compose
```
That `env` file is remarkably simple.
```text
POSTGRES_USER=postgres
POSTGRES_PASSWORD=passwordgoeshere
POSTGRES_DB=yourdbname
```
## SMTP Server
The service for the SMTP server is super simple.
```docker
mail:
image: bytemark/smtp
```
## S3 Backups for PostgreSQL
The `docker-compose.yml` for this is as below, which sets a daily schedule and connection details for both the Postgres database and S3. Note that the Postgres host uses the internal Docker hostname for the database container.
```docker
pgbackups3:
build:
context: .
dockerfile: postgres-backup-s3/Dockerfile
links:
- db
environment:
SCHEDULE: '@daily'
S3_REGION: eu-west-2
S3_ACCESS_KEY_ID: keygoeshere
S3_SECRET_ACCESS_KEY: secretkeygoeshere
S3_BUCKET: yourapp-backups
S3_PREFIX: backup
POSTGRES_HOST: db
POSTGRES_DATABASE: yourdbname
POSTGRES_USER: postgres
POSTGRES_PASSWORD: passwordgoeshere
POSTGRES_EXTRA_OPTS: '--schema=public --blobs'
```
I won't go into how this application works here, that'll be in another post.
## S3 Restores for PostgreSQL
The `docker-compose.yml` for this is as below, which sets connection details for both the Postgres database and S3. Note that the Postgres host uses the internal Docker hostname for the database container.
This container should be run at setup time then stopped and commented out of the `docker-compose.yml` file or removed entirely, to prevent it from being accidentally run and restoring unintentionally, overwriting the database (notice the drop public option - this will wipe everything in the database before restoring).
```docker
pgrestores3:
build:
context: .
dockerfile: postgres-restore-s3/Dockerfile
links:
- db
environment:
S3_ACCESS_KEY_ID: keygoeshere
S3_SECRET_ACCESS_KEY: secretkeygoeshere
S3_BUCKET: yourapp-backups
S3_PREFIX: backup
POSTGRES_HOST: db
POSTGRES_DATABASE: yourdbname
POSTGRES_USER: postgres
POSTGRES_PASSWORD: passwordgoeshere
DROP_PUBLIC: 'yes'
```
I won't go into how this application works here, that'll be in another post.
## Putting it all together
The complete `docker-compose.yml` file, after the initial `docker-compose up` command has been run, looks like this. Note that migrations is commented out (we don't need it as we have the S3 backup and restore) and that the S3 restore is commented out to prevent accidental restores.
```docker
version: '3.4'
services:
aspprojectname:
container_name: myaspprojectname
ports:
- "80:80"
build:
context: .
dockerfile: MyAspProjectName/Dockerfile
volumes:
- ./MyAspProjectName/logs:/app/logs
environment:
- ASPNETCORE_ENVIRONMENT=Production
depends_on:
- db
# - migrations
# migrations:
# container_name: dbmigrations
# build:
# context: .
# dockerfile: MyAspProjectName/Migrations.Dockerfile
# environment:
# - ASPNETCORE_ENVIRONMENT=Production
# depends_on:
# - db
db:
container_name: myappdb
image: "postgres"
ports:
- "5432:5432"
env_file:
- database.env # configure postgres
volumes:
- database-data:/var/lib/postgresql/data/ # persist data even if container shuts down
mail:
image: bytemark/smtp
pgbackups3:
build:
context: .
dockerfile: postgres-backup-s3/Dockerfile
links:
- db
environment:
SCHEDULE: '@daily'
S3_REGION: eu-west-2
S3_ACCESS_KEY_ID: keygoeshere
S3_SECRET_ACCESS_KEY: secretkeygoeshere
S3_BUCKET: yourapp-backups
S3_PREFIX: backup
POSTGRES_HOST: db
POSTGRES_DATABASE: yourdbname
POSTGRES_USER: postgres
POSTGRES_PASSWORD: passwordgoeshere
POSTGRES_EXTRA_OPTS: '--schema=public --blobs'
# pgrestores3:
# build:
# context: .
# dockerfile: postgres-restore-s3/Dockerfile
# links:
# - db
# environment:
# S3_ACCESS_KEY_ID: keygoeshere
# S3_SECRET_ACCESS_KEY: secretkeygoeshere
# S3_BUCKET: yourapp-backups
# S3_PREFIX: backup
# POSTGRES_HOST: db
# POSTGRES_DATABASE: yourdbname
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: passwordgoeshere
# DROP_PUBLIC: 'yes'
volumes:
database-data: # named volumes can be managed easier using docker-compose
```
@@ -1,316 +0,0 @@
---
layout: post
parent: Containers & Deploying
nav_order: 2
title: "Docker Compose for S3 Backup and Restore of PostgreSQL"
date: 2021-07-12 21:27:19 +0100
categories: deploying docker
---
# {{page.title}}
_{{page.date}}_
In this post, I will cover how you can set up two Docker containers for backing up and restoring Postgres databases using S3 in AWS.
The complete `docker-compose.yml` file looks like this. Note that it's set to link these containers to a database container also configured within the same compose file - this could be modified to connect to an external database if desired by removing the `links` and changing the `POSTGRES_HOST` environment variables.
```docker
version: '3.4'
services:
pgbackups3:
build:
context: .
dockerfile: postgres-backup-s3/Dockerfile
links:
- db
environment:
SCHEDULE: '@daily'
S3_REGION: eu-west-2
S3_ACCESS_KEY_ID: keygoeshere
S3_SECRET_ACCESS_KEY: secretkeygoeshere
S3_BUCKET: yourapp-backups
S3_PREFIX: backup
POSTGRES_HOST: db
POSTGRES_DATABASE: yourdbname
POSTGRES_USER: postgres
POSTGRES_PASSWORD: passwordgoeshere
POSTGRES_EXTRA_OPTS: '--schema=public --blobs'
pgrestores3:
build:
context: .
dockerfile: postgres-restore-s3/Dockerfile
links:
- db
environment:
S3_ACCESS_KEY_ID: keygoeshere
S3_SECRET_ACCESS_KEY: secretkeygoeshere
S3_BUCKET: yourapp-backups
S3_PREFIX: backup
POSTGRES_HOST: db
POSTGRES_DATABASE: yourdbname
POSTGRES_USER: postgres
POSTGRES_PASSWORD: passwordgoeshere
DROP_PUBLIC: 'yes'
```
## Backing up PostgreSQL to S3
The Dockerfile used looks like this:
```docker
FROM alpine:3.13
RUN apk update \
&& apk add coreutils \
&& apk add postgresql-client \
&& apk add python3 py3-pip && pip3 install --upgrade pip && pip3 install awscli \
&& apk add openssl \
&& apk add curl \
&& curl -L --insecure https://github.com/odise/go-cron/releases/download/v0.0.6/go-cron-linux.gz | zcat > /usr/local/bin/go-cron && chmod u+x /usr/local/bin/go-cron \
&& apk del curl \
&& rm -rf /var/cache/apk/*
ENV POSTGRES_DATABASE **None**
ENV POSTGRES_HOST **None**
ENV POSTGRES_PORT 5432
ENV POSTGRES_USER **None**
ENV POSTGRES_PASSWORD **None**
ENV POSTGRES_EXTRA_OPTS ''
ENV S3_ACCESS_KEY_ID **None**
ENV S3_SECRET_ACCESS_KEY **None**
ENV S3_BUCKET **None**
ENV S3_REGION us-west-1
ENV S3_PATH 'backup'
ENV S3_ENDPOINT **None**
ENV S3_S3V4 no
ENV SCHEDULE **None**
COPY ["postgres-backup-s3/run.sh", "run.sh"]
COPY ["postgres-backup-s3/backup.sh", "backup.sh"]
CMD ["sh", "run.sh"]
```
This is a combination of parts from [here](https://github.com/itbm/postgresql-backup-s3/blob/master/Dockerfile) and the original [here](https://github.com/schickling/dockerfiles/tree/master/postgres-backup-s3) with some further customisation by me.
It essentially sets up a Linux environment to be able to connect to both S3 and Postgres, and to be able to run two script files.
### `run.sh`
This one handles a bit of aws configuration for S3 connections, as well as setting up the behaviour for scheduling or just immediately running the container. I like to set it to run daily so I can set it and forget it.
```bash
#! /bin/sh
set -e
if [ "${S3_S3V4}" = "yes" ]; then
aws configure set default.s3.signature_version s3v4
fi
if [ "${SCHEDULE}" = "**None**" ]; then
sh backup.sh
else
exec go-cron "$SCHEDULE" /bin/sh backup.sh
fi
```
### `backup.sh`
This one handles checking all the requisite connection details are present, then makes a compressed dump file of the PostgreSQL database, and uploads it to S3.
```bash
#! /bin/sh
set -e
set -o pipefail
if [ "${S3_ACCESS_KEY_ID}" = "**None**" ]; then
echo "You need to set the S3_ACCESS_KEY_ID environment variable."
exit 1
fi
if [ "${S3_SECRET_ACCESS_KEY}" = "**None**" ]; then
echo "You need to set the S3_SECRET_ACCESS_KEY environment variable."
exit 1
fi
if [ "${S3_BUCKET}" = "**None**" ]; then
echo "You need to set the S3_BUCKET environment variable."
exit 1
fi
if [ "${POSTGRES_DATABASE}" = "**None**" ]; then
echo "You need to set the POSTGRES_DATABASE environment variable."
exit 1
fi
if [ "${POSTGRES_HOST}" = "**None**" ]; then
if [ -n "${POSTGRES_PORT_5432_TCP_ADDR}" ]; then
POSTGRES_HOST=$POSTGRES_PORT_5432_TCP_ADDR
POSTGRES_PORT=$POSTGRES_PORT_5432_TCP_PORT
else
echo "You need to set the POSTGRES_HOST environment variable."
exit 1
fi
fi
if [ "${POSTGRES_USER}" = "**None**" ]; then
echo "You need to set the POSTGRES_USER environment variable."
exit 1
fi
if [ "${POSTGRES_PASSWORD}" = "**None**" ]; then
echo "You need to set the POSTGRES_PASSWORD environment variable or link to a container named POSTGRES."
exit 1
fi
if [ "${S3_ENDPOINT}" == "**None**" ]; then
AWS_ARGS=""
else
AWS_ARGS="--endpoint-url ${S3_ENDPOINT}"
fi
# env vars needed for aws tools
export AWS_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY
export AWS_DEFAULT_REGION=$S3_REGION
export PGPASSWORD=$POSTGRES_PASSWORD
POSTGRES_HOST_OPTS="-h $POSTGRES_HOST -p $POSTGRES_PORT -U $POSTGRES_USER $POSTGRES_EXTRA_OPTS"
echo "Creating dump of ${POSTGRES_DATABASE} database from ${POSTGRES_HOST}..."
pg_dump $POSTGRES_HOST_OPTS $POSTGRES_DATABASE | gzip > dump.sql.gz
echo "Uploading dump to $S3_BUCKET"
cat dump.sql.gz | aws $AWS_ARGS s3 cp - s3://$S3_BUCKET/$S3_PREFIX/${POSTGRES_DATABASE}_$(date +"%Y-%m-%dT%H:%M:%SZ").sql.gz || exit 2
echo "SQL backup uploaded successfully"
```
## Restoring PostgreSQL from S3
The Dockerfile used looks like this:
```docker
FROM alpine:3.13
RUN apk update \
&& apk add coreutils \
&& apk add postgresql-client \
&& apk add python3 py3-pip && pip3 install --upgrade pip && pip3 install awscli \
&& apk add openssl \
&& apk add curl \
&& curl -L --insecure https://github.com/odise/go-cron/releases/download/v0.0.6/go-cron-linux.gz | zcat > /usr/local/bin/go-cron && chmod u+x /usr/local/bin/go-cron \
&& apk del curl \
&& rm -rf /var/cache/apk/*
ENV POSTGRES_DATABASE **None**
ENV POSTGRES_HOST **None**
ENV POSTGRES_PORT 5432
ENV POSTGRES_USER **None**
ENV POSTGRES_PASSWORD **None**
ENV S3_ACCESS_KEY_ID **None**
ENV S3_SECRET_ACCESS_KEY **None**
ENV S3_BUCKET **None**
ENV S3_REGION us-west-1
ENV S3_PATH 'backup'
ENV DROP_PUBLIC 'no'
COPY ["postgres-restore-s3/restore.sh", "restore.sh"]
CMD ["sh", "restore.sh"]
```
This is once again a combination of parts from [here](https://github.com/itbm/postgresql-backup-s3/blob/master/Dockerfile) and the original [here](https://github.com/schickling/dockerfiles/tree/master/postgres-backup-s3) with some further customisation by me.
It essentially sets up a Linux environment to be able to connect to both S3 and Postgres, and to be able to run one script file.
### `restore.sh`
This one handles checking all the requisite connection details are present, then retrieves the most recent compressed dump file of the PostgreSQL database from S3, drops everything in the public space in PostgreSQL, and restores the backup. Crucially it then deletes the downloaded backup dump file to enable running the same container again for future restores without issue.
```bash
#! /bin/sh
set -e
set -o pipefail
if [ "${S3_ACCESS_KEY_ID}" = "**None**" ]; then
echo "You need to set the S3_ACCESS_KEY_ID environment variable."
exit 1
fi
if [ "${S3_SECRET_ACCESS_KEY}" = "**None**" ]; then
echo "You need to set the S3_SECRET_ACCESS_KEY environment variable."
exit 1
fi
if [ "${S3_BUCKET}" = "**None**" ]; then
echo "You need to set the S3_BUCKET environment variable."
exit 1
fi
if [ "${POSTGRES_DATABASE}" = "**None**" ]; then
echo "You need to set the POSTGRES_DATABASE environment variable."
exit 1
fi
if [ "${POSTGRES_HOST}" = "**None**" ]; then
if [ -n "${POSTGRES_PORT_5432_TCP_ADDR}" ]; then
POSTGRES_HOST=$POSTGRES_PORT_5432_TCP_ADDR
POSTGRES_PORT=$POSTGRES_PORT_5432_TCP_PORT
else
echo "You need to set the POSTGRES_HOST environment variable."
exit 1
fi
fi
if [ "${POSTGRES_USER}" = "**None**" ]; then
echo "You need to set the POSTGRES_USER environment variable."
exit 1
fi
if [ "${POSTGRES_PASSWORD}" = "**None**" ]; then
echo "You need to set the POSTGRES_PASSWORD environment variable or link to a container named POSTGRES."
exit 1
fi
# env vars needed for aws tools
export AWS_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY
export AWS_DEFAULT_REGION=$S3_REGION
export PGPASSWORD=$POSTGRES_PASSWORD
POSTGRES_HOST_OPTS="-h $POSTGRES_HOST -p $POSTGRES_PORT -U $POSTGRES_USER"
echo "Finding latest backup"
LATEST_BACKUP=$(aws s3 ls s3://$S3_BUCKET/$S3_PREFIX/ | sort | tail -n 1 | awk '{ print $4 }')
echo "Fetching ${LATEST_BACKUP} from S3"
aws s3 cp s3://$S3_BUCKET/$S3_PREFIX/${LATEST_BACKUP} dump.sql.gz
gzip -d dump.sql.gz
if [ "${DROP_PUBLIC}" == "yes" ]; then
echo "Recreating the public schema"
psql $POSTGRES_HOST_OPTS -d $POSTGRES_DATABASE -c "drop schema public cascade; create schema public;"
fi
echo "Restoring ${LATEST_BACKUP}"
psql $POSTGRES_HOST_OPTS -d $POSTGRES_DATABASE < dump.sql
echo "Restore complete"
rm -f ./dump.sql
echo "Deleted dump files"
```
@@ -1,67 +0,0 @@
---
layout: post
parent: Containers & Deploying
nav_order: 3
title: "Docker Compose for Elasticsearch and Kibana 7.9"
date: 2021-07-13 21:38:05 +0100
categories: deploying docker
---
# {{page.title}}
_{{page.date}}_
In this post, I will cover how you can set up Elasticsearch and Kibana with a single `docker-compose.yml` file.
## Set up and install ELK stack on Ubuntu server
You must run `sudo sysctl -w vm.max_map_count=262144` to get Elasticsearch to work. To make this permanent, run `sudo nano /etc/sysctl.conf` and add `vm.max_map_count=262144` to the end of the file on a new line, then save and exit.
Go to the directory with the files in. Make sure you've used SFTP to put the `docker-compose.yml` file in there first. For example you may need to use this command `cd /home/administrator/docker/elk-stack`
Install it all with `docker-compose up -d` and you're finished! It really is that simple.
The compose file is as below.
```docker
version: '2.2'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.9.3
container_name: elasticsearch
environment:
- node.name=elasticsearch
- discovery.seed_hosts=elasticsearch
- cluster.initial_master_nodes=elasticsearch
- cluster.name=docker-cluster
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
volumes:
- esdata1:/usr/share/elasticsearch/data
ports:
- 9200:9200
kibana:
image: docker.elastic.co/kibana/kibana:7.9.3
container_name: kibana
environment:
ELASTICSEARCH_URL: "http://elasticsearch:9200"
ELASTICSEARCH_SHARDTIMEOUT: 300000
ports:
- 5601:5601
depends_on:
- elasticsearch
volumes:
esdata1:
driver: local
```
-6
View File
@@ -1,6 +0,0 @@
---
layout: default
title: Containers & Deploying
nav_order: 2
has_children: true
---
-189
View File
@@ -1,189 +0,0 @@
---
layout: post
parent: Development Teams
nav_order: 1
title: "The way I recommend using git in a dev team"
date: 2022-03-11 22:46:33 +0000
categories: git
---
# {{page.title}}
_{{page.date}}_
Looking back at my old git commits reveals an entertaining, if slightly embarassing mess.
```
$ git log --oneline -10 --author jcreek --before "Wed Nov 5 2014"
8b6d283 Updated instructions
6b271ba Undid that last change
8c4f48a MySQL -> MySQLi changes begun
c07ae83 Fixed that bug
e5ab087 Some small cosmetic changes and failed fix of bug
4467af9 Updated to-do list
3578065 Cleanup and to-do list
ddefa3a Added admin tools, provided link to admin page
8a2f8b6 Further admin tools
63cc797 Unimportant changes
```
The contrast with more recent commits is staggering.
```
$ git log --oneline -13 --author jcreek --before "Wed Sep 1 2021"
c05c2c5 build(CAN-186): Add missing package information to csproj
06aca19 docs(CAN-190): Add documentation to StringHelper
4600ebf (tag: 1.0.1) feat(CAN-182): Add filename validity checks to SftpRepository
d80cc82 (tag: 1.0.0) chore(CAN-173): Update readme and licence for NuGet
615994a refactor(CAN-215): Rename to Creek.FileRepository
0fe9924 feat(CAN-171): Add all repositories to factory
b22d2f7 feat(CAN-170): Add base implementations of all repositories
952a9a9 refactor(CAN-211): Rename FileName to Filename in interface
62665fe test(CAN-169): Add SftpRepositoryShould tests
ebccd4a feat(CAN-168): Add GenerateStreamFromString helper method for testing
82fd258 fix(CAN-135): Initialise content with a new stream so it can be written to
4232dd9 fix(CAN-162): Add missing throw to ensure exceptions get passed up the stack
26a953d feat(CAN-167): Add initialiser for SftpRepository to use an IConfiguration
```
My recent commits follow a clear convention, are concise and clear at a glance. Without having to do any code comparison you can quickly assess what was done in any given commit. A commit message shows whether a developer is a good collaborator, with good ones reducing the need to re-establish the context of a commit as much as possible.
I never used to care about my commit messages because it was only me reading them, and because I rarely, if ever, used tools like `git log`, `git blame`, `revert` or `rebase`. Nowadays I use git blame all day, every day, thanks to VSCode git extensions putting the blame on every line of code, and it's incredibly useful as part of a team. If I'm looking at code I always know who worked on it, when they worked on it, and the context of why they did what they did, which makes the codebases infinitely more maintainable.
There is a single git commit that I think should be required reading for all developers. It is from a developer by the name of [Dan Carley](https://twitter.com/dancarley) working on GOV.UK, and it has the rather unassuming name of ["Convert template to US-ASCII to fix error"](https://github.com/alphagov/govuk-puppet/commit/63b36f93bf75a848e2125008aa1e880c5861cf46).
![The Carley Commit](/assets/the-carley-commit.png)
I found it through [this blog](https://dhwthompson.com/2019/my-favourite-git-commit) and I'd highly recommend reading through it, but essentially the thing I like most about this commit is it explains why the change was made, not just what the change was.
As with most developers, I have my own strong opinions founded in habit for what dev teams should do in terms of Git usage. In the rest of this doc I will outline that - let me know if you have any suggestions for improvements, or if it helped you.
## Branching, Pull Requests & Merging
For every ticket, there should be a separate git branch. This code will not be merged into the main development branch until it has been through a pull request and been reviewed.
If it's a big feature ticket, a feature branch should be created for that ticket, and it should be split into sub-tasks, each of which have their own branch and get merged into the feature branch after PRs are approved. Once all the sub-tasks have been PRed and their branches have been merged into the feature branch, the feature branch can be PRed and merge into the development branch.
Unless you're working on an open source project, the merge strategy for PRs should not involve squashing. Merge commits are fine, and they leave all individual commits available for comparison, reverting and cherry-picking.
Not using squashed commits makes having good quality commit messages even more important. Committing often and messily is a bad habit. Instead break down tasks into suitably small sub-tasks, so that they can be worked on in isolation without taking long enough to warrant committing unfinished code.
A key part of the PR process is reviewing code, not only for bugs, but for quality. The git commit messages are a critical part of this. No developer is perfect, so helping one another in PRs is key to having an excellent git history. If a PR comes in with bad commit messages, it should be rejected and redone with good commit messages.
More detail about code reviews and PRs can be found in a separate doc.
## Git Messages
Git commit messges subjects should always follow this pattern:
`<type>(scope): <description>`
Types are:
- `build`: build-related changes
- `ci`: continuous integration-related changes
- `chore`: updating grunt tasks etc; no production code change
- `docs`: changes to the documentation
- `feat`: new feature for the user, not a new feature for build script
- `fix`: bug fix for the user, not a fix to a build script
- `perf`: a code change that improves performance
- `refactor`: refactoring production code, eg. renaming a variable
- `revert`: reverting things
- `style`: formatting, missing semi colons, etc; no production code change
- `test`: adding missing tests, refactoring tests; no production code change
The scope, generally considered optional, really should be mandatory. Typically this would be a ticket number, enabling devs both now and in the future to easily find commits associated with tickets and tickets associated with commits. Full details of what was being worked on can often be found in a ticket if they are not in the commit message, and in the event a feature's code needs to be examined or reverted. If you use a ticket management system like Jira having the ticket number in your commit message enables the management system to automatically detect it and it can be embedded directly in a card that you are currently working on, and even associated with Pull Requests and Epics.
The description is also incredibly important. A properly formed description should always be able to complete the following sentence:
`If applied, this commit will <description>`
It contains a succinct description of the change:
- Use the imperative, present tense: "change" not "changed" nor "changes"
- No full stop/period at the end
The body of a commit message should also use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior.
The 7 rules of a great commit message:
- Separate subject from the body with a blank line
- Limit the subject line to 50 characters
- Summary in the present tense. Not capitalized.
- Do not end the subject line with a period
- Use the imperative mood in the subject line
- Wrap the body at 72 characters
- Use the body to explain what and why vs. how
By incorporating the above convention, you can quickly determine what changes have been made and what the commit message for the changes would look like. With that format, you as a developer can immediately know (or at least guess), whats changed in the specific commit. If theres an issue with a new merge to the master branch, you can also quickly scan the git history to figure out what changes possibly can cause the problem without the need to look at the differences.
Some very good reasons to use this convention are:
- Automatically generating CHANGELOGs.
- Automatically determining a semantic version bump (based on the types of commits landed).
- Communicating the nature of changes to teammates, the public, and other stakeholders.
- Triggering build and publish processes.
- Making it easier for people to contribute to your projects by allowing them to explore a more structured commit history.
## Automating Git Commit Message Formatting
You can tell git to set up your commit messages with a set structure. This is done by running the command `git config --global commit.template ~/.gitmessage` or by manually setting `commit.template` in `~/.gitconfig` using your text editor of choice:
```
[commit]
template = ~/.gitmessage
```
Next, create `~/.gitmessage` with your new default template.
For your convenience, the following git commit template can be used:
```
# A properly formed Git commit subject line should always be able to complete
# the following sentence:
# * If applied, this commit will <description>
#
# ** Example:
# <type>(scope): <description>
#
# [optional body]
#
# [optional footer]
# ** Type
# Must be one of the following:
# * build: build-related changes
# * ci: continuous integration-related changes
# * chore: updating grunt tasks etc; no production code change
# * docs: changes to the documentation
# * feat: new feature for the user, not a new feature for build script
# * fix: bug fix for the user, not a fix to a build script
# * perf: a code change that improves performance
# * refactor: refactoring production code, eg. renaming a variable
# * revert: reverting things
# * style: formatting, missing semi colons, etc; no production code change
# * test: adding missing tests, refactoring tests; no production code change
# ** Subject
# The subject contains a succint description of the change:
# * Use the imperative, present tense: "change" not "changed" nor "changes"
# * No full stop/period at the end
# ** Scope
# A scope should be provided to a commits type if possible, to provide additional contextual information
# and is contained within parenthesis, e.g. feat(JIRA-1234): Add NewGitDocumentation endpoint
# ** Body
# Just as in the subject, use the imperative, present tense: "change" not "changed" nor "changes".
# The body should include the motivation for the change and contrast this with previous behavior.
# ** Rules
# The 7 rules of a great commit message
# 1. Separate subject from body with a blank line
# 2. Limit the subject line to 50 characters
# 3. Summary in present tense. Not capitalized
# 4. Do not end the subject line with a period
# 5. Use the imperative mood in the subject line
# 6. Wrap the body at 72 characters
# 7. Use the body to explain what and why vs. how
```
-6
View File
@@ -1,6 +0,0 @@
---
layout: default
title: Development Teams
nav_order: 5
has_children: true
---
-64
View File
@@ -1,64 +0,0 @@
---
layout: post
parent: Development Teams
nav_order: 2
title: "How to write an excellent readme file"
date: 2022-03-16 19:01:03 +0000
categories: git readme
---
# {{page.title}}
_{{page.date}}_
All git projects need a readme file, to explain what it is, why it exists, how it works and how to use it. A good starting point with guidance can be found below.
```md
# Project title
A little info about the application and an overview that explains **what** the application is for.
## Motivation
A short description of the motivation behind the creation and maintenance of the application. This should explain **why** the application exists.
## How the dev/staging/prod versions are built
This should be a brief explanation of how the application gets to where it's meant to be used. Typically this will involve linking to build pipelines in Jenkins/Azure/etc.
## Code style
Usually this would just link to the documentation for your established code style. In the event that the application uses additional or different styles or linting tooling this can be detailed here.
## Screenshots
Include some logos and demos screenshots to briefly give a rough idea of the application.
## Tech/framework used
Hopefully this will be identical across most of your organisation's applications, but the value of this section really becomes clear when an application does differ, and developers can see this at a glance here, without having to dig into the code or run the project locally to work it out.
## Features
What are the key features of this application? A few bullet points are ideal here.
## Getting it running locally for development
Provide a step by step series of examples and explanations about how to get a development environment running. If it is dependent on other local environmental factors, for example a specific database, this should also be covered here. This should be a single source of truth for getting this application running, without being dependent on any external sources of knowledge, even if that means there is some duplication across projects within this readme file.
## API Reference
If it's appropriate, a link should be included here to the API documentation, for example OpenAPI docs or a Swagger file.
## Tests
Describe and show how to run the tests with basic examples.
## How to use as a user
A brief guide on how to use the application AS A USER should be included here. A developer picking up the application should be able to find their way around it quickly as a user based on the guidance included here, to be able to sufficiently understand how most parts of the application are used.
## Anything else that seems useful
If there is anything else that seems useful to include in the readme file, this should be included here. Documentation of specific parts of the code SHOULD NOT live in this readme file, instead they should be in external documentation such as Confluence or in the code itself, whichever is more appropriate. References to such documentation could be included in this readme file for significant examples, but largely is not necessary.
```
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

-6
View File
@@ -1,6 +0,0 @@
---
layout: default
title: Home Server
nav_order: 4
has_children: true
---
@@ -1,46 +0,0 @@
---
layout: post
parent: Home Server
nav_order: 1
title: "Installing Home Assistant OS on Truenas Scale"
date: 2022-03-08 18:33:12 +0000
categories: home assistant, truenas scale, home server
---
# {{page.title}}
_{{page.date}}_
Mostly taken from https://www.truenas.com/community/threads/home-assistant-vm-on-scale.91058/post-666766 and cleaned up.
Make sure to use a location on your data pool as a working directory, don't use any system directory. I made a `hass` folder on my data pool `plex-media`:
`cd /mnt/plex-nas/plex-media/hass`
Use wget to get the ova file:
`wget https://github.com/home-assistant/operating-system/releases/download/6.6/haos_ova-6.6.ova`
Extract the ova file using tar:
`tar -xvf haos_ova-6.6.ova`
Convert the vmdk to a raw image file, I had to use the full working directory for the source:
`qemu-img convert -f vmdk -O raw /mnt/plex-nas/plex-media/hass/home-assistant.vmdk hassos.img`
Create a Zvol using the TrueNas Scale GUI - Be sure to make it large enough for dd to complete, I used 35 Gib.
Use dd to write the image file to your zvol
`dd if=hassos.img of=/dev/plex-nas/home-assistant`
Create a virtual machine using the gui and attach the zvol you just created as the hdd.
Minimum recommended assignments:
- 2GB RAM
- 32GB Storage
- 2vCPU
All these can be extended if your usage calls for more resources.
-79
View File
@@ -1,79 +0,0 @@
---
layout: default
title: Home
nav_order: 1
description: "The website for Josh Creek."
permalink: /
---
# Welcome to the site
{: .fs-9 }
This is the place where I ramble about technology and coding. Sometimes Computer Science education too.
{: .fs-6 .fw-300 }
[Learn who I am](#about-me){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 }
[View my GitHub](https://github.com/jcreek){: .btn .fs-5 .mb-4 .mb-md-0 }
---
## About Me
### Highlights
- Full Stack Web Developer, with a bit of devops and old-school (literally) network management
- Computer Science Educator
- Teach First 2015 Ambassador
- Writer
- Photographer
- Content Creator
- Google Trusted Tester, plus once worked at Google for 2 weeks
### Blurb
I like to spend my time working on agile projects to solve problems in the education and ecommerce spheres, creating educational content, writing poetry, doing portraiture and event photography, playing competitive video games and playing piano and ukulele or singing in a choir.
I also enjoy recording audiobooks for LibriVox, making YouTube videos on a variety of themes and streaming on Twitch. You can find me using the links below.
### Technologies
I code in a variety of languages, and use a number of different tools. Here they are for your perusal.
- HTML5
- CSS3
- JavaScipt ES6
- SCSS
- Node.js
- Vue.js
- jQuery
- C#
- ASP.Net Core
- SQL/MSSQL/PostgreSQL/MySQL
- Wordpress (if I really have to)
- MongoDB (if I really have to)
- Python (a bit rusty these days but fine for making simple scripts)
- Docker & Docker Compose
- AWS
- Proxmox (for baremetal virtualisation)
- Windows/MacOS/Linux
- Photoshop/Lightroom
- Davinci Resolve/Premiere Pro
### Education
#### UCL Institute of Education
##### July 2015 - July 2016
Postgraduate Certificate in Education
#### University of Exeter
##### September 2012 - July 2015
English with Proficiency in French
### Awards
- 2011 Headgate Theatre Young Playwrights Award
- 2013 Microsoft Imagine Cup World Citizenship Award (Ticklo)
-85
View File
@@ -1,85 +0,0 @@
---
layout: default
title: Privacy Policy
permalink: /privacy-policy/
---
# {{page.title}}
## Paladins Bounty Marketplace Chrome Extension
This extension uses your bounty_id cookie (from the bounty marketplace) and makes a POST request to the official endpoint for retrieving the list of current offers on the market. This data is not stored anywhere, is just used within your current browser session, and is only sent to the official endpoint that the bounty marketplace itself uses.
The below privacy policy applies other than this cookie.
## What data do we collect?
None.
## How do we collect your data?
I don't.
## How will we use your data?
I won't.
## How do we store your data?
I don't.
## Marketing
I don't market anything to you.
## What are your data protection rights?
I would like to make sure you are fully aware of all of your data protection rights. Every user is entitled to the following:
* The right to access You have the right to request Our Company for copies of your personal data. We may charge you a small fee for this service.
* The right to rectification You have the right to request that Our Company correct any information you believe is inaccurate. You also have the right to request Our Company to complete the information you believe is incomplete.
* The right to erasure You have the right to request that Our Company erase your personal data, under certain conditions.
* The right to restrict processing You have the right to request that Our Company restrict the processing of your personal data, under certain conditions.
* The right to object to processing You have the right to object to Our Companys processing of your personal data, under certain conditions.
* The right to data portability You have the right to request that Our Company transfer the data that we have collected to another organization, or directly to you, under certain conditions.
If you make a request, I have one month to respond to you. If you would like to exercise any of these rights, please contact me via Twitter.
## What are cookies?
Cookies are text files placed on your computer to collect standard Internet log information and visitor behavior information. When you visit our websites, we may collect information from you automatically through cookies or similar technology
For further information, visit allaboutcookies.org.
## How do we use cookies?
I don't.
## What types of cookies do we use?
None.
## How to manage your cookies
You can set your browser not to accept cookies, and the above website tells you how to remove cookies from your browser. However, in a few cases, some of our website features may not function as a result.
## Privacy policies of other websites
This website contains links to other websites. My privacy policy applies only to this website, so if you click on a link to another website, you should read their privacy policy.
## Changes to our privacy policy
I keep my privacy policy under regular review and places any updates on this web page. This privacy policy was last updated on 4th January 2022.
## How to contact us
If you have any questions about this privacy policy, the data I hold on you, or you would like to exercise one of your data protection rights, please do not hesitate to contact me via Twitter.
## How to contact the appropriate authorities
Should you wish to report a complaint or if you feel that I have not addressed your concern in a satisfactory manner, you may contact the Information Commissioners Office.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

@@ -1,190 +0,0 @@
---
layout: post
parent: Web Development
nav_order: 3
title: "Setting up remote containers for IAC dev environments in dotnet 6 with postgres and SMTP"
date: 2022-04-20 15:51:03 +0000
categories: dotnet core asp vscode remote-container remote container iac postgress smtp dev environment
---
# {{page.title}}
_{{page.date}}_
![](/assets/devcontainer.gif)
In this post I will go through how to set up remote containers for a dotnet 6 API project using PostgreSQL and an SMTP server, so that any new devs picking up the project only have to clone down the git repo and open it in Visual Studio code to get started debugging it.
No dependencies need to be installed onto developer machines for each project they work on, other than:
- Docker
- Visual Studio Code
Yes, that's all they need. They don't even need to install any extensions - the only one they need will be prompted the first time they open the project in vscode. Any extensions they need for the project will be loaded in a vscode server instance within the docker container, and then their local vscode will allow access to the vscode server as if they're using it natively, right down to providing terminal access within the container.
## How to set up the project
You will need to ensure that you've created a `.gitattributes` file in the root folder if you don't have one already, and specified `* text=auto eol=lf` to default all the line endings to LF, if you're on Windows and find that swapping between the host and the container changes the line endings in all your files. Alternatively, if you're already using Windows line endings, use `crlf` instead of `lf`.
In the root of the repo create a folder called `.devcontainer`. Within this you need to create three files:
1. `devcontainer.json`
2. `docker-compose.yml`
3. `Dockerfile`
### `devcontainer.json`
This is customised from the default Microsoft file. You can change the name, the vscode settings (although if you're working with newer dotnet projects I'd recommend leaving `omnisharp.useModernNet` set to `true`), add any extensions you want to use in vscode for this project (find the ID for an extension by installing it in your local vscode, then right clicking it and clicking `Copy Extension ID` - I've included some of my favourites for aspdotnet projects but the only one you *need* is `ms-dotnettools.csharp`), the post-create commands and the remote user.
There is an issue where Omnisharp, the underlying technology in the C# vscode plugin doesn't work properly unless `dotnet restore` is run at the point of setting up the dev container, so do not remove that command.
```json
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.231.6/containers/dotnet-postgres
{
"name": "Dotnet 6 API, Postgres & SMTP",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
// Set *default* container specific settings.json values on container create.
"settings": {
"omnisharp.useModernNet": true //Use the build of OmniSharp that runs on the .NET 6 SDK. This build requires that the .NET 6 SDK be installed and does not use Visual Studio MSBuild tools or Mono. It only supports newer SDK-style projects that are buildable with dotnet build. Unity projects and other Full Framework projects are not supported.
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"formulahendry.auto-rename-tag",
"ms-dotnettools.csharp",
"EditorConfig.EditorConfig",
"dbaeumer.vscode-eslint",
"xabikos.JavaScriptSnippets",
"PKief.material-icon-theme",
"eg2.vscode-npm-script",
"christian-kohler.path-intellisense",
"esbenp.prettier-vscode",
"gencer.html-slim-scss-css-class-completion"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [5000, 5001],
// [Optional] To reuse of your local HTTPS dev cert:
//
// 1. Export it locally using this command:
// * Windows PowerShell:
// dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere"
// * macOS/Linux terminal:
// dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere"
//
// 2. Uncomment these 'remoteEnv' lines:
// "remoteEnv": {
// "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere",
// "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx",
// },
//
// 3. Next, copy your certificate into the container:
// 1. Start the container
// 2. Drag ~/.aspnet/https/aspnetapp.pfx into the root of the file explorer
// 3. Open a terminal in VS Code and run "mkdir -p /home/vscode/.aspnet/https && mv aspnetapp.pfx /home/vscode/.aspnet/https"
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "dotnet restore", // had to use otherwise omnisharp wouldn't work properly, so no intellisense in vscode
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
}
```
### `docker-compose.yml`
The docker compose file sets up the three components of this example project:
1. The dotnet 6 API
2. The PostgreSQL database
3. The SMTP server
Take note of the name of the SMTP server container as this will be needed later, as will the environment variables for the database. These should be changed from `postgres`.
```yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
# Update 'VARIANT' to pick a version of .NET: 3.1, 5.0, 6.0
VARIANT: "6.0"
# Optional version of Node.js
NODE_VERSION: "lts/*"
volumes:
- ..:/workspace:cached
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
network_mode: service:db
# Uncomment the next line to use a non-root user for all processes.
# user: vscode
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
db:
image: postgres:14.1
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: postgres
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
mail:
image: bytemark/smtp
restart: unless-stopped
volumes:
postgres-data:
```
### `Dockerfile`
This comes directly from Microsoft, I've made no modifications to it.
```dockerfile
# [Choice] .NET version: 6.0, 5.0, 3.1, 6.0-bullseye, 5.0-bullseye, 3.1-bullseye, 6.0-focal, 5.0-focal, 3.1-focal
ARG VARIANT="6.0"
FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT}
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
```
### `appsettings.json` and `appsettings.Development.json`
To get the SMTP working, use these keys. If you're doing anything more fancy with `bytemark/smtp` than just the default functionality you may need to change the SMTP port.
```json
"SmtpHost": "mail",
"SmtpPort": 25,
```
To connect to the Postgres database set your connection string to `"User ID=postgres;Password=postgres;Host=db;Port=5432;Database=postgres;Pooling=true;"`, substituting in your chosen username, password and database name.
@@ -1,192 +0,0 @@
---
layout: post
parent: Web Development
nav_order: 3
title: "C# 8 Pattern Matching"
date: 2022-02-15 19:07:21 +0000
categories: dotnet csharp
---
# {{page.title}}
_{{page.date}}_
This serves as introductory documentation to the pattern matching features introduced in C# 8, which I think are particularly useful.
## Deconstructors & Positional Patterns
Adding a deconstructor to your class can be done as below. It must be named `Deconstruct` and be a `public void`. Any values we want deconstructed can be set as `out` parameters, then populated. Here all properties are being deconstructed, but a subset could be deconstructed if desired.
```csharp
class Student
{
public string Name { get; set; }
public int Year { get; set; }
public Teacher FormTutor { get; set; }
public void Deconstruct (out string name, out int year, out Teacher formTutor)
{
name = Name;
year = Year;
formTutor = FormTutor;
}
}
```
To use positional parameters, I'll also set up a second model.
```csharp
class Teacher
{
public string Name { get; set; }
public string Subject { get; set; }
public void Deconstruct (out string name, out string subject)
{
name = Name;
subject = Subject;
}
}
```
Here is an example positional pattern. The discards (`_`) are used to 'match all'.
```csharp
public static class YourClassHere
{
public static bool IsInYear9English(Student student)
{
return student is Student(
_,
9,
Teacher (_, "English")
);
}
}
```
While an if statement would probably be easier to read and maintain in this example, positional patterns enable recursion to be used, which can be very useful.
## Property Patterns
I've updated the Student model to add a School property.
```csharp
class Student
{
public string Name { get; set; }
public int Year { get; set; }
public string School { get; set; }
public Teacher FormTutor { get; set; }
}
```
I prefer using property patterns to positional patterns as they are much more readable. Here's an example method to check whether a student attends a particular school and has a form tutor who teaches Maths.
```csharp
public static class YourClassHere
{
public static bool IsStudentInGreenAcademyWithMathsFormTutor(Student student)
{
return student is {
School: "Green Academy",
FormTutor: {
Subject: "Maths"
}
};
}
}
```
This can be made more generic to accept an `object` rather than a `Student`, and check that object is a Student.
```csharp
public static class YourClassHere
{
public static bool IsStudentInGreenAcademyWithMathsFormTutor(object obj)
{
return obj is Student student &&
student is {
School: "Green Academy",
FormTutor: {
Subject: "Maths"
}
};
}
}
```
## Switch Expressions
These can be used in place of standard switch cases. I'm using a discard (`_`) for catching the default case for unmatched patterns.
```csharp
public static class YourClassHere
{
public static string DisplayPersonInfo (object person)
{
string result = person switch
{
Student student => $"Student at {student.School} in Year {student.Year}",
Teacher teacher => $"Teacher of {teacher.Subject}",
_ => "Person is not a student or a teacher"
};
return result;
}
}
```
The syntax can make it easier to read, and it can be a lot more powerful. You can also define recursive switch patterns.
```csharp
public static class YourClassHere
{
public static string DisplayPersonInfo (object person)
{
string result = person switch
{
Student student => student switch
{
_ when student.Year < 10 and student.Year > 6 => "Student in Key Stage 3",
_ when student.Year >= 10 and student.Year <= 13> => "Student in Key Stage 4",
_ => $"Student in Year {student.Year}"
},
Teacher teacher => $"Teacher of {teacher.Subject}",
_ => "Person is not a student or a teacher"
};
return result;
}
}
```
## Tuple Patterns (with Switch Expressions)
You can also use Switch Expressions with Tuples to write even more useful code. For example, you could be creating a game with crafting, combining two items to make another.
```csharp
public static class YourClassHere
{
public static CraftingMaterial GetCraftingMaterial (CraftingMaterial item1, CraftingMaterial item2)
{
return (item1, item2) switch
{
// Match the items in both positions
(CraftingMaterial.MountainFlowers, CraftingMaterial.SoulGems) => CraftingMaterial.DwemerMetal,
(CraftingMaterial.SoulGems, CraftingMaterial.MountainFlowers) => CraftingMaterial.DwemerMetal,
(CraftingMaterial.Ore, CraftingMaterial.DwemerMetal) => CraftingMaterial.Ingots,
(CraftingMaterial.DwemerMetal, CraftingMaterial.Ore) => CraftingMaterial.Ingots,
// Handle both items being the same (discard for both, to match any)
(_, _) when item1 == item2 => item1,
// Default case (with discard)
_ => CraftingMaterial.Unknown
};
}
}
```
This is lovely and easy to read, as well as very powerful.
-6
View File
@@ -1,6 +0,0 @@
---
layout: default
title: Web Development
nav_order: 3
has_children: true
---
-345
View File
@@ -1,345 +0,0 @@
---
layout: post
parent: Web Development
nav_order: 2
title: "Adding JSNLog to ASP .Net 6 with Serilog"
date: 2022-01-18 21:43:32 +0000
categories: dotnet core asp serilog jsnlog
---
# {{page.title}}
_{{page.date}}_
In this post I will go through the steps required to get an optimal install of JSNLog in ASP dotnet 6 projects using Serilog. JSNLog is a fantastic tool for logging uncaught javascript exceptions to the backend's logging system, and can be used for a variety of other things. This config includes batching of messages to reduce calls made to the server, as well as buffering, where helpful debugging logs are only sent if there's a fatal log that's also being sent, to make it easier to diagnose bugs without unnecessary diagnostic logs appearing at all times.
First you need to install `JSNLog` and `Destructurama.JsonNet` via NuGet. After that, make the following changes to these files.
## _ViewImports.cshtml
Add `@addTagHelper "*, jsnlog"` to enable the tag helper.
## Startup.cs
```csharp
using Destructurama;
using JSNLog;
...
public Startup(IConfiguration configuration, IWebHostEnvironment environment)
{
...
Log.Logger = SerilogLogger.CreateSerilogLogger(serilogConfiguration).Destructure.JsonNetTypes().CreateLogger();
...
}
...
public void Configure(
...,
ILoggerFactory loggerFactory)
{
...
loggerFactory.AddSerilog();
JsnlogConfiguration jsnlogConfiguration = new JsnlogConfiguration
{
ajaxAppenders = new List<AjaxAppender> {
new AjaxAppender {
name = "appender1",
storeInBufferLevel = "TRACE", // Log messages with severity smaller than TRACE are ignored
level = "WARN", // Log messages with severity equal or greater than TRACE and lower than WARN are stored in the internal buffer, but not sent to the server
// Log messages with severity equal or greater than WARN and lower than FATAL are sent to the server on their own
sendWithBufferLevel = "FATAL", // Log messages with severity equal or greater than FATAL are sent to the server, along with all messages stored in the internal buffer
bufferSize = 20, // Stores the last up to 20 debug messages in browser memory,
batchSize = 20,
batchTimeout = 2000, // Logs are guaranteed to be sent within this period (in ms), even if the batch size has not been reached yet
maxBatchSize = 50 // When the server is unreachable and log messages are being stored until it is reachable again, this is the maximum number of messages that will be stored. Cannot be smaller than batchSize
}
},
loggers = new List<Logger> { // Get the loggers to use the new appender
new Logger {
appenders = "appender1"
}
},
insertJsnlogInHtmlResponses = false, // There's an outstanding bug setting this to true so the workaround is using the jl-javascript-logger-definitions tag helper in _Layout.cshtml, via the reference in _ViewImports.cshtml
productionLibraryPath = null // We're using a fallback from the CDN with hashing, so do not use this
};
// Must be before UseStaticFiles and UseAuthorization
app.UseJSNLog(new CustomLoggingAdapter(loggerFactory), jsnlogConfiguration);
...
}
```
## _Layout.cshtml
This one has some interesting configuration.
Firstly, using local fallbacks if the CDN versions of the scripts are unavailable, or do not match their sha384 hashes (i.e. have been hacked or changed).
Secondly, it injects some additional information into the logs: client IP address, user ID (your model may differ from my example), and the user agent.
Thirdly, it sets up logging any uncaught JS exceptions, and any exceptions inside promises where no rejection method is provided.
Fourthly, using stacktrace.js it handles getting the relevant stacktrace using sourcemaps.
```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsnlog/2.30.0/jsnlog.min.js"
asp-fallback-src="~/jsnlog.min.js"
asp-fallback-test="window.JL"
crossorigin="anonymous"
integrity="sha384-ANmgu3V8Mc5/Usd/GeIS0xu0spgLKTIqkSMQAgVvV5C2SRp/rICkLLw5XG/u6BQ9">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stacktrace.js/2.0.2/stacktrace.min.js"
asp-fallback-src="~/stacktrace.min.js"
asp-fallback-test="window.StackTrace"
crossorigin="anonymous"
integrity="sha384-4PjQM+vlPbdcaPnFOyBIOfqz90Hvhp+QHb3rBMOy78OaxDHw9mnmzjUSNqJkn+W5">
</script>
<jl-javascript-logger-definitions />
<script>
async function fetchClientIp() {
try {
let response = await fetch('https://api.ipify.org?format=json');
return await response.json();
} catch (error) {
JL().fatalException("Failed to get client IP for logging", error);
}
}
fetchClientIp().then(data => {
const userDetailsForLogging = {
'clientIp': data.ip,
'userId': '@Model.UserId',
'userAgent': navigator.userAgent
}
@* Log uncaught JavaScript errors to the serverside log *@
if (window) {
window.onerror = function (errorMsg, url, lineNumber, column, errorObj) {
var callback = function(stackframes) {
var stringifiedStack = stackframes.map(function(sf) {
return sf.toString();
}).join('\n');
const msgObj = {
'msg': 'Uncaught exception',
'sourceMapStack': stringifiedStack,
'user': userDetailsForLogging
};
JL('serverLog').fatalException({
msg: JSON.stringify(msgObj),
errorMsg: errorMsg,
url: url,
lineNumber: lineNumber,
column: column
}, errorObj);
};
var errback = function(err) { console.log(err.message); };
StackTrace.fromError(errorObj).then(callback).catch(errback);
return false;
};
}
@* Log uncaught JavaScript exceptions inside promises where no rejection method is provided *@
if (typeof window !== 'undefined') {
window.onunhandledrejection = function (event) {
var callback = function(stackframes) {
var stringifiedStack = stackframes.map(function(sf) {
return sf.toString();
}).join('\n');
const msgObj = {
'msg': 'Unhandled promise rejection',
'sourceMapStack': stringifiedStack,
'user': userDetailsForLogging
};
JL("onerrorLogger").fatalException({
msg: JSON.stringify(msgObj),
errorMsg: event.reason ? event.reason.message : null
}, event.reason);
};
var errback = function(err) { console.log(err.message); };
StackTrace.fromError(errorObj).then(callback).catch(errback);
return false;
};
}
});
</script>
```
## CustomLoggingAdapter.cs
```csharp
using JSNLog;
using Newtonsoft.Json;
using System.Text;
namespace YourProjectHere
{
/// <summary>
/// This adapter is required to get JavaScript objects logged by JSNLog to appear correctly in Serilog. Regretably it is required
/// because of a defficiency in JSNLog that hasn't been fixed in at least 5 years at the time of writing, and depends on slightly
/// customised versions of classes and methods taken from that library, included here in this one file for tidiness.
/// </summary>
public class CustomLoggingAdapter : ILoggingAdapter
{
private readonly ILoggerFactory _loggerFactory;
public CustomLoggingAdapter(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
}
public void Log(FinalLogData finalLogData)
{
ILogger logger = _loggerFactory.CreateLogger(finalLogData.FinalLogger);
Object message = LogMessageHelpers.DeserializeIfPossible(finalLogData.FinalMessage);
switch (finalLogData.FinalLevel)
{
case Level.TRACE: logger.LogTrace("{@logMessage}", message); break;
case Level.DEBUG: logger.LogDebug("{@logMessage}", message); break;
case Level.INFO: logger.LogInformation("{@logMessage}", message); break;
case Level.WARN: logger.LogWarning("{@logMessage}", message); break;
case Level.ERROR: logger.LogError("{@logMessage}", message); break;
case Level.FATAL: logger.LogCritical("{@logMessage}", message); break;
default:
break;
}
}
}
internal class LogMessageHelpers
{
public static T DeserializeJson<T>(string json)
{
T result = JsonConvert.DeserializeObject<T>(json);
return result;
}
public static bool IsPotentialJson(string msg)
{
string trimmedMsg = msg.Trim();
return (trimmedMsg.StartsWith("{") && trimmedMsg.EndsWith("}"));
}
/// <summary>
/// Tries to deserialize msg.
/// If that works, returns the resulting object.
/// Otherwise returns msg itself (which is a string).
/// </summary>
/// <param name="msg"></param>
/// <returns></returns>
public static Object DeserializeIfPossible(string msg)
{
try
{
if (IsPotentialJson(msg))
{
Object result = DeserializeJson<Object>(msg);
return result;
}
}
catch
{
}
return msg;
}
/// <summary>
/// Returns true if the msg contains a valid JSON string.
/// </summary>
/// <param name="msg"></param>
/// <returns></returns>
public static bool IsJsonString(string msg)
{
try
{
if (IsPotentialJson(msg))
{
// Try to deserialise the msg. If that does not throw an exception,
// decide that msg is a good JSON string.
DeserializeJson<Dictionary<string, Object>>(msg);
return true;
}
}
catch
{
}
return false;
}
/// <summary>
/// Takes a log message and finds out if it contains a valid JSON string.
/// If so, returns it unchanged.
///
/// Otherwise, surrounds the string with quotes (") and escapes the string for JavaScript.
/// </summary>
/// <returns></returns>
public static string EnsureValidJson(string msg)
{
if (IsJsonString(msg))
{
return msg;
}
return JavaScriptStringEncode(msg, true);
}
public static string JavaScriptStringEncode(string value, bool addDoubleQuotes)
{
#if NETFRAMEWORK
return System.Web.HttpUtility.JavaScriptStringEncode(value, addDoubleQuotes);
#else
// copied from https://github.com/mono/mono/blob/master/mcs/class/System.Web/System.Web/HttpUtility.cs
if (String.IsNullOrEmpty(value))
return addDoubleQuotes ? "\"\"" : String.Empty;
int len = value.Length;
bool needEncode = false;
char c;
for (int i = 0; i < len; i++)
{
c = value[i];
if (c >= 0 && c <= 31 || c == 34 || c == 39 || c == 60 || c == 62 || c == 92)
{
needEncode = true;
break;
}
}
if (!needEncode)
return addDoubleQuotes ? "\"" + value + "\"" : value;
var sb = new StringBuilder();
if (addDoubleQuotes)
sb.Append('"');
for (int i = 0; i < len; i++)
{
c = value[i];
if (c >= 0 && c <= 7 || c == 11 || c >= 14 && c <= 31 || c == 39 || c == 60 || c == 62)
sb.AppendFormat("\\u{0:x4}", (int)c);
else switch ((int)c)
{
case 8:
sb.Append("\\b");
break;
case 9:
sb.Append("\\t");
break;
case 10:
sb.Append("\\n");
break;
case 12:
sb.Append("\\f");
break;
case 13:
sb.Append("\\r");
break;
case 34:
sb.Append("\\\"");
break;
case 92:
sb.Append("\\\\");
break;
default:
sb.Append(c);
break;
}
}
if (addDoubleQuotes)
sb.Append('"');
return sb.ToString();
#endif
}
}
}
```
-330
View File
@@ -1,330 +0,0 @@
---
layout: post
parent: Web Development
nav_order: 4
title: "How to set up a Node.js Typescript project well"
date: 2022-06-15 19:26:00 +0000
categories: nodejs typescript eslint webpack babel sourcemaps scss vscode devcontainer docker
---
# {{page.title}}
_{{page.date}}_
This post covers how to set up a project with Node.js and Typescript using eslint, webpack, babel, source maps, SCSS and vscode devcontainers using Docker.
This project makes use of eslint with airbnb styling and typescript support.
All linting errors can be automatically fixed (usually) by running `npm run lint -- --fix` before you commit your changes.
## Getting it running locally for development
### Devcontainer + vscode
If you have `Visual Studio Code` and `Docker` installed then you can open this project using the `Remote - Containers` extension (ms-vscode-remote.remote-containers). Click the button at the bottom left of vscode, then 'Reopen in container'. You can now run the application using the vscode debugger.
### Standard local install
Running `npm run dev` will build the development version in the `dist` folder and enable hot reloading. It will also open the application in a new browser tab automatically.
Running `npm run build` will create the production assets in the `dist` folder.
## Devcontainer
In the `.devcontainer` folder you'll need three files.
### `base.Dockerfile`
```dockerfile
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=16-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
# Install tslint, typescript. eslint is installed by javascript image
ARG NODE_MODULES="tslint-to-eslint-config typescript"
COPY library-scripts/meta.env /usr/local/etc/vscode-dev-containers
RUN su node -c "umask 0002 && npm install -g ${NODE_MODULES}" \
&& npm cache clean --force > /dev/null 2>&1
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
```
### `devcontainer.json`
```json
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/typescript-node
{
"name": "Node.js & TypeScript",
"build": {
"dockerfile": "Dockerfile",
// Update 'VARIANT' to pick a Node version: 18, 16, 14.
// Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local on arm64/Apple Silicon.
"args": {
"VARIANT": "16-bullseye"
}
},
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"dbaeumer.vscode-eslint"
]
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [3000],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "npm install",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node"
}
```
### `Dockerfile`
One important thing to note in this file is the installation of `inotify-tools` to the underlying Debian OS, as this is required for webpack to be able to poll and do hot reloading.
```dockerfile
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=16-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
# [Optional] Uncomment this section to install additional OS packages.
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends inotify-tools
# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
# [Optional] Uncomment if you want to install more global node packages
# RUN su node -c "npm install -g <your-package-list -here>"
```
## Visual Studio Code Debugging
Within the `.vscode` folder you'll need to create this `launch.json` file:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch via NPM",
"request": "launch",
"runtimeArgs": [
"run",
"dev"
],
"runtimeExecutable": "npm",
"skipFiles": [
"<node_internals>/**"
],
"type": "pwa-node",
"stopOnEntry": true,
}
]
}
```
## Eslint
Create the following files:
### `.eslintignore`
Assuming you're using git, this will be the same as your `.gitignore` file.
```text
dist
node_modules
```
### `.eslintrc.json`
```json
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"airbnb-base",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"import/extensions": [
"error",
"ignorePackages",
{
"js": "never",
"jsx": "never",
"ts": "never",
"tsx": "never"
}
]
}
}
```
## Webpack with SCSS and TS support, plus hot reloading
The structure for this application is to have a `src` folder that contains: all `.html` and `.ts`; `assets` folder for images; `styles` folder for `.scss` files. The files will be compiled to a `dist` folder for deploying to a web server.
Create the following files:
### `custom.d.ts`
```ts
declare module '*.svg' {
const content: any;
export default content;
}
```
### `tsconfig.json`
```json
{
"compilerOptions": {
"preserveConstEnums": true
},
"include": ["src/**/*", "src/custom.d.ts"],
"exclude": ["node_modules", "**/*.spec.ts"]
}
```
### `webpack.config.js`
```js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
bundle: path.resolve(__dirname, 'src/index.ts'),
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true,
assetModuleFilename: '[name].[contenthash][ext]',
},
devtool: 'source-map',
devServer: {
static: {
directory: path.resolve(__dirname, 'dist'),
},
port: 3000,
open: true,
hot: true,
compress: true,
historyApiFallback: true,
},
watchOptions: { poll: true },
module: {
rules: [
{
// Use these loaders for any matching scss file types
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader',
],
},
{
// Add backwards compatibility
test: /\.(js|ts)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-typescript'],
},
},
},
{
// Add support for images
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
],
},
resolve: {
// Specify the order in which to resolve files by their extension
extensions: ['*', '.js', '.ts'],
},
};
```
### `package.json`
```json
{
"name": "your app name",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack",
"dev": "webpack serve",
"lint": "eslint --ignore-path .eslintignore --ext .js,.ts ."
},
"repository": {
"type": "git",
"url": ""
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.18.5",
"@babel/preset-env": "^7.18.2",
"@babel/preset-typescript": "^7.17.12",
"@typescript-eslint/eslint-plugin": "^5.28.0",
"@typescript-eslint/parser": "^5.28.0",
"babel-loader": "^8.2.5",
"css-loader": "^6.7.1",
"eslint": "^8.17.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.26.0",
"html-webpack-plugin": "^5.5.0",
"sass": "^1.52.3",
"sass-loader": "^13.0.0",
"style-loader": "^3.3.1",
"typescript": "^4.7.3",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.2"
}
}
```
@@ -1,199 +0,0 @@
---
layout: post
parent: Web Development
nav_order: 1
title: "Creating a reactive SPA simply within an ASP.Net Core web app with Vue.js"
date: 2021-07-12 21:42:57 +0100
categories: vue dotnet core asp
---
# {{page.title}}
_{{page.date}}_
![](/assets/vuejsexample1.gif)
In this post, I will cover how you can quickly and easily use Vue.js to make a reactive 'SPA' within ASP.Net Core. This is a slightly dirty way to do things, but for rapid prototyping or a simple project it does the job just fine without any unnecessary complications. By 'SPA' I mean a Single Page Application, except without any navigation as that's handled by the underlying ASP.Net Core application, as is the API called by Vue.js methods.
In the `<head>` tags of your Razor page/view, you'll need to include a reference to Vue.js, for example via a CDN.
`<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>`
At the bottom of your Razor page/view, you'll need to include a `<script>` tag to hold your Vue instance definition. In Vue.js this includes:
- the element to have the Vue instance applied to
- the data object including any default properties
- computed properties (these act as if part of the data object but reactively update like functions)
- methods (functions that can interact with the data object)
In this example, it's a Vue instance for creating invoices and updating stock levels. The above gif shows some of the functionality achieved with this, including dynamically adding classes, and looping through elements in an array with markup for each.
```html
<script>
const createInvoice = new Vue({
el: '#create-invoice',
data() {
return {
errorMessage: '',
customer: {
Name: '',
Address1: '',
Address2: '',
Address3: '',
PostCode: ''
},
invoiceNumber: new Date().valueOf(),
products: [],
successMessage: ''
};
},
computed: {
calculatedVat() {
// take total and get 20% of it
return this.total * 0.2;
},
subTotal() {
// Take total and get 80% of it
return this.total * 0.8;
},
total() {
// For each product, add total price
let total = 0;
this.products.forEach(product => {
total += product.totalPrice;
});
return total - this.promoDiscount;
},
},
methods: {
calculateItemTotal(product) {
const totalPrice = product.unitPrice * product.quantity;
Vue.set(product, 'totalPrice', totalPrice)
},
getDuplicateBarcodeClass(product) {
const matchingBarcodes = this.products.filter(obj => {
return obj.barcode === product.barcode;
})
if (matchingBarcodes.length > 1) {
return 'alert-danger';
}
},
removeProduct(product) {
const index = this.products.findIndex(
(x) => x.barcode === product.barcode
);
if (index > -1) {
this.products.splice(index, 1);
}
},
updateStock() {
// Check the user really wants to update the stock and start a new invoice
const userResponse = confirm("Are you sure you wish to finish this invoice and update stock levels? You will not be able to print it again.");
if (userResponse) {
const data = {
invoiceNumber: String(this.invoiceNumber),
products: this.products,
};
// Update the stock & log the invoice
fetch(`Invoices/UpdateStockAndLogInvoice/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
.then((response) => {
return response.json();
})
.then((data) => {
if (data.statusCode === 404) {
Vue.set(this, "errorMessage", data.message);
} else {
Vue.set(this, 'successMessage', data.message);
// If no errors, clear the products list to be ready for the next invoice
Vue.set(this, "products", []);
}
})
.catch((err) => {
console.error(err);
Vue.set(this, "errorMessage", err);
});
}
},
validateNumber: (event) => {
// Only accept integers, so prevent anything but digits 0-9
let keyCode = event.keyCode;
if (keyCode < 48 || keyCode > 57) {
event.preventDefault();
}
},
viewAndPrintInvoice() {
if (this.isDuplicateBarcode) {
alert("There is a duplicate barcode - look for red barcodes and remove duplicates");
}
else {
window.print();
}
}
},
});
</script>
```
To hook this into the DOM, just add the element identifier to an element, for example `<div id="create-invoice">` will bind a div to this Vue instance.
Within that, you are free to use a variety of tools, like `v-if`, interpolation and `v-on` event handlers. In this example, if there's any text in the `successMessage` property of the Vue data object, this alert div will be displayed, interpolating the message, and clearing it (thus hiding the alert) when the close button is clicked.
```html
<div v-if="successMessage" class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill"></i>
<strong>Success:</strong> {{ successMessage }}
<button type="button" class="close" aria-label="Close" v-on:click="successMessage = ''">
<span aria-hidden="true">&times;</span>
</button>
</div>
```
In this example, a `v-for` is used to loop through all the products in the array, producing a `<tr>` for each of them, with an appropriate class bound from a method taking that product as a parameter. `v-if` and `v-else` statements enable conditionally showing one (or none) of three different pooltips. The product's `name` property is interpolated and shown, as is the `unitPrice` property, which is nicely formatted for readability.
The `barcode` field is mapped to a `v-model`, which means as it changes it directly alters the value in the Vue data object, and as that changes this updates reactively. It also makes use of `v-on:blur` to run a method that makes an API call to retrieve product data, conditionally sets the disabled property once a barcode has been entered, and binds a class based on the output of a method taking the product as a parameter.
The `quantity` field also uses a `v-model`, but specifically forces it to be a number (no need for Typescript here) runs a method on keypress (the `@@` syntax is purely due to Razor escaping, normally you'd only need one) and on change.
```html
<tr v-for="product in products" v-bind:class="getProductRowClass(product)">
<td>
<div class="tooltip" v-if="product.barcode.length > 0 && product.stockLevel - product.quantity < 0">
<i class="bi bi-x-circle-fill text-danger"></i>
<span class="tooltiptext">You will need to order more stock to fulfill this order</span>
</div>
<div class="tooltip" v-else-if="product.barcode.length > 0 && product.stockLevel - product.quantity == 0">
<i class="bi bi-exclamation-triangle-fill text-warning"></i>
<span class="tooltiptext">You will run out of stock</span>
</div>
<div class="tooltip" v-else-if="product.barcode.length > 0 && product.stockLevel - product.quantity <= 10">
<i class="bi bi-info-circle-fill text-info"></i>
<span class="tooltiptext">Stock will be low after this order</span>
</div>
</td>
<td>
<label>{{product.name}}</label>
</td>
<td>
<input type="text" name="barcode" v-model="product.barcode" v-on:blur="getProductInformation(product)" :disabled="product.barcode.length > 0" v-bind:class="getDuplicateBarcodeClass(product)">
</td>
<td>
<input type="number" step="1" name="quantity" v-model.number="product.quantity" @@keypress="validateNumber" v-on:change="calculateItemTotal(product)">
</td>
<td>
<label>&#163;{{parseFloat(product.unitPrice).toFixed(2)}}</label>
</td>
</tr>
```
This is a very simple, quick and dirty example, but hopefully it gives you an idea of the kind of things you can quickly and easily achieve using Vue.js in this way. While this example uses ASP.Net Core, you could easily plug this into any other website, so long as it's using HTML, CSS and JavaScript, and there is an API to make requests to.