diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/404.html b/404.html new file mode 100644 index 00000000..1a177d2d --- /dev/null +++ b/404.html @@ -0,0 +1,1910 @@ + + + + + + + + + + + + + + + + + + DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 00000000..1cf13b9f Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/javascripts/bundle.2a6f1dda.min.js b/assets/javascripts/bundle.2a6f1dda.min.js new file mode 100644 index 00000000..2f912a0b --- /dev/null +++ b/assets/javascripts/bundle.2a6f1dda.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Hi=Object.create;var xr=Object.defineProperty;var Pi=Object.getOwnPropertyDescriptor;var $i=Object.getOwnPropertyNames,Ht=Object.getOwnPropertySymbols,Ii=Object.getPrototypeOf,Er=Object.prototype.hasOwnProperty,an=Object.prototype.propertyIsEnumerable;var on=(e,t,r)=>t in e?xr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))Er.call(t,r)&&on(e,r,t[r]);if(Ht)for(var r of Ht(t))an.call(t,r)&&on(e,r,t[r]);return e};var sn=(e,t)=>{var r={};for(var n in e)Er.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Ht)for(var n of Ht(e))t.indexOf(n)<0&&an.call(e,n)&&(r[n]=e[n]);return r};var Pt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Fi=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of $i(t))!Er.call(e,o)&&o!==r&&xr(e,o,{get:()=>t[o],enumerable:!(n=Pi(t,o))||n.enumerable});return e};var yt=(e,t,r)=>(r=e!=null?Hi(Ii(e)):{},Fi(t||!e||!e.__esModule?xr(r,"default",{value:e,enumerable:!0}):r,e));var fn=Pt((wr,cn)=>{(function(e,t){typeof wr=="object"&&typeof cn!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(wr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,s={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function a(O){return!!(O&&O!==document&&O.nodeName!=="HTML"&&O.nodeName!=="BODY"&&"classList"in O&&"contains"in O.classList)}function f(O){var Ke=O.type,De=O.tagName;return!!(De==="INPUT"&&s[Ke]&&!O.readOnly||De==="TEXTAREA"&&!O.readOnly||O.isContentEditable)}function c(O){O.classList.contains("focus-visible")||(O.classList.add("focus-visible"),O.setAttribute("data-focus-visible-added",""))}function u(O){O.hasAttribute("data-focus-visible-added")&&(O.classList.remove("focus-visible"),O.removeAttribute("data-focus-visible-added"))}function p(O){O.metaKey||O.altKey||O.ctrlKey||(a(r.activeElement)&&c(r.activeElement),n=!0)}function m(O){n=!1}function d(O){a(O.target)&&(n||f(O.target))&&c(O.target)}function h(O){a(O.target)&&(O.target.classList.contains("focus-visible")||O.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(O.target))}function v(O){document.visibilityState==="hidden"&&(o&&(n=!0),B())}function B(){document.addEventListener("mousemove",z),document.addEventListener("mousedown",z),document.addEventListener("mouseup",z),document.addEventListener("pointermove",z),document.addEventListener("pointerdown",z),document.addEventListener("pointerup",z),document.addEventListener("touchmove",z),document.addEventListener("touchstart",z),document.addEventListener("touchend",z)}function ne(){document.removeEventListener("mousemove",z),document.removeEventListener("mousedown",z),document.removeEventListener("mouseup",z),document.removeEventListener("pointermove",z),document.removeEventListener("pointerdown",z),document.removeEventListener("pointerup",z),document.removeEventListener("touchmove",z),document.removeEventListener("touchstart",z),document.removeEventListener("touchend",z)}function z(O){O.target.nodeName&&O.target.nodeName.toLowerCase()==="html"||(n=!1,ne())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",m,!0),document.addEventListener("pointerdown",m,!0),document.addEventListener("touchstart",m,!0),document.addEventListener("visibilitychange",v,!0),B(),r.addEventListener("focus",d,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var un=Pt(Sr=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(c){return!1}},r=t(),n=function(c){var u={next:function(){var p=c.shift();return{done:p===void 0,value:p}}};return r&&(u[Symbol.iterator]=function(){return u}),u},o=function(c){return encodeURIComponent(c).replace(/%20/g,"+")},i=function(c){return decodeURIComponent(String(c).replace(/\+/g," "))},s=function(){var c=function(p){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var m=typeof p;if(m!=="undefined")if(m==="string")p!==""&&this._fromString(p);else if(p instanceof c){var d=this;p.forEach(function(ne,z){d.append(z,ne)})}else if(p!==null&&m==="object")if(Object.prototype.toString.call(p)==="[object Array]")for(var h=0;hd[0]?1:0}),c._entries&&(c._entries={});for(var p=0;p1?i(d[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Sr);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(f,c){typeof f!="string"&&(f=String(f)),c&&typeof c!="string"&&(c=String(c));var u=document,p;if(c&&(e.location===void 0||c!==e.location.href)){c=c.toLowerCase(),u=document.implementation.createHTMLDocument(""),p=u.createElement("base"),p.href=c,u.head.appendChild(p);try{if(p.href.indexOf(c)!==0)throw new Error(p.href)}catch(O){throw new Error("URL unable to set base "+c+" due to "+O)}}var m=u.createElement("a");m.href=f,p&&(u.body.appendChild(m),m.href=m.href);var d=u.createElement("input");if(d.type="url",d.value=f,m.protocol===":"||!/:/.test(m.href)||!d.checkValidity()&&!c)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:m});var h=new e.URLSearchParams(this.search),v=!0,B=!0,ne=this;["append","delete","set"].forEach(function(O){var Ke=h[O];h[O]=function(){Ke.apply(h,arguments),v&&(B=!1,ne.search=h.toString(),B=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var z=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==z&&(z=this.search,B&&(v=!1,this.searchParams._fromString(this.search),v=!0))}})},s=i.prototype,a=function(f){Object.defineProperty(s,f,{get:function(){return this._anchorElement[f]},set:function(c){this._anchorElement[f]=c},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(f){a(f)}),Object.defineProperty(s,"search",{get:function(){return this._anchorElement.search},set:function(f){this._anchorElement.search=f,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(s,{toString:{get:function(){var f=this;return function(){return f.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(f){this._anchorElement.href=f,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(f){this._anchorElement.pathname=f},enumerable:!0},origin:{get:function(){var f={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],c=this._anchorElement.port!=f&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(c?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(f){},enumerable:!0},username:{get:function(){return""},set:function(f){},enumerable:!0}}),i.createObjectURL=function(f){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(f){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Sr)});var Qr=Pt((Lt,Kr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Lt=="object"&&typeof Kr=="object"?Kr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Lt=="object"?Lt.ClipboardJS=r():t.ClipboardJS=r()})(Lt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return ki}});var s=i(279),a=i.n(s),f=i(370),c=i.n(f),u=i(817),p=i.n(u);function m(j){try{return document.execCommand(j)}catch(T){return!1}}var d=function(T){var w=p()(T);return m("cut"),w},h=d;function v(j){var T=document.documentElement.getAttribute("dir")==="rtl",w=document.createElement("textarea");w.style.fontSize="12pt",w.style.border="0",w.style.padding="0",w.style.margin="0",w.style.position="absolute",w.style[T?"right":"left"]="-9999px";var k=window.pageYOffset||document.documentElement.scrollTop;return w.style.top="".concat(k,"px"),w.setAttribute("readonly",""),w.value=j,w}var B=function(T,w){var k=v(T);w.container.appendChild(k);var F=p()(k);return m("copy"),k.remove(),F},ne=function(T){var w=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},k="";return typeof T=="string"?k=B(T,w):T instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(T==null?void 0:T.type)?k=B(T.value,w):(k=p()(T),m("copy")),k},z=ne;function O(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?O=function(w){return typeof w}:O=function(w){return w&&typeof Symbol=="function"&&w.constructor===Symbol&&w!==Symbol.prototype?"symbol":typeof w},O(j)}var Ke=function(){var T=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},w=T.action,k=w===void 0?"copy":w,F=T.container,q=T.target,Le=T.text;if(k!=="copy"&&k!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&O(q)==="object"&&q.nodeType===1){if(k==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(k==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Le)return z(Le,{container:F});if(q)return k==="cut"?h(q):z(q,{container:F})},De=Ke;function Fe(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Fe=function(w){return typeof w}:Fe=function(w){return w&&typeof Symbol=="function"&&w.constructor===Symbol&&w!==Symbol.prototype?"symbol":typeof w},Fe(j)}function Oi(j,T){if(!(j instanceof T))throw new TypeError("Cannot call a class as a function")}function nn(j,T){for(var w=0;w0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof F.action=="function"?F.action:this.defaultAction,this.target=typeof F.target=="function"?F.target:this.defaultTarget,this.text=typeof F.text=="function"?F.text:this.defaultText,this.container=Fe(F.container)==="object"?F.container:document.body}},{key:"listenClick",value:function(F){var q=this;this.listener=c()(F,"click",function(Le){return q.onClick(Le)})}},{key:"onClick",value:function(F){var q=F.delegateTarget||F.currentTarget,Le=this.action(q)||"copy",kt=De({action:Le,container:this.container,target:this.target(q),text:this.text(q)});this.emit(kt?"success":"error",{action:Le,text:kt,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(F){return yr("action",F)}},{key:"defaultTarget",value:function(F){var q=yr("target",F);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(F){return yr("text",F)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(F){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return z(F,q)}},{key:"cut",value:function(F){return h(F)}},{key:"isSupported",value:function(){var F=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof F=="string"?[F]:F,Le=!!document.queryCommandSupported;return q.forEach(function(kt){Le=Le&&!!document.queryCommandSupported(kt)}),Le}}]),w}(a()),ki=Ri},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function s(a,f){for(;a&&a.nodeType!==o;){if(typeof a.matches=="function"&&a.matches(f))return a;a=a.parentNode}}n.exports=s},438:function(n,o,i){var s=i(828);function a(u,p,m,d,h){var v=c.apply(this,arguments);return u.addEventListener(m,v,h),{destroy:function(){u.removeEventListener(m,v,h)}}}function f(u,p,m,d,h){return typeof u.addEventListener=="function"?a.apply(null,arguments):typeof m=="function"?a.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return a(v,p,m,d,h)}))}function c(u,p,m,d){return function(h){h.delegateTarget=s(h.target,p),h.delegateTarget&&d.call(u,h)}}n.exports=f},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var s=Object.prototype.toString.call(i);return i!==void 0&&(s==="[object NodeList]"||s==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var s=Object.prototype.toString.call(i);return s==="[object Function]"}},370:function(n,o,i){var s=i(879),a=i(438);function f(m,d,h){if(!m&&!d&&!h)throw new Error("Missing required arguments");if(!s.string(d))throw new TypeError("Second argument must be a String");if(!s.fn(h))throw new TypeError("Third argument must be a Function");if(s.node(m))return c(m,d,h);if(s.nodeList(m))return u(m,d,h);if(s.string(m))return p(m,d,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(m,d,h){return m.addEventListener(d,h),{destroy:function(){m.removeEventListener(d,h)}}}function u(m,d,h){return Array.prototype.forEach.call(m,function(v){v.addEventListener(d,h)}),{destroy:function(){Array.prototype.forEach.call(m,function(v){v.removeEventListener(d,h)})}}}function p(m,d,h){return a(document.body,m,d,h)}n.exports=f},817:function(n){function o(i){var s;if(i.nodeName==="SELECT")i.focus(),s=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var a=i.hasAttribute("readonly");a||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),a||i.removeAttribute("readonly"),s=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var f=window.getSelection(),c=document.createRange();c.selectNodeContents(i),f.removeAllRanges(),f.addRange(c),s=f.toString()}return s}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,s,a){var f=this.e||(this.e={});return(f[i]||(f[i]=[])).push({fn:s,ctx:a}),this},once:function(i,s,a){var f=this;function c(){f.off(i,c),s.apply(a,arguments)}return c._=s,this.on(i,c,a)},emit:function(i){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[i]||[]).slice(),f=0,c=a.length;for(f;f{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var is=/["'&<>]/;Jo.exports=as;function as(e){var t=""+e,r=is.exec(t);if(!r)return t;var n,o="",i=0,s=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function W(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],s;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(a){s={error:a}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(s)throw s.error}}return i}function D(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||a(m,d)})})}function a(m,d){try{f(n[m](d))}catch(h){p(i[0][3],h)}}function f(m){m.value instanceof Ze?Promise.resolve(m.value.v).then(c,u):p(i[0][2],m)}function c(m){a("next",m)}function u(m){a("throw",m)}function p(m,d){m(d),i.shift(),i.length&&a(i[0][0],i[0][1])}}function mn(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof xe=="function"?xe(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(s){return new Promise(function(a,f){s=e[i](s),o(a,f,s.done,s.value)})}}function o(i,s,a,f){Promise.resolve(f).then(function(c){i({value:c,done:a})},s)}}function A(e){return typeof e=="function"}function at(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var It=at(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(n,o){return o+1+") "+n.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Ve(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var je=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var s=this._parentage;if(s)if(this._parentage=null,Array.isArray(s))try{for(var a=xe(s),f=a.next();!f.done;f=a.next()){var c=f.value;c.remove(this)}}catch(v){t={error:v}}finally{try{f&&!f.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}else s.remove(this);var u=this.initialTeardown;if(A(u))try{u()}catch(v){i=v instanceof It?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var m=xe(p),d=m.next();!d.done;d=m.next()){var h=d.value;try{dn(h)}catch(v){i=i!=null?i:[],v instanceof It?i=D(D([],W(i)),W(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{d&&!d.done&&(o=m.return)&&o.call(m)}finally{if(n)throw n.error}}}if(i)throw new It(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)dn(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Ve(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Ve(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Tr=je.EMPTY;function Ft(e){return e instanceof je||e&&"closed"in e&&A(e.remove)&&A(e.add)&&A(e.unsubscribe)}function dn(e){A(e)?e():e.unsubscribe()}var Ae={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var st={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,s=o.isStopped,a=o.observers;return i||s?Tr:(this.currentObservers=null,a.push(r),new je(function(){n.currentObservers=null,Ve(a,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,s=n.isStopped;o?r.error(i):s&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new wn(r,n)},t}(U);var wn=function(e){ie(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Tr},t}(E);var Et={now:function(){return(Et.delegate||Date).now()},delegate:void 0};var wt=function(e){ie(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=Et);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,s=n._infiniteTimeWindow,a=n._timestampProvider,f=n._windowTime;o||(i.push(r),!s&&i.push(a.now()+f)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,s=o._buffer,a=s.slice(),f=0;f0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=ut.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var s=r.actions;n!=null&&((i=s[s.length-1])===null||i===void 0?void 0:i.id)!==n&&(ut.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Wt);var Tn=function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(Dt);var we=new Tn(On);var R=new U(function(e){return e.complete()});function Vt(e){return e&&A(e.schedule)}function kr(e){return e[e.length-1]}function Qe(e){return A(kr(e))?e.pop():void 0}function Se(e){return Vt(kr(e))?e.pop():void 0}function zt(e,t){return typeof kr(e)=="number"?e.pop():t}var pt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Nt(e){return A(e==null?void 0:e.then)}function qt(e){return A(e[ft])}function Kt(e){return Symbol.asyncIterator&&A(e==null?void 0:e[Symbol.asyncIterator])}function Qt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Ki(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Yt=Ki();function Gt(e){return A(e==null?void 0:e[Yt])}function Bt(e){return ln(this,arguments,function(){var r,n,o,i;return $t(this,function(s){switch(s.label){case 0:r=e.getReader(),s.label=1;case 1:s.trys.push([1,,9,10]),s.label=2;case 2:return[4,Ze(r.read())];case 3:return n=s.sent(),o=n.value,i=n.done,i?[4,Ze(void 0)]:[3,5];case 4:return[2,s.sent()];case 5:return[4,Ze(o)];case 6:return[4,s.sent()];case 7:return s.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Jt(e){return A(e==null?void 0:e.getReader)}function $(e){if(e instanceof U)return e;if(e!=null){if(qt(e))return Qi(e);if(pt(e))return Yi(e);if(Nt(e))return Gi(e);if(Kt(e))return _n(e);if(Gt(e))return Bi(e);if(Jt(e))return Ji(e)}throw Qt(e)}function Qi(e){return new U(function(t){var r=e[ft]();if(A(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Yi(e){return new U(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?_(function(o,i){return e(o,i,n)}):de,Te(1),r?Pe(t):zn(function(){return new Zt}))}}function Nn(){for(var e=[],t=0;t=2,!0))}function ue(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new E}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,s=i===void 0?!0:i,a=e.resetOnRefCountZero,f=a===void 0?!0:a;return function(c){var u,p,m,d=0,h=!1,v=!1,B=function(){p==null||p.unsubscribe(),p=void 0},ne=function(){B(),u=m=void 0,h=v=!1},z=function(){var O=u;ne(),O==null||O.unsubscribe()};return g(function(O,Ke){d++,!v&&!h&&B();var De=m=m!=null?m:r();Ke.add(function(){d--,d===0&&!v&&!h&&(p=jr(z,f))}),De.subscribe(Ke),!u&&d>0&&(u=new tt({next:function(Fe){return De.next(Fe)},error:function(Fe){v=!0,B(),p=jr(ne,o,Fe),De.error(Fe)},complete:function(){h=!0,B(),p=jr(ne,s),De.complete()}}),$(O).subscribe(u))})(c)}}function jr(e,t){for(var r=[],n=2;ne.next(document)),e}function K(e,t=document){return Array.from(t.querySelectorAll(e))}function V(e,t=document){let r=ce(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ce(e,t=document){return t.querySelector(e)||void 0}function _e(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function rr(e){return L(b(document.body,"focusin"),b(document.body,"focusout")).pipe(He(1),l(()=>{let t=_e();return typeof t!="undefined"?e.contains(t):!1}),N(e===_e()),G())}function Je(e){return{x:e.offsetLeft,y:e.offsetTop}}function Yn(e){return L(b(window,"load"),b(window,"resize")).pipe(Re(0,we),l(()=>Je(e)),N(Je(e)))}function nr(e){return{x:e.scrollLeft,y:e.scrollTop}}function dt(e){return L(b(e,"scroll"),b(window,"resize")).pipe(Re(0,we),l(()=>nr(e)),N(nr(e)))}var Bn=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!zr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),xa?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!zr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=ya.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),Jn=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),Zn=typeof WeakMap!="undefined"?new WeakMap:new Bn,eo=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=Ea.getInstance(),n=new Ra(t,r,this);Zn.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){eo.prototype[e]=function(){var t;return(t=Zn.get(this))[e].apply(t,arguments)}});var ka=function(){return typeof or.ResizeObserver!="undefined"?or.ResizeObserver:eo}(),to=ka;var ro=new E,Ha=I(()=>H(new to(e=>{for(let t of e)ro.next(t)}))).pipe(x(e=>L(Oe,H(e)).pipe(C(()=>e.disconnect()))),J(1));function he(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){return Ha.pipe(S(t=>t.observe(e)),x(t=>ro.pipe(_(({target:r})=>r===e),C(()=>t.unobserve(e)),l(()=>he(e)))),N(he(e)))}function bt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function sr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var no=new E,Pa=I(()=>H(new IntersectionObserver(e=>{for(let t of e)no.next(t)},{threshold:0}))).pipe(x(e=>L(Oe,H(e)).pipe(C(()=>e.disconnect()))),J(1));function cr(e){return Pa.pipe(S(t=>t.observe(e)),x(t=>no.pipe(_(({target:r})=>r===e),C(()=>t.unobserve(e)),l(({isIntersecting:r})=>r))))}function oo(e,t=16){return dt(e).pipe(l(({y:r})=>{let n=he(e),o=bt(e);return r>=o.height-n.height-t}),G())}var fr={drawer:V("[data-md-toggle=drawer]"),search:V("[data-md-toggle=search]")};function io(e){return fr[e].checked}function qe(e,t){fr[e].checked!==t&&fr[e].click()}function Ue(e){let t=fr[e];return b(t,"change").pipe(l(()=>t.checked),N(t.checked))}function $a(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ia(){return L(b(window,"compositionstart").pipe(l(()=>!0)),b(window,"compositionend").pipe(l(()=>!1))).pipe(N(!1))}function ao(){let e=b(window,"keydown").pipe(_(t=>!(t.metaKey||t.ctrlKey)),l(t=>({mode:io("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),_(({mode:t,type:r})=>{if(t==="global"){let n=_e();if(typeof n!="undefined")return!$a(n,r)}return!0}),ue());return Ia().pipe(x(t=>t?R:e))}function Me(){return new URL(location.href)}function ot(e){location.href=e.href}function so(){return new E}function co(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)co(e,r)}function M(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)co(n,o);return n}function ur(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function fo(){return location.hash.substring(1)}function uo(e){let t=M("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Fa(){return b(window,"hashchange").pipe(l(fo),N(fo()),_(e=>e.length>0),J(1))}function po(){return Fa().pipe(l(e=>ce(`[id="${e}"]`)),_(e=>typeof e!="undefined"))}function Nr(e){let t=matchMedia(e);return er(r=>t.addListener(()=>r(t.matches))).pipe(N(t.matches))}function lo(){let e=matchMedia("print");return L(b(window,"beforeprint").pipe(l(()=>!0)),b(window,"afterprint").pipe(l(()=>!1))).pipe(N(e.matches))}function qr(e,t){return e.pipe(x(r=>r?t():R))}function pr(e,t={credentials:"same-origin"}){return pe(fetch(`${e}`,t)).pipe(fe(()=>R),x(r=>r.status!==200?Ot(()=>new Error(r.statusText)):H(r)))}function We(e,t){return pr(e,t).pipe(x(r=>r.json()),J(1))}function mo(e,t){let r=new DOMParser;return pr(e,t).pipe(x(n=>n.text()),l(n=>r.parseFromString(n,"text/xml")),J(1))}function lr(e){let t=M("script",{src:e});return I(()=>(document.head.appendChild(t),L(b(t,"load"),b(t,"error").pipe(x(()=>Ot(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(l(()=>{}),C(()=>document.head.removeChild(t)),Te(1))))}function ho(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function bo(){return L(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(l(ho),N(ho()))}function vo(){return{width:innerWidth,height:innerHeight}}function go(){return b(window,"resize",{passive:!0}).pipe(l(vo),N(vo()))}function yo(){return Q([bo(),go()]).pipe(l(([e,t])=>({offset:e,size:t})),J(1))}function mr(e,{viewport$:t,header$:r}){let n=t.pipe(X("size")),o=Q([n,r]).pipe(l(()=>Je(e)));return Q([r,t,o]).pipe(l(([{height:i},{offset:s,size:a},{x:f,y:c}])=>({offset:{x:s.x-f,y:s.y-c+i},size:a})))}(()=>{function e(n,o){parent.postMessage(n,o||"*")}function t(...n){return n.reduce((o,i)=>o.then(()=>new Promise(s=>{let a=document.createElement("script");a.src=i,a.onload=s,document.body.appendChild(a)})),Promise.resolve())}var r=class extends EventTarget{constructor(n){super(),this.url=n,this.m=i=>{i.source===this.w&&(this.dispatchEvent(new MessageEvent("message",{data:i.data})),this.onmessage&&this.onmessage(i))},this.e=(i,s,a,f,c)=>{if(s===`${this.url}`){let u=new ErrorEvent("error",{message:i,filename:s,lineno:a,colno:f,error:c});this.dispatchEvent(u),this.onerror&&this.onerror(u)}};let o=document.createElement("iframe");o.hidden=!0,document.body.appendChild(this.iframe=o),this.w.document.open(),this.w.document.write(` + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

API Development

+

We use the RESTful Web Services and OpenAPI REST Drupal modules +to expose endpoints from Drupal as an API to be consumed by external parties.

+

Howtos

+

Create a new endpoint

+
    +
  1. Implement a new REST resource plugin by extending + Drupal\rest\Plugin\ResourceBase and annotating it with @RestResource
  2. +
  3. Describe uri_paths, route_parameters and responses in the annotation as + detailed as possible to create a strong specification.
  4. +
  5. Install the REST UI module drush pm-enable restui
  6. +
  7. Enable and configure the new REST resource. It is important to use the + dpl_login_user_token authentication provider for all resources which will + be used by the frontend this will provide a library or user token by default.
  8. +
  9. Inspect the updated OpenAPI specification at /openapi/rest?_format=json to + ensure looks as intended
  10. +
  11. Run task ci:openapi:validate to validate the updated OpenAPI specification
  12. +
  13. Run task ci:openapi:download to download the updated OpenAPI specification
  14. +
  15. Uninstall the REST UI module drush pm-uninstall restui
  16. +
  17. Export the updated configuration drush config-export
  18. +
  19. Commit your changes including the updated configuration and openapi.json
  20. +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/architecture/adr-001-configuration-management/index.html b/dpl-cms/architecture/adr-001-configuration-management/index.html new file mode 100644 index 00000000..a682f30e --- /dev/null +++ b/dpl-cms/architecture/adr-001-configuration-management/index.html @@ -0,0 +1,2214 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Configuration Management - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Configuration Management

+

Context

+

Configuration management for DPL CMS is a complex issue. The complexity stems +from different types of DPL CMS sites.

+

There are two approaches to the problem:

+
    +
  1. All configuration is local unless explicitly marked as core configuration
  2. +
  3. All configuration is core unless explicitly marked as local configuration
  4. +
+

A solution to configuration management must live up to the following test:

+
    +
  1. Initialize a local environment to represent a site
  2. +
  3. Import the provided configuration through site installation using + drush site-install --existing-config -y
  4. +
  5. Log in to see that Core configuration is imported. This can be verified if + the site name is set to DPL CMS.
  6. +
  7. Change a Core configuration value e.g. on http://dpl-cms.docker/admin/config/development/performance
  8. +
  9. Run drush config-import -y and see that the change is rolled back and the + configuration value is back to default. This shows that Core configuration + will remain managed by the configuration system.
  10. +
  11. Change a local configuration value like the site name on http://dpl-cms.docker/admin/config/system/site-information
  12. +
  13. Run drush config-import -y to see that no configuration is imported. This + shows that local configuration which can be managed by Editor libraries will + be left unchanged.
  14. +
  15. Enable and configure the Shortcut module and add a new Shortcut set.
  16. +
  17. Run drush config-import -y to see that the module is not disabled and the + configuration remains unchanged. This shows that local configuration in the + form of new modules added by Webmaster libraries will be left unchanged.
  18. +
+

Decision

+

We use the Configuration Ignore module +to manage configuration.

+

The module maintains a list of patterns for configuration which will be ignored +during the configuration import process. This allows us to avoid updating local +configuration.

+

By adding the wildcard * at the top of this list we choose an approach where +all configuration is considered local by default.

+

Core configuration which should not be ignored can then be added to subsequent +lines with the ~ which prefix. On a site these configuration entries will be +updated to match what is in the core configuration.

+

Config Ignore also has the option of ignoring specific values within settings. +This is relevant for settings such as system.site where we consider the site +name local configuration but 404 page paths core configuration.

+

Alternatives considered

+

Deconfig + Partial Imports

+

The Deconfig module allows developers +to mark configuration entries as exempt from import/export. This would allow us +to exempt configuration which can be managed by the library.

+

This does not handle configuration coming from new modules uploaded on webmaster +sites. Since we cannot know which configuration entities such modules will +provide and Deconfig has no concept of wildcards we cannot exempt the +configuration from these modules. Their configuration will be removed again at +deployment.

+

We could use partial imports through drush config-import --partial to not +remove configuration which is not present in the configuration filesystem.

+

We prefer Config Ignore as it provides a single solution to handle the entire +problem space.

+

Config Ignore Auto

+

The Config Ignore Auto module +extends the Config Ignore module. Config Ignore Auto registers configuration +changes and adds them to an ignore list. This way they are not overridden on +future deployments.

+

The module is based on the assumption that if an user has access to a +configuration form they should also be allowed to modify that configuration for +their site.

+

This turns the approach from Config Ignore on its head. All configuration is now +considered core until it is changed on the individual site.

+

We prefer Config Ignore as it only has local configuration which may vary +between sites. With Config Ignore Auto we would have local configuration and +the configuration of Config Ignore Auto.

+

Config Ignore Auto also have special handling of the core.extensions +configuration which manages the set of installed modules. Since webmaster sites +can have additional modules installed we would need workarounds to handle these.

+

Config Split

+

The Config Split module allows +developers to split configurations into multiple groups called settings.

+

This would allow us to map the different types of configuration to different +settings.

+

We have not been able to configure this module in a meaningful way which also +passed the provided test.

+

Consequences

+
    +
  • Core developers will have to explicitly select new configuration to not ignore + during the development process. One can not simply run drush config-export + and have the appropriate configuration not ignored.
  • +
  • Because core.extension is ignored Core developers will have to explicitly + enable and uninstall modules through code as a part of the development + process.
  • +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/architecture/adr-002-user-handling/index.html b/dpl-cms/architecture/adr-002-user-handling/index.html new file mode 100644 index 00000000..7851d397 --- /dev/null +++ b/dpl-cms/architecture/adr-002-user-handling/index.html @@ -0,0 +1,2111 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: User Handling - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: User Handling

+

Context

+

There are different types of users that are interacticting with the CMS system:

+
    +
  • Patrons that is authenticated by logging into Adgangsplatformen.
  • +
  • Editors and administrators (and similar roles) that are are handling content + and configuration of the site.
  • +
+

We need to be able to handle that both type of users can be authenticated and +authorized in the scope of permissions that are tied to the user type.

+

We had some discussions wether the Adgangsplatform users should be tied to a +Drupal user or not. As we saw it we had two options when a user logs in:

+
    +
  1. Keep session/access token client side in the browser and not creating a + Drupal user.
  2. +
  3. Create a Drupal user and map the user with the external user.
  4. +
+

Decision

+

We ended up with desicion no. 2 mentioned above. So we create a Drupal user upon +login if it is not existing already.

+

We use the OpeOpenID Connect / OAuth client module +to manage patron authentication and authorization. And we have developed a +plugin for the module called: Adgangsplatformen which connects the external +oauth service with dpl-cms.

+

Editors and administrators a.k.a normal Drupal users and does not require +additional handling.

+

Consequences

+
    +
  • By having a Drupal user tied to the external user we can use that context and + make the server side rendering show different content according to the + authenticated user.
  • +
  • Adgangsplatform settings have to be configured in the plugin in order to work.
  • +
+

Future considerations

+

Instead of creating a new user for every single user logging in via +Adgangsplatformen you could consider having just one Drupal user for all the +external users. That would get rid of the UUID -> Drupal user id mapping that +has been implemented as it is now. And it would prevent creation of a lot +of users. The decision depends on if it is necessary to distinguish between the +different users on a server side level.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/architecture/adr-003-ddb-react-integration/index.html b/dpl-cms/architecture/adr-003-ddb-react-integration/index.html new file mode 100644 index 00000000..de6f1104 --- /dev/null +++ b/dpl-cms/architecture/adr-003-ddb-react-integration/index.html @@ -0,0 +1,2060 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: DPL React integration - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: DPL React integration

+

Context

+

The DPL React components needs to be integrated and available for rendering in +Drupal. The components are depending on a library token and an access token +being set in javascript.

+

Decision

+

We decided to download the components with composer and integrate them as Drupal +libraries.

+

As described in adr-002-user-handling +we are setting an access token in the user session when a user has been through +a succesful login at Adgangsplatformen.

+

We decided that the library token is fetched by a cron job on a regular basis +and saved in a KeyValueExpirable store which automatically expires the token +when it is outdated.

+

The library token and the access token are set in javascript on the endpoint: +/dpl-react/user.js. By loading the script asynchronically when mounting the +components i javascript we are able to complete the rendering.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/architecture/adr-004-ddb-react-caching/index.html b/dpl-cms/architecture/adr-004-ddb-react-caching/index.html new file mode 100644 index 00000000..f0a9bf17 --- /dev/null +++ b/dpl-cms/architecture/adr-004-ddb-react-caching/index.html @@ -0,0 +1,2078 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Caching of DPL React and other js resources - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Caching of DPL React and other js resources

+

Context

+

The general caching strategy is defined in another document and this focused on +describing the caching strategy of DPL react and other js resources.

+

We need to have a caching strategy that makes sure that:

+
    +
  • The js files defined as Drupal libraries (which DPL react is) and pages that + make use of them are being cached.
  • +
  • The same cache is being flushed upon deploy because that is the moment where + new versions of DPL React can be introduced.
  • +
+

Decision

+

We have created a purger in the Drupal Varnish/Purge setup that is able to purge +everything. The purger is being used in the deploy routine by the command: +drush cache:rebuild-external -y

+

Consequences

+
    +
  • Everything will be invalidated on every deploy. Note: Although we are sending + a PURGE request we found out, by studing the vcl of Lagoon, that the PURGE + request actually is being translated into a BAN on req.url.
  • +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/architecture/adr-005-api-mocking/index.html b/dpl-cms/architecture/adr-005-api-mocking/index.html new file mode 100644 index 00000000..b6ca3dae --- /dev/null +++ b/dpl-cms/architecture/adr-005-api-mocking/index.html @@ -0,0 +1,2177 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: API mocking - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: API mocking

+

Context

+

DPL CMS integrates with a range of other business systems through APIs. These +APIs are called both clientside (from Browsers) and serverside (from +Drupal/PHP).

+

Historically these systems have provided setups accessible from automated +testing environments. Two factors make this approach problematic going forward:

+
    +
  1. In the future not all systems are guaranteed to provide such environments + with useful data.
  2. +
  3. Test systems have not been as stable as is necessary for automated testing. + Systems may be down or data updated which cause problems.
  4. +
+

To address these problems and help achieve a goal of a high degree of test +coverage the project needs a way to decouple from these external APIs during +testing.

+

Decision

+

We use WireMock to mock API calls. Wiremock provides +the following feature relevant to the project:

+
    +
  • Wiremock is free open source software which can be deployed in development and + tests environment using Docker
  • +
  • Wiremock can run in HTTP(S) proxy mode. This allows us to run a single + instance and mock requests to all external APIs
  • +
  • We can use the wiremock-php + client library to instrument WireMock from PHP code. We modernized the + behat-wiremock-extension + to instrument with Behat tests which we use for integration testing.
  • +
+

Instrumentation vs. record/replay

+

Software for mocking API requests generally provide two approaches:

+
    +
  • Instrumentation where an API can be used to define which responses will be + returned for what requests programmatically.
  • +
  • Record/replay where requests passing through are persisted (typically to the + filesystem) and can be modified and restored at a later point in time.
  • +
+

Generally record/replay makes it easy to setup a lot of mock data quickly. +However, it can be hard to maintain these records as it is not obvious what part +of the data is important for the test and the relationship between the +individual tests and the corresponding data is hard to determine.

+

Consequently, this project prefers instrumentation.

+

Alternatives considered

+

There are many other tools which provide features similar to Wiremock. These +include:

+
    +
  • Hoverfly: FOSS, Docker image and proxy + support. PHP + clients are less mature and no Behat + integration.
  • +
  • Mountebank: FOSS and Docker image. No proxy support, + PHP client + is less mature and no Behat integration.
  • +
  • MockServer: FOSS, Docker image and proxy + support. No PHP client and no Behat integration.
  • +
  • Mockoon: FOSS and Docker image. Does not provide + instrumentation.
  • +
+

Consequences

+
    +
  • Developers may have to engage in maintenance of the wiremock-php and + behat-wiremock-extension library
  • +
+

Status

+

Instrumentation of Wiremock with PHP is made obsolete with the migration from +Behat to Cypress.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/architecture/adr-006-api-specification/index.html b/dpl-cms/architecture/adr-006-api-specification/index.html new file mode 100644 index 00000000..d4cf59b7 --- /dev/null +++ b/dpl-cms/architecture/adr-006-api-specification/index.html @@ -0,0 +1,2122 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: API specification - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: API specification

+

Context

+

DPL CMS provides HTTP end points which are consumed by the React components. We +want to document these in an established structured format.

+

Documenting endpoints in a established structured format allows us to use tools +to generate client code for these end points. This makes consumption easier and +is a practice which is already used with other +services +in the React components.

+

Currently these end points expose business logic tied to configuration in the +CMS. There might be a future where we also need to expose editorial content +through APIs.

+

Decision

+

We use the RESTful Web Services Drupal module +to expose an API from DPL CMS and document the API using the OpenAPI 2.0/Swagger +2.0 specification as supported by the +OpenAPI and OpenAPI REST +Drupal modules.

+

This is a technically manageable, standards compliant and performant solution +which supports our initial use cases and can be expanded to support future +needs.

+

Alternatives considered

+

There are two other approaches to working with APIs and specifications for +Drupal:

+
    +
  • JSON:API: + Drupals JSON:API module + provides many features over the REST module + when it comes to exposing editorial content (or Drupal entities in general). + However it does not work well with other types of functionality which is what + we need for our initial use cases.
  • +
  • GraphQL: + GraphQL is an approach which does not work well with Drupals HTTP based + caching layer. This is important for endpoints which are called many times + for each client. + Also from version 4.x and beyond the GraphQL Drupal module + provides no easy way for us to expose editorial content at a later point in time.
  • +
+

Consequences

+ + + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/architecture/adr-007-cypress-functional-testing/index.html b/dpl-cms/architecture/adr-007-cypress-functional-testing/index.html new file mode 100644 index 00000000..94cc2012 --- /dev/null +++ b/dpl-cms/architecture/adr-007-cypress-functional-testing/index.html @@ -0,0 +1,2121 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Cypress for functional testing - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Cypress for functional testing

+

Context

+

DPL CMS employs functional testing to ensure the functional integrity of the +project.

+

This is currently implemented using Behat +which allows developers to instrument a browser navigating through different use +cases using Gherkin, a +business readable, domain specific language. Behat is used within the project +based on experience using it from the previous generation of DPL CMS.

+

Several factors have caused us to reevaluate that decision:

+ +

Decision

+

We choose to replace Behat with Cypress for functional testing.

+

Alternatives considered

+

There are other prominent tools which can be used for browser based functional +testing:

+ +

Consequences

+ + + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/architecture/adr-008-external-system-integration/index.html b/dpl-cms/architecture/adr-008-external-system-integration/index.html new file mode 100644 index 00000000..4c26c26f --- /dev/null +++ b/dpl-cms/architecture/adr-008-external-system-integration/index.html @@ -0,0 +1,2101 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Integration with external systems - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Integration with external systems

+

Context

+

DPL CMS is only intended to integrate with one external system: +Adgangsplatformen. This integration is necessary to obtain patron and library +tokens needed for authentication with other business systems. All these +integrations should occur in the browser through React components.

+

The purpose of this is to avoid having data passing through the CMS as an +intermediary. This way the CMS avoids storing or transmitting sensitive data. +It may also improve performance.

+

In some situations it may be beneficiary to let the CMS access external systems +to provide a better experience for business users e.g. by displaying options +with understandable names instead of technical ids or validating data before it +reaches end users.

+

Decision

+

We choose to allow CMS to access external systems server-side using PHP. +This must be done on behalf of the library - never the patron.

+

Alternatives considered

+
    +
  • Implementing React components to provide administrative controls in the CMS. + This would increase the complexity of implementing such controls and cause + implementors to not consider improvements to the business user experience.
  • +
+

Consequences

+
    +
  • We allow PHP client code generation for external services. These should not + only include APIs to be used with library tokens. This signals what APIs are + OK to be accessed server-side.
  • +
  • The CMS must only access services using the library token provided by the + dpl_library_token.handler service.
  • +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/architecture/adr-009-translation-system/index.html b/dpl-cms/architecture/adr-009-translation-system/index.html new file mode 100644 index 00000000..aa7ef998 --- /dev/null +++ b/dpl-cms/architecture/adr-009-translation-system/index.html @@ -0,0 +1,2189 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Translation system - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Translation system

+

Context

+

The current translation system for UI strings in DPL CMS is based solely on code +deployment of .po files.

+

However DPL CMS is expected to be deployed in about 100 instances just to cover +the Danish Public Library institutions. Making small changes to the UI texts in +the codebase would require a new deployment for each of the instances.

+

Requiring code changes to update translations also makes it difficult for +non-technical participants to manage the process themselves. They have to find a +suitable tool to edit .po files and then pass the updated files to a +developer.

+

This process could be optimized if:

+
    +
  1. Translations were provided by a central source
  2. +
  3. Translations could be managed directly by non-technical users
  4. +
  5. Distribution of translations is decoupled from deployment
  6. +
+

Decision

+

We keep using GitHub as a central source for translation files.

+

We configure Drupal to consume translations from GitHub. The Drupal translation +system already supports runtime updates and consuming translations from a +remote source.

+

We use POEditor to perform translations. POEditor is a +translation management tool that supports .po files and integrates with +GitHub. To detect new UI strings a GitHub Actions workflow scans the codebase +for new strings and notifies POEditor. Here they can be translated by +non-technical users. POEditor supports committing translations back to GitHub +where they can be consumed by DPL CMS instances.

+

Consequences

+

This approach has a number of benefits apart from addressing the original +issue:

+
    +
  • POEditor is a specialized tool to manage translations. It supports features + such as translation memory, glossaries and machine translation.
  • +
  • POEditor is web-based. Translators avoid having to find and install a suitable + tool to edit .po files.
  • +
  • POEditor is software-as-a-service. We do not need to maintain the translation + interface ourselves.
  • +
  • POEditor is free for open source projects. This means that we can use it + without having to pay for a license.
  • +
  • Code scanning means that new UI strings are automatically detected and + available for translation. We do not have to manually synchronize translation + files or ensure that UI strings are rendered by the system before they can be + translated. This can be complex when working with special cases, error + messages etc.
  • +
  • Translations are stored in version control. Managing state is complex and this + means that we have easy visibility into changes.
  • +
  • Translations are stored on GitHub. We can move away from POEditor at any time + and still have access to all translations.
  • +
  • We reuse existing systems instead of building our own.
  • +
+

A consequence of this approach is that developers have to write code that +supports scanning. This is partly supported by the Drupal Code Standards. To +support contexts developers also have to include these as a part of the t() +function call e.g.

+
// Good
+$this->t('A string to be translated', [], ['context' => 'The context']);
+$this->t('Another string', [], ['context' => 'The context']);
+// Bad
+$c = ['context' => 'The context']
+$this->t('A string to be translated', [], $c);
+$this->t('Another string', [], $c);
+
+

We could consider writing a custom sniff or PHPStan rule to enforce this

+

Potion

+

For covering the functionality of scanning the code we had two potential +projects that could solve the case:

+ +

Both projects can scan the codebase and generate a .po or .pot file with the +translation strings and context.

+

At first it made most sense to go for Potx since it is used by +localize.drupal.org and it has a long history. +But Potx is extracting strings to a .pot file without having the possibility +of filling in the existing translations. So we ended up using Potion which can +fill in the existing strings.

+

A flip side using Potion is that it is not being maintained anymore. But it +seems quite stable and a lot of work has been put into it. We could consider to +back it ourselves.

+

Alternatives considered

+

We considered the following alternatives:

+
    +
  1. Establishing our own localization server. This proved to be very complex. + Available solutions are either technically outdated + or still under heavy development. + None of them have integration with GitHub where our project is located.
  2. +
  3. Using a separate instance of DPL CMS in Lagoon as a central translation hub. + Such an instance would require maintenance and we would have to implement a + method for exposing translations to other instances.
  4. +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/caching/index.html b/dpl-cms/caching/index.html new file mode 100644 index 00000000..e2f92156 --- /dev/null +++ b/dpl-cms/caching/index.html @@ -0,0 +1,2057 @@ + + + + + + + + + + + + + + + + + + + + + + Caching - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Caching

+

DPL-CMS relies on two levels of caching. Standard Drupal Core caching, and +Varnish as an accelerating HTTP cache.

+

Drupal

+

The Drupal Core cache uses Redis as its storage backend. This takes the load off +of the database-server that is typically shared with other sites.

+

Further more, as we rely on Varnish for all caching of anonymous traffic, the +core Internal Page Cache module has been disabled.

+

Varnish

+

Varnish uses the standard Drupal VCL from lagoon.

+

The site is configured with the Varnish Purge module and configured with a +cache-tags based purger that ensures that changes made to the site, is purged +from Varnish instantly.

+

The configuration follows the Lagoon best practices - reference the +Lagoon documentation on Varnish +for further details.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/code-guidelines/index.html b/dpl-cms/code-guidelines/index.html new file mode 100644 index 00000000..7f5606a2 --- /dev/null +++ b/dpl-cms/code-guidelines/index.html @@ -0,0 +1,2596 @@ + + + + + + + + + + + + + + + + + + + + + + Code guidelines - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Code guidelines

+

The following guidelines describe best practices for developing code for the DPL +CMS project. The guidelines should help achieve:

+
    +
  • A stable, secure and high quality foundation for building and maintaining + library websites
  • +
  • Consistency across multiple developers participating in the project
  • +
  • The best possible conditions for sharing modules between DPL CMS websites
  • +
  • The best possible conditions for the individual DPL CMS website to customize + configuration and appearance
  • +
+

Contributions to the core DPL CMS project will be reviewed by members of the +Core team. These guidelines should inform contributors about what to expect in +such a review. If a review comment cannot be traced back to one of these +guidelines it indicates that the guidelines should be updated to ensure +transparency.

+

Coding standards

+

The project follows the Drupal Coding Standards +and best practices for all parts of the project: PHP, JavaScript and CSS. This +makes the project recognizable for developers with experience from other Drupal +projects. All developers are expected to make themselves familiar with these +standards.

+

The following lists significant areas where the project either intentionally +expands or deviates from the official standards or areas which developers should +be especially aware of.

+

General

+
    +
  • The default language for all code and comments is English.
  • +
+

PHP

+
    +
  • Code must be compatible with all currently available minor and major versions + of PHP from 8.0 and onwards. This is important when trying to ensure smooth + updates going forward. Note that this only applies to custom code.
  • +
  • Code must be compatible with Drupal Best Practices as defined by the + Drupal Coder module
  • +
  • Code must use types + to define function arguments, return values and class properties.
  • +
  • Code must use strict typing.
  • +
+

JavaScript

+
    +
  • All functionality exposed through JavaScript should use the + Drupal JavaScript API + and must be attached to the page using Drupal behaviors.
  • +
  • All classes used for selectors in Javascript must be prefixed with js-. + Example: <div class="gallery js-gallery"> - .gallery must only be used in + CSS, js-gallery must only be used in JS.
  • +
  • Javascript should not affect classes that are not state-classes. State classes + such as is-active, has-child or similar are classes that can be used as + an interlink between JS and CSS.
  • +
+

CSS

+
    +
  • Modules and themes should use SCSS (The project uses PostCSS + and PostCSS-SCSS). The Core system + will ensure that these are compiled to CSS files automatically as a part of + the development process.
  • +
  • Class names should follow the Block-Element-Modifier architecture + (BEM). This rule does not apply to state classes.
  • +
  • Components (blocks) should be isolated from each other. We aim for an + atomic frontend + where components should be able to stand alone. In practice, there will be + times where this is impossible, or where components can technically stand + alone, but will not make sense from a design perspective (e.g. putting a + gallery in a sidebar).
  • +
  • Components should be technically isolated by having 1 component per scss file. + **As a general rule, you can have a file called gallery.scss which contains + .gallery, .gallery__container, .gallery__* and so on. Avoid referencing + other components when possible.
  • +
  • All components/mixins/similar must be documented with a code comment. When you + create a new component.scss, there must be a comment at the top, describing + the purpose of the component.
  • +
  • Avoid using auto-generated Drupal selectors such as .pane-content. Use + the Drupal theme system to write custom HTML and use precise, descriptive + class names. It is better to have several class names on the same element, + rather than reuse the same class name for several components.
  • +
  • All "magic" numbers must be documented. If you need to make something e.g. + 350 px, you preferably need to find the number using calculating from the + context ($layout-width * 0.60) or give it a descriptive variable name + ($side-bar-width // 350px works well with the current $layout-width_)
  • +
  • Avoid using the parent selector (.class &). The use of parent selector + results in complex deeply nested code which is very hard to maintain. There + are times where it makes sense, but for the most part it can and should be + avoided.
  • +
+

Naming

+

Modules

+
    +
  • All modules written specifically for Ding3 must be prefixed with dpl.
  • +
  • The dpl prefix is not required for modules which provide functionality deemed + relevant outside the DPL community and are intended for publication on + Drupal.org.
  • +
+

Files

+

Files provided by modules must be placed in the following folders and have the +extensions defined here.

+
    +
  • General
  • +
  • MODULENAME.*.yml
  • +
  • MODULENAME.module
  • +
  • MODULENAME.install
  • +
  • templates/*.html.twig
  • +
  • Classes, interfaces and traits
  • +
  • src/**/*.php
  • +
  • PHPUnit tests
  • +
  • tests/**/*.php
  • +
  • CSS
  • +
  • If the module does not not use processing: /css/COMPONENTNAME.css
  • +
  • If the module uses preprocessing: /scss/COMPONENTNAME.scss
  • +
  • JavaScript
  • +
  • js/*.js
  • +
  • Images
  • +
  • img/*.(png|jpeg|gif|svg)
  • +
+

Module elements

+

Programmatic elements such as settings, state values and views modules must +comply to a set of common guidelines.

+
    +
  • Machine names should be prefixed with the name of the module that is + responsible for managing the elements.
  • +
  • Administrative titles, human readable names and descriptions should be + relatable to the module name.
  • +
+

As there is no finite set of programmatic elements for a DPL CMS site these +apply to all types unless explicitly specified.

+

Code Structure

+

The project follows the code structure suggested by the +drupal/recommended-project Composer template.

+

Modules, themes etc. must be placed within the corresponding folder in this +repository. If a module developed in relation to this project is of general +purpose to the Drupal community it should be placed on Drupal.org and included +as an external dependency.

+

A module must provide all required code and resources for it to work on its own +or through dependencies. This includes all configuration, theming, CSS, images +and JavaScript libraries.

+

All default configuration required for a module to function should be +implemented using the Drupal configuration system and stored in the version +control with the rest of the project source code.

+

Updating modules

+

If an existing module is expanded with updates to current functionality the +default behavior must be the same as previous versions or as close to this as +possible. This also includes new modules which replaces current modules.

+

If an update does not provide a way to reuse existing content and/or +configuration then the decision on whether to include the update resides with +the business.

+

Altering existing modules

+

Modules which alter or extend functionality provided by other modules should use +appropriate methods for overriding these e.g. by implementing alter hooks or +overriding dependencies.

+

Translations

+

All interface text in modules must be in English. Localization of such texts +must be handled using the Drupal translation API.

+

All interface texts must be provided with a context. This supports separation +between the same text used in different contexts. Unless explicitly stated +otherwise the module machine name should be used as the context.

+

Third party code

+

The project uses package managers to handle code which is developed outside of +the Core project repository. Such code must not be committed to the Core project +repository.

+

The project uses two package manages for this:

+
    +
  • Composer - primarily for managing PHP packages, + Drupal modules and other code libraries which are executed at runtime in the + production environment.
  • +
  • Yarn - primarily for managing code needed to establish + the pipeline for managing frontend assets like linting, preprocessing and + optimization of JavaScript, CSS and images.
  • +
+

When specifying third party package versions the project follows these +guidelines:

+
    +
  • Use the ^ next significant release operator + for packages which follow semantic versioning.
  • +
  • The version specified must be the latest known working and secure version. We + do not want accidental downgrades.
  • +
  • We want to allow easy updates to all working releases within the same major + version.
  • +
  • Packages which are not intended to be executed at runtime in the production + environment should be marked as development dependencies.
  • +
+

Altering third party code

+

The project uses patches rather than forks to modify third party packages. This +makes maintenance of modified packages easier and avoids a collection of forked +repositories within the project.

+
    +
  • Use an appropriate method for the corresponding package manager for managing + the patch.
  • +
  • Patches should be external by default. In rare cases it may be needed to + commit them as a part of the project.
  • +
  • When providing a patch you must document the origin of the patch e.g. through + an url in a commit comment or preferably in the package manager configuration + for the project.
  • +
+

Error handling and logging

+

Code may return null or an empty array for empty results but must throw +exceptions for signalling errors.

+

When throwing an exception the exception must include a meaningful error message +to make debugging easier. When rethrowing an exception then the original +exception must be included to expose the full stack trace.

+

When handling an exception code must either log the exception and continue +execution or (re)throw the exception - not both. This avoids duplicate log +content.

+

Drupal modules must use the Logging API. +When logging data the module must use its name as the logging channel and +an appropriate logging level.

+

Modules integrating with third party services must implement a Drupal setting +for logging requests and responses and provide a way to enable and disable this +at runtime using the administration interface. Sensitive information (such as +passwords, CPR-numbers or the like) must be stripped or obfuscated in the logged +data.

+

Code comments

+

Code comments which describe what an implementation does should only be used +for complex implementations usually consisting of multiple loops, conditional +statements etc.

+

Inline code comments should focus on why an unusual implementation has been +implemented the way it is. This may include references to such things as +business requirements, odd system behavior or browser inconsistencies.

+

Commit messages

+

Commit messages in the version control system help all developers understand the +current state of the code base, how it has evolved and the context of each +change. This is especially important for a project which is expected to have a +long lifetime.

+

Commit messages must follow these guidelines:

+
    +
  1. Each line must not be more than 72 characters long
  2. +
  3. The first line of your commit message (the subject) must contain a short + summary of the change. The subject should be kept around 50 characters long.
  4. +
  5. The subject must be followed by a blank line
  6. +
  7. Subsequent lines (the body) should explain what you have changed and why the + change is necessary. This provides context for other developers who have not + been part of the development process. The larger the change the more + description in the body is expected.
  8. +
  9. If the commit is a result of an issue in a public issue tracker, + platform.dandigbib.dk, then the subject must start with the issue number + followed by a colon (:). If the commit is a result of a private issue tracker + then the issue id must be kept in the commit body.
  10. +
+

When creating a pull request the pull request description should not contain any +information that is not already available in the commit messages.

+

Developers are encouraged to read How to Write a Git Commit Message +by Chris Beams.

+

Tool support

+

The project aims to automate compliance checks as much as possible using static +code analysis tools. This should make it easier for developers to check +contributions before submitting them for review and thus make the review process +easier.

+

The following tools pay a key part here:

+
    +
  1. PHP_Codesniffer with the + following rulesets:
  2. +
  3. Drupal Coding Standards + as defined the Drupal Coder module
  4. +
  5. RequireStrictTypesSniff + as defined by PHP_Codesniffer
  6. +
  7. Eslint and Airbnb JavaScript coding standards + as defined by Drupal Core
  8. +
  9. Prettier as defined by Drupal Core
  10. +
  11. Stylelint with the following rulesets:
  12. +
  13. As defined by Drupal Core
  14. +
  15. BEM as defined by the stylelint-bem project
  16. +
  17. Browsersupport as defined by the + stylelint-no-unsupported-browser-features project
  18. +
  19. PHPStan with the following configuration:
  20. +
  21. Analysis level 8 to support detection of missing types
  22. +
  23. Drupal support as defined by the phpstan-drupal project
  24. +
  25. Detection of deprecated code as defined by the phpstan-deprecation-rules project
  26. +
+

In general all tools must be able to run locally. This allows developers to get +quick feedback on their work.

+

Tools which provide automated fixes are preferred. This reduces the burden of +keeping code compliant for developers.

+

Code which is to be exempt from these standards must be marked accordingly in +the codebase - usually through inline comments (Eslint, +PHP Codesniffer). +This must also include a human readable reasoning. This ensures that deviations +do not affect future analysis and the Core project should always pass through +static analysis.

+

If there are discrepancies between the automated checks and the standards +defined here then developers are encouraged to point this out so the automated +checks or these standards can be updated accordingly.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/config-import/index.html b/dpl-cms/config-import/index.html new file mode 100644 index 00000000..33c5d5ee --- /dev/null +++ b/dpl-cms/config-import/index.html @@ -0,0 +1,2078 @@ + + + + + + + + + + + + + + + + + + + + + + Configuration import - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Configuration import

+

Setting up a new site for testing certain scenarios can be repetitive. To avoid +this the project provides a module: DPL Config Import. This module can be used +to import configuration changes into the site and install/uninstall modules in a +single step.

+

The configuration changes are described in a YAML file with configuration entry +keys and values as well as module ids to install or uninstall.

+

How to use

+
    +
  1. Download the example file + that comes with the module.
  2. +
  3. Edit it to set the different configuration values.
  4. +
  5. Upload the file at /admin/config/configuration/import
  6. +
  7. Clear the cache.
  8. +
+

How it is parsed

+

The yaml file has two root elements configuration and modules.

+

A basic file looks like this:

+
configuration:
+  # Add keys for configuration entries to set.
+  # Values will be merged with existing values.
+  system.site:
+    # Configuration values can be set directly
+    slogan: 'Imported by DPL config import'
+    # Nested configuration is also supported
+    page:
+      # All values in nested configuration must have a key. This is required to
+      # support numeric configuration keys.
+      403: '/user/login'
+
+modules:
+  # Add module ids to install or uninstall
+  install:
+    - menu_ui
+  uninstall:
+    - redis
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/configuration-management/index.html b/dpl-cms/configuration-management/index.html new file mode 100644 index 00000000..ba8bef61 --- /dev/null +++ b/dpl-cms/configuration-management/index.html @@ -0,0 +1,2310 @@ + + + + + + + + + + + + + + + + + + + + + + Configuration Management - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Configuration Management

+

We use the Configuration Ignore module +to manage configuration.

+

In general all configuration is ignored except for configuration which should +explicitly be managed by DPL CMS core.

+

Background

+

Configuration management for DPL CMS is a complex issue. The complexity stems +from the following factors:

+

Site types

+

There are multiple types of DPL CMS sites all using the same code base:

+
    +
  1. Developer (In Danish: Programmør) sites where the library is entirely free + to work with the codebase for DPL CMS as they please for their site
  2. +
  3. Webmaster sites where the library can install and + manage additional modules for their DPL CMS site
  4. +
  5. Editor (In Danish: Redaktør) sites where the library can configure their + site based on predefined configuration options provided by DPL CMS
  6. +
  7. Core sites which are default versions of DPL CMS used for development and + testing purposes
  8. +
+

All these site types must support the following properties:

+
    +
  1. It must be possible for system administrators to deploy new versions of + DPL CMS which may include changes to the site configuration
  2. +
  3. It must be possible for libraries to configure their site based on the + options provided by their type site. This configuration must not be + overridden by new versions of DPL CMS.
  4. +
+

Configuration types

+

This can be split into different types of configuration:

+
    +
  1. Core configuration: This is the configuration for the base installation of + DPL CMS which is shared across all sites. The configuration will be imported + on deployment to support central development of the system.
  2. +
  3. Local configuration: This is the local configuration for the individual + site. The level of configuration depends on the site type but no matter the + type this configuration must not be overridden on deployment of new versions + of DPL CMS.
  4. +
+

Howtos

+

Install a new site from scratch

+
    +
  1. Run drush site-install --existing-config -y
  2. +
+

Add new core configuration

+
    +
  1. Create the relevant configuration through the administration interface
  2. +
  3. Run drush config-export -y
  4. +
  5. Append the key for the configuration to + config_ignore.settings.ignored_config_entities with the ~ prefix
  6. +
  7. Commit the new configuration files and the updated config_ignore.settings + file
  8. +
+

Update existing core configuration

+
    +
  1. Update the relevant configuration through the administration interface
  2. +
  3. Run drush config-export -y
  4. +
  5. Commit the updated configuration files
  6. +
+

NB: The keys for these configuration files should already be in +config_ignore.settings.ignored_config_entities.

+

Add new local configuration

+
    +
  1. Update the relevant configuration through the administration interface
  2. +
  3. Run drush config-export -y
  4. +
  5. Commit the updated configuration files
  6. +
+

Enable a new module

+ +
    +
  1. Add the module to the project code base or as a Composer dependency
  2. +
  3. Create an update hook in the DPL CMS installation profile which enables the + module1
  4. +
+
function dpl_cms_update_9000() {
+   \Drupal::service('module_installer')->install(['shortcut']);
+}
+
+
    +
  1. Run the update hook locally drush updatedb -y
  2. +
  3. Export configuration drush config-export -y
  4. +
  5. Commit the resulting changes to the site configuration, codebase and/or + Composer files
  6. +
+

Uninstall a existing module

+
    +
  1. Create an update hook in the DPL CMS installation profile which uninstalls + the module1
  2. +
+
function dpl_cms_update_9001() {
+   \Drupal::service('module_installer')->uninstall(['shortcut']);
+}
+
+
    +
  1. Run the update hook locally drush updatedb -y
  2. +
  3. Commit the resulting changes to the site configuration
  4. +
  5. Export configuration drush config-export -y
  6. +
  7. Plan for a future removal of code for the module
  8. +
+ + +

Deploy configuration changes

+
    +
  1. Run drush deploy
  2. +
+

NB: It is important that the official Drupal deployment procedure is followed. +Database updates must be executed before configuration is imported. Otherwise +we risk ending up in a situation where the configuration contains references +to modules which are not enabled.

+
+
+
    +
  1. +

    Creating update hooks for modules is only necessary once we have sites +running in production which will not be reinstalled. Until then it is OK to +enable/uninstall modules as normal and committing changes to core.extensions

    +
  2. +
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/diagrams/add-or-update-translation.puml b/dpl-cms/diagrams/add-or-update-translation.puml new file mode 100644 index 00000000..e574e9af --- /dev/null +++ b/dpl-cms/diagrams/add-or-update-translation.puml @@ -0,0 +1,6 @@ +@startuml +Translator -> Poeditor: Translates strings +Translator -> Poeditor: Pushes button to export strings to GitHub +Poeditor -> GitHub: Commits translations to GitHub (develop) +DplCms -> GitHub: By manually requesting or by a cron job translations are imported to DPL CMS. +@enduml diff --git a/dpl-cms/diagrams/adgangsplatform-login.puml b/dpl-cms/diagrams/adgangsplatform-login.puml new file mode 100644 index 00000000..0dfa5ce1 --- /dev/null +++ b/dpl-cms/diagrams/adgangsplatform-login.puml @@ -0,0 +1,56 @@ +@startuml +actor User as user +participant DPL_CMS as cms +participant OpenidConnect as oc +participant Adgangsplatformen as ap +user -> cms: Click login into Adgangsplatformen +cms -> oc: Get authorization url and return url +oc -> oc: Gets urls and creates oauth state hash + +oc -> user: Tell browser to redirect to authorization url +activate user +note left +The authorization url contains: +* returnurl +* state hash +* agency id +end note +user -> ap: Redirect to external site using the full authorization url +ap -> ap: Internal authentication + +ap -> user: Redirect to the return url +deactivate user +user -> cms: Send Adgangsplatform reponse +cms -> oc: Validate values from the Adgangsplatform reponse + +oc -> ap: Request access token +activate oc +ap -> oc: Returning access token with expire time stamp +oc -> ap: Requesting user info +ap -> oc: Returning user info (UUID) +deactivate oc + +alt First time login - the user is not in Drupal yet + +oc -> cms: Create user +note left +* Random unique email/username set on user. +* The UUID from Adgangsplatformen is encrypted +and used for mapping external user to the Drupal user. +* The Drupal user gets the Drupal role: Patron. +end note + +else Recurrent login - the user exists in Drupal + +oc -> cms: Update user +note left +Nothing is updated on the user +end note + +end + + +oc -> cms: Begin Drupal user session. +oc -> cms: Saving access token in active user session +oc -> user: Redirecting inlogged user to the frontpage +@enduml diff --git a/dpl-cms/diagrams/adgangsplatform-logout.puml b/dpl-cms/diagrams/adgangsplatform-logout.puml new file mode 100644 index 00000000..8293592f --- /dev/null +++ b/dpl-cms/diagrams/adgangsplatform-logout.puml @@ -0,0 +1,30 @@ +@startuml +actor User as user +participant DPL_CMS as cms +participant Adgangsplatformen as ap +user -> cms: Clicks logout +group The user has an access token +cms -> ap: Requests the single logout service at Adgangsplatformen\nThe access token is used in the request +ap -> cms: Response to the cms + +cms -> cms: Logs user out by ending session +note right +The access token is a part of the user session +and gets flushed in the procedure. +end note +cms -> user: Redirects to front page +end + +group The user has no access token +cms -> cms: Logs user out by ending session +cms -> user: Redirects to front page +end +note left +There can be two reasons why the user +does not have an access token: +* The user is a "non-adgangsplatformen" user, eg. an editor. +* Something failed in the access token retrival +and the access token is missing. +end note + +@enduml diff --git a/dpl-cms/diagrams/render-png/adgangsplatform-login.png b/dpl-cms/diagrams/render-png/adgangsplatform-login.png new file mode 100644 index 00000000..b045fe3b Binary files /dev/null and b/dpl-cms/diagrams/render-png/adgangsplatform-login.png differ diff --git a/dpl-cms/diagrams/render-png/adgangsplatform-logout.png b/dpl-cms/diagrams/render-png/adgangsplatform-logout.png new file mode 100644 index 00000000..4cece783 Binary files /dev/null and b/dpl-cms/diagrams/render-png/adgangsplatform-logout.png differ diff --git a/dpl-cms/diagrams/render-svg/adgangsplatform-login.svg b/dpl-cms/diagrams/render-svg/adgangsplatform-login.svg new file mode 100644 index 00000000..8eb693be --- /dev/null +++ b/dpl-cms/diagrams/render-svg/adgangsplatform-login.svg @@ -0,0 +1,66 @@ +UserUserDPL_CMSDPL_CMSOpenidConnectOpenidConnectAdgangsplatformenAdgangsplatformenClick login into AdgangsplatformenGet authorization url and return urlGets urls and creates oauth state hashTell browser to redirect to authorization urlThe authorization url contains:returnurlstate hashagency idRedirect to external site using the full authorization urlInternal authenticationRedirect to the return urlSend Adgangsplatform reponseValidate values from the Adgangsplatform reponseRequest access tokenReturning access token with expire time stampRequesting user infoReturning user info (UUID)alt[First time login - the user is not in Drupal yet]Create userRandom unique email/username set on user.The UUID from Adgangsplatformen is encryptedand used for mapping external user to the Drupal user.The Drupal user gets the Drupal role: Patron.[Recurrent login - the user exists in Drupal]Update userNothing is updated on the userBegin Drupal user session.Saving access token in active user sessionRedirecting inlogged user to the frontpage \ No newline at end of file diff --git a/dpl-cms/diagrams/render-svg/adgangsplatform-logout.svg b/dpl-cms/diagrams/render-svg/adgangsplatform-logout.svg new file mode 100644 index 00000000..084ba9b4 --- /dev/null +++ b/dpl-cms/diagrams/render-svg/adgangsplatform-logout.svg @@ -0,0 +1,40 @@ +UserUserDPL_CMSDPL_CMSAdgangsplatformenAdgangsplatformenClicks logoutThe user has an access tokenRequests the single logout service at AdgangsplatformenThe access token is used in the requestResponse to the cmsLogs user out by ending sessionThe access token is a part of the user sessionand gets flushed in the procedure.Redirects to front pageThe user has no access tokenThere can be two reasons why the userdoes not have an access token:The user is a "non-adgangsplatformen" user, eg. an editor.Something failed in the access token retrivaland the access token is missing.Logs user out by ending sessionRedirects to front page \ No newline at end of file diff --git a/dpl-cms/example-content/index.html b/dpl-cms/example-content/index.html new file mode 100644 index 00000000..0fa1964c --- /dev/null +++ b/dpl-cms/example-content/index.html @@ -0,0 +1,2092 @@ + + + + + + + + + + + + + + + + + + + + + + Example content - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Example content

+

We use the Default Content module +to manage example content. Such content is typically used when setting up +development and testing environments.

+

All actual example content is stored with the DPL Example Content module.

+

Usage of the module in this project is derived from the official documentation.

+

Howtos

+

Add additional default content

+
    +
  1. Create the default content
  2. +
  3. Determine the UUIDs for the entities which should be exported as default + content. The easiest way to do this is to enable the Devel module, view + the entity and go to the Devel tab.
  4. +
  5. Add the UUID (s) (and if necessary entity types) to the + dpl_example_content.info.yml file
  6. +
  7. Export the entities by running drush default-content:export-module dpl_example_content
  8. +
  9. Commit the new files under web/modules/custom/dpl_example_content
  10. +
+

Update existing default content

+
    +
  1. Update existing content
  2. +
  3. Export the entities by running drush default-content:export-module dpl_example_content
  4. +
  5. Remove references to and files from UUIDs which are no longer relevant.
  6. +
  7. Commit updated files under web/modules/custom/dpl_example_content
  8. +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/images/backup_tab.png b/dpl-cms/images/backup_tab.png new file mode 100644 index 00000000..af029053 Binary files /dev/null and b/dpl-cms/images/backup_tab.png differ diff --git a/dpl-cms/images/retrieve.png b/dpl-cms/images/retrieve.png new file mode 100644 index 00000000..e4c0137f Binary files /dev/null and b/dpl-cms/images/retrieve.png differ diff --git a/dpl-cms/images/virtiofs.png b/dpl-cms/images/virtiofs.png new file mode 100644 index 00000000..1bc9d4a7 Binary files /dev/null and b/dpl-cms/images/virtiofs.png differ diff --git a/dpl-cms/index.html b/dpl-cms/index.html new file mode 100644 index 00000000..f437bab8 --- /dev/null +++ b/dpl-cms/index.html @@ -0,0 +1,2023 @@ + + + + + + + + + + + + + + + + + + + + + + DPL CMS Documentation - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

DPL CMS Documentation

+

The documentation in this folder describes how to develop DPL CMS.

+

The focus of the documentation is to inform developers of how to develop the +CMS, and give some background behind the various architectural choices.

+

Layout

+

The documentation falls into two categories:

+

The Markdown-files in this +directory document the system as it. Eg. you can read about how to add a new +entry to the core configuration.

+

The ./architecture folder contains our +Architectural Decision Records +that describes the reasoning behind key architecture decisions. Consult these +records if you need background on some part of the CMS, or plan on making any +modifications to the architecture.

+

As for the remaining files and directories

+
    +
  • ./diagrams contains diagram files like draw.io or PlantUML and + rendered diagrams in png/svg format. See the section for details.
  • +
  • ./images this is just plain images used by documentation files.
  • +
  • ./Taskfile.yml a go-task Taskfile, + run task to list available tasks.
  • +
+

Diagrams

+

We strive to keep the diagrams and illustrations used in the documentation as +maintainable as possible. A big part of this is our use of programmatic +diagramming via PlantUML and Open Source based +manual diagramming via diagrams.net (formerly +known as draw.io).

+

When a change has been made to a *.puml or *.drawio file, you should +re-render the diagrams using the command task render and commit the result.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/lagoon-environments/index.html b/dpl-cms/lagoon-environments/index.html new file mode 100644 index 00000000..71a09765 --- /dev/null +++ b/dpl-cms/lagoon-environments/index.html @@ -0,0 +1,2288 @@ + + + + + + + + + + + + + + + + + + + + + + Lagoon environments - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Lagoon environments

+

We use the Lagoon application delivery platform to +host environments for different stages of the DPL CMS project. Our Lagoon +installation is managed by the DPL Platform project.

+

One such type of environment is pull request environments. +These environments are automatically created when a developer creates a pull +request with a change against the project and allows developers and project +owners to test the result before the change is accepted.

+

Howtos

+

Create an environment for a pull request

+
    +
  1. Create a pull request for the change on GitHub. The pull request must be + created from a branch in the same repository as the target branch.
  2. +
  3. Wait for GitHub Actions related to Lagoon deployment to complete. Note: This + deployment process can take a while. Be patient.
  4. +
  5. A link to the deployed environment is available in the section between pull + request activity and Actions
  6. +
  7. The environment is deleted when the pull request is closed
  8. +
+

Access the administration interface for a pull request environment

+

Accessing the administration interface for a pull request environment may be +needed to test certain functionalities. This can be achieved in two ways:

+

Through the Lagoon administration UI

+
    +
  1. Access the administration UI (see below)
  2. +
  3. Go to the environment corresponding to the pull request number
  4. +
  5. Go to the Task section for the environment
  6. +
  7. Select the "Generate login link [drush uli]" task and click "Run task"
  8. +
  9. Refresh the page to see the task in the task list and wait a bit
  10. +
  11. Refresh the page to see the task complete
  12. +
  13. Go to the task page
  14. +
  15. The log output contains a one-time login link which can be used to access + the administration UI
  16. +
+

Through the Lagoon CLI

+
    +
  1. Run task lagoon:drush:uli
  2. +
  3. The log output contains a one-time login link which can be used to access + the administration UI
  4. +
+

Access the Lagoon administration UI

+
    +
  1. Contact administrators of the DPL Platform Lagoon instance to apply for an + user account.
  2. +
  3. Access the URL for the UI of the instance e.g https://ui.lagoon.dplplat01.dpl.reload.dk/
  4. +
  5. Log in with your user account (see above)
  6. +
  7. Go to the dpl-cms project
  8. +
+

Setup the Lagoon CLI

+
    +
  1. Locate information about the Lagoon instance to use in the DPL Platform + documentation
  2. +
  3. Access the URL for the UI of the instance
  4. +
  5. Log in with your user account (see above)
  6. +
  7. Go to the Settings page
  8. +
  9. Add your SSH public key to your account
  10. +
  11. Install the Lagoon CLI
  12. +
  13. Configure the Lagoon CLI to use the instance:
  14. +
+
lagoon config add \
+  --lagoon [instance name e.g. "dpl-platform"] \
+  --hostname [host to connect to with SSH] \
+  --port [SSH port] \
+  --graphql [url to GraphQL endpoint] \
+  --ui [url to UI] \
+
+
    +
  1. Verify the installation:
  2. +
+
lagoon login --lagoon [instance name]
+lagoon whoami --lagoon [instance name]
+
+
    +
  1. Use the DPL Platform as your default Lagoon instance:
  2. +
+
lagoon config default --lagoon [instance name]
+
+

Using cron in pull request environments

+

The .lagoon.yml has an environments section where it is possible to control +various settings. +On root level you specify the environment you want to address (eg.: main). +And on the sub level of that you can define the cron settings. +The cron settings for the main branch looks (in the moment of this writing) +like this:

+
environments:
+  main:
+    cronjobs:
+    - name: drush cron
+      schedule: "M/15 * * * *"
+      command: drush cron
+      service: cli
+
+

If you want to have cron running on a pull request environment, you have to +make a similar block under the environment name of the PR. +Example: In case you would have a PR with the number #135 it would look +like this:

+
environments:
+  pr-135:
+    cronjobs:
+    - name: drush cron
+      schedule: "M/15 * * * *"
+      command: drush cron
+      service: cli
+
+

Workflow with cron in pull request environments

+

This way of making sure cronb is running in the PR environments is +a bit tedious but it follows the way Lagoon is handling it. +A suggested workflow with it could be:

+
    +
  • Create PR with code changes as normally
  • +
  • Write the .lagoon.yml configuration block connected to the current PR #
  • +
  • When the PR has been approved you delete the configuration block again
  • +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/local-development/index.html b/dpl-cms/local-development/index.html new file mode 100644 index 00000000..89594fd9 --- /dev/null +++ b/dpl-cms/local-development/index.html @@ -0,0 +1,2117 @@ + + + + + + + + + + + + + + + + + + + + + + Local development - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Local development

+

Copy database from Lagoon environment to local setup

+

Prerequisites:

+
    +
  • Login credentials to the Lagoon UI, or an existing database dump
  • +
+

The following describes how to first fetch a database-dump and then import the +dump into a running local environment. Be aware that this only gives you the +database, not any files from the site.

+
    +
  1. To retrieve a database-dump from a running site, consult the + "How do I download a database dump?" + guide in the official Lagoon. Skip this step if you already have a + database-dump.
  2. +
  3. Place the dump in the database-dump directory, be aware + that the directory is only allowed to contain a single .sql file.
  4. +
  5. Start a local environment using task dev:reset
  6. +
  7. Import the database by running task dev:restore:database
  8. +
+

Copy files from Lagoon environment to local setup

+

Prerequisites:

+
    +
  • Login credentials to the Lagoon UI, or an existing nginx files dump
  • +
+

The following describes how to first fetch a files backup package +and then replace the files in a local environment.

+

If you need to get new backup files from the remote site:

+ +
    +
  1. Login to the lagoon administration and navigate to the project/environment.
  2. +
  3. Select the backup tab:
  4. +
+

backup_tab image

+
    +
  1. Retrieve the files backup you need:
  2. +
+

retrieve image +4. Due to a UI bug you need to RELOAD the window and then it should be possible + to download the nginx package.

+ + +

Replace files locally:

+
    +
  1. Place the files dump in the files-backup directory, be aware + that the directory is only allowed to contain a single .tar.gz file.
  2. +
  3. Start a local environment using task dev:reset
  4. +
  5. Restore the filesš by running task dev:restore:files
  6. +
+

Get a specific release of dpl-react - without using composer install

+

In a development context it is not very handy only +to be able to get the latest version of the main branch of dpl-react.

+

So a command has been implemented that downloads the specific version +of the assets and overwrites the existing library.

+

You need to specify which branch you need to get the assets from. +The latest HEAD of the given branch is automatically build by Github actions +so you just need to specify the branch you want.

+

It is used like this:

+
BRANCH=[BRANCH_FROM_DPL_REACT_REPOSITORY] task dev:dpl-react:overwrite
+
+

Example:

+
BRANCH=feature/more-releases task dev:dpl-react:overwrite
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/translation-system/index.html b/dpl-cms/translation-system/index.html new file mode 100644 index 00000000..c568b191 --- /dev/null +++ b/dpl-cms/translation-system/index.html @@ -0,0 +1,1975 @@ + + + + + + + + + + + + + + + + + + + + + + Translation system - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Translation system

+ + + + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-cms/translation/index.html b/dpl-cms/translation/index.html new file mode 100644 index 00000000..38f37d9f --- /dev/null +++ b/dpl-cms/translation/index.html @@ -0,0 +1,2163 @@ + + + + + + + + + + + + + + + + + + + + + + Translation - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Translation

+

We manage translations as a part of the codebase using .po translation files. +Consequently translations must be part of either official or local translations +to take effect on the individual site.

+

DPL CMS is configured to use English as the master language but is configured +to use Danish for all users through language negotiation. This allows us to +follow a process where English is the default for the codebase but actual usage +of the system is in Danish.

+

Translation system

+

To make the "translation traffic" work following components are being used:

+
    +
  • GitHub
  • +
  • Stores .po files in git with + translatable strings and translations
  • +
  • GitHub Actions
  • +
  • Scans codebase for new translatable strings and commits them to GitHub
  • +
  • Notifies POEditor that new translatable strings are available
  • +
  • Publishes .po files to GitHub Pages
  • +
  • POEditor
  • +
  • Provides an interface for translators
  • +
  • Links translations with .po files on GitHub
  • +
  • Provides webhooks where external systems can notify of new translations
  • +
  • DPL CMS
  • +
  • Drupal installation which is configured to use GitHub Pages as an interface + translation server from which .po + files can be consumed.
  • +
+

The following diagram show how these systems interact to support the flow of +from introducing a new translateable string in the codebase to DPL CMS consuming +an updated translation with said string.

+

case

+
sequenceDiagram
+  Actor Translator
+  Actor Developer
+  Developer ->> Developer: Open pull request with new translatable string
+  Developer ->> GitHubActions: Merge pull request into develop
+  GitHubActions ->> GitHubActions: Scan codebase and write strings to .po file
+  GitHubActions ->> GitHubActions: Fill .po file with existing translations
+  GitHubActions ->> GitHub: Commit .po file with updated strings
+  GitHubActions ->> Poeditor: Call webhook
+  Poeditor ->> GitHub: Fetch updated .po file
+  Poeditor ->> Poeditor: Synchronize translations with latest strings and translations
+  Translator ->> Poeditor: Translate strings
+  Translator ->> Poeditor: Export strings to GitHub
+  Poeditor ->> GitHub: Commit .po file with updated translations to develop
+  DplCms ->> GitHub: Fetch .po file with latest translations
+  DplCms ->> DplCms: Import updated translations
+

Howtos

+

Add new or update existing translation

+
    +
  1. Log into POEditor.com and go to the dpl-cms project
  2. +
  3. Go to the relevant language
  4. +
  5. Locate the string (term) to be translated
  6. +
  7. Translate the string
  8. +
+

Publish updated translations

+
    +
  1. Log into POEditor.com
  2. +
  3. Select the "Settings" tab
  4. +
  5. Click the GitHub code hosting service
  6. +
  7. Check the relevant language(s)
  8. +
  9. Select "Export to GitHub" and click "Go"
  10. +
+

Import updated translations

+
    +
  1. Run drush locale-check
  2. +
  3. Run drush locale-update
  4. +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-design-system/architecture/adr-001-skeleton-screens/index.html b/dpl-design-system/architecture/adr-001-skeleton-screens/index.html new file mode 100644 index 00000000..03fe52a6 --- /dev/null +++ b/dpl-design-system/architecture/adr-001-skeleton-screens/index.html @@ -0,0 +1,2129 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Skeleton Screens - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Skeleton Screens

+

Context

+

In the work of trying to improve the performance of the search results +we needed a way to fill the viewport with a simulated interface in order to:

+
    +
  • Show some content immediately to the user
  • +
  • Prevent layout shifting between loading state and ready state
  • +
+

Decision

+

We decided to implement skeleton screens when loading data. The skeleton screens +are rendered in pure css. +The css classes are coming from the library: skeleton-screen-css

+

Alternatives considered

+

The library is very small and based on simple css rules, so we could have +considered replicating it in our own design system or make something similar. +But by using the open source library we are ensured, to a certain extent, +that the code is being maintained, corrected and evolves as time goes by.

+

We could also have chosen to use images or GIF's to render the screens. +But by using the simple toolbox of skeleton-screen-css we should be able +to make screens for all the different use cases in the different apps.

+

Consequences

+

It is now possible, with a limited amount of work, to construct skeleton screens +in the loading state of the various user interfaces.

+

Because we use library where skeletons are implemented purely in CSS +we also provide a solution which can be consumed in any technology +already using the design system without any additional dependencies, +client side or server side.

+

BEM rules when using Skeleton Screen Classes in dpl-design-system

+

Because we want to use existing styling setup in conjunction +with the Skeleton Screen Classes we sometimes need to ignore the existing +BEM rules that we normally comply to. +See eg. the search result styling.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-design-system/index.html b/dpl-design-system/index.html new file mode 100644 index 00000000..991d3ddd --- /dev/null +++ b/dpl-design-system/index.html @@ -0,0 +1,2417 @@ + + + + + + + + + + + + + + + + + + + + + + DPL Design System - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

DPL Design System

+

DPL Design System is a library of UI components that should be used as a common +base system for "Danmarks Biblioteker" / "Det Digitale Folkebibliotek". The +design is implemented +with Storybook +/ React and is output with HTML markup and css-classes +through an addon in Storybook.

+

The codebase follows the naming that designers have used in Figma closely +to ensure consistency.

+

Requirements

+

This project comes with go-task and docker +compose, hence the requirements are limited to having docker install and tasks.

+

Manual requirements

+

This project can be used outside docker with the following requirements:

+
    +
  • node 16
  • +
  • yarn
  • +
+

Check in the terminal which versions you have installed with node -v.

+

Installation

+

Use the tasks defined in Taskfile to run the project:

+
task dev:install
+
+

Installation outside docker

+

Use the node package manager to install project dependencies:

+
yarn install
+
+

Development

+

To start the docker compose setup in development simple use the start task:

+
task dev:start
+
+

To see the output from the compile process and start of storybook:

+
task dev:logs
+
+

Use task and tabulator key in the terminal to see the other predefined tasks:

+
task dev:[TAB]
+
+

Development without docker

+

To start developing run:

+
yarn dev
+
+

Components and CSS will be automatically recompiled when making changes in the +source code.

+

Usage

+

The project is available in two ways and should be consumed accordingly:

+
    +
  1. As package in the local npm registry for this repository
  2. +
  3. As a dist.zip file attached to a release for this repository
  4. +
+

Both releases contain the built assets of the project: JavaScript files, CSS +styles and icons.

+

You can find the HTML output for a given story under the HTML tab inside +storybook.

+

NPM package

+

The GitHub NPM package registry requires authentication if you are to access +packages there.

+

Consequently, if you want to use the design system as an NPM package or if you +use a project that depends on the design system as an NPM package you must +authenticate:

+
    +
  1. Create a GitHub token with the required scopes: repo and read:packages
  2. +
  3. Run npm login --registry=https://npm.pkg.github.com
  4. +
  5. Enter the following information:
  6. +
+
> Username: [Your GitHub username]
+> Password: [Your GitHub token]
+> Email: [An email address used with your GitHub account]
+
+

Note that you will need to reauthenticate when your personal access token +expires.

+

Deployment and releases

+

The project is automatically built and deployed +on pushes to every branch and every tag and the result is available as releases +which support both types of usage. This applies for the original +repository on GitHub and all GitHub forks.

+

You can follow the status of deployments in the Actions list for the repository +on GitHub. +The action logs also contain additional details regarding the contents and +publication of each release. If using a fork then deployment actions can be +seen on the corresponding list.

+

In general consuming projects should prefer tagged releases as they are stable +proper releases.

+

During development where the design system is being updated in parallel with +the implementation of a consuming project it may be advantageous to use a +release tagging a branch.

+

Tagged releases

+

Run the following to publish a tag and create a release:

+
git tag -a v*.*.* && git push origin v*.*.*
+
+

Usage: npm package

+

In the consuming project update usage to the new release:

+
npm install @danskernesdigitalebibliotek/dpl-design-system@*.*.*
+
+

Usage: Release file

+

Find the release for the tag on the releases page on GitHub +and download the dist.zip file from there and use it as needed in the +consuming project.

+

Branch releases

+

The project automatically creates a release for each branch.

+

Example: Pushing a commit to a new branch feature/reservation-modal will +create the following parts:

+
    +
  1. A git tag for the commit release-feature/reservation-modal. A tag is needed + to create a GitHub release.
  2. +
  3. A GitHub release for the called feature/reservation-modal. The build is + attached here.
  4. +
  5. A package in the local npm repository tagged feature-reservation-modal. + Special characters like / are not supported by npm tags and are converted + to -.
  6. +
+

Updating the branch will update all parts accordingly.

+

Usage: npm package

+

In the consuming project update usage to the new release:

+
npm install @danskernesdigitalebibliotek/dpl-design-system@feature-reservation-modal
+
+

If your release belongs to a fork you can use aliasing +to point to the release of the package in the npm repository for the fork:

+
npm config set @my-fork:registry=https://npm.pkg.github.com
+npm install @danskernesdigitalebibliotek/dpl-design-system@npm:@my-fork/dpl-design-system@feature-reservation-modal
+
+

This will update your package.json and lock files accordingly. Note that +branch releases use temporary versions in the format 0.0.0-[GIT-SHA] and you +may in practice see these referenced in both files.

+

If you push new code to the branch you have to update the version used in the +consuming project:

+
npm update @danskernesdigitalebibliotek/dpl-design-system
+
+

Aliasing, +repository configuration and +updating installed packages +are also supported by Yarn.

+

Usage: Release file

+

Find the release for the branch on the releases page on GitHub +and download the dist.zip file from there and use it as needed in the +consuming project.

+

If your branch belongs to a fork then you can find the release on the releases +page for the fork.

+

Repeat the process if you push new code to the branch.

+

Storybook

+

Spin up storybook by running this command in the terminal:

+
yarn storybook
+
+

When storybook is ready it automatically opens up in a browser with the +interface ready to use.

+

Chromatic

+

We are using Chromatic for visual test. You can access the dashboard +under the danskernesdigitalebibliotek (organisation) dpl-design-system +(project).

+

https://www.chromatic.com/builds?appId=616ffdab9acbf5003ad5fd2b

+

You can deploy a version locally to Chromatic by running:

+
yarn chromatic
+
+

Make sure to set the CHROMATIC_PROJECT_TOKEN environment variable is available +in your shell context. You can access the token from:

+

https://www.chromatic.com/manage?appId=616ffdab9acbf5003ad5fd2b&view=configure

+

What is Storybook

+

Storybook is an +open source tool for building UI components and pages in isolation from your +app's business logic, data, and context. Storybook helps you document components +for reuse and automatically visually test your components to prevent bugs. It +promotes the component-driven process and agile +development.

+

It is possible to extend Storybook with an ecosystem of addons that help you do +things like fine-tune responsive layouts or verify accessibility.

+

How to use

+

The Storybook interface is simple and intuitive to use. Browse the project's +stories now by navigating to them in the sidebar.

+

The stories are placed in a flat structure, where developers should not spend +time thinking of structure, since we want to keep all parts of the system under +a heading called Library. This Library is then dividid in folders where common +parts are kept together.

+

To expose to the user how we think these parts stitch together for example +for the new website, we have a heading called Blocks, to resemble what cms +blocks a user can expect to find when building pages in the choosen CMS.

+

This could replicate in to mobile applications, newsletters etc. all pulling +parts from the Library.

+

Each story has a corresponding .stories file. View their code in the +src/stories directory to learn how they work. +The stories file is used to add the component to the Storybook interface via +the title. Start the title with "Library" or "Blocks" and use / to +divide into folders fx. Library / Buttons / Button

+

Addons

+

Storybook ships with some essential +pre-installed addons +to power the core Storybook experience.

+ +

There are many other helpful addons to customise the usage and experience. +Additional addons used for this project:

+
    +
  • +

    HTML / storybook-addon-html: + This addon is used to display compiled HTML markup for each story and make it + easier for developers to grab the code. Because we are developing with React, + it is necessary to be able to show the HTML markup with the css-classes to + make it easier for other developers that will implement it in the future. + If a story has controls the HTML markup changes according to the controls that + are set.

    +
  • +
  • +

    Designs / storybook-addon-designs: + This addon is used to embed Figma in the addon panel for a better + design-development workflow.

    +
  • +
  • +

    A11Y: + This addon is used to check the accessibility of the components.

    +
  • +
+

All the addons can be found in storybook/main.js directory.

+

Important to notice

+
Internal classes
+

To display some components (fx Colors, Spacing) in a more presentable way, we +are using some "internal" css-classes which can be found in the +styles/internal.scss file. All css-classes with "internal" in the front should +therefore be ignored in the HTML markup.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/architecture/adr/adr-001-lagoon/index.html b/dpl-platform/architecture/adr/adr-001-lagoon/index.html new file mode 100644 index 00000000..9e10783b --- /dev/null +++ b/dpl-platform/architecture/adr/adr-001-lagoon/index.html @@ -0,0 +1,2094 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Lagoon - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Lagoon

+

Context

+

The Danish Libraries needed a platform for hosting a large number of Drupal +installations. As it was unclear exactly how to build such a platform and how +best to fulfill a number of requirements, a Proof Of Concept project was +initiated to determine whether to use an existing solution or build a platform +from scratch.

+

After an evaluation, Lagoon was chosen.

+

Decision

+

The main factors behind the decision to use Lagoon where:

+
    +
  • Much lower cost of maintenance than a self-built platform.
  • +
  • The platform is continually updated, and the updates are available for free.
  • +
  • A well-established platform with a lot of proven functionality right out of + the box.
  • +
  • The option of professional support by Amazee
  • +
+

When using and integrating with Lagoon we should strive to

+
    +
  • Make as little modifications to Lagoon as possible
  • +
  • Whenever possible, use the defaults, recommendations and best practices + documented on eg. docs.lagoon.sh
  • +
+

We do this to keep true to the initial thought behind choosing Lagoon as a +platform that gives us a lot of functionality for a (comparatively) small +investment.

+

Alternatives considered

+

The main alternative that was evaluated was to build a platform from scratch. +While this may have lead to a more customized solution that more closely matched +any requirements the libraries may have, it also required a very large +investment would require a large ongoing investment to keep the platform +maintained and updated.

+

We could also choose to fork Lagoon, and start making heavy modifications to the +platform to end up with a solution customized for our needs. The downsides of +this approach has already been outlined.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/architecture/adr/adr-002-rightsizing/index.html b/dpl-platform/architecture/adr/adr-002-rightsizing/index.html new file mode 100644 index 00000000..0ec96475 --- /dev/null +++ b/dpl-platform/architecture/adr/adr-002-rightsizing/index.html @@ -0,0 +1,2275 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Rightsizing - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Rightsizing

+

Context

+

Expected traffic

+

The request path and expected traffic +The platform is required to be able to handle an estimated 275.000 page-views +per day spread out over 100 websites. A visit to a website that causes the +browser to request a single html-document followed by a number of assets is only +counted as a single page-view.

+

On a given day about half of the page-views to be made by an authenticated +user. We further more expect the busiest site receive about 12% of the traffic.

+

Given these numbers, we can make some estimates of the expected average load. +To stay fairly conservative we will still assume that about 50% of the traffic +is anonymous and can thus be cached by Varnish, but we will assume that all +all sites gets traffic as if they where the most busy site on the platform (12%).

+

12% of 275.000 requests gives us a an average of 33.000 requests. To keep to the +conservative side, we concentrate the load to a period of 8 hours. We then end +up with roughly 1 page-view pr. second.

+

Expected workload characteristics

+

The platform is hosted on a Kubernetes cluster on which Lagoon is installed. +As of late 2021, Lagoons approach to handling rightsizing of PHP- and in +particular Drupal-applications is based on a number factors:

+
    +
  1. Web workloads are extremely spiky. While a site looks to have to have a + sustained load of 5 rps when looking from afar, it will in fact have anything + from (eg) 0 to 20 simultaneous users on a given second.
  2. +
  3. Resource-requirements are every ephemeral. Even though a request as a peak + memory-usage of 128MB, it only requires that amount of memory for a very + short period of time.
  4. +
  5. Kubernetes nodes has a limit of how many pods will fit on a given node. + This will constraint the scheduler from scheduling too many pods to a node + even if the workloads has declared a very low resource request.
  6. +
  7. With metrics-server enabled, Kubernetes will keep track of the actual + available resources on a given node. So, a node with eg. 4GB ram, hosting + workloads with a requested resource allocation of 1GB, but actually taking + up 3.8GB of ram, will not get scheduled another pod as long as there are + other nodes in the cluster that has more resources available.
  8. +
+

The consequence of the above is that we can pack a lot more workload onto a single +node than what would be expected if you only look at the theoretical maximum +resource requirements.

+

Lagoon resource request defaults

+

Lagoon sets its resource-requests based on a helm values default in the +kubectl-build-deploy-dind image. The default is typically 10Mi pr. container +which can be seen in the nginx-php chart which runs a php and +nginx container. Lagoon configures php-fpm to allow up to 50 children +and allows php to use up to 400Mi memory.

+

Combining these numbers we can see that a site that is scheduled as if it only +uses 20 Megabytes of memory, can in fact take up to 20 Gigabytes. The main thing +that keeps this from happening in practice is a a combination of the above +assumptions. No node will have more than a limited number of pods, and on a +given second, no site will have nearly as many inbound requests as it could have.

+

Decision

+

Lagoon is a very large and complex solution. Any modification to Lagoon will +need to be well tested, and maintained going forward. With this in mind, we +should always strive to use Lagoon as it is, unless the alternative is too +costly or problematic.

+

Based on real-live operations feedback from Amazee (creators of Lagoon) and the +context outline above we will

+
    +
  • Leave the Lagoon defaults as they are, meaning most pods will request 10Mi of + memory.
  • +
  • Let the scheduler be informed by runtime metrics instead of up front pod + resource requests.
  • +
  • Rely on the node maximum pods to provide some horizontal spread of pods.
  • +
+

Alternatives considered

+

As Lagoon does not give us any manual control over rightsizing out of the box, +all alternatives involves modifying Lagoon.

+

Altering Lagoon Defaults

+

We've inspected the process Lagoon uses to deploy workloads, and determined that +it would be possible to alter the defaults used without too many modifications.

+

The build-deploy-docker-compose.sh script that renders the manifests that +describes a sites workloads via Helm includes a +service-specific values-file. This file can be used to +modify the defaults for the Helm chart. By creating custom container-image for +the build-process based on the upstream Lagoon build image, we can deliver our +own version of this image.

+

As an example, the following Dockerfile will add a custom values file for the +redis service.

+
FROM docker.io/uselagoon/kubectl-build-deploy-dind:latest
+COPY redis-values.yaml /kubectl-build-deploy/
+
+

Given the following redis-values.yaml

+
resources:
+  requests:
+    cpu: 10m
+    memory: 100Mi
+
+

The Redis deployment would request 100Mi instead of the previous default of 10Mi.

+

Introduce "t-shirt" sizes

+

Building upon the modification described in the previous chapter, we could go +even further and modify the build-script itself. By inspecting project variables +we could have the build-script pass in eg. a configurable value for +replicaCount for a pod. This would allow us to introduce a +small/medium/large concept for sites. This could be taken even further to eg. +introduce whole new services into Lagoon.

+

Consequences

+

This could lead to problems for sites that requires a lot of resources, but +given the expected average load, we do not expect this to be a problem even if +a site receives an order of magnitude more traffic than the average.

+

The approach to rightsizing may also be a bad fit if we see a high concentration +of "non-spiky" workloads. We know for instance that Redis and in particular +Varnish is likely to use a close to constant amount of memory. Should a lot of +Redis and Varnish pods end up on the same node, evictions are very likely to +occur.

+

The best way to handle these potential situations is to be knowledgeable about +how to operate Kubernetes and Lagoon, and to monitor the workloads as they are +in use.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/architecture/adr/adr-003-system-alerts/index.html b/dpl-platform/architecture/adr/adr-003-system-alerts/index.html new file mode 100644 index 00000000..469e4365 --- /dev/null +++ b/dpl-platform/architecture/adr/adr-003-system-alerts/index.html @@ -0,0 +1,2079 @@ + + + + + + + + + + + + + + + + + + + + + + ADR-003 System alerts - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

ADR-003 System alerts

+

Context

+

There has been a wish for a functionality that alerts administrators if certain +system values have gone beyond defined thresholds rules.

+

Decision

+

We have decided to use alertmanager +that is a part of the Prometheus package that is +already used for monitoring the cluster.

+

Consequences

+
    +
  • We have tried to install alertmanager and testing it. + It works and given the various possibilities of defining + alert rules we consider + the demands to be fulfilled.
  • +
  • We will be able to get alerts regarding thresholds on both container and + cluster level which is what we need.
  • +
  • Alertmanager fits in the general focus of being cloud agnostic. It is + CNCF approved + and does not have any external infrastructure dependencies.
  • +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/architecture/adr/adr-004-declarative-site-management/index.html b/dpl-platform/architecture/adr/adr-004-declarative-site-management/index.html new file mode 100644 index 00000000..6329a00f --- /dev/null +++ b/dpl-platform/architecture/adr/adr-004-declarative-site-management/index.html @@ -0,0 +1,2128 @@ + + + + + + + + + + + + + + + + + + + + + + ADR 004: Declarative Site management - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

ADR 004: Declarative Site management

+

Context

+

Lagoon requires a site to be deployed from at Git repository containing a +.lagoon.yml and docker-compose.yml +A potential logical consequence of this is that we require a Git repository pr +site we want to deploy, and that we require that repository to maintain those +two files.

+

Administering the creation and maintenance of 100+ Git repositories can not be +done manually with risk of inconsistency and errors. The industry best practice +for administering large-scale infrastructure is to follow a declarative +Infrastructure As Code(IoC) +pattern. By keeping the approach declarative it is much easier for automation +to reason about the intended state of the system.

+

Further more, in a standard Lagoon setup, Lagoon is connected to the "live" +application repository that contains the source-code you wish to deploy. In this +approach Lagoon will just deploy whatever the HEAD of a given branch points. In +our case, we perform the build of a sites source release separate from deploying +which means the sites repository needs to be updated with a release-version +whenever we wish it to be updated. This is not a problem for a small number of +sites - you can just update the repository directly - but for a large set of +sites that you may wish to administer in bulk - keeping track of which version +is used where becomes a challenge. This is yet another good case for declarative +configuration: Instead of modifying individual repositories by hand to deploy, +we would much rather just declare that a set of sites should be on a specific +release and then let automation take over.

+

While there are no authoritative discussion of imperative vs declarative IoC, +the following quote from an OVH Tech Blog +summarizes the current consensus in the industry pretty well:

+
+

In summary declarative infrastructure tools like Terraform and CloudFormation +offer a much lower overhead to create powerful infrastructure definitions that +can grow to a massive scale with minimal overheads. The complexities of hierarchy, +timing, and resource updates are handled by the underlying implementation so +you can focus on defining what you want rather than how to do it.

+

The additional power and control offered by imperative style languages can be +a big draw but they also move a lot of the responsibility and effort onto the +developer, be careful when choosing to take this approach.

+
+

Decision

+

We administer the deployment to and the Lagoon configuration of a library site +in a repository pr. library. The repositories are provisioned via Terraform that +reads in a central sites.yaml file. The same file is used as input for the +automated deployment process which renderers the various files contained in the +repository including a reference to which release of DPL-CMS Lagoon should use.

+

It is still possible to create and maintain sites on Lagoon independent of this +approach. We can for instance create a separate project for the dpl-cms +repository to support core development.

+

Status

+

Accepted

+

Alternatives considered

+

We could have run each site as a branch of off a single large repository. +This was rejected as a possibility as it would have made the administration of +access to a given libraries deployed revision hard to control. By using individual +repositories we have the option of grating an outside developer access to a +full repository without affecting any other.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/architecture/adr/index.html b/dpl-platform/architecture/adr/index.html new file mode 100644 index 00000000..17819e3e --- /dev/null +++ b/dpl-platform/architecture/adr/index.html @@ -0,0 +1,2024 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Records - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Records

+

We loosely follow the guidelines for ADRs described by Michael Nygard.

+

A record should attempt to capture the situation that led to the need for a +discrete choice to be made, and then proceed to describe the core of the +decision, its status and the consequences of the decision.

+

To summaries a ADR could contain the following sections (quoted from the above +article):

+
    +
  • +

    Title: These documents have names that are short noun phrases. For example, + "ADR 1: Deployment on Ruby on Rails 3.0.10" or "ADR 9: LDAP for Multitenant Integration"

    +
  • +
  • +

    Context: This section describes the forces at play, including technological + , political, social, and project local. These forces are probably in tension, + and should be called out as such. The language in this section is value-neutral. + It is simply describing facts.

    +
  • +
  • +

    Decision: This section describes our response to these forces. It is stated + in full sentences, with active voice. "We will …"

    +
  • +
  • +

    Status: A decision may be "proposed" if the project stakeholders haven't + agreed with it yet, or "accepted" once it is agreed. If a later ADR changes + or reverses a decision, it may be marked as "deprecated" or "superseded" with + a reference to its replacement.

    +
  • +
  • +

    Consequences: This section describes the resulting context, after applying + the decision. All consequences should be listed here, not just the "positive" + ones. A particular decision may have positive, negative, and neutral consequences, + but all of them affect the team and project in the future.

    +
  • +
+ + + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/architecture/alertmanager-setup/index.html b/dpl-platform/architecture/alertmanager-setup/index.html new file mode 100644 index 00000000..8c8220a6 --- /dev/null +++ b/dpl-platform/architecture/alertmanager-setup/index.html @@ -0,0 +1,2123 @@ + + + + + + + + + + + + + + + + + + + + + + Alertmanager Setup - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Alertmanager Setup

+

The We use the alertmanager automatically +ties to the metrics of Prometheus but in order to make it work the configuration +and rules need to be setup.

+

Configuration

+

The configuration is stored in a secret:

+
kubectl get secret \
+  -n prometheus alertmanager-promstack-kube-prometheus-alertmanager -o yaml
+
+

In order to update the configuration you need to get the secret resource definition +yaml output and retrieve the data.alertmanager.yaml property.

+

You need to base64 decode the value, update configuration with SMTP settings, +receivers and so forth.

+

Rules

+

It is possible to set up various rules(thresholds), both on cluster level and for +separate containers and namespaces.

+

Here is a site with examples of rules to get an idea of the +possibilities.

+

Test

+

We have tested the setup by making a configuration looking like this:

+

Get the configuration form the secret as described above.

+

Change it with smtp settings in order to be able to debug the alerts:

+
global:
+  resolve_timeout: 5m
+  smtp_smarthost: smtp.gmail.com:587
+  smtp_from: xxx@xxx.xx
+  smtp_auth_username: xxx@xxx.xx
+  smtp_auth_password: xxxx
+receivers:
+- name: default
+- name: email-notification
+  email_configs:
+    - to: xxx@xxx.xx
+route:
+  group_by:
+  - namespace
+  group_interval: 5m
+  group_wait: 30s
+  receiver: default
+  repeat_interval: 12h
+  routes:
+  - match:
+      alertname: testing
+    receiver: email-notification
+  - match:
+      severity: critical
+    receiver: email-notification
+
+

Base64 encode the configuration and update the secret with the new configuration +hash.

+

Find the cluster ip of the alertmanager service running (the service name can +possibly vary):

+
kubectl get svc -n prometheus promstack-kube-prometheus-alertmanager
+
+

And then run a curl command in the cluster (you need to find the IP o):

+
# 1.
+kubectl run -i --rm --tty debug --image=curlimages/curl --restart=Never -- sh
+
+# 2
+curl -XPOST http://[ALERTMANAGER_SERVICE_CLUSTER_IP]:9093/api/v1/alerts \
+ -d '[{"status": "firing","labels": {"alertname": "testing","service": "curl",\
+ "severity": "critical","instance": "0"},"annotations": {"summary": \
+ "This is a summary","description": "This is a description."},"generatorURL": \
+ "http://prometheus.int.example.net/<generating_expression>",\
+ "startsAt": "2020-07-22T01:05:38+00:00"}]'
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/architecture/index.html b/dpl-platform/architecture/index.html new file mode 100644 index 00000000..355868d6 --- /dev/null +++ b/dpl-platform/architecture/index.html @@ -0,0 +1,1985 @@ + + + + + + + + + + + + + + + + + + + + + + DPL Platform architecture documentation - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

DPL Platform architecture documentation

+
    +
  • Architecture Decision Records (ADR) describes the reasoning behind key + decisions made during the design and implementation of the platforms + architecture. These documents stands apart from the remaining documentation in + that they keep a historical record, while the rest of the documentation is a + snapshot of the current system.
  • +
  • Platform Environment Architecture + gives an overview of the parts that makes up a single DPL Platform environment.
  • +
  • Performance strategy Describes the approach the + platform takes to meet performance requirements.
  • +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/architecture/performance-strategy/index.html b/dpl-platform/architecture/performance-strategy/index.html new file mode 100644 index 00000000..589277c4 --- /dev/null +++ b/dpl-platform/architecture/performance-strategy/index.html @@ -0,0 +1,2095 @@ + + + + + + + + + + + + + + + + + + + + + + Performance strategy - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Performance strategy

+

The DPL-CMS Drupal sites utilizes a multi-tier caching strategy. HTTP responses +are cached by Varnish and Drupal caches its various internal data-structures +in a Redis key/value store.

+

The request-path

+

The request path and expected traffic

+
    +
  1. All inbound requests are passed in to an Ingress Nginx controller which + forwards the traffic for the individual sites to their individual Varnish + instances.
  2. +
  3. Varnish serves up any static or anonymous responses it has cached from its + object-store.
  4. +
  5. If the request is cache miss the request is passed further on Nginx which + serves any requests for static assets.
  6. +
  7. If the request is for a dynamic page the request is forwarded to the Drupal- + installation hosted by PHP-FPM.
  8. +
  9. Drupal bootstraps, and produces the requested response.
  10. +
  11. During this process it will either populate or reuse it cache which is stored + in Redis.
  12. +
  13. Depending on the request Drupal will execute a number of queries against + MariaDB and a search index.
  14. +
+

Caching of http responses

+

Varnish will cache any http responses that fulfills the following requirements

+
    +
  • Is not associated with a php-session (ie, the user is logged in)
  • +
  • Is a 200
  • +
+

Refer the Lagoon drupal.vcl, +docs.lagoon.sh documentation on the Varnish service +and the varnish-drupal image +for the specifics on the service.

+

Refer to the caching documentation in dpl-cms +for specifics on how DPL-CMS is integrated with Varnish.

+

Redis as caching backend

+

DPL-CMS is configured to use Redis as the backend for its core cache as an +alternative to the default use of the sql-database as backend. This ensures that + a busy site does not overload the shared mariadb-server.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/architecture/platform-environment-architecture/index.html b/dpl-platform/architecture/platform-environment-architecture/index.html new file mode 100644 index 00000000..fef03b3c --- /dev/null +++ b/dpl-platform/architecture/platform-environment-architecture/index.html @@ -0,0 +1,2356 @@ + + + + + + + + + + + + + + + + + + + + + + A DPL Platform environment - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

A DPL Platform environment

+

A DPL Platform environment consists of a range of infrastructure components +on top of which we run a managed Kubernetes instance into with we install a +number of software product. One of these is Lagoon +which gives us a platform for hosting library sites.

+

An environment is created in two separate stages. First all required +infrastructure resources are provisioned, then a semi-automated deployment +process carried out which configures all the various software-components that +makes up an environment. Consult the relevant runbooks and the +DPL Platform Infrastructure documents for the +guides on how to perform the actual installation.

+

This document describes all the parts that makes up a platform environment +raging from the infrastructure to the sites.

+
    +
  • Azure Infrastructure describes the raw cloud infrastructure
  • +
  • Software Components describes the base software products + we install to support the platform including Lagoon
  • +
  • Sites describes how we define the individual sites on a platform and + the approach the platform takes to deployment.
  • +
+

Azure Infrastructure

+

All resources of a Platform environment is contained in a single Azure Resource +Group. The resources are provisioned via a Terraform setup +that keeps its resources in a separate resource group.

+

The overview of current platform environments along with the various urls and +a summary of its primary configurations can be found the +Current Platform environments document.

+

A platform environment uses the following Azure infrastructure resources.

+

An overview of the Azure Infrastructure

+
    +
  • A virtual Network - with a subnet, configured with access to a number of services.
  • +
  • Separate storage accounts for
  • +
  • Monitoring data (logs)
  • +
  • Lagoon files (eg. results of running user-triggered administrative actions)
  • +
  • Backups
  • +
  • Drupal site files
  • +
  • A MariaDB used to host the sites databases.
  • +
  • A Key Vault that holds administrative credentials to resources that Lagoon + needs administrative access to.
  • +
  • An Azure Kubernetes Service cluster that hosts the platform itself.
  • +
  • Two Public IPs: one for ingress one for egress.
  • +
+

The Azure Kubernetes Service in return creates its own resource group that +contains a number of resources that are automatically managed by the AKS service. +AKS also has a managed control-plane component that is mostly invisible to us. +It has a separate managed identity which we need to grant access to any +additional infrastructure-resources outside the "MC" resource-group that we +need AKS to manage.

+

Software Components

+

The Platform consists of a number of software components deployed into the +AKS cluster. The components are generally installed via Helm, +and their configuration controlled via values-files.

+

Essential configurations such as the urls for the site can be found in the wiki

+

The following sections will describe the overall role of the component and how +it integrates with other components. For more details on how the component is +configured, consult the corresponding values-file for the component found in +the individual environments configuration +folder.

+

Depiction of the support workloads in the cluster

+

Lagoon

+

Lagoon is an Open Soured Platform As A Service +created by Amazee. The platform builds on top of a +Kubernetes cluster, and provides features such as automated builds and the +hosting of a large number of sites.

+

Ingress Nginx

+

Kubernetes does not come with an Ingress Controller out of the box. An ingress- +controllers job is to accept traffic approaching the cluster, and route it via +services to pods that has requested ingress traffic.

+

We use the widely used Ingress Nginx +Ingress controller.

+

Cert Manager

+

Cert Manager allows an administrator specify +a request for a TLS certificate, eg. as a part of an Ingress, and have the +request automatically fulfilled.

+

The platform uses a cert-manager configured to handle certificate requests via +Let's Encrypt.

+

Prometheus and Alertmanager

+

Prometheus is a time series database used by the platform +to store and index runtime metrics from both the platform itself and the sites +running on the platform.

+

Prometheus is configured to scrape and ingest the following sources

+ +

Prometheus is installed via an Operator +which amongst other things allows us to configure Prometheus and Alertmanager via + ServiceMonitor and AlertmanagerConfig.

+

Alertmanager handles +the delivery of alerts produced by Prometheus.

+

Grafana

+

Grafana provides the graphical user-interface +to Prometheus and Loki. It is configured with a number of data sources via its +values-file, which connects it to Prometheus and Loki.

+

Loki and Promtail

+

Loki stores and indexes logs produced by the pods + running in AKS. Promtail +streams the logs to Loki, and Loki in turn makes the logs available to the +administrator via Grafana.

+

Sites

+

Each individual library has a Github repository that describes which sites +should exist on the platform for the library. The creation of the repository +and its contents is automated, and controlled by an entry in a sites.yaml- +file shared by all sites on the platform.

+

Consult the following runbooks to see the procedures for:

+ +

sites.yaml

+

sites.yaml is found in infrastructure/environments/<environment>/sites.yaml. +The file contains a single map, where the configuration of the +individual sites are contained under the property sites.<unique site key>, eg.

+

yaml +sites: + # Site objects are indexed by a unique key that must be a valid lagoon, and + # github project name. That is, alphanumeric and dashes. + core-test1: + name: "Core test 1" + description: "Core test site no. 1" + # releaseImageRepository and releaseImageName describes where to pull the + # container image a release from. + releaseImageRepository: ghcr.io/danskernesdigitalebibliotek + releaseImageName: dpl-cms-source + # Sites can optionally specify primary and secondary domains. + primary-domain: core-test.example.com + # Fully configured sites will have a deployment key generated by Lagoon. + deploy_key: "ssh-ed25519 <key here>" + bib-ros: + name: "Roskilde Bibliotek" + description: "Webmaster environment for Roskilde Bibliotek" + primary-domain: "www.roskildebib.dk" + # The secondary domain will redirect to the primary. + secondary-domains: ["roskildebib.dk", "www2.roskildebib.dk"] + # A series of sites that shares the same image source may choose to reuse + # properties via anchors + << : *default-release-image-source

+

Environment Site Git Repositories

+

Each platform-site is controlled via a GitHub repository. The repositories are +provisioned via Terraform. The following depicts the authorization and control- +flow in use: +Provisioning of Github repositories

+

The configuration of each repository is reconciled each time a site is created,

+

Deployment

+

Releases of DPL CMS are deployed to sites via the dpladm +tool. It consults the sites.yaml file for the environment and performs any +needed deployment.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/backup/index.html b/dpl-platform/backup/index.html new file mode 100644 index 00000000..789a02ac --- /dev/null +++ b/dpl-platform/backup/index.html @@ -0,0 +1,2043 @@ + + + + + + + + + + + + + + + + + + + + + + Backup - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Backup

+

Site backup configuration

+

We configure all production backups with a backup schedule that ensure that the +site is backed up at least once a day.

+

Backups executed by the k8up operator follows a backup +schedule and then uses Restic to perform the backup +itself. The backups are stored in a Azure Blob Container, see the Environment infrastructure +for a depiction of its place in the architecture.

+

The backup schedule and retention is configured via the individual sites +.lagoon.yml. The file is re-rendered from a template every time the a site is +deployed. The templates for the different site types can be found as a part +of dpladm.

+

Refer to the lagoon documentation on backups +for more general information.

+

Refer to any runbooks relevant to backups for operational instructions +on eg. retrieving a backup.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/code-guidelines/index.html b/dpl-platform/code-guidelines/index.html new file mode 100644 index 00000000..2d85bf34 --- /dev/null +++ b/dpl-platform/code-guidelines/index.html @@ -0,0 +1,2235 @@ + + + + + + + + + + + + + + + + + + + + + + Code guidelines - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Code guidelines

+

The following guidelines describe best practices for developing code for the DPL +Platform project. The guidelines should help achieve:

+
    +
  • A stable, secure and high quality foundation for building and maintaining + the platform and its infrastructure.
  • +
  • Consistency across multiple developers participating in the project
  • +
+

Contributions to the core DPL Platform project will be reviewed by members of the +Core team. These guidelines should inform contributors about what to expect in +such a review. If a review comment cannot be traced back to one of these +guidelines it indicates that the guidelines should be updated to ensure +transparency.

+

Coding standards

+

The project follows the Drupal Coding Standards +and best practices for all parts of the project: PHP, JavaScript and CSS. This +makes the project recognizable for developers with experience from other Drupal +projects. All developers are expected to make themselves familiar with these +standards.

+

The following lists significant areas where the project either intentionally +expands or deviates from the official standards or areas which developers should +be especially aware of.

+

General

+
    +
  • The default language for all code and comments is English.
  • +
+

Shell scripts

+
    +
  • Shell-scripts must pass a shellcheck validation
  • +
+

Terraform

+
    +
  • Any Terraform HCL must be formatted to match the format required by + terraform fmt
  • +
  • Terraform configuration should be organized into submodules instantiated by + root modules.
  • +
+

Markdown

+ +

Code comments

+

Code comments which describe what an implementation does should only be used +for complex implementations usually consisting of multiple loops, conditional +statements etc.

+

Inline code comments should focus on why an unusual implementation has been +implemented the way it is. This may include references to such things as +business requirements, odd system behavior or browser inconsistencies.

+

Commit messages

+

Commit messages in the version control system help all developers understand the +current state of the code base, how it has evolved and the context of each +change. This is especially important for a project which is expected to have a +long lifetime.

+

Commit messages must follow these guidelines:

+
    +
  1. Each line must not be more than 72 characters long
  2. +
  3. The first line of your commit message (the subject) must contain a short + summary of the change. The subject should be kept around 50 characters long.
  4. +
  5. The subject must be followed by a blank line
  6. +
  7. Subsequent lines (the body) should explain what you have changed and why the + change is necessary. This provides context for other developers who have not + been part of the development process. The larger the change the more + description in the body is expected.
  8. +
  9. If the commit is a result of an issue in a public issue tracker, + platform.dandigbib.dk, then the subject must start with the issue number + followed by a colon (:). If the commit is a result of a private issue tracker + then the issue id must be kept in the commit body.
  10. +
+

When creating a pull request the pull request description should not contain any +information that is not already available in the commit messages.

+

Developers are encouraged to read How to Write a Git Commit Message +by Chris Beams.

+

Tool support

+

The project aims to automate compliance checks as much as possible using static +code analysis tools. This should make it easier for developers to check +contributions before submitting them for review and thus make the review process +easier.

+

The following tools pay a key part here:

+
    +
  1. terraform fmt for standard + Terraform formatting.
  2. +
  3. markdownlint-cli2 for + linting markdown files. The tool is configured via /.markdownlint-cli2.yaml
  4. +
  5. ShellCheck with its default configuration.
  6. +
+

In general all tools must be able to run locally. This allows developers to get +quick feedback on their work.

+

Tools which provide automated fixes are preferred. This reduces the burden of +keeping code compliant for developers.

+

Code which is to be exempt from these standards must be marked accordingly in +the codebase - usually through inline comments (markdownlint, +ShellCheck). +This must also include a human readable reasoning. This ensures that deviations +do not affect future analysis and the Core project should always pass through +static analysis.

+

If there are discrepancies between the automated checks and the standards +defined here then developers are encouraged to point this out so the automated +checks or these standards can be updated accordingly.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/diagrams/build-release-deploy.drawio b/dpl-platform/diagrams/build-release-deploy.drawio new file mode 100644 index 00000000..eb4adb02 --- /dev/null +++ b/dpl-platform/diagrams/build-release-deploy.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/dpl-platform/diagrams/cluster-support-workloads.drawio b/dpl-platform/diagrams/cluster-support-workloads.drawio new file mode 100644 index 00000000..d1c05d25 --- /dev/null +++ b/dpl-platform/diagrams/cluster-support-workloads.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/dpl-platform/diagrams/dpl-platform-azure.drawio b/dpl-platform/diagrams/dpl-platform-azure.drawio new file mode 100644 index 00000000..155a0048 --- /dev/null +++ b/dpl-platform/diagrams/dpl-platform-azure.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/dpl-platform/diagrams/github-environment-repositories.drawio b/dpl-platform/diagrams/github-environment-repositories.drawio new file mode 100644 index 00000000..0510d2aa --- /dev/null +++ b/dpl-platform/diagrams/github-environment-repositories.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/dpl-platform/diagrams/profiles.drawio b/dpl-platform/diagrams/profiles.drawio new file mode 100644 index 00000000..9ed2435c --- /dev/null +++ b/dpl-platform/diagrams/profiles.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/dpl-platform/diagrams/render-png/build-release-deploy.png b/dpl-platform/diagrams/render-png/build-release-deploy.png new file mode 100644 index 00000000..c5088168 Binary files /dev/null and b/dpl-platform/diagrams/render-png/build-release-deploy.png differ diff --git a/dpl-platform/diagrams/render-png/cluster-support-workloads.png b/dpl-platform/diagrams/render-png/cluster-support-workloads.png new file mode 100644 index 00000000..cf0cf313 Binary files /dev/null and b/dpl-platform/diagrams/render-png/cluster-support-workloads.png differ diff --git a/dpl-platform/diagrams/render-png/dpl-platform-azure.png b/dpl-platform/diagrams/render-png/dpl-platform-azure.png new file mode 100644 index 00000000..b76b9e60 Binary files /dev/null and b/dpl-platform/diagrams/render-png/dpl-platform-azure.png differ diff --git a/dpl-platform/diagrams/render-png/github-environment-repositories.png b/dpl-platform/diagrams/render-png/github-environment-repositories.png new file mode 100644 index 00000000..0742d53e Binary files /dev/null and b/dpl-platform/diagrams/render-png/github-environment-repositories.png differ diff --git a/dpl-platform/diagrams/render-png/profiles.png b/dpl-platform/diagrams/render-png/profiles.png new file mode 100644 index 00000000..989eabb6 Binary files /dev/null and b/dpl-platform/diagrams/render-png/profiles.png differ diff --git a/dpl-platform/diagrams/render-png/request-path.png b/dpl-platform/diagrams/render-png/request-path.png new file mode 100644 index 00000000..2c1c84cb Binary files /dev/null and b/dpl-platform/diagrams/render-png/request-path.png differ diff --git a/dpl-platform/diagrams/render-png/terraform_overview.png b/dpl-platform/diagrams/render-png/terraform_overview.png new file mode 100644 index 00000000..203123e4 Binary files /dev/null and b/dpl-platform/diagrams/render-png/terraform_overview.png differ diff --git a/dpl-platform/diagrams/render-svg/build-release-deploy.svg b/dpl-platform/diagrams/render-svg/build-release-deploy.svg new file mode 100644 index 00000000..76f049f1 --- /dev/null +++ b/dpl-platform/diagrams/render-svg/build-release-deploy.svg @@ -0,0 +1,2 @@ + +
Goal:
Create
 core release 1.2.3 of dpl-cms
and deploy it to a site.
Goal:...
dpl_cms
dpl_...
Action: The developer tags the point (sha) in git history that should be released. Eg. uses the tag: "dpl-cms-source_1.2.3"
Action: The developer tag...
Github action produces a source docker image as a package in a Github registry
Github action produces a so...
Step 1 - trigger build of release
Step 1 - trigger build of rel...
dpl_cms
dpl_...
operator
oper...
Action: The operator updates the sites configuration in sites.yaml and synchronizes the site. sites.yaml tells the sync tool
  • Which version that is being released (1.2.3)
  • Which lagoon environment/branch that should be deployed
  • Either:
    • Forked releae image registry/name
    • Default release image registry/nae
Action: The operator updates the sites configuration in site...
Step 3 - deploying site
Step 3 - deploying site
Action: An operator pulls down the deployment tools by cloning the dpl_platform repository locally
Action: An operator pulls...
Step 2 - get deploy tool
Step 2 - get deploy tool
An environment repository is cloned based on the argument given to the deploy command
An environment reposit...
Clone environment repoRender tags into dockerfile
References to the core source and dockerfiles references gets updated inside of the dockerfiles which resides in the lagoon directory in the environment repo
References to the core s...
Example cli.dockerfile:
Example cli.dockerfile:
FROM default-registry/dpl-cms-source:1.2.3 AS release
FROM uselagoon/php-7.4-cli-drupal:<lagoon_version>
COPY release:/app ./app
FROM default-registry/dpl-cms-source:1.2.3 AS release...
git clone <site> -b <environment>
git clone <site> -b <environment>
Push changes to environment
The changes are pushed to the environment repo which triggers the deployment procedure in Lagoon.

The changes are pushed t...
git push origin <environment>
git push origin <environment>
dpl_platform
dpl_...
vi sites.yaml
task site:sync
vi sites....
git clone...
dplsh
git clone....
developer
deve...
operator
oper...
Lagoons spins up a new site replacing the old one.
Lagoons spins u...
Lagoon updates site

Biblo

Lorem ipsum dolor sit amet, consectetur adipisicing elit...

Biblo...
The site has been updated
and the end user can see the result of it
The site has been updated...
end user
end...
default-registry/dpl-cms-source:1.2.3
default-registry/dpl-cms-source:1.2.3
dpl_cms
dpl...
Goal:
Create
 forked release 3.2.1 of
dpl-cms deploy it to a site.
Goal:...
"Step 0" - fork repository
"Step 0" - fork repository
dpl_cms
dpl...
forked_cms
for...
Fork
Fork
Action: Initial setup of the repository by forking dpl_cms and eg. adjusting release action configuration
Action: Initial setup of the repository by fo...
Create
Create
Configure
Configure
developer
deve...
Step 1 - trigger build of release
Step 1 - trigger build of rel...
forked-registry/forked-cms-source:3.2.1
forked-registry/forked-cms-source:3....
forked_cms
forked_cms
developer
de...
Action: 
Same release-
procedure as dpl_cms.

But, the source-image is pushed to a different registry
Action:...
FROM forked-registry/forked-cms-source:3.2.1 AS release
FROM uselagoon/php-7.4-cli-drupal:<lagoon_version>
COPY release:/app ./app
FROM forked-registry/forked-cms-source:3.2.1 AS release...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/dpl-platform/diagrams/render-svg/cluster-support-workloads.svg b/dpl-platform/diagrams/render-svg/cluster-support-workloads.svg new file mode 100644 index 00000000..63500a17 --- /dev/null +++ b/dpl-platform/diagrams/render-svg/cluster-support-workloads.svg @@ -0,0 +1,2 @@ + +
Site
Site
Azure Kubernetes Service (AKS) Cluster
Forwards
To
Forwards...
Ingests logs
via Promtail
Ingests logs...
Azure
Load Balancer
Azure...
Fetches
Fetches
Scrapes
Metrics
Scrapes...
Scrapes
Endpoint
Scrapes...
Queries
Queries
Configures
Configures
Configures
Configures
Ingress
HTTPS  Certificate
HTTPS  Certi...
Browses
Browses
Administrator
Admini...
Inbound traffic
from users
Inbound traffic...
Internet
Internet
Ingests logs
via Promtail
Ingests logs...
Container
Container
Queries
Queries
Alerts via
Alertmanager
Alerts via...
IngressNginxServiceMonitors
Informs of
metrics to
scrape
Informs of...
DPL Platform
Cluster Workloads
Updated 2021-10-07
DPL Platform...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/dpl-platform/diagrams/render-svg/dpl-platform-azure.svg b/dpl-platform/diagrams/render-svg/dpl-platform-azure.svg new file mode 100644 index 00000000..5ddca0d9 --- /dev/null +++ b/dpl-platform/diagrams/render-svg/dpl-platform-azure.svg @@ -0,0 +1,2 @@ + +
Resource Group
rg-tfstate-alpha
Resourc...
Resource Group
rg-env-<environment>
Resourc...
Storage Accountst<setup-name><increment>Blob"state"Key Vaultkv-<setup-name><increment>
Grants access
Grants access
Storage Account Key
Stor...
Terraform setup
Terraform setup
Virtual Networkvnet-aksPublic IPpip-aks-egressPublic IPpip-aks-ingress
DNS Record
*.<environment>.dpl.reload.dk
DNS Rec...
Creates
Creates
Accesses
Accesses
Kubernetes Clusteraks-<environment>-01Key Vaultkv-<env>-03-kvStorage Account: site filesst<environment>
DPL Platform
Azure Infrastructure
Updated 2021-11-15
DPL Platform...
Subnetsubnet-aks
Filters
Access
Filters...
ServiceEndpoints- Microsoft.SQL- Microsoft.ContainerRegistry- Microsoft.Storage
MC-rg-env-<environment>_aks...
MC-rg-e...
Uses
Uses
Forwards
traffic
Forwards...
Load balancer
kubernetes
Load ba...
RBAC
NetworkContributor
RBAC...
Managed Identityaks-<environment>-01-agentpool
Runs as
Runs as
Virtual Machine ScaleSet
(pr kubernetes node pool)
Virtual...
Network Security Group
Networ...
Access
Access
Internet
Internet
 
 
Managed by Azure
Not visible to us
Managed by Azure...
RBAC
Virtual Machine Contributor + 
Storage Account Contributor
RBAC...
Managed Identity"control-plane"aks-<environment>-01
Kubernetes Control Plane
Kubernet...
Service Endpoint Policykkwebhostingfile<env>01st-service-endpoint-policy
Inserts Storage + SQL
Credentials
Inserts Storage + SQL...
Provisions
Provisions
Inserts secrets
Inserts secrets
Deployment Script
Deployment...
Platform Environment
Platform Environment
Machine Created AKS Resources
Machine Created AKS Resources
Storage Account: monitoringst<environment>monStorage Account: Lagoon filesst<environment>lagfilStorage Account: Backupst<environment>backupMariaDBdb-<environment>Blob containerloggingFiles sharekubernetes-dyn*Blob containerlagoon-filesBlob containerloggingBlob containerharborStorage Account: Backupst<environment>harbor
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/dpl-platform/diagrams/render-svg/github-environment-repositories.svg b/dpl-platform/diagrams/render-svg/github-environment-repositories.svg new file mode 100644 index 00000000..cb56b9cf --- /dev/null +++ b/dpl-platform/diagrams/render-svg/github-environment-repositories.svg @@ -0,0 +1,2 @@ + +
DPL Shell
DPL Shell
GitHub organisation
Launch
Launch
Administrator
(human)
Admini...
SSH Key + PAT
Passed to Terraform
SSH Key + PAT...
Acts as
Acts as
Key Vault
Access Token
Acc...
Create/Edit/Delete
Create/Edit/Delete
Administrative
GitHub Account
Administ...
SSH Key
SSH...
Environment Repositories
Environment Repositories
Organization settings
Organiza...
Teams / Permissions
Teams / Pe...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/dpl-platform/diagrams/render-svg/profiles.svg b/dpl-platform/diagrams/render-svg/profiles.svg new file mode 100644 index 00000000..6f25f0d7 --- /dev/null +++ b/dpl-platform/diagrams/render-svg/profiles.svg @@ -0,0 +1,2 @@ + +
Programmer Repo
Programmer Repo
Fork
Fork
Redaktør
Redakt...
Webmaster
Webmast...
Programmer
Programmer
operator
oper...
vi sites.yaml
task site:sync
vi sites....
DPL CMS Core
Repo
DPL CMS Core...
Develop
Develop
Release
Release
Deploy
Deploy
Run
Run
Permissions
+ use update module
+ enable module
Permissions...
Modules
+ update (core)
Modules...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/dpl-platform/diagrams/render-svg/request-path.svg b/dpl-platform/diagrams/render-svg/request-path.svg new file mode 100644 index 00000000..f5f52239 --- /dev/null +++ b/dpl-platform/diagrams/render-svg/request-path.svg @@ -0,0 +1,2 @@ + +
site
site
12%
33k rpd
~1 rps
12%...
Ingress
Nginx
Ingress...
100%:
275k rpd
9.5 rps
100%:...
Internet
Internet
50%
17k rpd
~ 0.5 rps
50%...
Varnish
Varnish
100%
17k rpd
~ 0.5 rps
100%...
Nginx
Nginx
of all searches: 12%
6500 rpd
0,2 rps
of all searches: 12%...
PHP-FPM
PHP-FPM
(replicas)
(replicas)
Redis
Redis
Search
Search
(replicas)
(replicas)
MariaDB
MariaDB
Each link is annotated with
- The percentage of traffic expected from previous link
- Expected requests pr. day
- Requests pr second assuming a 8 hour day
Each link is annotated with...
Estemated Capacities
Estemated Capacities
Ingress
Nginx
Ingress...
Varnish
Varnish
Nginx
Nginx
PHP-FPM
PHP-FPM
Redis
Redis
(replicas)
(replicas)
No relevant limit
No relevant lim...
500 MB (Lagoon default)
500 MB (Lagoon default)
No relevant limit
No relevant lim...
100 MB (Lagoon default)
100 MB (Lagoon default)
~5rps depending  available memory on node (php memory_limit= 400MB)
~5rps depending  available...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/dpl-platform/diagrams/render-svg/terraform_overview.svg b/dpl-platform/diagrams/render-svg/terraform_overview.svg new file mode 100644 index 00000000..47dbccd2 --- /dev/null +++ b/dpl-platform/diagrams/render-svg/terraform_overview.svg @@ -0,0 +1,54 @@ +SubscriptionResource Group: TerraformStorage accountKey VaultResource Group: MainTerraform stateStorage Account KeyResources...Continuous IntegrationOperatorTerraform1: Unlocks storage account key2: Reads state3: Provisions resources and reconciles stateUsesUses \ No newline at end of file diff --git a/dpl-platform/diagrams/request-path.drawio b/dpl-platform/diagrams/request-path.drawio new file mode 100644 index 00000000..543ed200 --- /dev/null +++ b/dpl-platform/diagrams/request-path.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/dpl-platform/diagrams/terraform-setup.puml b/dpl-platform/diagrams/terraform-setup.puml new file mode 100644 index 00000000..6163bffd --- /dev/null +++ b/dpl-platform/diagrams/terraform-setup.puml @@ -0,0 +1,28 @@ +@startuml terraform_overview +actor "Continuous Integration" as ci +actor "Operator" as ops +agent "Terraform" as tf + +node "Subscription" as sub { + package "Resource Group: Terraform" as rgterra { + package "Storage account" { + storage "Terraform state" as tfstate + } + + package "Key Vault" { + storage "Storage Account Key" as storagekey + } + } + package "Resource Group: Main" as rgmain { + card "Resources..." + } +} + +tf <--> storagekey: 1: Unlocks storage account key +tf <--> tfstate: 2: Reads state +tf --> rgmain: 3: Provisions resources and reconciles state + +ops -> tf: Uses +ci -> tf: Uses + +@enduml diff --git a/dpl-platform/images/loki-grafana-download-logs.png b/dpl-platform/images/loki-grafana-download-logs.png new file mode 100644 index 00000000..6cb47f37 Binary files /dev/null and b/dpl-platform/images/loki-grafana-download-logs.png differ diff --git a/dpl-platform/index.html b/dpl-platform/index.html new file mode 100644 index 00000000..d32fcc75 --- /dev/null +++ b/dpl-platform/index.html @@ -0,0 +1,1998 @@ + + + + + + + + + + + + + + + + + + + + + + DPL Platform Documentation - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

DPL Platform Documentation

+

This directory contains the documentation of the DPL Platforms architecture and +overall concepts.

+

Documentation of how to use the various sub-components of the project can be +found in READMEs in the respective components directory.

+

Table of contents

+ + + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/infrastructure/index.html b/dpl-platform/infrastructure/index.html new file mode 100644 index 00000000..4d1c3d3c --- /dev/null +++ b/dpl-platform/infrastructure/index.html @@ -0,0 +1,2241 @@ + + + + + + + + + + + + + + + + + + + + + + DPL Platform Infrastructure - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

DPL Platform Infrastructure

+

This directory contains the Infrastructure as Code and scripts that are used +for maintaining the infrastructure-component that each platform environment +consists of. A "platform environment" is an umbrella term for the Azure +infrastructure, the Kubernetes cluster, the Lagoon installation and the set of +GitHub environments that makes up a single DPL Platform installation.

+

Directory layout

+
    +
  • dpladm/: a tool used for deploying individual sites. The tools can + be run manually, but the recommended way is via the common infrastructure Taskfile.
  • +
  • environments/: contains a directory for each platform environment.
  • +
  • terraform: terraform setup and tooling that is shared between + environments.
  • +
  • task/: Configuration and scripts used by our Taskfile-based automation + The scripts included in this directory can be run by hand in an emergency + but te recommended way to invoke these via task.
  • +
  • Taskfile.yml: the common infrastructure task + configuration. Invoke task to get a list of targets. Must be run from within + an instance of DPL shell unless otherwise + noted.
  • +
+

Platform Environment configurations

+

The environments directory contains a subdirectory for each platform +environment. You generally interact with the files and directories within the +directory to configure the environment. When a modification has been made, it +is put in to effect by running the appropiate task :

+
    +
  • configuration: contains the various configurations the + applications that are installed on top of the infrastructure requires. These + are used by the support:provision:* tasks.
  • +
  • env_repos contains the Terraform root-module for provisioning GitHub site- + environment repositories. The module is run via the env_repos:provision task.
  • +
  • infrastructure: contains the Terraform root-module used to provision the basic + Azure infrastructure components that the platform requires.The module is run + via the infra:provision task.
  • +
  • lagoon: contains Kubernetes manifests and Helm values-files used for installing + the Lagoon Core and Remote that is at the heart of a DPL Platform installation. + THe module is run via the lagoon:provision:* tasks.
  • +
+

Basic usage of dplsh and an environment configuration

+

The remaining guides in this document assumes that you work from an instance +of the DPL shell. See the +DPLSH Runbook for a basic introduction +to how to use dplsh.

+

Installing a platform environment from scratch

+

The following describes how to set up a whole new platform environment to host + platform sites.

+

The easiest way to set up a new environment is to create a new environments/<name> +directory and copy the contents of an existing environment replacing any +references to the previous environment with a new value corresponding to the new +environment. Take note of the various URLs, and make sure to update the +Current Platform environments +documentation.

+

If this is the very first environment, remember to first initialize the Terraform- +setup, see the terraform README.md.

+

Provisioning infrastructure

+

When you have prepared the environment directory, launch dplsh and go through +the following steps to provision the infrastructure:

+
# We export the variable to simplify the example, you can also specify it inline.
+export DPLPLAT_ENV=dplplat01
+
+# Provision the Azure resources
+task infra:provision
+
+# Create DNS record
+Create an A record in the administration area of your DNS provider.
+Take the terraform output: "ingress_ip" of the former command and create an entry
+like: "*.[DOMAN_NAME].[TLD]": "[ingress_ip]"
+
+# Provision the support software that the Platform relies on
+task support:provision
+
+

Installing and configuring Lagoon

+

The previous step has established the raw infrastructure and the Kubernetes support +projects that Lagoon needs to function. You can proceed to follow the official +Lagoon installation procedure.

+

The execution of the individual steps of the guide has been somewhat automated, +the following describes how to use the automation, make sure to follow along +in the official documentation to understand the steps and some of the +additional actions you have to take.

+
# The following must be carried out from within dplsh, launched as described
+# in the previous step including the definition of DPLPLAT_ENV.
+
+# 1. Provision a lagoon core into the cluster.
+task lagoon:provision:core
+
+# 2. Skip the steps in the documentation that speaks about setting up email, as
+# we currently do not support sending emails.
+
+# 3. Setup ssh-keys for the lagoonadmin user
+# Access the Lagoon UI (consult the platform-environments.md for the url) and
+# log in with lagoonadmin + the admin password that can be extracted from a
+# Kubernetes secret:
+kubectl \
+  -o jsonpath="{.data.KEYCLOAK_LAGOON_ADMIN_PASSWORD}" \
+  -n lagoon-core \
+  get secret lagoon-core-keycloak \
+| base64 --decode
+
+# Then go to settings and add the ssh-keys that should be able to access the
+# lagoon admin user. Consider keeping this list short, and instead add
+# additional users with fewer privileges laster.
+
+# 4. If your ssh-key is passphrase-projected we'll need to setup an ssh-agent
+# instance:
+$ eval $(ssh-agent); ssh-add
+
+# 5. Configure the CLI to verify that access (the cli itself has already been
+#    installed in dplsh)
+task lagoon:cli:config
+
+# You can now add additional users, this step is currently skipped.
+
+# (6. Install Harbor.)
+# This step has already been performed as a part of the installation of
+# support software.
+
+# 7. Install a Lagoon Remote into the cluster
+task lagoon:provision:remote
+
+# 8. Register the cluster administered by the Remote with Lagoon Core
+# Notice that you must provide a bearer token via the USER_TOKEN environment-
+# variable. The token can be found in $HOME/.lagoon.yml after a successful
+# "lagoon login"
+USER_TOKEN=<token> task lagoon:add:cluster:
+
+

The Lagoon core has now been installed, and the remote registered with it.

+

Setting up a GitHub organization and repositories for a new platform environment

+

Prerequisites:

+
    +
  • An properly authenticated azure CLI (az). See the section on initial + Terraform setup for more details on the requirements
  • +
+

First create a new administrative github user and create a new organization +with the user. The administrative user should only be used for administering +the organization via terraform and its credentials kept as safe as possible! The +accounts password can be used as a last resort for gaining access to the account +and will not be stored in Key Vault. Thus, make sure to store the password +somewhere safe, eg. in a password-manager or as a physical printout.

+

This requires the infrastructure to have been created as we're going to store +credentials into the azure Key Vault.

+
# cd into the infrastructure folder and launch a shell
+(host)$ cd infrastructure
+(host)$ dplsh
+
+# Remaining commands are run from within dplsh
+
+# export the platform environment name.
+# export DPLPLAT_ENV=<name>, eg
+$ export DPLPLAT_ENV=dplplat01
+
+# 1. Create a ssh keypair for the user, eg by running
+# ssh-keygen -t ed25519 -C "<comment>" -f dplplatinfra01_id_ed25519
+# eg.
+$ ssh-keygen -t ed25519 -C "dplplatinfra@0120211014073225" -f dplplatinfra01_id_ed25519
+
+# 2. Then access github and add the public-part of the key to the account
+# 3. Add the key to keyvault under the key name "github-infra-admin-ssh-key"
+# eg.
+$ SECRET_KEY=github-infra-admin-ssh-key SECRET_VALUE=$(cat dplplatinfra01_id_ed25519)\
+  task infra:keyvault:secret:set
+
+# 4. Access GitHub again, and generate a Personal Access Token for the account.
+#    The token should
+#     - be named after the platform environment (eg. dplplat01-terraform-timestamp)
+#     - Have a fairly long expiration - do remember to renew it
+#     - Have the following permissions: admin:org, delete_repo, repo
+# 5. Add the access token to Key Vault under the name "github-infra-admin-pat"
+# eg.
+$ SECRET_KEY=github-infra-admin-pat SECRET_VALUE=githubtokengoeshere task infra:keyvault:secret:set
+
+# Our tooling can now administer the GitHub organization
+
+

Renewing the administrative GitHub Personal Access Token

+

The Personal Access Token we use for impersonating the administrative GitHub +user needs to be recreated periodically:

+
# cd into the infrastructure folder and launch a shell
+(host)$ cd infrastructure
+(host)$ dplsh
+
+# Remaining commands are run from within dplsh
+
+# export the platform environment name.
+# export DPLPLAT_ENV=<name>, eg
+$ export DPLPLAT_ENV=dplplat01
+
+# 1. Access GitHub, and generate a Personal Access Token for the account.
+#    The token should
+#     - be named after the platform environment (eg. dplplat01-terraform)
+#     - Have a fairly long expiration - do remember to renew it
+#     - Have the following permissions: admin:org, delete_repo, repo
+# 2. Add the access token to Key Vault under the name "github-infra-admin-pat"
+# eg.
+$ SECRET_KEY=github-infra-admin-pat SECRET_VALUE=githubtokengoeshere \
+  task infra:keyvault:secret:set
+
+# 3. Delete the previous token
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/infrastructure/terraform/index.html b/dpl-platform/infrastructure/terraform/index.html new file mode 100644 index 00000000..b0d37742 --- /dev/null +++ b/dpl-platform/infrastructure/terraform/index.html @@ -0,0 +1,2143 @@ + + + + + + + + + + + + + + + + + + + + + + Terraform - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Terraform

+

This directory contains the configuration and tooling we use to support our +use of terraform.

+

The Terraform setup

+

The setup keeps a single terraform-state pr. environment. Each state is kept as +separate blobs in a Azure Storage Account.

+

Overview of the Terraform setup

+

Access to the storage account is granted via a Storage Account Key which is +kept in a Azure Key Vault in the same resource-group. The key vault, storage account +and the resource-group that contains these resources are the only resources +that are not provisioned via Terraform.

+

Initial setup of Terraform

+

The following procedure must be carried out before the first environment can be +created.

+

Prerequisites:

+
    +
  • A Azure subscription
  • +
  • An authenticated azure CLI that is allowed to use create resources and grant + access to these resources under the subscription including Key Vaults. + The easiest way to achieve this is to grant the user the Owner and + Key Vault Administrator roles to on subscription.
  • +
+

Use the scripts/bootstrap-tf.sh for bootstrapping. After the script has been +run successfully it outputs instructions for how to set up a terraform module +that uses the newly created storage-account for state-tracking.

+

As a final step you must grant any administrative users that are to use the setup +permission to read from the created key vault.

+

Dnsimple

+

The setup uses an integration with DNSimple to set a domain name when the +environments ingress ip has been provisioned. To use this integration first +obtain a api-key for +the DNSimple account. Then use scripts/add-dnsimple-apikey.sh to write it to +the setups Key Vault and finally add the following section to .dplsh.profile ( +get the subscription id and key vault name from existing export for ARM_ACCESS_KEY).

+
export DNSIMPLE_TOKEN=$(az keyvault secret show --subscription "<subscriptionid>"\
+ --name dnsimple-api-key --vault-name <key vault-name> --query value -o tsv)
+export DNSIMPLE_ACCOUNT="<dnsimple-account-id>"
+
+

Terraform Setups

+

A setup is used to manage a set of environments. We currently have a single that +manages all environments.

+

Alpha

+
    +
  • Name: alpha
  • +
  • Resource-group: rg-tfstate-alpha
  • +
  • Key Vault name: kv-dpltfstatealpha001
  • +
  • Storage account: stdpltfstatealpha001
  • +
+

Terraform Modules

+

Root module

+

The platform environments share a number of general modules, which are then +used via a number of root-modules set up for each environment.

+

Consult the general environment documentation +for descriptions on which resources you can expect to find in an environment and +how they are used.

+

Consult the environment overview for an overview of +environments.

+

DPL Platform Infrastructure Module

+

The dpl-platform-environment Terraform module +provisions all resources that are required for a single DPL Platform Environment.

+

Inspect variables.tf for a description +of the required module-variables.

+

Inspect outputs.tf for a list of outputs.

+

Inspect the individual module files for documentation of the resources.

+

The following diagram depicts (amongst other things) the provisioned resources. +Consult the platform environment documentation +for more details on the role the various resources plays. +The Azure infrastructure

+

DPL Platform Site Environment Module

+

The dpl-platform-env-repos Terraform module provisions +the GitHub Git repositories that the platform uses to integrate with Lagoon. Each +site hosted on the platform has a registry.

+

Inspect variables.tf for a description +of the required module-variables.

+

Inspect outputs.tf for a list of outputs.

+

Inspect the individual module files for documentation of the resources.

+

The following diagram depicts how the module gets its credentials for accessing +GitHub and what it provisions. +Provisioning Github infrastructure

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/platform-environments/index.html b/dpl-platform/platform-environments/index.html new file mode 100644 index 00000000..fc849c1a --- /dev/null +++ b/dpl-platform/platform-environments/index.html @@ -0,0 +1,2118 @@ + + + + + + + + + + + + + + + + + + + + + + Current Platform environments - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Current Platform environments

+

dplplat01

+

Roots

+ +

URLs

+ +

Lagoon CLI configuration

+ +

Obtaining Lagoon CLI configuration

+

See Connecting the Lagoon CLI

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/access-kubernetes/index.html b/dpl-platform/runbooks/access-kubernetes/index.html new file mode 100644 index 00000000..359136fb --- /dev/null +++ b/dpl-platform/runbooks/access-kubernetes/index.html @@ -0,0 +1,2077 @@ + + + + + + + + + + + + + + + + + + + + + + Access Kubernetes - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Access Kubernetes

+

When to use

+

When you need to gain kubectl access to a platform-environments Kubernetes cluster.

+

Prerequisites

+
    +
  • An authenticated az cli (from the host). This likely means az login + --tenant TENANT_ID, where the tenant id is that of "DPL Platform". See Azure + Portal > Tenant Properties. The logged in user must have permissions to list + cluster credentials.
  • +
  • docker cli which is authenticated against the GitHub Container Registry. + The access token used must have the read:packages scope.
  • +
+

Procedure

+
    +
  1. cd to dpl-platform/infrastructure
  2. +
  3. Launch the dpl shell: dplsh
  4. +
  5. Set the platform envionment, eg. for "dplplat01": export DPLPLAT_ENV=dplplat01
  6. +
  7. Authenticate: task cluster:auth
  8. +
+

Your dplsh session should now be authenticated against the cluster.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/add-generic-site-to-platform/index.html b/dpl-platform/runbooks/add-generic-site-to-platform/index.html new file mode 100644 index 00000000..3325a466 --- /dev/null +++ b/dpl-platform/runbooks/add-generic-site-to-platform/index.html @@ -0,0 +1,2145 @@ + + + + + + + + + + + + + + + + + + + + + + Add a generic site to the platform - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Add a generic site to the platform

+

When to use

+

When you want to add a "generic" site to the platform. By Generic we mean a site +stored in a repository that that is Prepared for Lagoon +and contains a .lagoon.yml at its root.

+

The current main example of such as site is dpl-cms +which is used to develop the shared DPL install profile.

+

Prerequisites

+
    +
  • An authenticated az cli. The logged in user must have full administrative + permissions to the platforms azure infrastructure.
  • +
  • A running dplsh with DPLPLAT_ENV set to the platform + environment name.
  • +
  • A Lagoon account on the Lagoon core with your ssh-key associated
  • +
  • The git-url for the sites environment repository
  • +
  • A personal access-token that is allowed to pull images from the image-registry + that hosts our images.
  • +
  • The platform environment name (Consult the platform environment documentation)
  • +
+

Procedure

+

The following describes a semi-automated version of "Add a Project" in +the official documentation.

+
# From within dplsh:
+
+# Set an environment,
+# export DPLPLAT_ENV=<platform environment name>
+# eg.
+$ export DPLPLAT_ENV=dplplat01
+
+# If your ssh-key is passphrase-projected we'll need to setup an ssh-agent
+# instance:
+$ eval $(ssh-agent); ssh-add
+
+# 1. Authenticate against the cluster and lagoon
+$ task cluster:auth
+$ task lagoon:cli:config
+
+# 2. Add a project
+# PROJECT_NAME=<project name>  GIT_URL=<url> task lagoon:project:add
+$ PROJECT_NAME=dpl-cms GIT_URL=git@github.com:danskernesdigitalebibliotek/dpl-cms.git\
+  task lagoon:project:add
+
+# 2.b You can also run lagoon add project manually, consult the documentation linked
+#     in the beginning of this section for details.
+
+# 3. Deployment key
+# The project is added, and a deployment key is printed. Copy it and configure
+# the GitHub repository. See the official documentation for examples.
+
+# 4. Webhook
+# Configure Github to post events to Lagoons webhook url.
+# The webhook url for the environment will be
+#  https://webhookhandler.lagoon.<environment>.dpl.reload.dk
+# eg for the environment dplplat01
+#  https://webhookhandler.lagoon.dplplat01.dpl.reload.dk
+#
+# Referer to the official documentation linked above for an example on how to
+# set up webhooks in github.
+
+# 5. Configure image registry credentials Lagoon should use for the project
+#    IF your project references private images in repositories that requires
+#    authentication
+# Refresh your Lagoon token.
+$ lagoon login
+
+# Then export a github personal access-token with pull access.
+# We could pass this to task directly like the rest of the variables but we
+# opt for the export to keep the execution of task a bit shorter.
+$ export VARIABLE_VALUE=<github pat>
+
+# Then get the project id by listing your projects
+$ lagoon list projects
+
+# Finally, add the credentials
+$ VARIABLE_TYPE_ID=<project id> \
+  VARIABLE_TYPE=PROJECT \
+  VARIABLE_SCOPE=CONTAINER_REGISTRY \
+  VARIABLE_NAME=GITHUB_REGISTRY_CREDENTIALS \
+  task lagoon:set:environment-variable
+
+# If you get a "Invalid Auth Token" your token has probably expired, generated a
+# new with "lagoon login" and try again.
+
+# 5. Trigger a deployment manually, this will fail as the repository is empty
+#    but will serve to prepare Lagoon for future deployments.
+# lagoon deploy branch -p <project-name> -b <branch>
+$ lagoon deploy branch -p dpl-cms -b main
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/add-library-site-to-platform/index.html b/dpl-platform/runbooks/add-library-site-to-platform/index.html new file mode 100644 index 00000000..8763c351 --- /dev/null +++ b/dpl-platform/runbooks/add-library-site-to-platform/index.html @@ -0,0 +1,2246 @@ + + + + + + + + + + + + + + + + + + + + + + Add a new library site to the platform - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Add a new library site to the platform

+

When to use

+

When you want to add a new core-test, editor, webmaster or programmer dpl-cms +site to the platform.

+

Prerequisites

+
    +
  • An authenticated az cli. The logged in user must have full administrative + permissions to the platforms azure infrastructure.
  • +
  • A running dplsh with DPLPLAT_ENV set to the platform + environment name.
  • +
+

Procedure

+

The following sections describes how to

+
    +
  • Add the site to sites.yaml
  • +
  • Provision Github "environment" repository
  • +
  • Create a Lagoon project and connect it to the repository
  • +
+

After these steps has been completed, you can continue to deploying to the +site. See the deploy-a-release.md for details.

+

Step 1, update sites.yaml

+

Create an entry for the site in sites.yaml.

+

For now specify an unique site key (its key in the map of sites), name and +description. Leave out the deployment-key, you will add it in a later step.

+

Sample entry (beware that this example be out of sync with the environment you +are operating, so make sure to compare it with existing entries from the +environment)

+
sites:
+  bib-rb:
+    name: "Roskilde Bibliotek"
+    description: "Roskilde Bibliotek"
+    primary-domain: "www.roskildebib.dk"
+    secondary-domains: ["roskildebib.dk"]
+    dpl-cms-release: "1.2.3"
+    << : *default-release-image-source
+
+

The last entry merges in a default set of properties for the source of release- +images. If the site is on the "programmer" plan, specify a custom set of +properties like so:

+
sites:
+  bib-rb:
+    name: "Roskilde Bibliotek"
+    description: "Roskilde Bibliotek"
+    primary-domain: "www.roskildebib.dk"
+    secondary-domains: ["roskildebib.dk"]
+    dpl-cms-release: "1.2.3"
+    # Github package registry used as an example here, but any registry will
+    # work.
+    releaseImageRepository: ghcr.io/some-github-org
+    releaseImageName: some-image-name
+
+

Be aware that the referenced images needs to be publicly available as Lagoon +currently only authenticates against ghcr.io.

+

Then continue to provision the a Github repository for the site.

+

Step 2: Provision a Github repository

+

Run task env_repos:provision to create the repository.

+

Create a Lagoon project and connect the GitHub repository

+

Prerequisites:

+
    +
  • A Lagoon account on the Lagoon core with your ssh-key associated
  • +
  • The git-url for the sites environment repository
  • +
  • A personal access-token that is allowed to pull images from the image-registry + that hosts our images.
  • +
  • The platform environment name (Consult the platform environment documentation)
  • +
+

The following describes a semi-automated version of "Add a Project" in +the official documentation.

+
# From within dplsh:
+
+# Set an environment,
+# export DPLPLAT_ENV=<platform environment name>
+# eg.
+$ export DPLPLAT_ENV=dplplat01
+
+# If your ssh-key is passphrase-projected we'll need to setup an ssh-agent
+# instance:
+$ eval $(ssh-agent); ssh-add
+
+# 1. Authenticate against the cluster and lagoon
+$ task cluster:auth
+$ task lagoon:cli:config
+
+# 2. Add a project
+# PROJECT_NAME=<project name>  GIT_URL=<url> task lagoon:project:add
+$ PROJECT_NAME=core-test1 GIT_URL=git@github.com:danishpubliclibraries/env-core-test1.git\
+  task lagoon:project:add
+
+# The project is added, and a deployment key is printed, use it for the next step.
+
+# 3. Add the deployment key to sites.yaml under the key "deploy_key".
+$ vi environments/${DPLPLAT_ENV}/sites.yaml
+# Then update the repositories using Terraform
+$ task env_repos:provision
+
+# 4. Configure image registry credentials Lagoon should use for the project:
+# Refresh your Lagoon token.
+$ lagoon login
+
+# Then export a github personal access-token with pull access.
+# We could pass this to task directly like the rest of the variables but we
+# opt for the export to keep the execution of task a bit shorter.
+$ export VARIABLE_VALUE=<github pat>
+
+# Then get the project id by listing your projects
+$ lagoon list projects
+
+# Finally, add the credentials
+$ VARIABLE_TYPE_ID=<project id> \
+  VARIABLE_TYPE=PROJECT \
+  VARIABLE_SCOPE=CONTAINER_REGISTRY \
+  VARIABLE_NAME=GITHUB_REGISTRY_CREDENTIALS \
+  task lagoon:set:environment-variable
+
+# If you get a "Invalid Auth Token" your token has probably expired, generated a
+# new with "lagoon login" and try again.
+
+# 5. Trigger a deployment manually, this will fail as the repository is empty
+#    but will serve to prepare Lagoon for future deployments.
+# lagoon deploy branch -p <project-name> -b <branch>
+$ lagoon deploy branch -p core-test1 -b main
+
+

If you want to deploy a release to the site, continue to +Deploying a release.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/connecting-the-lagoon-cli/index.html b/dpl-platform/runbooks/connecting-the-lagoon-cli/index.html new file mode 100644 index 00000000..c5b4e8cc --- /dev/null +++ b/dpl-platform/runbooks/connecting-the-lagoon-cli/index.html @@ -0,0 +1,2178 @@ + + + + + + + + + + + + + + + + + + + + + + Connecting the Lagoon CLI - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Connecting the Lagoon CLI

+

When to use

+

When you want to use the Lagoon API vha. the CLI. You can connect from the DPL +Shell, or from a local installation of the CLI.

+

Using the DPL Shell requires administrative privileges to the infrastructure while +a local cli may connect using only the ssh-key associated to a Lagoon user.

+

This runbook documents both cases, as well as how an administrator can extract the +basic connection details a standard user needs to connect to the Lagoon installation.

+

Prerequisites

+
    +
  • Your ssh-key associated with a lagoon user. This has to be done via the Lagoon + UI by either you for your personal account, or by an administrator who has + access to edit your Lagoon account.
  • +
  • For local installations of the cli:
  • +
  • The Lagoon CLI installed locally
  • +
  • Connectivity details for the Lagoon environment
  • +
  • For administrative access to extract connection details or use the lagoon cli + from within the dpl shell:
  • +
  • A valid dplsh setup to extract the connectivity details
  • +
+

Procedure

+

Obtain the connection details for the environment

+

You can skip this step and go to Configure your local lagoon cli) +if your environment is already in Current Platform environments +and you just want to have a local lagoon cli working.

+

If it is missing, go through the steps below and update the document if you have +access, or ask someone who has.

+
# Launch dplsh.
+$ cd infrastructure
+$ dplsh
+
+# 1. Set an environment,
+# export DPLPLAT_ENV=<platform environment name>
+# eg.
+$ export DPLPLAT_ENV=dplplat01
+
+# 2. Authenticate against AKS, needed by infrastructure and Lagoon tasks
+$ task cluster:auth
+
+# 3. Generate the Lagoon CLI configuration and authenticate
+# The Lagoon CLI is authenticated via ssh-keys. DPLSH will mount your .ssh
+# folder from your homedir, but if your keys are passphrase protected, we need
+# to unlock them.
+$ eval $(ssh-agent); ssh-add
+# Authorize the lagoon cli
+$ task lagoon:cli:config
+
+# List the connection details
+$ lagoon config list
+
+

Configure your local lagoon cli

+

Get the details in the angle-brackets from +Current Platform environments:

+
$ lagoon config add \
+    --graphql https://<GraphQL endpoint> \
+    --ui https://<Lagoon UI> \
+    --hostname <SSH host> \
+    --ssh-key <SSH Key Path> \
+    --port <SSH port>> \
+    --lagoon <Lagoon name>
+
+# Eg.
+$ lagoon config add \
+    --graphql https://api.lagoon.dplplat01.dpl.reload.dk/graphql \
+    --force \
+    --ui https://ui.lagoon.dplplat01.dpl.reload.dk \
+    --hostname 20.238.147.183 \
+    --port 22 \
+    --lagoon dplplat01
+
+

Then log in

+
# Set the configuration as default.
+lagoon config default --lagoon <Lagoon name>
+lagoon login
+lagoon whoami
+
+# Eg.
+lagoon config default --lagoon dplplat01
+lagoon login
+lagoon whoami
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/deploy-a-release/index.html b/dpl-platform/runbooks/deploy-a-release/index.html new file mode 100644 index 00000000..bbb37ae9 --- /dev/null +++ b/dpl-platform/runbooks/deploy-a-release/index.html @@ -0,0 +1,2086 @@ + + + + + + + + + + + + + + + + + + + + + + Deploy a dpl-cms release to a site - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Deploy a dpl-cms release to a site

+

When to use

+

When you wish to roll out a release of DPL-CMS +or a fork to a single site.

+

If you want to deploy to more than one site, simply repeat the procedure for each +site.

+

Prerequisites

+
    +
  • A dplsh session with DPLPLAT_ENV exported and ssh-agent configured.
  • +
  • a shell with a user that is authorized to interact with the environment + repositories in the github organisation used for + the environment over ssh.
  • +
  • The release-tag you whish to deploy, consult the readme in the dpl-cms repository + for instructions on how to build an publish a release.
  • +
+

Procedure

+
# 1. Make any changes to the sites entry sites.yml you need.
+# 2. (optional) diff the deployment
+DIFF=1 SITE=<sitename> task site:sync
+# 3a. Synchronize the site, triggering a deployment if the current state differs
+#    from the intended state in sites.yaml
+SITE=<sitename> task site:sync
+# 3b. If the state does not differ but you still want to trigger a deployment,
+#     specify FORCE=1
+FORCE=1 SITE=<sitename> task site:sync
+
+

The following diagram outlines the full release-flow starting from dpl-cms (or +a fork) and to the release is deployed: +release flow

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/index.html b/dpl-platform/runbooks/index.html new file mode 100644 index 00000000..857ea99c --- /dev/null +++ b/dpl-platform/runbooks/index.html @@ -0,0 +1,2000 @@ + + + + + + + + + + + + + + + + + + + + + + DPL Platform Runbooks - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

DPL Platform Runbooks

+

This directory contains our operational runbooks for standard procedures +you may need to carry out while maintaining and operating a DPL Platform +environment.

+

Most runbooks has the following layout.

+
    +
  • Title - Short title that follows the name of the markdown file for quick + lookup.
  • +
  • When to use - Outlines when this runbook should be used.
  • +
  • Prerequisites - Any requirements that should be met before the procedure is + followed.
  • +
  • Procedure - Stepwise description of the procedure, sometimes these will + be whole subheadings, sometimes just a single section with lists.
  • +
+

The runbooks should focus on the "How", and avoid explaining any.

+ + + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/remove-site-from-platform/index.html b/dpl-platform/runbooks/remove-site-from-platform/index.html new file mode 100644 index 00000000..16e7158b --- /dev/null +++ b/dpl-platform/runbooks/remove-site-from-platform/index.html @@ -0,0 +1,2109 @@ + + + + + + + + + + + + + + + + + + + + + + Removing a site from the platform - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Removing a site from the platform

+

When to use

+

When you wish to delete a site and all its data from the platform

+

Prerequisites:

+
    +
  • The platform environment name
  • +
  • An user with administrative access to the environment repository
  • +
  • A lagoon account with your ssh-key associated
  • +
  • The site key (its key in sites.yaml)
  • +
  • An properly authenticated azure CLI (az) that has administrative access to + the cluster running the lagoon installation
  • +
+

Procedure

+

The procedure consists of the following steps (numbers does not correspond to +the numbers in the script below).

+
    +
  1. Download and archive relevant backups
  2. +
  3. Remove the project from Lagoon
  4. +
  5. Delete the projects namespace from kubernetes.
  6. +
  7. Delete the site from sites.yaml
  8. +
  9. Delete the sites environment repository
  10. +
+

Your first step should be to secure any backups you think might be relevant to +archive. Whether this step is necessary depends on the site. Consult the +Retrieve and Restore backups runbook for the +operational steps.

+

You are now ready to perform the actual removal of the site.

+
# Launch dplsh.
+$ cd infrastructure
+$ dplsh
+
+# You are assumed to be inside dplsh from now on.
+
+# 1. Set an environment,
+# export DPLPLAT_ENV=<platform environment name>
+# eg.
+$ export DPLPLAT_ENV=dplplat01
+
+# 2. Setup access to ssh-keys so that the lagoon cli can authenticate.
+$ eval $(ssh-agent); ssh-add
+
+# 3. Authenticate against lagoon
+$ task lagoon:cli:config
+
+# 4. Delete the project from Lagoon
+# lagoon delete project --project <site machine-name>
+$ lagoon delete project  --project core-test1
+
+# 5. Authenticate against kubernetes
+$ task cluster:auth
+
+# 6. List the namespaces
+# Identify all the project namespace with the syntax <sitename>-<branchname>
+# eg "core-test1-main" for the main branch for the "core-test1" site.
+$ kubectl get ns
+
+# 7. Delete each site namespace
+# kubectl delete ns <namespace>
+# eg.
+$ kubectl delete ns core-test1-main
+
+# 8. Edit sites.yaml, remove the the entry for the site
+$ vi environments/${DPLPLAT_ENV}/sites.yaml
+# Then have Terraform delete the sites repository.
+$ task env_repos:provision
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/retrieve-restore-backup/index.html b/dpl-platform/runbooks/retrieve-restore-backup/index.html new file mode 100644 index 00000000..7ef0a76f --- /dev/null +++ b/dpl-platform/runbooks/retrieve-restore-backup/index.html @@ -0,0 +1,2268 @@ + + + + + + + + + + + + + + + + + + + + + + Retrieving backups - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Retrieving backups

+

When to use

+

When you wish to download an automatic backup made by Lagoon, and optionally +restore it into an existing site.

+

Prerequisites

+
    +
  • Administrative access to the site in the Lagoon UI
  • +
  • (for restore) administrative cluster-access to the site
  • +
+

Procedure

+

Step overview:

+
    +
  1. Download the backup
  2. +
  3. Upload the backup to relevant pods
  4. +
  5. Extract the backup.
  6. +
  7. Cache clearing
  8. +
  9. Cleanup
  10. +
+

While most steps are different for file and database backups, step 1 is close to +identical for the two guides.

+

Be aware that the guide instructs you to copy the backups to /tmp inside the +cli pod. Depending on the resources available on the node /tmp may not have +enough space in which case you may need to modify the cli deployment to add +a temporary volume, or place the backup inside the existing /site/default/files +folder.

+

Step 1, downloading the backup

+

To download the backup access the Lagoon UI and schedule the retrieval of a + backup. To do this,

+
    +
  1. Log in to the environments Lagoon UI (consult the + environment documentation for the url)
  2. +
  3. Access the sites project
  4. +
  5. Access the sites environment ("Main" for production)
  6. +
  7. Click on the "Backups" tab
  8. +
  9. Click on the "Retrieve" button for the backups you wish to download and/or + restore. Use to "Source" column to differentiate the types of backups. + "nginx" are backups of the sites files, while "mariadb" are backups of the + sites database.
  10. +
  11. The Buttons changes to "Downloading..." when pressed, wait for them to + change to "Download", then click them again to download the backup
  12. +
+

Step 2a, restore a database

+

To restore the database we must first copy the backup into a running cli-pod +for a site, and then import the database-dump on top of the running site.

+
    +
  1. Copy the uncompressed mariadb sql file you want to restore into the dpl-platform/infrastructure + folder from which we will launch dplsh
  2. +
  3. Launch dplsh from the infrastructure folder (instructions) + and follow the procedure below:
  4. +
+
# 1. Authenticate against the cluster.
+$ task cluster:auth
+
+# 2. List the namespaces to identify the sites namespace
+# The namespace will be on the form <sitename>-<branchname>
+# eg "bib-rb-main" for the "main" branch for the "bib-rb" site.
+$ kubectl get ns
+
+# 3. Export the name of the namespace as SITE_NS
+# eg.
+$ export SITE_NS=bib-rb-main
+
+# 4. Copy the *mariadb.sql file to the CLI pod in the sites namespace
+# eg.
+kubectl cp \
+  -n $SITE_NS  \
+  *mariadb.sql \
+  $(kubectl -n $SITE_NS get pod -l app.kubernetes.io/instance=cli -o jsonpath="{.items[0].metadata.name}"):/tmp/database-backup.sql
+
+# 5. Have drush inside the CLI-pod import the database and clear out the backup
+kubectl exec \
+  -n $SITE_NS \
+  deployment/cli \
+  -- \
+    bash -c " \
+         echo Verifying file \
+      && test -s /tmp/database-backup.sql \
+         || (echo database-backup.sql is missing or empty && exit 1) \
+      && echo Dropping database \
+      && drush sql-drop -y \
+      && echo Importing backup \
+      && drush sqlc < /tmp/database-backup.sql \
+      && echo Clearing cache \
+      && drush cr \
+      && rm /tmp/database-backup.sql
+    "
+
+

Step 2b, restore a sites files

+

To restore backed up files into a site we must first copy the backup into a +running cli-pod for a site, and then rsync the files on top top of the running +site.

+
    +
  1. Copy tar.gz file into the dpl-platform/infrastructure folder from which we + will launch dplsh
  2. +
  3. Launch dplsh from the infrastructure folder (instructions) + and follow the procedure below:
  4. +
+
# 1. Authenticate against the cluster.
+$ task cluster:auth
+
+# 2. List the namespaces to identify the sites namespace
+# The namespace will be on the form <sitename>-<branchname>
+# eg "bib-rb-main" for the "main" branch for the "bib-rb" site.
+$ kubectl get ns
+
+# 3. Export the name of the namespace as SITE_NS
+# eg.
+$ export SITE_NS=bib-rb-main
+
+# 4. Copy the files tar-ball into the CLI pod in the sites namespace
+# eg.
+kubectl cp \
+  -n $SITE_NS  \
+  backup*-nginx-*.tar.gz \
+  $(kubectl -n $SITE_NS get pod -l app.kubernetes.io/instance=cli -o jsonpath="{.items[0].metadata.name}"):/tmp/files-backup.tar.gz
+
+# 5. Replace the current files with the backup.
+# The following
+# - Verifies the backup exists
+# - Removes the existing sites/default/files
+# - Un-tars the backup into its new location
+# - Fixes permissions and clears the cache
+# - Removes the backup archive
+#
+# These steps can also be performed one by one if you want to.
+kubectl exec \
+  -n $SITE_NS \
+  deployment/cli \
+  -- \
+    bash -c " \
+         echo Verifying file \
+      && test -s /tmp/files-backup.tar.gz \
+         || (echo files-backup.tar.gz is missing or empty && exit 1) \
+      && tar ztf /tmp/files-backup.tar.gz data/nginx &> /dev/null \
+         || (echo could not verify the tar.gz file files-backup.tar && exit 1) \
+      && test -d /app/web/sites/default/files \
+         || (echo Could not find destination /app/web/sites/default/files \
+             && exit 1) \
+      && echo Removing existing sites/default/files \
+      && rm -fr /app/web/sites/default/files \
+      && echo Unpacking backup \
+      && mkdir -p /app/web/sites/default/files \
+      && tar --strip 2 --gzip --extract --file /tmp/files-backup.tar.gz \
+             --directory /app/web/sites/default/files data/nginx \
+      && echo Fixing permissions \
+      && chmod -R 777 /app/web/sites/default/files \
+      && echo Clearing cache \
+      && drush cr \
+      && echo Deleting backup archive \
+      && rm /tmp/files-backup.tar.gz
+    "
+
+#  NOTE: In some situations some files in /app/web/sites/default/files might
+#  be locked by running processes. In that situations delete all the files you
+#  can from /app/web/sites/default/files manually, and then repeat the step
+#  above skipping the removal of /app/web/sites/default/files
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/retrieve-sites-logs/index.html b/dpl-platform/runbooks/retrieve-sites-logs/index.html new file mode 100644 index 00000000..785deca0 --- /dev/null +++ b/dpl-platform/runbooks/retrieve-sites-logs/index.html @@ -0,0 +1,2090 @@ + + + + + + + + + + + + + + + + + + + + + + Retrieve site logs - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Retrieve site logs

+

When to use

+

When you want to inspects the logs produced by as specific site

+

Prerequisites

+
    +
  • Login credentials for Grafana. As a fallback the password for the admin can + password can be fetched from the cluster if it has not been changed via
  • +
+
kubectl get secret \
+  --namespace grafana \
+  -o jsonpath="{.data.admin-password}" \
+  grafana \
+| base64 -d
+
+

Consult the access-kubernetes Run book for instructions +on how to access the cluster.

+

Procedure - Loki / Grafana

+

Using the inspector to download logs

+
    +
  1. Access the environments Grafana installation - consult the + platform-environments.md for the url.
  2. +
  3. Select "Explorer" in the left-most menu and select the "Loki" data source in + the top.
  4. +
  5. Query the logs for the environment by either
  6. +
  7. Use the Log Browser to pick the namespace for the site. It will follow the + pattern <sitename>-<branchname>.
  8. +
  9. Do a custom LogQL, eg. to + fetch all logs from the nginx container for the site "main" branch of the + "rdb" site do query on the form + {app="nginx-php-persistent",container="nginx",namespace="rdb-main"}
  10. +
  11. Eg, for the main branch for the site "rdb": {namespace="rdb-main"}
  12. +
  13. Click "Inspector" -> "Data" -> "Download Logs" to download the log lines.
  14. +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/run-a-lagoon-task/index.html b/dpl-platform/runbooks/run-a-lagoon-task/index.html new file mode 100644 index 00000000..f1d8ab68 --- /dev/null +++ b/dpl-platform/runbooks/run-a-lagoon-task/index.html @@ -0,0 +1,2069 @@ + + + + + + + + + + + + + + + + + + + + + + Run a Lagoon Task - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Run a Lagoon Task

+

When to use

+

When you need to run a Lagoon task

+

Prerequisites

+

You need access to the Lagoon UI

+

Procedure

+
    +
  • Login to the Lagoon UI.
  • +
  • Navigate to the project you want to access.
  • +
  • Choose the environment you want to interact with.
  • +
  • Click the "Tasks" tab.
  • +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/scale-aks/index.html b/dpl-platform/runbooks/scale-aks/index.html new file mode 100644 index 00000000..1e016645 --- /dev/null +++ b/dpl-platform/runbooks/scale-aks/index.html @@ -0,0 +1,2124 @@ + + + + + + + + + + + + + + + + + + + + + + Scaling AKS - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Scaling AKS

+

When to use

+

When the cluster is over or underprovisioned and needs to be scaled.

+

Prerequisites

+
    +
  • A running dplsh launched from ./infrastructure with + DPLPLAT_ENV set to the platform environment name.
  • +
+

References

+ +

Procedure

+

There are multiple approaches to scaling AKS. We run with the auto-scaler enabled +which means that in most cases the thing you want to do is to adjust the max or +minimum configuration for the autoscaler.

+ +

Adjusting the autoscaler

+

Edit the infrastructure configuration for your environment. Eg for dplplat01 +edit dpl-platform/dpl-platform/infrastructure/environments/dplplat01/infrastructure/main.tf.

+

Adjust the *_count_min / *_count_min corrospodning to the node-pool you +want to grow/shrink.

+

Then run infra:provision to have terraform effect the change.

+
task infra:provision
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/set-environment-variable/index.html b/dpl-platform/runbooks/set-environment-variable/index.html new file mode 100644 index 00000000..b8e04648 --- /dev/null +++ b/dpl-platform/runbooks/set-environment-variable/index.html @@ -0,0 +1,2108 @@ + + + + + + + + + + + + + + + + + + + + + + Set an environment variable for a site - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Set an environment variable for a site

+

When to use

+

When you wish to set an environment variable on a site. The variable can be +available for either all sites in the project, or for a specific site in the +project.

+

The variables are safe for holding secrets, and as such can be used both for +"normal" configuration values, and secrets such as api-keys.

+

The variabel will be available to all containers in the environment can can be +picked up and parsed eg. in Drupals settings.php.

+

Prerequisites

+
    +
  • A running dplsh with DPLPLAT_ENV set to the platform + environment name and ssh-agent running if your ssh-keys are passphrase + protected.
  • +
  • An user that has administrative privileges on the lagoon project in question.
  • +
+

Procedure

+
# From within a dplsh session authorized to use your ssh keys:
+
+# 1. Authenticate against the cluster and lagoon
+$ task cluster:auth
+$ task lagoon:cli:config
+
+# 2. Refresh your Lagoon token.
+$ lagoon login
+
+# 3. Then get the project id by listing your projects
+$ lagoon list projects
+
+# 4. Optionally, if you wish to set a variable for a single environment - eg a
+# pull-request site, list the environments in the project
+$ lagoon list environments -p <project name>
+
+# 5. Finally, set the variable
+# - Set the the type_id to the id of the project or environment depending on
+#   whether you want all or a single environment to access the variable
+# - Set scope to VARIABLE_TYPE or ENVIRONMENT depending on the choice above
+# - set
+$ VARIABLE_TYPE_ID=<project or environment id> \
+  VARIABLE_TYPE=<PROJECT or ENVIRONMENT> \
+  VARIABLE_SCOPE=RUNTIME \
+  VARIABLE_NAME=<your variable name> \
+  VARIABLE_VALUE=<your variable value> \
+  task lagoon:set:environment-variable
+
+# If you get a "Invalid Auth Token" your token has probably expired, generated a
+# new with "lagoon login" and try again.
+
+

The variable will be available the next time the site is deployed by Lagoon. +Use the deployment runbook to trigger a deployment, use +a forced deployment if you do not have a new release to deploy.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/update-upgrade-status/index.html b/dpl-platform/runbooks/update-upgrade-status/index.html new file mode 100644 index 00000000..0a885acc --- /dev/null +++ b/dpl-platform/runbooks/update-upgrade-status/index.html @@ -0,0 +1,2074 @@ + + + + + + + + + + + + + + + + + + + + + + Update the support workload upgrade status - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Update the support workload upgrade status

+

When to use

+

When you need to update the support workload version sheet.

+

Prerequisites

+ +

Procedure

+

Run dplsh to extract the current and latest version for all support workloads

+
# First authenticate against the cluster
+task cluster:auth
+# Then pull the status
+task ops:get-versions
+
+

Then access the version status sheet +and update the status from the output.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/upgrading-aks/index.html b/dpl-platform/runbooks/upgrading-aks/index.html new file mode 100644 index 00000000..a4b2e62c --- /dev/null +++ b/dpl-platform/runbooks/upgrading-aks/index.html @@ -0,0 +1,2204 @@ + + + + + + + + + + + + + + + + + + + + + + Upgrading AKS - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + + + + +

Upgrading AKS

+

When to use

+

When you want to upgrade Azure Kubernetes Service to a newer version.

+

Prerequisites

+
    +
  • A running dplsh launched from ./infrastructure with + DPLPLAT_ENV set to the platform environment name.
  • +
  • Knowledge about the version of AKS you wish to upgrade to.
  • +
  • Consult AKS Kubernetes Release Calendar + for a list of the various versions and when they are End of Life
  • +
+

References

+ +

Procedure

+

We use Terraform to upgrade AKS. Should you need to do a manual upgrade consult +Azures documentation on upgrading a cluster +and on upgrading node pools. +Be aware in both cases that the Terraform state needs to be brought into sync +via some means, so this is not a recommended approach.

+

Find out which versions of kubernetes an environment can upgrade to

+

In order to find out which versions of kubernetes we can upgrade to, we need to +use the following command:

+
task cluster:get-upgrades
+
+

This will output a table of in which the column "Upgrades" lists the available +upgrades for the highest available minor versions.

+

A Kubernetes cluster can can at most be upgraded to the nearest minor version, +which means you may be in a situation where you have several versions between +you and the intended version.

+

Minor versions can be skipped, and AKS will accept a cluster being upgraded to +a version that does not specify a patch version. So if you for instance want +to go from 1.20.9 to 1.22.15, you can do 1.21, and then 1.22.15. When +upgrading to 1.21 Azure will substitute the version for an the hightest available +patch version, e.g. 1.21.14.

+

You should know know which version(s) you need to upgrade to, and can continue to +the actual upgrade.

+

Ensuring the Terraform state is in sync

+

As we will be using Terraform to perform the upgrade we want to make sure it its +state is in sync. Execute the following task and resolve any drift:

+
task infra:provision
+
+

Upgrade the cluster

+

Upgrade the control-plane:

+
    +
  1. +

    Update the control_plane_version reference in infrastructure/environments/<environment>/infrastructure/main.tf + and run task infra:provision to apply. You can skip patch-versions, but you + can only do one minor-version at the time

    +
  2. +
  3. +

    Monitor the upgrade as it progresses. A control-plane upgrade is usually performed + in under 5 minutes.

    +
  4. +
+

Monitor via eg.

+
watch -n 5 kubectl version
+
+

Then upgrade the system, admin and application node-pools in that order one by +one.

+
    +
  1. +

    Update the pool_[name]_version reference in + infrastructure/environments/<environment>/infrastructure/main.tf. + The same rules applies for the version as with control_plane_version.

    +
  2. +
  3. +

    Monitor the upgrade as it progresses. Expect the provisioning of and workload + scheduling to a single node to take about 5-10 minutes. In particular be aware + that the admin node-pool where harbor runs has a tendency to take a long time + as the harbor pvcs are slow to migrate to the new node.

    +
  4. +
+

Monitor via eg.

+
watch -n 5 kubectl get nodes
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/upgrading-lagoon/index.html b/dpl-platform/runbooks/upgrading-lagoon/index.html new file mode 100644 index 00000000..b517fcd2 --- /dev/null +++ b/dpl-platform/runbooks/upgrading-lagoon/index.html @@ -0,0 +1,2132 @@ + + + + + + + + + + + + + + + + + + + + + + Upgrading Lagoon - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Upgrading Lagoon

+

When to use

+

When there is a need to upgrade Lagoon to a new patch or minor version.

+

References

+ +

Prerequisites

+
    +
  • A running dplsh launched from ./infrastructure with + DPLPLAT_ENV set to the platform environment name.
  • +
  • Knowledge about the version of Lagoon you want to upgrade to.
  • +
  • You can extract version (= chart version) and appVersion (= lagoon release + version) for the lagoon-remote / lagoon-core charts via the following commands + (replace lagoon-core for lagoon-remote if necessary).
  • +
+

Lagoon-core:

+
curl -s https://uselagoon.github.io/lagoon-charts/index.yaml \
+  | yq '.entries.lagoon-core[] | [.name, .appVersion, .version, .created] | @tsv'
+
+

Lagoon-remote:

+
curl -s https://uselagoon.github.io/lagoon-charts/index.yaml \
+  | yq '.entries.lagoon-remote[] | [.name, .appVersion, .version, .created] | @tsv'
+
+
    +
  • Knowledge of any breaking changes or necessary actions that may affect the + platform when upgrading. See chart release notes for all intermediate chart + releases.
  • +
+

Procedure

+
    +
  1. Upgrade Lagoon core
      +
    1. Backup the API and Keycloak dbs as described in the official documentation
    2. +
    3. Bump the chart version VERSION_LAGOON_CORE in + infrastructure/environments/<env>/lagoon/lagoon-versions.env
    4. +
    5. Perform a helm diff
        +
      • DIFF=1 task lagoon:provision:core
      • +
      +
    6. +
    7. Perform the actual upgrade
        +
      • task lagoon:provision:core
      • +
      +
    8. +
    +
  2. +
  3. Upgrade Lagoon remote
      +
    1. Bump the chart version VERSION_LAGOON_REMOTE in + infrastructure/environments/dplplat01/lagoon/lagoon-versions.env
    2. +
    3. Perform a helm diff
        +
      • DIFF=1 task lagoon:provision:remote
      • +
      +
    4. +
    5. Perform the actual upgrade
        +
      • task lagoon:provision:remote
      • +
      +
    6. +
    7. Take note in the output from Helm of any CRD updates that may be required
    8. +
    +
  4. +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/upgrading-support-workloads/index.html b/dpl-platform/runbooks/upgrading-support-workloads/index.html new file mode 100644 index 00000000..87d75091 --- /dev/null +++ b/dpl-platform/runbooks/upgrading-support-workloads/index.html @@ -0,0 +1,2938 @@ + + + + + + + + + + + + + + + + + + + + + + Upgrading Support Workloads - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Upgrading Support Workloads

+

When to use

+

When you want to upgrade support workloads in the cluster. This includes.

+
    +
  • Cert-manager
  • +
  • Grafana
  • +
  • Harbor
  • +
  • Ingress Nginx
  • +
  • K8up
  • +
  • Loki
  • +
  • Minio
  • +
  • Prometheus
  • +
  • Promtail
  • +
+

This document contains general instructions for how to upgrade support workloads, +followed by specific instructions for each workload (linked above).

+

Prerequisites

+
    +
  • Dplsh instance authorized against the cluster.
  • +
+

General Procedure

+
    +
  1. +

    Identify the version you want to bump in the environment/configuration directory + eg. for dplplat01 infrastructure/environments/dplplat01/configuration/versions.env. + The file contains links to the relevant Artifact Hub pages for the individual + projects and can often be used to determine both the latest version, but also + details about the chart such as how a specific manifest is used. + You can find the latest version of the support workload in the + Version status + sheet which itself is updated via the procedure described in the + Update Upgrade status runbook.

    +
  2. +
  3. +

    Consult any relevant changelog to determine if the upgrade will require any + extra work beside the upgrade itself. + To determine which version to look up in the changelog, be aware of the difference + between the chart version + and the app version. + We currently track the chart versions, and not the actual version of the + application inside the chart. In order to determine the change in appVersion + between chart releases you can do a diff between releases, and keep track of the + appVersion property in the charts Chart.yaml. Using using grafana as an example: + https://github.com/grafana/helm-charts/compare/grafana-6.55.1...grafana-6.56.0. + The exact way to do this differs from chart to chart, and is documented in the + Specific producedures and tests below.

    +
  4. +
  5. +

    Carry out any chart-specific preparations described in the charts update-procedure. + This could be upgrading a Custom Resource Definition that the chart does not + upgrade.

    +
  6. +
  7. +

    Identify the relevant task in the main Taskfile + for upgrading the workload. For example, for cert-manager, the task is called + support:provision:cert-manager and run the task with DIFF=1, eg + DIFF=1 task support:provision:cert-manager.

    +
  8. +
  9. +

    If the diff looks good, run the task without DIFF=1, eg task support:provision:cert-manager.

    +
  10. +
  11. +

    Then proceeded to perform the verification test for the relevant workload. See + the following section for known verification tests.

    +
  12. +
+

Specific producedures and tests

+ +

Cert Manager

+

Comparing cert-manager versions

+

The project project versions its Helm chart together with the app itself. So, +simply use the chart version in the following checks.

+

Cert Manager keeps Release notes +for the individual minor releases of the project. Consult these for every +upgrade past a minor version.

+

As both are versioned in the same repository, simply use the following link +for looking up the release notes for a specific patch release, replacing the +example tag with the version you wish to upgrade to.

+

https://github.com/cert-manager/cert-manager/releases/tag/v1.11.2

+

To compare two reversions, do the same using the following link:

+

https://github.com/cert-manager/cert-manager/compare/v1.11.1...v1.11.2

+

Upgrade cert-manager

+

Commands

+
# Diff
+DIFF=1 task support:provision:cert-manager
+
+# Upgrade
+task support:provision:cert-manager
+
+

Verify cert-manager upgrade

+

Verify that cert-manager itself and webhook pods are all running and healthy.

+
task support:verify:cert-manager
+
+

Grafana

+

Comparing Grafana versions

+

Insert the chart version in the following link to see the release note.

+

https://github.com/grafana/helm-charts/releases/tag/grafana-6.52.9

+

The note will most likely be empty. Now diff the chart version with the current +version, again replacing the version with the relevant for your releases.

+

https://github.com/grafana/helm-charts/compare/grafana-6.43.3...grafana-6.52.9

+

As the repository contains a lot of charts, you will need to do a bit of +digging. Look for at least charts/grafana/Chart.yaml which can tell you the +app version.

+

With the app-version in hand, you can now look at the release notes for the +grafana app +itself.

+

Upgrade grafana

+

Diff command

+
DIFF=1 task support:provision:grafana
+
+

Upgrade command

+
task support:provision:grafana
+
+

Verify grafana upgrade

+

Verify that the Grafana pods are all running and healthy.

+
kubectl get pods --namespace grafana
+
+

Access the Grafana UI and see if you can log in. If you do not have a user +but have access to read secrets in the grafana namespace, you can retrive the +admin password with the following command:

+
# Password for admin
+UI_NAME=grafana task ui-password
+
+# Hostname for grafana
+kubectl -n grafana get -o jsonpath="{.spec.rules[0].host}" ingress grafana ; echo
+
+

Harbor

+

Comparing Harbor versions

+

Harbor has different app and chart versions.

+

An overview of the chart versions can be retrived +from Github. +the chart does not have a changelog.

+

Link for comparing two chart releases: +https://github.com/goharbor/harbor-helm/compare/v1.10.1...v1.12.0

+

Having identified the relevant appVersions, consult the list of +Harbor releases to see a description +of the changes included in the release in question. If this approach fails you +can also use the diff-command described below to determine which image-tags are +going to change and thus determine the version delta.

+

Harbor is a quite active project, so it may make sense mostly to pay attention +to minor/major releases and ignore the changes made in patch-releases.

+

Upgrade Harbor

+

Harbor documents the general upgrade procedure for non-kubernetes upgrades for minor +versions on their website. +This documentation is of little use to our Kubernetes setup, but it can be useful +to consult the page for minor/major version upgrades to see if there are any +special considerations to be made.

+

The Harbor chart repository has upgrade instructions as well. +The instructions asks you to do a snapshot of the database and backup the tls +secret. Snapshotting the database is currently out of scope, but could be a thing +that is considered in the future. The tls secret is handled by cert-manager, and +as such does not need to be backed up.

+

With knowledge of the app version, you can now update versions.env as described +in the General Procedure section, diff to see the changes +that are going to be applied, and finally do the actual upgrade.

+

Diff command

+
DIFF=1 task support:provision:harbor
+
+

Upgrade command

+
task support:provision:harbor
+
+

Verify Harbor upgrade

+

First verify that pods are coming up

+
kubectl -n harbor get pods
+
+

When Harbor seems to be working, you can verify that the UI is working by +accessing https://harbor.lagoon.dplplat01.dpl.reload.dk/. The password for +the user admin can be retrived with the following command:

+
UI_NAME=harbor task ui-password
+
+

If everything looks good, you can consider to deploying a site. One way to do this +is to identify an existing site of low importance, and re-deploy it. A re-deploy +will require Lagoon to both fetch and push images. Instructions for how to access +the lagoon UI is out of scope of this document, but can be found in the runbook +for running a lagoon task. In this case you are looking +for the "Deploy" button on the sites "Deployments" tab.

+

Ingress-nginx

+

Comparing ingress-nginx versions

+

When working with the ingress-nginx chart we have at least 3 versions to keep +track off.

+

The chart version tracks the version of the chart itself. The charts appVersion +tracks a controller application which dynamically configures a bundles nginx. +The version of nginx used is determined configuration-files in the controller. +Amongst others the +ingress-nginx.yaml.

+

Link for diffing two chart versions: +https://github.com/kubernetes/ingress-nginx/compare/helm-chart-4.6.0...helm-chart-4.6.1

+

The project keeps a quite good changelog for the chart

+

Link for diffing two controller versions: +https://github.com/kubernetes/ingress-nginx/compare/controller-v1.7.1...controller-v1.7.0

+

Consult the individual GitHub releases +for descriptions of what has changed in the controller for a given release.

+

Upgrade ingress-nginx

+

With knowledge of the app version, you can now update versions.env as described +in the General Procedure section, diff to see the changes +that are going to be applied, and finally do the actual upgrade.

+

Diff command

+
DIFF=1 task support:provision:ingress-nginx
+
+

Upgrade command

+
task support:provision:ingress-nginx
+
+

Verify ingress-nginx upgrade

+

The ingress-controller is very central to the operation of all public accessible +parts of the platform. It's area of resposibillity is on the other hand quite +narrow, so it is easy to verify that it is working as expected.

+

First verify that pods are coming up

+
kubectl -n ingress-nginx get pods
+
+

Then verify that the ingress-controller is able to serve traffic. This can be +done by accessing the UI of one of the apps that are deployed in the platform.

+

Access eg. https://ui.lagoon.dplplat01.dpl.reload.dk/.

+

K8up

+

We can currently not upgrade to version 2.x of K8up as Lagoon +is not yet ready

+

Loki

+

Comparing Loki versions

+

The Loki chart is versioned separatly from Loki. The version of Loki installed +by the chart is tracked by its appVersion. So when upgrading, you should always +look at the diff between both the chart and app version.

+

The general upgrade procedure will give you the chart version, access the +following link to get the release note for the chart. Remember to insert your +version:

+

https://github.com/grafana/loki/releases/tag/helm-loki-5.5.1

+

Notice that the Loki helm-chart is maintained in the same repository as Loki +itself. You can find the diff between the chart versions by comparing two +chart release tags.

+

https://github.com/grafana/loki/compare/helm-loki-5.5.0...helm-loki-5.5.1

+

As the repository contains changes to Loki itself as well, you should seek out +the file production/helm/loki/Chart.yaml which contains the appVersion that +defines which version of Loki a given chart release installes.

+

Direct link to the file for a specific tag: +https://github.com/grafana/loki/blob/helm-loki-3.3.1/production/helm/loki/Chart.yaml

+

With the app-version in hand, you can now look at the +release notes for Loki +to see what has changed between the two appVersions.

+

Last but not least the Loki project maintains a upgrading guide that can be +found here: https://grafana.com/docs/loki/latest/upgrading/

+

Upgrade Loki

+

Diff command

+
DIFF=1 task support:provision:loki
+
+

Upgrade command

+
task support:provision:loki
+
+

Verify Loki upgrade

+

List pods in the loki namespace to see if the upgrade has completed +successfully.

+
  kubectl --namespace loki get pods
+
+

Next verify that Loki is still accessibel from Grafana and collects logs by +logging in to Grafana. Then verify the Loki datasource, and search out some +logs for a site. See the validation steps for Grafana +for instructions on how to access the Grafana UI.

+

MinIO

+

We can currently not upgrade MinIO without loosing the Azure blob gateway. +see:

+ +

Prometheus

+

Comparing Prometheus versions

+

The kube-prometheus-stack helm chart is quite well maintained and is versioned +and developed separately from the application itself.

+

A specific release of the chart can be accessed via the following link:

+

https://github.com/prometheus-community/helm-charts/releases/tag/kube-prometheus-stack-45.27.2

+

The chart is developed alongside a number of other community driven prometheus- +related charts in https://github.com/prometheus-community/helm-charts.

+

This means that the following comparison between two releases of the chart +will also contain changes to a number of other charts. You will have to look +for changes in the charts/kube-prometheus-stack/ directory.

+

https://github.com/prometheus-community/helm-charts/compare/kube-prometheus-stack-45.26.0...kube-prometheus-stack-45.27.2

+

Upgrade Prometheus

+

The Readme for the chart contains a good Upgrading Chart +section that describes things to be aware of when upgrading between specific +minor and major versions. The same documentation can also be found on +artifact hub.

+

Consult the section that matches the version you are upgrading from and to. Be +aware that upgrades past a minor version often requires a CRD update. The +CRDs may have to be applied before you can do the diff and upgrade. Once the +CRDs has been applied you are committed to the upgrade as there is no simple +way to downgrade the CRDs.

+

Diff command

+
DIFF=1 task support:provision:prometheus
+
+

Upgrade command

+
task support:provision:prometheus
+
+

Verify Prometheus upgrade

+

List pods in the prometheus namespace to see if the upgrade has completed +successfully. You should expect to see two types of workloads. First a single +a single promstack-kube-prometheus-operator pod that runs Prometheus, and then +a promstack-prometheus-node-exporter pod for each node in the cluster.

+
  kubectl --namespace prometheus get pods -l "release=promstack"
+
+

As the Prometheus UI is not directly exposed, the easiest way to verify that +Prometheus is running is to access the Grafana UI and verify that the dashboards +that uses Prometheus are working, or as a minimum that the prometheus datasource +passes validation. See the validation steps for Grafana +for instructions on how to access the Grafana UI.

+

Promtail

+

Comparing Promtail versions

+

The Promtail chart is versioned separatly from Promtail which itself is a part of +Loki. The version of Promtail installed by the chart is tracked by its appVersion. +So when upgrading, you should always look at the diff between both the chart and +app version.

+

The general upgrade procedure will give you the chart version, access the +following link to get the release note for the chart. Remember to insert your +version:

+

https://github.com/grafana/helm-charts/releases/tag/promtail-6.6.0

+

The note will most likely be empty. Now diff the chart version with the current +version, again replacing the version with the relevant for your releases.

+

https://github.com/grafana/helm-charts/compare/promtail-6.6.0...promtail-6.6.1

+

As the repository contains a lot of charts, you will need to do a bit of +digging. Look for at least charts/promtail/Chart.yaml which can tell you the +app version.

+

With the app-version in hand, you can now look at the +release notes for Loki +(which promtail is part of). Look for notes in the Promtail sections of the +release notes.

+

Upgrade Promtail

+

Diff command

+
DIFF=1 task support:provision:promtail
+
+

Upgrade command

+
task support:provision:promtail
+
+

Verify Promtail upgrade

+

List pods in the promtail namespace to see if the upgrade has completed +successfully.

+
  kubectl --namespace promtail get pods
+
+

With the pods running, you can verify that the logs are being collected seeking +out logs via Grafana. See the validation steps for Grafana +for details on how to access the Grafana UI.

+

You can also inspect the logs of the individual pods via

+
kubectl --namespace promtail logs -l "app.kubernetes.io/name=promtail"
+
+

And verify that there are no obvious error messages.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-platform/runbooks/using-dplsh/index.html b/dpl-platform/runbooks/using-dplsh/index.html new file mode 100644 index 00000000..3becfa1b --- /dev/null +++ b/dpl-platform/runbooks/using-dplsh/index.html @@ -0,0 +1,2099 @@ + + + + + + + + + + + + + + + + + + + + + + Using the DPL Shell - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Using the DPL Shell

+

The Danish Public Libraries Shell is a container-based shell used by platform- +operators for all cli operations.

+

When to use

+

Whenever you perform any administrative actions on the platform. If you want +to know more about the shell itself? Refer to tools/dplsh.

+

Prerequisites

+
    +
  • Docker
  • +
  • jq
  • +
  • Bash 4 or newer
  • +
  • An authorized Azure az cli. The version should match the version found in + FROM mcr.microsoft.com/azure-cli:version in the dplsh Dockerfile + You can choose to authorize the az cli from within dplsh, but your session + will only last as long as the shell-session. The use you authorize as must + have permission to read the Terraform state from the Terraform setup + , and Contributor permissions on the environments + resource-group in order to provision infrastructure.
  • +
  • dplsh.sh symlinked into your path as dplsh, see Launching the Shell + (optional, but assumed below)
  • +
+

Procedure

+
# Launch dplsh.
+$ cd infrastructure
+$ dplsh
+
+# 1. Set an environment,
+# export DPLPLAT_ENV=<platform environment name>
+# eg.
+$ export DPLPLAT_ENV=dplplat01
+
+# 2a. Authenticate against AKS, needed by infrastructure and Lagoon tasks
+$ task cluster:auth
+
+# 2b - if you want to use the Lagoon CLI)
+# The Lagoon CLI is authenticated via ssh-keys. DPLSH will mount your .ssh
+# folder from your homedir, but if your keys are passphrase protected, we need
+# to unlock them.
+$ eval $(ssh-agent); ssh-add
+# Then authorize the lagoon cli
+$ task lagoon:cli:config
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-react/architecture/adr-001-rehydration/index.html b/dpl-react/architecture/adr-001-rehydration/index.html new file mode 100644 index 00000000..0ab7d9ee --- /dev/null +++ b/dpl-react/architecture/adr-001-rehydration/index.html @@ -0,0 +1,2247 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Rehydration - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Rehydration

+

Context

+

We are not able to persist and execute a users intentions across page loads. +This is expressed through a number of issues. The main agitator is maintaining +intent whenever a user tries to do anything that requires them to be +authenticated. In these situations they get redirected off the page and after a +successful login they get redirected back to the origin page but without the +intended action fulfilled.

+

One example is the AddToChecklist functionality. Whenever a user wants to add +a material to their checklist they click the "Tilføj til huskelist" button next +to the material presentation. +They then get redirected to Adgangsplatformen. +After a successful login they get redirected back to the material page but the +material has not been added to their checklist.

+

Decision

+

After an intent has been stated we want the intention to be executed even though +a page reload comes in the way.

+

We move to implementing what we define as an explicit intention before the +actual action is tried for executing.

+
    +
  1. User clicks the button.
  2. +
  3. Intent state is generated and committed.
  4. +
  5. Implementation checks if the intended action meets all the requirements. In + this case, being logged in and having the necessary payload.
  6. +
  7. If the intention meets all requirements we then fire the addToChecklist + action.
  8. +
  9. Material is added to the users checklist.
  10. +
+

The difference between the two might seem superfluous but the important +distinction to make is that with our current implementation we are not able to +serialize and persist the actions as the application state across page loads. By +defining intent explicitly we are able to serialize it and persist it between +page loads.

+

This resolves in the implementation being able to rehydrate the persisted state, +look at the persisted intentions and have the individual application +implementations decide what to do with the intention.

+

A mock implementation of the case by case business logic looks as follows.

+
const initialStore = {
+  authenticated: false,
+  intent: {
+    status: '',
+    payload: {}
+  }
+}
+
+const fulfillAction = store.authenticated && 
+    (store.intent.status === 'pending' || store.intent.status === 'tried')
+const getRequirements = !store.authenticated && store.intent.status === 'pending'
+const abandonIntention = !store.authenticated && store.intent.status === 'tried'
+
+function AddToChecklist ({ materialId, store }) {
+  useEffect(() => {
+    if (fulfillAction) {
+      // We fire the actual functionality required to add a material to the 
+      // checklist and we remove the intention as a result of it being
+      // fulfilled.
+      addToChecklistAction(store, materialId)
+    } else if (getRequirements) {
+      // Before we redirect we set the status to be "tried".
+      redirectToLogin(store)
+    } else if (abandonIntention) {
+      // We abandon the intent so that we won't have an infinite loop of retries
+      // at every page load.
+      abandonAddToChecklistIntention(store)
+    }
+  }, [materialId, store.intent.status])
+  return (
+    <button
+      onClick={() => {
+        // We do not fire the actual logic that is required to add a material to
+        // the checklist. Instead we add the intention of said action to the
+        // store. This is when we would set the status of the intent to pending
+        // and provide the payload.
+        addToChecklistIntention(store, materialId)
+      }}
+    >
+      Tilføj til huskeliste
+    </button>
+  )
+}
+
+

We utilize session storage to persist the state on the client due to it's short +lived nature and porous features.

+

We choose Redux as the framework to implemenent this. Redux is a blessed choice +in this instance. It has widespread use, an approachable design and is +well-documented. The best way to go about a current Redux implementation as of +now is @reduxjs/toolkit. Redux is a +sufficiently advanced framework to support other uses of application state and +even co-locating shared state between applications.

+

For our persistence concerns we want to use the most commonly used tool for +that, redux-persist. There are some +implementation details +to take into consideration when integrating the two.

+

Alternatives considered

+

Persistence in URL

+

We could persist the intentions in the URL that is delivered back to the client +after a page reload. This would still imply some of the architectural decisions +described in Decision in regards to having an "intent" state, but some of the +different status flags etc. would not be needed since state is virtually shared +across page loads in the url. However this simpler solution cannot handle more +complex situations than what can be described in the URL feasibly.

+

useContext

+

React offers useContext() +for state management as an alternative to Redux.

+

We prefer Redux as it provides a more complete environment when working with +state management. There is already a community of established practices and +libraries which integrate with Redux. One example of this is our need to persist +actions. When using Redux we can handle this with redux-persist. With +useContext() we would have to roll our own implementation.

+

Some of the disadvantages of using Redux e.g. the amount of required boilerplate +code are addressed by using @reduxjs/toolkit.

+

Status

+

Accepted

+

Consequences

+
    +
  • We are able to support most if not all of our rehydration cases and therefore + pick up user flow from where we left it.
  • +
  • Heavy degree of complexity is added to tasks that requires an intention + instead of a simple action.
  • +
  • Saving the immediate state to the session storage makes for yet another place + to "clear cache".
  • +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-react/architecture/adr-002-ui-text-handling/index.html b/dpl-react/architecture/adr-002-ui-text-handling/index.html new file mode 100644 index 00000000..5ad924be --- /dev/null +++ b/dpl-react/architecture/adr-002-ui-text-handling/index.html @@ -0,0 +1,2101 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: UI Text Handling - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: UI Text Handling

+

Context

+

It has been decided that app context/settings should be passed from the +server side rendered mount points via data props. +One type of settings is text strings that is defined by +the system/host rendering the mount points. +Since we are going to have quite some levels of nested components +it would be nice to have a way to extract the string +without having to define them all the way down the tree.

+

Decision

+

A solution has been made that extracts the props holding the strings +and puts them in the Redux store under the index: text at the app entry level. +That is done with the help of the withText() High Order Component. +The solution of having the strings in redux +enables us to fetch the strings at any point in the tree. +A hook called: useText() makes it simple to request a certain string +inside a given component.

+

Alternatives considered

+

One major alternative would be not doing it and pass down the props. +But that leaves us with text props all the way down the tree +which we would like to avoid. +Some translation libraries has been investigated +but that would in most cases give us a lot of tools and complexity +that is not needed in order to solve the relatively simple task.

+

Consequences

+

Since we omit the text props down the tree +it leaves us with fewer props and a cleaner component setup. +Although some "magic" has been introduced +with text prop matching and storage in redux, +it is outweighed by the simplicity of the HOC wrapper and useText hook.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-react/architecture/adr-003-downshift/index.html b/dpl-react/architecture/adr-003-downshift/index.html new file mode 100644 index 00000000..7cb398e0 --- /dev/null +++ b/dpl-react/architecture/adr-003-downshift/index.html @@ -0,0 +1,2158 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Downshift - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Downshift

+

Context

+

As a part of the project, we need to implement a dropdown autosuggest component. +This component needs to be complient with modern website accessibility rules:

+
    +
  • The component dropdown items are accessible by keyboard by using arrow keys - +down and up.
  • +
  • The items visibly change when they are in current focus.
  • +
  • Items are selectable using the keyboard.
  • +
+

Apart from these accessibility features, the component needs to follow a somewhat +complex design described in +this Figma file. +As visible in the design this autosuggest dropdown doesn't consist only of single +line text items, but also contains suggestions for specific works - utilizing +more complex suggestion items with cover pictures, and release years.

+

Decision

+

Our research on the most popular and supported javascript libraries heavily leans +on this specific article. +In combination with our needs described above in the context section, but also +considering what it would mean to build this component from scratch without any +libraries, the decision taken favored a library called +Downshift.

+

This library is the second most popular JS library used to handle autsuggest +dropdowns, multiselects, and select dropdowns with a large following and +continuous support. Out of the box, it follows the +ARIA principles, and +handles problems that we would normally have to solve ourselves (e.g. opening and +closing of the dropdown/which item is currently in focus/etc.).

+

Another reason why we choose Downshift over its peer libraries is the amount of +flexibility that it provides. In our eyes, this is a strong quality of the library +that allows us to also implement more complex suggestion dropdown items.

+

Alternatives considered

+

Building the autosuggest dropdown not using javascript libraries

+

In this case, we would have to handle accessibility and state management of the +component with our own custom solutition.

+

Status

+

Accepted.

+

Consequences

+
    +
  • We are able to comply with ARIA accesibility design principles for autosuggest +dropdowns/comboboxes.
  • +
  • We introduced complexity to the project for initial project integration of the +library.
  • +
  • After initial integration, this library can be utilized for all other select, +multiselect, and autosuggest/combobox solutions.
  • +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-react/architecture/adr-004-relative-ci/index.html b/dpl-react/architecture/adr-004-relative-ci/index.html new file mode 100644 index 00000000..88dd445a --- /dev/null +++ b/dpl-react/architecture/adr-004-relative-ci/index.html @@ -0,0 +1,2167 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: RelativeCI - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: RelativeCI

+

Context

+

Staying informed about how the size of the JavaScript we require browsers to +download to use the project plays an important part in ensuring a performant +solution.

+

We currently have no awareness of this in this project and the result surfaces +down the line when the project is integrated with the CMS, which is +tested with Lighthouse.

+

To address this we want a solution that will help us monitor the changes to the +size of the bundle we ship for each PR.

+

Decision

+

We add integration to RelativeCI to the project. +RelativeCI supports our primary use case and has a number of qualities which we +value:

+
    +
  • Support for GitHub actions and reporting as GitHub status checks
  • +
  • Support for fork-based development workflows
  • +
  • A free tier for open source projects
  • +
  • Other types of analysis e.g. duplicate packages, continual monitoring
  • +
+

Alternatives considered

+

Bundlewatch

+

Bundlewatch and its ancestor, bundlesize +combine a CLI tool and a web app to provide bundle analysis and feedback on +GitHub as status checks.

+

These solutions no longer seem to be actively maintained. There are several +bugs that would affect +us and fixes remain unmerged. The project relies on a custom secret instead of +GITHUB_TOKEN. This makes supporting our fork-based development workflow +harder.

+

Bundle comparison

+

This is a GitHub Action which can be used in configurations where statistics +for two bundles are compared e.g. for the base and head of a pull request. This +results in a table of changes displayed as a comment in the pull request. +This is managed using GITHUB_TOKEN.

+

Status

+

Accepted.

+

Consequences

+
    +
  • We can determine the effect of adding a new JavaScript library to our project
  • +
  • We add another dependency to a third party system
  • +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-react/architecture/adr-005-react-use/index.html b/dpl-react/architecture/adr-005-react-use/index.html new file mode 100644 index 00000000..f4088f8a --- /dev/null +++ b/dpl-react/architecture/adr-005-react-use/index.html @@ -0,0 +1,2098 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: React Use - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: React Use

+

Context

+

The decision of obtaining react-use as a part of the project originated +from the problem that arose from having an useEffect hook with +an object as a dependency.

+

useEffect does not support comparison of objects or arrays and we needed +a method for comparing such natives.

+

Decision

+

We decided to go for the react-use package +react-use. +The reason is threefold:

+
    +
  • It could solve the problem with deep comparison of dependencies by using + useDeepCompareEffect
  • +
  • It offered an alternative to the + react-hook-inview viewport handling. + So we did not need to use two packages.
  • +
  • It has a range of other utility hooks that we can make use of in the future.
  • +
+

Alternatives considered

+

We could have used our own implementation of the problem. +But since it is a common problem we might as well use a community backed solution. +And react-use gives us a wealth of other tools.

+

Consequences

+

We can now use useDeepCompareEffect instead of useEffect +in cases where we have arrays or objects amomg the dependencies. +And we can make use of all the other utility hooks that the package provides.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-react/architecture/adr-006-unit-tests/index.html b/dpl-react/architecture/adr-006-unit-tests/index.html new file mode 100644 index 00000000..bae3cd21 --- /dev/null +++ b/dpl-react/architecture/adr-006-unit-tests/index.html @@ -0,0 +1,2080 @@ + + + + + + + + + + + + + + + + + + + + Architecture Decision Record: Unit Tests - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture Decision Record: Unit Tests

+

Context

+

The code base is growing and so does the number of functions and custom hooks.

+

While we have a good coverage in our UI tests from Cypress we are lacking +something to tests the inner workings of the applications.

+

With unit tests added we can test bits of functionality that is shared +between different areas of the application and make sure that we get the +expected output giving different variations of input.

+

Decision

+

We decided to go for Vitest which is an easy to use and +very fast unit testing tool.

+

It has more or less the same capabilities as Jest +which is another popular testing framework which is similar.

+

Vitest is framework agnostic so in order to make it possible to test hooks +we found @testing-library/react-hooks +that works in conjunction with Vitest.

+

Alternatives considered

+

We could have used Jest. But trying that we experienced major problems +with having both Jest and Cypress in the same codebase. +They have colliding test function names and Typescript could not figure it out.

+

There is probably a solution but at the same time we got Vitest recommended. +It seemed very fast and just as capable as Jest. And we did not have the +colliding issues of shared function/object names.

+

Consequences

+

We now have unit test as a part of the mix which brings more stability +and certainty that the individual pieces of the application work.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-react/campaigns/index.html b/dpl-react/campaigns/index.html new file mode 100644 index 00000000..36a1f553 --- /dev/null +++ b/dpl-react/campaigns/index.html @@ -0,0 +1,2123 @@ + + + + + + + + + + + + + + + + + + + + + + Campaigns - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Campaigns

+

Campaigns are elements that are shown on the search result page above the search +result list. There are three types of campaigns:

+
    +
  1. Full campaigns - containing an image and a some text.
  2. +
  3. Text-only campaigns - they don't show any images.
  4. +
  5. Image-only campaigns - they don't show any text.
  6. +
+

However, they are only shown in case certain criteria are met. We check for this +by contacting the dpl-cms API.

+

How campaign setup works in dpl-cms

+

Dpl-cms is a cms system based on Drupal, where the system administrators can set +up campaigns they want to show to their users. Drupal also allows the cms system +to act as an API endpoint that we then can contact from our apps.

+

The cms administrators can specify the content (image, text) and the visibility +criteria for each campaign they create. The visibility criteria is based on +search filter facets. +Does that sound familiar? Yes, we use another API to get that very data +in THIS project - in the search result app. +The facets differ based on the search string the user uses for their search.

+

As an example, the dpl-cms admin could wish to show a Harry Potter related +campaign to all the users whose search string retreives search facets which +have "Harry Potter" as one of the most relevant subjects. +Campaigns in dpl-cms can use triggers such as subject, main language, etc.

+

React code example

+

An example code snippet for retreiving a campaign from our react apps would then +look something like this:

+
  // Creating a state to store the campaign data in.
+  const [campaignData, setCampaignData] = useState<CampaignMatchPOST200 | null>(
+    null
+  );
+
+  // Retreiving facets for the campaign based on the search query and existing
+  // filters.
+  const { facets: campaignFacets } = useGetFacets(q, filters);
+
+  // Using the campaign hook generated by Orval from src/core/dpl-cms/dpl-cms.ts
+  // in order to get the mutate function that lets us retreive campaigns.
+  const { mutate } = useCampaignMatchPOST();
+
+  // Only fire the campaign data call if campaign facets or the mutate function
+  // change their value.
+  useDeepCompareEffect(() => {
+    if (campaignFacets) {
+      mutate(
+        {
+          data: campaignFacets as CampaignMatchPOSTBodyItem[]
+        },
+        {
+          onSuccess: (campaign) => {
+            setCampaignData(campaign);
+          },
+          onError: () => {
+            // Handle error.
+          }
+        }
+      );
+    }
+  }, [campaignFacets, mutate]);
+
+

Showing campaigns in dpl-react when in development mode

+

You first need to make sure to have a campaign set up in your locally running +dpl-cms ( +run this repo locally) +Then, in order to see campaigns locally in dpl-react in development mode, you +will most likely need a browser plugin such as Google Chrome's +"Allow CORS: Access-Control-Allow-Origin" +in order to bypass CORS policy for API data calls.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-react/code_guidelines/index.html b/dpl-react/code_guidelines/index.html new file mode 100644 index 00000000..279a1a50 --- /dev/null +++ b/dpl-react/code_guidelines/index.html @@ -0,0 +1,2666 @@ + + + + + + + + + + + + + + + + + + + + + + React Code guidelines - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

React Code guidelines

+

The following guidelines describe best practices for developing code for React +components for the Danish Public Libraries CMS project. The guidelines should +help achieve:

+
    +
  • A stable, secure and high quality foundation for building and maintaining + client-side JavaScript components for library websites
  • +
  • Consistency across multiple developers participating in the project
  • +
  • The best possible conditions for sharing components between library websites
  • +
  • The best possible conditions for the individual library website to customize + configuration and appearance
  • +
+

Contributions to the DDB React project will be reviewed by members of the Core +team. These guidelines should inform contributors about what to expect in such a +review. If a review comment cannot be traced back to one of these guidelines it +indicates that the guidelines should be updated to ensure transparency.

+

Coding standards

+

The project follows the Airbnb JavaScript Style Guide +and Airbnb React/JSX Style Guide. This +choice is based on multiple factors:

+
    +
  1. Historically the community of developers working with DDB + has ties to the Drupal project. + Drupal has adopted the Airbnb JavaScript Style Guide + so this choice should ensure consistency between the two projects.
  2. +
  3. Airbnb's standard is one of the best known and most used in the JavaScript + ccoding standard landscape.
  4. +
  5. Airbnb’s standard is both comprehensive and well documented.
  6. +
  7. Airbnb’s standards cover both JavaScript in general React/JSX specifically. + This avoids potential conflicts between multiple standards.
  8. +
+

The following lists significant areas where the project either intentionally +expands or deviates from the official standards or areas which developers should +be especially aware of.

+

General

+
    +
  • The default language for all code and comments is English.
  • +
  • Components must be compatible with the latest stable version of the following + browsers:
  • +
  • Desktop
      +
    • Microsoft Edge
    • +
    • Google Chrome
    • +
    • Safari
    • +
    • Firefox
    • +
    +
  • +
  • Mobile
      +
    • Google Chrome
    • +
    • Safari
    • +
    • Firefox
    • +
    • Samsung Browser
    • +
    +
  • +
+

JavaScript

+

Named functions vs. anonymous arrow functions

+

AirBnB's only guideline towards this is that +anonymous arrow function nation is preferred over the normal anonymous function +notation.

+

This project sticks to the above guideline as well. If we need to pass a +function as part of a callback or in a promise chain and we on top of that need +to pass some contextual variables that are not passed implicitly from either the +callback or the previous link in the promise chain we want to make use of an +anonymous arrow function as our default.

+

This comes with the build in disclaimer that if an anonymous function isn't +required the implementer should heavily consider moving the logic out into its +own named function expression.

+

The named function is primarily desired due to it's easier to debug nature in +stacktraces.

+

React

+
    +
  • Configuration must be passed as props for components. This allows the host + system to modify how a component works when it is inserted.
  • +
  • All components should be provided with skeleton screens. + This ensures that the user interface reflects the final state even when data + is loaded asynchronously. This reduces load time frustration.
  • +
  • Components should be optimistic. + Unless we have reason to believe that an operation may fail we should provide + fast response to users.
  • +
  • All interface text must be implemented as props for components. This allows + the host system to provide a suitable translation/version when using the + component.
  • +
+

CSS

+
    +
  • All classes must have the dpl- prefix. This makes them distinguishable from + classes provided by the host system.
  • +
  • Class names should follow the Block-Element-Modifier architecture.
  • +
  • Components must use and/or provide a default style sheet which at least + provides a minimum of styling showing the purpose of the component.
  • +
  • Elements must be provided with meaningful classes even though they are not + targeted by the default style sheet. This helps host systems provide + additional styling of the components. Consider how the component consists of + blocks and elements with modifiers and how these can be nested within each + other.
  • +
  • Components must use SCSS for styling. The project uses PostCSS + and PostCSS-SCSS within Webpack for + processing.
  • +
+

HTML

+
    +
  • Components must use semantic HTML5 markup.
  • +
  • Components must provide configuration to set a top headline level for the + component. This helps provide a proper document outline to ensure the + accessibility of the system.
  • +
+

Naming

+

Files

+

Files provided by components must be placed in the following folders and have +the extensions defined here.

+
    +
  • Components (React applications)
  • +
  • apps/[component-name]/[component-name].jsx
      +
    • Core JSX component.
    • +
    +
  • +
  • components/[component-name]/[component-name].scss
      +
    • Stylesheet for the component.
    • +
    +
  • +
  • apps/[component-name]/[component-name].entry.jsx
      +
    • Main application entrypoint.
    • +
    • This will usually also be where state management is implemented.
    • +
    • This must not include the default stylesheet.
    • +
    +
  • +
  • apps/[component-name]/[component-name].dev.jsx
      +
    • Storybook entry for the component.
    • +
    • If the component has a stylesheet this must also be included here.
    • +
    +
  • +
  • apps/[component-name]/[component-name].mount.js
      +
    • Code for registering the application to be booted when a page is loaded on + the host system.
    • +
    +
  • +
  • apps/[component-name]/[component-name].test.js
      +
    • Test of the component implemented with Cypress
    • +
    +
  • +
  • Reusable elements (React components)
  • +
  • components/[component-name]/[component-name].dev.jsx
  • +
  • components/[component-name]/[component-name].jsx
  • +
  • components/[component-name]/[component-name].scss
  • +
  • Reusable functions and classes
  • +
  • core/[function].js
  • +
  • core/[Class].js
  • +
+

Third party code

+

The project uses Yarn as a package +manager to handle code which is developed outside the project repository. Such +code must not be committed to the Core project repository.

+

When specifying third party package versions the project follows these +guidelines:

+
    +
  • Use the ^ next significant release operator + for packages which follow semantic versioning.
  • +
  • The version specified must be the latest known working and secure version. We + do not want accidental downgrades.
  • +
  • We want to allow easy updates to all working releases within the same major + version.
  • +
  • Packages which are not intended to be executed at runtime in the production + environment should be marked as development dependencies.
  • +
+

Reusing dependencies

+

Components must reuse existing dependencies in the project before adding new +ones which provide similar functionality. This ensures consistency and avoids +unnecessary increases in the package size of the project.

+

The reasoning behind the choice of key dependencies have been documented in +the architecture directory.

+

Altering third party code

+

The project uses patches rather than forks to modify third party packages. This +makes maintenance of modified packages easier and avoids a collection of forked +repositories within the project.

+
    +
  • Use an appropriate method for the corresponding package manager for managing + the patch.
  • +
  • Patches should be external by default. In rare cases it may be needed to + commit them as a part of the project.
  • +
  • When providing a patch you must document the origin of the patch e.g. through + an url in a commit comment or preferably in the package manager configuration + for the project.
  • +
+

Code comments

+

Code comments which describe what an implementation does should only be used +for complex implementations usually consisting of multiple loops, conditional +statements etc.

+

Inline code comments should focus on why an unusual implementation has been +implemented the way it is. This may include references to such things as +business requirements, odd system behavior or browser inconsistencies.

+

Commit messages

+

Commit messages in the version control system help all developers understand the +current state of the code base, how it has evolved and the context of each +change. This is especially important for a project which is expected to have a +long lifetime.

+

Commit messages must follow these guidelines:

+
    +
  1. Each line must not be more than 72 characters long
  2. +
  3. The first line of your commit message (the subject) must contain a short + summary of the change. The subject should be kept around 50 characters long.
  4. +
  5. The subject must be followed by a blank line
  6. +
  7. Subsequent lines (the body) should explain what you have changed and why the + change is necessary. This provides context for other developers who have not + been part of the development process. The larger the change the more + description in the body is expected.
  8. +
  9. If the commit is a result of an issue in a public issue tracker, + platform.dandigbib.dk, then the subject must start with the issue number + followed by a colon (:). If the commit is a result of a private issue tracker + then the issue id must be kept in the commit body.
  10. +
+

When creating a pull request the pull request description should not contain any +information that is not already available in the commit messages.

+

Developers are encouraged to read How to Write a Git Commit Message by Chris Beams.

+

Tool support

+

The project aims to automate compliance checks as much as possible using static +code analysis tools. This should make it easier for developers to check +contributions before submitting them for review and thus make the review process +easier.

+

The following tools pay a key part here:

+
    +
  1. Eslint with the following rulesets and plugins:
      +
    1. Airbnb JavaScript Style Guide
    2. +
    3. Airbnb React/JSX Style Guide
    4. +
    5. Prettier
    6. +
    7. Cypress
    8. +
    +
  2. +
  3. Stylelint with the following rulesets and plugins
      +
    1. Recommended SCSS
    2. +
    3. Prettier
    4. +
    5. BEM support
    6. +
    +
  4. +
+

In general all tools must be able to run locally. This allows developers to get +quick feedback on their work.

+

Tools which provide automated fixes are preferred. This reduces the burden of +keeping code compliant for developers.

+

Code which is to be exempt from these standards must be marked accordingly in +the codebase - usually through inline comments (Eslint, +Stylelint). This must also +include a human readable reasoning. This ensures that deviations do not affect +future analysis and the project should always pass through static analysis.

+

If there are discrepancies between the automated checks and the standards +defined here then developers are encouraged to point this out so the automated +checks or these standards can be updated accordingly.

+

Writing frontend tests

+

The frontend tests are executed in +Cypress.

+

The test files are placed alongside the application components +and are named following pattern: "*.test.ts". Eg.: material.test.ts.

+

Test structuring

+

After quite a lot of bad experiences with unstable tests +and reading both the official documentation +and articles about the best practices we have ended up with a recommendation of +how to write the tests.

+

According to this article +it is important to distinguish between commands and assertions. +Commands are used in the beginning of a statement and yields a chainable element +that can be followed by one or more assertions in the end.

+

So first we target an element. +Next we can make one or more assertions on the element.

+

We have created some helper commands for targeting an element: +getBySel, getBySelLike and getBySelStartEnd. +They look for elements as advised by the +Selecting Elements +section from the Cypress documentation about best practices.

+

Example of a statement:

+
// Targeting.
+cy.getBySel("reservation-success-title-text")
+  // Assertion.
+  .should("be.visible")
+  // Another assertion.
+  .and("contain", "Material is available and reserved for you!");
+
+

Writing Unit Tests

+

We are using Vitest as framework for running unit tests. +By using that we can test functions (and therefore also hooks) and classes.

+

Where do I place my tests?

+

They have to be placed in src/tests/unit.

+

Or they can also be placed next to the code at the end of a file as described +here.

+
export const sum = (...numbers: number[]) =>
+  numbers.reduce((total, number) => total + number, 0);
+
+if (import.meta.vitest) {
+  const { describe, expect, it } = import.meta.vitest;
+
+  describe("sum", () => {
+    it("should sum numbers", () => {
+      expect(sum(1, 2, 3)).toBe(6);
+    });
+  });
+}
+
+

In that way it helps us to test and mock unexported functions.

+

Testing hooks

+

For testing hooks we are using the library +@testing-library/react-hooks +and you can also take a look at the text test +to see how it can be done.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-react/error_handling/index.html b/dpl-react/error_handling/index.html new file mode 100644 index 00000000..36b16c93 --- /dev/null +++ b/dpl-react/error_handling/index.html @@ -0,0 +1,2174 @@ + + + + + + + + + + + + + + + + + + + + + + Error Handling - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Error Handling

+

Error handling is something that is done on multiple levels: +Eg.: Form validation, network/fetch error handling, runtime errors. +You could also argue that the fact that the codebase is making use of typescript +and code generation from the various http services (graphql/REST) belongs to +the same idiom of error handling in order to make the applications more robust.

+

Error Boundary

+

Error boundary was introduced +in React 16 and makes it possible to implement a "catch all" feature +catching "uncatched" errors and replacing the application with a component +to the users that something went wrong. +It is meant ato be a way of having a safety net and always be able to tell +the end user that something went wrong. +The apps are being wrapped in the error boundary handling which makes it +possible to catch thrown errors at runtime.

+

Fetch and Error Boundary

+

Async operations and therby also fetch are not being handled out of the box +by the React Error Boundary. But fortunately react-query, which is being used +both by the REST services (Orval) and graphql (Graphql Code Generator), has a +way of addressing the problem. The QueryClient can be configured to trigger +the Error Boundary system +if an error is thrown. +So that is what we are doing.

+

Fetch error classes

+

Two different types of error classes have been made in order to handle errors +in the fetchers: http errors and fetcher errors.

+

Http errors are the ones originating from http errors +and have a status code attached.

+

Fetcher errors are whatever else bad that could apart from http errors. +Eg. JSON parsing gone wrong.

+

Both types of errors comes in two variants: "normal" and "critical". The idea is +that only critical errors will trigger an Error Boundary.

+

For instance if you look at the +DBC Gateway fetcher +it throws a DbcGateWayHttpError in case of a http error occurs. +DbcGateWayHttpError +extends the +FetcherCriticalHttpError +which makes sure to trigger the Error Boundary system.

+
Using Error.name
+

The reason why *.name is set in the errors is to make it clear which error +was thrown. If we don't do that the name of the parent class is used in the +error log. And then it is more difficult to trace where the error originated +from.

+

Future considerations

+

The initial implementation is handling http errors on a coarse level: +If response.ok is false then throw an error. If the error is critical +the error boundary is triggered. +In future version you could could take more things into consideration +regarding the error:

+
    +
  • Should all status codes trigger an error?
  • +
  • Should we have different types of error level depending on request +and/or http method?
  • +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-react/fbs-adapter-client/index.html b/dpl-react/fbs-adapter-client/index.html new file mode 100644 index 00000000..145263b8 --- /dev/null +++ b/dpl-react/fbs-adapter-client/index.html @@ -0,0 +1,2125 @@ + + + + + + + + + + + + + + + + + + + + + + FBS adapter client - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

FBS adapter client

+

The FBS client adapter is autogenerated base the swagger 1.2 json files from +FBS. But Orval requires that we use swagger version 2.0 and the adapter has some +changes in paths and parameters. So some conversion is need at the time of this +writing.

+

FBS documentation can be found here.

+

All this will hopefully be changed when/or if the adapter comes with its own +specifications.

+

API spec converter

+

A repository dpl-fbs-adapter-tool +tool build around PHP and NodeJS can translate the FBS specifikation into one +usable for Orval client generator. It also filters out all the FBS calls not +need by the DPL project.

+

The tool uses go-task to simply the execution of the +command.

+

Setup

+

Simple use the installation task.

+
task dev:install
+
+

Convert swagger

+

First convert the swagger 1.2 (located in /fbs/externalapidocs) to swagger 2.0 +using the api-spec-converter tool.

+
dev:swagger2yaml
+
+

Build the Adapter specifications

+

Build the swagger specification usable by Orval then run Orval.

+
task dev:convert
+
+

FBS Adapter

+

The FSB adapter lives at: https://github.com/DBCDK/fbs-cms-adapter

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-react/index.html b/dpl-react/index.html new file mode 100644 index 00000000..3e1c9381 --- /dev/null +++ b/dpl-react/index.html @@ -0,0 +1,2791 @@ + + + + + + + + + + + + + + + + + + + + + + DPL React - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

DPL React

+

A set of React components and applications providing self-service features for +Danish public libraries.

+

Development

+

Requirements

+ +

Before you can install the project you need to create the file ~/.npmrc to +access the GitHub package registry as described using a personal access token. +The token must be created with the required scopes: repo and read:packages

+

If you have npm installed locally this can be achieved by running the following +command and using the token when prompted for password.

+
npm login --registry=https://npm.pkg.github.com
+
+

Howto

+
    +
  1. Run task dev:start
  2. +
  3. Access Storybook at http://dpl-react.docker
  4. +
+

Alternative without Docker

+
    +
  1. Add 127.0.0.1 dpl-react.docker to your /etc/hosts file
  2. +
  3. Ensure that your node version matches what is specified in package.json.
  4. +
  5. Install dependencies: yarn install
  6. +
  7. Start storybook sudo yarn start:storybook:dev
  8. +
  9. If you need to log in through Adgangsplatformen, you need to change + your url to http://dpl-react.docker/ instead of http://localhost. This + avoids getting log in errors
  10. +
+

Step Debugging in Visual Studio Code (no docker)

+

If you want to enable step debugging you need to:

+
    +
  • Copy .vscode.example/launch.json into .vscode/
  • +
  • Mark 1 or more breakpoints on a line in the left gutter on an open file
  • +
  • In the top menu in VS Code choose: Run -> Start Debugging
  • +
  • Type in your user password if ask to
  • +
  • Start debugging 🤖∿💻
  • +
+

Access tokens

+

Access token must be retrieved from Adgangsplatformen, +a single sign-on solution for public libraries in Denmark, and OpenPlatform, +an API for danish libraries.

+

Usage of these systems require a valid client id and secret which must be +obtained from your library partner or directly from DBC, the company responsible +for running Adgangsplatformen and OpenPlatform.

+

This project include a client id that matches the storybook setup which can be +used for development purposes. You can use the /auth story to sign in to +Adgangsplatformen for the storybook context.

+

(Note: if you enter Adgangsplatformen again after signing it, you will get +signed out, and need to log in again. This is not a bug, as you stay logged +in otherwise.)

+

Library token

+

To test the apps that is indifferent to wether the user is authenticated or not +it is possible to set a library token via the library component in Storybook. +Workflow:

+
    +
  • Retrieve a library token via OpenPlatform
  • +
  • Insert the library token in the Library Token story in storybook
  • +
+

Standard and style

+

JavaScript + JSX

+

For static code analysis we make use of the Airbnb JavaScript Style Guide +and for formatting we make use of Prettier +with the default configuration. The above choices have been influenced by a +multitude of factors:

+
    +
  • Historically Drupal core have been making use of the Airbnb JavaScript Style + Guide.
  • +
  • Airbnb's standard is comparatively the best known + and one of the most used + in the JavaScript coding standard landscape.
  • +
+

This makes future adoption easier for onboarding contributors and support is to +be expected for a long time.

+
Named functions Vs. Anonymous arrow functions
+

AirBnB's only guideline towards this is that anonymous arrow function are +preferred over the normal anonymous function notation.

+

When you must use an anonymous function (as when passing an inline callback), +use arrow function notation.

+
+

Why? It creates a version of the function that executes in the context of +this, which is usually what you want, and is a more concise syntax.

+

Why not? If you have a fairly complicated function, you might move that logic +out into its own named function expression.

+
+

Reference

+

This project stick to the above guideline as well. If we need to pass a function +as part of a callback or in a promise chain and we on top of that need to pass +some contextual variables that is not passed implicit from either the callback +or the previous link in the promise chain we want to make use of an anonymous +arrow function as our default.

+

This comes with the build in disclaimer that if an anonymous function isn't +required the implementer should heavily consider moving the logic out into it's +own named function expression.

+

The named function is primarily desired due to it's easier to debug nature in +stacktraces.

+

Create a new application

+
+ 1. Create a new application component + +
// ./src/apps/my-new-application/my-new-application.jsx
+import React from "react";
+import PropTypes from "prop-types";
+
+export function MyNewApplication({ text }) {
+  return (
+      <h2>{text}</h2>
+  );
+}
+
+MyNewApplication.defaultProps = {
+  text: "The fastest man alive!"
+};
+
+MyNewApplication.propTypes = {
+  text: PropTypes.string
+};
+
+export default MyNewApplication;
+
+ +
+ +
+ 2. Create the entry component + +
// ./src/apps/my-new-application/my-new-application.entry.jsx
+import React from "react";
+import PropTypes from "prop-types";
+import MyNewApplication from "./my-new-application";
+
+// The props of an entry is all of the data attributes that were
+// set on the DOM element. See the section on "Naive app mount." for
+// an example.
+export function MyNewApplicationEntry(props) {
+  return <MyNewApplication text='Might be from a server?' />;
+}
+
+export default MyNewApplicationEntry;
+
+ +
+ +
+ 3. Create the mount + +
// ./src/apps/my-new-application/my-new-application.mount.js
+import addMount from "../../core/addMount";
+import MyNewApplication from "./my-new-application.entry";
+
+addMount({ appName: "my-new-application", app: MyNewApplication });
+
+ +
+ +
+ 4. Add a story for local development + +
// ./src/apps/my-new-application/my-new-application.dev.jsx
+import React from "react";
+import MyNewApplicationEntry from "./my-new-application.entry";
+import MyNewApplication from "./my-new-application";
+
+export default { title: "Apps|My new application" };
+
+export function Entry() {
+  // Testing the version that will be shipped.
+  return <MyNewApplicationEntry />;
+}
+
+export function WithoutData() {
+  // Play around with the application itself without server side data.
+  return <MyNewApplication />;
+}
+
+ +
+ +
+ 5. Run the development environment + +
  yarn dev
+
+ +OR depending on your dev environment (docker or not) + +
  sudo yarn dev
+
+ +
+ +

Voila! You browser should have opened and a storybook environment is ready +for you to tinker around.

+

Application state-machine

+

Most applications will have multiple internal states, so to aid consistency, +it's recommended to:

+
  const [status, setStatus] = useState("<initial state>");
+
+

and use the following states where appropriate:

+

initial: Initial state for applications that require some sort of +initialization, such as making a request to see if a material can be ordered, +before rendering the order button. Errors in initialization can go directly to +the failed state, or add custom states for communication different error +conditions to the user. Should render either nothing or as a +skeleton/spinner/message.

+

ready: The general "ready state". Applications that doesn't need +initialization (a generic button for instance) can use ready as the initial +state set in the useState call. This is basically the main waiting state.

+

processing: The application is taking some action. For buttons this will be +the state used when the user has clicked the button and the application is +waiting for reply from the back end. More advanced applications may use it while +doing backend requests, if reflecting the processing in the UI is desired. +Applications using optimistic feedback will render this state the same as the +finished state.

+

failed: Processing failed. The application renders an error message.

+

finished: End state for one-shot actions. Communicates success to the user.

+

Applications can use additional states if desired, but prefer the above if +appropriate.

+

Style your application

+
+ 1. Create an application specific stylesheet + +
// ./src/apps/my-new-application/my-new-application.scss
+.dpl-warm {
+  color: maroon;
+}
+
+ +
+ +
+ 2. Add the class to your application + +
// ./src/apps/my-new-application/my-new-application.jsx
+import React from "react";
+import PropTypes from "prop-types";
+
+export function MyNewApplication({ text }) {
+  return (
+      <h2 className='warm'>{text}</h2>
+  );
+}
+
+MyNewApplication.defaultProps = {
+  text: "The fastest man alive!"
+};
+
+MyNewApplication.propTypes = {
+  text: PropTypes.string
+};
+
+export default MyNewApplication;
+
+ +
+ +
+ 3. Import the scss into your story + +
// ./src/apps/my-new-application/my-new-application.dev.jsx
+import React from "react";
+import MyNewApplicationEntry from "./my-new-application.entry";
+import MyNewApplication from "./my-new-application";
+
+import './my-new-application.scss';
+
+export default { title: "Apps|My new application" };
+
+export function Entry() {
+  // Testing the version that will be shipped.
+  return <MyNewApplicationEntry />;
+}
+
+export function WithoutData() {
+  // Play around with the application itself without server side data.
+  return <MyNewApplication />;
+}
+
+ +
+ +

Cowabunga! You now got styling in your application

+

Style using the DPL design system

+

This project includes styling created by its sister repository - +the design system +as a npm package.

+

By default the project should include a release of the design system matching +the current state of the project.

+

To update the design system to the latest stable release of the design system +run:

+
yarn add @danskernesdigitalebibliotek/dpl-design-system@latest
+
+

This command installs the latest released version of the package. Whenever a +new version of the design system package is released, it is necessary +to reinstall the package in this project using the same command to get the +newest styling, because yarn adds a specific version number to the package name +in package.json.

+

Using unreleased design

+

If you need to work with published but unreleased code from a specific branch +of the design system, you can also use the branch name as the tag for the npm +package, replacing all special characters with dashes (-).

+

Example: To use the latest styling from a branch in the design system called +feature/availability-label, run:

+
yarn add @danskernesdigitalebibliotek/dpl-design-system@feature-availability-label
+
+

If the branch resides in a fork (usually before a pull request is merged) you +can use aliasing +and run:

+
yarn config set "@my-fork:registry" "https://npm.pkg.github.com"
+yarn add @danskernesdigitalebibliotek/dpl-design-system@npm:@my-fork/dpl-design-system@feature-availability-label
+
+

If the branch is updated and you want the latest changes to take effect locally +update the release used:

+
yarn upgrade @danskernesdigitalebibliotek/dpl-design-system
+
+

Note that references to unreleased code should never make it into official +versions of the project.

+

Cross application components

+

If the component is simple enough to be a primitive you would use in multiple +occasions it's called an 'atom'. Such as a button or a link. If it's more +specific that that and to be used across apps we just call it a component. An +example would be some type of media presented alongside a header and some text.

+

The process when creating an atom or a component is more or less similar, but +some structural differences might be needed.

+

Creating an atom

+
+ 1. Create the atom + +
// ./src/components/atoms/my-new-atom/my-new-atom.jsx
+import React from "react";
+import PropTypes from 'prop-types';
+
+/**
+ * A simple button.
+ *
+ * @export
+ * @param {object} props
+ * @returns {ReactNode}
+ */
+export function MyNewAtom({ className, children }) {
+  return <button className={`btn ${className}`}>{children}</button>;
+}
+
+MyNewAtom.propTypes = {
+  className: PropTypes.string,
+  children: PropTypes.node.isRequired
+}
+
+MyNewAtom.defaultProps = {
+  className: ""
+}
+
+export default MyNewAtom;
+
+ +
+ +
+ 2. Create styles for the atom + +
// ./src/components/atoms/my-new-atom/my-new-atom.scss
+.dpl-btn {
+    color: blue;
+}
+
+ +
+ +
+ 3. Import the atom's styles into the component stylesheet + +
// ./src/components/components.scss
+@import 'atoms/button/button.scss';
+@import 'atoms/my-new-atom/my-new-atom.scss';
+
+ +
+ +
+ 4. Create a story for your atom + +
// ./src/components/atoms/my-new-atom/my-new-atom.dev.jsx
+import React from "react";
+import MyNewAtom from "./my-new-atom";
+
+export default { title: "Atoms|My new atom" };
+
+export function WithText() {
+  return <MyNewAtom>Cick me!</MyNewAtom>;
+}
+
+ +
+ +
+ 5. Import the atom into the applications or other components where +you would want to use it + +
// ./src/apps/my-new-application/my-new-application.jsx
+import React, {Fragment} from "react";
+import PropTypes from "prop-types";
+
+import MyNewAtom from "../../components/atom/my-new-atom/my-new-atom"
+
+export function MyNewApplication({ text }) {
+  return (
+      <Fragment>
+        <h2 className='warm'>{text}</h2>
+        <MyNewAtom className='additional-class' />
+      </Fragment>
+  );
+}
+
+MyNewApplication.defaultProps = {
+  text: "The fastest man alive!"
+};
+
+MyNewApplication.propTypes = {
+  text: PropTypes.string
+};
+
+export default MyNewApplication;
+
+ +
+ +

Finito! You now know how to share code across applications

+

Creating a component

+

Repeat all of the same steps as with an atom but place it in it's own directory +inside components.

+

Such as ./src/components/my-new-component/my-new-component.jsx

+

Editor example configuration

+

If you use Code we provide some easy to +use and nice defaults for this project. They are located in .vscode.example. +Simply rename the directory from .vscode.example to .vscode and you are good +to go. This overwrites your global user settings for this workspace and suggests +som extensions you might want.

+

Usage

+

There are two ways to use the components provided by this project:

+
    +
  1. As standalone JavaScript applications mounted within HTML pages generated by + a separate system.
  2. +
  3. As components within a larger JavaScript application (Under development)
  4. +
+

Naive app mount

+

So let's say you wanted to make use of an application within an existing HTML +page such as what might be generated serverside by platforms like Drupal, +WordPress etc.

+

For this use case you should download the dist.zip package from +the latest release of the project +and unzip somewhere within the web root of your project. The package contains a +set of artifacts needed to use one or more applications within an HTML page.

+
+ HTML Example + +A simple example of the required artifacts and how they are used looks like +this: + +
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Naive mount</title>
+    <!-- Include CSS files to provide default styling -->
+    <link rel="stylesheet" href="/dist/components.css">
+</head>
+<body>
+    <b>Here be dragons!</b>
+    <!-- Data attributes will be camelCased on the react side aka.
+         props.errorText and props.text -->
+    <div data-dpl-app='add-to-checklist' data-text="Chromatic dragon"
+         data-error-text="Minor mistake"></div>
+    <div data-dpl-app='a-none-existing-app'></div>
+
+    <!-- Load order og scripts is of importance here -->
+    <script src="/dist/runtime.js"></script>
+    <script src="/dist/bundle.js"></script>
+    <script src="/dist/mount.js"></script>
+    <!-- After the necessary scripts you can start loading applications -->
+    <script src="/dist/add-to-checklist.js"></script>
+    <script>
+      // For making successful requests to the different services we need one or
+      // more valid tokens.
+     window.dplReact.setToken("user","XXXXXXXXXXXXXXXXXXXXXX");
+     window.dplReact.setToken("library","YYYYYYYYYYYYYYYYYYYYYY");
+
+      // If this function isn't called no apps will display.
+      // An app will only be displayed if there is a container for it
+      // and a corresponding application loaded.
+      window.dplReact.mount(document);
+    </script>
+</body>
+</html>
+
+ +
+ +

As a minimum you will need the runtime.js and bundle.js. For styling +of atoms and components you will need to import components.css.

+

Each application also has its own JavaScript artifact and it might have a CSS +artifact as well. Such as add-to-checklist.js and add-to-checklist.css.

+

To mount the application you need an HTML element with the correct data +attribute.

+
<div data-dpl-app='add-to-checklist'></div>
+
+

The name of the data attribute should be data-dpl-app and the value should be +the name of the application - the value of the appName parameter assigned in +the application .mount.js file.

+

Data attributes and props

+

As stated above, every application needs the corresponding data-dpl-app +attribute to even be mounted and shown on the page. Additional data attributes +can be passed if necessary. Examples would be contextual ids etc. Normally these +would be passed in by the serverside platform e.g. Drupal, Wordpress etc.

+
<div data-dpl-app='add-to-checklist' data-id="870970-basis:54172613"
+     data-error-text="A mistake was made"></div>
+
+

The above data-id would be accessed as props.id and data-error-text as +props.errorText in the entrypoint of an application.

+
+ Example + +
// ./src/apps/my-new-application/my-new-application.entry.jsx
+import React from "react";
+import PropTypes from "prop-types";
+import MyNewApplication from './my-new-application.jsx';
+
+export function MyNewApplicationEntry({ id }) {
+  return (
+    <MyNewApplication
+      // 870970-basis:54172613
+      id={id}
+    />
+}
+
+export default MyNewApplicationEntry;
+
+ +
+ +

To fake this in our development environment we need to pass these same data +attributes into our entrypoint.

+
+ Example + +
// ./src/apps/my-new-application/my-new-application.dev.jsx
+import React from "react";
+import MyNewApplicationEntry from "./my-new-application.entry";
+import MyNewApplication from "./my-new-application";
+
+export default { title: "Apps|My new application" };
+
+export function Entry() {
+  // Testing the version that will be shipped.
+  return <MyNewApplicationEntry id="870970-basis:54172613" />;
+}
+
+export function WithoutData() {
+  // Play around with the application itself without server side data.
+  return <MyNewApplication />;
+}
+
+ +
+ +

Extending the project

+

If you want to extend this project - either by introducing new components or +expand the functionality of the existing ones - and your changes can be +implemented in a way that is valuable to users in general, please submit pull +requests.

+

Even if that is not the case and you have special needs the infrastructure of +the project should also be helpful to you.

+

In such a situation you should fork this project and extend it to your own needs +by implementing new applications. New applications +can reuse various levels of infrastructure provided by the project such as:

+
    +
  1. Integration with various webservices
  2. +
  3. User authentication and token management
  4. +
  5. Visual atoms or components
  6. +
  7. Visual representations of existing applications
  8. +
  9. Styling using SCSS
  10. +
  11. Test infrastructure
  12. +
  13. Application mounting
  14. +
+

Once the customization is complete the result can be packaged for distribution +by pushing the changes to the forked repository:

+
    +
  1. Changes pushed to the master branch of the forked repository will + automatically update the latest release of the fork.
  2. +
  3. Tags pushed to the forked repository also will be published as new releases + in the fork.
  4. +
+

The result can be used in the same ways as the original project.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-react/skeleton_screens/index.html b/dpl-react/skeleton_screens/index.html new file mode 100644 index 00000000..01d7f9dd --- /dev/null +++ b/dpl-react/skeleton_screens/index.html @@ -0,0 +1,2102 @@ + + + + + + + + + + + + + + + + + + + + + + Skeleton screens - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Skeleton screens

+

In order to improve both UX and the performance score you can choose to use +skeleton screens in situation where you need to fill the interface +with data from a requests to an external service.

+

Main Purpose

+

The skeleton screens are being showed instantly in order to deliver +some content to the end user fast while loading data. +When the data is arriving the skeleton screens are being replaced +with the real data.

+

How to use it

+

The skeleton screens are rendered with help from the +skeleton-screen-css library. + By using ssc classes + you can easily compose screens + that simulate the look of a "real" rendering with real data.

+

Example

+

In this example we are showing a search result item as a skeleton screen. +The skeleton screen consists of a cover, a headline and two lines of text. +In this case we wanted to maintain the styling of the .card-list-item +wrapper. And show the skeleton screen elements by using ssc classes.

+

```tsx +import React from "react";

+

const SearchResultListItemSkeleton: React.FC = () => { + return ( +

+
 
+
+
+
 
+
 
+
+
+ ); +};

+

export default SearchResultListItemSkeleton;

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/dpl-react/ui_text_handling/index.html b/dpl-react/ui_text_handling/index.html new file mode 100644 index 00000000..ec280874 --- /dev/null +++ b/dpl-react/ui_text_handling/index.html @@ -0,0 +1,2210 @@ + + + + + + + + + + + + + + + + + + + + + + UI Text Handling - DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

UI Text Handling

+

This document describes how to use the text functionality +that is partly defined in src/core/utils/text.tsx and in src/core/text.slice.ts.

+

Main Purpose

+

The main purpose of the functionality is to be able to access strings defined +at app level inside of sub components +without passing them all the way down via props. +You can read more about the decision +and considerations here.

+

How to use it

+

In order to use the system the component that has the text props +needs to be wrapped with the withText high order function. +The texts can hereafter be accessed by using the useText hook.

+

Simple example

+

In this example we have a HelloWorld app with three text props attached:

+
import React from "react";
+import { withText } from "../../core/utils/text";
+import HelloWorld from "./hello-world";
+
+export interface HelloWorldEntryProps {
+  titleText: string;
+  introductionText: string;
+  whatText: string;
+}
+
+const HelloWorldEntry: React.FC<HelloWorldEntryProps> = (
+  props: HelloWorldEntryProps
+) => <HelloWorld />;
+
+export default withText(HelloWorldEntry);
+
+

Now it is possible to access the strings like this:

+
import * as React from "react";
+import { Hello } from "../../components/hello/hello";
+import { useText } from "../../core/utils/text";
+
+const HelloWorld: React.FC = () => {
+  const t = useText();
+  return (
+    <article>
+      <h2>{t("titleText")}</h2>
+      <p>{t("introductionText")}</p>
+      <p>
+        <Hello shouldBeEmphasized />
+      </p>
+    </article>
+  );
+};
+export default HelloWorld;
+
+

Placeholder example

+

It is also possible to use placeholders in the text strings. +They can be handy when you want dynamic values embedded in the text.

+

A classic example is the welcome message to the authenticated user. +Let's say you have a text with the key: welcomeMessageText. +The value from the data prop is: Welcome @username, today is @date. +You would the need to reference it like this:

+
import * as React from "react";
+import { useText } from "../../core/utils/text";
+
+const HelloUser: React.FC = () => {
+  const t = useText();
+  const username = getUsername();
+  const currentDate = getCurrentDate();
+
+  const message = t("welcomeMessageText", {
+    placeholders: {
+      "@user": username,
+      "@date": currentDate
+    }
+  });
+
+  return (
+    <div>{message}</div>
+  );
+};
+export default HelloUser;
+
+

Plural example

+

Sometimes you want two versions of a text be shown +depending on if you have one or multiple items being referenced in the text.

+

That can be accommodated by using the plural text definition.

+

Let's say that an authenticated user has a list of unread messages in an inbox. +You could have a text key called: inboxStatusText. +The value from the data prop is:

+
{"type":"plural","text":["You have 1 message in the inbox",
+"You have @count messages in the inbox"]}.
+
+

You would then need to reference it like this:

+
import * as React from "react";
+import { useText } from "../../core/utils/text";
+
+const InboxStatus: React.FC = () => {
+  const t = useText();
+  const user = getUser();
+  const inboxMessageCount = getUserInboxMessageCount(user);
+
+  const status = t("inboxStatusText", {
+    count: inboxMessageCount,
+    placeholders: {
+      "@count": inboxMessageCount
+    }
+  });
+
+  return (
+    <div>{status}</div>
+    // If count == 1 the texts will be:
+    // "You have 1 message in the inbox"
+
+    // If count == 5  the texts will be:
+    // "You have 5 messages in the inbox"
+  );
+};
+export default InboxStatus;
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..647983a4 --- /dev/null +++ b/index.html @@ -0,0 +1,2091 @@ + + + + + + + + + + + + + + + + + + + + DDF/DPL Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Documentation Site

+

This project exists to consolidate and standardize documentation across DPL +(Danish Public Libraries) repositories. It's based on mkdocs +material and uses a handy +multirepo-plugin to import +markdown and build documentation from other DPL repositories dynamically.

+

Adding documentation

+

Any repository you want to add to the documentation site, must have a docs/ +folder in the project root on github and relevant markdown must be moved inside.

+

If you wish to further divide your documentation into subfolders, mkdocs will +accomodate this and build the site navigation accordingly. Here is an example:

+
docs/
+| README.md # Primary docs/index file for your navigation header
+└───folder1 # Left pane navigation category
+    file01.md
+   └───subfolder1 # Expandable sub-navigation of folder1
+         file02.md
+         file03.md
+└───folder2 # Additional left pane navigation category
+      file04.md
+
+

The docs/ folder is self-contained, meaning that any relative links e.g. +"../../" linking outside the docs/ directory, should use an absolute URL like: +https://github.com/danskernesdigitalebibliotek/dpl-docs/blob/main/README.md

+

Otherwise, following the link from the documentation site will cause a 404.

+

Site development

+

The documentation project is hosted +here.

+

It uses a Github workflow to build and publish to github pages. Since +documentation can be updated independently from other repos, a cron has been +established to publish the site every 6 hours. While this covers most use cases, +it's also possible to trigger it adhoc through Github +actions +with the workflow_dispatch event trigger.

+

Customizations (colors, font, social links) and general site configuration can +be configured in +mkdocs.yml

+

Requirements

+ + + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 00000000..965d4d0a --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Documentation Site","text":"

This project exists to consolidate and standardize documentation across DPL (Danish Public Libraries) repositories. It's based on mkdocs material and uses a handy multirepo-plugin to import markdown and build documentation from other DPL repositories dynamically.

"},{"location":"#adding-documentation","title":"Adding documentation","text":"

Any repository you want to add to the documentation site, must have a docs/ folder in the project root on github and relevant markdown must be moved inside.

If you wish to further divide your documentation into subfolders, mkdocs will accomodate this and build the site navigation accordingly. Here is an example:

docs/\n| README.md # Primary docs/index file for your navigation header\n\u2514\u2500\u2500\u2500folder1 # Left pane navigation category\n\u2502   \u2502 file01.md\n\u2502   \u2514\u2500\u2500\u2500subfolder1 # Expandable sub-navigation of folder1\n\u2502       \u2502  file02.md\n\u2502       \u2502  file03.md\n\u2514\u2500\u2500\u2500folder2 # Additional left pane navigation category\n\u2502  file04.md\n

The docs/ folder is self-contained, meaning that any relative links e.g. \"../../\" linking outside the docs/ directory, should use an absolute URL like: https://github.com/danskernesdigitalebibliotek/dpl-docs/blob/main/README.md

Otherwise, following the link from the documentation site will cause a 404.

"},{"location":"#site-development","title":"Site development","text":"

The documentation project is hosted here.

It uses a Github workflow to build and publish to github pages. Since documentation can be updated independently from other repos, a cron has been established to publish the site every 6 hours. While this covers most use cases, it's also possible to trigger it adhoc through Github actions with the workflow_dispatch event trigger.

Customizations (colors, font, social links) and general site configuration can be configured in mkdocs.yml

"},{"location":"#requirements","title":"Requirements","text":"
  • mkdocs material
  • multirepo-plugin
"},{"location":"dpl-cms/","title":"DPL CMS Documentation","text":"

The documentation in this folder describes how to develop DPL CMS.

The focus of the documentation is to inform developers of how to develop the CMS, and give some background behind the various architectural choices.

"},{"location":"dpl-cms/#layout","title":"Layout","text":"

The documentation falls into two categories:

The Markdown-files in this directory document the system as it. Eg. you can read about how to add a new entry to the core configuration.

The ./architecture folder contains our Architectural Decision Records that describes the reasoning behind key architecture decisions. Consult these records if you need background on some part of the CMS, or plan on making any modifications to the architecture.

As for the remaining files and directories

  • ./diagrams contains diagram files like draw.io or PlantUML and rendered diagrams in png/svg format. See the section for details.
  • ./images this is just plain images used by documentation files.
  • ./Taskfile.yml a go-task Taskfile, run task to list available tasks.
"},{"location":"dpl-cms/#diagrams","title":"Diagrams","text":"

We strive to keep the diagrams and illustrations used in the documentation as maintainable as possible. A big part of this is our use of programmatic diagramming via PlantUML and Open Source based manual diagramming via diagrams.net (formerly known as draw.io).

When a change has been made to a *.puml or *.drawio file, you should re-render the diagrams using the command task render and commit the result.

"},{"location":"dpl-cms/api-development/","title":"API Development","text":"

We use the RESTful Web Services and OpenAPI REST Drupal modules to expose endpoints from Drupal as an API to be consumed by external parties.

"},{"location":"dpl-cms/api-development/#howtos","title":"Howtos","text":""},{"location":"dpl-cms/api-development/#create-a-new-endpoint","title":"Create a new endpoint","text":"
  1. Implement a new REST resource plugin by extending Drupal\\rest\\Plugin\\ResourceBase and annotating it with @RestResource
  2. Describe uri_paths, route_parameters and responses in the annotation as detailed as possible to create a strong specification.
  3. Install the REST UI module drush pm-enable restui
  4. Enable and configure the new REST resource. It is important to use the dpl_login_user_token authentication provider for all resources which will be used by the frontend this will provide a library or user token by default.
  5. Inspect the updated OpenAPI specification at /openapi/rest?_format=json to ensure looks as intended
  6. Run task ci:openapi:validate to validate the updated OpenAPI specification
  7. Run task ci:openapi:download to download the updated OpenAPI specification
  8. Uninstall the REST UI module drush pm-uninstall restui
  9. Export the updated configuration drush config-export
  10. Commit your changes including the updated configuration and openapi.json
"},{"location":"dpl-cms/caching/","title":"Caching","text":"

DPL-CMS relies on two levels of caching. Standard Drupal Core caching, and Varnish as an accelerating HTTP cache.

"},{"location":"dpl-cms/caching/#drupal","title":"Drupal","text":"

The Drupal Core cache uses Redis as its storage backend. This takes the load off of the database-server that is typically shared with other sites.

Further more, as we rely on Varnish for all caching of anonymous traffic, the core Internal Page Cache module has been disabled.

"},{"location":"dpl-cms/caching/#varnish","title":"Varnish","text":"

Varnish uses the standard Drupal VCL from lagoon.

The site is configured with the Varnish Purge module and configured with a cache-tags based purger that ensures that changes made to the site, is purged from Varnish instantly.

The configuration follows the Lagoon best practices - reference the Lagoon documentation on Varnish for further details.

"},{"location":"dpl-cms/code-guidelines/","title":"Code guidelines","text":"

The following guidelines describe best practices for developing code for the DPL CMS project. The guidelines should help achieve:

  • A stable, secure and high quality foundation for building and maintaining library websites
  • Consistency across multiple developers participating in the project
  • The best possible conditions for sharing modules between DPL CMS websites
  • The best possible conditions for the individual DPL CMS website to customize configuration and appearance

Contributions to the core DPL CMS project will be reviewed by members of the Core team. These guidelines should inform contributors about what to expect in such a review. If a review comment cannot be traced back to one of these guidelines it indicates that the guidelines should be updated to ensure transparency.

"},{"location":"dpl-cms/code-guidelines/#coding-standards","title":"Coding standards","text":"

The project follows the Drupal Coding Standards and best practices for all parts of the project: PHP, JavaScript and CSS. This makes the project recognizable for developers with experience from other Drupal projects. All developers are expected to make themselves familiar with these standards.

The following lists significant areas where the project either intentionally expands or deviates from the official standards or areas which developers should be especially aware of.

"},{"location":"dpl-cms/code-guidelines/#general","title":"General","text":"
  • The default language for all code and comments is English.
"},{"location":"dpl-cms/code-guidelines/#php","title":"PHP","text":"
  • Code must be compatible with all currently available minor and major versions of PHP from 8.0 and onwards. This is important when trying to ensure smooth updates going forward. Note that this only applies to custom code.
  • Code must be compatible with Drupal Best Practices as defined by the Drupal Coder module
  • Code must use types to define function arguments, return values and class properties.
  • Code must use strict typing.
"},{"location":"dpl-cms/code-guidelines/#javascript","title":"JavaScript","text":"
  • All functionality exposed through JavaScript should use the Drupal JavaScript API and must be attached to the page using Drupal behaviors.
  • All classes used for selectors in Javascript must be prefixed with js-. Example: <div class=\"gallery js-gallery\"> - .gallery must only be used in CSS, js-gallery must only be used in JS.
  • Javascript should not affect classes that are not state-classes. State classes such as is-active, has-child or similar are classes that can be used as an interlink between JS and CSS.
"},{"location":"dpl-cms/code-guidelines/#css","title":"CSS","text":"
  • Modules and themes should use SCSS (The project uses PostCSS and PostCSS-SCSS). The Core system will ensure that these are compiled to CSS files automatically as a part of the development process.
  • Class names should follow the Block-Element-Modifier architecture (BEM). This rule does not apply to state classes.
  • Components (blocks) should be isolated from each other. We aim for an atomic frontend where components should be able to stand alone. In practice, there will be times where this is impossible, or where components can technically stand alone, but will not make sense from a design perspective (e.g. putting a gallery in a sidebar).
  • Components should be technically isolated by having 1 component per scss file. **As a general rule, you can have a file called gallery.scss which contains .gallery, .gallery__container, .gallery__* and so on. Avoid referencing other components when possible.
  • All components/mixins/similar must be documented with a code comment. When you create a new component.scss, there must be a comment at the top, describing the purpose of the component.
  • Avoid using auto-generated Drupal selectors such as .pane-content. Use the Drupal theme system to write custom HTML and use precise, descriptive class names. It is better to have several class names on the same element, rather than reuse the same class name for several components.
  • All \"magic\" numbers must be documented. If you need to make something e.g. 350 px, you preferably need to find the number using calculating from the context ($layout-width * 0.60) or give it a descriptive variable name ($side-bar-width // 350px works well with the current $layout-width_)
  • Avoid using the parent selector (.class &). The use of parent selector results in complex deeply nested code which is very hard to maintain. There are times where it makes sense, but for the most part it can and should be avoided.
"},{"location":"dpl-cms/code-guidelines/#naming","title":"Naming","text":""},{"location":"dpl-cms/code-guidelines/#modules","title":"Modules","text":"
  • All modules written specifically for Ding3 must be prefixed with dpl.
  • The dpl prefix is not required for modules which provide functionality deemed relevant outside the DPL community and are intended for publication on Drupal.org.
"},{"location":"dpl-cms/code-guidelines/#files","title":"Files","text":"

Files provided by modules must be placed in the following folders and have the extensions defined here.

  • General
  • MODULENAME.*.yml
  • MODULENAME.module
  • MODULENAME.install
  • templates/*.html.twig
  • Classes, interfaces and traits
  • src/**/*.php
  • PHPUnit tests
  • tests/**/*.php
  • CSS
  • If the module does not not use processing: /css/COMPONENTNAME.css
  • If the module uses preprocessing: /scss/COMPONENTNAME.scss
  • JavaScript
  • js/*.js
  • Images
  • img/*.(png|jpeg|gif|svg)
"},{"location":"dpl-cms/code-guidelines/#module-elements","title":"Module elements","text":"

Programmatic elements such as settings, state values and views modules must comply to a set of common guidelines.

  • Machine names should be prefixed with the name of the module that is responsible for managing the elements.
  • Administrative titles, human readable names and descriptions should be relatable to the module name.

As there is no finite set of programmatic elements for a DPL CMS site these apply to all types unless explicitly specified.

"},{"location":"dpl-cms/code-guidelines/#code-structure","title":"Code Structure","text":"

The project follows the code structure suggested by the drupal/recommended-project Composer template.

Modules, themes etc. must be placed within the corresponding folder in this repository. If a module developed in relation to this project is of general purpose to the Drupal community it should be placed on Drupal.org and included as an external dependency.

A module must provide all required code and resources for it to work on its own or through dependencies. This includes all configuration, theming, CSS, images and JavaScript libraries.

All default configuration required for a module to function should be implemented using the Drupal configuration system and stored in the version control with the rest of the project source code.

"},{"location":"dpl-cms/code-guidelines/#updating-modules","title":"Updating modules","text":"

If an existing module is expanded with updates to current functionality the default behavior must be the same as previous versions or as close to this as possible. This also includes new modules which replaces current modules.

If an update does not provide a way to reuse existing content and/or configuration then the decision on whether to include the update resides with the business.

"},{"location":"dpl-cms/code-guidelines/#altering-existing-modules","title":"Altering existing modules","text":"

Modules which alter or extend functionality provided by other modules should use appropriate methods for overriding these e.g. by implementing alter hooks or overriding dependencies.

"},{"location":"dpl-cms/code-guidelines/#translations","title":"Translations","text":"

All interface text in modules must be in English. Localization of such texts must be handled using the Drupal translation API.

All interface texts must be provided with a context. This supports separation between the same text used in different contexts. Unless explicitly stated otherwise the module machine name should be used as the context.

"},{"location":"dpl-cms/code-guidelines/#third-party-code","title":"Third party code","text":"

The project uses package managers to handle code which is developed outside of the Core project repository. Such code must not be committed to the Core project repository.

The project uses two package manages for this:

  • Composer - primarily for managing PHP packages, Drupal modules and other code libraries which are executed at runtime in the production environment.
  • Yarn - primarily for managing code needed to establish the pipeline for managing frontend assets like linting, preprocessing and optimization of JavaScript, CSS and images.

When specifying third party package versions the project follows these guidelines:

  • Use the ^ next significant release operator for packages which follow semantic versioning.
  • The version specified must be the latest known working and secure version. We do not want accidental downgrades.
  • We want to allow easy updates to all working releases within the same major version.
  • Packages which are not intended to be executed at runtime in the production environment should be marked as development dependencies.
"},{"location":"dpl-cms/code-guidelines/#altering-third-party-code","title":"Altering third party code","text":"

The project uses patches rather than forks to modify third party packages. This makes maintenance of modified packages easier and avoids a collection of forked repositories within the project.

  • Use an appropriate method for the corresponding package manager for managing the patch.
  • Patches should be external by default. In rare cases it may be needed to commit them as a part of the project.
  • When providing a patch you must document the origin of the patch e.g. through an url in a commit comment or preferably in the package manager configuration for the project.
"},{"location":"dpl-cms/code-guidelines/#error-handling-and-logging","title":"Error handling and logging","text":"

Code may return null or an empty array for empty results but must throw exceptions for signalling errors.

When throwing an exception the exception must include a meaningful error message to make debugging easier. When rethrowing an exception then the original exception must be included to expose the full stack trace.

When handling an exception code must either log the exception and continue execution or (re)throw the exception - not both. This avoids duplicate log content.

Drupal modules must use the Logging API. When logging data the module must use its name as the logging channel and an appropriate logging level.

Modules integrating with third party services must implement a Drupal setting for logging requests and responses and provide a way to enable and disable this at runtime using the administration interface. Sensitive information (such as passwords, CPR-numbers or the like) must be stripped or obfuscated in the logged data.

"},{"location":"dpl-cms/code-guidelines/#code-comments","title":"Code comments","text":"

Code comments which describe what an implementation does should only be used for complex implementations usually consisting of multiple loops, conditional statements etc.

Inline code comments should focus on why an unusual implementation has been implemented the way it is. This may include references to such things as business requirements, odd system behavior or browser inconsistencies.

"},{"location":"dpl-cms/code-guidelines/#commit-messages","title":"Commit messages","text":"

Commit messages in the version control system help all developers understand the current state of the code base, how it has evolved and the context of each change. This is especially important for a project which is expected to have a long lifetime.

Commit messages must follow these guidelines:

  1. Each line must not be more than 72 characters long
  2. The first line of your commit message (the subject) must contain a short summary of the change. The subject should be kept around 50 characters long.
  3. The subject must be followed by a blank line
  4. Subsequent lines (the body) should explain what you have changed and why the change is necessary. This provides context for other developers who have not been part of the development process. The larger the change the more description in the body is expected.
  5. If the commit is a result of an issue in a public issue tracker, platform.dandigbib.dk, then the subject must start with the issue number followed by a colon (:). If the commit is a result of a private issue tracker then the issue id must be kept in the commit body.

When creating a pull request the pull request description should not contain any information that is not already available in the commit messages.

Developers are encouraged to read How to Write a Git Commit Message by Chris Beams.

"},{"location":"dpl-cms/code-guidelines/#tool-support","title":"Tool support","text":"

The project aims to automate compliance checks as much as possible using static code analysis tools. This should make it easier for developers to check contributions before submitting them for review and thus make the review process easier.

The following tools pay a key part here:

  1. PHP_Codesniffer with the following rulesets:
  2. Drupal Coding Standards as defined the Drupal Coder module
  3. RequireStrictTypesSniff as defined by PHP_Codesniffer
  4. Eslint and Airbnb JavaScript coding standards as defined by Drupal Core
  5. Prettier as defined by Drupal Core
  6. Stylelint with the following rulesets:
  7. As defined by Drupal Core
  8. BEM as defined by the stylelint-bem project
  9. Browsersupport as defined by the stylelint-no-unsupported-browser-features project
  10. PHPStan with the following configuration:
  11. Analysis level 8 to support detection of missing types
  12. Drupal support as defined by the phpstan-drupal project
  13. Detection of deprecated code as defined by the phpstan-deprecation-rules project

In general all tools must be able to run locally. This allows developers to get quick feedback on their work.

Tools which provide automated fixes are preferred. This reduces the burden of keeping code compliant for developers.

Code which is to be exempt from these standards must be marked accordingly in the codebase - usually through inline comments (Eslint, PHP Codesniffer). This must also include a human readable reasoning. This ensures that deviations do not affect future analysis and the Core project should always pass through static analysis.

If there are discrepancies between the automated checks and the standards defined here then developers are encouraged to point this out so the automated checks or these standards can be updated accordingly.

"},{"location":"dpl-cms/config-import/","title":"Configuration import","text":"

Setting up a new site for testing certain scenarios can be repetitive. To avoid this the project provides a module: DPL Config Import. This module can be used to import configuration changes into the site and install/uninstall modules in a single step.

The configuration changes are described in a YAML file with configuration entry keys and values as well as module ids to install or uninstall.

"},{"location":"dpl-cms/config-import/#how-to-use","title":"How to use","text":"
  1. Download the example file that comes with the module.
  2. Edit it to set the different configuration values.
  3. Upload the file at /admin/config/configuration/import
  4. Clear the cache.
"},{"location":"dpl-cms/config-import/#how-it-is-parsed","title":"How it is parsed","text":"

The yaml file has two root elements configuration and modules.

A basic file looks like this:

configuration:\n# Add keys for configuration entries to set.\n# Values will be merged with existing values.\nsystem.site:\n# Configuration values can be set directly\nslogan: 'Imported by DPL config import'\n# Nested configuration is also supported\npage:\n# All values in nested configuration must have a key. This is required to\n# support numeric configuration keys.\n403: '/user/login'\nmodules:\n# Add module ids to install or uninstall\ninstall:\n- menu_ui\nuninstall:\n- redis\n
"},{"location":"dpl-cms/configuration-management/","title":"Configuration Management","text":"

We use the Configuration Ignore module to manage configuration.

In general all configuration is ignored except for configuration which should explicitly be managed by DPL CMS core.

"},{"location":"dpl-cms/configuration-management/#background","title":"Background","text":"

Configuration management for DPL CMS is a complex issue. The complexity stems from the following factors:

"},{"location":"dpl-cms/configuration-management/#site-types","title":"Site types","text":"

There are multiple types of DPL CMS sites all using the same code base:

  1. Developer (In Danish: Programm\u00f8r) sites where the library is entirely free to work with the codebase for DPL CMS as they please for their site
  2. Webmaster sites where the library can install and manage additional modules for their DPL CMS site
  3. Editor (In Danish: Redakt\u00f8r) sites where the library can configure their site based on predefined configuration options provided by DPL CMS
  4. Core sites which are default versions of DPL CMS used for development and testing purposes

All these site types must support the following properties:

  1. It must be possible for system administrators to deploy new versions of DPL CMS which may include changes to the site configuration
  2. It must be possible for libraries to configure their site based on the options provided by their type site. This configuration must not be overridden by new versions of DPL CMS.
"},{"location":"dpl-cms/configuration-management/#configuration-types","title":"Configuration types","text":"

This can be split into different types of configuration:

  1. Core configuration: This is the configuration for the base installation of DPL CMS which is shared across all sites. The configuration will be imported on deployment to support central development of the system.
  2. Local configuration: This is the local configuration for the individual site. The level of configuration depends on the site type but no matter the type this configuration must not be overridden on deployment of new versions of DPL CMS.
"},{"location":"dpl-cms/configuration-management/#howtos","title":"Howtos","text":""},{"location":"dpl-cms/configuration-management/#install-a-new-site-from-scratch","title":"Install a new site from scratch","text":"
  1. Run drush site-install --existing-config -y
"},{"location":"dpl-cms/configuration-management/#add-new-core-configuration","title":"Add new core configuration","text":"
  1. Create the relevant configuration through the administration interface
  2. Run drush config-export -y
  3. Append the key for the configuration to config_ignore.settings.ignored_config_entities with the ~ prefix
  4. Commit the new configuration files and the updated config_ignore.settings file
"},{"location":"dpl-cms/configuration-management/#update-existing-core-configuration","title":"Update existing core configuration","text":"
  1. Update the relevant configuration through the administration interface
  2. Run drush config-export -y
  3. Commit the updated configuration files

NB: The keys for these configuration files should already be in config_ignore.settings.ignored_config_entities.

"},{"location":"dpl-cms/configuration-management/#add-new-local-configuration","title":"Add new local configuration","text":"
  1. Update the relevant configuration through the administration interface
  2. Run drush config-export -y
  3. Commit the updated configuration files
"},{"location":"dpl-cms/configuration-management/#enable-a-new-module","title":"Enable a new module","text":"
  1. Add the module to the project code base or as a Composer dependency
  2. Create an update hook in the DPL CMS installation profile which enables the module1
function dpl_cms_update_9000() {\n   \\Drupal::service('module_installer')->install(['shortcut']);\n}\n
  1. Run the update hook locally drush updatedb -y
  2. Export configuration drush config-export -y
  3. Commit the resulting changes to the site configuration, codebase and/or Composer files
"},{"location":"dpl-cms/configuration-management/#uninstall-a-existing-module","title":"Uninstall a existing module","text":"
  1. Create an update hook in the DPL CMS installation profile which uninstalls the module1
function dpl_cms_update_9001() {\n   \\Drupal::service('module_installer')->uninstall(['shortcut']);\n}\n
  1. Run the update hook locally drush updatedb -y
  2. Commit the resulting changes to the site configuration
  3. Export configuration drush config-export -y
  4. Plan for a future removal of code for the module
"},{"location":"dpl-cms/configuration-management/#deploy-configuration-changes","title":"Deploy configuration changes","text":"
  1. Run drush deploy

NB: It is important that the official Drupal deployment procedure is followed. Database updates must be executed before configuration is imported. Otherwise we risk ending up in a situation where the configuration contains references to modules which are not enabled.

  1. Creating update hooks for modules is only necessary once we have sites running in production which will not be reinstalled. Until then it is OK to enable/uninstall modules as normal and committing changes to core.extensions.\u00a0\u21a9\u21a9

"},{"location":"dpl-cms/example-content/","title":"Example content","text":"

We use the Default Content module to manage example content. Such content is typically used when setting up development and testing environments.

All actual example content is stored with the DPL Example Content module.

Usage of the module in this project is derived from the official documentation.

"},{"location":"dpl-cms/example-content/#howtos","title":"Howtos","text":""},{"location":"dpl-cms/example-content/#add-additional-default-content","title":"Add additional default content","text":"
  1. Create the default content
  2. Determine the UUIDs for the entities which should be exported as default content. The easiest way to do this is to enable the Devel module, view the entity and go to the Devel tab.
  3. Add the UUID (s) (and if necessary entity types) to the dpl_example_content.info.yml file
  4. Export the entities by running drush default-content:export-module dpl_example_content
  5. Commit the new files under web/modules/custom/dpl_example_content
"},{"location":"dpl-cms/example-content/#update-existing-default-content","title":"Update existing default content","text":"
  1. Update existing content
  2. Export the entities by running drush default-content:export-module dpl_example_content
  3. Remove references to and files from UUIDs which are no longer relevant.
  4. Commit updated files under web/modules/custom/dpl_example_content
"},{"location":"dpl-cms/lagoon-environments/","title":"Lagoon environments","text":"

We use the Lagoon application delivery platform to host environments for different stages of the DPL CMS project. Our Lagoon installation is managed by the DPL Platform project.

One such type of environment is pull request environments. These environments are automatically created when a developer creates a pull request with a change against the project and allows developers and project owners to test the result before the change is accepted.

"},{"location":"dpl-cms/lagoon-environments/#howtos","title":"Howtos","text":""},{"location":"dpl-cms/lagoon-environments/#create-an-environment-for-a-pull-request","title":"Create an environment for a pull request","text":"
  1. Create a pull request for the change on GitHub. The pull request must be created from a branch in the same repository as the target branch.
  2. Wait for GitHub Actions related to Lagoon deployment to complete. Note: This deployment process can take a while. Be patient.
  3. A link to the deployed environment is available in the section between pull request activity and Actions
  4. The environment is deleted when the pull request is closed
"},{"location":"dpl-cms/lagoon-environments/#access-the-administration-interface-for-a-pull-request-environment","title":"Access the administration interface for a pull request environment","text":"

Accessing the administration interface for a pull request environment may be needed to test certain functionalities. This can be achieved in two ways:

"},{"location":"dpl-cms/lagoon-environments/#through-the-lagoon-administration-ui","title":"Through the Lagoon administration UI","text":"
  1. Access the administration UI (see below)
  2. Go to the environment corresponding to the pull request number
  3. Go to the Task section for the environment
  4. Select the \"Generate login link [drush uli]\" task and click \"Run task\"
  5. Refresh the page to see the task in the task list and wait a bit
  6. Refresh the page to see the task complete
  7. Go to the task page
  8. The log output contains a one-time login link which can be used to access the administration UI
"},{"location":"dpl-cms/lagoon-environments/#through-the-lagoon-cli","title":"Through the Lagoon CLI","text":"
  1. Run task lagoon:drush:uli
  2. The log output contains a one-time login link which can be used to access the administration UI
"},{"location":"dpl-cms/lagoon-environments/#access-the-lagoon-administration-ui","title":"Access the Lagoon administration UI","text":"
  1. Contact administrators of the DPL Platform Lagoon instance to apply for an user account.
  2. Access the URL for the UI of the instance e.g https://ui.lagoon.dplplat01.dpl.reload.dk/
  3. Log in with your user account (see above)
  4. Go to the dpl-cms project
"},{"location":"dpl-cms/lagoon-environments/#setup-the-lagoon-cli","title":"Setup the Lagoon CLI","text":"
  1. Locate information about the Lagoon instance to use in the DPL Platform documentation
  2. Access the URL for the UI of the instance
  3. Log in with your user account (see above)
  4. Go to the Settings page
  5. Add your SSH public key to your account
  6. Install the Lagoon CLI
  7. Configure the Lagoon CLI to use the instance:
lagoon config add \\\n--lagoon [instance name e.g. \"dpl-platform\"] \\\n--hostname [host to connect to with SSH] \\\n--port [SSH port] \\\n--graphql [url to GraphQL endpoint] \\\n--ui [url to UI] \\\n
  1. Verify the installation:
lagoon login --lagoon [instance name]\nlagoon whoami --lagoon [instance name]\n
  1. Use the DPL Platform as your default Lagoon instance:
lagoon config default --lagoon [instance name]\n
"},{"location":"dpl-cms/lagoon-environments/#using-cron-in-pull-request-environments","title":"Using cron in pull request environments","text":"

The .lagoon.yml has an environments section where it is possible to control various settings. On root level you specify the environment you want to address (eg.: main). And on the sub level of that you can define the cron settings. The cron settings for the main branch looks (in the moment of this writing) like this:

environments:\nmain:\ncronjobs:\n- name: drush cron\nschedule: \"M/15 * * * *\"\ncommand: drush cron\nservice: cli\n

If you want to have cron running on a pull request environment, you have to make a similar block under the environment name of the PR. Example: In case you would have a PR with the number #135 it would look like this:

environments:\npr-135:\ncronjobs:\n- name: drush cron\nschedule: \"M/15 * * * *\"\ncommand: drush cron\nservice: cli\n
"},{"location":"dpl-cms/lagoon-environments/#workflow-with-cron-in-pull-request-environments","title":"Workflow with cron in pull request environments","text":"

This way of making sure cronb is running in the PR environments is a bit tedious but it follows the way Lagoon is handling it. A suggested workflow with it could be:

  • Create PR with code changes as normally
  • Write the .lagoon.yml configuration block connected to the current PR #
  • When the PR has been approved you delete the configuration block again
"},{"location":"dpl-cms/local-development/","title":"Local development","text":""},{"location":"dpl-cms/local-development/#copy-database-from-lagoon-environment-to-local-setup","title":"Copy database from Lagoon environment to local setup","text":"

Prerequisites:

  • Login credentials to the Lagoon UI, or an existing database dump

The following describes how to first fetch a database-dump and then import the dump into a running local environment. Be aware that this only gives you the database, not any files from the site.

  1. To retrieve a database-dump from a running site, consult the \"How do I download a database dump?\" guide in the official Lagoon. Skip this step if you already have a database-dump.
  2. Place the dump in the database-dump directory, be aware that the directory is only allowed to contain a single .sql file.
  3. Start a local environment using task dev:reset
  4. Import the database by running task dev:restore:database
"},{"location":"dpl-cms/local-development/#copy-files-from-lagoon-environment-to-local-setup","title":"Copy files from Lagoon environment to local setup","text":"

Prerequisites:

  • Login credentials to the Lagoon UI, or an existing nginx files dump

The following describes how to first fetch a files backup package and then replace the files in a local environment.

If you need to get new backup files from the remote site:

  1. Login to the lagoon administration and navigate to the project/environment.
  2. Select the backup tab:

  1. Retrieve the files backup you need:

4. Due to a UI bug you need to RELOAD the window and then it should be possible to download the nginx package.

Replace files locally:

  1. Place the files dump in the files-backup directory, be aware that the directory is only allowed to contain a single .tar.gz file.
  2. Start a local environment using task dev:reset
  3. Restore the files\u0161 by running task dev:restore:files
"},{"location":"dpl-cms/local-development/#get-a-specific-release-of-dpl-react-without-using-composer-install","title":"Get a specific release of dpl-react - without using composer install","text":"

In a development context it is not very handy only to be able to get the latest version of the main branch of dpl-react.

So a command has been implemented that downloads the specific version of the assets and overwrites the existing library.

You need to specify which branch you need to get the assets from. The latest HEAD of the given branch is automatically build by Github actions so you just need to specify the branch you want.

It is used like this:

BRANCH=[BRANCH_FROM_DPL_REACT_REPOSITORY] task dev:dpl-react:overwrite\n

Example:

BRANCH=feature/more-releases task dev:dpl-react:overwrite\n
"},{"location":"dpl-cms/translation/","title":"Translation","text":"

We manage translations as a part of the codebase using .po translation files. Consequently translations must be part of either official or local translations to take effect on the individual site.

DPL CMS is configured to use English as the master language but is configured to use Danish for all users through language negotiation. This allows us to follow a process where English is the default for the codebase but actual usage of the system is in Danish.

"},{"location":"dpl-cms/translation/#translation-system","title":"Translation system","text":"

To make the \"translation traffic\" work following components are being used:

  • GitHub
  • Stores .po files in git with translatable strings and translations
  • GitHub Actions
  • Scans codebase for new translatable strings and commits them to GitHub
  • Notifies POEditor that new translatable strings are available
  • Publishes .po files to GitHub Pages
  • POEditor
  • Provides an interface for translators
  • Links translations with .po files on GitHub
  • Provides webhooks where external systems can notify of new translations
  • DPL CMS
  • Drupal installation which is configured to use GitHub Pages as an interface translation server from which .po files can be consumed.

The following diagram show how these systems interact to support the flow of from introducing a new translateable string in the codebase to DPL CMS consuming an updated translation with said string.

case

sequenceDiagram\n  Actor Translator\n  Actor Developer\n  Developer ->> Developer: Open pull request with new translatable string\n  Developer ->> GitHubActions: Merge pull request into develop\n  GitHubActions ->> GitHubActions: Scan codebase and write strings to .po file\n  GitHubActions ->> GitHubActions: Fill .po file with existing translations\n  GitHubActions ->> GitHub: Commit .po file with updated strings\n  GitHubActions ->> Poeditor: Call webhook\n  Poeditor ->> GitHub: Fetch updated .po file\n  Poeditor ->> Poeditor: Synchronize translations with latest strings and translations\n  Translator ->> Poeditor: Translate strings\n  Translator ->> Poeditor: Export strings to GitHub\n  Poeditor ->> GitHub: Commit .po file with updated translations to develop\n  DplCms ->> GitHub: Fetch .po file with latest translations\n  DplCms ->> DplCms: Import updated translations
"},{"location":"dpl-cms/translation/#howtos","title":"Howtos","text":""},{"location":"dpl-cms/translation/#add-new-or-update-existing-translation","title":"Add new or update existing translation","text":"
  1. Log into POEditor.com and go to the dpl-cms project
  2. Go to the relevant language
  3. Locate the string (term) to be translated
  4. Translate the string
"},{"location":"dpl-cms/translation/#publish-updated-translations","title":"Publish updated translations","text":"
  1. Log into POEditor.com
  2. Select the \"Settings\" tab
  3. Click the GitHub code hosting service
  4. Check the relevant language(s)
  5. Select \"Export to GitHub\" and click \"Go\"
"},{"location":"dpl-cms/translation/#import-updated-translations","title":"Import updated translations","text":"
  1. Run drush locale-check
  2. Run drush locale-update
"},{"location":"dpl-cms/architecture/adr-001-configuration-management/","title":"Architecture Decision Record: Configuration Management","text":""},{"location":"dpl-cms/architecture/adr-001-configuration-management/#context","title":"Context","text":"

Configuration management for DPL CMS is a complex issue. The complexity stems from different types of DPL CMS sites.

There are two approaches to the problem:

  1. All configuration is local unless explicitly marked as core configuration
  2. All configuration is core unless explicitly marked as local configuration

A solution to configuration management must live up to the following test:

  1. Initialize a local environment to represent a site
  2. Import the provided configuration through site installation using drush site-install --existing-config -y
  3. Log in to see that Core configuration is imported. This can be verified if the site name is set to DPL CMS.
  4. Change a Core configuration value e.g. on http://dpl-cms.docker/admin/config/development/performance
  5. Run drush config-import -y and see that the change is rolled back and the configuration value is back to default. This shows that Core configuration will remain managed by the configuration system.
  6. Change a local configuration value like the site name on http://dpl-cms.docker/admin/config/system/site-information
  7. Run drush config-import -y to see that no configuration is imported. This shows that local configuration which can be managed by Editor libraries will be left unchanged.
  8. Enable and configure the Shortcut module and add a new Shortcut set.
  9. Run drush config-import -y to see that the module is not disabled and the configuration remains unchanged. This shows that local configuration in the form of new modules added by Webmaster libraries will be left unchanged.
"},{"location":"dpl-cms/architecture/adr-001-configuration-management/#decision","title":"Decision","text":"

We use the Configuration Ignore module to manage configuration.

The module maintains a list of patterns for configuration which will be ignored during the configuration import process. This allows us to avoid updating local configuration.

By adding the wildcard * at the top of this list we choose an approach where all configuration is considered local by default.

Core configuration which should not be ignored can then be added to subsequent lines with the ~ which prefix. On a site these configuration entries will be updated to match what is in the core configuration.

Config Ignore also has the option of ignoring specific values within settings. This is relevant for settings such as system.site where we consider the site name local configuration but 404 page paths core configuration.

"},{"location":"dpl-cms/architecture/adr-001-configuration-management/#alternatives-considered","title":"Alternatives considered","text":""},{"location":"dpl-cms/architecture/adr-001-configuration-management/#deconfig-partial-imports","title":"Deconfig + Partial Imports","text":"

The Deconfig module allows developers to mark configuration entries as exempt from import/export. This would allow us to exempt configuration which can be managed by the library.

This does not handle configuration coming from new modules uploaded on webmaster sites. Since we cannot know which configuration entities such modules will provide and Deconfig has no concept of wildcards we cannot exempt the configuration from these modules. Their configuration will be removed again at deployment.

We could use partial imports through drush config-import --partial to not remove configuration which is not present in the configuration filesystem.

We prefer Config Ignore as it provides a single solution to handle the entire problem space.

"},{"location":"dpl-cms/architecture/adr-001-configuration-management/#config-ignore-auto","title":"Config Ignore Auto","text":"

The Config Ignore Auto module extends the Config Ignore module. Config Ignore Auto registers configuration changes and adds them to an ignore list. This way they are not overridden on future deployments.

The module is based on the assumption that if an user has access to a configuration form they should also be allowed to modify that configuration for their site.

This turns the approach from Config Ignore on its head. All configuration is now considered core until it is changed on the individual site.

We prefer Config Ignore as it only has local configuration which may vary between sites. With Config Ignore Auto we would have local configuration and the configuration of Config Ignore Auto.

Config Ignore Auto also have special handling of the core.extensions configuration which manages the set of installed modules. Since webmaster sites can have additional modules installed we would need workarounds to handle these.

"},{"location":"dpl-cms/architecture/adr-001-configuration-management/#config-split","title":"Config Split","text":"

The Config Split module allows developers to split configurations into multiple groups called settings.

This would allow us to map the different types of configuration to different settings.

We have not been able to configure this module in a meaningful way which also passed the provided test.

"},{"location":"dpl-cms/architecture/adr-001-configuration-management/#consequences","title":"Consequences","text":"
  • Core developers will have to explicitly select new configuration to not ignore during the development process. One can not simply run drush config-export and have the appropriate configuration not ignored.
  • Because core.extension is ignored Core developers will have to explicitly enable and uninstall modules through code as a part of the development process.
"},{"location":"dpl-cms/architecture/adr-002-user-handling/","title":"Architecture Decision Record: User Handling","text":""},{"location":"dpl-cms/architecture/adr-002-user-handling/#context","title":"Context","text":"

There are different types of users that are interacticting with the CMS system:

  • Patrons that is authenticated by logging into Adgangsplatformen.
  • Editors and administrators (and similar roles) that are are handling content and configuration of the site.

We need to be able to handle that both type of users can be authenticated and authorized in the scope of permissions that are tied to the user type.

We had some discussions wether the Adgangsplatform users should be tied to a Drupal user or not. As we saw it we had two options when a user logs in:

  1. Keep session/access token client side in the browser and not creating a Drupal user.
  2. Create a Drupal user and map the user with the external user.
"},{"location":"dpl-cms/architecture/adr-002-user-handling/#decision","title":"Decision","text":"

We ended up with desicion no. 2 mentioned above. So we create a Drupal user upon login if it is not existing already.

We use the OpeOpenID Connect / OAuth client module to manage patron authentication and authorization. And we have developed a plugin for the module called: Adgangsplatformen which connects the external oauth service with dpl-cms.

Editors and administrators a.k.a normal Drupal users and does not require additional handling.

"},{"location":"dpl-cms/architecture/adr-002-user-handling/#consequences","title":"Consequences","text":"
  • By having a Drupal user tied to the external user we can use that context and make the server side rendering show different content according to the authenticated user.
  • Adgangsplatform settings have to be configured in the plugin in order to work.
"},{"location":"dpl-cms/architecture/adr-002-user-handling/#future-considerations","title":"Future considerations","text":"

Instead of creating a new user for every single user logging in via Adgangsplatformen you could consider having just one Drupal user for all the external users. That would get rid of the UUID -> Drupal user id mapping that has been implemented as it is now. And it would prevent creation of a lot of users. The decision depends on if it is necessary to distinguish between the different users on a server side level.

"},{"location":"dpl-cms/architecture/adr-003-ddb-react-integration/","title":"Architecture Decision Record: DPL React integration","text":""},{"location":"dpl-cms/architecture/adr-003-ddb-react-integration/#context","title":"Context","text":"

The DPL React components needs to be integrated and available for rendering in Drupal. The components are depending on a library token and an access token being set in javascript.

"},{"location":"dpl-cms/architecture/adr-003-ddb-react-integration/#decision","title":"Decision","text":"

We decided to download the components with composer and integrate them as Drupal libraries.

As described in adr-002-user-handling we are setting an access token in the user session when a user has been through a succesful login at Adgangsplatformen.

We decided that the library token is fetched by a cron job on a regular basis and saved in a KeyValueExpirable store which automatically expires the token when it is outdated.

The library token and the access token are set in javascript on the endpoint: /dpl-react/user.js. By loading the script asynchronically when mounting the components i javascript we are able to complete the rendering.

"},{"location":"dpl-cms/architecture/adr-004-ddb-react-caching/","title":"Architecture Decision Record: Caching of DPL React and other js resources","text":""},{"location":"dpl-cms/architecture/adr-004-ddb-react-caching/#context","title":"Context","text":"

The general caching strategy is defined in another document and this focused on describing the caching strategy of DPL react and other js resources.

We need to have a caching strategy that makes sure that:

  • The js files defined as Drupal libraries (which DPL react is) and pages that make use of them are being cached.
  • The same cache is being flushed upon deploy because that is the moment where new versions of DPL React can be introduced.
"},{"location":"dpl-cms/architecture/adr-004-ddb-react-caching/#decision","title":"Decision","text":"

We have created a purger in the Drupal Varnish/Purge setup that is able to purge everything. The purger is being used in the deploy routine by the command: drush cache:rebuild-external -y

"},{"location":"dpl-cms/architecture/adr-004-ddb-react-caching/#consequences","title":"Consequences","text":"
  • Everything will be invalidated on every deploy. Note: Although we are sending a PURGE request we found out, by studing the vcl of Lagoon, that the PURGE request actually is being translated into a BAN on req.url.
"},{"location":"dpl-cms/architecture/adr-005-api-mocking/","title":"Architecture Decision Record: API mocking","text":""},{"location":"dpl-cms/architecture/adr-005-api-mocking/#context","title":"Context","text":"

DPL CMS integrates with a range of other business systems through APIs. These APIs are called both clientside (from Browsers) and serverside (from Drupal/PHP).

Historically these systems have provided setups accessible from automated testing environments. Two factors make this approach problematic going forward:

  1. In the future not all systems are guaranteed to provide such environments with useful data.
  2. Test systems have not been as stable as is necessary for automated testing. Systems may be down or data updated which cause problems.

To address these problems and help achieve a goal of a high degree of test coverage the project needs a way to decouple from these external APIs during testing.

"},{"location":"dpl-cms/architecture/adr-005-api-mocking/#decision","title":"Decision","text":"

We use WireMock to mock API calls. Wiremock provides the following feature relevant to the project:

  • Wiremock is free open source software which can be deployed in development and tests environment using Docker
  • Wiremock can run in HTTP(S) proxy mode. This allows us to run a single instance and mock requests to all external APIs
  • We can use the wiremock-php client library to instrument WireMock from PHP code. We modernized the behat-wiremock-extension to instrument with Behat tests which we use for integration testing.
"},{"location":"dpl-cms/architecture/adr-005-api-mocking/#instrumentation-vs-recordreplay","title":"Instrumentation vs. record/replay","text":"

Software for mocking API requests generally provide two approaches:

  • Instrumentation where an API can be used to define which responses will be returned for what requests programmatically.
  • Record/replay where requests passing through are persisted (typically to the filesystem) and can be modified and restored at a later point in time.

Generally record/replay makes it easy to setup a lot of mock data quickly. However, it can be hard to maintain these records as it is not obvious what part of the data is important for the test and the relationship between the individual tests and the corresponding data is hard to determine.

Consequently, this project prefers instrumentation.

"},{"location":"dpl-cms/architecture/adr-005-api-mocking/#alternatives-considered","title":"Alternatives considered","text":"

There are many other tools which provide features similar to Wiremock. These include:

  • Hoverfly: FOSS, Docker image and proxy support. PHP clients are less mature and no Behat integration.
  • Mountebank: FOSS and Docker image. No proxy support, PHP client is less mature and no Behat integration.
  • MockServer: FOSS, Docker image and proxy support. No PHP client and no Behat integration.
  • Mockoon: FOSS and Docker image. Does not provide instrumentation.
"},{"location":"dpl-cms/architecture/adr-005-api-mocking/#consequences","title":"Consequences","text":"
  • Developers may have to engage in maintenance of the wiremock-php and behat-wiremock-extension library
"},{"location":"dpl-cms/architecture/adr-005-api-mocking/#status","title":"Status","text":"

Instrumentation of Wiremock with PHP is made obsolete with the migration from Behat to Cypress.

"},{"location":"dpl-cms/architecture/adr-006-api-specification/","title":"Architecture Decision Record: API specification","text":""},{"location":"dpl-cms/architecture/adr-006-api-specification/#context","title":"Context","text":"

DPL CMS provides HTTP end points which are consumed by the React components. We want to document these in an established structured format.

Documenting endpoints in a established structured format allows us to use tools to generate client code for these end points. This makes consumption easier and is a practice which is already used with other services in the React components.

Currently these end points expose business logic tied to configuration in the CMS. There might be a future where we also need to expose editorial content through APIs.

"},{"location":"dpl-cms/architecture/adr-006-api-specification/#decision","title":"Decision","text":"

We use the RESTful Web Services Drupal module to expose an API from DPL CMS and document the API using the OpenAPI 2.0/Swagger 2.0 specification as supported by the OpenAPI and OpenAPI REST Drupal modules.

This is a technically manageable, standards compliant and performant solution which supports our initial use cases and can be expanded to support future needs.

"},{"location":"dpl-cms/architecture/adr-006-api-specification/#alternatives-considered","title":"Alternatives considered","text":"

There are two other approaches to working with APIs and specifications for Drupal:

  • JSON:API: Drupals JSON:API module provides many features over the REST module when it comes to exposing editorial content (or Drupal entities in general). However it does not work well with other types of functionality which is what we need for our initial use cases.
  • GraphQL: GraphQL is an approach which does not work well with Drupals HTTP based caching layer. This is important for endpoints which are called many times for each client. Also from version 4.x and beyond the GraphQL Drupal module provides no easy way for us to expose editorial content at a later point in time.
"},{"location":"dpl-cms/architecture/adr-006-api-specification/#consequences","title":"Consequences","text":"
  • This is an automatically generated API and specification. To avoid other changes leading to unintended changes this we keep the latest version of the specification in VCS and setup automations to ensure that the generated specification matches the inteded one. When developers update the API they have to use the provided tasks to update the stored API accordingly.
  • OpenAPI and OpenAPI REST are Drupal modules which have not seen updates for a while. We have to apply patches to get them to work for us. Also they do not support the latest version of the OpenAPI specification, 3.0. We risk increased maintenance because of this.
"},{"location":"dpl-cms/architecture/adr-007-cypress-functional-testing/","title":"Architecture Decision Record: Cypress for functional testing","text":""},{"location":"dpl-cms/architecture/adr-007-cypress-functional-testing/#context","title":"Context","text":"

DPL CMS employs functional testing to ensure the functional integrity of the project.

This is currently implemented using Behat which allows developers to instrument a browser navigating through different use cases using Gherkin, a business readable, domain specific language. Behat is used within the project based on experience using it from the previous generation of DPL CMS.

Several factors have caused us to reevaluate that decision:

  • The Drupal community has introduced Nightwatch for browser testing
  • Usage of Behat within the Drupal community has stagnated
  • Developers within this project have questioned the developer experience and future maintenance of Behat
  • Developers have gained experience using Cypress for browser based testing of the React components
"},{"location":"dpl-cms/architecture/adr-007-cypress-functional-testing/#decision","title":"Decision","text":"

We choose to replace Behat with Cypress for functional testing.

"},{"location":"dpl-cms/architecture/adr-007-cypress-functional-testing/#alternatives-considered","title":"Alternatives considered","text":"

There are other prominent tools which can be used for browser based functional testing:

  • Playwright: Playwright is a promising tool for browser based testing. It supports many desktop and mobile browsers. It does not have the same widespread usage as Cypress.
"},{"location":"dpl-cms/architecture/adr-007-cypress-functional-testing/#consequences","title":"Consequences","text":"
  • Although Cypress supports intercepting requests to external systems this only works for clientside requests. To maintain a consistent approach to mocking both serverside and clientside requests to external systems we integrate Cypress with Wiremock using a similar approach to what we have done with Behat.
  • There is a community developed module which integrates Drupal with Cypress. We choose not to use this as it provided limited value to our use case and we prefer to avoid increased complexity.
  • We will not only be able to test on mobile browsers as this is not supported by Cypress. We prefer consistency across projects and expected improved developer efficiency over what we expect to be improved complexity of introducing a tool supporting this natively or expanding Cypress setup to support mobile testing.
  • We opt not to use Gherkin to describe our test cases. The business has decided that this has not provided sufficient value for the existing project that the additional complexity is not needed. Cypress community plugins support writing tests in Gherkin. These could be used in the future.
"},{"location":"dpl-cms/architecture/adr-008-external-system-integration/","title":"Architecture Decision Record: Integration with external systems","text":""},{"location":"dpl-cms/architecture/adr-008-external-system-integration/#context","title":"Context","text":"

DPL CMS is only intended to integrate with one external system: Adgangsplatformen. This integration is necessary to obtain patron and library tokens needed for authentication with other business systems. All these integrations should occur in the browser through React components.

The purpose of this is to avoid having data passing through the CMS as an intermediary. This way the CMS avoids storing or transmitting sensitive data. It may also improve performance.

In some situations it may be beneficiary to let the CMS access external systems to provide a better experience for business users e.g. by displaying options with understandable names instead of technical ids or validating data before it reaches end users.

"},{"location":"dpl-cms/architecture/adr-008-external-system-integration/#decision","title":"Decision","text":"

We choose to allow CMS to access external systems server-side using PHP. This must be done on behalf of the library - never the patron.

"},{"location":"dpl-cms/architecture/adr-008-external-system-integration/#alternatives-considered","title":"Alternatives considered","text":"
  • Implementing React components to provide administrative controls in the CMS. This would increase the complexity of implementing such controls and cause implementors to not consider improvements to the business user experience.
"},{"location":"dpl-cms/architecture/adr-008-external-system-integration/#consequences","title":"Consequences","text":"
  • We allow PHP client code generation for external services. These should not only include APIs to be used with library tokens. This signals what APIs are OK to be accessed server-side.
  • The CMS must only access services using the library token provided by the dpl_library_token.handler service.
"},{"location":"dpl-cms/architecture/adr-009-translation-system/","title":"Architecture Decision Record: Translation system","text":""},{"location":"dpl-cms/architecture/adr-009-translation-system/#context","title":"Context","text":"

The current translation system for UI strings in DPL CMS is based solely on code deployment of .po files.

However DPL CMS is expected to be deployed in about 100 instances just to cover the Danish Public Library institutions. Making small changes to the UI texts in the codebase would require a new deployment for each of the instances.

Requiring code changes to update translations also makes it difficult for non-technical participants to manage the process themselves. They have to find a suitable tool to edit .po files and then pass the updated files to a developer.

This process could be optimized if:

  1. Translations were provided by a central source
  2. Translations could be managed directly by non-technical users
  3. Distribution of translations is decoupled from deployment
"},{"location":"dpl-cms/architecture/adr-009-translation-system/#decision","title":"Decision","text":"

We keep using GitHub as a central source for translation files.

We configure Drupal to consume translations from GitHub. The Drupal translation system already supports runtime updates and consuming translations from a remote source.

We use POEditor to perform translations. POEditor is a translation management tool that supports .po files and integrates with GitHub. To detect new UI strings a GitHub Actions workflow scans the codebase for new strings and notifies POEditor. Here they can be translated by non-technical users. POEditor supports committing translations back to GitHub where they can be consumed by DPL CMS instances.

"},{"location":"dpl-cms/architecture/adr-009-translation-system/#consequences","title":"Consequences","text":"

This approach has a number of benefits apart from addressing the original issue:

  • POEditor is a specialized tool to manage translations. It supports features such as translation memory, glossaries and machine translation.
  • POEditor is web-based. Translators avoid having to find and install a suitable tool to edit .po files.
  • POEditor is software-as-a-service. We do not need to maintain the translation interface ourselves.
  • POEditor is free for open source projects. This means that we can use it without having to pay for a license.
  • Code scanning means that new UI strings are automatically detected and available for translation. We do not have to manually synchronize translation files or ensure that UI strings are rendered by the system before they can be translated. This can be complex when working with special cases, error messages etc.
  • Translations are stored in version control. Managing state is complex and this means that we have easy visibility into changes.
  • Translations are stored on GitHub. We can move away from POEditor at any time and still have access to all translations.
  • We reuse existing systems instead of building our own.

A consequence of this approach is that developers have to write code that supports scanning. This is partly supported by the Drupal Code Standards. To support contexts developers also have to include these as a part of the t() function call e.g.

// Good\n$this->t('A string to be translated', [], ['context' => 'The context']);\n$this->t('Another string', [], ['context' => 'The context']);\n// Bad\n$c = ['context' => 'The context']\n$this->t('A string to be translated', [], $c);\n$this->t('Another string', [], $c);\n

We could consider writing a custom sniff or PHPStan rule to enforce this

"},{"location":"dpl-cms/architecture/adr-009-translation-system/#potion","title":"Potion","text":"

For covering the functionality of scanning the code we had two potential projects that could solve the case:

  • Potion
  • Potx

Both projects can scan the codebase and generate a .po or .pot file with the translation strings and context.

At first it made most sense to go for Potx since it is used by localize.drupal.org and it has a long history. But Potx is extracting strings to a .pot file without having the possibility of filling in the existing translations. So we ended up using Potion which can fill in the existing strings.

A flip side using Potion is that it is not being maintained anymore. But it seems quite stable and a lot of work has been put into it. We could consider to back it ourselves.

"},{"location":"dpl-cms/architecture/adr-009-translation-system/#alternatives-considered","title":"Alternatives considered","text":"

We considered the following alternatives:

  1. Establishing our own localization server. This proved to be very complex. Available solutions are either technically outdated or still under heavy development. None of them have integration with GitHub where our project is located.
  2. Using a separate instance of DPL CMS in Lagoon as a central translation hub. Such an instance would require maintenance and we would have to implement a method for exposing translations to other instances.
"},{"location":"dpl-design-system/","title":"DPL Design System","text":"

DPL Design System is a library of UI components that should be used as a common base system for \"Danmarks Biblioteker\" / \"Det Digitale Folkebibliotek\". The design is implemented with Storybook / React and is output with HTML markup and css-classes through an addon in Storybook.

The codebase follows the naming that designers have used in Figma closely to ensure consistency.

"},{"location":"dpl-design-system/#requirements","title":"Requirements","text":"

This project comes with go-task and docker compose, hence the requirements are limited to having docker install and tasks.

"},{"location":"dpl-design-system/#manual-requirements","title":"Manual requirements","text":"

This project can be used outside docker with the following requirements:

  • node 16
  • yarn

Check in the terminal which versions you have installed with node -v.

"},{"location":"dpl-design-system/#installation","title":"Installation","text":"

Use the tasks defined in Taskfile to run the project:

task dev:install\n
"},{"location":"dpl-design-system/#installation-outside-docker","title":"Installation outside docker","text":"

Use the node package manager to install project dependencies:

yarn install\n
"},{"location":"dpl-design-system/#development","title":"Development","text":"

To start the docker compose setup in development simple use the start task:

task dev:start\n

To see the output from the compile process and start of storybook:

task dev:logs\n

Use task and tabulator key in the terminal to see the other predefined tasks:

task dev:[TAB]\n
"},{"location":"dpl-design-system/#development-without-docker","title":"Development without docker","text":"

To start developing run:

yarn dev\n

Components and CSS will be automatically recompiled when making changes in the source code.

"},{"location":"dpl-design-system/#usage","title":"Usage","text":"

The project is available in two ways and should be consumed accordingly:

  1. As package in the local npm registry for this repository
  2. As a dist.zip file attached to a release for this repository

Both releases contain the built assets of the project: JavaScript files, CSS styles and icons.

You can find the HTML output for a given story under the HTML tab inside storybook.

"},{"location":"dpl-design-system/#npm-package","title":"NPM package","text":"

The GitHub NPM package registry requires authentication if you are to access packages there.

Consequently, if you want to use the design system as an NPM package or if you use a project that depends on the design system as an NPM package you must authenticate:

  1. Create a GitHub token with the required scopes: repo and read:packages
  2. Run npm login --registry=https://npm.pkg.github.com
  3. Enter the following information:
> Username: [Your GitHub username]\n> Password: [Your GitHub token]\n> Email: [An email address used with your GitHub account]\n

Note that you will need to reauthenticate when your personal access token expires.

"},{"location":"dpl-design-system/#deployment-and-releases","title":"Deployment and releases","text":"

The project is automatically built and deployed on pushes to every branch and every tag and the result is available as releases which support both types of usage. This applies for the original repository on GitHub and all GitHub forks.

You can follow the status of deployments in the Actions list for the repository on GitHub. The action logs also contain additional details regarding the contents and publication of each release. If using a fork then deployment actions can be seen on the corresponding list.

In general consuming projects should prefer tagged releases as they are stable proper releases.

During development where the design system is being updated in parallel with the implementation of a consuming project it may be advantageous to use a release tagging a branch.

"},{"location":"dpl-design-system/#tagged-releases","title":"Tagged releases","text":"

Run the following to publish a tag and create a release:

git tag -a v*.*.* && git push origin v*.*.*\n
"},{"location":"dpl-design-system/#usage-npm-package","title":"Usage: npm package","text":"

In the consuming project update usage to the new release:

npm install @danskernesdigitalebibliotek/dpl-design-system@*.*.*\n
"},{"location":"dpl-design-system/#usage-release-file","title":"Usage: Release file","text":"

Find the release for the tag on the releases page on GitHub and download the dist.zip file from there and use it as needed in the consuming project.

"},{"location":"dpl-design-system/#branch-releases","title":"Branch releases","text":"

The project automatically creates a release for each branch.

Example: Pushing a commit to a new branch feature/reservation-modal will create the following parts:

  1. A git tag for the commit release-feature/reservation-modal. A tag is needed to create a GitHub release.
  2. A GitHub release for the called feature/reservation-modal. The build is attached here.
  3. A package in the local npm repository tagged feature-reservation-modal. Special characters like / are not supported by npm tags and are converted to -.

Updating the branch will update all parts accordingly.

"},{"location":"dpl-design-system/#usage-npm-package_1","title":"Usage: npm package","text":"

In the consuming project update usage to the new release:

npm install @danskernesdigitalebibliotek/dpl-design-system@feature-reservation-modal\n

If your release belongs to a fork you can use aliasing to point to the release of the package in the npm repository for the fork:

npm config set @my-fork:registry=https://npm.pkg.github.com\nnpm install @danskernesdigitalebibliotek/dpl-design-system@npm:@my-fork/dpl-design-system@feature-reservation-modal\n

This will update your package.json and lock files accordingly. Note that branch releases use temporary versions in the format 0.0.0-[GIT-SHA] and you may in practice see these referenced in both files.

If you push new code to the branch you have to update the version used in the consuming project:

npm update @danskernesdigitalebibliotek/dpl-design-system\n

Aliasing, repository configuration and updating installed packages are also supported by Yarn.

"},{"location":"dpl-design-system/#usage-release-file_1","title":"Usage: Release file","text":"

Find the release for the branch on the releases page on GitHub and download the dist.zip file from there and use it as needed in the consuming project.

If your branch belongs to a fork then you can find the release on the releases page for the fork.

Repeat the process if you push new code to the branch.

"},{"location":"dpl-design-system/#storybook","title":"Storybook","text":"

Spin up storybook by running this command in the terminal:

yarn storybook\n

When storybook is ready it automatically opens up in a browser with the interface ready to use.

"},{"location":"dpl-design-system/#chromatic","title":"Chromatic","text":"

We are using Chromatic for visual test. You can access the dashboard under the danskernesdigitalebibliotek (organisation) dpl-design-system (project).

https://www.chromatic.com/builds?appId=616ffdab9acbf5003ad5fd2b

You can deploy a version locally to Chromatic by running:

yarn chromatic\n

Make sure to set the CHROMATIC_PROJECT_TOKEN environment variable is available in your shell context. You can access the token from:

https://www.chromatic.com/manage?appId=616ffdab9acbf5003ad5fd2b&view=configure

"},{"location":"dpl-design-system/#what-is-storybook","title":"What is Storybook","text":"

Storybook is an open source tool for building UI components and pages in isolation from your app's business logic, data, and context. Storybook helps you document components for reuse and automatically visually test your components to prevent bugs. It promotes the component-driven process and agile development.

It is possible to extend Storybook with an ecosystem of addons that help you do things like fine-tune responsive layouts or verify accessibility.

"},{"location":"dpl-design-system/#how-to-use","title":"How to use","text":"

The Storybook interface is simple and intuitive to use. Browse the project's stories now by navigating to them in the sidebar.

The stories are placed in a flat structure, where developers should not spend time thinking of structure, since we want to keep all parts of the system under a heading called Library. This Library is then dividid in folders where common parts are kept together.

To expose to the user how we think these parts stitch together for example for the new website, we have a heading called Blocks, to resemble what cms blocks a user can expect to find when building pages in the choosen CMS.

This could replicate in to mobile applications, newsletters etc. all pulling parts from the Library.

Each story has a corresponding .stories file. View their code in the src/stories directory to learn how they work. The stories file is used to add the component to the Storybook interface via the title. Start the title with \"Library\" or \"Blocks\" and use / to divide into folders fx. Library / Buttons / Button

"},{"location":"dpl-design-system/#addons","title":"Addons","text":"

Storybook ships with some essential pre-installed addons to power the core Storybook experience.

  • Controls
  • Actions
  • Docs
  • Viewport
  • Backgrounds
  • Toolbars
  • Measure
  • Outline

There are many other helpful addons to customise the usage and experience. Additional addons used for this project:

  • HTML / storybook-addon-html: This addon is used to display compiled HTML markup for each story and make it easier for developers to grab the code. Because we are developing with React, it is necessary to be able to show the HTML markup with the css-classes to make it easier for other developers that will implement it in the future. If a story has controls the HTML markup changes according to the controls that are set.

  • Designs / storybook-addon-designs: This addon is used to embed Figma in the addon panel for a better design-development workflow.

  • A11Y: This addon is used to check the accessibility of the components.

All the addons can be found in storybook/main.js directory.

"},{"location":"dpl-design-system/#important-to-notice","title":"Important to notice","text":""},{"location":"dpl-design-system/#internal-classes","title":"Internal classes","text":"

To display some components (fx Colors, Spacing) in a more presentable way, we are using some \"internal\" css-classes which can be found in the styles/internal.scss file. All css-classes with \"internal\" in the front should therefore be ignored in the HTML markup.

"},{"location":"dpl-design-system/architecture/adr-001-skeleton-screens/","title":"Architecture Decision Record: Skeleton Screens","text":""},{"location":"dpl-design-system/architecture/adr-001-skeleton-screens/#context","title":"Context","text":"

In the work of trying to improve the performance of the search results we needed a way to fill the viewport with a simulated interface in order to:

  • Show some content immediately to the user
  • Prevent layout shifting between loading state and ready state
"},{"location":"dpl-design-system/architecture/adr-001-skeleton-screens/#decision","title":"Decision","text":"

We decided to implement skeleton screens when loading data. The skeleton screens are rendered in pure css. The css classes are coming from the library: skeleton-screen-css

"},{"location":"dpl-design-system/architecture/adr-001-skeleton-screens/#alternatives-considered","title":"Alternatives considered","text":"

The library is very small and based on simple css rules, so we could have considered replicating it in our own design system or make something similar. But by using the open source library we are ensured, to a certain extent, that the code is being maintained, corrected and evolves as time goes by.

We could also have chosen to use images or GIF's to render the screens. But by using the simple toolbox of skeleton-screen-css we should be able to make screens for all the different use cases in the different apps.

"},{"location":"dpl-design-system/architecture/adr-001-skeleton-screens/#consequences","title":"Consequences","text":"

It is now possible, with a limited amount of work, to construct skeleton screens in the loading state of the various user interfaces.

Because we use library where skeletons are implemented purely in CSS we also provide a solution which can be consumed in any technology already using the design system without any additional dependencies, client side or server side.

"},{"location":"dpl-design-system/architecture/adr-001-skeleton-screens/#bem-rules-when-using-skeleton-screen-classes-in-dpl-design-system","title":"BEM rules when using Skeleton Screen Classes in dpl-design-system","text":"

Because we want to use existing styling setup in conjunction with the Skeleton Screen Classes we sometimes need to ignore the existing BEM rules that we normally comply to. See eg. the search result styling.

"},{"location":"dpl-platform/","title":"DPL Platform Documentation","text":"

This directory contains the documentation of the DPL Platforms architecture and overall concepts.

Documentation of how to use the various sub-components of the project can be found in READMEs in the respective components directory.

"},{"location":"dpl-platform/#table-of-contents","title":"Table of contents","text":"
  • Architecture Contains the documentation of the platforms architecture.
  • Backup How backups of sites are handled.
  • Current Platform Environments Describes the current operational environments.
"},{"location":"dpl-platform/backup/","title":"Backup","text":""},{"location":"dpl-platform/backup/#site-backup-configuration","title":"Site backup configuration","text":"

We configure all production backups with a backup schedule that ensure that the site is backed up at least once a day.

Backups executed by the k8up operator follows a backup schedule and then uses Restic to perform the backup itself. The backups are stored in a Azure Blob Container, see the Environment infrastructure for a depiction of its place in the architecture.

The backup schedule and retention is configured via the individual sites .lagoon.yml. The file is re-rendered from a template every time the a site is deployed. The templates for the different site types can be found as a part of dpladm.

Refer to the lagoon documentation on backups for more general information.

Refer to any runbooks relevant to backups for operational instructions on eg. retrieving a backup.

"},{"location":"dpl-platform/code-guidelines/","title":"Code guidelines","text":"

The following guidelines describe best practices for developing code for the DPL Platform project. The guidelines should help achieve:

  • A stable, secure and high quality foundation for building and maintaining the platform and its infrastructure.
  • Consistency across multiple developers participating in the project

Contributions to the core DPL Platform project will be reviewed by members of the Core team. These guidelines should inform contributors about what to expect in such a review. If a review comment cannot be traced back to one of these guidelines it indicates that the guidelines should be updated to ensure transparency.

"},{"location":"dpl-platform/code-guidelines/#coding-standards","title":"Coding standards","text":"

The project follows the Drupal Coding Standards and best practices for all parts of the project: PHP, JavaScript and CSS. This makes the project recognizable for developers with experience from other Drupal projects. All developers are expected to make themselves familiar with these standards.

The following lists significant areas where the project either intentionally expands or deviates from the official standards or areas which developers should be especially aware of.

"},{"location":"dpl-platform/code-guidelines/#general","title":"General","text":"
  • The default language for all code and comments is English.
"},{"location":"dpl-platform/code-guidelines/#shell-scripts","title":"Shell scripts","text":"
  • Shell-scripts must pass a shellcheck validation
"},{"location":"dpl-platform/code-guidelines/#terraform","title":"Terraform","text":"
  • Any Terraform HCL must be formatted to match the format required by terraform fmt
  • Terraform configuration should be organized into submodules instantiated by root modules.
"},{"location":"dpl-platform/code-guidelines/#markdown","title":"Markdown","text":"
  • Markdown must pass validation by markdownlint
"},{"location":"dpl-platform/code-guidelines/#code-comments","title":"Code comments","text":"

Code comments which describe what an implementation does should only be used for complex implementations usually consisting of multiple loops, conditional statements etc.

Inline code comments should focus on why an unusual implementation has been implemented the way it is. This may include references to such things as business requirements, odd system behavior or browser inconsistencies.

"},{"location":"dpl-platform/code-guidelines/#commit-messages","title":"Commit messages","text":"

Commit messages in the version control system help all developers understand the current state of the code base, how it has evolved and the context of each change. This is especially important for a project which is expected to have a long lifetime.

Commit messages must follow these guidelines:

  1. Each line must not be more than 72 characters long
  2. The first line of your commit message (the subject) must contain a short summary of the change. The subject should be kept around 50 characters long.
  3. The subject must be followed by a blank line
  4. Subsequent lines (the body) should explain what you have changed and why the change is necessary. This provides context for other developers who have not been part of the development process. The larger the change the more description in the body is expected.
  5. If the commit is a result of an issue in a public issue tracker, platform.dandigbib.dk, then the subject must start with the issue number followed by a colon (:). If the commit is a result of a private issue tracker then the issue id must be kept in the commit body.

When creating a pull request the pull request description should not contain any information that is not already available in the commit messages.

Developers are encouraged to read How to Write a Git Commit Message by Chris Beams.

"},{"location":"dpl-platform/code-guidelines/#tool-support","title":"Tool support","text":"

The project aims to automate compliance checks as much as possible using static code analysis tools. This should make it easier for developers to check contributions before submitting them for review and thus make the review process easier.

The following tools pay a key part here:

  1. terraform fmt for standard Terraform formatting.
  2. markdownlint-cli2 for linting markdown files. The tool is configured via /.markdownlint-cli2.yaml
  3. ShellCheck with its default configuration.

In general all tools must be able to run locally. This allows developers to get quick feedback on their work.

Tools which provide automated fixes are preferred. This reduces the burden of keeping code compliant for developers.

Code which is to be exempt from these standards must be marked accordingly in the codebase - usually through inline comments (markdownlint, ShellCheck). This must also include a human readable reasoning. This ensures that deviations do not affect future analysis and the Core project should always pass through static analysis.

If there are discrepancies between the automated checks and the standards defined here then developers are encouraged to point this out so the automated checks or these standards can be updated accordingly.

"},{"location":"dpl-platform/platform-environments/","title":"Current Platform environments","text":""},{"location":"dpl-platform/platform-environments/#dplplat01","title":"dplplat01","text":""},{"location":"dpl-platform/platform-environments/#roots","title":"Roots","text":"
  • Environment repository Github Organisation: github.com/danishpubliclibraries
  • Azure Resource Group: rg-env-dplplat01
"},{"location":"dpl-platform/platform-environments/#urls","title":"URLs","text":"
  • Base domain: dplplat01.dpl.reload.dk
  • Grafana: https://grafana.lagoon.dplplat01.dpl.reload.dk/
  • Lagoon UI: https://ui.lagoon.dplplat01.dpl.reload.dk/
"},{"location":"dpl-platform/platform-environments/#lagoon-cli-configuration","title":"Lagoon CLI configuration","text":"
  • Lagoon name: dplplat01
  • Lagoon UI: https://ui.lagoon.dplplat01.dpl.reload.dk/
  • GraphQL endpoint: https://api.lagoon.dplplat01.dpl.reload.dk/graphql
  • SSH host: 20.238.147.183
  • SSH port: 22
"},{"location":"dpl-platform/platform-environments/#obtaining-lagoon-cli-configuration","title":"Obtaining Lagoon CLI configuration","text":"

See Connecting the Lagoon CLI

"},{"location":"dpl-platform/architecture/","title":"DPL Platform architecture documentation","text":"
  • Architecture Decision Records (ADR) describes the reasoning behind key decisions made during the design and implementation of the platforms architecture. These documents stands apart from the remaining documentation in that they keep a historical record, while the rest of the documentation is a snapshot of the current system.
  • Platform Environment Architecture gives an overview of the parts that makes up a single DPL Platform environment.
  • Performance strategy Describes the approach the platform takes to meet performance requirements.
"},{"location":"dpl-platform/architecture/alertmanager-setup/","title":"Alertmanager Setup","text":"

The We use the alertmanager automatically ties to the metrics of Prometheus but in order to make it work the configuration and rules need to be setup.

"},{"location":"dpl-platform/architecture/alertmanager-setup/#configuration","title":"Configuration","text":"

The configuration is stored in a secret:

kubectl get secret \\\n-n prometheus alertmanager-promstack-kube-prometheus-alertmanager -o yaml\n

In order to update the configuration you need to get the secret resource definition yaml output and retrieve the data.alertmanager.yaml property.

You need to base64 decode the value, update configuration with SMTP settings, receivers and so forth.

"},{"location":"dpl-platform/architecture/alertmanager-setup/#rules","title":"Rules","text":"

It is possible to set up various rules(thresholds), both on cluster level and for separate containers and namespaces.

Here is a site with examples of rules to get an idea of the possibilities.

"},{"location":"dpl-platform/architecture/alertmanager-setup/#test","title":"Test","text":"

We have tested the setup by making a configuration looking like this:

Get the configuration form the secret as described above.

Change it with smtp settings in order to be able to debug the alerts:

global:\n  resolve_timeout: 5m\n  smtp_smarthost: smtp.gmail.com:587\n  smtp_from: xxx@xxx.xx\n  smtp_auth_username: xxx@xxx.xx\n  smtp_auth_password: xxxx\nreceivers:\n- name: default\n- name: email-notification\n  email_configs:\n    - to: xxx@xxx.xx\nroute:\n  group_by:\n  - namespace\n  group_interval: 5m\n  group_wait: 30s\n  receiver: default\n  repeat_interval: 12h\n  routes:\n  - match:\n      alertname: testing\n    receiver: email-notification\n  - match:\n      severity: critical\n    receiver: email-notification\n

Base64 encode the configuration and update the secret with the new configuration hash.

Find the cluster ip of the alertmanager service running (the service name can possibly vary):

kubectl get svc -n prometheus promstack-kube-prometheus-alertmanager\n

And then run a curl command in the cluster (you need to find the IP o):

# 1.\nkubectl run -i --rm --tty debug --image=curlimages/curl --restart=Never -- sh\n\n# 2\ncurl -XPOST http://[ALERTMANAGER_SERVICE_CLUSTER_IP]:9093/api/v1/alerts \\\n-d '[{\"status\": \"firing\",\"labels\": {\"alertname\": \"testing\",\"service\": \"curl\",\\\n \"severity\": \"critical\",\"instance\": \"0\"},\"annotations\": {\"summary\": \\\n \"This is a summary\",\"description\": \"This is a description.\"},\"generatorURL\": \\\n \"http://prometheus.int.example.net/<generating_expression>\",\\\n \"startsAt\": \"2020-07-22T01:05:38+00:00\"}]'\n
"},{"location":"dpl-platform/architecture/performance-strategy/","title":"Performance strategy","text":"

The DPL-CMS Drupal sites utilizes a multi-tier caching strategy. HTTP responses are cached by Varnish and Drupal caches its various internal data-structures in a Redis key/value store.

"},{"location":"dpl-platform/architecture/performance-strategy/#the-request-path","title":"The request-path","text":"
  1. All inbound requests are passed in to an Ingress Nginx controller which forwards the traffic for the individual sites to their individual Varnish instances.
  2. Varnish serves up any static or anonymous responses it has cached from its object-store.
  3. If the request is cache miss the request is passed further on Nginx which serves any requests for static assets.
  4. If the request is for a dynamic page the request is forwarded to the Drupal- installation hosted by PHP-FPM.
  5. Drupal bootstraps, and produces the requested response.
  6. During this process it will either populate or reuse it cache which is stored in Redis.
  7. Depending on the request Drupal will execute a number of queries against MariaDB and a search index.
"},{"location":"dpl-platform/architecture/performance-strategy/#caching-of-http-responses","title":"Caching of http responses","text":"

Varnish will cache any http responses that fulfills the following requirements

  • Is not associated with a php-session (ie, the user is logged in)
  • Is a 200

Refer the Lagoon drupal.vcl, docs.lagoon.sh documentation on the Varnish service and the varnish-drupal image for the specifics on the service.

Refer to the caching documentation in dpl-cms for specifics on how DPL-CMS is integrated with Varnish.

"},{"location":"dpl-platform/architecture/performance-strategy/#redis-as-caching-backend","title":"Redis as caching backend","text":"

DPL-CMS is configured to use Redis as the backend for its core cache as an alternative to the default use of the sql-database as backend. This ensures that a busy site does not overload the shared mariadb-server.

"},{"location":"dpl-platform/architecture/platform-environment-architecture/","title":"A DPL Platform environment","text":"

A DPL Platform environment consists of a range of infrastructure components on top of which we run a managed Kubernetes instance into with we install a number of software product. One of these is Lagoon which gives us a platform for hosting library sites.

An environment is created in two separate stages. First all required infrastructure resources are provisioned, then a semi-automated deployment process carried out which configures all the various software-components that makes up an environment. Consult the relevant runbooks and the DPL Platform Infrastructure documents for the guides on how to perform the actual installation.

This document describes all the parts that makes up a platform environment raging from the infrastructure to the sites.

  • Azure Infrastructure describes the raw cloud infrastructure
  • Software Components describes the base software products we install to support the platform including Lagoon
  • Sites describes how we define the individual sites on a platform and the approach the platform takes to deployment.
"},{"location":"dpl-platform/architecture/platform-environment-architecture/#azure-infrastructure","title":"Azure Infrastructure","text":"

All resources of a Platform environment is contained in a single Azure Resource Group. The resources are provisioned via a Terraform setup that keeps its resources in a separate resource group.

The overview of current platform environments along with the various urls and a summary of its primary configurations can be found the Current Platform environments document.

A platform environment uses the following Azure infrastructure resources.

  • A virtual Network - with a subnet, configured with access to a number of services.
  • Separate storage accounts for
  • Monitoring data (logs)
  • Lagoon files (eg. results of running user-triggered administrative actions)
  • Backups
  • Drupal site files
  • A MariaDB used to host the sites databases.
  • A Key Vault that holds administrative credentials to resources that Lagoon needs administrative access to.
  • An Azure Kubernetes Service cluster that hosts the platform itself.
  • Two Public IPs: one for ingress one for egress.

The Azure Kubernetes Service in return creates its own resource group that contains a number of resources that are automatically managed by the AKS service. AKS also has a managed control-plane component that is mostly invisible to us. It has a separate managed identity which we need to grant access to any additional infrastructure-resources outside the \"MC\" resource-group that we need AKS to manage.

"},{"location":"dpl-platform/architecture/platform-environment-architecture/#software-components","title":"Software Components","text":"

The Platform consists of a number of software components deployed into the AKS cluster. The components are generally installed via Helm, and their configuration controlled via values-files.

Essential configurations such as the urls for the site can be found in the wiki

The following sections will describe the overall role of the component and how it integrates with other components. For more details on how the component is configured, consult the corresponding values-file for the component found in the individual environments configuration folder.

"},{"location":"dpl-platform/architecture/platform-environment-architecture/#lagoon","title":"Lagoon","text":"

Lagoon is an Open Soured Platform As A Service created by Amazee. The platform builds on top of a Kubernetes cluster, and provides features such as automated builds and the hosting of a large number of sites.

"},{"location":"dpl-platform/architecture/platform-environment-architecture/#ingress-nginx","title":"Ingress Nginx","text":"

Kubernetes does not come with an Ingress Controller out of the box. An ingress- controllers job is to accept traffic approaching the cluster, and route it via services to pods that has requested ingress traffic.

We use the widely used Ingress Nginx Ingress controller.

"},{"location":"dpl-platform/architecture/platform-environment-architecture/#cert-manager","title":"Cert Manager","text":"

Cert Manager allows an administrator specify a request for a TLS certificate, eg. as a part of an Ingress, and have the request automatically fulfilled.

The platform uses a cert-manager configured to handle certificate requests via Let's Encrypt.

"},{"location":"dpl-platform/architecture/platform-environment-architecture/#prometheus-and-alertmanager","title":"Prometheus and Alertmanager","text":"

Prometheus is a time series database used by the platform to store and index runtime metrics from both the platform itself and the sites running on the platform.

Prometheus is configured to scrape and ingest the following sources

  • Node Exporter (Kubernetes runtime metrics)
  • Ingress Nginx

Prometheus is installed via an Operator which amongst other things allows us to configure Prometheus and Alertmanager via ServiceMonitor and AlertmanagerConfig.

Alertmanager handles the delivery of alerts produced by Prometheus.

"},{"location":"dpl-platform/architecture/platform-environment-architecture/#grafana","title":"Grafana","text":"

Grafana provides the graphical user-interface to Prometheus and Loki. It is configured with a number of data sources via its values-file, which connects it to Prometheus and Loki.

"},{"location":"dpl-platform/architecture/platform-environment-architecture/#loki-and-promtail","title":"Loki and Promtail","text":"

Loki stores and indexes logs produced by the pods running in AKS. Promtail streams the logs to Loki, and Loki in turn makes the logs available to the administrator via Grafana.

"},{"location":"dpl-platform/architecture/platform-environment-architecture/#sites","title":"Sites","text":"

Each individual library has a Github repository that describes which sites should exist on the platform for the library. The creation of the repository and its contents is automated, and controlled by an entry in a sites.yaml- file shared by all sites on the platform.

Consult the following runbooks to see the procedures for:

  • Adding a site to the platform
  • Deploying to a site
  • Removing a site
"},{"location":"dpl-platform/architecture/platform-environment-architecture/#sitesyaml","title":"sites.yaml","text":"

sites.yaml is found in infrastructure/environments/<environment>/sites.yaml. The file contains a single map, where the configuration of the individual sites are contained under the property sites.<unique site key>, eg.

yaml sites: # Site objects are indexed by a unique key that must be a valid lagoon, and # github project name. That is, alphanumeric and dashes. core-test1: name: \"Core test 1\" description: \"Core test site no. 1\" # releaseImageRepository and releaseImageName describes where to pull the # container image a release from. releaseImageRepository: ghcr.io/danskernesdigitalebibliotek releaseImageName: dpl-cms-source # Sites can optionally specify primary and secondary domains. primary-domain: core-test.example.com # Fully configured sites will have a deployment key generated by Lagoon. deploy_key: \"ssh-ed25519 <key here>\" bib-ros: name: \"Roskilde Bibliotek\" description: \"Webmaster environment for Roskilde Bibliotek\" primary-domain: \"www.roskildebib.dk\" # The secondary domain will redirect to the primary. secondary-domains: [\"roskildebib.dk\", \"www2.roskildebib.dk\"] # A series of sites that shares the same image source may choose to reuse # properties via anchors << : *default-release-image-source

"},{"location":"dpl-platform/architecture/platform-environment-architecture/#environment-site-git-repositories","title":"Environment Site Git Repositories","text":"

Each platform-site is controlled via a GitHub repository. The repositories are provisioned via Terraform. The following depicts the authorization and control- flow in use:

The configuration of each repository is reconciled each time a site is created,

"},{"location":"dpl-platform/architecture/platform-environment-architecture/#deployment","title":"Deployment","text":"

Releases of DPL CMS are deployed to sites via the dpladm tool. It consults the sites.yaml file for the environment and performs any needed deployment.

"},{"location":"dpl-platform/architecture/adr/","title":"Architecture Decision Records","text":"

We loosely follow the guidelines for ADRs described by Michael Nygard.

A record should attempt to capture the situation that led to the need for a discrete choice to be made, and then proceed to describe the core of the decision, its status and the consequences of the decision.

To summaries a ADR could contain the following sections (quoted from the above article):

  • Title: These documents have names that are short noun phrases. For example, \"ADR 1: Deployment on Ruby on Rails 3.0.10\" or \"ADR 9: LDAP for Multitenant Integration\"

  • Context: This section describes the forces at play, including technological , political, social, and project local. These forces are probably in tension, and should be called out as such. The language in this section is value-neutral. It is simply describing facts.

  • Decision: This section describes our response to these forces. It is stated in full sentences, with active voice. \"We will \u2026\"

  • Status: A decision may be \"proposed\" if the project stakeholders haven't agreed with it yet, or \"accepted\" once it is agreed. If a later ADR changes or reverses a decision, it may be marked as \"deprecated\" or \"superseded\" with a reference to its replacement.

  • Consequences: This section describes the resulting context, after applying the decision. All consequences should be listed here, not just the \"positive\" ones. A particular decision may have positive, negative, and neutral consequences, but all of them affect the team and project in the future.

"},{"location":"dpl-platform/architecture/adr/adr-001-lagoon/","title":"Architecture Decision Record: Lagoon","text":""},{"location":"dpl-platform/architecture/adr/adr-001-lagoon/#context","title":"Context","text":"

The Danish Libraries needed a platform for hosting a large number of Drupal installations. As it was unclear exactly how to build such a platform and how best to fulfill a number of requirements, a Proof Of Concept project was initiated to determine whether to use an existing solution or build a platform from scratch.

After an evaluation, Lagoon was chosen.

"},{"location":"dpl-platform/architecture/adr/adr-001-lagoon/#decision","title":"Decision","text":"

The main factors behind the decision to use Lagoon where:

  • Much lower cost of maintenance than a self-built platform.
  • The platform is continually updated, and the updates are available for free.
  • A well-established platform with a lot of proven functionality right out of the box.
  • The option of professional support by Amazee

When using and integrating with Lagoon we should strive to

  • Make as little modifications to Lagoon as possible
  • Whenever possible, use the defaults, recommendations and best practices documented on eg. docs.lagoon.sh

We do this to keep true to the initial thought behind choosing Lagoon as a platform that gives us a lot of functionality for a (comparatively) small investment.

"},{"location":"dpl-platform/architecture/adr/adr-001-lagoon/#alternatives-considered","title":"Alternatives considered","text":"

The main alternative that was evaluated was to build a platform from scratch. While this may have lead to a more customized solution that more closely matched any requirements the libraries may have, it also required a very large investment would require a large ongoing investment to keep the platform maintained and updated.

We could also choose to fork Lagoon, and start making heavy modifications to the platform to end up with a solution customized for our needs. The downsides of this approach has already been outlined.

"},{"location":"dpl-platform/architecture/adr/adr-002-rightsizing/","title":"Architecture Decision Record: Rightsizing","text":""},{"location":"dpl-platform/architecture/adr/adr-002-rightsizing/#context","title":"Context","text":""},{"location":"dpl-platform/architecture/adr/adr-002-rightsizing/#expected-traffic","title":"Expected traffic","text":"

The platform is required to be able to handle an estimated 275.000 page-views per day spread out over 100 websites. A visit to a website that causes the browser to request a single html-document followed by a number of assets is only counted as a single page-view.

On a given day about half of the page-views to be made by an authenticated user. We further more expect the busiest site receive about 12% of the traffic.

Given these numbers, we can make some estimates of the expected average load. To stay fairly conservative we will still assume that about 50% of the traffic is anonymous and can thus be cached by Varnish, but we will assume that all all sites gets traffic as if they where the most busy site on the platform (12%).

12% of 275.000 requests gives us a an average of 33.000 requests. To keep to the conservative side, we concentrate the load to a period of 8 hours. We then end up with roughly 1 page-view pr. second.

"},{"location":"dpl-platform/architecture/adr/adr-002-rightsizing/#expected-workload-characteristics","title":"Expected workload characteristics","text":"

The platform is hosted on a Kubernetes cluster on which Lagoon is installed. As of late 2021, Lagoons approach to handling rightsizing of PHP- and in particular Drupal-applications is based on a number factors:

  1. Web workloads are extremely spiky. While a site looks to have to have a sustained load of 5 rps when looking from afar, it will in fact have anything from (eg) 0 to 20 simultaneous users on a given second.
  2. Resource-requirements are every ephemeral. Even though a request as a peak memory-usage of 128MB, it only requires that amount of memory for a very short period of time.
  3. Kubernetes nodes has a limit of how many pods will fit on a given node. This will constraint the scheduler from scheduling too many pods to a node even if the workloads has declared a very low resource request.
  4. With metrics-server enabled, Kubernetes will keep track of the actual available resources on a given node. So, a node with eg. 4GB ram, hosting workloads with a requested resource allocation of 1GB, but actually taking up 3.8GB of ram, will not get scheduled another pod as long as there are other nodes in the cluster that has more resources available.

The consequence of the above is that we can pack a lot more workload onto a single node than what would be expected if you only look at the theoretical maximum resource requirements.

"},{"location":"dpl-platform/architecture/adr/adr-002-rightsizing/#lagoon-resource-request-defaults","title":"Lagoon resource request defaults","text":"

Lagoon sets its resource-requests based on a helm values default in the kubectl-build-deploy-dind image. The default is typically 10Mi pr. container which can be seen in the nginx-php chart which runs a php and nginx container. Lagoon configures php-fpm to allow up to 50 children and allows php to use up to 400Mi memory.

Combining these numbers we can see that a site that is scheduled as if it only uses 20 Megabytes of memory, can in fact take up to 20 Gigabytes. The main thing that keeps this from happening in practice is a a combination of the above assumptions. No node will have more than a limited number of pods, and on a given second, no site will have nearly as many inbound requests as it could have.

"},{"location":"dpl-platform/architecture/adr/adr-002-rightsizing/#decision","title":"Decision","text":"

Lagoon is a very large and complex solution. Any modification to Lagoon will need to be well tested, and maintained going forward. With this in mind, we should always strive to use Lagoon as it is, unless the alternative is too costly or problematic.

Based on real-live operations feedback from Amazee (creators of Lagoon) and the context outline above we will

  • Leave the Lagoon defaults as they are, meaning most pods will request 10Mi of memory.
  • Let the scheduler be informed by runtime metrics instead of up front pod resource requests.
  • Rely on the node maximum pods to provide some horizontal spread of pods.
"},{"location":"dpl-platform/architecture/adr/adr-002-rightsizing/#alternatives-considered","title":"Alternatives considered","text":"

As Lagoon does not give us any manual control over rightsizing out of the box, all alternatives involves modifying Lagoon.

"},{"location":"dpl-platform/architecture/adr/adr-002-rightsizing/#altering-lagoon-defaults","title":"Altering Lagoon Defaults","text":"

We've inspected the process Lagoon uses to deploy workloads, and determined that it would be possible to alter the defaults used without too many modifications.

The build-deploy-docker-compose.sh script that renders the manifests that describes a sites workloads via Helm includes a service-specific values-file. This file can be used to modify the defaults for the Helm chart. By creating custom container-image for the build-process based on the upstream Lagoon build image, we can deliver our own version of this image.

As an example, the following Dockerfile will add a custom values file for the redis service.

FROM docker.io/uselagoon/kubectl-build-deploy-dind:latest\nCOPY redis-values.yaml /kubectl-build-deploy/\n

Given the following redis-values.yaml

resources:\nrequests:\ncpu: 10m\nmemory: 100Mi\n

The Redis deployment would request 100Mi instead of the previous default of 10Mi.

"},{"location":"dpl-platform/architecture/adr/adr-002-rightsizing/#introduce-t-shirt-sizes","title":"Introduce \"t-shirt\" sizes","text":"

Building upon the modification described in the previous chapter, we could go even further and modify the build-script itself. By inspecting project variables we could have the build-script pass in eg. a configurable value for replicaCount for a pod. This would allow us to introduce a small/medium/large concept for sites. This could be taken even further to eg. introduce whole new services into Lagoon.

"},{"location":"dpl-platform/architecture/adr/adr-002-rightsizing/#consequences","title":"Consequences","text":"

This could lead to problems for sites that requires a lot of resources, but given the expected average load, we do not expect this to be a problem even if a site receives an order of magnitude more traffic than the average.

The approach to rightsizing may also be a bad fit if we see a high concentration of \"non-spiky\" workloads. We know for instance that Redis and in particular Varnish is likely to use a close to constant amount of memory. Should a lot of Redis and Varnish pods end up on the same node, evictions are very likely to occur.

The best way to handle these potential situations is to be knowledgeable about how to operate Kubernetes and Lagoon, and to monitor the workloads as they are in use.

"},{"location":"dpl-platform/architecture/adr/adr-003-system-alerts/","title":"ADR-003 System alerts","text":""},{"location":"dpl-platform/architecture/adr/adr-003-system-alerts/#context","title":"Context","text":"

There has been a wish for a functionality that alerts administrators if certain system values have gone beyond defined thresholds rules.

"},{"location":"dpl-platform/architecture/adr/adr-003-system-alerts/#decision","title":"Decision","text":"

We have decided to use alertmanager that is a part of the Prometheus package that is already used for monitoring the cluster.

"},{"location":"dpl-platform/architecture/adr/adr-003-system-alerts/#consequences","title":"Consequences","text":"
  • We have tried to install alertmanager and testing it. It works and given the various possibilities of defining alert rules we consider the demands to be fulfilled.
  • We will be able to get alerts regarding thresholds on both container and cluster level which is what we need.
  • Alertmanager fits in the general focus of being cloud agnostic. It is CNCF approved and does not have any external infrastructure dependencies.
"},{"location":"dpl-platform/architecture/adr/adr-004-declarative-site-management/","title":"ADR 004: Declarative Site management","text":""},{"location":"dpl-platform/architecture/adr/adr-004-declarative-site-management/#context","title":"Context","text":"

Lagoon requires a site to be deployed from at Git repository containing a .lagoon.yml and docker-compose.yml A potential logical consequence of this is that we require a Git repository pr site we want to deploy, and that we require that repository to maintain those two files.

Administering the creation and maintenance of 100+ Git repositories can not be done manually with risk of inconsistency and errors. The industry best practice for administering large-scale infrastructure is to follow a declarative Infrastructure As Code(IoC) pattern. By keeping the approach declarative it is much easier for automation to reason about the intended state of the system.

Further more, in a standard Lagoon setup, Lagoon is connected to the \"live\" application repository that contains the source-code you wish to deploy. In this approach Lagoon will just deploy whatever the HEAD of a given branch points. In our case, we perform the build of a sites source release separate from deploying which means the sites repository needs to be updated with a release-version whenever we wish it to be updated. This is not a problem for a small number of sites - you can just update the repository directly - but for a large set of sites that you may wish to administer in bulk - keeping track of which version is used where becomes a challenge. This is yet another good case for declarative configuration: Instead of modifying individual repositories by hand to deploy, we would much rather just declare that a set of sites should be on a specific release and then let automation take over.

While there are no authoritative discussion of imperative vs declarative IoC, the following quote from an OVH Tech Blog summarizes the current consensus in the industry pretty well:

In summary declarative infrastructure tools like Terraform and CloudFormation offer a much lower overhead to create powerful infrastructure definitions that can grow to a massive scale with minimal overheads. The complexities of hierarchy, timing, and resource updates are handled by the underlying implementation so you can focus on defining what you want rather than how to do it.

The additional power and control offered by imperative style languages can be a big draw but they also move a lot of the responsibility and effort onto the developer, be careful when choosing to take this approach.

"},{"location":"dpl-platform/architecture/adr/adr-004-declarative-site-management/#decision","title":"Decision","text":"

We administer the deployment to and the Lagoon configuration of a library site in a repository pr. library. The repositories are provisioned via Terraform that reads in a central sites.yaml file. The same file is used as input for the automated deployment process which renderers the various files contained in the repository including a reference to which release of DPL-CMS Lagoon should use.

It is still possible to create and maintain sites on Lagoon independent of this approach. We can for instance create a separate project for the dpl-cms repository to support core development.

"},{"location":"dpl-platform/architecture/adr/adr-004-declarative-site-management/#status","title":"Status","text":"

Accepted

"},{"location":"dpl-platform/architecture/adr/adr-004-declarative-site-management/#alternatives-considered","title":"Alternatives considered","text":"

We could have run each site as a branch of off a single large repository. This was rejected as a possibility as it would have made the administration of access to a given libraries deployed revision hard to control. By using individual repositories we have the option of grating an outside developer access to a full repository without affecting any other.

"},{"location":"dpl-platform/infrastructure/","title":"DPL Platform Infrastructure","text":"

This directory contains the Infrastructure as Code and scripts that are used for maintaining the infrastructure-component that each platform environment consists of. A \"platform environment\" is an umbrella term for the Azure infrastructure, the Kubernetes cluster, the Lagoon installation and the set of GitHub environments that makes up a single DPL Platform installation.

"},{"location":"dpl-platform/infrastructure/#directory-layout","title":"Directory layout","text":"
  • dpladm/: a tool used for deploying individual sites. The tools can be run manually, but the recommended way is via the common infrastructure Taskfile.
  • environments/: contains a directory for each platform environment.
  • terraform: terraform setup and tooling that is shared between environments.
  • task/: Configuration and scripts used by our Taskfile-based automation The scripts included in this directory can be run by hand in an emergency but te recommended way to invoke these via task.
  • Taskfile.yml: the common infrastructure task configuration. Invoke task to get a list of targets. Must be run from within an instance of DPL shell unless otherwise noted.
"},{"location":"dpl-platform/infrastructure/#platform-environment-configurations","title":"Platform Environment configurations","text":"

The environments directory contains a subdirectory for each platform environment. You generally interact with the files and directories within the directory to configure the environment. When a modification has been made, it is put in to effect by running the appropiate task :

  • configuration: contains the various configurations the applications that are installed on top of the infrastructure requires. These are used by the support:provision:* tasks.
  • env_repos contains the Terraform root-module for provisioning GitHub site- environment repositories. The module is run via the env_repos:provision task.
  • infrastructure: contains the Terraform root-module used to provision the basic Azure infrastructure components that the platform requires.The module is run via the infra:provision task.
  • lagoon: contains Kubernetes manifests and Helm values-files used for installing the Lagoon Core and Remote that is at the heart of a DPL Platform installation. THe module is run via the lagoon:provision:* tasks.
"},{"location":"dpl-platform/infrastructure/#basic-usage-of-dplsh-and-an-environment-configuration","title":"Basic usage of dplsh and an environment configuration","text":"

The remaining guides in this document assumes that you work from an instance of the DPL shell. See the DPLSH Runbook for a basic introduction to how to use dplsh.

"},{"location":"dpl-platform/infrastructure/#installing-a-platform-environment-from-scratch","title":"Installing a platform environment from scratch","text":"

The following describes how to set up a whole new platform environment to host platform sites.

The easiest way to set up a new environment is to create a new environments/<name> directory and copy the contents of an existing environment replacing any references to the previous environment with a new value corresponding to the new environment. Take note of the various URLs, and make sure to update the Current Platform environments documentation.

If this is the very first environment, remember to first initialize the Terraform- setup, see the terraform README.md.

"},{"location":"dpl-platform/infrastructure/#provisioning-infrastructure","title":"Provisioning infrastructure","text":"

When you have prepared the environment directory, launch dplsh and go through the following steps to provision the infrastructure:

# We export the variable to simplify the example, you can also specify it inline.\nexport DPLPLAT_ENV=dplplat01\n\n# Provision the Azure resources\ntask infra:provision\n\n# Create DNS record\nCreate an A record in the administration area of your DNS provider.\nTake the terraform output: \"ingress_ip\" of the former command and create an entry\nlike: \"*.[DOMAN_NAME].[TLD]\": \"[ingress_ip]\"\n# Provision the support software that the Platform relies on\ntask support:provision\n
"},{"location":"dpl-platform/infrastructure/#installing-and-configuring-lagoon","title":"Installing and configuring Lagoon","text":"

The previous step has established the raw infrastructure and the Kubernetes support projects that Lagoon needs to function. You can proceed to follow the official Lagoon installation procedure.

The execution of the individual steps of the guide has been somewhat automated, the following describes how to use the automation, make sure to follow along in the official documentation to understand the steps and some of the additional actions you have to take.

# The following must be carried out from within dplsh, launched as described\n# in the previous step including the definition of DPLPLAT_ENV.\n# 1. Provision a lagoon core into the cluster.\ntask lagoon:provision:core\n\n# 2. Skip the steps in the documentation that speaks about setting up email, as\n# we currently do not support sending emails.\n# 3. Setup ssh-keys for the lagoonadmin user\n# Access the Lagoon UI (consult the platform-environments.md for the url) and\n# log in with lagoonadmin + the admin password that can be extracted from a\n# Kubernetes secret:\nkubectl \\\n-o jsonpath=\"{.data.KEYCLOAK_LAGOON_ADMIN_PASSWORD}\" \\\n-n lagoon-core \\\nget secret lagoon-core-keycloak \\\n| base64 --decode\n\n# Then go to settings and add the ssh-keys that should be able to access the\n# lagoon admin user. Consider keeping this list short, and instead add\n# additional users with fewer privileges laster.\n# 4. If your ssh-key is passphrase-projected we'll need to setup an ssh-agent\n# instance:\n$ eval $(ssh-agent); ssh-add\n\n# 5. Configure the CLI to verify that access (the cli itself has already been\n#    installed in dplsh)\ntask lagoon:cli:config\n\n# You can now add additional users, this step is currently skipped.\n# (6. Install Harbor.)\n# This step has already been performed as a part of the installation of\n# support software.\n# 7. Install a Lagoon Remote into the cluster\ntask lagoon:provision:remote\n\n# 8. Register the cluster administered by the Remote with Lagoon Core\n# Notice that you must provide a bearer token via the USER_TOKEN environment-\n# variable. The token can be found in $HOME/.lagoon.yml after a successful\n# \"lagoon login\"\nUSER_TOKEN=<token> task lagoon:add:cluster:\n

The Lagoon core has now been installed, and the remote registered with it.

"},{"location":"dpl-platform/infrastructure/#setting-up-a-github-organization-and-repositories-for-a-new-platform-environment","title":"Setting up a GitHub organization and repositories for a new platform environment","text":"

Prerequisites:

  • An properly authenticated azure CLI (az). See the section on initial Terraform setup for more details on the requirements

First create a new administrative github user and create a new organization with the user. The administrative user should only be used for administering the organization via terraform and its credentials kept as safe as possible! The accounts password can be used as a last resort for gaining access to the account and will not be stored in Key Vault. Thus, make sure to store the password somewhere safe, eg. in a password-manager or as a physical printout.

This requires the infrastructure to have been created as we're going to store credentials into the azure Key Vault.

# cd into the infrastructure folder and launch a shell\n(host)$ cd infrastructure\n(host)$ dplsh\n\n# Remaining commands are run from within dplsh\n# export the platform environment name.\n# export DPLPLAT_ENV=<name>, eg\n$ export DPLPLAT_ENV=dplplat01\n\n# 1. Create a ssh keypair for the user, eg by running\n# ssh-keygen -t ed25519 -C \"<comment>\" -f dplplatinfra01_id_ed25519\n# eg.\n$ ssh-keygen -t ed25519 -C \"dplplatinfra@0120211014073225\" -f dplplatinfra01_id_ed25519\n\n# 2. Then access github and add the public-part of the key to the account\n# 3. Add the key to keyvault under the key name \"github-infra-admin-ssh-key\"\n# eg.\n$ SECRET_KEY=github-infra-admin-ssh-key SECRET_VALUE=$(cat dplplatinfra01_id_ed25519)\\\ntask infra:keyvault:secret:set\n\n# 4. Access GitHub again, and generate a Personal Access Token for the account.\n#    The token should\n#     - be named after the platform environment (eg. dplplat01-terraform-timestamp)\n#     - Have a fairly long expiration - do remember to renew it\n#     - Have the following permissions: admin:org, delete_repo, repo\n# 5. Add the access token to Key Vault under the name \"github-infra-admin-pat\"\n# eg.\n$ SECRET_KEY=github-infra-admin-pat SECRET_VALUE=githubtokengoeshere task infra:keyvault:secret:set\n\n# Our tooling can now administer the GitHub organization\n
"},{"location":"dpl-platform/infrastructure/#renewing-the-administrative-github-personal-access-token","title":"Renewing the administrative GitHub Personal Access Token","text":"

The Personal Access Token we use for impersonating the administrative GitHub user needs to be recreated periodically:

# cd into the infrastructure folder and launch a shell\n(host)$ cd infrastructure\n(host)$ dplsh\n\n# Remaining commands are run from within dplsh\n# export the platform environment name.\n# export DPLPLAT_ENV=<name>, eg\n$ export DPLPLAT_ENV=dplplat01\n\n# 1. Access GitHub, and generate a Personal Access Token for the account.\n#    The token should\n#     - be named after the platform environment (eg. dplplat01-terraform)\n#     - Have a fairly long expiration - do remember to renew it\n#     - Have the following permissions: admin:org, delete_repo, repo\n# 2. Add the access token to Key Vault under the name \"github-infra-admin-pat\"\n# eg.\n$ SECRET_KEY=github-infra-admin-pat SECRET_VALUE=githubtokengoeshere \\\ntask infra:keyvault:secret:set\n\n# 3. Delete the previous token\n
"},{"location":"dpl-platform/infrastructure/terraform/","title":"Terraform","text":"

This directory contains the configuration and tooling we use to support our use of terraform.

"},{"location":"dpl-platform/infrastructure/terraform/#the-terraform-setup","title":"The Terraform setup","text":"

The setup keeps a single terraform-state pr. environment. Each state is kept as separate blobs in a Azure Storage Account.

Access to the storage account is granted via a Storage Account Key which is kept in a Azure Key Vault in the same resource-group. The key vault, storage account and the resource-group that contains these resources are the only resources that are not provisioned via Terraform.

"},{"location":"dpl-platform/infrastructure/terraform/#initial-setup-of-terraform","title":"Initial setup of Terraform","text":"

The following procedure must be carried out before the first environment can be created.

Prerequisites:

  • A Azure subscription
  • An authenticated azure CLI that is allowed to use create resources and grant access to these resources under the subscription including Key Vaults. The easiest way to achieve this is to grant the user the Owner and Key Vault Administrator roles to on subscription.

Use the scripts/bootstrap-tf.sh for bootstrapping. After the script has been run successfully it outputs instructions for how to set up a terraform module that uses the newly created storage-account for state-tracking.

As a final step you must grant any administrative users that are to use the setup permission to read from the created key vault.

"},{"location":"dpl-platform/infrastructure/terraform/#dnsimple","title":"Dnsimple","text":"

The setup uses an integration with DNSimple to set a domain name when the environments ingress ip has been provisioned. To use this integration first obtain a api-key for the DNSimple account. Then use scripts/add-dnsimple-apikey.sh to write it to the setups Key Vault and finally add the following section to .dplsh.profile ( get the subscription id and key vault name from existing export for ARM_ACCESS_KEY).

export DNSIMPLE_TOKEN=$(az keyvault secret show --subscription \"<subscriptionid>\"\\\n--name dnsimple-api-key --vault-name <key vault-name> --query value -o tsv)\nexport DNSIMPLE_ACCOUNT=\"<dnsimple-account-id>\"\n
"},{"location":"dpl-platform/infrastructure/terraform/#terraform-setups","title":"Terraform Setups","text":"

A setup is used to manage a set of environments. We currently have a single that manages all environments.

"},{"location":"dpl-platform/infrastructure/terraform/#alpha","title":"Alpha","text":"
  • Name: alpha
  • Resource-group: rg-tfstate-alpha
  • Key Vault name: kv-dpltfstatealpha001
  • Storage account: stdpltfstatealpha001
"},{"location":"dpl-platform/infrastructure/terraform/#terraform-modules","title":"Terraform Modules","text":""},{"location":"dpl-platform/infrastructure/terraform/#root-module","title":"Root module","text":"

The platform environments share a number of general modules, which are then used via a number of root-modules set up for each environment.

Consult the general environment documentation for descriptions on which resources you can expect to find in an environment and how they are used.

Consult the environment overview for an overview of environments.

"},{"location":"dpl-platform/infrastructure/terraform/#dpl-platform-infrastructure-module","title":"DPL Platform Infrastructure Module","text":"

The dpl-platform-environment Terraform module provisions all resources that are required for a single DPL Platform Environment.

Inspect variables.tf for a description of the required module-variables.

Inspect outputs.tf for a list of outputs.

Inspect the individual module files for documentation of the resources.

The following diagram depicts (amongst other things) the provisioned resources. Consult the platform environment documentation for more details on the role the various resources plays.

"},{"location":"dpl-platform/infrastructure/terraform/#dpl-platform-site-environment-module","title":"DPL Platform Site Environment Module","text":"

The dpl-platform-env-repos Terraform module provisions the GitHub Git repositories that the platform uses to integrate with Lagoon. Each site hosted on the platform has a registry.

Inspect variables.tf for a description of the required module-variables.

Inspect outputs.tf for a list of outputs.

Inspect the individual module files for documentation of the resources.

The following diagram depicts how the module gets its credentials for accessing GitHub and what it provisions.

"},{"location":"dpl-platform/runbooks/","title":"DPL Platform Runbooks","text":"

This directory contains our operational runbooks for standard procedures you may need to carry out while maintaining and operating a DPL Platform environment.

Most runbooks has the following layout.

  • Title - Short title that follows the name of the markdown file for quick lookup.
  • When to use - Outlines when this runbook should be used.
  • Prerequisites - Any requirements that should be met before the procedure is followed.
  • Procedure - Stepwise description of the procedure, sometimes these will be whole subheadings, sometimes just a single section with lists.

The runbooks should focus on the \"How\", and avoid explaining any.

"},{"location":"dpl-platform/runbooks/access-kubernetes/","title":"Access Kubernetes","text":""},{"location":"dpl-platform/runbooks/access-kubernetes/#when-to-use","title":"When to use","text":"

When you need to gain kubectl access to a platform-environments Kubernetes cluster.

"},{"location":"dpl-platform/runbooks/access-kubernetes/#prerequisites","title":"Prerequisites","text":"
  • An authenticated az cli (from the host). This likely means az login --tenant TENANT_ID, where the tenant id is that of \"DPL Platform\". See Azure Portal > Tenant Properties. The logged in user must have permissions to list cluster credentials.
  • docker cli which is authenticated against the GitHub Container Registry. The access token used must have the read:packages scope.
"},{"location":"dpl-platform/runbooks/access-kubernetes/#procedure","title":"Procedure","text":"
  1. cd to dpl-platform/infrastructure
  2. Launch the dpl shell: dplsh
  3. Set the platform envionment, eg. for \"dplplat01\": export DPLPLAT_ENV=dplplat01
  4. Authenticate: task cluster:auth

Your dplsh session should now be authenticated against the cluster.

"},{"location":"dpl-platform/runbooks/add-generic-site-to-platform/","title":"Add a generic site to the platform","text":""},{"location":"dpl-platform/runbooks/add-generic-site-to-platform/#when-to-use","title":"When to use","text":"

When you want to add a \"generic\" site to the platform. By Generic we mean a site stored in a repository that that is Prepared for Lagoon and contains a .lagoon.yml at its root.

The current main example of such as site is dpl-cms which is used to develop the shared DPL install profile.

"},{"location":"dpl-platform/runbooks/add-generic-site-to-platform/#prerequisites","title":"Prerequisites","text":"
  • An authenticated az cli. The logged in user must have full administrative permissions to the platforms azure infrastructure.
  • A running dplsh with DPLPLAT_ENV set to the platform environment name.
  • A Lagoon account on the Lagoon core with your ssh-key associated
  • The git-url for the sites environment repository
  • A personal access-token that is allowed to pull images from the image-registry that hosts our images.
  • The platform environment name (Consult the platform environment documentation)
"},{"location":"dpl-platform/runbooks/add-generic-site-to-platform/#procedure","title":"Procedure","text":"

The following describes a semi-automated version of \"Add a Project\" in the official documentation.

# From within dplsh:\n# Set an environment,\n# export DPLPLAT_ENV=<platform environment name>\n# eg.\n$ export DPLPLAT_ENV=dplplat01\n\n# If your ssh-key is passphrase-projected we'll need to setup an ssh-agent\n# instance:\n$ eval $(ssh-agent); ssh-add\n\n# 1. Authenticate against the cluster and lagoon\n$ task cluster:auth\n$ task lagoon:cli:config\n\n# 2. Add a project\n# PROJECT_NAME=<project name>  GIT_URL=<url> task lagoon:project:add\n$ PROJECT_NAME=dpl-cms GIT_URL=git@github.com:danskernesdigitalebibliotek/dpl-cms.git\\\ntask lagoon:project:add\n\n# 2.b You can also run lagoon add project manually, consult the documentation linked\n#     in the beginning of this section for details.\n# 3. Deployment key\n# The project is added, and a deployment key is printed. Copy it and configure\n# the GitHub repository. See the official documentation for examples.\n# 4. Webhook\n# Configure Github to post events to Lagoons webhook url.\n# The webhook url for the environment will be\n#  https://webhookhandler.lagoon.<environment>.dpl.reload.dk\n# eg for the environment dplplat01\n#  https://webhookhandler.lagoon.dplplat01.dpl.reload.dk\n#\n# Referer to the official documentation linked above for an example on how to\n# set up webhooks in github.\n# 5. Configure image registry credentials Lagoon should use for the project\n#    IF your project references private images in repositories that requires\n#    authentication\n# Refresh your Lagoon token.\n$ lagoon login\n\n# Then export a github personal access-token with pull access.\n# We could pass this to task directly like the rest of the variables but we\n# opt for the export to keep the execution of task a bit shorter.\n$ export VARIABLE_VALUE=<github pat>\n\n# Then get the project id by listing your projects\n$ lagoon list projects\n\n# Finally, add the credentials\n$ VARIABLE_TYPE_ID=<project id> \\\nVARIABLE_TYPE=PROJECT \\\nVARIABLE_SCOPE=CONTAINER_REGISTRY \\\nVARIABLE_NAME=GITHUB_REGISTRY_CREDENTIALS \\\ntask lagoon:set:environment-variable\n\n# If you get a \"Invalid Auth Token\" your token has probably expired, generated a\n# new with \"lagoon login\" and try again.\n# 5. Trigger a deployment manually, this will fail as the repository is empty\n#    but will serve to prepare Lagoon for future deployments.\n# lagoon deploy branch -p <project-name> -b <branch>\n$ lagoon deploy branch -p dpl-cms -b main\n
"},{"location":"dpl-platform/runbooks/add-library-site-to-platform/","title":"Add a new library site to the platform","text":""},{"location":"dpl-platform/runbooks/add-library-site-to-platform/#when-to-use","title":"When to use","text":"

When you want to add a new core-test, editor, webmaster or programmer dpl-cms site to the platform.

"},{"location":"dpl-platform/runbooks/add-library-site-to-platform/#prerequisites","title":"Prerequisites","text":"
  • An authenticated az cli. The logged in user must have full administrative permissions to the platforms azure infrastructure.
  • A running dplsh with DPLPLAT_ENV set to the platform environment name.
"},{"location":"dpl-platform/runbooks/add-library-site-to-platform/#procedure","title":"Procedure","text":"

The following sections describes how to

  • Add the site to sites.yaml
  • Provision Github \"environment\" repository
  • Create a Lagoon project and connect it to the repository

After these steps has been completed, you can continue to deploying to the site. See the deploy-a-release.md for details.

"},{"location":"dpl-platform/runbooks/add-library-site-to-platform/#step-1-update-sitesyaml","title":"Step 1, update sites.yaml","text":"

Create an entry for the site in sites.yaml.

For now specify an unique site key (its key in the map of sites), name and description. Leave out the deployment-key, you will add it in a later step.

Sample entry (beware that this example be out of sync with the environment you are operating, so make sure to compare it with existing entries from the environment)

sites:\nbib-rb:\nname: \"Roskilde Bibliotek\"\ndescription: \"Roskilde Bibliotek\"\nprimary-domain: \"www.roskildebib.dk\"\nsecondary-domains: [\"roskildebib.dk\"]\ndpl-cms-release: \"1.2.3\"\n<< : *default-release-image-source\n

The last entry merges in a default set of properties for the source of release- images. If the site is on the \"programmer\" plan, specify a custom set of properties like so:

sites:\nbib-rb:\nname: \"Roskilde Bibliotek\"\ndescription: \"Roskilde Bibliotek\"\nprimary-domain: \"www.roskildebib.dk\"\nsecondary-domains: [\"roskildebib.dk\"]\ndpl-cms-release: \"1.2.3\"\n# Github package registry used as an example here, but any registry will\n# work.\nreleaseImageRepository: ghcr.io/some-github-org\nreleaseImageName: some-image-name\n

Be aware that the referenced images needs to be publicly available as Lagoon currently only authenticates against ghcr.io.

Then continue to provision the a Github repository for the site.

"},{"location":"dpl-platform/runbooks/add-library-site-to-platform/#step-2-provision-a-github-repository","title":"Step 2: Provision a Github repository","text":"

Run task env_repos:provision to create the repository.

"},{"location":"dpl-platform/runbooks/add-library-site-to-platform/#create-a-lagoon-project-and-connect-the-github-repository","title":"Create a Lagoon project and connect the GitHub repository","text":"

Prerequisites:

  • A Lagoon account on the Lagoon core with your ssh-key associated
  • The git-url for the sites environment repository
  • A personal access-token that is allowed to pull images from the image-registry that hosts our images.
  • The platform environment name (Consult the platform environment documentation)

The following describes a semi-automated version of \"Add a Project\" in the official documentation.

# From within dplsh:\n# Set an environment,\n# export DPLPLAT_ENV=<platform environment name>\n# eg.\n$ export DPLPLAT_ENV=dplplat01\n\n# If your ssh-key is passphrase-projected we'll need to setup an ssh-agent\n# instance:\n$ eval $(ssh-agent); ssh-add\n\n# 1. Authenticate against the cluster and lagoon\n$ task cluster:auth\n$ task lagoon:cli:config\n\n# 2. Add a project\n# PROJECT_NAME=<project name>  GIT_URL=<url> task lagoon:project:add\n$ PROJECT_NAME=core-test1 GIT_URL=git@github.com:danishpubliclibraries/env-core-test1.git\\\ntask lagoon:project:add\n\n# The project is added, and a deployment key is printed, use it for the next step.\n# 3. Add the deployment key to sites.yaml under the key \"deploy_key\".\n$ vi environments/${DPLPLAT_ENV}/sites.yaml\n# Then update the repositories using Terraform\n$ task env_repos:provision\n\n# 4. Configure image registry credentials Lagoon should use for the project:\n# Refresh your Lagoon token.\n$ lagoon login\n\n# Then export a github personal access-token with pull access.\n# We could pass this to task directly like the rest of the variables but we\n# opt for the export to keep the execution of task a bit shorter.\n$ export VARIABLE_VALUE=<github pat>\n\n# Then get the project id by listing your projects\n$ lagoon list projects\n\n# Finally, add the credentials\n$ VARIABLE_TYPE_ID=<project id> \\\nVARIABLE_TYPE=PROJECT \\\nVARIABLE_SCOPE=CONTAINER_REGISTRY \\\nVARIABLE_NAME=GITHUB_REGISTRY_CREDENTIALS \\\ntask lagoon:set:environment-variable\n\n# If you get a \"Invalid Auth Token\" your token has probably expired, generated a\n# new with \"lagoon login\" and try again.\n# 5. Trigger a deployment manually, this will fail as the repository is empty\n#    but will serve to prepare Lagoon for future deployments.\n# lagoon deploy branch -p <project-name> -b <branch>\n$ lagoon deploy branch -p core-test1 -b main\n

If you want to deploy a release to the site, continue to Deploying a release.

"},{"location":"dpl-platform/runbooks/connecting-the-lagoon-cli/","title":"Connecting the Lagoon CLI","text":""},{"location":"dpl-platform/runbooks/connecting-the-lagoon-cli/#when-to-use","title":"When to use","text":"

When you want to use the Lagoon API vha. the CLI. You can connect from the DPL Shell, or from a local installation of the CLI.

Using the DPL Shell requires administrative privileges to the infrastructure while a local cli may connect using only the ssh-key associated to a Lagoon user.

This runbook documents both cases, as well as how an administrator can extract the basic connection details a standard user needs to connect to the Lagoon installation.

"},{"location":"dpl-platform/runbooks/connecting-the-lagoon-cli/#prerequisites","title":"Prerequisites","text":"
  • Your ssh-key associated with a lagoon user. This has to be done via the Lagoon UI by either you for your personal account, or by an administrator who has access to edit your Lagoon account.
  • For local installations of the cli:
  • The Lagoon CLI installed locally
  • Connectivity details for the Lagoon environment
  • For administrative access to extract connection details or use the lagoon cli from within the dpl shell:
  • A valid dplsh setup to extract the connectivity details
"},{"location":"dpl-platform/runbooks/connecting-the-lagoon-cli/#procedure","title":"Procedure","text":""},{"location":"dpl-platform/runbooks/connecting-the-lagoon-cli/#obtain-the-connection-details-for-the-environment","title":"Obtain the connection details for the environment","text":"

You can skip this step and go to Configure your local lagoon cli) if your environment is already in Current Platform environments and you just want to have a local lagoon cli working.

If it is missing, go through the steps below and update the document if you have access, or ask someone who has.

# Launch dplsh.\n$ cd infrastructure\n$ dplsh\n\n# 1. Set an environment,\n# export DPLPLAT_ENV=<platform environment name>\n# eg.\n$ export DPLPLAT_ENV=dplplat01\n\n# 2. Authenticate against AKS, needed by infrastructure and Lagoon tasks\n$ task cluster:auth\n\n# 3. Generate the Lagoon CLI configuration and authenticate\n# The Lagoon CLI is authenticated via ssh-keys. DPLSH will mount your .ssh\n# folder from your homedir, but if your keys are passphrase protected, we need\n# to unlock them.\n$ eval $(ssh-agent); ssh-add\n# Authorize the lagoon cli\n$ task lagoon:cli:config\n\n# List the connection details\n$ lagoon config list\n
"},{"location":"dpl-platform/runbooks/connecting-the-lagoon-cli/#configure-your-local-lagoon-cli","title":"Configure your local lagoon cli","text":"

Get the details in the angle-brackets from Current Platform environments:

$ lagoon config add \\\n--graphql https://<GraphQL endpoint> \\\n--ui https://<Lagoon UI> \\\n--hostname <SSH host> \\\n--ssh-key <SSH Key Path> \\\n--port <SSH port>> \\\n--lagoon <Lagoon name>\n\n# Eg.\n$ lagoon config add \\\n--graphql https://api.lagoon.dplplat01.dpl.reload.dk/graphql \\\n--force \\\n--ui https://ui.lagoon.dplplat01.dpl.reload.dk \\\n--hostname 20.238.147.183 \\\n--port 22 \\\n--lagoon dplplat01\n

Then log in

# Set the configuration as default.\nlagoon config default --lagoon <Lagoon name>\nlagoon login\nlagoon whoami\n\n# Eg.\nlagoon config default --lagoon dplplat01\nlagoon login\nlagoon whoami\n
"},{"location":"dpl-platform/runbooks/deploy-a-release/","title":"Deploy a dpl-cms release to a site","text":""},{"location":"dpl-platform/runbooks/deploy-a-release/#when-to-use","title":"When to use","text":"

When you wish to roll out a release of DPL-CMS or a fork to a single site.

If you want to deploy to more than one site, simply repeat the procedure for each site.

"},{"location":"dpl-platform/runbooks/deploy-a-release/#prerequisites","title":"Prerequisites","text":"
  • A dplsh session with DPLPLAT_ENV exported and ssh-agent configured.
  • a shell with a user that is authorized to interact with the environment repositories in the github organisation used for the environment over ssh.
  • The release-tag you whish to deploy, consult the readme in the dpl-cms repository for instructions on how to build an publish a release.
"},{"location":"dpl-platform/runbooks/deploy-a-release/#procedure","title":"Procedure","text":"
# 1. Make any changes to the sites entry sites.yml you need.\n# 2. (optional) diff the deployment\nDIFF=1 SITE=<sitename> task site:sync\n# 3a. Synchronize the site, triggering a deployment if the current state differs\n#    from the intended state in sites.yaml\nSITE=<sitename> task site:sync\n# 3b. If the state does not differ but you still want to trigger a deployment,\n#     specify FORCE=1\nFORCE=1 SITE=<sitename> task site:sync\n

The following diagram outlines the full release-flow starting from dpl-cms (or a fork) and to the release is deployed:

"},{"location":"dpl-platform/runbooks/remove-site-from-platform/","title":"Removing a site from the platform","text":""},{"location":"dpl-platform/runbooks/remove-site-from-platform/#when-to-use","title":"When to use","text":"

When you wish to delete a site and all its data from the platform

Prerequisites:

  • The platform environment name
  • An user with administrative access to the environment repository
  • A lagoon account with your ssh-key associated
  • The site key (its key in sites.yaml)
  • An properly authenticated azure CLI (az) that has administrative access to the cluster running the lagoon installation
"},{"location":"dpl-platform/runbooks/remove-site-from-platform/#procedure","title":"Procedure","text":"

The procedure consists of the following steps (numbers does not correspond to the numbers in the script below).

  1. Download and archive relevant backups
  2. Remove the project from Lagoon
  3. Delete the projects namespace from kubernetes.
  4. Delete the site from sites.yaml
  5. Delete the sites environment repository

Your first step should be to secure any backups you think might be relevant to archive. Whether this step is necessary depends on the site. Consult the Retrieve and Restore backups runbook for the operational steps.

You are now ready to perform the actual removal of the site.

# Launch dplsh.\n$ cd infrastructure\n$ dplsh\n\n# You are assumed to be inside dplsh from now on.\n# 1. Set an environment,\n# export DPLPLAT_ENV=<platform environment name>\n# eg.\n$ export DPLPLAT_ENV=dplplat01\n\n# 2. Setup access to ssh-keys so that the lagoon cli can authenticate.\n$ eval $(ssh-agent); ssh-add\n\n# 3. Authenticate against lagoon\n$ task lagoon:cli:config\n\n# 4. Delete the project from Lagoon\n# lagoon delete project --project <site machine-name>\n$ lagoon delete project  --project core-test1\n\n# 5. Authenticate against kubernetes\n$ task cluster:auth\n\n# 6. List the namespaces\n# Identify all the project namespace with the syntax <sitename>-<branchname>\n# eg \"core-test1-main\" for the main branch for the \"core-test1\" site.\n$ kubectl get ns\n\n# 7. Delete each site namespace\n# kubectl delete ns <namespace>\n# eg.\n$ kubectl delete ns core-test1-main\n\n# 8. Edit sites.yaml, remove the the entry for the site\n$ vi environments/${DPLPLAT_ENV}/sites.yaml\n# Then have Terraform delete the sites repository.\n$ task env_repos:provision\n
"},{"location":"dpl-platform/runbooks/retrieve-restore-backup/","title":"Retrieving backups","text":""},{"location":"dpl-platform/runbooks/retrieve-restore-backup/#when-to-use","title":"When to use","text":"

When you wish to download an automatic backup made by Lagoon, and optionally restore it into an existing site.

"},{"location":"dpl-platform/runbooks/retrieve-restore-backup/#prerequisites","title":"Prerequisites","text":"
  • Administrative access to the site in the Lagoon UI
  • (for restore) administrative cluster-access to the site
"},{"location":"dpl-platform/runbooks/retrieve-restore-backup/#procedure","title":"Procedure","text":"

Step overview:

  1. Download the backup
  2. Upload the backup to relevant pods
  3. Extract the backup.
  4. Cache clearing
  5. Cleanup

While most steps are different for file and database backups, step 1 is close to identical for the two guides.

Be aware that the guide instructs you to copy the backups to /tmp inside the cli pod. Depending on the resources available on the node /tmp may not have enough space in which case you may need to modify the cli deployment to add a temporary volume, or place the backup inside the existing /site/default/files folder.

"},{"location":"dpl-platform/runbooks/retrieve-restore-backup/#step-1-downloading-the-backup","title":"Step 1, downloading the backup","text":"

To download the backup access the Lagoon UI and schedule the retrieval of a backup. To do this,

  1. Log in to the environments Lagoon UI (consult the environment documentation for the url)
  2. Access the sites project
  3. Access the sites environment (\"Main\" for production)
  4. Click on the \"Backups\" tab
  5. Click on the \"Retrieve\" button for the backups you wish to download and/or restore. Use to \"Source\" column to differentiate the types of backups. \"nginx\" are backups of the sites files, while \"mariadb\" are backups of the sites database.
  6. The Buttons changes to \"Downloading...\" when pressed, wait for them to change to \"Download\", then click them again to download the backup
"},{"location":"dpl-platform/runbooks/retrieve-restore-backup/#step-2a-restore-a-database","title":"Step 2a, restore a database","text":"

To restore the database we must first copy the backup into a running cli-pod for a site, and then import the database-dump on top of the running site.

  1. Copy the uncompressed mariadb sql file you want to restore into the dpl-platform/infrastructure folder from which we will launch dplsh
  2. Launch dplsh from the infrastructure folder (instructions) and follow the procedure below:
# 1. Authenticate against the cluster.\n$ task cluster:auth\n\n# 2. List the namespaces to identify the sites namespace\n# The namespace will be on the form <sitename>-<branchname>\n# eg \"bib-rb-main\" for the \"main\" branch for the \"bib-rb\" site.\n$ kubectl get ns\n\n# 3. Export the name of the namespace as SITE_NS\n# eg.\n$ export SITE_NS=bib-rb-main\n\n# 4. Copy the *mariadb.sql file to the CLI pod in the sites namespace\n# eg.\nkubectl cp \\\n-n $SITE_NS  \\\n*mariadb.sql \\\n$(kubectl -n $SITE_NS get pod -l app.kubernetes.io/instance=cli -o jsonpath=\"{.items[0].metadata.name}\"):/tmp/database-backup.sql\n\n# 5. Have drush inside the CLI-pod import the database and clear out the backup\nkubectl exec \\\n-n $SITE_NS \\\ndeployment/cli \\\n-- \\\nbash -c \" \\\n         echo Verifying file \\\n      && test -s /tmp/database-backup.sql \\\n         || (echo database-backup.sql is missing or empty && exit 1) \\\n      && echo Dropping database \\\n      && drush sql-drop -y \\\n      && echo Importing backup \\\n      && drush sqlc < /tmp/database-backup.sql \\\n      && echo Clearing cache \\\n      && drush cr \\\n      && rm /tmp/database-backup.sql\n    \"\n
"},{"location":"dpl-platform/runbooks/retrieve-restore-backup/#step-2b-restore-a-sites-files","title":"Step 2b, restore a sites files","text":"

To restore backed up files into a site we must first copy the backup into a running cli-pod for a site, and then rsync the files on top top of the running site.

  1. Copy tar.gz file into the dpl-platform/infrastructure folder from which we will launch dplsh
  2. Launch dplsh from the infrastructure folder (instructions) and follow the procedure below:
# 1. Authenticate against the cluster.\n$ task cluster:auth\n\n# 2. List the namespaces to identify the sites namespace\n# The namespace will be on the form <sitename>-<branchname>\n# eg \"bib-rb-main\" for the \"main\" branch for the \"bib-rb\" site.\n$ kubectl get ns\n\n# 3. Export the name of the namespace as SITE_NS\n# eg.\n$ export SITE_NS=bib-rb-main\n\n# 4. Copy the files tar-ball into the CLI pod in the sites namespace\n# eg.\nkubectl cp \\\n-n $SITE_NS  \\\nbackup*-nginx-*.tar.gz \\\n$(kubectl -n $SITE_NS get pod -l app.kubernetes.io/instance=cli -o jsonpath=\"{.items[0].metadata.name}\"):/tmp/files-backup.tar.gz\n\n# 5. Replace the current files with the backup.\n# The following\n# - Verifies the backup exists\n# - Removes the existing sites/default/files\n# - Un-tars the backup into its new location\n# - Fixes permissions and clears the cache\n# - Removes the backup archive\n#\n# These steps can also be performed one by one if you want to.\nkubectl exec \\\n-n $SITE_NS \\\ndeployment/cli \\\n-- \\\nbash -c \" \\\n         echo Verifying file \\\n      && test -s /tmp/files-backup.tar.gz \\\n         || (echo files-backup.tar.gz is missing or empty && exit 1) \\\n      && tar ztf /tmp/files-backup.tar.gz data/nginx &> /dev/null \\\n         || (echo could not verify the tar.gz file files-backup.tar && exit 1) \\\n      && test -d /app/web/sites/default/files \\\n         || (echo Could not find destination /app/web/sites/default/files \\\n             && exit 1) \\\n      && echo Removing existing sites/default/files \\\n      && rm -fr /app/web/sites/default/files \\\n      && echo Unpacking backup \\\n      && mkdir -p /app/web/sites/default/files \\\n      && tar --strip 2 --gzip --extract --file /tmp/files-backup.tar.gz \\\n             --directory /app/web/sites/default/files data/nginx \\\n      && echo Fixing permissions \\\n      && chmod -R 777 /app/web/sites/default/files \\\n      && echo Clearing cache \\\n      && drush cr \\\n      && echo Deleting backup archive \\\n      && rm /tmp/files-backup.tar.gz\n    \"\n#  NOTE: In some situations some files in /app/web/sites/default/files might\n#  be locked by running processes. In that situations delete all the files you\n#  can from /app/web/sites/default/files manually, and then repeat the step\n#  above skipping the removal of /app/web/sites/default/files\n
"},{"location":"dpl-platform/runbooks/retrieve-sites-logs/","title":"Retrieve site logs","text":""},{"location":"dpl-platform/runbooks/retrieve-sites-logs/#when-to-use","title":"When to use","text":"

When you want to inspects the logs produced by as specific site

"},{"location":"dpl-platform/runbooks/retrieve-sites-logs/#prerequisites","title":"Prerequisites","text":"
  • Login credentials for Grafana. As a fallback the password for the admin can password can be fetched from the cluster if it has not been changed via
kubectl get secret \\\n--namespace grafana \\\n-o jsonpath=\"{.data.admin-password}\" \\\ngrafana \\\n| base64 -d\n

Consult the access-kubernetes Run book for instructions on how to access the cluster.

"},{"location":"dpl-platform/runbooks/retrieve-sites-logs/#procedure-loki-grafana","title":"Procedure - Loki / Grafana","text":"
  1. Access the environments Grafana installation - consult the platform-environments.md for the url.
  2. Select \"Explorer\" in the left-most menu and select the \"Loki\" data source in the top.
  3. Query the logs for the environment by either
  4. Use the Log Browser to pick the namespace for the site. It will follow the pattern <sitename>-<branchname>.
  5. Do a custom LogQL, eg. to fetch all logs from the nginx container for the site \"main\" branch of the \"rdb\" site do query on the form {app=\"nginx-php-persistent\",container=\"nginx\",namespace=\"rdb-main\"}
  6. Eg, for the main branch for the site \"rdb\": {namespace=\"rdb-main\"}
  7. Click \"Inspector\" -> \"Data\" -> \"Download Logs\" to download the log lines.
"},{"location":"dpl-platform/runbooks/run-a-lagoon-task/","title":"Run a Lagoon Task","text":""},{"location":"dpl-platform/runbooks/run-a-lagoon-task/#when-to-use","title":"When to use","text":"

When you need to run a Lagoon task

"},{"location":"dpl-platform/runbooks/run-a-lagoon-task/#prerequisites","title":"Prerequisites","text":"

You need access to the Lagoon UI

"},{"location":"dpl-platform/runbooks/run-a-lagoon-task/#procedure","title":"Procedure","text":"
  • Login to the Lagoon UI.
  • Navigate to the project you want to access.
  • Choose the environment you want to interact with.
  • Click the \"Tasks\" tab.
"},{"location":"dpl-platform/runbooks/scale-aks/","title":"Scaling AKS","text":""},{"location":"dpl-platform/runbooks/scale-aks/#when-to-use","title":"When to use","text":"

When the cluster is over or underprovisioned and needs to be scaled.

"},{"location":"dpl-platform/runbooks/scale-aks/#prerequisites","title":"Prerequisites","text":"
  • A running dplsh launched from ./infrastructure with DPLPLAT_ENV set to the platform environment name.
"},{"location":"dpl-platform/runbooks/scale-aks/#references","title":"References","text":"
  • For general information about AKS and node-pools https://learn.microsoft.com/en-us/azure/aks/use-multiple-node-pools
"},{"location":"dpl-platform/runbooks/scale-aks/#procedure","title":"Procedure","text":"

There are multiple approaches to scaling AKS. We run with the auto-scaler enabled which means that in most cases the thing you want to do is to adjust the max or minimum configuration for the autoscaler.

  • Adjusting the autoscaler
"},{"location":"dpl-platform/runbooks/scale-aks/#adjusting-the-autoscaler","title":"Adjusting the autoscaler","text":"

Edit the infrastructure configuration for your environment. Eg for dplplat01 edit dpl-platform/dpl-platform/infrastructure/environments/dplplat01/infrastructure/main.tf.

Adjust the *_count_min / *_count_min corrospodning to the node-pool you want to grow/shrink.

Then run infra:provision to have terraform effect the change.

task infra:provision\n
"},{"location":"dpl-platform/runbooks/set-environment-variable/","title":"Set an environment variable for a site","text":""},{"location":"dpl-platform/runbooks/set-environment-variable/#when-to-use","title":"When to use","text":"

When you wish to set an environment variable on a site. The variable can be available for either all sites in the project, or for a specific site in the project.

The variables are safe for holding secrets, and as such can be used both for \"normal\" configuration values, and secrets such as api-keys.

The variabel will be available to all containers in the environment can can be picked up and parsed eg. in Drupals settings.php.

"},{"location":"dpl-platform/runbooks/set-environment-variable/#prerequisites","title":"Prerequisites","text":"
  • A running dplsh with DPLPLAT_ENV set to the platform environment name and ssh-agent running if your ssh-keys are passphrase protected.
  • An user that has administrative privileges on the lagoon project in question.
"},{"location":"dpl-platform/runbooks/set-environment-variable/#procedure","title":"Procedure","text":"
# From within a dplsh session authorized to use your ssh keys:\n# 1. Authenticate against the cluster and lagoon\n$ task cluster:auth\n$ task lagoon:cli:config\n\n# 2. Refresh your Lagoon token.\n$ lagoon login\n\n# 3. Then get the project id by listing your projects\n$ lagoon list projects\n\n# 4. Optionally, if you wish to set a variable for a single environment - eg a\n# pull-request site, list the environments in the project\n$ lagoon list environments -p <project name>\n\n# 5. Finally, set the variable\n# - Set the the type_id to the id of the project or environment depending on\n#   whether you want all or a single environment to access the variable\n# - Set scope to VARIABLE_TYPE or ENVIRONMENT depending on the choice above\n# - set\n$ VARIABLE_TYPE_ID=<project or environment id> \\\nVARIABLE_TYPE=<PROJECT or ENVIRONMENT> \\\nVARIABLE_SCOPE=RUNTIME \\\nVARIABLE_NAME=<your variable name> \\\nVARIABLE_VALUE=<your variable value> \\\ntask lagoon:set:environment-variable\n\n# If you get a \"Invalid Auth Token\" your token has probably expired, generated a\n# new with \"lagoon login\" and try again.\n

The variable will be available the next time the site is deployed by Lagoon. Use the deployment runbook to trigger a deployment, use a forced deployment if you do not have a new release to deploy.

"},{"location":"dpl-platform/runbooks/update-upgrade-status/","title":"Update the support workload upgrade status","text":""},{"location":"dpl-platform/runbooks/update-upgrade-status/#when-to-use","title":"When to use","text":"

When you need to update the support workload version sheet.

"},{"location":"dpl-platform/runbooks/update-upgrade-status/#prerequisites","title":"Prerequisites","text":"
  • Access to the version status sheet
  • Access to run dplsh
"},{"location":"dpl-platform/runbooks/update-upgrade-status/#procedure","title":"Procedure","text":"

Run dplsh to extract the current and latest version for all support workloads

# First authenticate against the cluster\ntask cluster:auth\n# Then pull the status\ntask ops:get-versions\n

Then access the version status sheet and update the status from the output.

"},{"location":"dpl-platform/runbooks/upgrading-aks/","title":"Upgrading AKS","text":""},{"location":"dpl-platform/runbooks/upgrading-aks/#when-to-use","title":"When to use","text":"

When you want to upgrade Azure Kubernetes Service to a newer version.

"},{"location":"dpl-platform/runbooks/upgrading-aks/#prerequisites","title":"Prerequisites","text":"
  • A running dplsh launched from ./infrastructure with DPLPLAT_ENV set to the platform environment name.
  • Knowledge about the version of AKS you wish to upgrade to.
  • Consult AKS Kubernetes Release Calendar for a list of the various versions and when they are End of Life
"},{"location":"dpl-platform/runbooks/upgrading-aks/#references","title":"References","text":"
  • https://learn.microsoft.com/en-us/azure/aks/upgrade-cluster
  • https://learn.microsoft.com/en-us/azure/aks/use-multiple-node-pools#upgrade-a-node-pool
  • https://learn.microsoft.com/en-us/azure/aks/supported-kubernetes-versions
"},{"location":"dpl-platform/runbooks/upgrading-aks/#procedure","title":"Procedure","text":"

We use Terraform to upgrade AKS. Should you need to do a manual upgrade consult Azures documentation on upgrading a cluster and on upgrading node pools. Be aware in both cases that the Terraform state needs to be brought into sync via some means, so this is not a recommended approach.

"},{"location":"dpl-platform/runbooks/upgrading-aks/#find-out-which-versions-of-kubernetes-an-environment-can-upgrade-to","title":"Find out which versions of kubernetes an environment can upgrade to","text":"

In order to find out which versions of kubernetes we can upgrade to, we need to use the following command:

task cluster:get-upgrades\n

This will output a table of in which the column \"Upgrades\" lists the available upgrades for the highest available minor versions.

A Kubernetes cluster can can at most be upgraded to the nearest minor version, which means you may be in a situation where you have several versions between you and the intended version.

Minor versions can be skipped, and AKS will accept a cluster being upgraded to a version that does not specify a patch version. So if you for instance want to go from 1.20.9 to 1.22.15, you can do 1.21, and then 1.22.15. When upgrading to 1.21 Azure will substitute the version for an the hightest available patch version, e.g. 1.21.14.

You should know know which version(s) you need to upgrade to, and can continue to the actual upgrade.

"},{"location":"dpl-platform/runbooks/upgrading-aks/#ensuring-the-terraform-state-is-in-sync","title":"Ensuring the Terraform state is in sync","text":"

As we will be using Terraform to perform the upgrade we want to make sure it its state is in sync. Execute the following task and resolve any drift:

task infra:provision\n
"},{"location":"dpl-platform/runbooks/upgrading-aks/#upgrade-the-cluster","title":"Upgrade the cluster","text":"

Upgrade the control-plane:

  1. Update the control_plane_version reference in infrastructure/environments/<environment>/infrastructure/main.tf and run task infra:provision to apply. You can skip patch-versions, but you can only do one minor-version at the time

  2. Monitor the upgrade as it progresses. A control-plane upgrade is usually performed in under 5 minutes.

Monitor via eg.

watch -n 5 kubectl version\n

Then upgrade the system, admin and application node-pools in that order one by one.

  1. Update the pool_[name]_version reference in infrastructure/environments/<environment>/infrastructure/main.tf. The same rules applies for the version as with control_plane_version.

  2. Monitor the upgrade as it progresses. Expect the provisioning of and workload scheduling to a single node to take about 5-10 minutes. In particular be aware that the admin node-pool where harbor runs has a tendency to take a long time as the harbor pvcs are slow to migrate to the new node.

Monitor via eg.

watch -n 5 kubectl get nodes\n
"},{"location":"dpl-platform/runbooks/upgrading-lagoon/","title":"Upgrading Lagoon","text":""},{"location":"dpl-platform/runbooks/upgrading-lagoon/#when-to-use","title":"When to use","text":"

When there is a need to upgrade Lagoon to a new patch or minor version.

"},{"location":"dpl-platform/runbooks/upgrading-lagoon/#references","title":"References","text":"
  • Official Updating documentation.
  • Lagoon Releases
"},{"location":"dpl-platform/runbooks/upgrading-lagoon/#prerequisites","title":"Prerequisites","text":"
  • A running dplsh launched from ./infrastructure with DPLPLAT_ENV set to the platform environment name.
  • Knowledge about the version of Lagoon you want to upgrade to.
  • You can extract version (= chart version) and appVersion (= lagoon release version) for the lagoon-remote / lagoon-core charts via the following commands (replace lagoon-core for lagoon-remote if necessary).

Lagoon-core:

curl -s https://uselagoon.github.io/lagoon-charts/index.yaml \\\n| yq '.entries.lagoon-core[] | [.name, .appVersion, .version, .created] | @tsv'\n

Lagoon-remote:

curl -s https://uselagoon.github.io/lagoon-charts/index.yaml \\\n| yq '.entries.lagoon-remote[] | [.name, .appVersion, .version, .created] | @tsv'\n
  • Knowledge of any breaking changes or necessary actions that may affect the platform when upgrading. See chart release notes for all intermediate chart releases.
"},{"location":"dpl-platform/runbooks/upgrading-lagoon/#procedure","title":"Procedure","text":"
  1. Upgrade Lagoon core
    1. Backup the API and Keycloak dbs as described in the official documentation
    2. Bump the chart version VERSION_LAGOON_CORE in infrastructure/environments/<env>/lagoon/lagoon-versions.env
    3. Perform a helm diff
      • DIFF=1 task lagoon:provision:core
    4. Perform the actual upgrade
      • task lagoon:provision:core
  2. Upgrade Lagoon remote
    1. Bump the chart version VERSION_LAGOON_REMOTE in infrastructure/environments/dplplat01/lagoon/lagoon-versions.env
    2. Perform a helm diff
      • DIFF=1 task lagoon:provision:remote
    3. Perform the actual upgrade
      • task lagoon:provision:remote
    4. Take note in the output from Helm of any CRD updates that may be required
"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/","title":"Upgrading Support Workloads","text":""},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#when-to-use","title":"When to use","text":"

When you want to upgrade support workloads in the cluster. This includes.

  • Cert-manager
  • Grafana
  • Harbor
  • Ingress Nginx
  • K8up
  • Loki
  • Minio
  • Prometheus
  • Promtail

This document contains general instructions for how to upgrade support workloads, followed by specific instructions for each workload (linked above).

"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#prerequisites","title":"Prerequisites","text":"
  • Dplsh instance authorized against the cluster.
"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#general-procedure","title":"General Procedure","text":"
  1. Identify the version you want to bump in the environment/configuration directory eg. for dplplat01 infrastructure/environments/dplplat01/configuration/versions.env. The file contains links to the relevant Artifact Hub pages for the individual projects and can often be used to determine both the latest version, but also details about the chart such as how a specific manifest is used. You can find the latest version of the support workload in the Version status sheet which itself is updated via the procedure described in the Update Upgrade status runbook.

  2. Consult any relevant changelog to determine if the upgrade will require any extra work beside the upgrade itself. To determine which version to look up in the changelog, be aware of the difference between the chart version and the app version. We currently track the chart versions, and not the actual version of the application inside the chart. In order to determine the change in appVersion between chart releases you can do a diff between releases, and keep track of the appVersion property in the charts Chart.yaml. Using using grafana as an example: https://github.com/grafana/helm-charts/compare/grafana-6.55.1...grafana-6.56.0. The exact way to do this differs from chart to chart, and is documented in the Specific producedures and tests below.

  3. Carry out any chart-specific preparations described in the charts update-procedure. This could be upgrading a Custom Resource Definition that the chart does not upgrade.

  4. Identify the relevant task in the main Taskfile for upgrading the workload. For example, for cert-manager, the task is called support:provision:cert-manager and run the task with DIFF=1, eg DIFF=1 task support:provision:cert-manager.

  5. If the diff looks good, run the task without DIFF=1, eg task support:provision:cert-manager.

  6. Then proceeded to perform the verification test for the relevant workload. See the following section for known verification tests.

"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#specific-producedures-and-tests","title":"Specific producedures and tests","text":"
  • Cert-manager
  • Grafana
  • Harbor
  • Ingress Nginx
  • K8up
  • Loki
  • Minio
  • Prometheus
  • Promtail
"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#cert-manager","title":"Cert Manager","text":""},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#comparing-cert-manager-versions","title":"Comparing cert-manager versions","text":"

The project project versions its Helm chart together with the app itself. So, simply use the chart version in the following checks.

Cert Manager keeps Release notes for the individual minor releases of the project. Consult these for every upgrade past a minor version.

As both are versioned in the same repository, simply use the following link for looking up the release notes for a specific patch release, replacing the example tag with the version you wish to upgrade to.

https://github.com/cert-manager/cert-manager/releases/tag/v1.11.2

To compare two reversions, do the same using the following link:

https://github.com/cert-manager/cert-manager/compare/v1.11.1...v1.11.2

"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#upgrade-cert-manager","title":"Upgrade cert-manager","text":"

Commands

# Diff\nDIFF=1 task support:provision:cert-manager\n\n# Upgrade\ntask support:provision:cert-manager\n
"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#verify-cert-manager-upgrade","title":"Verify cert-manager upgrade","text":"

Verify that cert-manager itself and webhook pods are all running and healthy.

task support:verify:cert-manager\n
"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#grafana","title":"Grafana","text":""},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#comparing-grafana-versions","title":"Comparing Grafana versions","text":"

Insert the chart version in the following link to see the release note.

https://github.com/grafana/helm-charts/releases/tag/grafana-6.52.9

The note will most likely be empty. Now diff the chart version with the current version, again replacing the version with the relevant for your releases.

https://github.com/grafana/helm-charts/compare/grafana-6.43.3...grafana-6.52.9

As the repository contains a lot of charts, you will need to do a bit of digging. Look for at least charts/grafana/Chart.yaml which can tell you the app version.

With the app-version in hand, you can now look at the release notes for the grafana app itself.

"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#upgrade-grafana","title":"Upgrade grafana","text":"

Diff command

DIFF=1 task support:provision:grafana\n

Upgrade command

task support:provision:grafana\n
"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#verify-grafana-upgrade","title":"Verify grafana upgrade","text":"

Verify that the Grafana pods are all running and healthy.

kubectl get pods --namespace grafana\n

Access the Grafana UI and see if you can log in. If you do not have a user but have access to read secrets in the grafana namespace, you can retrive the admin password with the following command:

# Password for admin\nUI_NAME=grafana task ui-password\n\n# Hostname for grafana\nkubectl -n grafana get -o jsonpath=\"{.spec.rules[0].host}\" ingress grafana ; echo\n
"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#harbor","title":"Harbor","text":""},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#comparing-harbor-versions","title":"Comparing Harbor versions","text":"

Harbor has different app and chart versions.

An overview of the chart versions can be retrived from Github. the chart does not have a changelog.

Link for comparing two chart releases: https://github.com/goharbor/harbor-helm/compare/v1.10.1...v1.12.0

Having identified the relevant appVersions, consult the list of Harbor releases to see a description of the changes included in the release in question. If this approach fails you can also use the diff-command described below to determine which image-tags are going to change and thus determine the version delta.

Harbor is a quite active project, so it may make sense mostly to pay attention to minor/major releases and ignore the changes made in patch-releases.

"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#upgrade-harbor","title":"Upgrade Harbor","text":"

Harbor documents the general upgrade procedure for non-kubernetes upgrades for minor versions on their website. This documentation is of little use to our Kubernetes setup, but it can be useful to consult the page for minor/major version upgrades to see if there are any special considerations to be made.

The Harbor chart repository has upgrade instructions as well. The instructions asks you to do a snapshot of the database and backup the tls secret. Snapshotting the database is currently out of scope, but could be a thing that is considered in the future. The tls secret is handled by cert-manager, and as such does not need to be backed up.

With knowledge of the app version, you can now update versions.env as described in the General Procedure section, diff to see the changes that are going to be applied, and finally do the actual upgrade.

Diff command

DIFF=1 task support:provision:harbor\n

Upgrade command

task support:provision:harbor\n
"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#verify-harbor-upgrade","title":"Verify Harbor upgrade","text":"

First verify that pods are coming up

kubectl -n harbor get pods\n

When Harbor seems to be working, you can verify that the UI is working by accessing https://harbor.lagoon.dplplat01.dpl.reload.dk/. The password for the user admin can be retrived with the following command:

UI_NAME=harbor task ui-password\n

If everything looks good, you can consider to deploying a site. One way to do this is to identify an existing site of low importance, and re-deploy it. A re-deploy will require Lagoon to both fetch and push images. Instructions for how to access the lagoon UI is out of scope of this document, but can be found in the runbook for running a lagoon task. In this case you are looking for the \"Deploy\" button on the sites \"Deployments\" tab.

"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#ingress-nginx","title":"Ingress-nginx","text":""},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#comparing-ingress-nginx-versions","title":"Comparing ingress-nginx versions","text":"

When working with the ingress-nginx chart we have at least 3 versions to keep track off.

The chart version tracks the version of the chart itself. The charts appVersion tracks a controller application which dynamically configures a bundles nginx. The version of nginx used is determined configuration-files in the controller. Amongst others the ingress-nginx.yaml.

Link for diffing two chart versions: https://github.com/kubernetes/ingress-nginx/compare/helm-chart-4.6.0...helm-chart-4.6.1

The project keeps a quite good changelog for the chart

Link for diffing two controller versions: https://github.com/kubernetes/ingress-nginx/compare/controller-v1.7.1...controller-v1.7.0

Consult the individual GitHub releases for descriptions of what has changed in the controller for a given release.

"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#upgrade-ingress-nginx","title":"Upgrade ingress-nginx","text":"

With knowledge of the app version, you can now update versions.env as described in the General Procedure section, diff to see the changes that are going to be applied, and finally do the actual upgrade.

Diff command

DIFF=1 task support:provision:ingress-nginx\n

Upgrade command

task support:provision:ingress-nginx\n
"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#verify-ingress-nginx-upgrade","title":"Verify ingress-nginx upgrade","text":"

The ingress-controller is very central to the operation of all public accessible parts of the platform. It's area of resposibillity is on the other hand quite narrow, so it is easy to verify that it is working as expected.

First verify that pods are coming up

kubectl -n ingress-nginx get pods\n

Then verify that the ingress-controller is able to serve traffic. This can be done by accessing the UI of one of the apps that are deployed in the platform.

Access eg. https://ui.lagoon.dplplat01.dpl.reload.dk/.

"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#k8up","title":"K8up","text":"

We can currently not upgrade to version 2.x of K8up as Lagoon is not yet ready

"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#loki","title":"Loki","text":""},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#comparing-loki-versions","title":"Comparing Loki versions","text":"

The Loki chart is versioned separatly from Loki. The version of Loki installed by the chart is tracked by its appVersion. So when upgrading, you should always look at the diff between both the chart and app version.

The general upgrade procedure will give you the chart version, access the following link to get the release note for the chart. Remember to insert your version:

https://github.com/grafana/loki/releases/tag/helm-loki-5.5.1

Notice that the Loki helm-chart is maintained in the same repository as Loki itself. You can find the diff between the chart versions by comparing two chart release tags.

https://github.com/grafana/loki/compare/helm-loki-5.5.0...helm-loki-5.5.1

As the repository contains changes to Loki itself as well, you should seek out the file production/helm/loki/Chart.yaml which contains the appVersion that defines which version of Loki a given chart release installes.

Direct link to the file for a specific tag: https://github.com/grafana/loki/blob/helm-loki-3.3.1/production/helm/loki/Chart.yaml

With the app-version in hand, you can now look at the release notes for Loki to see what has changed between the two appVersions.

Last but not least the Loki project maintains a upgrading guide that can be found here: https://grafana.com/docs/loki/latest/upgrading/

"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#upgrade-loki","title":"Upgrade Loki","text":"

Diff command

DIFF=1 task support:provision:loki\n

Upgrade command

task support:provision:loki\n
"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#verify-loki-upgrade","title":"Verify Loki upgrade","text":"

List pods in the loki namespace to see if the upgrade has completed successfully.

  kubectl --namespace loki get pods\n

Next verify that Loki is still accessibel from Grafana and collects logs by logging in to Grafana. Then verify the Loki datasource, and search out some logs for a site. See the validation steps for Grafana for instructions on how to access the Grafana UI.

"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#minio","title":"MinIO","text":"

We can currently not upgrade MinIO without loosing the Azure blob gateway. see:

  • https://blog.min.io/deprecation-of-the-minio-gateway/
  • https://github.com/minio/minio/issues/14331
  • https://github.com/bitnami/charts/issues/10258#issuecomment-1132929451
"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#prometheus","title":"Prometheus","text":""},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#comparing-prometheus-versions","title":"Comparing Prometheus versions","text":"

The kube-prometheus-stack helm chart is quite well maintained and is versioned and developed separately from the application itself.

A specific release of the chart can be accessed via the following link:

https://github.com/prometheus-community/helm-charts/releases/tag/kube-prometheus-stack-45.27.2

The chart is developed alongside a number of other community driven prometheus- related charts in https://github.com/prometheus-community/helm-charts.

This means that the following comparison between two releases of the chart will also contain changes to a number of other charts. You will have to look for changes in the charts/kube-prometheus-stack/ directory.

https://github.com/prometheus-community/helm-charts/compare/kube-prometheus-stack-45.26.0...kube-prometheus-stack-45.27.2

"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#upgrade-prometheus","title":"Upgrade Prometheus","text":"

The Readme for the chart contains a good Upgrading Chart section that describes things to be aware of when upgrading between specific minor and major versions. The same documentation can also be found on artifact hub.

Consult the section that matches the version you are upgrading from and to. Be aware that upgrades past a minor version often requires a CRD update. The CRDs may have to be applied before you can do the diff and upgrade. Once the CRDs has been applied you are committed to the upgrade as there is no simple way to downgrade the CRDs.

Diff command

DIFF=1 task support:provision:prometheus\n

Upgrade command

task support:provision:prometheus\n
"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#verify-prometheus-upgrade","title":"Verify Prometheus upgrade","text":"

List pods in the prometheus namespace to see if the upgrade has completed successfully. You should expect to see two types of workloads. First a single a single promstack-kube-prometheus-operator pod that runs Prometheus, and then a promstack-prometheus-node-exporter pod for each node in the cluster.

  kubectl --namespace prometheus get pods -l \"release=promstack\"\n

As the Prometheus UI is not directly exposed, the easiest way to verify that Prometheus is running is to access the Grafana UI and verify that the dashboards that uses Prometheus are working, or as a minimum that the prometheus datasource passes validation. See the validation steps for Grafana for instructions on how to access the Grafana UI.

"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#promtail","title":"Promtail","text":""},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#comparing-promtail-versions","title":"Comparing Promtail versions","text":"

The Promtail chart is versioned separatly from Promtail which itself is a part of Loki. The version of Promtail installed by the chart is tracked by its appVersion. So when upgrading, you should always look at the diff between both the chart and app version.

The general upgrade procedure will give you the chart version, access the following link to get the release note for the chart. Remember to insert your version:

https://github.com/grafana/helm-charts/releases/tag/promtail-6.6.0

The note will most likely be empty. Now diff the chart version with the current version, again replacing the version with the relevant for your releases.

https://github.com/grafana/helm-charts/compare/promtail-6.6.0...promtail-6.6.1

As the repository contains a lot of charts, you will need to do a bit of digging. Look for at least charts/promtail/Chart.yaml which can tell you the app version.

With the app-version in hand, you can now look at the release notes for Loki (which promtail is part of). Look for notes in the Promtail sections of the release notes.

"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#upgrade-promtail","title":"Upgrade Promtail","text":"

Diff command

DIFF=1 task support:provision:promtail\n

Upgrade command

task support:provision:promtail\n
"},{"location":"dpl-platform/runbooks/upgrading-support-workloads/#verify-promtail-upgrade","title":"Verify Promtail upgrade","text":"

List pods in the promtail namespace to see if the upgrade has completed successfully.

  kubectl --namespace promtail get pods\n

With the pods running, you can verify that the logs are being collected seeking out logs via Grafana. See the validation steps for Grafana for details on how to access the Grafana UI.

You can also inspect the logs of the individual pods via

kubectl --namespace promtail logs -l \"app.kubernetes.io/name=promtail\"\n

And verify that there are no obvious error messages.

"},{"location":"dpl-platform/runbooks/using-dplsh/","title":"Using the DPL Shell","text":"

The Danish Public Libraries Shell is a container-based shell used by platform- operators for all cli operations.

"},{"location":"dpl-platform/runbooks/using-dplsh/#when-to-use","title":"When to use","text":"

Whenever you perform any administrative actions on the platform. If you want to know more about the shell itself? Refer to tools/dplsh.

"},{"location":"dpl-platform/runbooks/using-dplsh/#prerequisites","title":"Prerequisites","text":"
  • Docker
  • jq
  • Bash 4 or newer
  • An authorized Azure az cli. The version should match the version found in FROM mcr.microsoft.com/azure-cli:version in the dplsh Dockerfile You can choose to authorize the az cli from within dplsh, but your session will only last as long as the shell-session. The use you authorize as must have permission to read the Terraform state from the Terraform setup , and Contributor permissions on the environments resource-group in order to provision infrastructure.
  • dplsh.sh symlinked into your path as dplsh, see Launching the Shell (optional, but assumed below)
"},{"location":"dpl-platform/runbooks/using-dplsh/#procedure","title":"Procedure","text":"
# Launch dplsh.\n$ cd infrastructure\n$ dplsh\n\n# 1. Set an environment,\n# export DPLPLAT_ENV=<platform environment name>\n# eg.\n$ export DPLPLAT_ENV=dplplat01\n\n# 2a. Authenticate against AKS, needed by infrastructure and Lagoon tasks\n$ task cluster:auth\n\n# 2b - if you want to use the Lagoon CLI)\n# The Lagoon CLI is authenticated via ssh-keys. DPLSH will mount your .ssh\n# folder from your homedir, but if your keys are passphrase protected, we need\n# to unlock them.\n$ eval $(ssh-agent); ssh-add\n# Then authorize the lagoon cli\n$ task lagoon:cli:config\n
"},{"location":"dpl-react/","title":"DPL React","text":"

A set of React components and applications providing self-service features for Danish public libraries.

"},{"location":"dpl-react/#development","title":"Development","text":""},{"location":"dpl-react/#requirements","title":"Requirements","text":"
  • go-task
  • Docker
  • Dory

Before you can install the project you need to create the file ~/.npmrc to access the GitHub package registry as described using a personal access token. The token must be created with the required scopes: repo and read:packages

If you have npm installed locally this can be achieved by running the following command and using the token when prompted for password.

npm login --registry=https://npm.pkg.github.com\n
"},{"location":"dpl-react/#howto","title":"Howto","text":"
  1. Run task dev:start
  2. Access Storybook at http://dpl-react.docker
"},{"location":"dpl-react/#alternative-without-docker","title":"Alternative without Docker","text":"
  1. Add 127.0.0.1 dpl-react.docker to your /etc/hosts file
  2. Ensure that your node version matches what is specified in package.json.
  3. Install dependencies: yarn install
  4. Start storybook sudo yarn start:storybook:dev
  5. If you need to log in through Adgangsplatformen, you need to change your url to http://dpl-react.docker/ instead of http://localhost. This avoids getting log in errors
"},{"location":"dpl-react/#step-debugging-in-visual-studio-code-no-docker","title":"Step Debugging in Visual Studio Code (no docker)","text":"

If you want to enable step debugging you need to:

  • Copy .vscode.example/launch.json into .vscode/
  • Mark 1 or more breakpoints on a line in the left gutter on an open file
  • In the top menu in VS Code choose: Run -> Start Debugging
  • Type in your user password if ask to
  • Start debugging \ud83e\udd16\u223f\ud83d\udcbb
"},{"location":"dpl-react/#access-tokens","title":"Access tokens","text":"

Access token must be retrieved from Adgangsplatformen, a single sign-on solution for public libraries in Denmark, and OpenPlatform, an API for danish libraries.

Usage of these systems require a valid client id and secret which must be obtained from your library partner or directly from DBC, the company responsible for running Adgangsplatformen and OpenPlatform.

This project include a client id that matches the storybook setup which can be used for development purposes. You can use the /auth story to sign in to Adgangsplatformen for the storybook context.

(Note: if you enter Adgangsplatformen again after signing it, you will get signed out, and need to log in again. This is not a bug, as you stay logged in otherwise.)

"},{"location":"dpl-react/#library-token","title":"Library token","text":"

To test the apps that is indifferent to wether the user is authenticated or not it is possible to set a library token via the library component in Storybook. Workflow:

  • Retrieve a library token via OpenPlatform
  • Insert the library token in the Library Token story in storybook
"},{"location":"dpl-react/#standard-and-style","title":"Standard and style","text":""},{"location":"dpl-react/#javascript-jsx","title":"JavaScript + JSX","text":"

For static code analysis we make use of the Airbnb JavaScript Style Guide and for formatting we make use of Prettier with the default configuration. The above choices have been influenced by a multitude of factors:

  • Historically Drupal core have been making use of the Airbnb JavaScript Style Guide.
  • Airbnb's standard is comparatively the best known and one of the most used in the JavaScript coding standard landscape.

This makes future adoption easier for onboarding contributors and support is to be expected for a long time.

"},{"location":"dpl-react/#named-functions-vs-anonymous-arrow-functions","title":"Named functions Vs. Anonymous arrow functions","text":"

AirBnB's only guideline towards this is that anonymous arrow function are preferred over the normal anonymous function notation.

When you must use an anonymous function (as when passing an inline callback), use arrow function notation.

Why? It creates a version of the function that executes in the context of this, which is usually what you want, and is a more concise syntax.

Why not? If you have a fairly complicated function, you might move that logic out into its own named function expression.

Reference

This project stick to the above guideline as well. If we need to pass a function as part of a callback or in a promise chain and we on top of that need to pass some contextual variables that is not passed implicit from either the callback or the previous link in the promise chain we want to make use of an anonymous arrow function as our default.

This comes with the build in disclaimer that if an anonymous function isn't required the implementer should heavily consider moving the logic out into it's own named function expression.

The named function is primarily desired due to it's easier to debug nature in stacktraces.

"},{"location":"dpl-react/#create-a-new-application","title":"Create a new application","text":"1. Create a new application component
// ./src/apps/my-new-application/my-new-application.jsx\nimport React from \"react\";\nimport PropTypes from \"prop-types\";\nexport function MyNewApplication({ text }) {\nreturn (\n<h2>{text}</h2>\n);\n}\nMyNewApplication.defaultProps = {\ntext: \"The fastest man alive!\"\n};\nMyNewApplication.propTypes = {\ntext: PropTypes.string\n};\nexport default MyNewApplication;\n
2. Create the entry component
// ./src/apps/my-new-application/my-new-application.entry.jsx\nimport React from \"react\";\nimport PropTypes from \"prop-types\";\nimport MyNewApplication from \"./my-new-application\";\n// The props of an entry is all of the data attributes that were\n// set on the DOM element. See the section on \"Naive app mount.\" for\n// an example.\nexport function MyNewApplicationEntry(props) {\nreturn <MyNewApplication text='Might be from a server?' />;\n}\nexport default MyNewApplicationEntry;\n
3. Create the mount
// ./src/apps/my-new-application/my-new-application.mount.js\nimport addMount from \"../../core/addMount\";\nimport MyNewApplication from \"./my-new-application.entry\";\naddMount({ appName: \"my-new-application\", app: MyNewApplication });\n
4. Add a story for local development
// ./src/apps/my-new-application/my-new-application.dev.jsx\nimport React from \"react\";\nimport MyNewApplicationEntry from \"./my-new-application.entry\";\nimport MyNewApplication from \"./my-new-application\";\nexport default { title: \"Apps|My new application\" };\nexport function Entry() {\n// Testing the version that will be shipped.\nreturn <MyNewApplicationEntry />;\n}\nexport function WithoutData() {\n// Play around with the application itself without server side data.\nreturn <MyNewApplication />;\n}\n
5. Run the development environment
  yarn dev\n
OR depending on your dev environment (docker or not)
  sudo yarn dev\n

Voila! You browser should have opened and a storybook environment is ready for you to tinker around.

"},{"location":"dpl-react/#application-state-machine","title":"Application state-machine","text":"

Most applications will have multiple internal states, so to aid consistency, it's recommended to:

  const [status, setStatus] = useState(\"<initial state>\");\n

and use the following states where appropriate:

initial: Initial state for applications that require some sort of initialization, such as making a request to see if a material can be ordered, before rendering the order button. Errors in initialization can go directly to the failed state, or add custom states for communication different error conditions to the user. Should render either nothing or as a skeleton/spinner/message.

ready: The general \"ready state\". Applications that doesn't need initialization (a generic button for instance) can use ready as the initial state set in the useState call. This is basically the main waiting state.

processing: The application is taking some action. For buttons this will be the state used when the user has clicked the button and the application is waiting for reply from the back end. More advanced applications may use it while doing backend requests, if reflecting the processing in the UI is desired. Applications using optimistic feedback will render this state the same as the finished state.

failed: Processing failed. The application renders an error message.

finished: End state for one-shot actions. Communicates success to the user.

Applications can use additional states if desired, but prefer the above if appropriate.

"},{"location":"dpl-react/#style-your-application","title":"Style your application","text":"1. Create an application specific stylesheet
// ./src/apps/my-new-application/my-new-application.scss\n.dpl-warm {\ncolor: maroon;\n}\n
2. Add the class to your application
// ./src/apps/my-new-application/my-new-application.jsx\nimport React from \"react\";\nimport PropTypes from \"prop-types\";\nexport function MyNewApplication({ text }) {\nreturn (\n<h2 className='warm'>{text}</h2>\n);\n}\nMyNewApplication.defaultProps = {\ntext: \"The fastest man alive!\"\n};\nMyNewApplication.propTypes = {\ntext: PropTypes.string\n};\nexport default MyNewApplication;\n
3. Import the scss into your story
// ./src/apps/my-new-application/my-new-application.dev.jsx\nimport React from \"react\";\nimport MyNewApplicationEntry from \"./my-new-application.entry\";\nimport MyNewApplication from \"./my-new-application\";\nimport './my-new-application.scss';\nexport default { title: \"Apps|My new application\" };\nexport function Entry() {\n// Testing the version that will be shipped.\nreturn <MyNewApplicationEntry />;\n}\nexport function WithoutData() {\n// Play around with the application itself without server side data.\nreturn <MyNewApplication />;\n}\n

Cowabunga! You now got styling in your application

"},{"location":"dpl-react/#style-using-the-dpl-design-system","title":"Style using the DPL design system","text":"

This project includes styling created by its sister repository - the design system as a npm package.

By default the project should include a release of the design system matching the current state of the project.

To update the design system to the latest stable release of the design system run:

yarn add @danskernesdigitalebibliotek/dpl-design-system@latest\n

This command installs the latest released version of the package. Whenever a new version of the design system package is released, it is necessary to reinstall the package in this project using the same command to get the newest styling, because yarn adds a specific version number to the package name in package.json.

"},{"location":"dpl-react/#using-unreleased-design","title":"Using unreleased design","text":"

If you need to work with published but unreleased code from a specific branch of the design system, you can also use the branch name as the tag for the npm package, replacing all special characters with dashes (-).

Example: To use the latest styling from a branch in the design system called feature/availability-label, run:

yarn add @danskernesdigitalebibliotek/dpl-design-system@feature-availability-label\n

If the branch resides in a fork (usually before a pull request is merged) you can use aliasing and run:

yarn config set \"@my-fork:registry\" \"https://npm.pkg.github.com\"\nyarn add @danskernesdigitalebibliotek/dpl-design-system@npm:@my-fork/dpl-design-system@feature-availability-label\n

If the branch is updated and you want the latest changes to take effect locally update the release used:

yarn upgrade @danskernesdigitalebibliotek/dpl-design-system\n

Note that references to unreleased code should never make it into official versions of the project.

"},{"location":"dpl-react/#cross-application-components","title":"Cross application components","text":"

If the component is simple enough to be a primitive you would use in multiple occasions it's called an 'atom'. Such as a button or a link. If it's more specific that that and to be used across apps we just call it a component. An example would be some type of media presented alongside a header and some text.

The process when creating an atom or a component is more or less similar, but some structural differences might be needed.

"},{"location":"dpl-react/#creating-an-atom","title":"Creating an atom","text":"1. Create the atom
// ./src/components/atoms/my-new-atom/my-new-atom.jsx\nimport React from \"react\";\nimport PropTypes from 'prop-types';\n/**\n * A simple button.\n *\n * @export\n * @param {object} props\n * @returns {ReactNode}\n */\nexport function MyNewAtom({ className, children }) {\nreturn <button className={`btn ${className}`}>{children}</button>;\n}\nMyNewAtom.propTypes = {\nclassName: PropTypes.string,\nchildren: PropTypes.node.isRequired\n}\nMyNewAtom.defaultProps = {\nclassName: \"\"\n}\nexport default MyNewAtom;\n
2. Create styles for the atom
// ./src/components/atoms/my-new-atom/my-new-atom.scss\n.dpl-btn {\ncolor: blue;\n}\n
3. Import the atom's styles into the component stylesheet
// ./src/components/components.scss\n@import 'atoms/button/button.scss';\n@import 'atoms/my-new-atom/my-new-atom.scss';\n
4. Create a story for your atom
// ./src/components/atoms/my-new-atom/my-new-atom.dev.jsx\nimport React from \"react\";\nimport MyNewAtom from \"./my-new-atom\";\nexport default { title: \"Atoms|My new atom\" };\nexport function WithText() {\nreturn <MyNewAtom>Cick me!</MyNewAtom>;\n}\n
5. Import the atom into the applications or other components where you would want to use it
// ./src/apps/my-new-application/my-new-application.jsx\nimport React, {Fragment} from \"react\";\nimport PropTypes from \"prop-types\";\nimport MyNewAtom from \"../../components/atom/my-new-atom/my-new-atom\"\nexport function MyNewApplication({ text }) {\nreturn (\n<Fragment>\n<h2 className='warm'>{text}</h2>\n<MyNewAtom className='additional-class' />\n</Fragment>\n);\n}\nMyNewApplication.defaultProps = {\ntext: \"The fastest man alive!\"\n};\nMyNewApplication.propTypes = {\ntext: PropTypes.string\n};\nexport default MyNewApplication;\n

Finito! You now know how to share code across applications

"},{"location":"dpl-react/#creating-a-component","title":"Creating a component","text":"

Repeat all of the same steps as with an atom but place it in it's own directory inside components.

Such as ./src/components/my-new-component/my-new-component.jsx

"},{"location":"dpl-react/#editor-example-configuration","title":"Editor example configuration","text":"

If you use Code we provide some easy to use and nice defaults for this project. They are located in .vscode.example. Simply rename the directory from .vscode.example to .vscode and you are good to go. This overwrites your global user settings for this workspace and suggests som extensions you might want.

"},{"location":"dpl-react/#usage","title":"Usage","text":"

There are two ways to use the components provided by this project:

  1. As standalone JavaScript applications mounted within HTML pages generated by a separate system.
  2. As components within a larger JavaScript application (Under development)
"},{"location":"dpl-react/#naive-app-mount","title":"Naive app mount","text":"

So let's say you wanted to make use of an application within an existing HTML page such as what might be generated serverside by platforms like Drupal, WordPress etc.

For this use case you should download the dist.zip package from the latest release of the project and unzip somewhere within the web root of your project. The package contains a set of artifacts needed to use one or more applications within an HTML page.

HTML Example A simple example of the required artifacts and how they are used looks like this:
<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n<title>Naive mount</title>\n<!-- Include CSS files to provide default styling -->\n<link rel=\"stylesheet\" href=\"/dist/components.css\">\n</head>\n<body>\n<b>Here be dragons!</b>\n<!-- Data attributes will be camelCased on the react side aka.\n         props.errorText and props.text -->\n<div data-dpl-app='add-to-checklist' data-text=\"Chromatic dragon\"\ndata-error-text=\"Minor mistake\"></div>\n<div data-dpl-app='a-none-existing-app'></div>\n<!-- Load order og scripts is of importance here -->\n<script src=\"/dist/runtime.js\"></script>\n<script src=\"/dist/bundle.js\"></script>\n<script src=\"/dist/mount.js\"></script>\n<!-- After the necessary scripts you can start loading applications -->\n<script src=\"/dist/add-to-checklist.js\"></script>\n<script>\n// For making successful requests to the different services we need one or\n// more valid tokens.\nwindow.dplReact.setToken(\"user\",\"XXXXXXXXXXXXXXXXXXXXXX\");\nwindow.dplReact.setToken(\"library\",\"YYYYYYYYYYYYYYYYYYYYYY\");\n// If this function isn't called no apps will display.\n// An app will only be displayed if there is a container for it\n// and a corresponding application loaded.\nwindow.dplReact.mount(document);\n</script>\n</body>\n</html>\n

As a minimum you will need the runtime.js and bundle.js. For styling of atoms and components you will need to import components.css.

Each application also has its own JavaScript artifact and it might have a CSS artifact as well. Such as add-to-checklist.js and add-to-checklist.css.

To mount the application you need an HTML element with the correct data attribute.

<div data-dpl-app='add-to-checklist'></div>\n

The name of the data attribute should be data-dpl-app and the value should be the name of the application - the value of the appName parameter assigned in the application .mount.js file.

"},{"location":"dpl-react/#data-attributes-and-props","title":"Data attributes and props","text":"

As stated above, every application needs the corresponding data-dpl-app attribute to even be mounted and shown on the page. Additional data attributes can be passed if necessary. Examples would be contextual ids etc. Normally these would be passed in by the serverside platform e.g. Drupal, Wordpress etc.

<div data-dpl-app='add-to-checklist' data-id=\"870970-basis:54172613\"\ndata-error-text=\"A mistake was made\"></div>\n

The above data-id would be accessed as props.id and data-error-text as props.errorText in the entrypoint of an application.

Example
// ./src/apps/my-new-application/my-new-application.entry.jsx\nimport React from \"react\";\nimport PropTypes from \"prop-types\";\nimport MyNewApplication from './my-new-application.jsx';\nexport function MyNewApplicationEntry({ id }) {\nreturn (\n<MyNewApplication\n// 870970-basis:54172613\nid={id}\n/>\n}\nexport default MyNewApplicationEntry;\n

To fake this in our development environment we need to pass these same data attributes into our entrypoint.

Example
// ./src/apps/my-new-application/my-new-application.dev.jsx\nimport React from \"react\";\nimport MyNewApplicationEntry from \"./my-new-application.entry\";\nimport MyNewApplication from \"./my-new-application\";\nexport default { title: \"Apps|My new application\" };\nexport function Entry() {\n// Testing the version that will be shipped.\nreturn <MyNewApplicationEntry id=\"870970-basis:54172613\" />;\n}\nexport function WithoutData() {\n// Play around with the application itself without server side data.\nreturn <MyNewApplication />;\n}\n
"},{"location":"dpl-react/#extending-the-project","title":"Extending the project","text":"

If you want to extend this project - either by introducing new components or expand the functionality of the existing ones - and your changes can be implemented in a way that is valuable to users in general, please submit pull requests.

Even if that is not the case and you have special needs the infrastructure of the project should also be helpful to you.

In such a situation you should fork this project and extend it to your own needs by implementing new applications. New applications can reuse various levels of infrastructure provided by the project such as:

  1. Integration with various webservices
  2. User authentication and token management
  3. Visual atoms or components
  4. Visual representations of existing applications
  5. Styling using SCSS
  6. Test infrastructure
  7. Application mounting

Once the customization is complete the result can be packaged for distribution by pushing the changes to the forked repository:

  1. Changes pushed to the master branch of the forked repository will automatically update the latest release of the fork.
  2. Tags pushed to the forked repository also will be published as new releases in the fork.

The result can be used in the same ways as the original project.

"},{"location":"dpl-react/campaigns/","title":"Campaigns","text":"

Campaigns are elements that are shown on the search result page above the search result list. There are three types of campaigns:

  1. Full campaigns - containing an image and a some text.
  2. Text-only campaigns - they don't show any images.
  3. Image-only campaigns - they don't show any text.

However, they are only shown in case certain criteria are met. We check for this by contacting the dpl-cms API.

"},{"location":"dpl-react/campaigns/#how-campaign-setup-works-in-dpl-cms","title":"How campaign setup works in dpl-cms","text":"

Dpl-cms is a cms system based on Drupal, where the system administrators can set up campaigns they want to show to their users. Drupal also allows the cms system to act as an API endpoint that we then can contact from our apps.

The cms administrators can specify the content (image, text) and the visibility criteria for each campaign they create. The visibility criteria is based on search filter facets. Does that sound familiar? Yes, we use another API to get that very data in THIS project - in the search result app. The facets differ based on the search string the user uses for their search.

As an example, the dpl-cms admin could wish to show a Harry Potter related campaign to all the users whose search string retreives search facets which have \"Harry Potter\" as one of the most relevant subjects. Campaigns in dpl-cms can use triggers such as subject, main language, etc.

"},{"location":"dpl-react/campaigns/#react-code-example","title":"React code example","text":"

An example code snippet for retreiving a campaign from our react apps would then look something like this:

  // Creating a state to store the campaign data in.\nconst [campaignData, setCampaignData] = useState<CampaignMatchPOST200 | null>(\nnull\n);\n// Retreiving facets for the campaign based on the search query and existing\n// filters.\nconst { facets: campaignFacets } = useGetFacets(q, filters);\n// Using the campaign hook generated by Orval from src/core/dpl-cms/dpl-cms.ts\n// in order to get the mutate function that lets us retreive campaigns.\nconst { mutate } = useCampaignMatchPOST();\n// Only fire the campaign data call if campaign facets or the mutate function\n// change their value.\nuseDeepCompareEffect(() => {\nif (campaignFacets) {\nmutate(\n{\ndata: campaignFacets as CampaignMatchPOSTBodyItem[]\n},\n{\nonSuccess: (campaign) => {\nsetCampaignData(campaign);\n},\nonError: () => {\n// Handle error.\n}\n}\n);\n}\n}, [campaignFacets, mutate]);\n
"},{"location":"dpl-react/campaigns/#showing-campaigns-in-dpl-react-when-in-development-mode","title":"Showing campaigns in dpl-react when in development mode","text":"

You first need to make sure to have a campaign set up in your locally running dpl-cms ( run this repo locally) Then, in order to see campaigns locally in dpl-react in development mode, you will most likely need a browser plugin such as Google Chrome's \"Allow CORS: Access-Control-Allow-Origin\" in order to bypass CORS policy for API data calls.

"},{"location":"dpl-react/code_guidelines/","title":"React Code guidelines","text":"

The following guidelines describe best practices for developing code for React components for the Danish Public Libraries CMS project. The guidelines should help achieve:

  • A stable, secure and high quality foundation for building and maintaining client-side JavaScript components for library websites
  • Consistency across multiple developers participating in the project
  • The best possible conditions for sharing components between library websites
  • The best possible conditions for the individual library website to customize configuration and appearance

Contributions to the DDB React project will be reviewed by members of the Core team. These guidelines should inform contributors about what to expect in such a review. If a review comment cannot be traced back to one of these guidelines it indicates that the guidelines should be updated to ensure transparency.

"},{"location":"dpl-react/code_guidelines/#coding-standards","title":"Coding standards","text":"

The project follows the Airbnb JavaScript Style Guide and Airbnb React/JSX Style Guide. This choice is based on multiple factors:

  1. Historically the community of developers working with DDB has ties to the Drupal project. Drupal has adopted the Airbnb JavaScript Style Guide so this choice should ensure consistency between the two projects.
  2. Airbnb's standard is one of the best known and most used in the JavaScript ccoding standard landscape.
  3. Airbnb\u2019s standard is both comprehensive and well documented.
  4. Airbnb\u2019s standards cover both JavaScript in general React/JSX specifically. This avoids potential conflicts between multiple standards.

The following lists significant areas where the project either intentionally expands or deviates from the official standards or areas which developers should be especially aware of.

"},{"location":"dpl-react/code_guidelines/#general","title":"General","text":"
  • The default language for all code and comments is English.
  • Components must be compatible with the latest stable version of the following browsers:
  • Desktop
    • Microsoft Edge
    • Google Chrome
    • Safari
    • Firefox
  • Mobile
    • Google Chrome
    • Safari
    • Firefox
    • Samsung Browser
"},{"location":"dpl-react/code_guidelines/#javascript","title":"JavaScript","text":""},{"location":"dpl-react/code_guidelines/#named-functions-vs-anonymous-arrow-functions","title":"Named functions vs. anonymous arrow functions","text":"

AirBnB's only guideline towards this is that anonymous arrow function nation is preferred over the normal anonymous function notation.

This project sticks to the above guideline as well. If we need to pass a function as part of a callback or in a promise chain and we on top of that need to pass some contextual variables that are not passed implicitly from either the callback or the previous link in the promise chain we want to make use of an anonymous arrow function as our default.

This comes with the build in disclaimer that if an anonymous function isn't required the implementer should heavily consider moving the logic out into its own named function expression.

The named function is primarily desired due to it's easier to debug nature in stacktraces.

"},{"location":"dpl-react/code_guidelines/#react","title":"React","text":"
  • Configuration must be passed as props for components. This allows the host system to modify how a component works when it is inserted.
  • All components should be provided with skeleton screens. This ensures that the user interface reflects the final state even when data is loaded asynchronously. This reduces load time frustration.
  • Components should be optimistic. Unless we have reason to believe that an operation may fail we should provide fast response to users.
  • All interface text must be implemented as props for components. This allows the host system to provide a suitable translation/version when using the component.
"},{"location":"dpl-react/code_guidelines/#css","title":"CSS","text":"
  • All classes must have the dpl- prefix. This makes them distinguishable from classes provided by the host system.
  • Class names should follow the Block-Element-Modifier architecture.
  • Components must use and/or provide a default style sheet which at least provides a minimum of styling showing the purpose of the component.
  • Elements must be provided with meaningful classes even though they are not targeted by the default style sheet. This helps host systems provide additional styling of the components. Consider how the component consists of blocks and elements with modifiers and how these can be nested within each other.
  • Components must use SCSS for styling. The project uses PostCSS and PostCSS-SCSS within Webpack for processing.
"},{"location":"dpl-react/code_guidelines/#html","title":"HTML","text":"
  • Components must use semantic HTML5 markup.
  • Components must provide configuration to set a top headline level for the component. This helps provide a proper document outline to ensure the accessibility of the system.
"},{"location":"dpl-react/code_guidelines/#naming","title":"Naming","text":""},{"location":"dpl-react/code_guidelines/#files","title":"Files","text":"

Files provided by components must be placed in the following folders and have the extensions defined here.

  • Components (React applications)
  • apps/[component-name]/[component-name].jsx
    • Core JSX component.
  • components/[component-name]/[component-name].scss
    • Stylesheet for the component.
  • apps/[component-name]/[component-name].entry.jsx
    • Main application entrypoint.
    • This will usually also be where state management is implemented.
    • This must not include the default stylesheet.
  • apps/[component-name]/[component-name].dev.jsx
    • Storybook entry for the component.
    • If the component has a stylesheet this must also be included here.
  • apps/[component-name]/[component-name].mount.js
    • Code for registering the application to be booted when a page is loaded on the host system.
  • apps/[component-name]/[component-name].test.js
    • Test of the component implemented with Cypress
  • Reusable elements (React components)
  • components/[component-name]/[component-name].dev.jsx
  • components/[component-name]/[component-name].jsx
  • components/[component-name]/[component-name].scss
  • Reusable functions and classes
  • core/[function].js
  • core/[Class].js
"},{"location":"dpl-react/code_guidelines/#third-party-code","title":"Third party code","text":"

The project uses Yarn as a package manager to handle code which is developed outside the project repository. Such code must not be committed to the Core project repository.

When specifying third party package versions the project follows these guidelines:

  • Use the ^ next significant release operator for packages which follow semantic versioning.
  • The version specified must be the latest known working and secure version. We do not want accidental downgrades.
  • We want to allow easy updates to all working releases within the same major version.
  • Packages which are not intended to be executed at runtime in the production environment should be marked as development dependencies.
"},{"location":"dpl-react/code_guidelines/#reusing-dependencies","title":"Reusing dependencies","text":"

Components must reuse existing dependencies in the project before adding new ones which provide similar functionality. This ensures consistency and avoids unnecessary increases in the package size of the project.

The reasoning behind the choice of key dependencies have been documented in the architecture directory.

"},{"location":"dpl-react/code_guidelines/#altering-third-party-code","title":"Altering third party code","text":"

The project uses patches rather than forks to modify third party packages. This makes maintenance of modified packages easier and avoids a collection of forked repositories within the project.

  • Use an appropriate method for the corresponding package manager for managing the patch.
  • Patches should be external by default. In rare cases it may be needed to commit them as a part of the project.
  • When providing a patch you must document the origin of the patch e.g. through an url in a commit comment or preferably in the package manager configuration for the project.
"},{"location":"dpl-react/code_guidelines/#code-comments","title":"Code comments","text":"

Code comments which describe what an implementation does should only be used for complex implementations usually consisting of multiple loops, conditional statements etc.

Inline code comments should focus on why an unusual implementation has been implemented the way it is. This may include references to such things as business requirements, odd system behavior or browser inconsistencies.

"},{"location":"dpl-react/code_guidelines/#commit-messages","title":"Commit messages","text":"

Commit messages in the version control system help all developers understand the current state of the code base, how it has evolved and the context of each change. This is especially important for a project which is expected to have a long lifetime.

Commit messages must follow these guidelines:

  1. Each line must not be more than 72 characters long
  2. The first line of your commit message (the subject) must contain a short summary of the change. The subject should be kept around 50 characters long.
  3. The subject must be followed by a blank line
  4. Subsequent lines (the body) should explain what you have changed and why the change is necessary. This provides context for other developers who have not been part of the development process. The larger the change the more description in the body is expected.
  5. If the commit is a result of an issue in a public issue tracker, platform.dandigbib.dk, then the subject must start with the issue number followed by a colon (:). If the commit is a result of a private issue tracker then the issue id must be kept in the commit body.

When creating a pull request the pull request description should not contain any information that is not already available in the commit messages.

Developers are encouraged to read How to Write a Git Commit Message by Chris Beams.

"},{"location":"dpl-react/code_guidelines/#tool-support","title":"Tool support","text":"

The project aims to automate compliance checks as much as possible using static code analysis tools. This should make it easier for developers to check contributions before submitting them for review and thus make the review process easier.

The following tools pay a key part here:

  1. Eslint with the following rulesets and plugins:
    1. Airbnb JavaScript Style Guide
    2. Airbnb React/JSX Style Guide
    3. Prettier
    4. Cypress
  2. Stylelint with the following rulesets and plugins
    1. Recommended SCSS
    2. Prettier
    3. BEM support

In general all tools must be able to run locally. This allows developers to get quick feedback on their work.

Tools which provide automated fixes are preferred. This reduces the burden of keeping code compliant for developers.

Code which is to be exempt from these standards must be marked accordingly in the codebase - usually through inline comments (Eslint, Stylelint). This must also include a human readable reasoning. This ensures that deviations do not affect future analysis and the project should always pass through static analysis.

If there are discrepancies between the automated checks and the standards defined here then developers are encouraged to point this out so the automated checks or these standards can be updated accordingly.

"},{"location":"dpl-react/code_guidelines/#writing-frontend-tests","title":"Writing frontend tests","text":"

The frontend tests are executed in Cypress.

The test files are placed alongside the application components and are named following pattern: \"*.test.ts\". Eg.: material.test.ts.

"},{"location":"dpl-react/code_guidelines/#test-structuring","title":"Test structuring","text":"

After quite a lot of bad experiences with unstable tests and reading both the official documentation and articles about the best practices we have ended up with a recommendation of how to write the tests.

According to this article it is important to distinguish between commands and assertions. Commands are used in the beginning of a statement and yields a chainable element that can be followed by one or more assertions in the end.

So first we target an element. Next we can make one or more assertions on the element.

We have created some helper commands for targeting an element: getBySel, getBySelLike and getBySelStartEnd. They look for elements as advised by the Selecting Elements section from the Cypress documentation about best practices.

Example of a statement:

// Targeting.\ncy.getBySel(\"reservation-success-title-text\")\n// Assertion.\n.should(\"be.visible\")\n// Another assertion.\n.and(\"contain\", \"Material is available and reserved for you!\");\n
"},{"location":"dpl-react/code_guidelines/#writing-unit-tests","title":"Writing Unit Tests","text":"

We are using Vitest as framework for running unit tests. By using that we can test functions (and therefore also hooks) and classes.

"},{"location":"dpl-react/code_guidelines/#where-do-i-place-my-tests","title":"Where do I place my tests?","text":"

They have to be placed in src/tests/unit.

Or they can also be placed next to the code at the end of a file as described here.

export const sum = (...numbers: number[]) =>\nnumbers.reduce((total, number) => total + number, 0);\nif (import.meta.vitest) {\nconst { describe, expect, it } = import.meta.vitest;\ndescribe(\"sum\", () => {\nit(\"should sum numbers\", () => {\nexpect(sum(1, 2, 3)).toBe(6);\n});\n});\n}\n

In that way it helps us to test and mock unexported functions.

"},{"location":"dpl-react/code_guidelines/#testing-hooks","title":"Testing hooks","text":"

For testing hooks we are using the library @testing-library/react-hooks and you can also take a look at the text test to see how it can be done.

"},{"location":"dpl-react/error_handling/","title":"Error Handling","text":"

Error handling is something that is done on multiple levels: Eg.: Form validation, network/fetch error handling, runtime errors. You could also argue that the fact that the codebase is making use of typescript and code generation from the various http services (graphql/REST) belongs to the same idiom of error handling in order to make the applications more robust.

"},{"location":"dpl-react/error_handling/#error-boundary","title":"Error Boundary","text":"

Error boundary was introduced in React 16 and makes it possible to implement a \"catch all\" feature catching \"uncatched\" errors and replacing the application with a component to the users that something went wrong. It is meant ato be a way of having a safety net and always be able to tell the end user that something went wrong. The apps are being wrapped in the error boundary handling which makes it possible to catch thrown errors at runtime.

"},{"location":"dpl-react/error_handling/#fetch-and-error-boundary","title":"Fetch and Error Boundary","text":"

Async operations and therby also fetch are not being handled out of the box by the React Error Boundary. But fortunately react-query, which is being used both by the REST services (Orval) and graphql (Graphql Code Generator), has a way of addressing the problem. The QueryClient can be configured to trigger the Error Boundary system if an error is thrown. So that is what we are doing.

"},{"location":"dpl-react/error_handling/#fetch-error-classes","title":"Fetch error classes","text":"

Two different types of error classes have been made in order to handle errors in the fetchers: http errors and fetcher errors.

Http errors are the ones originating from http errors and have a status code attached.

Fetcher errors are whatever else bad that could apart from http errors. Eg. JSON parsing gone wrong.

Both types of errors comes in two variants: \"normal\" and \"critical\". The idea is that only critical errors will trigger an Error Boundary.

For instance if you look at the DBC Gateway fetcher it throws a DbcGateWayHttpError in case of a http error occurs. DbcGateWayHttpError extends the FetcherCriticalHttpError which makes sure to trigger the Error Boundary system.

"},{"location":"dpl-react/error_handling/#using-errorname","title":"Using Error.name","text":"

The reason why *.name is set in the errors is to make it clear which error was thrown. If we don't do that the name of the parent class is used in the error log. And then it is more difficult to trace where the error originated from.

"},{"location":"dpl-react/error_handling/#future-considerations","title":"Future considerations","text":"

The initial implementation is handling http errors on a coarse level: If response.ok is false then throw an error. If the error is critical the error boundary is triggered. In future version you could could take more things into consideration regarding the error:

  • Should all status codes trigger an error?
  • Should we have different types of error level depending on request and/or http method?
"},{"location":"dpl-react/fbs-adapter-client/","title":"FBS adapter client","text":"

The FBS client adapter is autogenerated base the swagger 1.2 json files from FBS. But Orval requires that we use swagger version 2.0 and the adapter has some changes in paths and parameters. So some conversion is need at the time of this writing.

FBS documentation can be found here.

All this will hopefully be changed when/or if the adapter comes with its own specifications.

"},{"location":"dpl-react/fbs-adapter-client/#api-spec-converter","title":"API spec converter","text":"

A repository dpl-fbs-adapter-tool tool build around PHP and NodeJS can translate the FBS specifikation into one usable for Orval client generator. It also filters out all the FBS calls not need by the DPL project.

The tool uses go-task to simply the execution of the command.

"},{"location":"dpl-react/fbs-adapter-client/#setup","title":"Setup","text":"

Simple use the installation task.

task dev:install\n
"},{"location":"dpl-react/fbs-adapter-client/#convert-swagger","title":"Convert swagger","text":"

First convert the swagger 1.2 (located in /fbs/externalapidocs) to swagger 2.0 using the api-spec-converter tool.

dev:swagger2yaml\n
"},{"location":"dpl-react/fbs-adapter-client/#build-the-adapter-specifications","title":"Build the Adapter specifications","text":"

Build the swagger specification usable by Orval then run Orval.

task dev:convert\n
"},{"location":"dpl-react/fbs-adapter-client/#fbs-adapter","title":"FBS Adapter","text":"

The FSB adapter lives at: https://github.com/DBCDK/fbs-cms-adapter

"},{"location":"dpl-react/skeleton_screens/","title":"Skeleton screens","text":"

In order to improve both UX and the performance score you can choose to use skeleton screens in situation where you need to fill the interface with data from a requests to an external service.

"},{"location":"dpl-react/skeleton_screens/#main-purpose","title":"Main Purpose","text":"

The skeleton screens are being showed instantly in order to deliver some content to the end user fast while loading data. When the data is arriving the skeleton screens are being replaced with the real data.

"},{"location":"dpl-react/skeleton_screens/#how-to-use-it","title":"How to use it","text":"

The skeleton screens are rendered with help from the skeleton-screen-css library. By using ssc classes you can easily compose screens that simulate the look of a \"real\" rendering with real data.

"},{"location":"dpl-react/skeleton_screens/#example","title":"Example","text":"

In this example we are showing a search result item as a skeleton screen. The skeleton screen consists of a cover, a headline and two lines of text. In this case we wanted to maintain the styling of the .card-list-item wrapper. And show the skeleton screen elements by using ssc classes.

```tsx import React from \"react\";

const SearchResultListItemSkeleton: React.FC = () => { return ( ); };

export default SearchResultListItemSkeleton;

"},{"location":"dpl-react/ui_text_handling/","title":"UI Text Handling","text":"

This document describes how to use the text functionality that is partly defined in src/core/utils/text.tsx and in src/core/text.slice.ts.

"},{"location":"dpl-react/ui_text_handling/#main-purpose","title":"Main Purpose","text":"

The main purpose of the functionality is to be able to access strings defined at app level inside of sub components without passing them all the way down via props. You can read more about the decision and considerations here.

"},{"location":"dpl-react/ui_text_handling/#how-to-use-it","title":"How to use it","text":"

In order to use the system the component that has the text props needs to be wrapped with the withText high order function. The texts can hereafter be accessed by using the useText hook.

"},{"location":"dpl-react/ui_text_handling/#simple-example","title":"Simple example","text":"

In this example we have a HelloWorld app with three text props attached:

import React from \"react\";\nimport { withText } from \"../../core/utils/text\";\nimport HelloWorld from \"./hello-world\";\n\nexport interface HelloWorldEntryProps {\n  titleText: string;\n  introductionText: string;\n  whatText: string;\n}\n\nconst HelloWorldEntry: React.FC<HelloWorldEntryProps> = (\n  props: HelloWorldEntryProps\n) => <HelloWorld />;\n\nexport default withText(HelloWorldEntry);\n

Now it is possible to access the strings like this:

import * as React from \"react\";\nimport { Hello } from \"../../components/hello/hello\";\nimport { useText } from \"../../core/utils/text\";\n\nconst HelloWorld: React.FC = () => {\n  const t = useText();\n  return (\n    <article>\n      <h2>{t(\"titleText\")}</h2>\n      <p>{t(\"introductionText\")}</p>\n      <p>\n        <Hello shouldBeEmphasized />\n      </p>\n    </article>\n  );\n};\nexport default HelloWorld;\n
"},{"location":"dpl-react/ui_text_handling/#placeholder-example","title":"Placeholder example","text":"

It is also possible to use placeholders in the text strings. They can be handy when you want dynamic values embedded in the text.

A classic example is the welcome message to the authenticated user. Let's say you have a text with the key: welcomeMessageText. The value from the data prop is: Welcome @username, today is @date. You would the need to reference it like this:

import * as React from \"react\";\nimport { useText } from \"../../core/utils/text\";\n\nconst HelloUser: React.FC = () => {\n  const t = useText();\n  const username = getUsername();\n  const currentDate = getCurrentDate();\n\n  const message = t(\"welcomeMessageText\", {\n    placeholders: {\n      \"@user\": username,\n      \"@date\": currentDate\n    }\n  });\n\n  return (\n    <div>{message}</div>\n  );\n};\nexport default HelloUser;\n
"},{"location":"dpl-react/ui_text_handling/#plural-example","title":"Plural example","text":"

Sometimes you want two versions of a text be shown depending on if you have one or multiple items being referenced in the text.

That can be accommodated by using the plural text definition.

Let's say that an authenticated user has a list of unread messages in an inbox. You could have a text key called: inboxStatusText. The value from the data prop is:

{\"type\":\"plural\",\"text\":[\"You have 1 message in the inbox\",\n\"You have @count messages in the inbox\"]}.\n

You would then need to reference it like this:

import * as React from \"react\";\nimport { useText } from \"../../core/utils/text\";\n\nconst InboxStatus: React.FC = () => {\n  const t = useText();\n  const user = getUser();\n  const inboxMessageCount = getUserInboxMessageCount(user);\n\n  const status = t(\"inboxStatusText\", {\n    count: inboxMessageCount,\n    placeholders: {\n      \"@count\": inboxMessageCount\n    }\n  });\n\n  return (\n    <div>{status}</div>\n    // If count == 1 the texts will be:\n    // \"You have 1 message in the inbox\"\n\n    // If count == 5  the texts will be:\n    // \"You have 5 messages in the inbox\"\n  );\n};\nexport default InboxStatus;\n
"},{"location":"dpl-react/architecture/adr-001-rehydration/","title":"Architecture Decision Record: Rehydration","text":""},{"location":"dpl-react/architecture/adr-001-rehydration/#context","title":"Context","text":"

We are not able to persist and execute a users intentions across page loads. This is expressed through a number of issues. The main agitator is maintaining intent whenever a user tries to do anything that requires them to be authenticated. In these situations they get redirected off the page and after a successful login they get redirected back to the origin page but without the intended action fulfilled.

One example is the AddToChecklist functionality. Whenever a user wants to add a material to their checklist they click the \"Tilf\u00f8j til huskelist\" button next to the material presentation. They then get redirected to Adgangsplatformen. After a successful login they get redirected back to the material page but the material has not been added to their checklist.

"},{"location":"dpl-react/architecture/adr-001-rehydration/#decision","title":"Decision","text":"

After an intent has been stated we want the intention to be executed even though a page reload comes in the way.

We move to implementing what we define as an explicit intention before the actual action is tried for executing.

  1. User clicks the button.
  2. Intent state is generated and committed.
  3. Implementation checks if the intended action meets all the requirements. In this case, being logged in and having the necessary payload.
  4. If the intention meets all requirements we then fire the addToChecklist action.
  5. Material is added to the users checklist.

The difference between the two might seem superfluous but the important distinction to make is that with our current implementation we are not able to serialize and persist the actions as the application state across page loads. By defining intent explicitly we are able to serialize it and persist it between page loads.

This resolves in the implementation being able to rehydrate the persisted state, look at the persisted intentions and have the individual application implementations decide what to do with the intention.

A mock implementation of the case by case business logic looks as follows.

const initialStore = {\n  authenticated: false,\n  intent: {\n    status: '',\n    payload: {}\n  }\n}\n\nconst fulfillAction = store.authenticated && \n    (store.intent.status === 'pending' || store.intent.status === 'tried')\nconst getRequirements = !store.authenticated && store.intent.status === 'pending'\nconst abandonIntention = !store.authenticated && store.intent.status === 'tried'\n\nfunction AddToChecklist ({ materialId, store }) {\n  useEffect(() => {\n    if (fulfillAction) {\n      // We fire the actual functionality required to add a material to the \n      // checklist and we remove the intention as a result of it being\n      // fulfilled.\n      addToChecklistAction(store, materialId)\n    } else if (getRequirements) {\n      // Before we redirect we set the status to be \"tried\".\n      redirectToLogin(store)\n    } else if (abandonIntention) {\n      // We abandon the intent so that we won't have an infinite loop of retries\n      // at every page load.\n      abandonAddToChecklistIntention(store)\n    }\n  }, [materialId, store.intent.status])\n  return (\n    <button\n      onClick={() => {\n        // We do not fire the actual logic that is required to add a material to\n        // the checklist. Instead we add the intention of said action to the\n        // store. This is when we would set the status of the intent to pending\n        // and provide the payload.\n        addToChecklistIntention(store, materialId)\n      }}\n    >\n      Tilf\u00f8j til huskeliste\n    </button>\n  )\n}\n

We utilize session storage to persist the state on the client due to it's short lived nature and porous features.

We choose Redux as the framework to implemenent this. Redux is a blessed choice in this instance. It has widespread use, an approachable design and is well-documented. The best way to go about a current Redux implementation as of now is @reduxjs/toolkit. Redux is a sufficiently advanced framework to support other uses of application state and even co-locating shared state between applications.

For our persistence concerns we want to use the most commonly used tool for that, redux-persist. There are some implementation details to take into consideration when integrating the two.

"},{"location":"dpl-react/architecture/adr-001-rehydration/#alternatives-considered","title":"Alternatives considered","text":""},{"location":"dpl-react/architecture/adr-001-rehydration/#persistence-in-url","title":"Persistence in URL","text":"

We could persist the intentions in the URL that is delivered back to the client after a page reload. This would still imply some of the architectural decisions described in Decision in regards to having an \"intent\" state, but some of the different status flags etc. would not be needed since state is virtually shared across page loads in the url. However this simpler solution cannot handle more complex situations than what can be described in the URL feasibly.

"},{"location":"dpl-react/architecture/adr-001-rehydration/#usecontext","title":"useContext","text":"

React offers useContext() for state management as an alternative to Redux.

We prefer Redux as it provides a more complete environment when working with state management. There is already a community of established practices and libraries which integrate with Redux. One example of this is our need to persist actions. When using Redux we can handle this with redux-persist. With useContext() we would have to roll our own implementation.

Some of the disadvantages of using Redux e.g. the amount of required boilerplate code are addressed by using @reduxjs/toolkit.

"},{"location":"dpl-react/architecture/adr-001-rehydration/#status","title":"Status","text":"

Accepted

"},{"location":"dpl-react/architecture/adr-001-rehydration/#consequences","title":"Consequences","text":"
  • We are able to support most if not all of our rehydration cases and therefore pick up user flow from where we left it.
  • Heavy degree of complexity is added to tasks that requires an intention instead of a simple action.
  • Saving the immediate state to the session storage makes for yet another place to \"clear cache\".
"},{"location":"dpl-react/architecture/adr-002-ui-text-handling/","title":"Architecture Decision Record: UI Text Handling","text":""},{"location":"dpl-react/architecture/adr-002-ui-text-handling/#context","title":"Context","text":"

It has been decided that app context/settings should be passed from the server side rendered mount points via data props. One type of settings is text strings that is defined by the system/host rendering the mount points. Since we are going to have quite some levels of nested components it would be nice to have a way to extract the string without having to define them all the way down the tree.

"},{"location":"dpl-react/architecture/adr-002-ui-text-handling/#decision","title":"Decision","text":"

A solution has been made that extracts the props holding the strings and puts them in the Redux store under the index: text at the app entry level. That is done with the help of the withText() High Order Component. The solution of having the strings in redux enables us to fetch the strings at any point in the tree. A hook called: useText() makes it simple to request a certain string inside a given component.

"},{"location":"dpl-react/architecture/adr-002-ui-text-handling/#alternatives-considered","title":"Alternatives considered","text":"

One major alternative would be not doing it and pass down the props. But that leaves us with text props all the way down the tree which we would like to avoid. Some translation libraries has been investigated but that would in most cases give us a lot of tools and complexity that is not needed in order to solve the relatively simple task.

"},{"location":"dpl-react/architecture/adr-002-ui-text-handling/#consequences","title":"Consequences","text":"

Since we omit the text props down the tree it leaves us with fewer props and a cleaner component setup. Although some \"magic\" has been introduced with text prop matching and storage in redux, it is outweighed by the simplicity of the HOC wrapper and useText hook.

"},{"location":"dpl-react/architecture/adr-003-downshift/","title":"Architecture Decision Record: Downshift","text":""},{"location":"dpl-react/architecture/adr-003-downshift/#context","title":"Context","text":"

As a part of the project, we need to implement a dropdown autosuggest component. This component needs to be complient with modern website accessibility rules:

  • The component dropdown items are accessible by keyboard by using arrow keys - down and up.
  • The items visibly change when they are in current focus.
  • Items are selectable using the keyboard.

Apart from these accessibility features, the component needs to follow a somewhat complex design described in this Figma file. As visible in the design this autosuggest dropdown doesn't consist only of single line text items, but also contains suggestions for specific works - utilizing more complex suggestion items with cover pictures, and release years.

"},{"location":"dpl-react/architecture/adr-003-downshift/#decision","title":"Decision","text":"

Our research on the most popular and supported javascript libraries heavily leans on this specific article. In combination with our needs described above in the context section, but also considering what it would mean to build this component from scratch without any libraries, the decision taken favored a library called Downshift.

This library is the second most popular JS library used to handle autsuggest dropdowns, multiselects, and select dropdowns with a large following and continuous support. Out of the box, it follows the ARIA principles, and handles problems that we would normally have to solve ourselves (e.g. opening and closing of the dropdown/which item is currently in focus/etc.).

Another reason why we choose Downshift over its peer libraries is the amount of flexibility that it provides. In our eyes, this is a strong quality of the library that allows us to also implement more complex suggestion dropdown items.

"},{"location":"dpl-react/architecture/adr-003-downshift/#alternatives-considered","title":"Alternatives considered","text":""},{"location":"dpl-react/architecture/adr-003-downshift/#building-the-autosuggest-dropdown-not-using-javascript-libraries","title":"Building the autosuggest dropdown not using javascript libraries","text":"

In this case, we would have to handle accessibility and state management of the component with our own custom solutition.

"},{"location":"dpl-react/architecture/adr-003-downshift/#status","title":"Status","text":"

Accepted.

"},{"location":"dpl-react/architecture/adr-003-downshift/#consequences","title":"Consequences","text":"
  • We are able to comply with ARIA accesibility design principles for autosuggest dropdowns/comboboxes.
  • We introduced complexity to the project for initial project integration of the library.
  • After initial integration, this library can be utilized for all other select, multiselect, and autosuggest/combobox solutions.
"},{"location":"dpl-react/architecture/adr-004-relative-ci/","title":"Architecture Decision Record: RelativeCI","text":""},{"location":"dpl-react/architecture/adr-004-relative-ci/#context","title":"Context","text":"

Staying informed about how the size of the JavaScript we require browsers to download to use the project plays an important part in ensuring a performant solution.

We currently have no awareness of this in this project and the result surfaces down the line when the project is integrated with the CMS, which is tested with Lighthouse.

To address this we want a solution that will help us monitor the changes to the size of the bundle we ship for each PR.

"},{"location":"dpl-react/architecture/adr-004-relative-ci/#decision","title":"Decision","text":"

We add integration to RelativeCI to the project. RelativeCI supports our primary use case and has a number of qualities which we value:

  • Support for GitHub actions and reporting as GitHub status checks
  • Support for fork-based development workflows
  • A free tier for open source projects
  • Other types of analysis e.g. duplicate packages, continual monitoring
"},{"location":"dpl-react/architecture/adr-004-relative-ci/#alternatives-considered","title":"Alternatives considered","text":""},{"location":"dpl-react/architecture/adr-004-relative-ci/#bundlewatch","title":"Bundlewatch","text":"

Bundlewatch and its ancestor, bundlesize combine a CLI tool and a web app to provide bundle analysis and feedback on GitHub as status checks.

These solutions no longer seem to be actively maintained. There are several bugs that would affect us and fixes remain unmerged. The project relies on a custom secret instead of GITHUB_TOKEN. This makes supporting our fork-based development workflow harder.

"},{"location":"dpl-react/architecture/adr-004-relative-ci/#bundle-comparison","title":"Bundle comparison","text":"

This is a GitHub Action which can be used in configurations where statistics for two bundles are compared e.g. for the base and head of a pull request. This results in a table of changes displayed as a comment in the pull request. This is managed using GITHUB_TOKEN.

"},{"location":"dpl-react/architecture/adr-004-relative-ci/#status","title":"Status","text":"

Accepted.

"},{"location":"dpl-react/architecture/adr-004-relative-ci/#consequences","title":"Consequences","text":"
  • We can determine the effect of adding a new JavaScript library to our project
  • We add another dependency to a third party system
"},{"location":"dpl-react/architecture/adr-005-react-use/","title":"Architecture Decision Record: React Use","text":""},{"location":"dpl-react/architecture/adr-005-react-use/#context","title":"Context","text":"

The decision of obtaining react-use as a part of the project originated from the problem that arose from having an useEffect hook with an object as a dependency.

useEffect does not support comparison of objects or arrays and we needed a method for comparing such natives.

"},{"location":"dpl-react/architecture/adr-005-react-use/#decision","title":"Decision","text":"

We decided to go for the react-use package react-use. The reason is threefold:

  • It could solve the problem with deep comparison of dependencies by using useDeepCompareEffect
  • It offered an alternative to the react-hook-inview viewport handling. So we did not need to use two packages.
  • It has a range of other utility hooks that we can make use of in the future.
"},{"location":"dpl-react/architecture/adr-005-react-use/#alternatives-considered","title":"Alternatives considered","text":"

We could have used our own implementation of the problem. But since it is a common problem we might as well use a community backed solution. And react-use gives us a wealth of other tools.

"},{"location":"dpl-react/architecture/adr-005-react-use/#consequences","title":"Consequences","text":"

We can now use useDeepCompareEffect instead of useEffect in cases where we have arrays or objects amomg the dependencies. And we can make use of all the other utility hooks that the package provides.

"},{"location":"dpl-react/architecture/adr-006-unit-tests/","title":"Architecture Decision Record: Unit Tests","text":""},{"location":"dpl-react/architecture/adr-006-unit-tests/#context","title":"Context","text":"

The code base is growing and so does the number of functions and custom hooks.

While we have a good coverage in our UI tests from Cypress we are lacking something to tests the inner workings of the applications.

With unit tests added we can test bits of functionality that is shared between different areas of the application and make sure that we get the expected output giving different variations of input.

"},{"location":"dpl-react/architecture/adr-006-unit-tests/#decision","title":"Decision","text":"

We decided to go for Vitest which is an easy to use and very fast unit testing tool.

It has more or less the same capabilities as Jest which is another popular testing framework which is similar.

Vitest is framework agnostic so in order to make it possible to test hooks we found @testing-library/react-hooks that works in conjunction with Vitest.

"},{"location":"dpl-react/architecture/adr-006-unit-tests/#alternatives-considered","title":"Alternatives considered","text":"

We could have used Jest. But trying that we experienced major problems with having both Jest and Cypress in the same codebase. They have colliding test function names and Typescript could not figure it out.

There is probably a solution but at the same time we got Vitest recommended. It seemed very fast and just as capable as Jest. And we did not have the colliding issues of shared function/object names.

"},{"location":"dpl-react/architecture/adr-006-unit-tests/#consequences","title":"Consequences","text":"

We now have unit test as a part of the mix which brings more stability and certainty that the individual pieces of the application work.

"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 00000000..0f8724ef --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 00000000..ffb5d56a Binary files /dev/null and b/sitemap.xml.gz differ