diff --git a/config.yml b/config.yml --- a/config.yml +++ b/config.yml @@ -35,3 +35,4 @@ - "src/assets/js/app.js" - "src/assets/js/docker-registry.js" - "src/assets/js/servers-log.js" + - "src/assets/js/salt-config.js" diff --git a/src/assets/js/salt-config.js b/src/assets/js/salt-config.js new file mode 100644 --- /dev/null +++ b/src/assets/js/salt-config.js @@ -0,0 +1,378 @@ +/* ------------------------------------------------------------- + Nasqueron infrastructure + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Project: Nasqueron + Author: Sébastien Santoro aka Dereckson + Dependencies: jQuery + Filename: salt-config.js + Licence: CC-BY 4.0, MIT, BSD-2-Clause (multi-licensing) + ------------------------------------------------------------- */ + +/* ------------------------------------------------------------- + Table of contents + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + :: Servers list + :: States + :: Code to run when document is ready + + */ + +const ServersConfig = function (container) { + + /* ------------------------------------------------------------- + States + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + + const States = function (container, serverName, serverHost) { + const states = { + + /// + /// Constants + /// + + SALT_BASE_URL: "https://devcentral.nasqueron.org/source/operations/browse/main/", + SALT_STAGING_URL: "https://devcentral.nasqueron.org/source/staging/browse/master/", + + SALT_DOC_STATES_URL: "https://docs.saltproject.io/en/latest/ref/states/all/", + + /// + /// Private properties + /// + + /** + * A JQuery selector expression to a DOM element to publish to. + * + * @var string + */ + container: "", + + server: "", + + /// + /// Constructor + /// + + /** + * Initializes an instance of this object. + * + * @param container The DOM element JQuery selector where to write + * @param serverName The name of the server, to display it + * @param serverHost The FQDN of the server, to fetch config data + */ + load: function (container, serverName, serverHost) { + this.container = container; + this.server = serverName; + this.refreshData(serverHost); + }, + + /// + /// Main methods + /// + + refreshData: function (serverHost) { + let url = "https://" + serverHost + "/datasources/infra/all-states.json"; + $.getJSON(url, function (configurationStates) { + states.refreshUI(configurationStates); + }); + }, + + refreshUI: function (configurationStates) { + $(this.container).html(this.formatConfig(configurationStates)); + + $("#config-back-to-server-list").on("click", function () { + console.log("Back to servers list"); + new ServersList(container) + }) + }, + + formatConfig: function (states) { + return ` +<button id="config-back-to-server-list" class="button extra-action">« Back to servers list</button> +<h2 class="config-server">${this.server}</h2> +${this.formatStates(states)}` + }, + + formatState: function (name, state) { + let output = '<div class="state">' + output += '<div class="state-name">' + name + "</div>" + + for (const [key, properties] of Object.entries(state)) { + if (key.startsWith("__")) { + continue + } + output += `<div class="state-module"> + ${this.resolveSaltModuleMethod(key, properties)} + </div>` + + output += '<div class="state-properties">' + for (const property of properties) { + if (typeof property === "string") { + // Method is already parsed by extractMethod + continue + } + + if (property.order !== undefined) { + // We're lucky we already receive the states in the + // sorted order, so we can ignore this. + continue + } + + output += this.dump(property) + } + output += "</div>" + } + + output += "</div>" + + return output + }, + + formatStates: function (server_states) { + let current_unit = "" + let output = '<div class="states">' + + let roles_output = "" + let roles = [] + + for (const [role, role_states] of Object.entries(server_states)) { + roles.push(role) + + roles_output += `<div class="config-role"> +<h3 id="${this.makeId(role)}" class="config-role-title">${role}</h3> +<div class="config-role-content">` + + if ($.isEmptyObject(role_states)) { + roles_output += '<p class="config-error">No information gathered for this role. There is probably an error in Salt configuration.</p>'; + } + + for (const [name, individual_state] of Object.entries(role_states)) { + // Gets unit from the state source SLS to generate units headings + let unit = individual_state["__sls__"].replace(role + ".", "") + if (unit !== current_unit) { + roles_output += `<h4 class="config-unit">${unit}</h4>` + current_unit = unit + } + + roles_output += this.formatState(name, individual_state) + } + + roles_output += "</div></div>"; + } + + roles_output += '</div>'; + + output += ` +<div class="config-summary-roles"> +<h3 class="config-summary-roles-heading">Roles assigned</h3> +<ul class="config-summary-roles-list"> +` + for (const role of roles) { + output += ` +<li class="config-summary-role"> + <a href="#${this.makeId(role)}">${role}</a> +</li> + ` + } + output += "</ul></div>" + + output += roles_output; + + return output; + }, + + makeId: function (expression) { + return expression.replace("/", ".") + }, + + resolveSaltModuleMethod: function (module, properties) { + const method = this.extractMethod(properties); + const link = `${this.SALT_DOC_STATES_URL}salt.states.${module}.html#salt.states.${module}.${method}` + + return `<a class="salt-link" href="${link}">${module}.${method}</a>` + }, + + extractMethod: function (properties) { + for (const property of properties) { + if (typeof property === "string") { + return property + } + } + }, + + isInStagingRepo: url => url.startsWith("salt://software/") || url.startsWith("salt://wwwroot/"), + + resolveSaltLink: function (url) { + const base = this.isInStagingRepo(url) + ? this.SALT_STAGING_URL + : this.SALT_BASE_URL + + const link = base + url.replace("salt://", "") + return `<a class="salt-link" href="${link}">${url}</a>` + }, + // roles/core/rc/files/periodic.conf + + dump: function (data) { + if (typeof data === "string" && data.startsWith("salt://")) { + return this.resolveSaltLink(data) + } + + if (this.isScalar(data)) { + return data + } + if (typeof data === "object") { + if (data.constructor.name === "Array") { + return this.dumpArray(data); + } + + return this.dumpObject(data); + } + }, + + dumpArray: function (values) { + let dumped = '<ul class="state-list">' + + for (const value of values) { + dumped += `<li class="state-list-item">${this.dump(value)}</li>` + } + + dumped += '</ul>' + + return dumped + }, + + dumpObject: function (data) { + let dumped = "" + + for (const [key, value] of Object.entries(data)) { + dumped += ` + <div class="state-property"> + <span class="key">${key}</span> + <span class="value">${this.dump(value)}</span> + </div> + ` + } + + return dumped + }, + + isScalar: value => typeof value === "boolean" + || typeof value === "number" + || typeof value === "string" + }; + + states.load(container, serverName, serverHost) + + return states; + }; + + /* ------------------------------------------------------------- + Servers list + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + + const ServersList = function (container) { + const serversList = { + + /// + /// Constants + /// + + SERVERS_API_URL: "https://api.nasqueron.org/infra/servers.json", + + /// + /// Private properties + /// + + /** + * A JQuery selector expression to a DOM element to publish to. + * + * @var string + */ + container: "", + + servers: undefined, + + /// + /// Constructor + /// + + /** + * Initializes an instance of this object. + * + * @param container The DOM element JQuery selector where to write + */ + load: function (container) { + this.container = container; + this.refreshData(); + }, + + /// + /// Data model + /// + + fetchServers: function () { + let that = this + $.getJSON(this.SERVERS_API_URL, function (servers) { + that.servers = servers + that.refreshUI() + }) + }, + + refreshData: function () { + this.fetchServers(); + }, + + /// + /// UI representation + /// + + refreshUI: function () { + $(this.container).html(this.formatData()) + + for (const server of $(".server")) { + $(server).on("click", function () { + new States(container, server.id, server.dataset.hostname) + }) + } + }, + + formatData: function () { + let output = '<h2>Servers</h2><ul class="servers">'; + for (const [server, properties] of Object.entries(this.servers)) { + if (properties.configurator !== "salt") { + continue; + } + + output += ` + <li class="server" id="${server}" data-hostname="${properties.hostname}"> + <span class="server-property server-name">${properties.name}</span> + <span class="server-property server-description">${properties.description}</span> + </li> + ` + } + output += "</ul>" + + return output; + } + + }; + + serversList.load(container); + + return serversList; + }; + + /* ------------------------------------------------------------- + Initialization + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + + new ServersList(container) +} + +/* ------------------------------------------------------------- + Code to run when document is ready + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +$(document).ready(function() { + new ServersConfig("#config"); +}); diff --git a/src/assets/scss/app.scss b/src/assets/scss/app.scss --- a/src/assets/scss/app.scss +++ b/src/assets/scss/app.scss @@ -52,3 +52,4 @@ @import 'components/layout'; @import 'components/footer'; @import 'components/utilities-classes'; +@import 'components/salt-config'; diff --git a/src/assets/scss/components/_salt-config.scss b/src/assets/scss/components/_salt-config.scss new file mode 100644 --- /dev/null +++ b/src/assets/scss/components/_salt-config.scss @@ -0,0 +1,139 @@ +/* ------------------------------------------------------------- +General elements +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +.extra-action { + float: right; +} + +/* ------------------------------------------------------------- +Servers list +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +.servers { + li { + list-style-type: none; + padding: 1em; + border: solid 2px $primary-color; + width: 16em; + float: left; + margin-right: 1em; + margin-bottom: 1em; + + .server-property { + display: block; + } + + .server-name { + color: $secondary-color; + font-weight: bold; + } + } + + li:hover { + background-color: lighten($body-background, 10%); + + //background-color: #474747; + cursor: zoom-in; + } +} + +/* ------------------------------------------------------------- +Salt configuration +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +$config-margin: 1em; +$icon-width: 3.5rem; +$config-block-margin-height: 2.5em; + +.config-server::before { + content: "🖥️ "; + display: inline-block; + width: $icon-width; +} + +.config-summary-roles { + margin-bottom: $config-block-margin-height; +} + +.config-summary-roles-heading::before { + content: "📖 "; + display: inline-block; + width: $icon-width; +} + +.config-role { + margin-bottom: $config-block-margin-height; + line-height: 1.5em; + + .config-role-title::before { + content: "📦 "; + display: inline-block; + width: $icon-width; + } + + .config-role-content { + + a { + color: #b5c9c7; + } + + a:hover { + color: white; + } + + .state { + margin-bottom: 1.25em; + + .state-name { + color: #c4e3e9; + font-weight: bold; + } + + .state-module { + margin-left: $config-margin; + } + + .state-properties { + margin-left: 2 * $config-margin; + + .state-property { + .key { + color: #d2eaee; + } + + .key::after { + content: ": " + } + + .value .state-property { + padding-left: $config-margin; + } + } + + ul.state-list { + margin-bottom: 0; + } + + .state-list-item { + margin-left: $config-margin / 2; + + .state-property { + padding-left: 0 !important; + } + } + } + } + + } + + .config-error { + color: $warning-color; + font-weight: bold; + } + + .config-error::before { + content: "🔥 "; + } + +} diff --git a/src/pages/config/index.html b/src/pages/config/index.html new file mode 100644 --- /dev/null +++ b/src/pages/config/index.html @@ -0,0 +1,18 @@ +--- +title: Servers configuration +app: salt-config +--- + +<section class="row"> + <div class="large-12 columns"> + {{> config-intro}} + </div> +</section> + +<div class="row"> + <div class="large-9 columns" id="config"> + </div> + <div class="large-3 columns"> + {{> config-help}} + </div> +</div> diff --git a/src/partials/config/config-help.html b/src/partials/config/config-help.html new file mode 100644 --- /dev/null +++ b/src/partials/config/config-help.html @@ -0,0 +1,18 @@ +<div class="callout primary"> + <h3>Source</h3> + <p>These entries are compiled from the <a href="https://devcentral.nasqueron.org/source/operations/">Nasqueron Operations repository</a>.</p> + <p>They describe the state of the server, as known by Salt.</p> +</div> +<div class="callout primary"> + <h3>Change config</h3> + <p> + To amend the server configuration, you need to commit it to <a href="https://devcentral.nasqueron.org/source/operations/">rOPS</a>. + </p> + <p>The <a href="https://agora.nasqueron.org/Operations_grimoire">operations grimoire</a> contains help how to do so.</p> +</div> +<div class="callout primary"> + <h3>License</h3> + <p>Individual entries are too short to be original, and so are in the public domain.</p> + <p>When original enough, content is available under <a rel="license" href="https://creativecommons.org/licenses/by/4.0/">CC-BY 4.0</a> licence.</p> + <p>This configuration, as a database, is made available under the <a rel="license" href="https://www.opendatacommons.org/licenses/pddl/1.0/">Public Domain Dedication and License v1.0</a>.</p> +</div> diff --git a/src/partials/config/config-intro.html b/src/partials/config/config-intro.html new file mode 100644 --- /dev/null +++ b/src/partials/config/config-intro.html @@ -0,0 +1,4 @@ +<div class="callout"> + <p><strong>Nasqueron infrastructure servers</strong> support our budding community of creative people, writers, developers and thinkers.</p> + <p>According our transparency principle, our <strong>servers configuration</strong> is open and auditable.</p> +</div> diff --git a/src/partials/default-layout/footer.html b/src/partials/default-layout/footer.html --- a/src/partials/default-layout/footer.html +++ b/src/partials/default-layout/footer.html @@ -11,7 +11,8 @@ <div class="large-3 columns"> <dl> <dt>Ops repositories</dt> - <dd><a href="https://devcentral.nasqueron.org/source/operations/">Operations</a></dd> + <dd><a href="https://devcentral.nasqueron.org/source/operations/">Operations</a> → + <a href="/config">Config</a></dd> <dd><a href="https://devcentral.nasqueron.org/diffusion/query/NelStiRVmgP0/">Docker images</a></dd> <dt>Site repositories</dt>