`s.
+
+.nav {
+ display: flex;
+ flex-wrap: wrap;
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+}
+
+.nav-link {
+ display: block;
+ padding: $nav-link-padding-y $nav-link-padding-x;
+
+ @include hover-focus {
+ text-decoration: none;
+ }
+
+ // Disabled state lightens text
+ &.disabled {
+ color: $nav-link-disabled-color;
+ }
+}
+
+//
+// Tabs
+//
+
+.nav-tabs {
+ border-bottom: $nav-tabs-border-width solid $nav-tabs-border-color;
+
+ .nav-item {
+ margin-bottom: -$nav-tabs-border-width;
+ }
+
+ .nav-link {
+ border: $nav-tabs-border-width solid transparent;
+ @include border-top-radius($nav-tabs-border-radius);
+
+ @include hover-focus {
+ border-color: $nav-tabs-link-hover-border-color;
+ }
+
+ &.disabled {
+ color: $nav-link-disabled-color;
+ background-color: transparent;
+ border-color: transparent;
+ }
+ }
+
+ .nav-link.active,
+ .nav-item.show .nav-link {
+ color: $nav-tabs-link-active-color;
+ background-color: $nav-tabs-link-active-bg;
+ border-color: $nav-tabs-link-active-border-color;
+ }
+
+ .dropdown-menu {
+ // Make dropdown border overlap tab border
+ margin-top: -$nav-tabs-border-width;
+ // Remove the top rounded corners here since there is a hard edge above the menu
+ @include border-top-radius(0);
+ }
+}
+
+
+//
+// Pills
+//
+
+.nav-pills {
+ .nav-link {
+ @include border-radius($nav-pills-border-radius);
+ }
+
+ .nav-link.active,
+ .show > .nav-link {
+ color: $nav-pills-link-active-color;
+ background-color: $nav-pills-link-active-bg;
+ }
+}
+
+
+//
+// Justified variants
+//
+
+.nav-fill {
+ .nav-item {
+ flex: 1 1 auto;
+ text-align: center;
+ }
+}
+
+.nav-justified {
+ .nav-item {
+ flex-basis: 0;
+ flex-grow: 1;
+ text-align: center;
+ }
+}
+
+
+// Tabbable tabs
+//
+// Hide tabbable panes to start, show them when `.active`
+
+.tab-content {
+ > .tab-pane {
+ display: none;
+ }
+ > .active {
+ display: block;
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/_navbar.scss b/docs/assets/_scss/bootstrap-4.1.3/_navbar.scss
new file mode 100755
index 00000000..52de5050
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/_navbar.scss
@@ -0,0 +1,299 @@
+// Contents
+//
+// Navbar
+// Navbar brand
+// Navbar nav
+// Navbar text
+// Navbar divider
+// Responsive navbar
+// Navbar position
+// Navbar themes
+
+
+// Navbar
+//
+// Provide a static navbar from which we expand to create full-width, fixed, and
+// other navbar variations.
+
+.navbar {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap; // allow us to do the line break for collapsing content
+ align-items: center;
+ justify-content: space-between; // space out brand from logo
+ padding: $navbar-padding-y $navbar-padding-x;
+
+ // Because flex properties aren't inherited, we need to redeclare these first
+ // few properties so that content nested within behave properly.
+ > .container,
+ > .container-fluid {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ }
+}
+
+
+// Navbar brand
+//
+// Used for brand, project, or site names.
+
+.navbar-brand {
+ display: inline-block;
+ padding-top: $navbar-brand-padding-y;
+ padding-bottom: $navbar-brand-padding-y;
+ margin-right: $navbar-padding-x;
+ font-size: $navbar-brand-font-size;
+ line-height: inherit;
+ white-space: nowrap;
+
+ @include hover-focus {
+ text-decoration: none;
+ }
+}
+
+
+// Navbar nav
+//
+// Custom navbar navigation (doesn't require `.nav`, but does make use of `.nav-link`).
+
+.navbar-nav {
+ display: flex;
+ flex-direction: column; // cannot use `inherit` to get the `.navbar`s value
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+
+ .nav-link {
+ padding-right: 0;
+ padding-left: 0;
+ }
+
+ .dropdown-menu {
+ position: static;
+ float: none;
+ }
+}
+
+
+// Navbar text
+//
+//
+
+.navbar-text {
+ display: inline-block;
+ padding-top: $nav-link-padding-y;
+ padding-bottom: $nav-link-padding-y;
+}
+
+
+// Responsive navbar
+//
+// Custom styles for responsive collapsing and toggling of navbar contents.
+// Powered by the collapse Bootstrap JavaScript plugin.
+
+// When collapsed, prevent the toggleable navbar contents from appearing in
+// the default flexbox row orientation. Requires the use of `flex-wrap: wrap`
+// on the `.navbar` parent.
+.navbar-collapse {
+ flex-basis: 100%;
+ flex-grow: 1;
+ // For always expanded or extra full navbars, ensure content aligns itself
+ // properly vertically. Can be easily overridden with flex utilities.
+ align-items: center;
+}
+
+// Button for toggling the navbar when in its collapsed state
+.navbar-toggler {
+ padding: $navbar-toggler-padding-y $navbar-toggler-padding-x;
+ font-size: $navbar-toggler-font-size;
+ line-height: 1;
+ background-color: transparent; // remove default button style
+ border: $border-width solid transparent; // remove default button style
+ @include border-radius($navbar-toggler-border-radius);
+
+ @include hover-focus {
+ text-decoration: none;
+ }
+
+ // Opinionated: add "hand" cursor to non-disabled .navbar-toggler elements
+ &:not(:disabled):not(.disabled) {
+ cursor: pointer;
+ }
+}
+
+// Keep as a separate element so folks can easily override it with another icon
+// or image file as needed.
+.navbar-toggler-icon {
+ display: inline-block;
+ width: 1.5em;
+ height: 1.5em;
+ vertical-align: middle;
+ content: "";
+ background: no-repeat center center;
+ background-size: 100% 100%;
+}
+
+// Generate series of `.navbar-expand-*` responsive classes for configuring
+// where your navbar collapses.
+.navbar-expand {
+ @each $breakpoint in map-keys($grid-breakpoints) {
+ $next: breakpoint-next($breakpoint, $grid-breakpoints);
+ $infix: breakpoint-infix($next, $grid-breakpoints);
+
+ {$infix} {
+ @include media-breakpoint-down($breakpoint) {
+ > .container,
+ > .container-fluid {
+ padding-right: 0;
+ padding-left: 0;
+ }
+ }
+
+ @include media-breakpoint-up($next) {
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+
+ .navbar-nav {
+ flex-direction: row;
+
+ .dropdown-menu {
+ position: absolute;
+ }
+
+ .nav-link {
+ padding-right: $navbar-nav-link-padding-x;
+ padding-left: $navbar-nav-link-padding-x;
+ }
+ }
+
+ // For nesting containers, have to redeclare for alignment purposes
+ > .container,
+ > .container-fluid {
+ flex-wrap: nowrap;
+ }
+
+ .navbar-collapse {
+ display: flex !important; // stylelint-disable-line declaration-no-important
+
+ // Changes flex-bases to auto because of an IE10 bug
+ flex-basis: auto;
+ }
+
+ .navbar-toggler {
+ display: none;
+ }
+ }
+ }
+ }
+}
+
+
+// Navbar themes
+//
+// Styles for switching between navbars with light or dark background.
+
+// Dark links against a light background
+.navbar-light {
+ .navbar-brand {
+ color: $navbar-light-active-color;
+
+ @include hover-focus {
+ color: $navbar-light-active-color;
+ }
+ }
+
+ .navbar-nav {
+ .nav-link {
+ color: $navbar-light-color;
+
+ @include hover-focus {
+ color: $navbar-light-hover-color;
+ }
+
+ &.disabled {
+ color: $navbar-light-disabled-color;
+ }
+ }
+
+ .show > .nav-link,
+ .active > .nav-link,
+ .nav-link.show,
+ .nav-link.active {
+ color: $navbar-light-active-color;
+ }
+ }
+
+ .navbar-toggler {
+ color: $navbar-light-color;
+ border-color: $navbar-light-toggler-border-color;
+ }
+
+ .navbar-toggler-icon {
+ background-image: $navbar-light-toggler-icon-bg;
+ }
+
+ .navbar-text {
+ color: $navbar-light-color;
+ a {
+ color: $navbar-light-active-color;
+
+ @include hover-focus {
+ color: $navbar-light-active-color;
+ }
+ }
+ }
+}
+
+// White links against a dark background
+.navbar-dark {
+ .navbar-brand {
+ color: $navbar-dark-active-color;
+
+ @include hover-focus {
+ color: $navbar-dark-active-color;
+ }
+ }
+
+ .navbar-nav {
+ .nav-link {
+ color: $navbar-dark-color;
+
+ @include hover-focus {
+ color: $navbar-dark-hover-color;
+ }
+
+ &.disabled {
+ color: $navbar-dark-disabled-color;
+ }
+ }
+
+ .show > .nav-link,
+ .active > .nav-link,
+ .nav-link.show,
+ .nav-link.active {
+ color: $navbar-dark-active-color;
+ }
+ }
+
+ .navbar-toggler {
+ color: $navbar-dark-color;
+ border-color: $navbar-dark-toggler-border-color;
+ }
+
+ .navbar-toggler-icon {
+ background-image: $navbar-dark-toggler-icon-bg;
+ }
+
+ .navbar-text {
+ color: $navbar-dark-color;
+ a {
+ color: $navbar-dark-active-color;
+
+ @include hover-focus {
+ color: $navbar-dark-active-color;
+ }
+ }
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/_pagination.scss b/docs/assets/_scss/bootstrap-4.1.3/_pagination.scss
new file mode 100755
index 00000000..9349f3f9
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/_pagination.scss
@@ -0,0 +1,78 @@
+.pagination {
+ display: flex;
+ @include list-unstyled();
+ @include border-radius();
+}
+
+.page-link {
+ position: relative;
+ display: block;
+ padding: $pagination-padding-y $pagination-padding-x;
+ margin-left: -$pagination-border-width;
+ line-height: $pagination-line-height;
+ color: $pagination-color;
+ background-color: $pagination-bg;
+ border: $pagination-border-width solid $pagination-border-color;
+
+ &:hover {
+ z-index: 2;
+ color: $pagination-hover-color;
+ text-decoration: none;
+ background-color: $pagination-hover-bg;
+ border-color: $pagination-hover-border-color;
+ }
+
+ &:focus {
+ z-index: 2;
+ outline: $pagination-focus-outline;
+ box-shadow: $pagination-focus-box-shadow;
+ }
+
+ // Opinionated: add "hand" cursor to non-disabled .page-link elements
+ &:not(:disabled):not(.disabled) {
+ cursor: pointer;
+ }
+}
+
+.page-item {
+ &:first-child {
+ .page-link {
+ margin-left: 0;
+ @include border-left-radius($border-radius);
+ }
+ }
+ &:last-child {
+ .page-link {
+ @include border-right-radius($border-radius);
+ }
+ }
+
+ &.active .page-link {
+ z-index: 1;
+ color: $pagination-active-color;
+ background-color: $pagination-active-bg;
+ border-color: $pagination-active-border-color;
+ }
+
+ &.disabled .page-link {
+ color: $pagination-disabled-color;
+ pointer-events: none;
+ // Opinionated: remove the "hand" cursor set previously for .page-link
+ cursor: auto;
+ background-color: $pagination-disabled-bg;
+ border-color: $pagination-disabled-border-color;
+ }
+}
+
+
+//
+// Sizing
+//
+
+.pagination-lg {
+ @include pagination-size($pagination-padding-y-lg, $pagination-padding-x-lg, $font-size-lg, $line-height-lg, $border-radius-lg);
+}
+
+.pagination-sm {
+ @include pagination-size($pagination-padding-y-sm, $pagination-padding-x-sm, $font-size-sm, $line-height-sm, $border-radius-sm);
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/_popover.scss b/docs/assets/_scss/bootstrap-4.1.3/_popover.scss
new file mode 100755
index 00000000..3ef5f628
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/_popover.scss
@@ -0,0 +1,183 @@
+.popover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: $zindex-popover;
+ display: block;
+ max-width: $popover-max-width;
+ // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.
+ // So reset our font and text properties to avoid inheriting weird values.
+ @include reset-text();
+ font-size: $popover-font-size;
+ // Allow breaking very long words so they don't overflow the popover's bounds
+ word-wrap: break-word;
+ background-color: $popover-bg;
+ background-clip: padding-box;
+ border: $popover-border-width solid $popover-border-color;
+ @include border-radius($popover-border-radius);
+ @include box-shadow($popover-box-shadow);
+
+ .arrow {
+ position: absolute;
+ display: block;
+ width: $popover-arrow-width;
+ height: $popover-arrow-height;
+ margin: 0 $border-radius-lg;
+
+ &::before,
+ &::after {
+ position: absolute;
+ display: block;
+ content: "";
+ border-color: transparent;
+ border-style: solid;
+ }
+ }
+}
+
+.bs-popover-top {
+ margin-bottom: $popover-arrow-height;
+
+ .arrow {
+ bottom: calc((#{$popover-arrow-height} + #{$popover-border-width}) * -1);
+ }
+
+ .arrow::before,
+ .arrow::after {
+ border-width: $popover-arrow-height ($popover-arrow-width / 2) 0;
+ }
+
+ .arrow::before {
+ bottom: 0;
+ border-top-color: $popover-arrow-outer-color;
+ }
+
+ .arrow::after {
+ bottom: $popover-border-width;
+ border-top-color: $popover-arrow-color;
+ }
+}
+
+.bs-popover-right {
+ margin-left: $popover-arrow-height;
+
+ .arrow {
+ left: calc((#{$popover-arrow-height} + #{$popover-border-width}) * -1);
+ width: $popover-arrow-height;
+ height: $popover-arrow-width;
+ margin: $border-radius-lg 0; // make sure the arrow does not touch the popover's rounded corners
+ }
+
+ .arrow::before,
+ .arrow::after {
+ border-width: ($popover-arrow-width / 2) $popover-arrow-height ($popover-arrow-width / 2) 0;
+ }
+
+ .arrow::before {
+ left: 0;
+ border-right-color: $popover-arrow-outer-color;
+ }
+
+ .arrow::after {
+ left: $popover-border-width;
+ border-right-color: $popover-arrow-color;
+ }
+}
+
+.bs-popover-bottom {
+ margin-top: $popover-arrow-height;
+
+ .arrow {
+ top: calc((#{$popover-arrow-height} + #{$popover-border-width}) * -1);
+ }
+
+ .arrow::before,
+ .arrow::after {
+ border-width: 0 ($popover-arrow-width / 2) $popover-arrow-height ($popover-arrow-width / 2);
+ }
+
+ .arrow::before {
+ top: 0;
+ border-bottom-color: $popover-arrow-outer-color;
+ }
+
+ .arrow::after {
+ top: $popover-border-width;
+ border-bottom-color: $popover-arrow-color;
+ }
+
+ // This will remove the popover-header's border just below the arrow
+ .popover-header::before {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ display: block;
+ width: $popover-arrow-width;
+ margin-left: ($popover-arrow-width / -2);
+ content: "";
+ border-bottom: $popover-border-width solid $popover-header-bg;
+ }
+}
+
+.bs-popover-left {
+ margin-right: $popover-arrow-height;
+
+ .arrow {
+ right: calc((#{$popover-arrow-height} + #{$popover-border-width}) * -1);
+ width: $popover-arrow-height;
+ height: $popover-arrow-width;
+ margin: $border-radius-lg 0; // make sure the arrow does not touch the popover's rounded corners
+ }
+
+ .arrow::before,
+ .arrow::after {
+ border-width: ($popover-arrow-width / 2) 0 ($popover-arrow-width / 2) $popover-arrow-height;
+ }
+
+ .arrow::before {
+ right: 0;
+ border-left-color: $popover-arrow-outer-color;
+ }
+
+ .arrow::after {
+ right: $popover-border-width;
+ border-left-color: $popover-arrow-color;
+ }
+}
+
+.bs-popover-auto {
+ &[x-placement^="top"] {
+ @extend .bs-popover-top;
+ }
+ &[x-placement^="right"] {
+ @extend .bs-popover-right;
+ }
+ &[x-placement^="bottom"] {
+ @extend .bs-popover-bottom;
+ }
+ &[x-placement^="left"] {
+ @extend .bs-popover-left;
+ }
+}
+
+
+// Offset the popover to account for the popover arrow
+.popover-header {
+ padding: $popover-header-padding-y $popover-header-padding-x;
+ margin-bottom: 0; // Reset the default from Reboot
+ font-size: $font-size-base;
+ color: $popover-header-color;
+ background-color: $popover-header-bg;
+ border-bottom: $popover-border-width solid darken($popover-header-bg, 5%);
+ $offset-border-width: calc(#{$border-radius-lg} - #{$popover-border-width});
+ @include border-top-radius($offset-border-width);
+
+ &:empty {
+ display: none;
+ }
+}
+
+.popover-body {
+ padding: $popover-body-padding-y $popover-body-padding-x;
+ color: $popover-body-color;
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/_print.scss b/docs/assets/_scss/bootstrap-4.1.3/_print.scss
new file mode 100755
index 00000000..1df94873
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/_print.scss
@@ -0,0 +1,141 @@
+// stylelint-disable declaration-no-important, selector-no-qualifying-type
+
+// Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css
+
+// ==========================================================================
+// Print styles.
+// Inlined to avoid the additional HTTP request:
+// https://www.phpied.com/delay-loading-your-print-css/
+// ==========================================================================
+
+@if $enable-print-styles {
+ @media print {
+ *,
+ *::before,
+ *::after {
+ // Bootstrap specific; comment out `color` and `background`
+ //color: $black !important; // Black prints faster
+ text-shadow: none !important;
+ //background: transparent !important;
+ box-shadow: none !important;
+ }
+
+ a {
+ &:not(.btn) {
+ text-decoration: underline;
+ }
+ }
+
+ // Bootstrap specific; comment the following selector out
+ //a[href]::after {
+ // content: " (" attr(href) ")";
+ //}
+
+ abbr[title]::after {
+ content: " (" attr(title) ")";
+ }
+
+ // Bootstrap specific; comment the following selector out
+ //
+ // Don't show links that are fragment identifiers,
+ // or use the `javascript:` pseudo protocol
+ //
+
+ //a[href^="#"]::after,
+ //a[href^="javascript:"]::after {
+ // content: "";
+ //}
+
+ pre {
+ white-space: pre-wrap !important;
+ }
+ pre,
+ blockquote {
+ border: $border-width solid $gray-500; // Bootstrap custom code; using `$border-width` instead of 1px
+ page-break-inside: avoid;
+ }
+
+ //
+ // Printing Tables:
+ // http://css-discuss.incutio.com/wiki/Printing_Tables
+ //
+
+ thead {
+ display: table-header-group;
+ }
+
+ tr,
+ img {
+ page-break-inside: avoid;
+ }
+
+ p,
+ h2,
+ h3 {
+ orphans: 3;
+ widows: 3;
+ }
+
+ h2,
+ h3 {
+ page-break-after: avoid;
+ }
+
+ // Bootstrap specific changes start
+
+ // Specify a size and min-width to make printing closer across browsers.
+ // We don't set margin here because it breaks `size` in Chrome. We also
+ // don't use `!important` on `size` as it breaks in Chrome.
+ @page {
+ size: $print-page-size;
+ }
+ body {
+ min-width: $print-body-min-width !important;
+ }
+ .container {
+ min-width: $print-body-min-width !important;
+ }
+
+ // Bootstrap components
+ .navbar {
+ display: none;
+ }
+ .badge {
+ border: $border-width solid $black;
+ }
+
+ .table {
+ border-collapse: collapse !important;
+
+ td,
+ th {
+ background-color: $white !important;
+ }
+ }
+
+ .table-bordered {
+ th,
+ td {
+ border: 1px solid $gray-300 !important;
+ }
+ }
+
+ .table-dark {
+ color: inherit;
+
+ th,
+ td,
+ thead th,
+ tbody + tbody {
+ border-color: $table-border-color;
+ }
+ }
+
+ .table .thead-dark th {
+ color: inherit;
+ border-color: $table-border-color;
+ }
+
+ // Bootstrap specific changes end
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/_progress.scss b/docs/assets/_scss/bootstrap-4.1.3/_progress.scss
new file mode 100755
index 00000000..0ac3e0c9
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/_progress.scss
@@ -0,0 +1,34 @@
+@keyframes progress-bar-stripes {
+ from { background-position: $progress-height 0; }
+ to { background-position: 0 0; }
+}
+
+.progress {
+ display: flex;
+ height: $progress-height;
+ overflow: hidden; // force rounded corners by cropping it
+ font-size: $progress-font-size;
+ background-color: $progress-bg;
+ @include border-radius($progress-border-radius);
+ @include box-shadow($progress-box-shadow);
+}
+
+.progress-bar {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ color: $progress-bar-color;
+ text-align: center;
+ white-space: nowrap;
+ background-color: $progress-bar-bg;
+ @include transition($progress-bar-transition);
+}
+
+.progress-bar-striped {
+ @include gradient-striped();
+ background-size: $progress-height $progress-height;
+}
+
+.progress-bar-animated {
+ animation: progress-bar-stripes $progress-bar-animation-timing;
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/_reboot.scss b/docs/assets/_scss/bootstrap-4.1.3/_reboot.scss
new file mode 100755
index 00000000..f297d095
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/_reboot.scss
@@ -0,0 +1,483 @@
+// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix
+
+// Reboot
+//
+// Normalization of HTML elements, manually forked from Normalize.css to remove
+// styles targeting irrelevant browsers while applying new styles.
+//
+// Normalize is licensed MIT. https://github.com/necolas/normalize.css
+
+
+// Document
+//
+// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.
+// 2. Change the default font family in all browsers.
+// 3. Correct the line height in all browsers.
+// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.
+// 5. Setting @viewport causes scrollbars to overlap content in IE11 and Edge, so
+// we force a non-overlapping, non-auto-hiding scrollbar to counteract.
+// 6. Change the default tap highlight to be completely transparent in iOS.
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box; // 1
+}
+
+html {
+ font-family: sans-serif; // 2
+ line-height: 1.15; // 3
+ -webkit-text-size-adjust: 100%; // 4
+ -ms-text-size-adjust: 100%; // 4
+ -ms-overflow-style: scrollbar; // 5
+ -webkit-tap-highlight-color: rgba($black, 0); // 6
+}
+
+// IE10+ doesn't honor ` ` in some cases.
+@at-root {
+ @-ms-viewport {
+ width: device-width;
+ }
+}
+
+// stylelint-disable selector-list-comma-newline-after
+// Shim for "new" HTML5 structural elements to display correctly (IE10, older browsers)
+article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
+ display: block;
+}
+// stylelint-enable selector-list-comma-newline-after
+
+// Body
+//
+// 1. Remove the margin in all browsers.
+// 2. As a best practice, apply a default `background-color`.
+// 3. Set an explicit initial text-align value so that we can later use the
+// the `inherit` value on things like `` elements.
+
+body {
+ margin: 0; // 1
+ font-family: $font-family-base;
+ font-size: $font-size-base;
+ font-weight: $font-weight-base;
+ line-height: $line-height-base;
+ color: $body-color;
+ text-align: left; // 3
+ background-color: $body-bg; // 2
+}
+
+// Suppress the focus outline on elements that cannot be accessed via keyboard.
+// This prevents an unwanted focus outline from appearing around elements that
+// might still respond to pointer events.
+//
+// Credit: https://github.com/suitcss/base
+[tabindex="-1"]:focus {
+ outline: 0 !important;
+}
+
+
+// Content grouping
+//
+// 1. Add the correct box sizing in Firefox.
+// 2. Show the overflow in Edge and IE.
+
+hr {
+ box-sizing: content-box; // 1
+ height: 0; // 1
+ overflow: visible; // 2
+}
+
+
+//
+// Typography
+//
+
+// Remove top margins from headings
+//
+// By default, ``-`` all receive top and bottom margins. We nuke the top
+// margin for easier control within type scales as it avoids margin collapsing.
+// stylelint-disable selector-list-comma-newline-after
+h1, h2, h3, h4, h5, h6 {
+ margin-top: 0;
+ margin-bottom: $headings-margin-bottom;
+}
+// stylelint-enable selector-list-comma-newline-after
+
+// Reset margins on paragraphs
+//
+// Similarly, the top margin on ` `s get reset. However, we also reset the
+// bottom margin to use `rem` units instead of `em`.
+p {
+ margin-top: 0;
+ margin-bottom: $paragraph-margin-bottom;
+}
+
+// Abbreviations
+//
+// 1. Remove the bottom border in Firefox 39-.
+// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+// 3. Add explicit cursor to indicate changed behavior.
+// 4. Duplicate behavior to the data-* attribute for our tooltip plugin
+
+abbr[title],
+abbr[data-original-title] { // 4
+ text-decoration: underline; // 2
+ text-decoration: underline dotted; // 2
+ cursor: help; // 3
+ border-bottom: 0; // 1
+}
+
+address {
+ margin-bottom: 1rem;
+ font-style: normal;
+ line-height: inherit;
+}
+
+ol,
+ul,
+dl {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+
+ol ol,
+ul ul,
+ol ul,
+ul ol {
+ margin-bottom: 0;
+}
+
+dt {
+ font-weight: $dt-font-weight;
+}
+
+dd {
+ margin-bottom: .5rem;
+ margin-left: 0; // Undo browser default
+}
+
+blockquote {
+ margin: 0 0 1rem;
+}
+
+dfn {
+ font-style: italic; // Add the correct font style in Android 4.3-
+}
+
+// stylelint-disable font-weight-notation
+b,
+strong {
+ font-weight: bolder; // Add the correct font weight in Chrome, Edge, and Safari
+}
+// stylelint-enable font-weight-notation
+
+small {
+ font-size: 80%; // Add the correct font size in all browsers
+}
+
+//
+// Prevent `sub` and `sup` elements from affecting the line height in
+// all browsers.
+//
+
+sub,
+sup {
+ position: relative;
+ font-size: 75%;
+ line-height: 0;
+ vertical-align: baseline;
+}
+
+sub { bottom: -.25em; }
+sup { top: -.5em; }
+
+
+//
+// Links
+//
+
+a {
+ color: $link-color;
+ text-decoration: $link-decoration;
+ background-color: transparent; // Remove the gray background on active links in IE 10.
+ -webkit-text-decoration-skip: objects; // Remove gaps in links underline in iOS 8+ and Safari 8+.
+
+ @include hover {
+ color: $link-hover-color;
+ text-decoration: $link-hover-decoration;
+ }
+}
+
+// And undo these styles for placeholder links/named anchors (without href)
+// which have not been made explicitly keyboard-focusable (without tabindex).
+// It would be more straightforward to just use a[href] in previous block, but that
+// causes specificity issues in many other styles that are too complex to fix.
+// See https://github.com/twbs/bootstrap/issues/19402
+
+a:not([href]):not([tabindex]) {
+ color: inherit;
+ text-decoration: none;
+
+ @include hover-focus {
+ color: inherit;
+ text-decoration: none;
+ }
+
+ &:focus {
+ outline: 0;
+ }
+}
+
+
+//
+// Code
+//
+
+pre,
+code,
+kbd,
+samp {
+ font-family: $font-family-monospace;
+ font-size: 1em; // Correct the odd `em` font sizing in all browsers.
+}
+
+pre {
+ // Remove browser default top margin
+ margin-top: 0;
+ // Reset browser default of `1em` to use `rem`s
+ margin-bottom: 1rem;
+ // Don't allow content to break outside
+ overflow: auto;
+ // We have @viewport set which causes scrollbars to overlap content in IE11 and Edge, so
+ // we force a non-overlapping, non-auto-hiding scrollbar to counteract.
+ -ms-overflow-style: scrollbar;
+}
+
+
+//
+// Figures
+//
+
+figure {
+ // Apply a consistent margin strategy (matches our type styles).
+ margin: 0 0 1rem;
+}
+
+
+//
+// Images and content
+//
+
+img {
+ vertical-align: middle;
+ border-style: none; // Remove the border on images inside links in IE 10-.
+}
+
+svg {
+ // Workaround for the SVG overflow bug in IE10/11 is still required.
+ // See https://github.com/twbs/bootstrap/issues/26878
+ overflow: hidden;
+ vertical-align: middle;
+}
+
+
+//
+// Tables
+//
+
+table {
+ border-collapse: collapse; // Prevent double borders
+}
+
+caption {
+ padding-top: $table-cell-padding;
+ padding-bottom: $table-cell-padding;
+ color: $table-caption-color;
+ text-align: left;
+ caption-side: bottom;
+}
+
+th {
+ // Matches default `
` alignment by inheriting from the ``, or the
+ // closest parent with a set `text-align`.
+ text-align: inherit;
+}
+
+
+//
+// Forms
+//
+
+label {
+ // Allow labels to use `margin` for spacing.
+ display: inline-block;
+ margin-bottom: $label-margin-bottom;
+}
+
+// Remove the default `border-radius` that macOS Chrome adds.
+//
+// Details at https://github.com/twbs/bootstrap/issues/24093
+button {
+ border-radius: 0;
+}
+
+// Work around a Firefox/IE bug where the transparent `button` background
+// results in a loss of the default `button` focus styles.
+//
+// Credit: https://github.com/suitcss/base/
+button:focus {
+ outline: 1px dotted;
+ outline: 5px auto -webkit-focus-ring-color;
+}
+
+input,
+button,
+select,
+optgroup,
+textarea {
+ margin: 0; // Remove the margin in Firefox and Safari
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+}
+
+button,
+input {
+ overflow: visible; // Show the overflow in Edge
+}
+
+button,
+select {
+ text-transform: none; // Remove the inheritance of text transform in Firefox
+}
+
+// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
+// controls in Android 4.
+// 2. Correct the inability to style clickable types in iOS and Safari.
+button,
+html [type="button"], // 1
+[type="reset"],
+[type="submit"] {
+ -webkit-appearance: button; // 2
+}
+
+// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+ padding: 0;
+ border-style: none;
+}
+
+input[type="radio"],
+input[type="checkbox"] {
+ box-sizing: border-box; // 1. Add the correct box sizing in IE 10-
+ padding: 0; // 2. Remove the padding in IE 10-
+}
+
+
+input[type="date"],
+input[type="time"],
+input[type="datetime-local"],
+input[type="month"] {
+ // Remove the default appearance of temporal inputs to avoid a Mobile Safari
+ // bug where setting a custom line-height prevents text from being vertically
+ // centered within the input.
+ // See https://bugs.webkit.org/show_bug.cgi?id=139848
+ // and https://github.com/twbs/bootstrap/issues/11266
+ -webkit-appearance: listbox;
+}
+
+textarea {
+ overflow: auto; // Remove the default vertical scrollbar in IE.
+ // Textareas should really only resize vertically so they don't break their (horizontal) containers.
+ resize: vertical;
+}
+
+fieldset {
+ // Browsers set a default `min-width: min-content;` on fieldsets,
+ // unlike e.g. ``s, which have `min-width: 0;` by default.
+ // So we reset that to ensure fieldsets behave more like a standard block element.
+ // See https://github.com/twbs/bootstrap/issues/12359
+ // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements
+ min-width: 0;
+ // Reset the default outline behavior of fieldsets so they don't affect page layout.
+ padding: 0;
+ margin: 0;
+ border: 0;
+}
+
+// 1. Correct the text wrapping in Edge and IE.
+// 2. Correct the color inheritance from `fieldset` elements in IE.
+legend {
+ display: block;
+ width: 100%;
+ max-width: 100%; // 1
+ padding: 0;
+ margin-bottom: .5rem;
+ font-size: 1.5rem;
+ line-height: inherit;
+ color: inherit; // 2
+ white-space: normal; // 1
+}
+
+progress {
+ vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.
+}
+
+// Correct the cursor style of increment and decrement buttons in Chrome.
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+[type="search"] {
+ // This overrides the extra rounded corners on search inputs in iOS so that our
+ // `.form-control` class can properly style them. Note that this cannot simply
+ // be added to `.form-control` as it's not specific enough. For details, see
+ // https://github.com/twbs/bootstrap/issues/11586.
+ outline-offset: -2px; // 2. Correct the outline style in Safari.
+ -webkit-appearance: none;
+}
+
+//
+// Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
+//
+
+[type="search"]::-webkit-search-cancel-button,
+[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+//
+// 1. Correct the inability to style clickable types in iOS and Safari.
+// 2. Change font properties to `inherit` in Safari.
+//
+
+::-webkit-file-upload-button {
+ font: inherit; // 2
+ -webkit-appearance: button; // 1
+}
+
+//
+// Correct element displays
+//
+
+output {
+ display: inline-block;
+}
+
+summary {
+ display: list-item; // Add the correct display in all browsers
+ cursor: pointer;
+}
+
+template {
+ display: none; // Add the correct display in IE
+}
+
+// Always hide an element with the `hidden` HTML attribute (from PureCSS).
+// Needed for proper display in IE 10-.
+[hidden] {
+ display: none !important;
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/_root.scss b/docs/assets/_scss/bootstrap-4.1.3/_root.scss
new file mode 100755
index 00000000..ad550df3
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/_root.scss
@@ -0,0 +1,19 @@
+:root {
+ // Custom variable values only support SassScript inside `#{}`.
+ @each $color, $value in $colors {
+ --#{$color}: #{$value};
+ }
+
+ @each $color, $value in $theme-colors {
+ --#{$color}: #{$value};
+ }
+
+ @each $bp, $value in $grid-breakpoints {
+ --breakpoint-#{$bp}: #{$value};
+ }
+
+ // Use `inspect` for lists so that quoted items keep the quotes.
+ // See https://github.com/sass/sass/issues/2383#issuecomment-336349172
+ --font-family-sans-serif: #{inspect($font-family-sans-serif)};
+ --font-family-monospace: #{inspect($font-family-monospace)};
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/_tables.scss b/docs/assets/_scss/bootstrap-4.1.3/_tables.scss
new file mode 100755
index 00000000..5fa6a866
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/_tables.scss
@@ -0,0 +1,187 @@
+//
+// Basic Bootstrap table
+//
+
+.table {
+ width: 100%;
+ margin-bottom: $spacer;
+ background-color: $table-bg; // Reset for nesting within parents with `background-color`.
+
+ th,
+ td {
+ padding: $table-cell-padding;
+ vertical-align: top;
+ border-top: $table-border-width solid $table-border-color;
+ }
+
+ thead th {
+ vertical-align: bottom;
+ border-bottom: (2 * $table-border-width) solid $table-border-color;
+ }
+
+ tbody + tbody {
+ border-top: (2 * $table-border-width) solid $table-border-color;
+ }
+
+ .table {
+ background-color: $body-bg;
+ }
+}
+
+
+//
+// Condensed table w/ half padding
+//
+
+.table-sm {
+ th,
+ td {
+ padding: $table-cell-padding-sm;
+ }
+}
+
+
+// Border versions
+//
+// Add or remove borders all around the table and between all the columns.
+
+.table-bordered {
+ border: $table-border-width solid $table-border-color;
+
+ th,
+ td {
+ border: $table-border-width solid $table-border-color;
+ }
+
+ thead {
+ th,
+ td {
+ border-bottom-width: (2 * $table-border-width);
+ }
+ }
+}
+
+.table-borderless {
+ th,
+ td,
+ thead th,
+ tbody + tbody {
+ border: 0;
+ }
+}
+
+// Zebra-striping
+//
+// Default zebra-stripe styles (alternating gray and transparent backgrounds)
+
+.table-striped {
+ tbody tr:nth-of-type(#{$table-striped-order}) {
+ background-color: $table-accent-bg;
+ }
+}
+
+
+// Hover effect
+//
+// Placed here since it has to come after the potential zebra striping
+
+.table-hover {
+ tbody tr {
+ @include hover {
+ background-color: $table-hover-bg;
+ }
+ }
+}
+
+
+// Table backgrounds
+//
+// Exact selectors below required to override `.table-striped` and prevent
+// inheritance to nested tables.
+
+@each $color, $value in $theme-colors {
+ @include table-row-variant($color, theme-color-level($color, -9));
+}
+
+@include table-row-variant(active, $table-active-bg);
+
+
+// Dark styles
+//
+// Same table markup, but inverted color scheme: dark background and light text.
+
+// stylelint-disable-next-line no-duplicate-selectors
+.table {
+ .thead-dark {
+ th {
+ color: $table-dark-color;
+ background-color: $table-dark-bg;
+ border-color: $table-dark-border-color;
+ }
+ }
+
+ .thead-light {
+ th {
+ color: $table-head-color;
+ background-color: $table-head-bg;
+ border-color: $table-border-color;
+ }
+ }
+}
+
+.table-dark {
+ color: $table-dark-color;
+ background-color: $table-dark-bg;
+
+ th,
+ td,
+ thead th {
+ border-color: $table-dark-border-color;
+ }
+
+ &.table-bordered {
+ border: 0;
+ }
+
+ &.table-striped {
+ tbody tr:nth-of-type(odd) {
+ background-color: $table-dark-accent-bg;
+ }
+ }
+
+ &.table-hover {
+ tbody tr {
+ @include hover {
+ background-color: $table-dark-hover-bg;
+ }
+ }
+ }
+}
+
+
+// Responsive tables
+//
+// Generate series of `.table-responsive-*` classes for configuring the screen
+// size of where your table will overflow.
+
+.table-responsive {
+ @each $breakpoint in map-keys($grid-breakpoints) {
+ $next: breakpoint-next($breakpoint, $grid-breakpoints);
+ $infix: breakpoint-infix($next, $grid-breakpoints);
+
+ {$infix} {
+ @include media-breakpoint-down($breakpoint) {
+ display: block;
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ -ms-overflow-style: -ms-autohiding-scrollbar; // See https://github.com/twbs/bootstrap/pull/10057
+
+ // Prevent double border on horizontal scroll due to use of `display: block;`
+ > .table-bordered {
+ border: 0;
+ }
+ }
+ }
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/_tooltip.scss b/docs/assets/_scss/bootstrap-4.1.3/_tooltip.scss
new file mode 100755
index 00000000..1286ebfc
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/_tooltip.scss
@@ -0,0 +1,115 @@
+// Base class
+.tooltip {
+ position: absolute;
+ z-index: $zindex-tooltip;
+ display: block;
+ margin: $tooltip-margin;
+ // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.
+ // So reset our font and text properties to avoid inheriting weird values.
+ @include reset-text();
+ font-size: $tooltip-font-size;
+ // Allow breaking very long words so they don't overflow the tooltip's bounds
+ word-wrap: break-word;
+ opacity: 0;
+
+ &.show { opacity: $tooltip-opacity; }
+
+ .arrow {
+ position: absolute;
+ display: block;
+ width: $tooltip-arrow-width;
+ height: $tooltip-arrow-height;
+
+ &::before {
+ position: absolute;
+ content: "";
+ border-color: transparent;
+ border-style: solid;
+ }
+ }
+}
+
+.bs-tooltip-top {
+ padding: $tooltip-arrow-height 0;
+
+ .arrow {
+ bottom: 0;
+
+ &::before {
+ top: 0;
+ border-width: $tooltip-arrow-height ($tooltip-arrow-width / 2) 0;
+ border-top-color: $tooltip-arrow-color;
+ }
+ }
+}
+
+.bs-tooltip-right {
+ padding: 0 $tooltip-arrow-height;
+
+ .arrow {
+ left: 0;
+ width: $tooltip-arrow-height;
+ height: $tooltip-arrow-width;
+
+ &::before {
+ right: 0;
+ border-width: ($tooltip-arrow-width / 2) $tooltip-arrow-height ($tooltip-arrow-width / 2) 0;
+ border-right-color: $tooltip-arrow-color;
+ }
+ }
+}
+
+.bs-tooltip-bottom {
+ padding: $tooltip-arrow-height 0;
+
+ .arrow {
+ top: 0;
+
+ &::before {
+ bottom: 0;
+ border-width: 0 ($tooltip-arrow-width / 2) $tooltip-arrow-height;
+ border-bottom-color: $tooltip-arrow-color;
+ }
+ }
+}
+
+.bs-tooltip-left {
+ padding: 0 $tooltip-arrow-height;
+
+ .arrow {
+ right: 0;
+ width: $tooltip-arrow-height;
+ height: $tooltip-arrow-width;
+
+ &::before {
+ left: 0;
+ border-width: ($tooltip-arrow-width / 2) 0 ($tooltip-arrow-width / 2) $tooltip-arrow-height;
+ border-left-color: $tooltip-arrow-color;
+ }
+ }
+}
+
+.bs-tooltip-auto {
+ &[x-placement^="top"] {
+ @extend .bs-tooltip-top;
+ }
+ &[x-placement^="right"] {
+ @extend .bs-tooltip-right;
+ }
+ &[x-placement^="bottom"] {
+ @extend .bs-tooltip-bottom;
+ }
+ &[x-placement^="left"] {
+ @extend .bs-tooltip-left;
+ }
+}
+
+// Wrapper for the tooltip content
+.tooltip-inner {
+ max-width: $tooltip-max-width;
+ padding: $tooltip-padding-y $tooltip-padding-x;
+ color: $tooltip-color;
+ text-align: center;
+ background-color: $tooltip-bg;
+ @include border-radius($tooltip-border-radius);
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/_transitions.scss b/docs/assets/_scss/bootstrap-4.1.3/_transitions.scss
new file mode 100755
index 00000000..c8d91e27
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/_transitions.scss
@@ -0,0 +1,22 @@
+// stylelint-disable selector-no-qualifying-type
+
+.fade {
+ @include transition($transition-fade);
+
+ &:not(.show) {
+ opacity: 0;
+ }
+}
+
+.collapse {
+ &:not(.show) {
+ display: none;
+ }
+}
+
+.collapsing {
+ position: relative;
+ height: 0;
+ overflow: hidden;
+ @include transition($transition-collapse);
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/_type.scss b/docs/assets/_scss/bootstrap-4.1.3/_type.scss
new file mode 100755
index 00000000..57d610f0
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/_type.scss
@@ -0,0 +1,125 @@
+// stylelint-disable declaration-no-important, selector-list-comma-newline-after
+
+//
+// Headings
+//
+
+h1, h2, h3, h4, h5, h6,
+.h1, .h2, .h3, .h4, .h5, .h6 {
+ margin-bottom: $headings-margin-bottom;
+ font-family: $headings-font-family;
+ font-weight: $headings-font-weight;
+ line-height: $headings-line-height;
+ color: $headings-color;
+}
+
+h1, .h1 { font-size: $h1-font-size; }
+h2, .h2 { font-size: $h2-font-size; }
+h3, .h3 { font-size: $h3-font-size; }
+h4, .h4 { font-size: $h4-font-size; }
+h5, .h5 { font-size: $h5-font-size; }
+h6, .h6 { font-size: $h6-font-size; }
+
+.lead {
+ font-size: $lead-font-size;
+ font-weight: $lead-font-weight;
+}
+
+// Type display classes
+.display-1 {
+ font-size: $display1-size;
+ font-weight: $display1-weight;
+ line-height: $display-line-height;
+}
+.display-2 {
+ font-size: $display2-size;
+ font-weight: $display2-weight;
+ line-height: $display-line-height;
+}
+.display-3 {
+ font-size: $display3-size;
+ font-weight: $display3-weight;
+ line-height: $display-line-height;
+}
+.display-4 {
+ font-size: $display4-size;
+ font-weight: $display4-weight;
+ line-height: $display-line-height;
+}
+
+
+//
+// Horizontal rules
+//
+
+hr {
+ margin-top: $hr-margin-y;
+ margin-bottom: $hr-margin-y;
+ border: 0;
+ border-top: $hr-border-width solid $hr-border-color;
+}
+
+
+//
+// Emphasis
+//
+
+small,
+.small {
+ font-size: $small-font-size;
+ font-weight: $font-weight-normal;
+}
+
+mark,
+.mark {
+ padding: $mark-padding;
+ background-color: $mark-bg;
+}
+
+
+//
+// Lists
+//
+
+.list-unstyled {
+ @include list-unstyled;
+}
+
+// Inline turns list items into inline-block
+.list-inline {
+ @include list-unstyled;
+}
+.list-inline-item {
+ display: inline-block;
+
+ &:not(:last-child) {
+ margin-right: $list-inline-padding;
+ }
+}
+
+
+//
+// Misc
+//
+
+// Builds on `abbr`
+.initialism {
+ font-size: 90%;
+ text-transform: uppercase;
+}
+
+// Blockquotes
+.blockquote {
+ margin-bottom: $spacer;
+ font-size: $blockquote-font-size;
+}
+
+.blockquote-footer {
+ display: block;
+ font-size: 80%; // back to default font-size
+ color: $blockquote-small-color;
+
+ &::before {
+ content: "\2014 \00A0"; // em dash, nbsp
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/_utilities.scss b/docs/assets/_scss/bootstrap-4.1.3/_utilities.scss
new file mode 100755
index 00000000..6c7a7cdd
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/_utilities.scss
@@ -0,0 +1,15 @@
+@import "utilities/align";
+@import "utilities/background";
+@import "utilities/borders";
+@import "utilities/clearfix";
+@import "utilities/display";
+@import "utilities/embed";
+@import "utilities/flex";
+@import "utilities/float";
+@import "utilities/position";
+@import "utilities/screenreaders";
+@import "utilities/shadows";
+@import "utilities/sizing";
+@import "utilities/spacing";
+@import "utilities/text";
+@import "utilities/visibility";
diff --git a/docs/assets/_scss/bootstrap-4.1.3/_variables.scss b/docs/assets/_scss/bootstrap-4.1.3/_variables.scss
new file mode 100755
index 00000000..3ff6fa85
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/_variables.scss
@@ -0,0 +1,953 @@
+// Variables
+//
+// Variables should follow the `$component-state-property-size` formula for
+// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.
+
+
+//
+// Color system
+//
+
+$white: #fff !default;
+$gray-100: #f8f9fa !default;
+$gray-200: #e9ecef !default;
+$gray-300: #dee2e6 !default;
+$gray-400: #ced4da !default;
+$gray-500: #adb5bd !default;
+$gray-600: #6c757d !default;
+$gray-700: #495057 !default;
+$gray-800: #343a40 !default;
+$gray-900: #212529 !default;
+$black: #000 !default;
+
+$grays: () !default;
+// stylelint-disable-next-line scss/dollar-variable-default
+$grays: map-merge(
+ (
+ "100": $gray-100,
+ "200": $gray-200,
+ "300": $gray-300,
+ "400": $gray-400,
+ "500": $gray-500,
+ "600": $gray-600,
+ "700": $gray-700,
+ "800": $gray-800,
+ "900": $gray-900
+ ),
+ $grays
+);
+
+
+$blue: #007bff !default;
+$indigo: #6610f2 !default;
+$purple: #6f42c1 !default;
+$pink: #e83e8c !default;
+$red: #dc3545 !default;
+$orange: #fd7e14 !default;
+$yellow: #ffc107 !default;
+$green: #28a745 !default;
+$teal: #20c997 !default;
+$cyan: #17a2b8 !default;
+
+$colors: () !default;
+// stylelint-disable-next-line scss/dollar-variable-default
+$colors: map-merge(
+ (
+ "blue": $blue,
+ "indigo": $indigo,
+ "purple": $purple,
+ "pink": $pink,
+ "red": $red,
+ "orange": $orange,
+ "yellow": $yellow,
+ "green": $green,
+ "teal": $teal,
+ "cyan": $cyan,
+ "white": $white,
+ "gray": $gray-600,
+ "gray-dark": $gray-800
+ ),
+ $colors
+);
+
+$primary: $blue !default;
+$secondary: $gray-600 !default;
+$success: $green !default;
+$info: $cyan !default;
+$warning: $yellow !default;
+$danger: $red !default;
+$light: $gray-100 !default;
+$dark: $gray-800 !default;
+
+$theme-colors: () !default;
+// stylelint-disable-next-line scss/dollar-variable-default
+$theme-colors: map-merge(
+ (
+ "primary": $primary,
+ "secondary": $secondary,
+ "success": $success,
+ "info": $info,
+ "warning": $warning,
+ "danger": $danger,
+ "light": $light,
+ "dark": $dark
+ ),
+ $theme-colors
+);
+
+// Set a specific jump point for requesting color jumps
+$theme-color-interval: 8% !default;
+
+// The yiq lightness value that determines when the lightness of color changes from "dark" to "light". Acceptable values are between 0 and 255.
+$yiq-contrasted-threshold: 150 !default;
+
+// Customize the light and dark text colors for use in our YIQ color contrast function.
+$yiq-text-dark: $gray-900 !default;
+$yiq-text-light: $white !default;
+
+// Options
+//
+// Quickly modify global styling by enabling or disabling optional features.
+
+$enable-caret: true !default;
+$enable-rounded: true !default;
+$enable-shadows: false !default;
+$enable-gradients: false !default;
+$enable-transitions: true !default;
+$enable-hover-media-query: false !default; // Deprecated, no longer affects any compiled CSS
+$enable-grid-classes: true !default;
+$enable-print-styles: true !default;
+
+
+// Spacing
+//
+// Control the default styling of most Bootstrap elements by modifying these
+// variables. Mostly focused on spacing.
+// You can add more entries to the $spacers map, should you need more variation.
+
+$spacer: 1rem !default;
+$spacers: () !default;
+// stylelint-disable-next-line scss/dollar-variable-default
+$spacers: map-merge(
+ (
+ 0: 0,
+ 1: ($spacer * .25),
+ 2: ($spacer * .5),
+ 3: $spacer,
+ 4: ($spacer * 1.5),
+ 5: ($spacer * 3)
+ ),
+ $spacers
+);
+
+// This variable affects the `.h-*` and `.w-*` classes.
+$sizes: () !default;
+// stylelint-disable-next-line scss/dollar-variable-default
+$sizes: map-merge(
+ (
+ 25: 25%,
+ 50: 50%,
+ 75: 75%,
+ 100: 100%,
+ auto: auto
+ ),
+ $sizes
+);
+
+// Body
+//
+// Settings for the `` element.
+
+$body-bg: $white !default;
+$body-color: $gray-900 !default;
+
+// Links
+//
+// Style anchor elements.
+
+$link-color: theme-color("primary") !default;
+$link-decoration: none !default;
+$link-hover-color: darken($link-color, 15%) !default;
+$link-hover-decoration: underline !default;
+
+// Paragraphs
+//
+// Style p element.
+
+$paragraph-margin-bottom: 1rem !default;
+
+
+// Grid breakpoints
+//
+// Define the minimum dimensions at which your layout will change,
+// adapting to different screen sizes, for use in media queries.
+
+$grid-breakpoints: (
+ xs: 0,
+ sm: 576px,
+ md: 768px,
+ lg: 992px,
+ xl: 1200px
+) !default;
+
+@include _assert-ascending($grid-breakpoints, "$grid-breakpoints");
+@include _assert-starts-at-zero($grid-breakpoints);
+
+
+// Grid containers
+//
+// Define the maximum width of `.container` for different screen sizes.
+
+$container-max-widths: (
+ sm: 540px,
+ md: 720px,
+ lg: 960px,
+ xl: 1140px
+) !default;
+
+@include _assert-ascending($container-max-widths, "$container-max-widths");
+
+
+// Grid columns
+//
+// Set the number of columns and specify the width of the gutters.
+
+$grid-columns: 12 !default;
+$grid-gutter-width: 30px !default;
+
+// Components
+//
+// Define common padding and border radius sizes and more.
+
+$line-height-lg: 1.5 !default;
+$line-height-sm: 1.5 !default;
+
+$border-width: 1px !default;
+$border-color: $gray-300 !default;
+
+$border-radius: .25rem !default;
+$border-radius-lg: .3rem !default;
+$border-radius-sm: .2rem !default;
+
+$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;
+$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;
+$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;
+
+$component-active-color: $white !default;
+$component-active-bg: theme-color("primary") !default;
+
+$caret-width: .3em !default;
+
+$transition-base: all .2s ease-in-out !default;
+$transition-fade: opacity .15s linear !default;
+$transition-collapse: height .35s ease !default;
+
+
+// Fonts
+//
+// Font, line-height, and color for body text, headings, and more.
+
+// stylelint-disable value-keyword-case
+$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default;
+$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !default;
+$font-family-base: $font-family-sans-serif !default;
+// stylelint-enable value-keyword-case
+
+$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`
+$font-size-lg: ($font-size-base * 1.25) !default;
+$font-size-sm: ($font-size-base * .875) !default;
+$font-size-xs: ($font-size-base * .75) !default;
+
+$font-weight-light: 300 !default;
+$font-weight-normal: 400 !default;
+$font-weight-bold: 700 !default;
+
+$font-weight-base: $font-weight-normal !default;
+$line-height-base: 1.5 !default;
+
+$h1-font-size: $font-size-base * 2.5 !default;
+$h2-font-size: $font-size-base * 2 !default;
+$h3-font-size: $font-size-base * 1.75 !default;
+$h4-font-size: $font-size-base * 1.5 !default;
+$h5-font-size: $font-size-base * 1.25 !default;
+$h6-font-size: $font-size-base !default;
+
+$headings-margin-bottom: ($spacer / 2) !default;
+$headings-font-family: inherit !default;
+$headings-font-weight: 500 !default;
+$headings-line-height: 1.2 !default;
+$headings-color: inherit !default;
+
+$display1-size: 6rem !default;
+$display2-size: 5.5rem !default;
+$display3-size: 4.5rem !default;
+$display4-size: 3.5rem !default;
+
+$display1-weight: 300 !default;
+$display2-weight: 300 !default;
+$display3-weight: 300 !default;
+$display4-weight: 300 !default;
+$display-line-height: $headings-line-height !default;
+
+$lead-font-size: ($font-size-base * 1.25) !default;
+$lead-font-weight: 300 !default;
+
+$small-font-size: 80% !default;
+
+$text-muted: $gray-600 !default;
+
+$blockquote-small-color: $gray-600 !default;
+$blockquote-font-size: ($font-size-base * 1.25) !default;
+
+$hr-border-color: rgba($black, .1) !default;
+$hr-border-width: $border-width !default;
+
+$mark-padding: .2em !default;
+
+$dt-font-weight: $font-weight-bold !default;
+
+$kbd-box-shadow: inset 0 -.1rem 0 rgba($black, .25) !default;
+$nested-kbd-font-weight: $font-weight-bold !default;
+
+$list-inline-padding: .5rem !default;
+
+$mark-bg: #fcf8e3 !default;
+
+$hr-margin-y: $spacer !default;
+
+
+// Tables
+//
+// Customizes the `.table` component with basic values, each used across all table variations.
+
+$table-cell-padding: .75rem !default;
+$table-cell-padding-sm: .3rem !default;
+
+$table-bg: transparent !default;
+$table-accent-bg: rgba($black, .05) !default;
+$table-hover-bg: rgba($black, .075) !default;
+$table-active-bg: $table-hover-bg !default;
+
+$table-border-width: $border-width !default;
+$table-border-color: $gray-300 !default;
+
+$table-head-bg: $gray-200 !default;
+$table-head-color: $gray-700 !default;
+
+$table-dark-bg: $gray-900 !default;
+$table-dark-accent-bg: rgba($white, .05) !default;
+$table-dark-hover-bg: rgba($white, .075) !default;
+$table-dark-border-color: lighten($gray-900, 7.5%) !default;
+$table-dark-color: $body-bg !default;
+
+$table-striped-order: odd !default;
+
+$table-caption-color: $text-muted !default;
+
+// Buttons + Forms
+//
+// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.
+
+$input-btn-padding-y: .375rem !default;
+$input-btn-padding-x: .75rem !default;
+$input-btn-line-height: $line-height-base !default;
+
+$input-btn-focus-width: .2rem !default;
+$input-btn-focus-color: rgba($component-active-bg, .25) !default;
+$input-btn-focus-box-shadow: 0 0 0 $input-btn-focus-width $input-btn-focus-color !default;
+
+$input-btn-padding-y-sm: .25rem !default;
+$input-btn-padding-x-sm: .5rem !default;
+$input-btn-line-height-sm: $line-height-sm !default;
+
+$input-btn-padding-y-lg: .5rem !default;
+$input-btn-padding-x-lg: 1rem !default;
+$input-btn-line-height-lg: $line-height-lg !default;
+
+$input-btn-border-width: $border-width !default;
+
+
+// Buttons
+//
+// For each of Bootstrap's buttons, define text, background, and border color.
+
+$btn-padding-y: $input-btn-padding-y !default;
+$btn-padding-x: $input-btn-padding-x !default;
+$btn-line-height: $input-btn-line-height !default;
+
+$btn-padding-y-sm: $input-btn-padding-y-sm !default;
+$btn-padding-x-sm: $input-btn-padding-x-sm !default;
+$btn-line-height-sm: $input-btn-line-height-sm !default;
+
+$btn-padding-y-lg: $input-btn-padding-y-lg !default;
+$btn-padding-x-lg: $input-btn-padding-x-lg !default;
+$btn-line-height-lg: $input-btn-line-height-lg !default;
+
+$btn-border-width: $input-btn-border-width !default;
+
+$btn-font-weight: $font-weight-normal !default;
+$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;
+$btn-focus-width: $input-btn-focus-width !default;
+$btn-focus-box-shadow: $input-btn-focus-box-shadow !default;
+$btn-disabled-opacity: .65 !default;
+$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default;
+
+$btn-link-disabled-color: $gray-600 !default;
+
+$btn-block-spacing-y: .5rem !default;
+
+// Allows for customizing button radius independently from global border radius
+$btn-border-radius: $border-radius !default;
+$btn-border-radius-lg: $border-radius-lg !default;
+$btn-border-radius-sm: $border-radius-sm !default;
+
+$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;
+
+
+// Forms
+
+$label-margin-bottom: .5rem !default;
+
+$input-padding-y: $input-btn-padding-y !default;
+$input-padding-x: $input-btn-padding-x !default;
+$input-line-height: $input-btn-line-height !default;
+
+$input-padding-y-sm: $input-btn-padding-y-sm !default;
+$input-padding-x-sm: $input-btn-padding-x-sm !default;
+$input-line-height-sm: $input-btn-line-height-sm !default;
+
+$input-padding-y-lg: $input-btn-padding-y-lg !default;
+$input-padding-x-lg: $input-btn-padding-x-lg !default;
+$input-line-height-lg: $input-btn-line-height-lg !default;
+
+$input-bg: $white !default;
+$input-disabled-bg: $gray-200 !default;
+
+$input-color: $gray-700 !default;
+$input-border-color: $gray-400 !default;
+$input-border-width: $input-btn-border-width !default;
+$input-box-shadow: inset 0 1px 1px rgba($black, .075) !default;
+
+$input-border-radius: $border-radius !default;
+$input-border-radius-lg: $border-radius-lg !default;
+$input-border-radius-sm: $border-radius-sm !default;
+
+$input-focus-bg: $input-bg !default;
+$input-focus-border-color: lighten($component-active-bg, 25%) !default;
+$input-focus-color: $input-color !default;
+$input-focus-width: $input-btn-focus-width !default;
+$input-focus-box-shadow: $input-btn-focus-box-shadow !default;
+
+$input-placeholder-color: $gray-600 !default;
+$input-plaintext-color: $body-color !default;
+
+$input-height-border: $input-border-width * 2 !default;
+
+$input-height-inner: ($font-size-base * $input-btn-line-height) + ($input-btn-padding-y * 2) !default;
+$input-height: calc(#{$input-height-inner} + #{$input-height-border}) !default;
+
+$input-height-inner-sm: ($font-size-sm * $input-btn-line-height-sm) + ($input-btn-padding-y-sm * 2) !default;
+$input-height-sm: calc(#{$input-height-inner-sm} + #{$input-height-border}) !default;
+
+$input-height-inner-lg: ($font-size-lg * $input-btn-line-height-lg) + ($input-btn-padding-y-lg * 2) !default;
+$input-height-lg: calc(#{$input-height-inner-lg} + #{$input-height-border}) !default;
+
+$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;
+
+$form-text-margin-top: .25rem !default;
+
+$form-check-input-gutter: 1.25rem !default;
+$form-check-input-margin-y: .3rem !default;
+$form-check-input-margin-x: .25rem !default;
+
+$form-check-inline-margin-x: .75rem !default;
+$form-check-inline-input-margin-x: .3125rem !default;
+
+$form-group-margin-bottom: 1rem !default;
+
+$input-group-addon-color: $input-color !default;
+$input-group-addon-bg: $gray-200 !default;
+$input-group-addon-border-color: $input-border-color !default;
+
+$custom-forms-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;
+
+$custom-control-gutter: 1.5rem !default;
+$custom-control-spacer-x: 1rem !default;
+
+$custom-control-indicator-size: 1rem !default;
+$custom-control-indicator-bg: $gray-300 !default;
+$custom-control-indicator-bg-size: 50% 50% !default;
+$custom-control-indicator-box-shadow: inset 0 .25rem .25rem rgba($black, .1) !default;
+
+$custom-control-indicator-disabled-bg: $gray-200 !default;
+$custom-control-label-disabled-color: $gray-600 !default;
+
+$custom-control-indicator-checked-color: $component-active-color !default;
+$custom-control-indicator-checked-bg: $component-active-bg !default;
+$custom-control-indicator-checked-disabled-bg: rgba(theme-color("primary"), .5) !default;
+$custom-control-indicator-checked-box-shadow: none !default;
+
+$custom-control-indicator-focus-box-shadow: 0 0 0 1px $body-bg, $input-btn-focus-box-shadow !default;
+
+$custom-control-indicator-active-color: $component-active-color !default;
+$custom-control-indicator-active-bg: lighten($component-active-bg, 35%) !default;
+$custom-control-indicator-active-box-shadow: none !default;
+
+$custom-checkbox-indicator-border-radius: $border-radius !default;
+$custom-checkbox-indicator-icon-checked: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='#{$custom-control-indicator-checked-color}' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E"), "#", "%23") !default;
+
+$custom-checkbox-indicator-indeterminate-bg: $component-active-bg !default;
+$custom-checkbox-indicator-indeterminate-color: $custom-control-indicator-checked-color !default;
+$custom-checkbox-indicator-icon-indeterminate: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='#{$custom-checkbox-indicator-indeterminate-color}' d='M0 2h4'/%3E%3C/svg%3E"), "#", "%23") !default;
+$custom-checkbox-indicator-indeterminate-box-shadow: none !default;
+
+$custom-radio-indicator-border-radius: 50% !default;
+$custom-radio-indicator-icon-checked: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='#{$custom-control-indicator-checked-color}'/%3E%3C/svg%3E"), "#", "%23") !default;
+
+$custom-select-padding-y: .375rem !default;
+$custom-select-padding-x: .75rem !default;
+$custom-select-height: $input-height !default;
+$custom-select-indicator-padding: 1rem !default; // Extra padding to account for the presence of the background-image based indicator
+$custom-select-line-height: $input-btn-line-height !default;
+$custom-select-color: $input-color !default;
+$custom-select-disabled-color: $gray-600 !default;
+$custom-select-bg: $input-bg !default;
+$custom-select-disabled-bg: $gray-200 !default;
+$custom-select-bg-size: 8px 10px !default; // In pixels because image dimensions
+$custom-select-indicator-color: $gray-800 !default;
+$custom-select-indicator: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='#{$custom-select-indicator-color}' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E"), "#", "%23") !default;
+$custom-select-border-width: $input-btn-border-width !default;
+$custom-select-border-color: $input-border-color !default;
+$custom-select-border-radius: $border-radius !default;
+$custom-select-box-shadow: inset 0 1px 2px rgba($black, .075) !default;
+
+$custom-select-focus-border-color: $input-focus-border-color !default;
+$custom-select-focus-width: $input-btn-focus-width !default;
+$custom-select-focus-box-shadow: 0 0 0 $custom-select-focus-width rgba($custom-select-focus-border-color, .5) !default;
+
+$custom-select-font-size-sm: 75% !default;
+$custom-select-height-sm: $input-height-sm !default;
+
+$custom-select-font-size-lg: 125% !default;
+$custom-select-height-lg: $input-height-lg !default;
+
+$custom-range-track-width: 100% !default;
+$custom-range-track-height: .5rem !default;
+$custom-range-track-cursor: pointer !default;
+$custom-range-track-bg: $gray-300 !default;
+$custom-range-track-border-radius: 1rem !default;
+$custom-range-track-box-shadow: inset 0 .25rem .25rem rgba($black, .1) !default;
+
+$custom-range-thumb-width: 1rem !default;
+$custom-range-thumb-height: $custom-range-thumb-width !default;
+$custom-range-thumb-bg: $component-active-bg !default;
+$custom-range-thumb-border: 0 !default;
+$custom-range-thumb-border-radius: 1rem !default;
+$custom-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default;
+$custom-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-btn-focus-box-shadow !default;
+$custom-range-thumb-focus-box-shadow-width: $input-btn-focus-width !default; // For focus box shadow issue in IE/Edge
+$custom-range-thumb-active-bg: lighten($component-active-bg, 35%) !default;
+
+$custom-file-height: $input-height !default;
+$custom-file-height-inner: $input-height-inner !default;
+$custom-file-focus-border-color: $input-focus-border-color !default;
+$custom-file-focus-box-shadow: $input-btn-focus-box-shadow !default;
+$custom-file-disabled-bg: $input-disabled-bg !default;
+
+$custom-file-padding-y: $input-btn-padding-y !default;
+$custom-file-padding-x: $input-btn-padding-x !default;
+$custom-file-line-height: $input-btn-line-height !default;
+$custom-file-color: $input-color !default;
+$custom-file-bg: $input-bg !default;
+$custom-file-border-width: $input-btn-border-width !default;
+$custom-file-border-color: $input-border-color !default;
+$custom-file-border-radius: $input-border-radius !default;
+$custom-file-box-shadow: $input-box-shadow !default;
+$custom-file-button-color: $custom-file-color !default;
+$custom-file-button-bg: $input-group-addon-bg !default;
+$custom-file-text: (
+ en: "Browse"
+) !default;
+
+
+// Form validation
+$form-feedback-margin-top: $form-text-margin-top !default;
+$form-feedback-font-size: $small-font-size !default;
+$form-feedback-valid-color: theme-color("success") !default;
+$form-feedback-invalid-color: theme-color("danger") !default;
+
+
+// Dropdowns
+//
+// Dropdown menu container and contents.
+
+$dropdown-min-width: 10rem !default;
+$dropdown-padding-y: .5rem !default;
+$dropdown-spacer: .125rem !default;
+$dropdown-bg: $white !default;
+$dropdown-border-color: rgba($black, .15) !default;
+$dropdown-border-radius: $border-radius !default;
+$dropdown-border-width: $border-width !default;
+$dropdown-divider-bg: $gray-200 !default;
+$dropdown-box-shadow: 0 .5rem 1rem rgba($black, .175) !default;
+
+$dropdown-link-color: $gray-900 !default;
+$dropdown-link-hover-color: darken($gray-900, 5%) !default;
+$dropdown-link-hover-bg: $gray-100 !default;
+
+$dropdown-link-active-color: $component-active-color !default;
+$dropdown-link-active-bg: $component-active-bg !default;
+
+$dropdown-link-disabled-color: $gray-600 !default;
+
+$dropdown-item-padding-y: .25rem !default;
+$dropdown-item-padding-x: 1.5rem !default;
+
+$dropdown-header-color: $gray-600 !default;
+
+
+// Z-index master list
+//
+// Warning: Avoid customizing these values. They're used for a bird's eye view
+// of components dependent on the z-axis and are designed to all work together.
+
+$zindex-dropdown: 1000 !default;
+$zindex-sticky: 1020 !default;
+$zindex-fixed: 1030 !default;
+$zindex-modal-backdrop: 1040 !default;
+$zindex-modal: 1050 !default;
+$zindex-popover: 1060 !default;
+$zindex-tooltip: 1070 !default;
+
+// Navs
+
+$nav-link-padding-y: .5rem !default;
+$nav-link-padding-x: 1rem !default;
+$nav-link-disabled-color: $gray-600 !default;
+
+$nav-tabs-border-color: $gray-300 !default;
+$nav-tabs-border-width: $border-width !default;
+$nav-tabs-border-radius: $border-radius !default;
+$nav-tabs-link-hover-border-color: $gray-200 $gray-200 $nav-tabs-border-color !default;
+$nav-tabs-link-active-color: $gray-700 !default;
+$nav-tabs-link-active-bg: $body-bg !default;
+$nav-tabs-link-active-border-color: $gray-300 $gray-300 $nav-tabs-link-active-bg !default;
+
+$nav-pills-border-radius: $border-radius !default;
+$nav-pills-link-active-color: $component-active-color !default;
+$nav-pills-link-active-bg: $component-active-bg !default;
+
+$nav-divider-color: $gray-200 !default;
+$nav-divider-margin-y: ($spacer / 2) !default;
+
+// Navbar
+
+$navbar-padding-y: ($spacer / 2) !default;
+$navbar-padding-x: $spacer !default;
+
+$navbar-nav-link-padding-x: .5rem !default;
+
+$navbar-brand-font-size: $font-size-lg !default;
+// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link
+$nav-link-height: ($font-size-base * $line-height-base + $nav-link-padding-y * 2) !default;
+$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default;
+$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) / 2 !default;
+
+$navbar-toggler-padding-y: .25rem !default;
+$navbar-toggler-padding-x: .75rem !default;
+$navbar-toggler-font-size: $font-size-lg !default;
+$navbar-toggler-border-radius: $btn-border-radius !default;
+
+$navbar-dark-color: rgba($white, .5) !default;
+$navbar-dark-hover-color: rgba($white, .75) !default;
+$navbar-dark-active-color: $white !default;
+$navbar-dark-disabled-color: rgba($white, .25) !default;
+$navbar-dark-toggler-icon-bg: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='#{$navbar-dark-color}' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"), "#", "%23") !default;
+$navbar-dark-toggler-border-color: rgba($white, .1) !default;
+
+$navbar-light-color: rgba($black, .5) !default;
+$navbar-light-hover-color: rgba($black, .7) !default;
+$navbar-light-active-color: rgba($black, .9) !default;
+$navbar-light-disabled-color: rgba($black, .3) !default;
+$navbar-light-toggler-icon-bg: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='#{$navbar-light-color}' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"), "#", "%23") !default;
+$navbar-light-toggler-border-color: rgba($black, .1) !default;
+
+// Pagination
+
+$pagination-padding-y: .5rem !default;
+$pagination-padding-x: .75rem !default;
+$pagination-padding-y-sm: .25rem !default;
+$pagination-padding-x-sm: .5rem !default;
+$pagination-padding-y-lg: .75rem !default;
+$pagination-padding-x-lg: 1.5rem !default;
+$pagination-line-height: 1.25 !default;
+
+$pagination-color: $link-color !default;
+$pagination-bg: $white !default;
+$pagination-border-width: $border-width !default;
+$pagination-border-color: $gray-300 !default;
+
+$pagination-focus-box-shadow: $input-btn-focus-box-shadow !default;
+$pagination-focus-outline: 0 !default;
+
+$pagination-hover-color: $link-hover-color !default;
+$pagination-hover-bg: $gray-200 !default;
+$pagination-hover-border-color: $gray-300 !default;
+
+$pagination-active-color: $component-active-color !default;
+$pagination-active-bg: $component-active-bg !default;
+$pagination-active-border-color: $pagination-active-bg !default;
+
+$pagination-disabled-color: $gray-600 !default;
+$pagination-disabled-bg: $white !default;
+$pagination-disabled-border-color: $gray-300 !default;
+
+
+// Jumbotron
+
+$jumbotron-padding: 2rem !default;
+$jumbotron-bg: $gray-200 !default;
+
+
+// Cards
+
+$card-spacer-y: .75rem !default;
+$card-spacer-x: 1.25rem !default;
+$card-border-width: $border-width !default;
+$card-border-radius: $border-radius !default;
+$card-border-color: rgba($black, .125) !default;
+$card-inner-border-radius: calc(#{$card-border-radius} - #{$card-border-width}) !default;
+$card-cap-bg: rgba($black, .03) !default;
+$card-bg: $white !default;
+
+$card-img-overlay-padding: 1.25rem !default;
+
+$card-group-margin: ($grid-gutter-width / 2) !default;
+$card-deck-margin: $card-group-margin !default;
+
+$card-columns-count: 3 !default;
+$card-columns-gap: 1.25rem !default;
+$card-columns-margin: $card-spacer-y !default;
+
+
+// Tooltips
+
+$tooltip-font-size: $font-size-sm !default;
+$tooltip-max-width: 200px !default;
+$tooltip-color: $white !default;
+$tooltip-bg: $black !default;
+$tooltip-border-radius: $border-radius !default;
+$tooltip-opacity: .9 !default;
+$tooltip-padding-y: .25rem !default;
+$tooltip-padding-x: .5rem !default;
+$tooltip-margin: 0 !default;
+
+$tooltip-arrow-width: .8rem !default;
+$tooltip-arrow-height: .4rem !default;
+$tooltip-arrow-color: $tooltip-bg !default;
+
+
+// Popovers
+
+$popover-font-size: $font-size-sm !default;
+$popover-bg: $white !default;
+$popover-max-width: 276px !default;
+$popover-border-width: $border-width !default;
+$popover-border-color: rgba($black, .2) !default;
+$popover-border-radius: $border-radius-lg !default;
+$popover-box-shadow: 0 .25rem .5rem rgba($black, .2) !default;
+
+$popover-header-bg: darken($popover-bg, 3%) !default;
+$popover-header-color: $headings-color !default;
+$popover-header-padding-y: .5rem !default;
+$popover-header-padding-x: .75rem !default;
+
+$popover-body-color: $body-color !default;
+$popover-body-padding-y: $popover-header-padding-y !default;
+$popover-body-padding-x: $popover-header-padding-x !default;
+
+$popover-arrow-width: 1rem !default;
+$popover-arrow-height: .5rem !default;
+$popover-arrow-color: $popover-bg !default;
+
+$popover-arrow-outer-color: fade-in($popover-border-color, .05) !default;
+
+
+// Badges
+
+$badge-font-size: 75% !default;
+$badge-font-weight: $font-weight-bold !default;
+$badge-padding-y: .25em !default;
+$badge-padding-x: .4em !default;
+$badge-border-radius: $border-radius !default;
+
+$badge-pill-padding-x: .6em !default;
+// Use a higher than normal value to ensure completely rounded edges when
+// customizing padding or font-size on labels.
+$badge-pill-border-radius: 10rem !default;
+
+
+// Modals
+
+// Padding applied to the modal body
+$modal-inner-padding: 1rem !default;
+
+$modal-dialog-margin: .5rem !default;
+$modal-dialog-margin-y-sm-up: 1.75rem !default;
+
+$modal-title-line-height: $line-height-base !default;
+
+$modal-content-bg: $white !default;
+$modal-content-border-color: rgba($black, .2) !default;
+$modal-content-border-width: $border-width !default;
+$modal-content-border-radius: $border-radius-lg !default;
+$modal-content-box-shadow-xs: 0 .25rem .5rem rgba($black, .5) !default;
+$modal-content-box-shadow-sm-up: 0 .5rem 1rem rgba($black, .5) !default;
+
+$modal-backdrop-bg: $black !default;
+$modal-backdrop-opacity: .5 !default;
+$modal-header-border-color: $gray-200 !default;
+$modal-footer-border-color: $modal-header-border-color !default;
+$modal-header-border-width: $modal-content-border-width !default;
+$modal-footer-border-width: $modal-header-border-width !default;
+$modal-header-padding: 1rem !default;
+
+$modal-lg: 800px !default;
+$modal-md: 500px !default;
+$modal-sm: 300px !default;
+
+$modal-transition: transform .3s ease-out !default;
+
+
+// Alerts
+//
+// Define alert colors, border radius, and padding.
+
+$alert-padding-y: .75rem !default;
+$alert-padding-x: 1.25rem !default;
+$alert-margin-bottom: 1rem !default;
+$alert-border-radius: $border-radius !default;
+$alert-link-font-weight: $font-weight-bold !default;
+$alert-border-width: $border-width !default;
+
+$alert-bg-level: -10 !default;
+$alert-border-level: -9 !default;
+$alert-color-level: 6 !default;
+
+
+// Progress bars
+
+$progress-height: 1rem !default;
+$progress-font-size: ($font-size-base * .75) !default;
+$progress-bg: $gray-200 !default;
+$progress-border-radius: $border-radius !default;
+$progress-box-shadow: inset 0 .1rem .1rem rgba($black, .1) !default;
+$progress-bar-color: $white !default;
+$progress-bar-bg: theme-color("primary") !default;
+$progress-bar-animation-timing: 1s linear infinite !default;
+$progress-bar-transition: width .6s ease !default;
+
+// List group
+
+$list-group-bg: $white !default;
+$list-group-border-color: rgba($black, .125) !default;
+$list-group-border-width: $border-width !default;
+$list-group-border-radius: $border-radius !default;
+
+$list-group-item-padding-y: .75rem !default;
+$list-group-item-padding-x: 1.25rem !default;
+
+$list-group-hover-bg: $gray-100 !default;
+$list-group-active-color: $component-active-color !default;
+$list-group-active-bg: $component-active-bg !default;
+$list-group-active-border-color: $list-group-active-bg !default;
+
+$list-group-disabled-color: $gray-600 !default;
+$list-group-disabled-bg: $list-group-bg !default;
+
+$list-group-action-color: $gray-700 !default;
+$list-group-action-hover-color: $list-group-action-color !default;
+
+$list-group-action-active-color: $body-color !default;
+$list-group-action-active-bg: $gray-200 !default;
+
+
+// Image thumbnails
+
+$thumbnail-padding: .25rem !default;
+$thumbnail-bg: $body-bg !default;
+$thumbnail-border-width: $border-width !default;
+$thumbnail-border-color: $gray-300 !default;
+$thumbnail-border-radius: $border-radius !default;
+$thumbnail-box-shadow: 0 1px 2px rgba($black, .075) !default;
+
+
+// Figures
+
+$figure-caption-font-size: 90% !default;
+$figure-caption-color: $gray-600 !default;
+
+
+// Breadcrumbs
+
+$breadcrumb-padding-y: .75rem !default;
+$breadcrumb-padding-x: 1rem !default;
+$breadcrumb-item-padding: .5rem !default;
+
+$breadcrumb-margin-bottom: 1rem !default;
+
+$breadcrumb-bg: $gray-200 !default;
+$breadcrumb-divider-color: $gray-600 !default;
+$breadcrumb-active-color: $gray-600 !default;
+$breadcrumb-divider: quote("/") !default;
+
+$breadcrumb-border-radius: $border-radius !default;
+
+
+// Carousel
+
+$carousel-control-color: $white !default;
+$carousel-control-width: 15% !default;
+$carousel-control-opacity: .5 !default;
+
+$carousel-indicator-width: 30px !default;
+$carousel-indicator-height: 3px !default;
+$carousel-indicator-spacer: 3px !default;
+$carousel-indicator-active-bg: $white !default;
+
+$carousel-caption-width: 70% !default;
+$carousel-caption-color: $white !default;
+
+$carousel-control-icon-width: 20px !default;
+
+$carousel-control-prev-icon-bg: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='#{$carousel-control-color}' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E"), "#", "%23") !default;
+$carousel-control-next-icon-bg: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='#{$carousel-control-color}' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E"), "#", "%23") !default;
+
+$carousel-transition: transform .6s ease !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)
+
+
+// Close
+
+$close-font-size: $font-size-base * 1.5 !default;
+$close-font-weight: $font-weight-bold !default;
+$close-color: $black !default;
+$close-text-shadow: 0 1px 0 $white !default;
+
+// Code
+
+$code-font-size: 87.5% !default;
+$code-color: $pink !default;
+
+$kbd-padding-y: .2rem !default;
+$kbd-padding-x: .4rem !default;
+$kbd-font-size: $code-font-size !default;
+$kbd-color: $white !default;
+$kbd-bg: $gray-900 !default;
+
+$pre-color: $gray-900 !default;
+$pre-scrollable-max-height: 340px !default;
+
+
+// Printing
+$print-page-size: a3 !default;
+$print-body-min-width: map-get($grid-breakpoints, "lg") !default;
diff --git a/docs/assets/_scss/bootstrap-4.1.3/bootstrap-grid.scss b/docs/assets/_scss/bootstrap-4.1.3/bootstrap-grid.scss
new file mode 100755
index 00000000..509171d4
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/bootstrap-grid.scss
@@ -0,0 +1,32 @@
+/*!
+ * Bootstrap Grid v4.1.3 (https://getbootstrap.com/)
+ * Copyright 2011-2018 The Bootstrap Authors
+ * Copyright 2011-2018 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+@at-root {
+ @-ms-viewport { width: device-width; } // stylelint-disable-line at-rule-no-vendor-prefix
+}
+
+html {
+ box-sizing: border-box;
+ -ms-overflow-style: scrollbar;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: inherit;
+}
+
+@import "functions";
+@import "variables";
+
+@import "mixins/breakpoints";
+@import "mixins/grid-framework";
+@import "mixins/grid";
+
+@import "grid";
+@import "utilities/display";
+@import "utilities/flex";
diff --git a/docs/assets/_scss/bootstrap-4.1.3/bootstrap-reboot.scss b/docs/assets/_scss/bootstrap-4.1.3/bootstrap-reboot.scss
new file mode 100755
index 00000000..75baeb71
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/bootstrap-reboot.scss
@@ -0,0 +1,12 @@
+/*!
+ * Bootstrap Reboot v4.1.3 (https://getbootstrap.com/)
+ * Copyright 2011-2018 The Bootstrap Authors
+ * Copyright 2011-2018 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
+ */
+
+@import "functions";
+@import "variables";
+@import "mixins";
+@import "reboot";
diff --git a/docs/assets/_scss/bootstrap-4.1.3/bootstrap.scss b/docs/assets/_scss/bootstrap-4.1.3/bootstrap.scss
new file mode 100755
index 00000000..e3f76546
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/bootstrap.scss
@@ -0,0 +1,42 @@
+/*!
+ * Bootstrap v4.1.3 (https://getbootstrap.com/)
+ * Copyright 2011-2018 The Bootstrap Authors
+ * Copyright 2011-2018 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+@import "functions";
+@import "variables";
+@import "mixins";
+@import "root";
+@import "reboot";
+@import "type";
+@import "images";
+@import "code";
+@import "grid";
+@import "tables";
+@import "forms";
+@import "buttons";
+@import "transitions";
+@import "dropdown";
+@import "button-group";
+@import "input-group";
+@import "custom-forms";
+@import "nav";
+@import "navbar";
+@import "card";
+@import "breadcrumb";
+@import "pagination";
+@import "badge";
+@import "jumbotron";
+@import "alert";
+@import "progress";
+@import "media";
+@import "list-group";
+@import "close";
+@import "modal";
+@import "tooltip";
+@import "popover";
+@import "carousel";
+@import "utilities";
+@import "print";
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_alert.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_alert.scss
new file mode 100755
index 00000000..db5a7eb4
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_alert.scss
@@ -0,0 +1,13 @@
+@mixin alert-variant($background, $border, $color) {
+ color: $color;
+ @include gradient-bg($background);
+ border-color: $border;
+
+ hr {
+ border-top-color: darken($border, 5%);
+ }
+
+ .alert-link {
+ color: darken($color, 10%);
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_background-variant.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_background-variant.scss
new file mode 100755
index 00000000..494439d2
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_background-variant.scss
@@ -0,0 +1,21 @@
+// stylelint-disable declaration-no-important
+
+// Contextual backgrounds
+
+@mixin bg-variant($parent, $color) {
+ #{$parent} {
+ background-color: $color !important;
+ }
+ a#{$parent},
+ button#{$parent} {
+ @include hover-focus {
+ background-color: darken($color, 10%) !important;
+ }
+ }
+}
+
+@mixin bg-gradient-variant($parent, $color) {
+ #{$parent} {
+ background: $color linear-gradient(180deg, mix($body-bg, $color, 15%), $color) repeat-x !important;
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_badge.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_badge.scss
new file mode 100755
index 00000000..eeca0b40
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_badge.scss
@@ -0,0 +1,12 @@
+@mixin badge-variant($bg) {
+ color: color-yiq($bg);
+ background-color: $bg;
+
+ &[href] {
+ @include hover-focus {
+ color: color-yiq($bg);
+ text-decoration: none;
+ background-color: darken($bg, 10%);
+ }
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_border-radius.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_border-radius.scss
new file mode 100755
index 00000000..2024febc
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_border-radius.scss
@@ -0,0 +1,35 @@
+// Single side border-radius
+
+@mixin border-radius($radius: $border-radius) {
+ @if $enable-rounded {
+ border-radius: $radius;
+ }
+}
+
+@mixin border-top-radius($radius) {
+ @if $enable-rounded {
+ border-top-left-radius: $radius;
+ border-top-right-radius: $radius;
+ }
+}
+
+@mixin border-right-radius($radius) {
+ @if $enable-rounded {
+ border-top-right-radius: $radius;
+ border-bottom-right-radius: $radius;
+ }
+}
+
+@mixin border-bottom-radius($radius) {
+ @if $enable-rounded {
+ border-bottom-right-radius: $radius;
+ border-bottom-left-radius: $radius;
+ }
+}
+
+@mixin border-left-radius($radius) {
+ @if $enable-rounded {
+ border-top-left-radius: $radius;
+ border-bottom-left-radius: $radius;
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_box-shadow.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_box-shadow.scss
new file mode 100755
index 00000000..b2410e53
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_box-shadow.scss
@@ -0,0 +1,5 @@
+@mixin box-shadow($shadow...) {
+ @if $enable-shadows {
+ box-shadow: $shadow;
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_breakpoints.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_breakpoints.scss
new file mode 100755
index 00000000..59f25a27
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_breakpoints.scss
@@ -0,0 +1,123 @@
+// Breakpoint viewport sizes and media queries.
+//
+// Breakpoints are defined as a map of (name: minimum width), order from small to large:
+//
+// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)
+//
+// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.
+
+// Name of the next breakpoint, or null for the last breakpoint.
+//
+// >> breakpoint-next(sm)
+// md
+// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
+// md
+// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))
+// md
+@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {
+ $n: index($breakpoint-names, $name);
+ @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);
+}
+
+// Minimum breakpoint width. Null for the smallest (first) breakpoint.
+//
+// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
+// 576px
+@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {
+ $min: map-get($breakpoints, $name);
+ @return if($min != 0, $min, null);
+}
+
+// Maximum breakpoint width. Null for the largest (last) breakpoint.
+// The maximum value is calculated as the minimum of the next one less 0.02px
+// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.
+// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max
+// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.
+// See https://bugs.webkit.org/show_bug.cgi?id=178261
+//
+// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
+// 767.98px
+@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {
+ $next: breakpoint-next($name, $breakpoints);
+ @return if($next, breakpoint-min($next, $breakpoints) - .02px, null);
+}
+
+// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.
+// Useful for making responsive utilities.
+//
+// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
+// "" (Returns a blank string)
+// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
+// "-sm"
+@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {
+ @return if(breakpoint-min($name, $breakpoints) == null, "", "-#{$name}");
+}
+
+// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.
+// Makes the @content apply to the given breakpoint and wider.
+@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {
+ $min: breakpoint-min($name, $breakpoints);
+ @if $min {
+ @media (min-width: $min) {
+ @content;
+ }
+ } @else {
+ @content;
+ }
+}
+
+// Media of at most the maximum breakpoint width. No query for the largest breakpoint.
+// Makes the @content apply to the given breakpoint and narrower.
+@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {
+ $max: breakpoint-max($name, $breakpoints);
+ @if $max {
+ @media (max-width: $max) {
+ @content;
+ }
+ } @else {
+ @content;
+ }
+}
+
+// Media that spans multiple breakpoint widths.
+// Makes the @content apply between the min and max breakpoints
+@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {
+ $min: breakpoint-min($lower, $breakpoints);
+ $max: breakpoint-max($upper, $breakpoints);
+
+ @if $min != null and $max != null {
+ @media (min-width: $min) and (max-width: $max) {
+ @content;
+ }
+ } @else if $max == null {
+ @include media-breakpoint-up($lower, $breakpoints) {
+ @content;
+ }
+ } @else if $min == null {
+ @include media-breakpoint-down($upper, $breakpoints) {
+ @content;
+ }
+ }
+}
+
+// Media between the breakpoint's minimum and maximum widths.
+// No minimum for the smallest breakpoint, and no maximum for the largest one.
+// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.
+@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {
+ $min: breakpoint-min($name, $breakpoints);
+ $max: breakpoint-max($name, $breakpoints);
+
+ @if $min != null and $max != null {
+ @media (min-width: $min) and (max-width: $max) {
+ @content;
+ }
+ } @else if $max == null {
+ @include media-breakpoint-up($name, $breakpoints) {
+ @content;
+ }
+ } @else if $min == null {
+ @include media-breakpoint-down($name, $breakpoints) {
+ @content;
+ }
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_buttons.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_buttons.scss
new file mode 100755
index 00000000..06ad6772
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_buttons.scss
@@ -0,0 +1,109 @@
+// Button variants
+//
+// Easily pump out default styles, as well as :hover, :focus, :active,
+// and disabled options for all buttons
+
+@mixin button-variant($background, $border, $hover-background: darken($background, 7.5%), $hover-border: darken($border, 10%), $active-background: darken($background, 10%), $active-border: darken($border, 12.5%)) {
+ color: color-yiq($background);
+ @include gradient-bg($background);
+ border-color: $border;
+ @include box-shadow($btn-box-shadow);
+
+ @include hover {
+ color: color-yiq($hover-background);
+ @include gradient-bg($hover-background);
+ border-color: $hover-border;
+ }
+
+ &:focus,
+ &.focus {
+ // Avoid using mixin so we can pass custom focus shadow properly
+ @if $enable-shadows {
+ box-shadow: $btn-box-shadow, 0 0 0 $btn-focus-width rgba($border, .5);
+ } @else {
+ box-shadow: 0 0 0 $btn-focus-width rgba($border, .5);
+ }
+ }
+
+ // Disabled comes first so active can properly restyle
+ &.disabled,
+ &:disabled {
+ color: color-yiq($background);
+ background-color: $background;
+ border-color: $border;
+ }
+
+ &:not(:disabled):not(.disabled):active,
+ &:not(:disabled):not(.disabled).active,
+ .show > &.dropdown-toggle {
+ color: color-yiq($active-background);
+ background-color: $active-background;
+ @if $enable-gradients {
+ background-image: none; // Remove the gradient for the pressed/active state
+ }
+ border-color: $active-border;
+
+ &:focus {
+ // Avoid using mixin so we can pass custom focus shadow properly
+ @if $enable-shadows {
+ box-shadow: $btn-active-box-shadow, 0 0 0 $btn-focus-width rgba($border, .5);
+ } @else {
+ box-shadow: 0 0 0 $btn-focus-width rgba($border, .5);
+ }
+ }
+ }
+}
+
+@mixin button-outline-variant($color, $color-hover: color-yiq($color), $active-background: $color, $active-border: $color) {
+ color: $color;
+ background-color: transparent;
+ background-image: none;
+ border-color: $color;
+
+ &:hover {
+ color: $color-hover;
+ background-color: $active-background;
+ border-color: $active-border;
+ }
+
+ &:focus,
+ &.focus {
+ box-shadow: 0 0 0 $btn-focus-width rgba($color, .5);
+ }
+
+ &.disabled,
+ &:disabled {
+ color: $color;
+ background-color: transparent;
+ }
+
+ &:not(:disabled):not(.disabled):active,
+ &:not(:disabled):not(.disabled).active,
+ .show > &.dropdown-toggle {
+ color: color-yiq($active-background);
+ background-color: $active-background;
+ border-color: $active-border;
+
+ &:focus {
+ // Avoid using mixin so we can pass custom focus shadow properly
+ @if $enable-shadows and $btn-active-box-shadow != none {
+ box-shadow: $btn-active-box-shadow, 0 0 0 $btn-focus-width rgba($color, .5);
+ } @else {
+ box-shadow: 0 0 0 $btn-focus-width rgba($color, .5);
+ }
+ }
+ }
+}
+
+// Button sizes
+@mixin button-size($padding-y, $padding-x, $font-size, $line-height, $border-radius) {
+ padding: $padding-y $padding-x;
+ font-size: $font-size;
+ line-height: $line-height;
+ // Manually declare to provide an override to the browser default
+ @if $enable-rounded {
+ border-radius: $border-radius;
+ } @else {
+ border-radius: 0;
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_caret.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_caret.scss
new file mode 100755
index 00000000..82aea421
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_caret.scss
@@ -0,0 +1,66 @@
+@mixin caret-down {
+ border-top: $caret-width solid;
+ border-right: $caret-width solid transparent;
+ border-bottom: 0;
+ border-left: $caret-width solid transparent;
+}
+
+@mixin caret-up {
+ border-top: 0;
+ border-right: $caret-width solid transparent;
+ border-bottom: $caret-width solid;
+ border-left: $caret-width solid transparent;
+}
+
+@mixin caret-right {
+ border-top: $caret-width solid transparent;
+ border-right: 0;
+ border-bottom: $caret-width solid transparent;
+ border-left: $caret-width solid;
+}
+
+@mixin caret-left {
+ border-top: $caret-width solid transparent;
+ border-right: $caret-width solid;
+ border-bottom: $caret-width solid transparent;
+}
+
+@mixin caret($direction: down) {
+ @if $enable-caret {
+ &::after {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ margin-left: $caret-width * .85;
+ vertical-align: $caret-width * .85;
+ content: "";
+ @if $direction == down {
+ @include caret-down;
+ } @else if $direction == up {
+ @include caret-up;
+ } @else if $direction == right {
+ @include caret-right;
+ }
+ }
+
+ @if $direction == left {
+ &::after {
+ display: none;
+ }
+
+ &::before {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ margin-right: $caret-width * .85;
+ vertical-align: $caret-width * .85;
+ content: "";
+ @include caret-left;
+ }
+ }
+
+ &:empty::after {
+ margin-left: 0;
+ }
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_clearfix.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_clearfix.scss
new file mode 100755
index 00000000..11a977b7
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_clearfix.scss
@@ -0,0 +1,7 @@
+@mixin clearfix() {
+ &::after {
+ display: block;
+ clear: both;
+ content: "";
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_float.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_float.scss
new file mode 100755
index 00000000..48fa8b6d
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_float.scss
@@ -0,0 +1,11 @@
+// stylelint-disable declaration-no-important
+
+@mixin float-left {
+ float: left !important;
+}
+@mixin float-right {
+ float: right !important;
+}
+@mixin float-none {
+ float: none !important;
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_forms.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_forms.scss
new file mode 100755
index 00000000..3a618786
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_forms.scss
@@ -0,0 +1,147 @@
+// Form control focus state
+//
+// Generate a customized focus state and for any input with the specified color,
+// which defaults to the `$input-focus-border-color` variable.
+//
+// We highly encourage you to not customize the default value, but instead use
+// this to tweak colors on an as-needed basis. This aesthetic change is based on
+// WebKit's default styles, but applicable to a wider range of browsers. Its
+// usability and accessibility should be taken into account with any change.
+//
+// Example usage: change the default blue border and shadow to white for better
+// contrast against a dark gray background.
+@mixin form-control-focus() {
+ &:focus {
+ color: $input-focus-color;
+ background-color: $input-focus-bg;
+ border-color: $input-focus-border-color;
+ outline: 0;
+ // Avoid using mixin so we can pass custom focus shadow properly
+ @if $enable-shadows {
+ box-shadow: $input-box-shadow, $input-focus-box-shadow;
+ } @else {
+ box-shadow: $input-focus-box-shadow;
+ }
+ }
+}
+
+
+@mixin form-validation-state($state, $color) {
+ .#{$state}-feedback {
+ display: none;
+ width: 100%;
+ margin-top: $form-feedback-margin-top;
+ font-size: $form-feedback-font-size;
+ color: $color;
+ }
+
+ .#{$state}-tooltip {
+ position: absolute;
+ top: 100%;
+ z-index: 5;
+ display: none;
+ max-width: 100%; // Contain to parent when possible
+ padding: $tooltip-padding-y $tooltip-padding-x;
+ margin-top: .1rem;
+ font-size: $tooltip-font-size;
+ line-height: $line-height-base;
+ color: color-yiq($color);
+ background-color: rgba($color, $tooltip-opacity);
+ @include border-radius($tooltip-border-radius);
+ }
+
+ .form-control,
+ .custom-select {
+ .was-validated &:#{$state},
+ &.is-#{$state} {
+ border-color: $color;
+
+ &:focus {
+ border-color: $color;
+ box-shadow: 0 0 0 $input-focus-width rgba($color, .25);
+ }
+
+ ~ .#{$state}-feedback,
+ ~ .#{$state}-tooltip {
+ display: block;
+ }
+ }
+ }
+
+ .form-control-file {
+ .was-validated &:#{$state},
+ &.is-#{$state} {
+ ~ .#{$state}-feedback,
+ ~ .#{$state}-tooltip {
+ display: block;
+ }
+ }
+ }
+
+ .form-check-input {
+ .was-validated &:#{$state},
+ &.is-#{$state} {
+ ~ .form-check-label {
+ color: $color;
+ }
+
+ ~ .#{$state}-feedback,
+ ~ .#{$state}-tooltip {
+ display: block;
+ }
+ }
+ }
+
+ .custom-control-input {
+ .was-validated &:#{$state},
+ &.is-#{$state} {
+ ~ .custom-control-label {
+ color: $color;
+
+ &::before {
+ background-color: lighten($color, 25%);
+ }
+ }
+
+ ~ .#{$state}-feedback,
+ ~ .#{$state}-tooltip {
+ display: block;
+ }
+
+ &:checked {
+ ~ .custom-control-label::before {
+ @include gradient-bg(lighten($color, 10%));
+ }
+ }
+
+ &:focus {
+ ~ .custom-control-label::before {
+ box-shadow: 0 0 0 1px $body-bg, 0 0 0 $input-focus-width rgba($color, .25);
+ }
+ }
+ }
+ }
+
+ // custom file
+ .custom-file-input {
+ .was-validated &:#{$state},
+ &.is-#{$state} {
+ ~ .custom-file-label {
+ border-color: $color;
+
+ &::after { border-color: inherit; }
+ }
+
+ ~ .#{$state}-feedback,
+ ~ .#{$state}-tooltip {
+ display: block;
+ }
+
+ &:focus {
+ ~ .custom-file-label {
+ box-shadow: 0 0 0 $input-focus-width rgba($color, .25);
+ }
+ }
+ }
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_gradients.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_gradients.scss
new file mode 100755
index 00000000..88c4d64b
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_gradients.scss
@@ -0,0 +1,45 @@
+// Gradients
+
+@mixin gradient-bg($color) {
+ @if $enable-gradients {
+ background: $color linear-gradient(180deg, mix($body-bg, $color, 15%), $color) repeat-x;
+ } @else {
+ background-color: $color;
+ }
+}
+
+// Horizontal gradient, from left to right
+//
+// Creates two color stops, start and end, by specifying a color and position for each color stop.
+@mixin gradient-x($start-color: $gray-700, $end-color: $gray-800, $start-percent: 0%, $end-percent: 100%) {
+ background-image: linear-gradient(to right, $start-color $start-percent, $end-color $end-percent);
+ background-repeat: repeat-x;
+}
+
+// Vertical gradient, from top to bottom
+//
+// Creates two color stops, start and end, by specifying a color and position for each color stop.
+@mixin gradient-y($start-color: $gray-700, $end-color: $gray-800, $start-percent: 0%, $end-percent: 100%) {
+ background-image: linear-gradient(to bottom, $start-color $start-percent, $end-color $end-percent);
+ background-repeat: repeat-x;
+}
+
+@mixin gradient-directional($start-color: $gray-700, $end-color: $gray-800, $deg: 45deg) {
+ background-image: linear-gradient($deg, $start-color, $end-color);
+ background-repeat: repeat-x;
+}
+@mixin gradient-x-three-colors($start-color: $blue, $mid-color: $purple, $color-stop: 50%, $end-color: $red) {
+ background-image: linear-gradient(to right, $start-color, $mid-color $color-stop, $end-color);
+ background-repeat: no-repeat;
+}
+@mixin gradient-y-three-colors($start-color: $blue, $mid-color: $purple, $color-stop: 50%, $end-color: $red) {
+ background-image: linear-gradient($start-color, $mid-color $color-stop, $end-color);
+ background-repeat: no-repeat;
+}
+@mixin gradient-radial($inner-color: $gray-700, $outer-color: $gray-800) {
+ background-image: radial-gradient(circle, $inner-color, $outer-color);
+ background-repeat: no-repeat;
+}
+@mixin gradient-striped($color: rgba($white, .15), $angle: 45deg) {
+ background-image: linear-gradient($angle, $color 25%, transparent 25%, transparent 50%, $color 50%, $color 75%, transparent 75%, transparent);
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_grid-framework.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_grid-framework.scss
new file mode 100755
index 00000000..7b37f868
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_grid-framework.scss
@@ -0,0 +1,67 @@
+// Framework grid generation
+//
+// Used only by Bootstrap to generate the correct number of grid classes given
+// any value of `$grid-columns`.
+
+@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {
+ // Common properties for all breakpoints
+ %grid-column {
+ position: relative;
+ width: 100%;
+ min-height: 1px; // Prevent columns from collapsing when empty
+ padding-right: ($gutter / 2);
+ padding-left: ($gutter / 2);
+ }
+
+ @each $breakpoint in map-keys($breakpoints) {
+ $infix: breakpoint-infix($breakpoint, $breakpoints);
+
+ // Allow columns to stretch full width below their breakpoints
+ @for $i from 1 through $columns {
+ .col#{$infix}-#{$i} {
+ @extend %grid-column;
+ }
+ }
+ .col#{$infix},
+ .col#{$infix}-auto {
+ @extend %grid-column;
+ }
+
+ @include media-breakpoint-up($breakpoint, $breakpoints) {
+ // Provide basic `.col-{bp}` classes for equal-width flexbox columns
+ .col#{$infix} {
+ flex-basis: 0;
+ flex-grow: 1;
+ max-width: 100%;
+ }
+ .col#{$infix}-auto {
+ flex: 0 0 auto;
+ width: auto;
+ max-width: none; // Reset earlier grid tiers
+ }
+
+ @for $i from 1 through $columns {
+ .col#{$infix}-#{$i} {
+ @include make-col($i, $columns);
+ }
+ }
+
+ .order#{$infix}-first { order: -1; }
+
+ .order#{$infix}-last { order: $columns + 1; }
+
+ @for $i from 0 through $columns {
+ .order#{$infix}-#{$i} { order: $i; }
+ }
+
+ // `$columns - 1` because offsetting by the width of an entire row isn't possible
+ @for $i from 0 through ($columns - 1) {
+ @if not ($infix == "" and $i == 0) { // Avoid emitting useless .offset-0
+ .offset#{$infix}-#{$i} {
+ @include make-col-offset($i, $columns);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_grid.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_grid.scss
new file mode 100755
index 00000000..b75ebcbc
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_grid.scss
@@ -0,0 +1,52 @@
+/// Grid system
+//
+// Generate semantic grid columns with these mixins.
+
+@mixin make-container() {
+ width: 100%;
+ padding-right: ($grid-gutter-width / 2);
+ padding-left: ($grid-gutter-width / 2);
+ margin-right: auto;
+ margin-left: auto;
+}
+
+
+// For each breakpoint, define the maximum width of the container in a media query
+@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {
+ @each $breakpoint, $container-max-width in $max-widths {
+ @include media-breakpoint-up($breakpoint, $breakpoints) {
+ max-width: $container-max-width;
+ }
+ }
+}
+
+@mixin make-row() {
+ display: flex;
+ flex-wrap: wrap;
+ margin-right: ($grid-gutter-width / -2);
+ margin-left: ($grid-gutter-width / -2);
+}
+
+@mixin make-col-ready() {
+ position: relative;
+ // Prevent columns from becoming too narrow when at smaller grid tiers by
+ // always setting `width: 100%;`. This works because we use `flex` values
+ // later on to override this initial width.
+ width: 100%;
+ min-height: 1px; // Prevent collapsing
+ padding-right: ($grid-gutter-width / 2);
+ padding-left: ($grid-gutter-width / 2);
+}
+
+@mixin make-col($size, $columns: $grid-columns) {
+ flex: 0 0 percentage($size / $columns);
+ // Add a `max-width` to ensure content within each column does not blow out
+ // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari
+ // do not appear to require this.
+ max-width: percentage($size / $columns);
+}
+
+@mixin make-col-offset($size, $columns: $grid-columns) {
+ $num: $size / $columns;
+ margin-left: if($num == 0, 0, percentage($num));
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_hover.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_hover.scss
new file mode 100755
index 00000000..192f847e
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_hover.scss
@@ -0,0 +1,37 @@
+// Hover mixin and `$enable-hover-media-query` are deprecated.
+//
+// Originally added during our alphas and maintained during betas, this mixin was
+// designed to prevent `:hover` stickiness on iOS-an issue where hover styles
+// would persist after initial touch.
+//
+// For backward compatibility, we've kept these mixins and updated them to
+// always return their regular pseudo-classes instead of a shimmed media query.
+//
+// Issue: https://github.com/twbs/bootstrap/issues/25195
+
+@mixin hover {
+ &:hover { @content; }
+}
+
+@mixin hover-focus {
+ &:hover,
+ &:focus {
+ @content;
+ }
+}
+
+@mixin plain-hover-focus {
+ &,
+ &:hover,
+ &:focus {
+ @content;
+ }
+}
+
+@mixin hover-focus-active {
+ &:hover,
+ &:focus,
+ &:active {
+ @content;
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_image.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_image.scss
new file mode 100755
index 00000000..0544f0d2
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_image.scss
@@ -0,0 +1,36 @@
+// Image Mixins
+// - Responsive image
+// - Retina image
+
+
+// Responsive image
+//
+// Keep images from scaling beyond the width of their parents.
+
+@mixin img-fluid {
+ // Part 1: Set a maximum relative to the parent
+ max-width: 100%;
+ // Part 2: Override the height to auto, otherwise images will be stretched
+ // when setting a width and height attribute on the img element.
+ height: auto;
+}
+
+
+// Retina image
+//
+// Short retina mixin for setting background-image and -size.
+
+// stylelint-disable indentation, media-query-list-comma-newline-after
+@mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) {
+ background-image: url($file-1x);
+
+ // Autoprefixer takes care of adding -webkit-min-device-pixel-ratio and -o-min-device-pixel-ratio,
+ // but doesn't convert dppx=>dpi.
+ // There's no such thing as unprefixed min-device-pixel-ratio since it's nonstandard.
+ // Compatibility info: https://caniuse.com/#feat=css-media-resolution
+ @media only screen and (min-resolution: 192dpi), // IE9-11 don't support dppx
+ only screen and (min-resolution: 2dppx) { // Standardized
+ background-image: url($file-2x);
+ background-size: $width-1x $height-1x;
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_list-group.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_list-group.scss
new file mode 100755
index 00000000..cd47a4e9
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_list-group.scss
@@ -0,0 +1,21 @@
+// List Groups
+
+@mixin list-group-item-variant($state, $background, $color) {
+ .list-group-item-#{$state} {
+ color: $color;
+ background-color: $background;
+
+ &.list-group-item-action {
+ @include hover-focus {
+ color: $color;
+ background-color: darken($background, 5%);
+ }
+
+ &.active {
+ color: $white;
+ background-color: $color;
+ border-color: $color;
+ }
+ }
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_lists.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_lists.scss
new file mode 100755
index 00000000..25185626
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_lists.scss
@@ -0,0 +1,7 @@
+// Lists
+
+// Unstyled keeps list items block level, just removes default browser padding and list-style
+@mixin list-unstyled {
+ padding-left: 0;
+ list-style: none;
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_nav-divider.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_nav-divider.scss
new file mode 100755
index 00000000..4fb37b62
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_nav-divider.scss
@@ -0,0 +1,10 @@
+// Horizontal dividers
+//
+// Dividers (basically an hr) within dropdowns and nav lists
+
+@mixin nav-divider($color: $nav-divider-color, $margin-y: $nav-divider-margin-y) {
+ height: 0;
+ margin: $margin-y 0;
+ overflow: hidden;
+ border-top: 1px solid $color;
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_pagination.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_pagination.scss
new file mode 100755
index 00000000..ff36eb6b
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_pagination.scss
@@ -0,0 +1,22 @@
+// Pagination
+
+@mixin pagination-size($padding-y, $padding-x, $font-size, $line-height, $border-radius) {
+ .page-link {
+ padding: $padding-y $padding-x;
+ font-size: $font-size;
+ line-height: $line-height;
+ }
+
+ .page-item {
+ &:first-child {
+ .page-link {
+ @include border-left-radius($border-radius);
+ }
+ }
+ &:last-child {
+ .page-link {
+ @include border-right-radius($border-radius);
+ }
+ }
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_reset-text.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_reset-text.scss
new file mode 100755
index 00000000..71edb006
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_reset-text.scss
@@ -0,0 +1,17 @@
+@mixin reset-text {
+ font-family: $font-family-base;
+ // We deliberately do NOT reset font-size or word-wrap.
+ font-style: normal;
+ font-weight: $font-weight-normal;
+ line-height: $line-height-base;
+ text-align: left; // Fallback for where `start` is not supported
+ text-align: start; // stylelint-disable-line declaration-block-no-duplicate-properties
+ text-decoration: none;
+ text-shadow: none;
+ text-transform: none;
+ letter-spacing: normal;
+ word-break: normal;
+ word-spacing: normal;
+ white-space: normal;
+ line-break: auto;
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_resize.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_resize.scss
new file mode 100755
index 00000000..66f233a6
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_resize.scss
@@ -0,0 +1,6 @@
+// Resize anything
+
+@mixin resizable($direction) {
+ overflow: auto; // Per CSS3 UI, `resize` only applies when `overflow` isn't `visible`
+ resize: $direction; // Options: horizontal, vertical, both
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_screen-reader.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_screen-reader.scss
new file mode 100755
index 00000000..812591bc
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_screen-reader.scss
@@ -0,0 +1,33 @@
+// Only display content to screen readers
+//
+// See: https://a11yproject.com/posts/how-to-hide-content/
+// See: https://hugogiraudel.com/2016/10/13/css-hide-and-seek/
+
+@mixin sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+// Use in conjunction with .sr-only to only display content when it's focused.
+//
+// Useful for "Skip to main content" links; see https://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
+//
+// Credit: HTML5 Boilerplate
+
+@mixin sr-only-focusable {
+ &:active,
+ &:focus {
+ position: static;
+ width: auto;
+ height: auto;
+ overflow: visible;
+ clip: auto;
+ white-space: normal;
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_size.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_size.scss
new file mode 100755
index 00000000..b9dd48e8
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_size.scss
@@ -0,0 +1,6 @@
+// Sizing shortcuts
+
+@mixin size($width, $height: $width) {
+ width: $width;
+ height: $height;
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_table-row.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_table-row.scss
new file mode 100755
index 00000000..84f1d305
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_table-row.scss
@@ -0,0 +1,30 @@
+// Tables
+
+@mixin table-row-variant($state, $background) {
+ // Exact selectors below required to override `.table-striped` and prevent
+ // inheritance to nested tables.
+ .table-#{$state} {
+ &,
+ > th,
+ > td {
+ background-color: $background;
+ }
+ }
+
+ // Hover states for `.table-hover`
+ // Note: this is not available for cells or rows within `thead` or `tfoot`.
+ .table-hover {
+ $hover-background: darken($background, 5%);
+
+ .table-#{$state} {
+ @include hover {
+ background-color: $hover-background;
+
+ > td,
+ > th {
+ background-color: $hover-background;
+ }
+ }
+ }
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_text-emphasis.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_text-emphasis.scss
new file mode 100755
index 00000000..58db3e0f
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_text-emphasis.scss
@@ -0,0 +1,14 @@
+// stylelint-disable declaration-no-important
+
+// Typography
+
+@mixin text-emphasis-variant($parent, $color) {
+ #{$parent} {
+ color: $color !important;
+ }
+ a#{$parent} {
+ @include hover-focus {
+ color: darken($color, 10%) !important;
+ }
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_text-hide.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_text-hide.scss
new file mode 100755
index 00000000..9ffab169
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_text-hide.scss
@@ -0,0 +1,13 @@
+// CSS image replacement
+@mixin text-hide($ignore-warning: false) {
+ // stylelint-disable-next-line font-family-no-missing-generic-family-keyword
+ font: 0/0 a;
+ color: transparent;
+ text-shadow: none;
+ background-color: transparent;
+ border: 0;
+
+ @if ($ignore-warning != true) {
+ @warn "The `text-hide()` mixin has been deprecated as of v4.1.0. It will be removed entirely in v5.";
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_text-truncate.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_text-truncate.scss
new file mode 100755
index 00000000..3504bb1a
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_text-truncate.scss
@@ -0,0 +1,8 @@
+// Text truncate
+// Requires inline-block or block for proper styling
+
+@mixin text-truncate() {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_transition.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_transition.scss
new file mode 100755
index 00000000..f8538213
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_transition.scss
@@ -0,0 +1,13 @@
+@mixin transition($transition...) {
+ @if $enable-transitions {
+ @if length($transition) == 0 {
+ transition: $transition-base;
+ } @else {
+ transition: $transition;
+ }
+ }
+
+ @media screen and (prefers-reduced-motion: reduce) {
+ transition: none;
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/mixins/_visibility.scss b/docs/assets/_scss/bootstrap-4.1.3/mixins/_visibility.scss
new file mode 100755
index 00000000..fe523d0e
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/mixins/_visibility.scss
@@ -0,0 +1,7 @@
+// stylelint-disable declaration-no-important
+
+// Visibility
+
+@mixin invisible($visibility) {
+ visibility: $visibility !important;
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/utilities/_align.scss b/docs/assets/_scss/bootstrap-4.1.3/utilities/_align.scss
new file mode 100755
index 00000000..8b7df9f7
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/utilities/_align.scss
@@ -0,0 +1,8 @@
+// stylelint-disable declaration-no-important
+
+.align-baseline { vertical-align: baseline !important; } // Browser default
+.align-top { vertical-align: top !important; }
+.align-middle { vertical-align: middle !important; }
+.align-bottom { vertical-align: bottom !important; }
+.align-text-bottom { vertical-align: text-bottom !important; }
+.align-text-top { vertical-align: text-top !important; }
diff --git a/docs/assets/_scss/bootstrap-4.1.3/utilities/_background.scss b/docs/assets/_scss/bootstrap-4.1.3/utilities/_background.scss
new file mode 100755
index 00000000..1f18b2f3
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/utilities/_background.scss
@@ -0,0 +1,19 @@
+// stylelint-disable declaration-no-important
+
+@each $color, $value in $theme-colors {
+ @include bg-variant(".bg-#{$color}", $value);
+}
+
+@if $enable-gradients {
+ @each $color, $value in $theme-colors {
+ @include bg-gradient-variant(".bg-gradient-#{$color}", $value);
+ }
+}
+
+.bg-white {
+ background-color: $white !important;
+}
+
+.bg-transparent {
+ background-color: transparent !important;
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/utilities/_borders.scss b/docs/assets/_scss/bootstrap-4.1.3/utilities/_borders.scss
new file mode 100755
index 00000000..b8832ef7
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/utilities/_borders.scss
@@ -0,0 +1,59 @@
+// stylelint-disable declaration-no-important
+
+//
+// Border
+//
+
+.border { border: $border-width solid $border-color !important; }
+.border-top { border-top: $border-width solid $border-color !important; }
+.border-right { border-right: $border-width solid $border-color !important; }
+.border-bottom { border-bottom: $border-width solid $border-color !important; }
+.border-left { border-left: $border-width solid $border-color !important; }
+
+.border-0 { border: 0 !important; }
+.border-top-0 { border-top: 0 !important; }
+.border-right-0 { border-right: 0 !important; }
+.border-bottom-0 { border-bottom: 0 !important; }
+.border-left-0 { border-left: 0 !important; }
+
+@each $color, $value in $theme-colors {
+ .border-#{$color} {
+ border-color: $value !important;
+ }
+}
+
+.border-white {
+ border-color: $white !important;
+}
+
+//
+// Border-radius
+//
+
+.rounded {
+ border-radius: $border-radius !important;
+}
+.rounded-top {
+ border-top-left-radius: $border-radius !important;
+ border-top-right-radius: $border-radius !important;
+}
+.rounded-right {
+ border-top-right-radius: $border-radius !important;
+ border-bottom-right-radius: $border-radius !important;
+}
+.rounded-bottom {
+ border-bottom-right-radius: $border-radius !important;
+ border-bottom-left-radius: $border-radius !important;
+}
+.rounded-left {
+ border-top-left-radius: $border-radius !important;
+ border-bottom-left-radius: $border-radius !important;
+}
+
+.rounded-circle {
+ border-radius: 50% !important;
+}
+
+.rounded-0 {
+ border-radius: 0 !important;
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/utilities/_clearfix.scss b/docs/assets/_scss/bootstrap-4.1.3/utilities/_clearfix.scss
new file mode 100755
index 00000000..e92522a9
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/utilities/_clearfix.scss
@@ -0,0 +1,3 @@
+.clearfix {
+ @include clearfix();
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/utilities/_display.scss b/docs/assets/_scss/bootstrap-4.1.3/utilities/_display.scss
new file mode 100755
index 00000000..20aeeb5f
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/utilities/_display.scss
@@ -0,0 +1,38 @@
+// stylelint-disable declaration-no-important
+
+//
+// Utilities for common `display` values
+//
+
+@each $breakpoint in map-keys($grid-breakpoints) {
+ @include media-breakpoint-up($breakpoint) {
+ $infix: breakpoint-infix($breakpoint, $grid-breakpoints);
+
+ .d#{$infix}-none { display: none !important; }
+ .d#{$infix}-inline { display: inline !important; }
+ .d#{$infix}-inline-block { display: inline-block !important; }
+ .d#{$infix}-block { display: block !important; }
+ .d#{$infix}-table { display: table !important; }
+ .d#{$infix}-table-row { display: table-row !important; }
+ .d#{$infix}-table-cell { display: table-cell !important; }
+ .d#{$infix}-flex { display: flex !important; }
+ .d#{$infix}-inline-flex { display: inline-flex !important; }
+ }
+}
+
+
+//
+// Utilities for toggling `display` in print
+//
+
+@media print {
+ .d-print-none { display: none !important; }
+ .d-print-inline { display: inline !important; }
+ .d-print-inline-block { display: inline-block !important; }
+ .d-print-block { display: block !important; }
+ .d-print-table { display: table !important; }
+ .d-print-table-row { display: table-row !important; }
+ .d-print-table-cell { display: table-cell !important; }
+ .d-print-flex { display: flex !important; }
+ .d-print-inline-flex { display: inline-flex !important; }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/utilities/_embed.scss b/docs/assets/_scss/bootstrap-4.1.3/utilities/_embed.scss
new file mode 100755
index 00000000..d3362b6f
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/utilities/_embed.scss
@@ -0,0 +1,52 @@
+// Credit: Nicolas Gallagher and SUIT CSS.
+
+.embed-responsive {
+ position: relative;
+ display: block;
+ width: 100%;
+ padding: 0;
+ overflow: hidden;
+
+ &::before {
+ display: block;
+ content: "";
+ }
+
+ .embed-responsive-item,
+ iframe,
+ embed,
+ object,
+ video {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border: 0;
+ }
+}
+
+.embed-responsive-21by9 {
+ &::before {
+ padding-top: percentage(9 / 21);
+ }
+}
+
+.embed-responsive-16by9 {
+ &::before {
+ padding-top: percentage(9 / 16);
+ }
+}
+
+.embed-responsive-4by3 {
+ &::before {
+ padding-top: percentage(3 / 4);
+ }
+}
+
+.embed-responsive-1by1 {
+ &::before {
+ padding-top: percentage(1 / 1);
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/utilities/_flex.scss b/docs/assets/_scss/bootstrap-4.1.3/utilities/_flex.scss
new file mode 100755
index 00000000..3d4266e0
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/utilities/_flex.scss
@@ -0,0 +1,51 @@
+// stylelint-disable declaration-no-important
+
+// Flex variation
+//
+// Custom styles for additional flex alignment options.
+
+@each $breakpoint in map-keys($grid-breakpoints) {
+ @include media-breakpoint-up($breakpoint) {
+ $infix: breakpoint-infix($breakpoint, $grid-breakpoints);
+
+ .flex#{$infix}-row { flex-direction: row !important; }
+ .flex#{$infix}-column { flex-direction: column !important; }
+ .flex#{$infix}-row-reverse { flex-direction: row-reverse !important; }
+ .flex#{$infix}-column-reverse { flex-direction: column-reverse !important; }
+
+ .flex#{$infix}-wrap { flex-wrap: wrap !important; }
+ .flex#{$infix}-nowrap { flex-wrap: nowrap !important; }
+ .flex#{$infix}-wrap-reverse { flex-wrap: wrap-reverse !important; }
+ .flex#{$infix}-fill { flex: 1 1 auto !important; }
+ .flex#{$infix}-grow-0 { flex-grow: 0 !important; }
+ .flex#{$infix}-grow-1 { flex-grow: 1 !important; }
+ .flex#{$infix}-shrink-0 { flex-shrink: 0 !important; }
+ .flex#{$infix}-shrink-1 { flex-shrink: 1 !important; }
+
+ .justify-content#{$infix}-start { justify-content: flex-start !important; }
+ .justify-content#{$infix}-end { justify-content: flex-end !important; }
+ .justify-content#{$infix}-center { justify-content: center !important; }
+ .justify-content#{$infix}-between { justify-content: space-between !important; }
+ .justify-content#{$infix}-around { justify-content: space-around !important; }
+
+ .align-items#{$infix}-start { align-items: flex-start !important; }
+ .align-items#{$infix}-end { align-items: flex-end !important; }
+ .align-items#{$infix}-center { align-items: center !important; }
+ .align-items#{$infix}-baseline { align-items: baseline !important; }
+ .align-items#{$infix}-stretch { align-items: stretch !important; }
+
+ .align-content#{$infix}-start { align-content: flex-start !important; }
+ .align-content#{$infix}-end { align-content: flex-end !important; }
+ .align-content#{$infix}-center { align-content: center !important; }
+ .align-content#{$infix}-between { align-content: space-between !important; }
+ .align-content#{$infix}-around { align-content: space-around !important; }
+ .align-content#{$infix}-stretch { align-content: stretch !important; }
+
+ .align-self#{$infix}-auto { align-self: auto !important; }
+ .align-self#{$infix}-start { align-self: flex-start !important; }
+ .align-self#{$infix}-end { align-self: flex-end !important; }
+ .align-self#{$infix}-center { align-self: center !important; }
+ .align-self#{$infix}-baseline { align-self: baseline !important; }
+ .align-self#{$infix}-stretch { align-self: stretch !important; }
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/utilities/_float.scss b/docs/assets/_scss/bootstrap-4.1.3/utilities/_float.scss
new file mode 100755
index 00000000..01655e9a
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/utilities/_float.scss
@@ -0,0 +1,9 @@
+@each $breakpoint in map-keys($grid-breakpoints) {
+ @include media-breakpoint-up($breakpoint) {
+ $infix: breakpoint-infix($breakpoint, $grid-breakpoints);
+
+ .float#{$infix}-left { @include float-left; }
+ .float#{$infix}-right { @include float-right; }
+ .float#{$infix}-none { @include float-none; }
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/utilities/_position.scss b/docs/assets/_scss/bootstrap-4.1.3/utilities/_position.scss
new file mode 100755
index 00000000..9ecdeeb9
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/utilities/_position.scss
@@ -0,0 +1,37 @@
+// stylelint-disable declaration-no-important
+
+// Common values
+
+// Sass list not in variables since it's not intended for customization.
+// stylelint-disable-next-line scss/dollar-variable-default
+$positions: static, relative, absolute, fixed, sticky;
+
+@each $position in $positions {
+ .position-#{$position} { position: $position !important; }
+}
+
+// Shorthand
+
+.fixed-top {
+ position: fixed;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: $zindex-fixed;
+}
+
+.fixed-bottom {
+ position: fixed;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: $zindex-fixed;
+}
+
+.sticky-top {
+ @supports (position: sticky) {
+ position: sticky;
+ top: 0;
+ z-index: $zindex-sticky;
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/utilities/_screenreaders.scss b/docs/assets/_scss/bootstrap-4.1.3/utilities/_screenreaders.scss
new file mode 100755
index 00000000..9f26fde0
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/utilities/_screenreaders.scss
@@ -0,0 +1,11 @@
+//
+// Screenreaders
+//
+
+.sr-only {
+ @include sr-only();
+}
+
+.sr-only-focusable {
+ @include sr-only-focusable();
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/utilities/_shadows.scss b/docs/assets/_scss/bootstrap-4.1.3/utilities/_shadows.scss
new file mode 100755
index 00000000..f5d03fcd
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/utilities/_shadows.scss
@@ -0,0 +1,6 @@
+// stylelint-disable declaration-no-important
+
+.shadow-sm { box-shadow: $box-shadow-sm !important; }
+.shadow { box-shadow: $box-shadow !important; }
+.shadow-lg { box-shadow: $box-shadow-lg !important; }
+.shadow-none { box-shadow: none !important; }
diff --git a/docs/assets/_scss/bootstrap-4.1.3/utilities/_sizing.scss b/docs/assets/_scss/bootstrap-4.1.3/utilities/_sizing.scss
new file mode 100755
index 00000000..e95a4db3
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/utilities/_sizing.scss
@@ -0,0 +1,12 @@
+// stylelint-disable declaration-no-important
+
+// Width and height
+
+@each $prop, $abbrev in (width: w, height: h) {
+ @each $size, $length in $sizes {
+ .#{$abbrev}-#{$size} { #{$prop}: $length !important; }
+ }
+}
+
+.mw-100 { max-width: 100% !important; }
+.mh-100 { max-height: 100% !important; }
diff --git a/docs/assets/_scss/bootstrap-4.1.3/utilities/_spacing.scss b/docs/assets/_scss/bootstrap-4.1.3/utilities/_spacing.scss
new file mode 100755
index 00000000..b2e2354b
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/utilities/_spacing.scss
@@ -0,0 +1,51 @@
+// stylelint-disable declaration-no-important
+
+// Margin and Padding
+
+@each $breakpoint in map-keys($grid-breakpoints) {
+ @include media-breakpoint-up($breakpoint) {
+ $infix: breakpoint-infix($breakpoint, $grid-breakpoints);
+
+ @each $prop, $abbrev in (margin: m, padding: p) {
+ @each $size, $length in $spacers {
+
+ .#{$abbrev}#{$infix}-#{$size} { #{$prop}: $length !important; }
+ .#{$abbrev}t#{$infix}-#{$size},
+ .#{$abbrev}y#{$infix}-#{$size} {
+ #{$prop}-top: $length !important;
+ }
+ .#{$abbrev}r#{$infix}-#{$size},
+ .#{$abbrev}x#{$infix}-#{$size} {
+ #{$prop}-right: $length !important;
+ }
+ .#{$abbrev}b#{$infix}-#{$size},
+ .#{$abbrev}y#{$infix}-#{$size} {
+ #{$prop}-bottom: $length !important;
+ }
+ .#{$abbrev}l#{$infix}-#{$size},
+ .#{$abbrev}x#{$infix}-#{$size} {
+ #{$prop}-left: $length !important;
+ }
+ }
+ }
+
+ // Some special margin utils
+ .m#{$infix}-auto { margin: auto !important; }
+ .mt#{$infix}-auto,
+ .my#{$infix}-auto {
+ margin-top: auto !important;
+ }
+ .mr#{$infix}-auto,
+ .mx#{$infix}-auto {
+ margin-right: auto !important;
+ }
+ .mb#{$infix}-auto,
+ .my#{$infix}-auto {
+ margin-bottom: auto !important;
+ }
+ .ml#{$infix}-auto,
+ .mx#{$infix}-auto {
+ margin-left: auto !important;
+ }
+ }
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/utilities/_text.scss b/docs/assets/_scss/bootstrap-4.1.3/utilities/_text.scss
new file mode 100755
index 00000000..9d96c465
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/utilities/_text.scss
@@ -0,0 +1,58 @@
+// stylelint-disable declaration-no-important
+
+//
+// Text
+//
+
+.text-monospace { font-family: $font-family-monospace; }
+
+// Alignment
+
+.text-justify { text-align: justify !important; }
+.text-nowrap { white-space: nowrap !important; }
+.text-truncate { @include text-truncate; }
+
+// Responsive alignment
+
+@each $breakpoint in map-keys($grid-breakpoints) {
+ @include media-breakpoint-up($breakpoint) {
+ $infix: breakpoint-infix($breakpoint, $grid-breakpoints);
+
+ .text#{$infix}-left { text-align: left !important; }
+ .text#{$infix}-right { text-align: right !important; }
+ .text#{$infix}-center { text-align: center !important; }
+ }
+}
+
+// Transformation
+
+.text-lowercase { text-transform: lowercase !important; }
+.text-uppercase { text-transform: uppercase !important; }
+.text-capitalize { text-transform: capitalize !important; }
+
+// Weight and italics
+
+.font-weight-light { font-weight: $font-weight-light !important; }
+.font-weight-normal { font-weight: $font-weight-normal !important; }
+.font-weight-bold { font-weight: $font-weight-bold !important; }
+.font-italic { font-style: italic !important; }
+
+// Contextual colors
+
+.text-white { color: $white !important; }
+
+@each $color, $value in $theme-colors {
+ @include text-emphasis-variant(".text-#{$color}", $value);
+}
+
+.text-body { color: $body-color !important; }
+.text-muted { color: $text-muted !important; }
+
+.text-black-50 { color: rgba($black, .5) !important; }
+.text-white-50 { color: rgba($white, .5) !important; }
+
+// Misc
+
+.text-hide {
+ @include text-hide($ignore-warning: true);
+}
diff --git a/docs/assets/_scss/bootstrap-4.1.3/utilities/_visibility.scss b/docs/assets/_scss/bootstrap-4.1.3/utilities/_visibility.scss
new file mode 100755
index 00000000..823406dc
--- /dev/null
+++ b/docs/assets/_scss/bootstrap-4.1.3/utilities/_visibility.scss
@@ -0,0 +1,11 @@
+//
+// Visibility utilities
+//
+
+.visible {
+ @include invisible(visible);
+}
+
+.invisible {
+ @include invisible(hidden);
+}
diff --git a/docs/assets/_scss/site/common/_core.scss b/docs/assets/_scss/site/common/_core.scss
new file mode 100644
index 00000000..c7c04d01
--- /dev/null
+++ b/docs/assets/_scss/site/common/_core.scss
@@ -0,0 +1,13 @@
+body {
+ -webkit-font-smoothing: antialiased;
+}
+
+
+a {
+ &.dark {
+ color: $body-color !important;
+ }
+ &.light {
+ color: $white !important;
+ }
+}
\ No newline at end of file
diff --git a/docs/assets/_scss/site/common/_fonts.scss b/docs/assets/_scss/site/common/_fonts.scss
new file mode 100644
index 00000000..db9bc821
--- /dev/null
+++ b/docs/assets/_scss/site/common/_fonts.scss
@@ -0,0 +1,45 @@
+// Metropolis
+
+@font-face {
+ font-family: "Metropolis";
+ src: local("Metropolis Regular"), local("Metropolis-Regular"),
+ url("#{$path-fonts}/Metropolis/Metropolis-Regular.woff") format("woff");
+ font-weight: $font-weight-normal;
+}
+
+@font-face {
+ font-family: "Metropolis";
+ src: local("Metropolis Light"), local("Metropolis-Light"),
+ url("#{$path-fonts}/Metropolis/Metropolis-Light.woff") format("woff");
+ font-weight: $font-weight-light;
+}
+
+@font-face {
+ font-family: "Metropolis";
+ src: local("Metropolis Medium"), local("Metropolis-Medium"),
+ url("#{$path-fonts}/Metropolis/Metropolis-Medium.woff") format("woff");
+ font-weight: $font-weight-medium;
+}
+
+@font-face {
+ font-family: "Metropolis";
+ src: local("Metropolis SemiBold"), local("Metropolis-SemiBold"),
+ url("#{$path-fonts}/Metropolis/Metropolis-SemiBold.woff") format("woff");
+ font-weight: $font-weight-semibold;
+}
+
+@font-face {
+ font-family: "Metropolis";
+ src: local("Metropolis Bold"), local("Metropolis-Bold"),
+ url("#{$path-fonts}/Metropolis/Metropolis-Bold.woff") format("woff");
+ font-weight: $font-weight-bold;
+}
+
+// IcoMoon
+
+@font-face {
+ font-family: "IcoMoon-Free";
+ src: url("#{$path-fonts}/IcoMoon/IcoMoon-Free.ttf") format("truetype");
+ font-weight: normal;
+ font-style: normal;
+}
diff --git a/docs/assets/_scss/site/common/_type.scss b/docs/assets/_scss/site/common/_type.scss
new file mode 100644
index 00000000..d6e653db
--- /dev/null
+++ b/docs/assets/_scss/site/common/_type.scss
@@ -0,0 +1,61 @@
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-weight: $font-weight-medium;
+}
+
+h1,
+h5,
+h6 {
+ font-weight: $font-weight-semibold;
+}
+
+h1 {
+ font-size: 2.875rem;
+ color: $body-color-darkest;
+
+ @include media-breakpoint-down(sm) {
+ font-size: 1.625rem;
+ }
+}
+
+h2 {
+ color: $body-color-darkest;
+ margin-bottom: 1rem;
+ margin-top: 2rem;
+}
+
+h3 {
+ margin-top: 2rem;
+}
+
+h5 {
+ font-size: 1.375rem;
+ margin-bottom: 1.25rem;
+}
+
+strong {
+ font-weight: $font-weight-semibold;
+}
+pre {
+ display: block;
+ font-size: $code-font-size;
+ color: $pre-color;
+ background-color: #f2f2f2;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ padding-left: 5px;
+ padding-right: 5px;
+
+
+ // Account for some code outputs that place code tags in pre tags
+ code {
+ font-size: inherit;
+ color: inherit;
+ word-break: normal;
+ }
+}
diff --git a/docs/assets/_scss/site/layouts/_container.scss b/docs/assets/_scss/site/layouts/_container.scss
new file mode 100644
index 00000000..cb0f9c42
--- /dev/null
+++ b/docs/assets/_scss/site/layouts/_container.scss
@@ -0,0 +1,9 @@
+.site-outer-container {
+ max-width: $container-max-width;
+ padding-left: 0;
+ padding-right: 0;
+}
+
+.site-container {
+ background: $container-background;
+}
diff --git a/docs/assets/_scss/site/layouts/_documentation.scss b/docs/assets/_scss/site/layouts/_documentation.scss
new file mode 100644
index 00000000..237e7de6
--- /dev/null
+++ b/docs/assets/_scss/site/layouts/_documentation.scss
@@ -0,0 +1,199 @@
+#docs {
+ .container.container-max {
+ max-width: 90%;
+ }
+
+ .section-card {
+ margin-bottom: $section-padding-y;
+ }
+
+ table {
+ @extend .table;
+ @extend .table-striped;
+ @extend .table-bordered;
+ thead {
+ @extend .thead-light;
+ }
+ td {
+ code {
+ word-break: keep-all;
+ }
+ }
+ }
+
+ h2, h3, p {
+ width: 100%;
+ margin-bottom: 0.5rem;
+ line-height: $line-height-base;
+ color: #292F33;
+ }
+
+ .toc {
+ nav {
+ ul {
+ padding: 0;
+ list-style: none;
+ }
+
+ li {
+ margin-left: 0px;
+ font-size: $font-size-sm;
+
+ span {
+ font-family: monospace;
+ color: #000000 !important;
+ }
+
+ &.selected {
+ a {
+ font-weight: $font-weight-semibold;
+ color: #033a56; }
+ }
+
+ }
+ }
+
+ h3 {
+ font-size: $font-size-base;
+ font-weight: $font-weight-normal;
+ margin-top: 1rem;
+ border-bottom: 1px solid rgba(15,70,100,0.25);
+ }
+ }
+
+ .documentation-container {
+ margin: 1rem 0 1rem 0;
+ font-size: $font-size-sm;
+ font-weight: $font-weight-normal;
+
+ @include media-breakpoint-down(lg) {
+ max-width: 100%;
+ overflow-x: scroll;
+ }
+
+ h1 {
+ margin-top: 2rem;
+ font-size: $font-size-lg;
+ font-weight: $font-weight-semibold;
+ }
+
+ h1:first-of-type {
+ margin-top: 0.5rem;
+ }
+
+ h2 {
+ margin-top: 1.5rem;
+ font-size: $font-size-base;
+ font-weight: $font-weight-semibold;
+ }
+
+ h2:first-of-type {
+ margin-top: 1rem;
+ }
+
+ h3 {
+ margin-top: 1rem;
+ padding-left: 0.25rem;
+ font-size: $font-size-sm;
+ font-weight: $font-weight-semibold;
+ }
+
+ h4 {
+ padding-left: 0.5rem;
+ font-size: $font-size-sm;
+ font-weight: $font-weight-normal;
+ }
+
+ blockquote {
+ margin: 0 .5rem;
+ padding: 0 1rem;
+ color: #6a737d;
+ border-left: .25em solid #dfe2e5;
+ }
+
+ pre {
+ code {
+ font-size: 90%;
+ word-break: break-all;
+ white-space: break-spaces;
+ }
+ }
+
+ .highlighter-rouge pre {
+ margin: 1rem .5rem;
+ padding: 0.5rem 1rem;
+ }
+
+
+ .highlighter-rouge-dark{
+ div.highlight {
+ background-color: black;
+ background-image: radial-gradient(
+ #667292, black 120%
+ );
+ pointer-events: none;
+ }
+ pre.highlight {
+ background: repeating-linear-gradient(
+ 0deg,
+ rgba(black, 0.15),
+ rgba(black, 0.15) 1px,
+ transparent 1px,
+ transparent 2px
+ );
+ color: greenyellow;
+ font: Inconsolata, monospace;
+ text-shadow: 0 0 5px #C8C8C8;
+ }
+ ::selection {
+ background: #0080FF;
+ text-shadow: none;
+ }
+ }
+
+ .copy-code-button {
+ position: absolute;
+ right: 15px;
+ margin: 5px 15px 0px 0px;
+ width: 80px;
+ height: 30px;
+ color: #FFFFFF;
+ background-color: grey;
+
+ .icon {
+ width:20px;
+ margin-top: 2px;
+ padding-right:2px;
+ background-size: 12px 12px;
+ float: left;
+ }
+ .text {
+ width: 60px;
+ font-size: 12px;
+ float: right;
+ padding-right:5px;
+ margin-top: 5px;
+ text-align: center;
+ }
+
+ }
+
+ .copy-code-button:hover {
+ cursor: pointer;
+
+ }
+
+ .copy-code-button:focus {
+ /* Avoid an ugly focus outline on click in Chrome,
+ but darken the button for accessibility.
+ See https://stackoverflow.com/a/25298082/1481479 */
+ outline: 0;
+ }
+ }
+}
+
+#navigation nav {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 80px;
+}
\ No newline at end of file
diff --git a/docs/assets/_scss/site/layouts/_resources.scss b/docs/assets/_scss/site/layouts/_resources.scss
new file mode 100644
index 00000000..226236ba
--- /dev/null
+++ b/docs/assets/_scss/site/layouts/_resources.scss
@@ -0,0 +1,92 @@
+.embed-container {
+ position: relative;
+ padding-bottom: 56.25%;
+ height: 0;
+ overflow: hidden;
+ max-width: 100%;
+ }
+
+ .embed-container iframe, .embed-container object, .embed-container embed {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ }
+
+ .resources {
+
+ h2,
+ h3,
+ p {
+ width: 100%;
+ margin-bottom: 0.5rem;
+ line-height: $line-height-base;
+ color: #292F33;
+ }
+
+ h2 {
+ font-size: $font-size-lg;
+ font-weight: $font-weight-normal;
+ margin-top: 0.5rem;
+ }
+
+ h3 {
+ font-size: $font-size-base;
+ font-weight: $font-weight-normal;
+ margin-top: 0.5rem;
+ border-bottom: 1px solid rgba(15,70,100,0.25);
+ }
+
+ h3:first-child {
+ margin-top: 1rem;
+ }
+
+ p {
+ font-size: $font-size-sm;
+ font-weight: $font-weight-normal;
+ }
+ }
+
+ #resources {
+ .container.container-max {
+ max-width: 90%;
+ }
+ table {
+ @extend .table;
+ @extend .table-striped;
+ @extend .table-bordered;
+ thead {
+ @extend .thead-light;
+ }
+ td {
+ code {
+ word-break: keep-all;
+ }
+ }
+ }
+ .toc {
+ padding-left: 30px;
+ nav {
+ ul {
+ padding: 0;
+ }
+ }
+ }
+ .documentation-container {
+ @include media-breakpoint-down(lg) {
+ max-width: 100%;
+ overflow-x: scroll;
+ }
+ }
+ }
+
+
+#twitter-widget-0 {
+ max-height: 1400px;
+}
+
+.tweetfeed {
+ padding-left: 0em !important;
+ border-right: 1px solid rgba(15,70,100,0.25);
+}
\ No newline at end of file
diff --git a/docs/assets/_scss/site/objects/_alternating-cards.scss b/docs/assets/_scss/site/objects/_alternating-cards.scss
new file mode 100644
index 00000000..51de67f5
--- /dev/null
+++ b/docs/assets/_scss/site/objects/_alternating-cards.scss
@@ -0,0 +1,75 @@
+.alternating-cards {
+ .row {
+ border: 1px solid $border-color;
+ margin-bottom: 20px;
+ }
+ .icon {
+ min-height: 120px;
+ background: cornflowerblue; // default
+ text-align: center;
+
+ img {
+ height: 50px;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -75%);
+ }
+ a {
+ position: absolute;
+ bottom: 0px;
+ left: 0px;
+ width: 100%;
+ vertical-align: baseline;
+ padding: 0.25rem;
+ border-radius: 0%;
+ }
+ }
+ .card-body {
+ padding: 0rem;
+ margin: 1rem;
+ .card-title {
+ font-weight: $font-weight-medium;
+ }
+ p {
+ font-weight: $font-weight-light;
+ font-size: $font-size-sm;
+ }
+ }
+
+ .bash {
+ background-color: black;
+ background-image: radial-gradient(
+ #6495ED, black 120%
+ );
+ margin: 0;
+ overflow: hidden;
+ padding: 1rem;
+ color: white;
+ font: Inconsolata, monospace;
+ text-shadow: 0 0 5px #C8C8C8;
+ &::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: repeating-linear-gradient(
+ 0deg,
+ rgba(black, 0.15),
+ rgba(black, 0.15) 1px,
+ transparent 1px,
+ transparent 2px
+ );
+ pointer-events: none;
+ }
+ }
+ ::selection {
+ background: #0080FF;
+ text-shadow: none;
+ }
+ pre {
+ margin: 0;
+ }
+}
\ No newline at end of file
diff --git a/docs/assets/_scss/site/objects/_arc-reactor.scss b/docs/assets/_scss/site/objects/_arc-reactor.scss
new file mode 100644
index 00000000..a885c620
--- /dev/null
+++ b/docs/assets/_scss/site/objects/_arc-reactor.scss
@@ -0,0 +1,350 @@
+@include media-breakpoint-up(xl) {
+
+ div#arc {
+
+ &.flicker {
+ animation: text-flicker 3s linear;
+
+ .offset {
+ animation: power-shadow 3s linear 2s;
+ animation-iteration-count: 1;
+ }
+
+ .offset1 {
+ animation: power-flicker 6s linear 2s;
+ animation-iteration-count: 1;
+ }
+
+ .offset2 {
+ animation: power-flicker 3s linear 2s;
+ animation-iteration-count: 1;
+ }
+
+ .reactor-container {
+ background-color: #182d31b8;
+ //border: 1px solid rgb(18, 20, 20);
+ box-shadow: 0px 0px 32px 8px rgb(18, 20, 20);
+ }
+
+ .reactor-container-inner {
+ //background-color: rgb(22, 26, 27);
+ box-shadow: 0px 0px 10px 2px #52FEFE inset;
+ }
+
+ .tunnel {
+ background-color: #FFFFFF;
+ box-shadow: 0px 0px 3px 3px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ }
+
+ .core-wrapper {
+ background-color: #073c4bcc;
+ box-shadow: 0px 0px 4px 3px #52FEFE, 0px 0px 3px 2px #52FEFE inset;
+ }
+ }
+
+ .reactor-container {
+ width: 425px;
+ height: 425px;
+ margin: auto;
+ position: relative;
+ border-radius: 50%;
+ //background-color: #b0b0b1cc;
+ //border: 1px solid rgb(18, 20, 20);
+ //box-shadow: 0px 0px 32px 8px #12141482 inset;
+ }
+
+ .reactor-container-inner {
+ height: 415px;
+ width: 415px;
+ background-color: #384c5000;
+ //box-shadow: 0px 0px 1px 2px #384c50 inset;
+ }
+
+ .tunnel {
+ width: 405px;
+ height: 405px;
+ //background-color: #2b3b3e00;
+ //box-shadow: 0px 0px 3px 3px #454d4e, 0px 0px 2px 1px #454d4e inset;
+ }
+
+ .core-wrapper {
+ width: 385px;
+ height: 385px;
+ background-color: #384c5000;
+ box-shadow: 0px 0px 4px 3px #454d4e inset;
+ }
+ }
+
+ #otto {
+ position: absolute;
+ top: 25px;
+ left: 25px;
+ z-index: 2;
+ height: 375px;
+ width: 375px;
+ }
+
+ #otto-pride {
+ position: absolute;
+ top: 25px;
+ left: 25px;
+ z-index: 1;
+ height: 375px;
+ width: 375px;
+ }
+
+ .fullpage-wrapper {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ z-index: 0;
+ width: 425px;
+ height: 425px;
+ }
+
+ .circle {
+ border-radius: 50%;
+ }
+
+ .abs-center {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ margin: auto;
+ }
+}
+
+div#reactor {
+ position: absolute;
+ z-index: 2;
+ /*Align with big Otto
+ width: 40px;
+ height: 40px;
+ top: 307px;
+ left: 202px;
+ */
+
+ width: 40px;
+ height: 40px;
+ top: 294px;
+ left: 200px;
+ opacity: 1;
+
+ &.flicker {
+ animation: 5s infinite linear reactor-anim;
+ animation-delay: 1s;
+ }
+
+ .core {
+ box-shadow: 0px 0px 3px 3px #52FEFE, 0px 0px 4px 2px #52FEFE inset;
+ background-color: #073c4bc2;
+ animation: 3s linear text-flicker;
+ }
+
+ .coil {
+ position: absolute;
+ width: 4px;
+ height: 6px;
+ top: calc(50% - 4px);
+ left: calc(50% - 15px);
+ transform-origin: 15px 4px;
+ background-color: #073c4b;
+ box-shadow: 0px 0px 3px #52FEFE inset;
+ }
+
+ .coil-1 {
+ transform: rotate(0deg);
+ }
+
+ .coil-2 {
+ transform: rotate(45deg);
+ }
+
+ .coil-3 {
+ transform: rotate(90deg);
+ }
+
+ .coil-4 {
+ transform: rotate(135deg);
+ }
+
+ .coil-5 {
+ transform: rotate(180deg);
+ }
+
+ .coil-6 {
+ transform: rotate(225deg);
+ }
+
+ .coil-7 {
+ transform: rotate(270deg);
+ }
+
+ .coil-8 {
+ transform: rotate(315deg);
+ }
+}
+
+@keyframes reactor-anim {
+ from {
+ opacity: 1;
+ transform: rotate(0deg);
+ }
+ to {
+ opacity: 1;
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes text-flicker {
+ 0% {
+ opacity:0.1;
+ text-shadow: 0px 0px 29px rgba(242, 22, 22, 1);
+ }
+
+ 2% {
+ opacity:0.5;
+ text-shadow: 0px 0px 29px rgba(242, 22, 22, 1);
+ }
+ 8% {
+ opacity:0.1;
+ text-shadow: 0px 0px 29px rgba(242, 22, 22, 1);
+ }
+ 9% {
+ opacity:0.5;
+ text-shadow: 0px 0px 29px rgba(242, 22, 22, 1);
+ }
+ 12% {
+ opacity:0.1;
+ text-shadow: 0px 0px rgba(242, 22, 22, 1);
+ }
+ 20% {
+ opacity:0.5;
+ text-shadow: 0px 0px 29px rgba(242, 22, 22, 1)
+ }
+ 25% {
+ opacity:0.3;
+ text-shadow: 0px 0px 29px rgba(242, 22, 22, 1)
+ }
+ 30% {
+ opacity:0.5;
+ text-shadow: 0px 0px 29px rgba(242, 22, 22, 1)
+ }
+
+ 70% {
+ opacity:0.1;
+ text-shadow: 0px 0px 29px rgba(242, 22, 22, 1)
+ }
+
+ 72% {
+ opacity:0.2;
+ text-shadow:0px 0px 29px rgba(242, 22, 22, 1)
+ }
+
+ 77% {
+ opacity:0.5;
+ text-shadow: 0px 0px 29px rgba(242, 22, 22, 1)
+ }
+ 100% {
+ opacity:0.5;
+ text-shadow: 0px 0px 29px rgba(242, 22, 22, 1)
+ }
+ }
+
+@keyframes power-shadow {
+ 0% {
+ opacity: 0;
+ -webkit-box-shadow: 0px 0px 78px 4px rgba(1, 21, 37, 0.75), 0px 0px 2px 1px #000 inset;
+ -moz-box-shadow: 0px 0px 78px 4px rgba(1, 21, 37, 0.75), 0px 0px 2px 1px #000 inset;
+ box-shadow: 0px 0px 78px 4px rgba(1, 21, 37, 0.75), 0px 0px 2px 1px #000 inset;
+ }
+ 10% {
+ opacity: 0.2;
+ -webkit-box-shadow: 0px 0px 1px 2px #008282;
+ -moz-box-shadow: 0px 0px 1px 2px #008282;
+ box-shadow: 0px 0px 1px 2px #008282;
+ }
+ 50% {
+ opacity: 0.1;
+ -webkit-box-shadow: 0px 0px 1px 2px #008282;
+ -moz-box-shadow: 0px 0px 1px 2px #008282;
+ box-shadow: 0px 0px 1px 2px #008282;
+ }
+ 100% {
+ opacity: 1;
+ -webkit-box-shadow: 0px 0px 1px 2px #008282;
+ -moz-box-shadow: 0px 0px 1px 2px #008282;
+ box-shadow: 0px 0px 1px 2px #008282;
+ }
+}
+
+@keyframes power-flicker {
+ 0% {
+ opacity: 0;
+ -webkit-box-shadow: 0px 0px 78px 4px rgba(1, 21, 37, 0.75), 0px 0px 2px 1px #000 inset;
+ -moz-box-shadow: 0px 0px 78px 4px rgba(1, 21, 37, 0.75), 0px 0px 2px 1px #000 inset;
+ box-shadow: 0px 0px 78px 4px rgba(1, 21, 37, 0.75), 0px 0px 2px 1px #000 inset;
+ }
+ 2% {
+ opacity: 1;
+ -webkit-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ -moz-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ }
+ 8% {
+ opacity: 0.1;
+ -webkit-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ -moz-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ }
+ 9% {
+ opacity: 1;
+ -webkit-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ -moz-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ }
+ 10% {
+ opacity: 0.2;
+ -webkit-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ -moz-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ }
+ 12% {
+ opacity: 1;
+ -webkit-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ -moz-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ }
+ 20% {
+ opacity: 0.5;
+ -webkit-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ -moz-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ }
+ 40% {
+ opacity: 0.2;
+ -webkit-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ -moz-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ }
+ 60% {
+ opacity: 0.3;
+ -webkit-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ -moz-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ }
+ 80% {
+ opacity: 0.4;
+ -webkit-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ -moz-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ }
+ 100% {
+ opacity: 1;
+ -webkit-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ -moz-box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ box-shadow: 0px 0px 3px 4px #52FEFE, 0px 0px 2px 1px #52FEFE inset;
+ }
+}
diff --git a/docs/assets/_scss/site/objects/_button.scss b/docs/assets/_scss/site/objects/_button.scss
new file mode 100644
index 00000000..65d057c4
--- /dev/null
+++ b/docs/assets/_scss/site/objects/_button.scss
@@ -0,0 +1,59 @@
+.btn {
+ padding: {
+ top: 13px;
+ bottom: 9px;
+ }
+ font-size: 0.75rem;
+ border-radius: 5px;
+ text-transform: uppercase;
+ font-weight: $font-weight-bold;
+ &.btn-no-border {
+ border-width: 0 !important;
+ }
+ &:hover {
+ text-decoration: underlin;
+ }
+}
+
+.btn-sm {
+ padding: 12px 52px;
+}
+
+.btn-primary {
+ color: $button-primary-foreground;
+ background-color: $button-primary-background;
+ border-color: $button-primary-foreground;
+ &:hover {
+ color: $button-primary-background;
+ background-color: $button-primary-background-hover;
+ border-color: $button-primary-border-hover;
+ text-decoration: underline;
+ }
+}
+
+.btn-secondary {
+ color: $button-secondary-foreground;
+ background-color: $button-secondary-background;
+ border-color: $button-secondary-border;
+ &:hover {
+ color: $button-secondary-background;
+ background-color: $button-secondary-background-hover;
+ border-color: $button-secondary-border-hover;
+ text-decoration: underline;
+ }
+}
+
+.btn-outline-primary {
+ color: $button-primary-foreground;
+ background-color: transparent;
+ border-color: $button-primary-foreground;
+ &:hover {
+ color: $white;
+ background: $button-primary-foreground;
+ }
+}
+.btn-outline-light {
+ &:hover {
+ color: $button-primary-foreground;
+ }
+}
\ No newline at end of file
diff --git a/docs/assets/_scss/site/objects/_card.scss b/docs/assets/_scss/site/objects/_card.scss
new file mode 100644
index 00000000..801821cb
--- /dev/null
+++ b/docs/assets/_scss/site/objects/_card.scss
@@ -0,0 +1,56 @@
+.card {
+ border-radius: 0;
+ .card-body {
+ text-align: center;
+ padding: 2rem 1.25rem;
+ }
+
+ &.card-dark {
+ color: $card-dark-foreground;
+ background-color: $card-dark-background;
+ a {
+ color: $card-dark-link;
+ &:hover {
+ color: $card-dark-link-hover;
+ }
+ }
+ }
+
+ &.card-light {
+ a {
+ color: $card-light-link;
+ font-weight: $font-weight-medium
+ }
+ color: #575757;
+ background-color: $card-light-background;
+ p {
+ font-size: .875rem;
+ }
+ }
+ // specific blog card styles
+ &.blog-card {
+ .card-body {
+ padding: 0;
+ }
+ article.post {
+ padding: 2rem 1.25rem;
+ }
+ }
+}
+
+// landing page promo cards
+.promo-cards {
+ figure {
+ text-align: center;
+ height: 90px;
+ img {
+ width: auto;
+ max-height: 90px;
+ }
+ }
+ h5 {
+ margin-left: auto;
+ margin-right: auto;
+ width: 90%;
+ }
+}
\ No newline at end of file
diff --git a/docs/assets/_scss/site/objects/_day-night.scss b/docs/assets/_scss/site/objects/_day-night.scss
new file mode 100644
index 00000000..5d6e884b
--- /dev/null
+++ b/docs/assets/_scss/site/objects/_day-night.scss
@@ -0,0 +1,53 @@
+.day-night {
+
+ height: 50px;
+ font-weight: 500;
+
+ span {
+ padding: 7.5px 0;
+ height: 50px;
+ }
+
+ .day-label {
+ font-weight: bold;
+ text-shadow: 2px 0px #011a4c;
+ }
+
+ .night-label {
+ opacity: 0.6;
+ font-weight: bold;
+ text-shadow: 2px 0px #000;
+ }
+
+ .container {
+ width: 70px;
+ }
+
+ svg
+ {
+ width: 100%;
+ height: 40px;
+ cursor: pointer;
+ }
+
+ #night-content
+ {
+ opacity: 0.5;
+ }
+
+ .inner-shadow
+ {
+ stroke-opacity: 0.1;
+ stroke-width: 5;
+ stroke: black;
+ fill: none;
+ }
+
+ .container input {
+ position: absolute;
+ opacity: 0;
+ cursor: pointer;
+ height: 0;
+ width: 0;
+ }
+}
\ No newline at end of file
diff --git a/docs/assets/_scss/site/objects/_footer.scss b/docs/assets/_scss/site/objects/_footer.scss
new file mode 100644
index 00000000..ba5be8d6
--- /dev/null
+++ b/docs/assets/_scss/site/objects/_footer.scss
@@ -0,0 +1,44 @@
+.site-footer {
+ padding: 24px 0 34px 0;
+ .social-links {
+ color: $footer-foreground;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ a {
+ font-size: 0.75rem;
+ color: $footer-link-color;
+ &:hover {
+ color: $footer-link-color;
+ text-decoration: underline;
+ }
+ }
+ li {
+ display: inline-block;
+ padding: 5px;
+ &:hover {
+ color: $footer-link-color;
+ a {
+ text-decoration: underline;
+ }
+ }
+ }
+ }
+ .copyright {
+ font-size: 0.75rem;
+ font-weight: $font-weight-light;
+ color: $footer-copyright;
+ }
+ .logo {
+ max-width: 146px;
+ height: auto;
+ margin-left: 30px;
+ }
+ .vm-logo {
+ font-size: .75rem;
+ img {
+ max-width: 75px;
+ margin-left: 30px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/assets/_scss/site/objects/_header.scss b/docs/assets/_scss/site/objects/_header.scss
new file mode 100644
index 00000000..006b9be9
--- /dev/null
+++ b/docs/assets/_scss/site/objects/_header.scss
@@ -0,0 +1,114 @@
+.site-header {
+ position: sticky;
+ position: -webkit-sticky;
+ top: 0px;
+ background: $header-background;
+ border-bottom: 8px #343a40 solid;
+ z-index: 5;
+
+ .site-header-content {
+ position: relative;
+ display: flex;
+ margin: 0 auto;
+ padding: 10px 0 10px 0;
+ max-width: $section-content-max-width;
+ @include media-breakpoint-down(lg) {
+ padding: {
+ left: $grid-gutter-width / 2 + $section-padding-x;
+ right: $grid-gutter-width / 2 + $section-padding-x;
+ }
+ }
+ }
+
+ .logo {
+ height: 45px;
+ width: auto;
+ }
+
+ .nav-mobile-link {
+ display: none;
+ position: absolute;
+ top: 50%;
+ right: 30px;
+ margin-top: -6px;
+
+ a {
+ display: block;
+ width: 23px;
+ height: 12px;
+ border-top: 4px solid $header-foreground;
+ border-bottom: 4px solid $header-foreground;
+ &.selected {
+ border-color: $header-foreground-selected;
+ }
+ }
+
+ @include media-breakpoint-down(sm) {
+ display: block;
+ }
+ }
+
+ .nav-menu {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ list-style: none;
+ margin: 0;
+ margin-left: 40px;
+ margin-right:10px;
+ padding: 0;
+
+ @include media-breakpoint-down(sm) {
+ display: none;
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ margin: 0;
+ padding: 30px;
+ min-height: 250px;
+ background: $header-background;
+ box-shadow: 3px 2px 4px 2px rgba(0,0,0,0.25);
+
+ &.on {
+ display: block;
+ }
+
+ li {
+ font-size: 1.5rem;
+ }
+ }
+
+ li {
+ margin-right: 35px;
+ a {
+ font-weight: $font-weight-normal;
+ font-size: .875rem;
+ }
+ &:last-child {
+ margin-right: 0;
+ }
+
+ &.selected {
+ a {
+ color: $header-foreground-selected;
+ font-weight: $font-weight-semibold;
+ &:hover {
+ color: $header-foreground-selected;
+ text-decoration: none;
+ }
+ }
+ }
+
+ a {
+ color: $header-foreground;
+
+ &:hover {
+ color: $header-foreground;
+ text-decoration: underline;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/assets/_scss/site/objects/_home-hero.scss b/docs/assets/_scss/site/objects/_home-hero.scss
new file mode 100644
index 00000000..1cf3b832
--- /dev/null
+++ b/docs/assets/_scss/site/objects/_home-hero.scss
@@ -0,0 +1,89 @@
+.home-hero {
+
+ @include media-breakpoint-up(md) {
+ min-height: 500px;
+ }
+
+ color: $white;
+ h1,
+ h2,
+ h3 {
+ color: $white;
+ font-weight: $font-weight-light;
+ }
+ .section-content {
+ margin-top: 15px;
+
+ .hero-content,
+ .hero-cta {
+
+
+ p {
+ @include media-breakpoint-up(md) {
+ width: 100% ! important;
+ }
+
+ @include media-breakpoint-down(md) {
+ width: 150% ! important;
+ }
+
+ @media only screen and (min-width: 900px) {
+ width: 100% ! important;
+ }
+
+ @include media-breakpoint-down(sm) {
+ width: 100% ! important;
+ }
+ }
+ }
+
+ }
+
+ .hero-veba-mascot {
+
+ overflow: hidden;
+
+
+ img {
+ height: 100%;
+ width: 100%;
+ }
+ @include media-breakpoint-down(sm) {
+ height: 1px;
+ background-image: none !important;
+ }
+ }
+
+ .hero-cta {
+ display: block;
+ margin-bottom: 70px;
+
+ @include media-breakpoint-up(md) {
+ display: flex;
+ flex-direction: row;
+ justify-content: left;
+ }
+
+ .btn {
+
+ @include media-breakpoint-up(md) {
+ min-width: 250px;
+ }
+
+ padding: {
+ top: 16px;
+ bottom: 16px;
+ }
+
+ margin: 0 32px 0 0;
+ }
+ }
+
+ .section-card {
+ margin-top: 68px;
+ .section-content {
+ background-color: transparent;
+ padding: 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/assets/_scss/site/objects/_post.scss b/docs/assets/_scss/site/objects/_post.scss
new file mode 100755
index 00000000..91dfe2fd
--- /dev/null
+++ b/docs/assets/_scss/site/objects/_post.scss
@@ -0,0 +1,272 @@
+.post-thumbnail {
+ width: 100%;
+ margin: 0 auto 1.875rem auto;
+ height: 125px;
+ position: relative;
+ img {
+ width: auto;
+ height: auto;
+ max-width: 100%;
+ max-height: 100%;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+}
+
+.post-single-hero {
+ background-color: map-get($field-backgrounds, 'med-blue'); // default
+ padding-bottom: 105px;
+ h1,
+ h2,
+ h3 {
+ color: $white;
+ font-weight: $font-weight-light;
+ text-align: center;
+ }
+ &.post-single-hero-short {
+ padding-bottom: 0;
+ }
+}
+
+.post-single-body {
+ margin-top: -105px;
+
+ font-size: $font-size-sm;
+ font-weight: $font-weight-normal;
+
+ h1 {
+ font-size: $font-size-lg;
+ font-weight: $font-weight-semibold;
+ }
+
+ h2 {
+ font-size: $font-size-base;
+ font-weight: $font-weight-semibold;
+ }
+
+ h3 {
+ font-size: $font-size-sm;
+ font-weight: $font-weight-semibold;
+ margin-left: 1rem;
+ margin-top: 1rem;
+ }
+}
+
+.post-single-meta {
+ display: flex;
+ align-items: center;
+ color: $post-meta-foreground;
+ margin-bottom: 3rem;
+
+ @include media-breakpoint-down(sm) {
+ // flex-direction: column;
+ display: block;
+ }
+}
+
+.post-single-meta-author {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ margin-right: 1rem;
+
+ @include media-breakpoint-down(sm) {
+ justify-content: center;
+ margin-right: 0;
+ margin-bottom: 1rem;
+ }
+}
+
+.post-single-meta-author-avatar {
+ border-radius: 50%;
+ width: 64px;
+ height: 64px;
+ margin-right: 1rem;
+ overflow: hidden;
+
+ img {
+ width: 100%;
+ height: auto;
+ }
+}
+
+.post-single-meta-author-name {
+ flex: 1;
+ font-weight: $font-weight-semibold;
+
+ @include media-breakpoint-down(sm) {
+ flex: none;
+ }
+}
+
+.post-single-meta-date {
+ text-align: right;
+
+ @include media-breakpoint-down(sm) {
+ text-align: center;
+ }
+}
+
+.post-single-image {
+ margin: 0 auto 3rem auto;
+ max-width: 700px;
+
+ img {
+ height: auto;
+ width: 100%;
+ }
+
+ @include media-breakpoint-down(sm) {
+ margin-left: -30px;
+ margin-right: -30px;
+ }
+}
+
+.post-single-content {
+ max-width: 98%;
+ margin: 0 auto 3rem auto;
+
+ h2,
+ h3,
+ p {
+ width: 100%;
+ margin-bottom: 0.5rem;
+ }
+
+ h2 {
+ margin-top: 0.5rem;
+ font-size: $font-size-lg;
+ font-weight: $font-weight-base;
+ }
+
+ h3 {
+ font-size: $font-size-base;
+ font-weight: $font-weight-semibold;
+ margin-top: 0.5rem;
+ }
+
+ p:last-child {
+ margin-bottom: 0;
+ }
+
+ img {
+ max-width: 100%;
+ }
+}
+
+table {
+ border-collapse: collapse;
+ width: 100%;
+ margin: 1rem 0rem;
+}
+
+table, th, td {
+ border: 1px solid black;
+}
+
+th, td {
+ padding: 10px;
+}
+
+.faqs {
+ svg {
+ padding-top: 3px;
+ }
+
+ p {
+ flex: 1;
+ margin: 0px;
+ padding: 0px 5px;
+ }
+}
+
+.examples {
+
+ h2 {
+ margin: 1rem 0rem;
+ }
+
+ p {
+ padding: 0rem 0.5rem;
+ margin: 0rem 0rem 0.25rem 0rem;
+ width: 100%;
+ }
+
+ .title {
+ margin-top: 1rem;
+ padding: 0.5rem 1.5rem 0rem 1.5rem;
+ width: 100%;
+
+ h3 {
+ font-weight: $font-weight-semibold;
+ font-size: $font-size-sm;
+ margin: 0.5rem 0rem 0.5rem 0rem;
+ }
+
+ .language {
+ vertical-align: bottom;
+ margin-top: auto;
+ margin-bottom: auto;
+ text-align: left;
+ img {
+ height: 20px;
+ }
+ span {
+ font-size: $font-size-xs;
+ font-weight: $font-weight-semibold;
+ }
+ }
+ }
+
+ .usecases {
+ padding: 0rem .5rem 0.1rem .5rem;
+ margin-bottom: 0.5rem;
+
+ span {
+ padding: 0.1rem 1rem;
+ border-radius: 10px;
+ font-size: $font-size-xs;
+ font-weight: $font-weight-semibold;
+ color: #FFFFFF;
+ border: 1px #4f676638 solid;
+
+ &.notification {
+ background-color: #a56fa5;
+ }
+
+ &.automation {
+ background-color: #c293e6;
+ }
+
+ &.integration {
+ background-color: rgb(156, 156, 238);
+ }
+
+ &.remediation {
+ background-color: #79ad79;
+ }
+
+ &.audit {
+ background-color: rgb(253, 253, 159);
+ }
+
+ &.analytics {
+ background-color: rgb(253, 212, 136);
+ }
+
+ &.other {
+ background-color: grey;
+ }
+ }
+ }
+}
+
+#otto-hack {
+ background: gray; /* For browsers that do not support gradients */
+ background: -webkit-linear-gradient(left, orange , yellow, green, cyan, blue, violet); /* For Safari 5.1 to 6.0 */
+ background: -o-linear-gradient(right, orange, yellow, green, cyan, blue, violet); /* For Opera 11.1 to 12.0 */
+ background: -moz-linear-gradient(right, orange, yellow, green, cyan, blue, violet); /* For Firefox 3.6 to 15 */
+ background: linear-gradient(to right, orange , yellow, green, cyan, blue, violet); /* Standard syntax (must be last) */
+}
\ No newline at end of file
diff --git a/docs/assets/_scss/site/objects/_section.scss b/docs/assets/_scss/site/objects/_section.scss
new file mode 100644
index 00000000..64ca798c
--- /dev/null
+++ b/docs/assets/_scss/site/objects/_section.scss
@@ -0,0 +1,54 @@
+.section {
+ padding: $section-padding-y $section-padding-x;
+ &.section-blue {
+ color: $section-blue-foreground;
+ background: $section-blue-background;
+ }
+
+ &.section-grey {
+ color: $body-color-darkest;
+ background: $section-grey-background;
+ a {
+ color: $darkest-blue;
+ }
+ }
+
+ &.section-card {
+ padding-top: 0;
+ padding-left: 0;
+ padding-right: 0;
+ &.section-card-offset-top {
+ .row {
+ margin-top: -120px;
+ }
+ .section-content {
+ @include media-breakpoint-up(lg) {
+ padding: {
+ left: 0;
+ right: 0;
+ }
+ }
+ }
+ }
+ .section-content {
+ max-width: $section-content-max-width;
+ background: $white;
+ border-radius: 0;
+ padding: 45px 30px;
+ min-height: 115px;
+ @include media-breakpoint-down(sm) {
+ border-radius: 0;
+ }
+ }
+ }
+
+ .section-content {
+ max-width: $section-content-max-width;
+ margin: 0 auto;
+
+ &.section-content-thin {
+ padding-left: 50px;
+ padding-right: 50px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/assets/_scss/site/objects/_team.scss b/docs/assets/_scss/site/objects/_team.scss
new file mode 100644
index 00000000..06a3fff7
--- /dev/null
+++ b/docs/assets/_scss/site/objects/_team.scss
@@ -0,0 +1,43 @@
+#team-veba {
+
+ .imageicon {
+ border: 1px solid #FFF !important;
+ box-shadow: 0px 2px 4px 0px #52FEFE !important;
+ }
+
+ .socials {
+ padding: 0.5rem 0 0.25rem 0;
+ color: #FFF;
+ //background-color: #343a40 !important;
+ text-align: center !important;
+ vertical-align: middle !important;
+
+ .github svg {
+ color: #000 !important;
+ background-color: #FFFFFFdd !important;
+ }
+
+ .twitter svg {
+ color: #007bff !important;
+ background-color: #FFFFFFdd !important;
+ }
+
+ .website svg {
+ color: #030303 !important;
+ background-color: #FFFFFFdd !important;
+ }
+
+ svg {
+ height: 30px;
+ width: 30px;
+ padding: 0.25rem;
+ border-radius: 50% !important;
+ border: 1px solid #FFF !important;
+ box-shadow: 0px 1px 2px 0px #FFF !important;
+ }
+ }
+
+ .membername {
+ padding: 0.5rem 0;
+ }
+}
\ No newline at end of file
diff --git a/docs/assets/_scss/site/objects/_thumbnail-grid.scss b/docs/assets/_scss/site/objects/_thumbnail-grid.scss
new file mode 100644
index 00000000..cb463e4e
--- /dev/null
+++ b/docs/assets/_scss/site/objects/_thumbnail-grid.scss
@@ -0,0 +1,27 @@
+.thumbnail-grid {
+ color: $white;
+ h6 {
+ font-size: 1rem;
+ margin-bottom: 0;
+ a {
+ color: $white;
+ }
+ }
+ p {
+ font-size: .875rem;
+ }
+ .thumbnail-item {
+ margin-bottom: 2.5rem;
+ img {
+ width: 80px;
+ height: 80px;
+ @include media-breakpoint-up(md) {
+ width: 120px;
+ height: 120px;
+ }
+ }
+ }
+ .media-body {
+ margin-left: 20px;
+ }
+}
\ No newline at end of file
diff --git a/docs/assets/_scss/site/settings/_variables.scss b/docs/assets/_scss/site/settings/_variables.scss
new file mode 100644
index 00000000..2569e812
--- /dev/null
+++ b/docs/assets/_scss/site/settings/_variables.scss
@@ -0,0 +1,124 @@
+// File Paths
+$path-fonts: "fonts";
+$path-images: "img";
+
+// Colors
+
+$black: #000;
+$white: #FFFFFF;
+$light: $white;
+$pink: #A41458;
+$darkest-blue: #1D428A;
+$dark-grey: #717074;
+// Bootstrap overrides
+
+$body-bg: #bbb;
+$body-color: #3F3E3E;
+$box-shadow-sm: 0px 2px 4px 0px rgba(0,0,0,0.5);
+$border-color: #E8E8E8;
+
+$card-border-width: 1px;
+$card-border-color: $border-color;
+
+$font-family-base: "Metropolis", Helvetica, Arial, sans-serif;
+$font-size-base: 1.125rem; // 18px
+
+$link-color: #007AB8;;
+$link-decoration: none;
+$link-hover-color: $link-color;
+$link-hover-decoration: underline;
+
+$font-weight-light: 300;
+$font-weight-normal: 400;
+$font-weight-medium: 500;
+$font-weight-semibold: 600;
+$font-weight-bold: 700;
+
+// Custom
+
+$body-color-darker: #333;
+$body-color-darkest: #111;
+
+// Container
+
+$container-background: $white;
+$container-max-width: 1440px;
+
+// Header
+
+$header-background: $white;
+$header-foreground: $black;
+$header-foreground-selected: $black;
+
+// Footer
+$footer-foreground: #808080;
+$footer-link-color: #474747;
+$footer-copyright: $black;
+
+// Sections
+
+$section-content-max-width: 996px;
+$section-grey-background: #f2f2f2;
+
+$section-blue-background: #0165AB;
+$section-blue-foreground: $white;
+
+// Cards
+
+$card-dark-background: #0165AB;
+$card-dark-foreground: $white;
+$card-dark-link: $white;
+$card-dark-link-hover: $white;
+
+$card-light-background: $white;
+$card-light-link: $link-color;
+
+// alternating cards
+$field-backgrounds: (
+ 'light-blue': #00C1D5,
+ 'med-blue': #0091DA,
+ 'dark-blue': #1D428A,
+ 'darkest-blue': #002538,
+ 'cornflower-blue': #6495ED,
+ 'green': #78BE20
+);
+
+@each $name, $hex in $field-backgrounds {
+ .section-background-#{$name} {
+ background-color: #{$hex} !important;
+ color: $white !important;
+ h1,
+ h2,
+ p {
+ color: $white !important;
+ }
+ }
+ .bg-color-#{$name} {
+ background-color: #{$hex} !important;
+ }
+}
+
+
+
+// Buttons
+
+$button-primary-background: $white;
+$button-primary-background-hover: $darkest-blue;
+$button-primary-foreground: $darkest-blue;
+$button-primary-border: $white;
+$button-primary-border-hover: $white;
+
+$button-secondary-background: $darkest-blue;
+$button-secondary-background-hover: $white;
+$button-secondary-foreground: $white;
+$button-secondary-border: $white;
+$button-secondary-border-hover: $darkest-blue;
+
+// Posts
+$post-hero-gradient-start: #fafafa;
+$post-hero-gradient-end: #ddd;
+$post-meta-foreground: $body-color-darker;
+
+// section
+$section-padding-x: 30px;
+$section-padding-y: 40px;
diff --git a/docs/assets/_scss/site/utilities/_image.scss b/docs/assets/_scss/site/utilities/_image.scss
new file mode 100644
index 00000000..44787da8
--- /dev/null
+++ b/docs/assets/_scss/site/utilities/_image.scss
@@ -0,0 +1,3 @@
+.thumbnail img {
+ width: 100%;
+}
diff --git a/docs/assets/_scss/site/utilities/_type.scss b/docs/assets/_scss/site/utilities/_type.scss
new file mode 100644
index 00000000..374900f1
--- /dev/null
+++ b/docs/assets/_scss/site/utilities/_type.scss
@@ -0,0 +1,44 @@
+// Weights
+
+.font-weight-light {
+ font-weight: $font-weight-light !important;
+}
+
+.font-weight-normal {
+ font-weight: $font-weight-normal !important;
+}
+
+.font-weight-medium {
+ font-weight: $font-weight-medium !important;
+}
+
+.font-weight-semibold {
+ font-weight: $font-weight-semibold !important;
+}
+
+.font-weight-bold {
+ font-weight: $font-weight-bold !important;
+}
+
+// Colors
+
+.font-color-darker {
+ color: $body-color-darker !important;
+}
+
+.font-color-darkest {
+ color: $body-color-darkest !important;
+}
+
+// Misc
+
+p {
+ &.lead-in {
+ color: $body-color-darker;
+ font-size: 1.375rem;
+
+ @include media-breakpoint-down(sm) {
+ font-size: 1.125rem;
+ }
+ }
+}
diff --git a/docs/assets/css/fonts/IcoMoon/IcoMoon-Free.ttf b/docs/assets/css/fonts/IcoMoon/IcoMoon-Free.ttf
new file mode 100755
index 00000000..56919449
Binary files /dev/null and b/docs/assets/css/fonts/IcoMoon/IcoMoon-Free.ttf differ
diff --git a/docs/assets/css/fonts/Metropolis/Metropolis-Bold.woff b/docs/assets/css/fonts/Metropolis/Metropolis-Bold.woff
new file mode 100755
index 00000000..b9eb2513
Binary files /dev/null and b/docs/assets/css/fonts/Metropolis/Metropolis-Bold.woff differ
diff --git a/docs/assets/css/fonts/Metropolis/Metropolis-BoldItalic.woff b/docs/assets/css/fonts/Metropolis/Metropolis-BoldItalic.woff
new file mode 100755
index 00000000..3d0d67d8
Binary files /dev/null and b/docs/assets/css/fonts/Metropolis/Metropolis-BoldItalic.woff differ
diff --git a/docs/assets/css/fonts/Metropolis/Metropolis-Light.woff b/docs/assets/css/fonts/Metropolis/Metropolis-Light.woff
new file mode 100755
index 00000000..7bdd5d9f
Binary files /dev/null and b/docs/assets/css/fonts/Metropolis/Metropolis-Light.woff differ
diff --git a/docs/assets/css/fonts/Metropolis/Metropolis-LightItalic.woff b/docs/assets/css/fonts/Metropolis/Metropolis-LightItalic.woff
new file mode 100755
index 00000000..9dbe5708
Binary files /dev/null and b/docs/assets/css/fonts/Metropolis/Metropolis-LightItalic.woff differ
diff --git a/docs/assets/css/fonts/Metropolis/Metropolis-Medium.woff b/docs/assets/css/fonts/Metropolis/Metropolis-Medium.woff
new file mode 100755
index 00000000..90cb752a
Binary files /dev/null and b/docs/assets/css/fonts/Metropolis/Metropolis-Medium.woff differ
diff --git a/docs/assets/css/fonts/Metropolis/Metropolis-MediumItalic.woff b/docs/assets/css/fonts/Metropolis/Metropolis-MediumItalic.woff
new file mode 100755
index 00000000..34335d64
Binary files /dev/null and b/docs/assets/css/fonts/Metropolis/Metropolis-MediumItalic.woff differ
diff --git a/docs/assets/css/fonts/Metropolis/Metropolis-Regular.woff b/docs/assets/css/fonts/Metropolis/Metropolis-Regular.woff
new file mode 100755
index 00000000..312202cf
Binary files /dev/null and b/docs/assets/css/fonts/Metropolis/Metropolis-Regular.woff differ
diff --git a/docs/assets/css/fonts/Metropolis/Metropolis-RegularItalic.woff b/docs/assets/css/fonts/Metropolis/Metropolis-RegularItalic.woff
new file mode 100755
index 00000000..63846f94
Binary files /dev/null and b/docs/assets/css/fonts/Metropolis/Metropolis-RegularItalic.woff differ
diff --git a/docs/assets/css/fonts/Metropolis/Metropolis-SemiBold.woff b/docs/assets/css/fonts/Metropolis/Metropolis-SemiBold.woff
new file mode 100755
index 00000000..c4107118
Binary files /dev/null and b/docs/assets/css/fonts/Metropolis/Metropolis-SemiBold.woff differ
diff --git a/docs/assets/css/fonts/Metropolis/Metropolis-SemiBoldItalic.woff b/docs/assets/css/fonts/Metropolis/Metropolis-SemiBoldItalic.woff
new file mode 100755
index 00000000..522f97da
Binary files /dev/null and b/docs/assets/css/fonts/Metropolis/Metropolis-SemiBoldItalic.woff differ
diff --git a/docs/assets/css/fonts/Metropolis/Open Font License.md b/docs/assets/css/fonts/Metropolis/Open Font License.md
new file mode 100755
index 00000000..9f52ca56
--- /dev/null
+++ b/docs/assets/css/fonts/Metropolis/Open Font License.md
@@ -0,0 +1,105 @@
+ Copyright (c) 2015, Chris Simpson
, with Reserved Font Name: "Metropolis".
+
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
+ This license is copied below, and is also available with a FAQ at:
+ http://scripts.sil.org/OFL
+
+ Version 2.0 - 18 March 2012
+
+
+SIL Open Font License
+====================================================
+
+
+Preamble
+----------
+
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+Definitions
+-------------
+
+`"Font Software"` refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+`"Reserved Font Name"` refers to any names specified as such after the
+copyright statement(s).
+
+`"Original Version"` refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+`"Modified Version"` refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+`"Author"` refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+Permission & Conditions
+------------------------
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1. Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2. Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3. No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4. The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5. The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+Termination
+-----------
+
+This license becomes null and void if any of the above conditions are
+not met.
+
+
+ DISCLAIMER
+
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+ OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/docs/assets/css/fonts/Metropolis/README.md b/docs/assets/css/fonts/Metropolis/README.md
new file mode 100755
index 00000000..e6b973ab
--- /dev/null
+++ b/docs/assets/css/fonts/Metropolis/README.md
@@ -0,0 +1,24 @@
+
+
+# The Metropolis Typeface
+
+The Vision
+---
+To create a modern, geometric typeface. Open sourced, and openly available. Influenced by other popular geometric, minimalist sans-serif typefaces of the new millenium. Designed for optimal readability at small point sizes while beautiful at large point sizes.
+
+December 2017 update
+---
+Currently working on greatly improving spacing and kerning of the base typeface. Once this is done, work on other variations (e.g. rounded or slab) can begin in earnest.
+
+The License
+---
+Licensed under Open Font License (OFL). Available to anyone and everyone. Contributions welcome.
+
+Contact
+---
+Contact me via chris.m.simpson@icloud.com or http://twitter.com/ChrisMSimpson for any questions, requests or improvements (or just submit a pull request).
+
+Support
+---
+You can now support work on Metropolis via Patreon at https://www.patreon.com/metropolis.
+
diff --git a/docs/assets/css/styles.scss b/docs/assets/css/styles.scss
new file mode 100644
index 00000000..9d14a504
--- /dev/null
+++ b/docs/assets/css/styles.scss
@@ -0,0 +1,3 @@
+---
+---
+@import '../_scss/_styles';
diff --git a/docs/assets/img/by-vmware.svg b/docs/assets/img/by-vmware.svg
new file mode 100644
index 00000000..37c40a64
--- /dev/null
+++ b/docs/assets/img/by-vmware.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/assets/img/case-study-icons/case-study-1.svg b/docs/assets/img/case-study-icons/case-study-1.svg
new file mode 100644
index 00000000..665ae339
--- /dev/null
+++ b/docs/assets/img/case-study-icons/case-study-1.svg
@@ -0,0 +1,30 @@
+
+
+
+ Group 15
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/assets/img/case-study-icons/case-study-2.svg b/docs/assets/img/case-study-icons/case-study-2.svg
new file mode 100644
index 00000000..84c3af34
--- /dev/null
+++ b/docs/assets/img/case-study-icons/case-study-2.svg
@@ -0,0 +1,50 @@
+
+
+
+ Group 26
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/assets/img/case-study-icons/case-study-3.svg b/docs/assets/img/case-study-icons/case-study-3.svg
new file mode 100644
index 00000000..c1bbec82
--- /dev/null
+++ b/docs/assets/img/case-study-icons/case-study-3.svg
@@ -0,0 +1,35 @@
+
+
+
+ icon
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/assets/img/cleanup/icon--github.svg b/docs/assets/img/cleanup/icon--github.svg
new file mode 100644
index 00000000..78a6265f
--- /dev/null
+++ b/docs/assets/img/cleanup/icon--github.svg
@@ -0,0 +1,18 @@
+
+
+
+ github-logo
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/assets/img/cleanup/icon--menu.svg b/docs/assets/img/cleanup/icon--menu.svg
new file mode 100644
index 00000000..15dc6e55
--- /dev/null
+++ b/docs/assets/img/cleanup/icon--menu.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/docs/assets/img/cleanup/icon--slack.svg b/docs/assets/img/cleanup/icon--slack.svg
new file mode 100644
index 00000000..f7009764
--- /dev/null
+++ b/docs/assets/img/cleanup/icon--slack.svg
@@ -0,0 +1,21 @@
+
+
+
+ logo-slack
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/assets/img/cleanup/icon--twitter.svg b/docs/assets/img/cleanup/icon--twitter.svg
new file mode 100644
index 00000000..9b52fbaf
--- /dev/null
+++ b/docs/assets/img/cleanup/icon--twitter.svg
@@ -0,0 +1,18 @@
+
+
+
+ logo-twitter
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/assets/img/cleanup/logo--contour.svg b/docs/assets/img/cleanup/logo--contour.svg
new file mode 100644
index 00000000..5b14015b
--- /dev/null
+++ b/docs/assets/img/cleanup/logo--contour.svg
@@ -0,0 +1,22 @@
+
+
+
+ logo_contour
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/assets/img/cleanup/nav-marker.png b/docs/assets/img/cleanup/nav-marker.png
new file mode 100644
index 00000000..513215f7
Binary files /dev/null and b/docs/assets/img/cleanup/nav-marker.png differ
diff --git a/docs/assets/img/cleanup/peelback.png b/docs/assets/img/cleanup/peelback.png
new file mode 100644
index 00000000..082e7546
Binary files /dev/null and b/docs/assets/img/cleanup/peelback.png differ
diff --git a/docs/assets/img/cta-icons/functions-icon.png b/docs/assets/img/cta-icons/functions-icon.png
new file mode 100644
index 00000000..51aca03a
Binary files /dev/null and b/docs/assets/img/cta-icons/functions-icon.png differ
diff --git a/docs/assets/img/cta-icons/scale-icon.png b/docs/assets/img/cta-icons/scale-icon.png
new file mode 100644
index 00000000..bd0f0a78
Binary files /dev/null and b/docs/assets/img/cta-icons/scale-icon.png differ
diff --git a/docs/assets/img/cta-icons/vcenter-icon.png b/docs/assets/img/cta-icons/vcenter-icon.png
new file mode 100644
index 00000000..496160d7
Binary files /dev/null and b/docs/assets/img/cta-icons/vcenter-icon.png differ
diff --git a/docs/assets/img/favicon.ico b/docs/assets/img/favicon.ico
new file mode 100644
index 00000000..5f16b75a
Binary files /dev/null and b/docs/assets/img/favicon.ico differ
diff --git a/docs/assets/img/heroes/veba_banner_lg.png b/docs/assets/img/heroes/veba_banner_lg.png
new file mode 100644
index 00000000..bc673328
Binary files /dev/null and b/docs/assets/img/heroes/veba_banner_lg.png differ
diff --git a/docs/assets/img/heroes/veba_banner_lg_pride.png b/docs/assets/img/heroes/veba_banner_lg_pride.png
new file mode 100644
index 00000000..da0b02c4
Binary files /dev/null and b/docs/assets/img/heroes/veba_banner_lg_pride.png differ
diff --git a/docs/assets/img/heroes/veba_banner_md.png b/docs/assets/img/heroes/veba_banner_md.png
new file mode 100644
index 00000000..e755ee88
Binary files /dev/null and b/docs/assets/img/heroes/veba_banner_md.png differ
diff --git a/docs/assets/img/heroes/veba_banner_md_pride.png b/docs/assets/img/heroes/veba_banner_md_pride.png
new file mode 100644
index 00000000..978d8735
Binary files /dev/null and b/docs/assets/img/heroes/veba_banner_md_pride.png differ
diff --git a/docs/assets/img/heroes/veba_banner_sm.png b/docs/assets/img/heroes/veba_banner_sm.png
new file mode 100644
index 00000000..93eb8b81
Binary files /dev/null and b/docs/assets/img/heroes/veba_banner_sm.png differ
diff --git a/docs/assets/img/heroes/veba_banner_sm_pride.png b/docs/assets/img/heroes/veba_banner_sm_pride.png
new file mode 100644
index 00000000..2ac3c580
Binary files /dev/null and b/docs/assets/img/heroes/veba_banner_sm_pride.png differ
diff --git a/docs/assets/img/heroes/veba_otto_the_orca_lg.png b/docs/assets/img/heroes/veba_otto_the_orca_lg.png
new file mode 100644
index 00000000..5d6cb6fd
Binary files /dev/null and b/docs/assets/img/heroes/veba_otto_the_orca_lg.png differ
diff --git a/docs/assets/img/heroes/veba_otto_the_orca_md.png b/docs/assets/img/heroes/veba_otto_the_orca_md.png
new file mode 100644
index 00000000..d0767a1a
Binary files /dev/null and b/docs/assets/img/heroes/veba_otto_the_orca_md.png differ
diff --git a/docs/assets/img/heroes/veba_otto_the_orca_sm.png b/docs/assets/img/heroes/veba_otto_the_orca_sm.png
new file mode 100644
index 00000000..937944cc
Binary files /dev/null and b/docs/assets/img/heroes/veba_otto_the_orca_sm.png differ
diff --git a/docs/assets/img/icons/email.svg b/docs/assets/img/icons/email.svg
new file mode 100644
index 00000000..fc799a55
--- /dev/null
+++ b/docs/assets/img/icons/email.svg
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/assets/img/icons/slack.svg b/docs/assets/img/icons/slack.svg
new file mode 100644
index 00000000..5e3119cd
--- /dev/null
+++ b/docs/assets/img/icons/slack.svg
@@ -0,0 +1,13 @@
+
+
+
+ Bitmap
+ Created with Sketch.
+
+
+
+
\ No newline at end of file
diff --git a/docs/assets/img/icons/twitter.svg b/docs/assets/img/icons/twitter.svg
new file mode 100644
index 00000000..8d40e177
--- /dev/null
+++ b/docs/assets/img/icons/twitter.svg
@@ -0,0 +1,13 @@
+
+
+
+ Bitmap
+ Created with Sketch.
+
+
+
+
\ No newline at end of file
diff --git a/docs/assets/img/languages/golang.png b/docs/assets/img/languages/golang.png
new file mode 100644
index 00000000..0e7fa4fd
Binary files /dev/null and b/docs/assets/img/languages/golang.png differ
diff --git a/docs/assets/img/languages/powercli.png b/docs/assets/img/languages/powercli.png
new file mode 100644
index 00000000..5044d6b4
Binary files /dev/null and b/docs/assets/img/languages/powercli.png differ
diff --git a/docs/assets/img/languages/powershell.png b/docs/assets/img/languages/powershell.png
new file mode 100644
index 00000000..54d1e962
Binary files /dev/null and b/docs/assets/img/languages/powershell.png differ
diff --git a/docs/assets/img/languages/python.png b/docs/assets/img/languages/python.png
new file mode 100644
index 00000000..183e4628
Binary files /dev/null and b/docs/assets/img/languages/python.png differ
diff --git a/docs/assets/img/usecases/usecase1.svg b/docs/assets/img/usecases/usecase1.svg
new file mode 100644
index 00000000..cd53c57d
--- /dev/null
+++ b/docs/assets/img/usecases/usecase1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/assets/img/usecases/usecase2.svg b/docs/assets/img/usecases/usecase2.svg
new file mode 100644
index 00000000..2cd5ecaa
--- /dev/null
+++ b/docs/assets/img/usecases/usecase2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/assets/img/usecases/usecase3.svg b/docs/assets/img/usecases/usecase3.svg
new file mode 100644
index 00000000..bfc47d59
--- /dev/null
+++ b/docs/assets/img/usecases/usecase3.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/assets/img/veba-logo.png b/docs/assets/img/veba-logo.png
new file mode 100644
index 00000000..ced61b92
Binary files /dev/null and b/docs/assets/img/veba-logo.png differ
diff --git a/docs/assets/img/vm-logo.png b/docs/assets/img/vm-logo.png
new file mode 100644
index 00000000..c216caaa
Binary files /dev/null and b/docs/assets/img/vm-logo.png differ
diff --git a/docs/assets/img/vmware-white.svg b/docs/assets/img/vmware-white.svg
new file mode 100644
index 00000000..2e71d7a3
--- /dev/null
+++ b/docs/assets/img/vmware-white.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/assets/img/vmware.svg b/docs/assets/img/vmware.svg
new file mode 100644
index 00000000..d38b007c
--- /dev/null
+++ b/docs/assets/img/vmware.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/assets/js/day-night.js b/docs/assets/js/day-night.js
new file mode 100644
index 00000000..90082021
--- /dev/null
+++ b/docs/assets/js/day-night.js
@@ -0,0 +1,87 @@
+console.clear();
+
+let duration = 0.4;
+let isDay = true;
+
+
+let back = document.getElementById('back');
+let front = document.getElementById('front');
+
+let switchTime = () => {
+
+ back.setAttribute('href', '#' + (isDay ? 'day' : 'night'));
+ front.setAttribute('href', '#' + (isDay ? 'night' : 'day'));
+}
+let scale = 30;
+let toNightAnimation = gsap.timeline();
+
+toNightAnimation
+.to('#night-content', {duration: duration * 0.5, opacity: 1, ease: 'power2.inOut', x: 0})
+.to('#circle', {
+ duration: duration,
+ ease: 'power4.in',
+ scaleX: scale,
+ scaleY: scale,
+ x: 1,
+ transformOrigin: '100% 50%',
+}, 0)
+.to('.day-label', {duration: duration * 2, ease: 'power2.inOut', opacity: 0.6}, 0)
+.to('.night-label', {duration: duration * 2, ease: 'power2.inOut', opacity: 1}, 0)
+.set('#circle', {
+ // transformOrigin: '0% 50%',
+ scaleX:-scale,
+ // x: 8.5,
+ onUpdate: () => switchTime()
+}, duration).to('#circle', {
+ duration: duration,
+ ease: 'power4.out',
+ scaleX: -1,
+ scaleY: 1,
+ x: 2,
+}, duration)
+.to('#day-content', {duration: duration * 0.5, opacity: 0.5}, duration * 1.5)
+.to('body', {backgroundColor: '#656363', color: 'black', duration: duration * 2}, 0)
+.to('#otto', 0.1, {display:'none', autoAlpha: 0})
+.to('#otto-pride', 0.1 , {autoAlpha: 1, display:'block'})
+.to('.hero-content h1', {color: 'black', duration: duration * 2}, 0)
+.to('.section', {color: 'black', duration: duration * 2}, 0)
+.to('.site-container', {backgroundColor: '#bbb', duration: duration * 2}, 0)
+.to('.promo-cards .section-content', {backgroundColor: '#bbb', duration: duration * 2}, 0)
+.to('footer', {backgroundColor: '#fff', duration: duration * 2}, 0)
+.to('.alternating-cards .row', {backgroundColor: '#fff', duration: duration * 2}, 0)
+
+let stars = Array.from(document.getElementsByClassName('star'));
+stars.map(star => gsap.to(star, {duration: 'random(0.4, 1.5)', repeat: -1, yoyo: true, opacity: 'random(0.2, 0.5)'}))
+gsap.to('.clouds-big', {duration: 15, repeat: -1, x: -74, ease: 'linear'})
+gsap.to('.clouds-medium', {duration: 20, repeat: -1, x: -65, ease: 'linear'})
+gsap.to('.clouds-small', {duration: 25, repeat: -1, x: -71, ease: 'linear'})
+
+let switchToggle = document.getElementById('darkmodeinput');
+switchToggle.addEventListener('change', () => toggle())
+
+let toggle = () =>
+{
+ isDay = switchToggle.checked == true;
+ if (isDay) {
+ toNightAnimation.reverse();
+ localStorage.setItem("darkmode",'day');
+ $('div#arc').removeClass('flicker');
+ $('div#reactor').removeClass('flicker');
+ } else {
+ toNightAnimation.play();
+ localStorage.setItem("darkmode",'night');
+ $('div#arc').addClass('flicker');
+ $('div#reactor').addClass('flicker');
+ }
+}
+
+toNightAnimation.reverse();
+toNightAnimation.pause();
+
+if (localStorage.getItem("darkmode") === 'night'){
+ isDay = false;
+ switchToggle.checked = false;
+ toNightAnimation.play();
+ $('div#arc').addClass('flicker');
+ $('div#reactor').addClass('flicker');
+}
\ No newline at end of file
diff --git a/docs/assets/js/jquery.matchHeight.js b/docs/assets/js/jquery.matchHeight.js
new file mode 100644
index 00000000..03f6b14b
--- /dev/null
+++ b/docs/assets/js/jquery.matchHeight.js
@@ -0,0 +1,388 @@
+/**
+* jquery-match-height master by @liabru
+* http://brm.io/jquery-match-height/
+* License: MIT
+*/
+
+; (function (factory) { // eslint-disable-line no-extra-semi
+ 'use strict';
+ if (typeof define === 'function' && define.amd) {
+ // AMD
+ define(['jquery'], factory);
+ } else if (typeof module !== 'undefined' && module.exports) {
+ // CommonJS
+ module.exports = factory(require('jquery'));
+ } else {
+ // Global
+ factory(jQuery);
+ }
+})(function ($) {
+ /*
+ * internal
+ */
+
+ var _previousResizeWidth = -1,
+ _updateTimeout = -1;
+
+ /*
+ * _parse
+ * value parse utility function
+ */
+
+ var _parse = function (value) {
+ // parse value and convert NaN to 0
+ return parseFloat(value) || 0;
+ };
+
+ /*
+ * _rows
+ * utility function returns array of jQuery selections representing each row
+ * (as displayed after float wrapping applied by browser)
+ */
+
+ var _rows = function (elements) {
+ var tolerance = 1,
+ $elements = $(elements),
+ lastTop = null,
+ rows = [];
+
+ // group elements by their top position
+ $elements.each(function () {
+ var $that = $(this),
+ top = $that.offset().top - _parse($that.css('margin-top')),
+ lastRow = rows.length > 0 ? rows[rows.length - 1] : null;
+
+ if (lastRow === null) {
+ // first item on the row, so just push it
+ rows.push($that);
+ } else {
+ // if the row top is the same, add to the row group
+ if (Math.floor(Math.abs(lastTop - top)) <= tolerance) {
+ rows[rows.length - 1] = lastRow.add($that);
+ } else {
+ // otherwise start a new row group
+ rows.push($that);
+ }
+ }
+
+ // keep track of the last row top
+ lastTop = top;
+ });
+
+ return rows;
+ };
+
+ /*
+ * _parseOptions
+ * handle plugin options
+ */
+
+ var _parseOptions = function (options) {
+ var opts = {
+ byRow: true,
+ property: 'height',
+ target: null,
+ remove: false
+ };
+
+ if (typeof options === 'object') {
+ return $.extend(opts, options);
+ }
+
+ if (typeof options === 'boolean') {
+ opts.byRow = options;
+ } else if (options === 'remove') {
+ opts.remove = true;
+ }
+
+ return opts;
+ };
+
+ /*
+ * matchHeight
+ * plugin definition
+ */
+
+ var matchHeight = $.fn.matchHeight = function (options) {
+ var opts = _parseOptions(options);
+
+ // handle remove
+ if (opts.remove) {
+ var that = this;
+
+ // remove fixed height from all selected elements
+ this.css(opts.property, '');
+
+ // remove selected elements from all groups
+ $.each(matchHeight._groups, function (key, group) {
+ group.elements = group.elements.not(that);
+ });
+
+ // TODO: cleanup empty groups
+
+ return this;
+ }
+
+ if (this.length <= 1 && !opts.target) {
+ return this;
+ }
+
+ // keep track of this group so we can re-apply later on load and resize events
+ matchHeight._groups.push({
+ elements: this,
+ options: opts
+ });
+
+ // match each element's height to the tallest element in the selection
+ matchHeight._apply(this, opts);
+
+ return this;
+ };
+
+ /*
+ * plugin global options
+ */
+
+ matchHeight.version = 'master';
+ matchHeight._groups = [];
+ matchHeight._throttle = 80;
+ matchHeight._maintainScroll = false;
+ matchHeight._beforeUpdate = null;
+ matchHeight._afterUpdate = null;
+ matchHeight._rows = _rows;
+ matchHeight._parse = _parse;
+ matchHeight._parseOptions = _parseOptions;
+
+ /*
+ * matchHeight._apply
+ * apply matchHeight to given elements
+ */
+
+ matchHeight._apply = function (elements, options) {
+ var opts = _parseOptions(options),
+ $elements = $(elements),
+ rows = [$elements];
+
+ // take note of scroll position
+ var scrollTop = $(window).scrollTop(),
+ htmlHeight = $('html').outerHeight(true);
+
+ // get hidden parents
+ var $hiddenParents = $elements.parents().filter(':hidden');
+
+ // cache the original inline style
+ $hiddenParents.each(function () {
+ var $that = $(this);
+ $that.data('style-cache', $that.attr('style'));
+ });
+
+ // temporarily must force hidden parents visible
+ $hiddenParents.css('display', 'block');
+
+ // get rows if using byRow, otherwise assume one row
+ if (opts.byRow && !opts.target) {
+
+ // must first force an arbitrary equal height so floating elements break evenly
+ $elements.each(function () {
+ var $that = $(this),
+ display = $that.css('display');
+
+ // temporarily force a usable display value
+ if (display !== 'inline-block' && display !== 'flex' && display !== 'inline-flex') {
+ display = 'block';
+ }
+
+ // cache the original inline style
+ $that.data('style-cache', $that.attr('style'));
+
+ $that.css({
+ 'display': display,
+ 'padding-top': '0',
+ 'padding-bottom': '0',
+ 'margin-top': '0',
+ 'margin-bottom': '0',
+ 'border-top-width': '0',
+ 'border-bottom-width': '0',
+ 'height': '100px',
+ 'overflow': 'hidden'
+ });
+ });
+
+ // get the array of rows (based on element top position)
+ rows = _rows($elements);
+
+ // revert original inline styles
+ $elements.each(function () {
+ var $that = $(this);
+ $that.attr('style', $that.data('style-cache') || '');
+ });
+ }
+
+ $.each(rows, function (key, row) {
+ var $row = $(row),
+ targetHeight = 0;
+
+ if (!opts.target) {
+ // skip apply to rows with only one item
+ if (opts.byRow && $row.length <= 1) {
+ $row.css(opts.property, '');
+ return;
+ }
+
+ // iterate the row and find the max height
+ $row.each(function () {
+ var $that = $(this),
+ style = $that.attr('style'),
+ display = $that.css('display');
+
+ // temporarily force a usable display value
+ if (display !== 'inline-block' && display !== 'flex' && display !== 'inline-flex') {
+ display = 'block';
+ }
+
+ // ensure we get the correct actual height (and not a previously set height value)
+ var css = { 'display': display };
+ css[opts.property] = '';
+ $that.css(css);
+
+ // find the max height (including padding, but not margin)
+ if ($that.outerHeight(false) > targetHeight) {
+ targetHeight = $that.outerHeight(false);
+ }
+
+ // revert styles
+ if (style) {
+ $that.attr('style', style);
+ } else {
+ $that.css('display', '');
+ }
+ });
+ } else {
+ // if target set, use the height of the target element
+ targetHeight = opts.target.outerHeight(false);
+ }
+
+ // iterate the row and apply the height to all elements
+ $row.each(function () {
+ var $that = $(this),
+ verticalPadding = 0;
+
+ // don't apply to a target
+ if (opts.target && $that.is(opts.target)) {
+ return;
+ }
+
+ // handle padding and border correctly (required when not using border-box)
+ if ($that.css('box-sizing') !== 'border-box') {
+ verticalPadding += _parse($that.css('border-top-width')) + _parse($that.css('border-bottom-width'));
+ verticalPadding += _parse($that.css('padding-top')) + _parse($that.css('padding-bottom'));
+ }
+
+ // set the height (accounting for padding and border)
+ $that.css(opts.property, (targetHeight - verticalPadding) + 'px');
+ });
+ });
+
+ // revert hidden parents
+ $hiddenParents.each(function () {
+ var $that = $(this);
+ $that.attr('style', $that.data('style-cache') || null);
+ });
+
+ // restore scroll position if enabled
+ if (matchHeight._maintainScroll) {
+ $(window).scrollTop((scrollTop / htmlHeight) * $('html').outerHeight(true));
+ }
+
+ return this;
+ };
+
+ /*
+ * matchHeight._applyDataApi
+ * applies matchHeight to all elements with a data-match-height attribute
+ */
+
+ matchHeight._applyDataApi = function () {
+ var groups = {};
+
+ // generate groups by their groupId set by elements using data-match-height
+ $('[data-match-height], [data-mh]').each(function () {
+ var $this = $(this),
+ groupId = $this.attr('data-mh') || $this.attr('data-match-height');
+
+ if (groupId in groups) {
+ groups[groupId] = groups[groupId].add($this);
+ } else {
+ groups[groupId] = $this;
+ }
+ });
+
+ // apply matchHeight to each group
+ $.each(groups, function () {
+ this.matchHeight(true);
+ });
+ };
+
+ /*
+ * matchHeight._update
+ * updates matchHeight on all current groups with their correct options
+ */
+
+ var _update = function (event) {
+ if (matchHeight._beforeUpdate) {
+ matchHeight._beforeUpdate(event, matchHeight._groups);
+ }
+
+ $.each(matchHeight._groups, function () {
+ matchHeight._apply(this.elements, this.options);
+ });
+
+ if (matchHeight._afterUpdate) {
+ matchHeight._afterUpdate(event, matchHeight._groups);
+ }
+ };
+
+ matchHeight._update = function (throttle, event) {
+ // prevent update if fired from a resize event
+ // where the viewport width hasn't actually changed
+ // fixes an event looping bug in IE8
+ if (event && event.type === 'resize') {
+ var windowWidth = $(window).width();
+ if (windowWidth === _previousResizeWidth) {
+ return;
+ }
+ _previousResizeWidth = windowWidth;
+ }
+
+ // throttle updates
+ if (!throttle) {
+ _update(event);
+ } else if (_updateTimeout === -1) {
+ _updateTimeout = setTimeout(function () {
+ _update(event);
+ _updateTimeout = -1;
+ }, matchHeight._throttle);
+ }
+ };
+
+ /*
+ * bind events
+ */
+
+ // apply on DOM ready event
+ $(matchHeight._applyDataApi);
+
+ // use on or bind where supported
+ var on = $.fn.on ? 'on' : 'bind';
+
+ // update heights on load and resize events
+ $(window)[on]('load', function (event) {
+ matchHeight._update(false, event);
+ });
+
+ // throttled update heights on resize events
+ $(window)[on]('resize orientationchange', function (event) {
+ matchHeight._update(true, event);
+ });
+
+});
\ No newline at end of file
diff --git a/docs/assets/js/scripts.js b/docs/assets/js/scripts.js
new file mode 100644
index 00000000..12d502a6
--- /dev/null
+++ b/docs/assets/js/scripts.js
@@ -0,0 +1,72 @@
+(function () {
+
+ $('.match-height').matchHeight();
+
+ $('#nav-mobile-toggle').click(function () {
+ $('#header-nav').toggleClass('on');
+ });
+
+ $('ul#header-nav li.' + $('body').attr('id')).addClass('selected');
+
+ $('nav#toc-nav ul li.' + $('div.documentation-container').attr('id')).addClass('selected');
+
+ if (localStorage.getItem("darkmode") === 'night'){
+ $('div.highlighter-rouge').addClass('highlighter-rouge-dark');
+ }
+
+ function addCopyButtons(clipboard) {
+ document.querySelectorAll('pre > code').forEach(function (codeBlock) {
+ var button = document.createElement('div');
+ button.className = 'copy-code-button';
+ button.type = 'button';
+ button.innerHTML = 'Copy ';
+
+ button.addEventListener('click', function () {
+ clipboard.writeText(codeBlock.innerText).then(function () {
+ /* Chrome doesn't seem to blur automatically,
+ leaving the button in a focused state. */
+ button.blur();
+
+ button.innerHTML = 'Copied ';
+
+ setTimeout(function () {
+ button.innerHTML = 'Copy ';
+ }, 1000);
+ }, function (error) {
+ button.innerText = 'Error';
+ });
+ });
+
+ var pre = codeBlock.parentNode;
+ if (pre.parentNode.classList.contains('highlight')) {
+ var highlight = pre.parentNode;
+ highlight.parentNode.insertBefore(button, highlight);
+ } else {
+ pre.parentNode.insertBefore(button, pre);
+ }
+ });
+ }
+
+ if (navigator && navigator.clipboard) {
+ addCopyButtons(navigator.clipboard);
+ } else {
+ var script = document.createElement('script');
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/clipboard-polyfill/2.7.0/clipboard-polyfill.promise.js';
+ script.integrity = 'sha256-waClS2re9NUbXRsryKoof+F9qc1gjjIhc2eT7ZbIv94=';
+ script.crossOrigin = 'anonymous';
+ script.onload = function() {
+ addCopyButtons(clipboard);
+ };
+
+ document.body.appendChild(script);
+ }
+
+ $('a').click(function() {
+ ga('send', 'event', {
+ eventCategory: 'Click',
+ eventAction: this.href,
+ eventLabel: this.innerText
+ });
+ });
+
+})();
\ No newline at end of file
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 00000000..c1ba8088
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,203 @@
+---
+layout: default
+id: home
+description: vCenter Event-driven Functions as a Service Platform
+backgrounds:
+ case_study: cornflower-blue
+ team: dark-blue
+hero:
+ headline: Unlocking the Hidden Potential of Events in the VMware SDDC
+ content: Use event-driven automation and take your vSphere Events to the next level! Easily trigger custom or pre-built actions to deliver powerful integrations within your datacenter but also across public cloud services. Integrations like Slack, Pager Duty, Service Now, etc. has never been easier before
+ cta_link1:
+ text: Download & Install
+ external_url: https://flings.vmware.com/vcenter-event-broker-appliance
+ cta_link2:
+ text: Deploy Functions
+ url: /examples
+ cta_link3: # ignored
+ text: Write Functions
+ external_url: https://github.com/vmware-samples/vcenter-event-broker-appliance
+ promo1:
+ icon: /assets/img/cta-icons/vcenter-icon.png
+ title: Extend vCenter Actions
+ content: Simplify integrations and deliver new automated capabilities to vCenter
+ promo2:
+ icon: /assets/img/cta-icons/scale-icon.png
+ title: Event-Driven Functions
+ content: Imagine new possibilities with functions triggered by vSphere Events.
+ promo3:
+ icon: /assets/img/cta-icons/functions-icon.png
+ title: Bring Your Own Language
+ content: Build functions using the language of your choice
+secondary_ctas:
+ cta1:
+ title: Explore our documentation to help you get started quickly
+ url: /kb
+ content: Learn more about VMware Event Broker Appliance and its capabilities
+ cta2:
+ title: Get started quickly with community-sourced, prebuilt functions
+ url: /examples
+ content: Deploy the appliance and prebuilt functions in under 60 minutes
+---
+
+
+
+
+
+
+
+
{{ page.hero.headline }}
+
{{ page.hero.content }}
+
+ {% include day-night.html%}
+
+
+
+
+
+ {% include arc-reactor.html%}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ page.secondary_ctas.cta1.content }}
+
+
+
+
{{ page.secondary_ctas.cta2.content }}
+
+
+
+
+
+
+
+
+
#VEBA Use Cases
+
+ {% assign limit = 6 %}
+ {% include usecase-posts.html %}
+
+
+
+
+
+
+
+
Featured Functions
+
+ {% assign limit = 3 %}
+ {% include functions-alternating.html %}
+
+
+
+
+
+
+ {% include team.html %}
+
+
\ No newline at end of file
diff --git a/docs/kb/advanced-certificates.md b/docs/kb/advanced-certificates.md
new file mode 100644
index 00000000..fca8bd4a
--- /dev/null
+++ b/docs/kb/advanced-certificates.md
@@ -0,0 +1,41 @@
+---
+layout: docs
+toc_id: advanced-certificates
+title: VMware Event Broker Appliance - Certificates
+description: Updating Certificates
+permalink: /kb/advanced-certificates
+cta:
+ description: With your certificates updated, you can now skip using the `--tls-no-verify` flag while working with faas-cli.
+---
+
+## Updating the TLS Certificate on VEBA
+
+The default certificate for OpenFaaS (/ui) or the EventBridge (/stats) and the other web endpoints running on VEBA are self signed. This might cause browsers to show the certificate as untrusted and would require providing the `--no-tls-verify` flag when working with faas-cli.
+
+In order to update the certificates with a certificate from a trusted authority, please follow the steps outlined below
+
+### Assumptions
+
+* Access to VMware Event Broker Appliance terminal
+* Certificates from a trusted authority pre-downloaded onto the Appliance
+ * The public/private key pair must exist before hand. The public key certificate must be .PEM encoded and match the given private key.
+
+### Steps
+
+Run the below commands to update the certificate on VEBA
+
+```bash
+cd /folder/certs/location
+CERT_NAME=eventrouter-tls #DO NOT CHANGE THIS
+KEY_FILE=.pem
+CERT_FILE=.cer
+
+#recreate the tls secret
+kubectl --kubeconfig /root/.kube/config -n vmware delete secret ${CERT_NAME}
+kubectl --kubeconfig /root/.kube/config -n vmware create secret tls ${CERT_NAME} --key ${KEY_FILE} --cert ${CERT_FILE}
+
+#reapply the config to take the new certificate
+kubectl --kubeconfig /root/.kube/config apply -f /root/config/ingressroute-gateway.yaml
+```
+
+Watch this short video to see the steps being performed to successfully update the certs for VEBA configured for OpenFaaS - [here](https://youtu.be/7oMCvxvL2ns){:target="_blank"}
diff --git a/docs/kb/advanced-deploy-k8s.md b/docs/kb/advanced-deploy-k8s.md
new file mode 100644
index 00000000..c9d96041
--- /dev/null
+++ b/docs/kb/advanced-deploy-k8s.md
@@ -0,0 +1,129 @@
+---
+layout: docs
+toc_id: advanced-deploy-k8s
+title: VMware Event Broker Appliance - Event Router Standalone
+description: Standalone Deployment of Event Router
+permalink: /kb/advanced-deploy-k8s
+cta:
+ title: Deploy a Function
+ description: At this point, you have successfully deployed the VMware Event Broker to Kubernetes! You are almost there..
+ actions:
+ - text: Deploy OpenFaaS to your Kubernetes - [guide](https://docs.openfaas.com/deployment/kubernetes/){:target="blank"}
+ - text: Deploy a Function - [here](use-functions).
+---
+
+# Deploy vCenter Event Broker Application to existing Kubernetes Cluster
+
+For customers with an existing Kubernetes ("K8s") cluster, you can deploy the underlying components that make up the vCenter Event Broker Appliance. The instructions below will guide you in downloading the required files and using the `create_k8s_config.sh` [shell script](https://github.com/vmware-samples/vcenter-event-broker-appliance/blob/development/vmware-event-router/hack/create_k8s_config.sh) to aide in deploying the VEBA K8s application.
+
+The script will prompt users for the required input and automatically setup and deploy both OpenFaaS and the VMware Event Router components giving you a similar setup like the vCenter Event Broke Appliance. If you have already deployed OpenFaaS, you can skip that step during the script input phase.
+
+## Pre-Req:
+* Ability to create namespaces, secrets and deployments in your K8s Cluster using kubectl
+* Outbound connectivity or access to private registry from the K8s Cluster to download the required containers to deploy OpenFaaS and/or VMware Event Router
+
+## Deploy VMware Event Router and OpenFaaS
+
+### Install
+
+Step 1 - Clone the OpenFaaS to your local system
+
+```
+git clone https://github.com/openfaas/faas-netes
+```
+
+Step 2 - Change into the `faas-netes` directory and checkout version `0.9.2` which has been tested with VEBA and then change back to previous working directory.
+
+
+```
+cd faas-netes
+git checkout 0.9.2
+cd ..
+```
+
+Step 3 - Download the `create_k8s_config.sh` script and ensure it has executable permission (`chmod +x create_k8s_config.sh`).
+
+```
+git clone https://github.com/vmware-samples/vcenter-event-broker-appliance
+cd vcenter-event-broker-appliance/vmware-event-router/hack
+chmod +x create_k8s_config.sh
+```
+
+Step 4 - Run the `create_k8s_config.sh` script which will prompt for vCenter Server address (FQDN/IP Address), the vCenter Server username and password which is authorized to retrieve vCenter Server Events (readOnly role is sufficient) and the admin password for OpenFaaS. Prior to deploying, you will be asked to confirm the input in case you need to change it.
+
+```
+./create_k8s_config.sh
+```
+
+Here is an example of what you should see if the deployment was successful:
+
+{:width="100%"}
+
+Step 5 - Ensure that all pods are running in both OpenFaaS and VMware namespace:
+
+```
+# kubectl get pods -n openfaas
+NAME READY STATUS RESTARTS AGE
+alertmanager-bdf9db7b9-ldwkz 1/1 Running 0 27s
+basic-auth-plugin-665bf4d59b-f87rm 1/1 Running 0 27s
+faas-idler-f4597f655-pr5tq 1/1 Running 0 27s
+gateway-cdf7b89fb-7589b 2/2 Running 1 27s
+nats-8455bfbb58-j4wpm 1/1 Running 0 27s
+prometheus-688d9cfbf7-wkvc9 1/1 Running 0 26s
+queue-worker-649bdf958f-k55g2 1/1 Running 0 27s
+```
+
+
+```
+# kubectl get pods -n vmware
+NAME READY STATUS RESTARTS AGE
+vmware-event-router-6744cc6447-xbpmn 1/1 Running 1 42s
+```
+
+To retrieve the OpenFaaS Gateway IP Address for function deployment, run the following command:
+
+```
+kubectl -n openfaas describe pods $(kubectl -n openfaas get pods | grep "gateway-" | awk '{print $1}') | grep "^Node:" | awk -F "/" '{print $2}'
+```
+
+**Note:** If you don't use an Ingress controller, load-balancer or other means to expose your Kubernetes deployments (services), then the default OpenFaaS endpoint is `http://:31112`
+
+### Uninstall
+
+To remove the VEBA and OpenFaaS K8s application, run the following commands:
+
+```
+kubectl delete ns vmware
+kubectl delete -f faas-netes/yaml
+kubectl delete -f faas-netes/namespaces.yml
+```
+
+## Deploy only VMware Event Router
+
+Step 1 - Download the `create_k8s_config.sh` script and ensure it has executable permission (`chmod +x create_k8s_config.sh`).
+
+Step 2 - Run the `create_k8s_config.sh` script which will prompt for vCenter Server address (FQDN/IP Address), the vCenter Server username and password which is authorized to retrieve vCenter Server Events (readOnly role is sufficient). Prior to deploying, you will be asked to confirm the input in case you need to change it.
+
+```
+./create_k8s_config.sh
+```
+
+Here is an example of what you should see if the deployment was successful:
+
+{:width="100%"}
+
+Step 3 - Ensure the VMware Event Router pod is running in the VMware namespace:
+
+```
+# kubectl get pods -n vmware
+NAME READY STATUS RESTARTS AGE
+vmware-event-router-6744cc6447-xbpmn 1/1 Running 1 42s
+```
+
+## Uninstall:
+
+To remove the VMware Event Router K8s application, run the following command:
+
+```
+kubectl delete ns vmware
+```
\ No newline at end of file
diff --git a/docs/kb/contribute-appliance.md b/docs/kb/contribute-appliance.md
new file mode 100644
index 00000000..13618cf2
--- /dev/null
+++ b/docs/kb/contribute-appliance.md
@@ -0,0 +1,50 @@
+---
+layout: docs
+toc_id: contribute-appliance
+title: Building the VMware Event Broker Appliance
+description: Building the VMware Event Broker Appliance
+permalink: /kb/contribute-appliance
+cta:
+ title: Have a question?
+ description: Please check our [Frequently Asked Questions](/faq) first.
+---
+
+## Getting Started Build Guide for VMware Event Broker Appliance
+
+## Requirements
+
+* 2 vCPU and 8GB of memory for VMware Event Broker Appliance
+* vCenter Server or Standalone ESXi host 6.x or greater
+* [VMware OVFTool](https://www.vmware.com/support/developer/ovf/){:target="_blank"}
+* [Docker Client](https://docs.docker.com/v17.09/engine/installation/){:target="_blank"}
+* [OpenFaaS CLI](https://github.com/openfaas/faas-cli){:target="_blank"}
+* [Packer](https://www.packer.io/intro/getting-started/install.html){:target="_blank"}
+
+
+Step 1 - Clone the VMware Event Broker Appliance Git repository
+
+```
+git clone https://github.com/vmware-samples/vcenter-event-broker-appliance.git
+```
+
+Step 2 - Edit the `photon-builder.json` file to configure the vSphere endpoint for building the VMware Event Broker Appliance
+
+```
+{
+ "builder_host": "192.168.30.10",
+ "builder_host_username": "root",
+ "builder_host_password": "VMware1!",
+ "builder_host_datastore": "vsanDatastore",
+ "builder_host_portgroup": "VM Network"
+}
+```
+
+> **Note:** If you need to change the default root password on the VMware Event Broker Appliance, take a look at `photon-version.json`
+
+Step 3 - Start the build by running the build script
+
+```
+./build.sh
+````
+
+If you wish to automatically deploy the VMware Event Broker Appliance after successfully building the OVA, please take a look at the script samples located in the test directory.
diff --git a/docs/kb/contribute-eventrouter.md b/docs/kb/contribute-eventrouter.md
new file mode 100644
index 00000000..6e769a39
--- /dev/null
+++ b/docs/kb/contribute-eventrouter.md
@@ -0,0 +1,409 @@
+---
+layout: docs
+toc_id: contribute-eventrouter
+title: Building the Event Router
+description: Building the Event Router
+permalink: /kb/contribute-eventrouter
+cta:
+ title: Have a question?
+ description: Please check our [Frequently Asked Questions](/faq) first.
+---
+
+# Build VMware Event Router from Source
+
+Requirements: This project uses [Golang](https://golang.org/dl/) and Go [modules](https://blog.golang.org/using-go-modules){:target="_blank"}. For convenience a Makefile and Dockerfile are provided requiring `make` and [Docker](https://www.docker.com/){:target="_blank"} to be installed as well.
+
+```bash
+git clone https://github.com/vmware-samples/vcenter-event-broker-appliance
+cd vcenter-event-broker-appliance/vmware-event-router
+
+# for Go versions before v1.13
+export GO111MODULE=on
+
+# defaults to build with Docker (use make binary for local executable instead)
+make
+```
+
+
+# VMware Event Router
+
+The VMware Event Router is used to connect to various VMware event `streams` (i.e. "sources") and forward these events to different `processors` (i.e. "sinks"). This project is currently used by the [*VMware Event Broker Appliance*](https://github.com/vmware-samples/vcenter-event-broker-appliance){:target="_blank"} as the core logic to forward vCenter events to configurable event `processors` (see below).
+
+**Supported event sources:**
+- [VMware vCenter Server](https://www.vmware.com/products/vcenter-server.html){:target="_blank"}
+
+**Supported event processors:**
+- [OpenFaaS](https://www.openfaas.com/){:target="_blank"}
+- [AWS EventBridge](https://aws.amazon.com/eventbridge/?nc1=h_ls){:target="_blank"}
+
+The VMware Event Router uses the [CloudEvents](https://cloudevents.io/){:target="_blank"} standard to format events from the supported `stream` providers in JSON. See [below](#example-event-structure) for an example.
+
+**Current limitations:**
+
+- Only one event `stream` and one event `processor` can be configured at a time
+ - It is possible though to run **multiple instances** of the event router
+- At-most-once delivery semantics are provided
+ - See [this FAQ](https://github.com/vmware-samples/vcenter-event-broker-appliance/blob/development/FAQ.md) for a deeper understanding of messaging semantics
+
+
+## Table of Contents
+- [Usage and Configuration](#usage-and-configuration)
+ - [Event Stream Provider and Processor Configuration Options](#event-stream-provider-and-processor-configuration-options)
+ - [Stream Provider: Configuration Details for VMware vCenter Server](#stream-provider-configuration-details-for-vmware-vcenter-server)
+ - [Stream Processor: Configuration Details for OpenFaaS](#stream-processor-configuration-details-for-openfaas)
+ - [Stream Processor: Configuration Details for AWS EventBridge](#stream-processor-configuration-details-for-aws-eventbridge)
+ - [Metrics Server: Configuration Details](#metrics-server-configuration-details)
+ - [Deployment](#deployment)
+- [Build from Source](#build-from-source)
+- [Example Event Structure](#example-event-structure)
+
+## Usage and Configuration
+
+The VMware Event Router can be run standalone (statically linked binary) or deployed as a Docker container, e.g. in a Kubernetes environment. See [deployment](#deployment) for further instructions. The configuration of event `stream` providers and `processors` and other internal components (such as metrics) is done via a JSON file passed in via the `"-config"` command line flag.
+
+```
+ _ ____ ___ ______ __ ____ __
+| | / / |/ / ______ _________ / ____/ _____ ____ / /_ / __ \____ __ __/ /____ _____
+| | / / /|_/ / | /| / / __ / ___/ _ \ / __/ | | / / _ \/ __ \/ __/ / /_/ / __ \/ / / / __/ _ \/ ___/
+| |/ / / / /| |/ |/ / /_/ / / / __/ / /___ | |/ / __/ / / / /_ / _, _/ /_/ / /_/ / /_/ __/ /
+|___/_/ /_/ |__/|__/\__,_/_/ \___/ /_____/ |___/\___/_/ /_/\__/ /_/ |_|\____/\__,_/\__/\___/_/
+
+
+Usage of ./vmware-event-router:
+
+ -config string
+ path to configuration file for metrics, stream source and processor (default "/etc/vmware-event-router/config")
+ -verbose
+ print event handling information
+
+commit:
+version:
+```
+
+The following sections describe the layout of the configuration file (JSON) and specific options for the event `stream` provider, `processor` and `metrics` server. A correct configuration file requires `stream`, `processor` and `metrics` to be defined. Configuration examples are provided [here](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/vmware-event-router/deploy){:target="_blank"}.
+
+> **Note:** Currently only one event `stream` (i.e. one vCenter Server) and one event `processor` can be configured at a time, e.g. one vCenter Server instance streaming events to OpenFaaS **or** AWS EventBridge. Specifying multiple instances of the same provider will lead to unintended behavior.
+
+### Event Stream Provider and Processor Configuration Options
+
+The following table lists allowed fields with their respective value types in the JSON configuration file. Detailed instructions for the specific event `stream` providers, `processors` and `metrics` are described in dedicated sections further below.
+
+| Field | Value | Description | Example |
+|----------|-------------------|------------------------------------------------|---------------------------------------------------------------------------------------------------|
+| type | string | event stream, processor or internal | "type": "stream" |
+| provider | string | identifier of stream, processor or metrics | "provider": "vmware_vcenter" |
+| address | string | URI of the provider (when required) | "address": "https://10.0.0.1:443/sdk" |
+| auth | map[string]string | authentication options for the type provider | "auth": { "method":"user_password","secret": {...}} **Note: see provider specific options below** |
+| options | map[string]string | provider specific options (see sections below) | "options":{"insecure": "true"} |
+
+> **Note:** Besides event `stream` providers and `processors` the configuration file is also used for router-internal components, such as metrics (and likely others in the future). The `type: internal` is reserved for these use cases.
+
+### Stream Provider: Configuration Details for VMware vCenter Server
+
+The following table lists allowed and optional fields for using VMware vCenter Server as an event `stream` provider.
+
+| Field | Value | Description |
+|----------------------|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
+| type | "stream" | VMware vCenter is an event **stream** provider. |
+| provider | "vmware_vcenter" | Use this exact value to use VMware vCenter Server as a provider. |
+| address | "https://10.0.0.1:443/sdk" | URI of the VMware vCenter Server (IP or FQDN incl. "<:PORT>/sdk"). |
+| auth.method | "user_password" | Use this exact value. Only username/password are supported to authenticate against VMware vCenter Server. |
+| auth.secret.username | "administrator@vsphere.local" | Replace with user/service account to use for connecting to this vCenter event stream. |
+| auth.secret.password | "REPLACE_ME" | Replace with password for the given user/service account to use for connecting to this vCenter event stream. |
+| options.insecure | "true" | Ignore TLS certificate warnings. **Note:** must use quotes around this value (is of type string). Default: "false". (optional) |
+
+Example of the configuration section for VMware vCenter Server:
+
+```json
+{
+ "type": "stream",
+ "provider": "vmware_vcenter",
+ "address": "https://10.0.0.1:443/sdk",
+ "auth": {
+ "method": "user_password",
+ "secret": {
+ "username": "administrator@vsphere.local",
+ "password": "REPLACE_ME"
+ }
+ },
+ "options": {
+ "insecure": "true"
+ }
+}
+```
+
+> **Note:** The JSON configuration file is an array of maps, ie. "[{},{}]". The snippet above is trimmed for readability. The examples provided [here](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/vmware-event-router/deploy){:target="_blank"} are properly formatted.
+
+### Stream Processor: Configuration Details for OpenFaaS
+
+OpenFaaS functions can subscribe to the event stream via function `"topic"` annotations in the function stack configuration (see OpenFaaS documentation for details on authoring functions), e.g.:
+
+```yaml
+annotations:
+ topic: "VmPoweredOnEvent,VmPoweredOffEvent"
+```
+
+> **Note:** One or more event categories can be specified, delimited via `","`. A list of event names (categories) and how to retrieve them can be found [here](https://github.com/lamw/vcenter-event-mapping/blob/master/vsphere-6.7-update-3.md){:target="_blank"}. A simple "echo" function useful for testing is provided [here](https://github.com/embano1/of-echo/blob/master/echo.yml){:target="_blank"}.
+
+The following table lists allowed and optional fields for using OpenFaaS as an event stream `processor`.
+
+| Field | Value | Description |
+|----------------------|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| type | "processor" | OpenFaaS is an event stream **processor**. |
+| provider | "openfaas" | Use this exact value to use OpenFaaS as a provider. |
+| address | "http://gateway.openfaas:8080" | URI of the OpenFaaS gateway (IP or FQDN incl. "<:PORT>"). |
+| auth.method | "basic_auth" | Use this exact value. Only `"basic_auth"` is supported to authenticate against OpenFaaS (must use authentication). |
+| auth.secret.username | "admin" | Replace with OpenFaaS gateway admin user name (is "admin" unless changed during gateway deployment). |
+| auth.secret.password | "REPLACE_ME" | Replace with password for the given admin account to use for connecting to the OpenFaaS gateway. |
+| options.async | "true" | Use `"async"` function invocation against the OpenFaaS gateway. **Note:** must use quotes around this value (is of type string). Default: "false". (optional) |
+
+Example of the configuration section for OpenFaaS:
+
+```json
+{
+ "type": "processor",
+ "provider": "openfaas",
+ "address": "http://gateway.openfaas:8080",
+ "auth": {
+ "method": "basic_auth",
+ "secret": {
+ "username": "admin",
+ "password": "REPLACE_ME"
+ }
+ },
+ "options": {
+ "async": "false"
+ }
+}
+```
+
+> **Note:** The JSON configuration file is an array of maps, ie. "[{},{}]". The snippet above is trimmed for readability. The examples provided [here](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/vmware-event-router/deploy){:target="_blank"} are properly formatted.
+
+### Stream Processor: Configuration Details for AWS EventBridge
+
+Amazon EventBridge is a serverless event bus that makes it easy to connect applications together using data from your own applications, integrated Software-as-a-Service (SaaS) applications, and AWS services. In order to reduce bandwidth and costs (number of events ingested, see [pricing](https://aws.amazon.com/eventbridge/pricing/){:target="_blank"}), VMware Event Router only forwards events configured in the associated `rule` of an event bus. Rules in AWS EventBridge use pattern matching ([docs](https://docs.aws.amazon.com/eventbridge/latest/userguide/filtering-examples-structure.html){:target="_blank"}). Upon start, VMware Event Router contacts EventBridge (using the given IAM role) to parse and extract event categories from the configured rule ARN (see configuration option below).
+
+The VMware Event Router uses the `"subject"` field in the event payload to store the event category, e.g. `"VmPoweredOnEvent"`. Thus it is required that you use a **specific pattern match** (`"detail->subject"`) that the VMware Event Router can parse to retrieve the desired event (forwarding) categories. For example, the following AWS EventBridge event pattern rule matches power on/off events (including DRS-enabled clusters):
+
+```json
+{
+ "detail": {
+ "subject": [
+ "VmPoweredOnEvent",
+ "VmPoweredOffEvent",
+ "DrsVmPoweredOnEvent"
+ ]
+ }
+}
+```
+
+`"subject"` can contain one or more event categories. Wildcards (`"*"`) are not supported. If one wants to modify the event pattern match rule **after** deploying the VMware Event Router, its internal rules cache is periodically synchronized with AWS EventBridge at a fixed interval of 5 minutes.
+
+> **Note:** A list of event names (categories) and how to retrieve them can be found [here](https://github.com/lamw/vcenter-event-mapping/blob/master/vsphere-6.7-update-3.md){:target="_blank"}.
+
+The following table lists allowed and optional fields for using AWS EventBridge as an event stream `processor`.
+
+| Field | Value | Description |
+|-----------------------------------|-------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
+| type | "processor" | AWS EventBridge is an event stream **processor**. |
+| provider | "aws_event_bridge" | Use this exact value to use AWS EventBridge as a provider. |
+| auth.method | "access_key" | Use this exact value. Only `"access_key"` is supported to authenticate against AWS EventBridge. |
+| auth.secret.aws_access_key_id | "ABCDEFGHIJK" | Access Key ID for the IAM role used. |
+| auth.secret.aws_secret_access_key | "ZYXWVUTSRQPO" | Secret Access Key for the IAM role used. |
+| options.aws_region | "eu-central-1" | AWS region to use, see region [overview](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html){:target="_blank"} |
+| options.aws_eventbridge_event_bus | "default" | Name of the event bus to use. Default: "default" (optional) |
+| options.aws_eventbridge_rule_arn | "arn:aws:events:eu-central-1:1234567890:rule/vmware-event-router" | Rule ARN to use for event pattern matching. |
+
+> **Note:** Currently only IAM user accounts with access key/secret are supported to authenticate against AWS EventBridge. Please follow the [user guide](https://docs.aws.amazon.com/eventbridge/latest/userguide/getting-set-up-eventbridge.html){:target="_blank"} before deploying the event router. Further information can also be found in the [authentication](https://docs.aws.amazon.com/eventbridge/latest/userguide/auth-and-access-control-eventbridge.html#authentication-eventbridge){:target="_blank"} section.
+
+In addition to the recommendation in the AWS EventBridge user guide you might want to lock down the IAM role for the VMware Event Router and scope it to these permissions ("Action"):
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "VisualEditor0",
+ "Effect": "Allow",
+ "Action": [
+ "events:PutEvents",
+ "events:ListRules",
+ "events:TestEventPattern"
+ ],
+ "Resource": "*"
+ }
+ ]
+}
+```
+
+Example of the configuration section for AWS EventBridge:
+
+```json
+{
+ "type": "processor",
+ "provider": "aws_event_bridge",
+ "auth": {
+ "method": "access_key",
+ "secret": {
+ "aws_access_key_id": "ABCDEFGHIJK",
+ "aws_secret_access_key": "ZYXWVUTSRQPO"
+ }
+ },
+ "options": {
+ "aws_region": "eu-central-1",
+ "aws_eventbridge_event_bus": "default",
+ "aws_eventbridge_rule_arn": "arn:aws:events:eu-central-1:1234567890:rule/vmware-event-router"
+ }
+}
+```
+
+> **Note:** The JSON configuration file is an array of maps, ie. "[{},{}]". The snippet above is trimmed for readability. The examples provided [here](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/vmware-event-router/deploy){:target="_blank"} are properly formatted.
+
+### Metrics Server: Configuration Details
+
+The VMware Event Router exposes metrics (JSON format) on the (currently hardcoded) HTTP endpoint `"http://IP:PORT/stats". The following table lists allowed and optional fields for configuring the metrics server.
+
+| Field | Value | Description |
+|----------------------|----------------|-----------------------------------------------------------------------------------------------------------------|
+| type | "internal" | Metrics server is of type `"internal"` |
+| provider | "metrics" | Use this exact value to configure the metrics server. |
+| address | "0.0.0.0:8080" | Bind address for the http server to listen on. |
+| auth.method | "basic_auth" | `"basic_auth"` or `"none"` (disabled) is supported to configure authentication of the metrics server endpoint. |
+| auth.secret.username | "admin" | Only required when `"basic_auth"` is configured. |
+| auth.secret.password | "REPLACE_ME" | Only required when `"basic_auth"` is configured. |
+
+Example of the configuration section for the metrics server:
+
+```json
+{
+ "type": "metrics",
+ "provider": "internal",
+ "address": "0.0.0.0:8080",
+ "auth": {
+ "method": "none"
+ }
+}
+```
+
+### Deployment
+VMware Event Router can be deployed and run as standalone binary (see [below](#build-from-source)). However, it is designed to be run in a Kubernetes cluster for increased availability and ease of scaling out. The following steps describe the deployment of the VMware Event Router in **a Kubernetes cluster** for an existing OpenFaaS ("faas-netes") environment, respectively AWS EventBridge.
+
+> **Note:** Docker images are available [here](https://hub.docker.com/r/vmware/veba-event-router).
+
+Create a namespace where the VMware Event Router will be deployed to:
+
+```bash
+kubectl create namespace vmware
+```
+
+Use one of the configuration files provided [here](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/vmware-event-router/deploy){:target="_blank"} to configure the router for **one** VMware vCenter Server event `stream` and **one** OpenFaaS **or** AWS EventBridge event stream `processor`. Change the values to match your environment. The following example will use the OpenFaaS config sample.
+
+> **Note:** Make sure your environment is up and running, i.e. Kubernetes and OpenFaaS (incl. a function for testing) up and running or AWS EventBridge correctly configured (IAM Role, event bus and pattern rule).
+
+After you made your changes to the configuration file, save it as `"event-router-config.json` in your current Git working directory.
+
+> **Note:** If you have changed the port of the metrics server in the configuration file (default: 8080) make sure to also change that value in the YAML manifest (under the Kubernetes service entry).
+
+Now, from your current Git working directory create a Kubernetes [secret](https://kubernetes.io/docs/concepts/configuration/secret/){:target="_blank"} from the configuration file:
+
+```bash
+kubectl -n vmware create secret generic event-router-config --from-file=event-router-config.json
+```
+
+> **Note:** You might want to delete the (local) configuration file to not leave behind sensitive information on your local machine.
+
+Now we can deploy the VMware Event Router:
+
+```bash
+kubectl -n vmware create -f deploy/event-router-k8s.yaml
+```
+
+Check the logs of the VMware Event Router to validate it started correctly:
+
+```bash
+kubectl -n vmware logs deploy/vmware-event-router -f
+```
+
+If you run into issues, the logs should give you a hint, e.g.:
+
+- configuration file not found -> file naming issue
+- connection to vCenter/OpenFaaS cannot be established -> check values in the configuration file
+- deployment/pod will not even come up -> check for resource issues, docker pull issues and other potential causes using the standard kubectl troubleshooting ways
+
+To delete the deployment and secret simply delete the namespace we created earlier:
+
+```bash
+kubectl delete namespace vmware
+```
+## Build from Source
+
+Requirements: This project uses [Golang](https://golang.org/dl/) and Go [modules](https://blog.golang.org/using-go-modules). For convenience a Makefile and Dockerfile are provided requiring `make` and [Docker](https://www.docker.com/) to be installed as well.
+
+```bash
+git clone https://github.com/vmware-samples/vcenter-event-broker-appliance
+cd vcenter-event-broker-appliance/vmware-event-router
+
+# for Go versions before v1.13
+export GO111MODULE=on
+
+# defaults to build with Docker (use make binary for local executable instead)
+make
+```
+
+## Example Event Structure
+
+The following example for a `VmPoweredOnEvent` shows the event structure and payload:
+
+```json
+{
+ "id": "08179137-b8e0-4973-b05f-8f212bf5003b",
+ "source": "https://10.0.0.1:443/sdk",
+ "specversion": "1.0",
+ "type": "com.vmware.event.router/event",
+ "subject": "VmPoweredOffEvent",
+ "time": "2020-02-11T21:29:54.9052539Z",
+ "data": {
+ "Key": 9902,
+ "ChainId": 9895,
+ "CreatedTime": "2020-02-11T21:28:23.677595Z",
+ "UserName": "VSPHERE.LOCAL\\Administrator",
+ "Datacenter": {
+ "Name": "testDC",
+ "Datacenter": {
+ "Type": "Datacenter",
+ "Value": "datacenter-2"
+ }
+ },
+ "ComputeResource": {
+ "Name": "cls",
+ "ComputeResource": {
+ "Type": "ClusterComputeResource",
+ "Value": "domain-c7"
+ }
+ },
+ "Host": {
+ "Name": "10.185.22.74",
+ "Host": {
+ "Type": "HostSystem",
+ "Value": "host-21"
+ }
+ },
+ "Vm": {
+ "Name": "test-01",
+ "Vm": {
+ "Type": "VirtualMachine",
+ "Value": "vm-56"
+ }
+ },
+ "Ds": null,
+ "Net": null,
+ "Dvs": null,
+ "FullFormattedMessage": "test-01 on 10.0.0.1 in testDC is powered off",
+ "ChangeTag": "",
+ "Template": false
+ },
+ "datacontenttype": "application/json"
+}
+```
+
+> **Note:** If you use the AWS EventBridge stream `processor` the event is wrapped and accessible under `""detail": {}"` as a JSON-formatted string.
diff --git a/docs/kb/contribute-functions.md b/docs/kb/contribute-functions.md
new file mode 100644
index 00000000..2c2f6f33
--- /dev/null
+++ b/docs/kb/contribute-functions.md
@@ -0,0 +1,228 @@
+---
+layout: docs
+toc_id: contribute-functions
+title: VMware Event Broker Appliance - Building Functions
+description: Building Functions
+permalink: /kb/contribute-functions
+cta:
+ title: Have a question?
+ description: Please check our [Frequently Asked Questions](/faq) first.
+---
+
+# Writing your own functions
+
+The VMware Event Broker Appliance uses OpenFaaS as a Function-as-a-Service (FaaS) platform. If you are looking to understand the basics of functions, start [here](kb/functions).
+
+You can also get started quickly with these quickstart [templates](https://github.com/pksrc/vebafn){:target="_blank"}.
+
+## Instructions
+
+> **ASSUMPTION:** The following steps assume VMware Event Broker Appliance has been [installed (configured with OpenFaaS)](/kb/install-openfaas) and is running.
+
+
+* Create a directory for your function and set up the secret config file
+
+ ```toml
+ # vcconfig.toml contents
+ # replace with your own values and use a dedicated user/service account with permissions to tag VMs if possible
+ [vcenter]
+ server = "VCENTER_FQDN/IP"
+ user = "tagging-admin@vsphere.local"
+ password = "DontUseThisPassword"
+
+ [tag]
+ urn = "urn:vmomi:InventoryServiceTag:019c0a9e-0672-48f5-ac2a-e394669e2916:GLOBAL" # replace the actual urn of the tag
+ action = "attach" # tagging action to perform, i.e. attach or detach tag
+ ```
+
+* Login to OpenFaaS and save the secret with `faas-cli secret create vcconfig --from-file=vcconfig.toml`
+* Grab the desired language template (there are multiple ways)
+ * The first way is with `faas template pull` and see it in `faas new --list`.
+ * The second way is to look through the OpenFaas-Incubator, for example, [openfaas-incubator](https://github.com/openfaas-incubator/golang-http-template.git). If found there, retrieve it with `faas template pull https://github.com/openfaas-incubator/golang-http-template`.
+ * The third way is `faas template store pull `
+ * An alternative to templates is not to use them, and make your own Dockerfile. Optionally, after doing that, you can make your own template.
+* Create scaffold for the function: `faas-cli new --lang faas-hello-world --prefix=""`.
+* Make changes inside scaffold
+ * A directory called `faas-hello-world` should be created and within it, should be a file called `handler.go`, except the extension should be appropriate for your choice of language. Edit that file to make a new function.
+ * Open and edit the `faas-hello-world.yml` provided. Change provider > gateway and functions >annotations > topic as per your environment/needs. Here is an example for Go:
+
+ ```yaml
+ provider:
+ name: openfaas
+ gateway: https://VEBA_FQDN_OR_IP # replace with your vCenter Event Broker Appliance environment
+ functions:
+ faas-hello-world:
+ lang: golang-http
+ handler: ./faas-hello-world
+ image: fgold/faas-hello-world:latest
+ environment:
+ write_debug: true
+ read_debug: true
+ secrets:
+ - vcconfig # leave as is unless you changed the name during the creation of the vCenter credentials secrets above
+ annotations:
+ topic: vm.powered.on # or drs.vm.powered.on in a DRS-enabled cluster
+ ```
+
+* Build the faas function with `faas-cli up -f faas-hello-world.yml`.
+ * For the Golang-http template, Build the faas function with `faas-cli up -f faas-hello-world.yml --build-arg GO111MODULE=on`
+
+### Run the Function in VEBA
+
+* Run `faas-cli deploy -f hello-world.yml --tls-no-verify` to deploy the function. It doesn't have to be run on the local machine; it can be run on the machine that is hosting the VEBA appliance.
+* Try to trigger the function with a vCenter event.
+
+## Coding - Best Practices
+
+Compared to writing repetitive boilerplate logic to handle vCenter events, the VMware Event Broker Appliance powered by OpenFaaS makes it remarkable easy to consume and process events with minimal code required.
+
+However, as outlined in previous sections in this guide, there are still some best practices and pitfalls to be considered when it comes to messaging in a distributed system. The following list tries to provide guidance for function authors. Before applying them thoroughly think about your problem statement and whether all of these recommendations apply to your specific scenario.
+
+
+
+
+### Single Responsibility Principle
+
+Avoid writing huge function handlers. Instead of describing a huge workflow in your function or using long if/else/switch statements to deal with any type of event, consider breaking your problem up into smaller pieces (functions). This makes your code cleaner, easier to understand/contribute to and maintainable. As a result, your function will likely run faster and return early, avoiding undesired blocking behavior.
+
+Single Responsibility Principle (SRP) is the philosophy behind the UNIX command line tools. "Do one job and do it well". Solve complex problems by breaking them down with composition where the output of one program becomes the input of the next program.
+
+> **Note:** Generally, workflows should not be handled in functions but by workflow engines, such as vRealize Orchestrator (vRO). vRO and the VMware Event Broker Appliance work well together, e.g. by triggering workflows from functions via the vRO REST API. Upon completion, or for intermediary steps, vRO might call back into the appliance and leverage other functions for lightweight execution handling.
+
+
+### Deterministic Behavior
+
+Simply speaking, given the same input your function should always produce the same output for predictability and consistency. There's always exceptions to the rule, e.g. when dealing with time(stamps) or leveraging random number generators within your function body.
+
+> **Note:** Whenever you lookup data in the event payload received when your function is invoked, make sure to check for missing/"NULL" keys to avoid your code from throwing an unhandled exception - or worse incorrectly interpreting (missing) data. Senders might retry invoking your function with this message, leading to an endless loop if not handled correctly.
+
+
+### Keep Functions slim and up to date
+
+Not only for security reasons should you keep your function (and dependencies, such as libraries) up to date with patches. Patches might also include performance improvements which your code immediately benefits from.
+
+> **Note:** Since functions in the VMware Event Broker Appliance are deployed as container images, consider using a registry that supports image scanning such as [VMware Harbor](https://goharbor.io/).
+
+Try to reduce the container image size by using a container optimized function image (template) and use Docker [multi-stage](https://docs.docker.com/develop/develop-images/multistage-build/) builds in your [custom](https://towardsdatascience.com/going-serverless-with-openfaas-and-golang-building-optimized-templates-730991084443) OpenFaaS templates. Remove unused libraries/files which unnecessarily bloat your image, leading to longer download and startup times.
+
+
+### Keep Functions "warm" - if possible
+
+Most OpenFaaS function templates support the [`"http"` mode](https://github.com/openfaas-incubator/of-watchdog#1-http-modehttp) for calling your function handler. This prevents the function execution stack `"main()"` from terminating and enables function authors to persist state, such as connections, in memory for faster access and reuse.
+
+This is especially useful when dealing with limited resources such as database or vCenter connections. Another benefit is that connections don't have to be newly established but can be reused. Pseudo-code example:
+
+```python
+# db defined outside function handler
+db = setup_db(user, password, db_server)
+def handle(req):
+ event_body = req.get("data")
+ db.put(event_body)
+```
+
+> **Note:** Your connection/session library should support "keep alive" to periodically send a heartbeat/ping to the remote server and keep the connection open (tokens fresh).
+
+
+### Return early/defer or externalize Work
+
+Your primary goal should be to avoid long-running functions (minutes) as much as possible. The longer your function runs, the more things can go wrong and you might have to start from scratch (which might not be possible without additional persistency safeties in your logic).
+
+Usually that's an indicator that your function can be further broken down into smaller steps or could be better handled with a workflow engine, see [Single Responsibility Principle](#single-responsibility-principle) above.
+
+If you can't avoid long-running functions an option is to persist the event payload (if it's important) to a durable (external) queue or database and use dedicated workers to process these items. The [OpenFaaS kafka-connector](https://github.com/openfaas-incubator/kafka-connector) can be a suitable approach.
+
+
+### Dealing with Side Effects
+
+A side effect is an irreversible action, such as sending an email or printing a log statement to standard output. Since generally you cannot avoid these, it's best to move the related logic for critical side effects to the end of the function handler (if possible). Memoizing state to prevent duplicate execution can be a useful approach to avoid undesired side effects, such as sending an email twice (also see section on idempotency below). Pseudo-code below:
+
+```python
+db = setup_db(user, password, db_server)
+def handle(req):
+ subject = req.get("subject")
+ event_id = req.get("id")
+ processed = db.get(event_id, "event_table")
+ if not processed and subject == "VmPoweredOffEvent":
+ send_email("alert", req)
+ db.write(event_id, "event_table")
+```
+
+> **Note:** Strictly speaking the pseudo-code above is flawed since `send_email` and `db.write` are not part of (the same) atomic operation (transaction). The [outbox pattern](https://debezium.io/blog/2019/02/19/reliable-microservices-data-exchange-with-the-outbox-pattern/), delayed processing and/or compensating transaction such as [Sagas](https://dzone.com/articles/distributed-sagas-for-microservices) are technical solutions for such complex requirements.
+
+
+### Persistency and Retries
+
+As discussed in earlier sections of this guide, the VMware Event Broker Appliance currently does not support retrying function invocation on failure/timeout, and also does not persist events for redelivery/re-drive.
+
+A workaround is to persist the event to an external (durable) datastore or queue and consume/process from there. If this fails a log message can be produced with debugging information (critical event payload) or the event sent to a backup system, e.g. dead letter queue (DLQ).
+
+>**Note:** Strictly speaking this does not address the appliance-internal scenario where the OpenFaaS vcenter-connector might not be able to invoke your function (resource busy, unavailable, etc.) but addresses common network communication issues when making outbound calls from the appliance.
+
+If your function executes quickly, retrying within the function might be a viable approach as well (retry three times with an increasing backoff delay). Pseudo-code:
+
+```python
+def handle(req):
+ success = False
+ failures = 0
+ while not success:
+ success = send_event(req.get("data"))
+ if not success:
+ failures += 1
+ if failures > 3:
+ return
+ print(f'failure, retrying after {failures * 3} seconds')
+ sleep(3 * failures)
+```
+
+
+### Idempotency (Message Deduplication)
+
+Although as of today the VMware Event Broker Appliance does not attempt to redeliver a message ("at least once" delivery, see [message delivery guarantees](#message-delivery-guarantees)) depending on the complexity of your function workflow and involved (external) components, message duplication might still be a concern. Your function logic or the receiving downstream system should be able to detect and deal with duplicate messages to prevent data consistency issues or unwanted [side effects](#dealing-with-side-effects).
+
+To support idempotency checks, the VMware Event Broker Appliance [event payload](#the-event-specification) provides fields which can be used to detect duplicates. It is usually sufficient to use a combination of the event "id" and "subject" or "source" fields in the JSON message body to construct and persist a unique message key in a database (or cache) for lookups:
+
+```json
+{
+ [...]
+ "id":"0058c998-cc0f-49ca-8cc3-1b60abf5957c",
+ "source":"10.160.94.63",
+ "subject":"UserLogoutSessionEvent"
+}
+```
+
+> **Note:** The "id" field is a UUID which, practically speaking, is guaranteed to be unique per event (even across multiple appliances). "Source" or "subject" can be used for faster indexing/lookups in tables or caches.
+
+
+
+### Out of Order Message Arrival
+
+Even though unlikely due to the underlying TCP/IP guarantees, but nevertheless possible in specific environments or deployments - dealing with out of order message arrival in your function/downstream logic might be a requirement.
+
+Therefore, your function or downstream system can use the vCenter event "Key", a monotonically increasing value set by vCenter, to discard late arriving messages with a lower "Key" value. If your function supports "warm" invocations (see `"http"` mode described above) the value can be cached in memory or alternatively (for increased durability) persisted in an external datastore/cache such as [Redis](https://redis.io/).
+
+```python
+last_key = 0
+def handle(req):
+ key = req.get("data").get("Key", 0)
+ if key > last_key:
+ # do work
+ last_key = key
+```
+
+> **Note:** Depending on your logic, it might still be desired to account for late arriving data. This is usually the case for stream processors. You might found this [paper](https://blog.acolyer.org/2015/08/21/millwheel-fault-tolerant-stream-processing-at-internet-scale/) on windowing and watermarks an interesting read.
+
+
+### Support Debugging
+
+Things will go wrong. Provide useful and correct information via logging to standard output. Example for an incorrect log statement in your code:
+
+```python
+def handle(req):
+ # do something
+ print('stored event in database')
+ store_event(event)
+```
+
+If `store_event` fails someone troubleshooting your function will have a hard time. Either rephrase the `print` statement to "storing ..." or, better, put it after the function call. Also, consider using a structured logging library that supports consistently formatted and parsable output.
+
+> **Note:** Avoid logging sensitive data, such as usernames, passwords, account information, etc.
diff --git a/docs/kb/contribute-site.md b/docs/kb/contribute-site.md
new file mode 100644
index 00000000..a930380c
--- /dev/null
+++ b/docs/kb/contribute-site.md
@@ -0,0 +1,107 @@
+---
+layout: docs
+toc_id: contribute-site
+title: VMware Event Broker Appliance - Docs
+description: Contributing Documentation/Website Updates
+permalink: /kb/contribute-site
+cta:
+ title: Have a question?
+ description: Please check our [Frequently Asked Questions](/faq) first.
+---
+
+# Contribute to the Documentation or the Website
+
+## Structure
+The website is hosted using [Github Pages](https://help.github.com/en/github/working-with-github-pages/about-github-pages){:target="_blank"} and built using [Jekyll](https://jekyllrb.com/){:target="_blank"}. The files that make up the website are contained within the `docs` folder (as Github Pages requires) within the master branch. You'll find more details about how they are organized and their purpose below.
+
+```
+.
+├── site > Contains MD files that need to go under the base website
+│ └── **.md
+├── kb > Contains MD files for the documentation section of the website
+│ ├── img
+│ │ └── **.png > images required for documentation
+│ └── **.md > All the MDs that make up the documentation
+├── assets > Contains JS, CSS, IMGs for the site
+│ ├── js
+│ ├── img
+│ └── css
+├── index.html > Website Landing page
+├── README.md *** You are here
+├── _config.yml > Site wide configuration and variables
+└── Gemfile > Plugins required for the website to be built by Jekyll
+```
+
+In order for Jekyll to process the MD files and render them as html, you'll need to add the below to the beginning of the each MD file.
+
+```yaml
+---
+layout: resources # choose between default, docs, page or resources
+title: Additional Resources # provide the title for the web page
+description: Update this # this shows up in the Website description
+permalink: /resources # this is the short link for the page, if empty the relative path of the md file is used
+#other yaml data that can be referenced within the page
+---
+```
+
+### Other Key Files and Folders
+- **_data/default.yml:** YAML content that drives the side-nav bar for the documentation
+- **_data/resources.yml:** YAML content for the videos, links and external references contained in the resources page
+- **_data/team.yml:** YAML data of the core team for the landing page
+- **_functions:** folder that contains all the featured functions showcased on the landing page
+- **_usecases:** folder that contains all the use cases showcased on the landing page
+- **_includes** all the reusable html components referenced with the layouts
+- **_layouts:** all the various layouts available to be used within the site
+ - **docs** - use this for layout for the docs
+ - **page** - use this for the pages that needs to go on the base site
+ - **resources** - specifically designed for the resources page
+
+
+## Build and Run the website locally
+To ensure the changes to any file or folder that power the website is valid, please setup this step below that allows you to build the website, verify changes locally before you push to the repo.
+
+### Dependencies for MacOS
+
+Install the following for an easy to use dev environment:
+
+```bash
+brew install rbenv
+rbenv install 2.6.3
+gem install bundler
+```
+
+### Dependencies for Linux
+If you are running a build on Ubuntu you will need the following packages:
+* ruby
+* ruby-dev
+* ruby-bundler
+* build-essential
+* zlib1g-dev
+* nginx (or apache2)
+
+### Dependencies for Windows
+If you are on Windows, all hope is not lost. Follow the steps here to install the dependencies - [here](https://jekyllrb.com/docs/installation/windows/)
+
+### Local Development
+* Install Jekyll and plug-ins in one fell swoop. `gem install github-pages`
+This mirrors the plug-ins used by GitHub Pages on your local machine including Jekyll, Sass, etc.
+* Clone down your own fork, or clone the main repo and add your own remote.
+
+```bash
+git clone git@github.com:vmware-samples/vcenter-event-broker-appliance.git
+cd vcenter-event-broker-appliance/docs
+bundle install
+```
+
+* Serve the site and watch for markup/sass changes `jekyll serve --livereload --incremental`. You may need to run `bundle exec jekyll serve --livereload --incremental`.
+* View your website at http://127.0.0.1:4000/
+* Commit any changes and push everything to your fork.
+* Once you're ready, submit a PR of your changes.
+
+## Troubleshooting
+* If you don't see your updates reflected on the website when running locally, try the following steps
+
+```zsh
+ bundle exec jekyll clean
+ bundle exec jekyll serve --incremental --livereload
+```
diff --git a/docs/kb/contribute-start.md b/docs/kb/contribute-start.md
new file mode 100644
index 00000000..fd5beda7
--- /dev/null
+++ b/docs/kb/contribute-start.md
@@ -0,0 +1,129 @@
+---
+layout: docs
+toc_id: contribute-start
+title: VMware Event Broker Appliance - Getting Started
+description: Getting Started
+permalink: /kb/contribute-start
+cta:
+ title: Join our community
+ description: Earn a place amongst our top contributors [here](/community#contributors-veba)
+ actions:
+ - text: Learn how you can contribute to our Appliance build process [here](/kb/contribute-appliance)
+ - text: Learn how you can contribute to our VMware Event Router [here](/kb/contribute-eventrouter)
+ - text: Learn how you can contribute to our Pre-built Functions [here](/kb/contribute-functions)
+ - text: Learn how you can contribute to our Website [here](/kb/contribute-site).
+---
+
+# Contributing
+
+The VMware Event Broker Appliance team welcomes contributions from the community.
+
+Before you start working with the VMware Event Broker Appliance, please read our [Developer Certificate of Origin](https://cla.vmware.com/dco){:target="_blank"}. All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch.
+
+# Step-by-step help
+
+If you're just starting out with containers and source control and are looking for additional guidance, browse to this [blog series](http://www.patrickkremer.com/veba/){:target="_blank"} on VEBA. The blog goes step-by-step with screenshots and assumes zero experience with any of the required tooling.
+
+# Preqrequisites
+
+Three tools are required in order to contribute functions - You must install [Git](https://git-scm.com/downloads){:target="_blank"}, [Docker](https://docs.docker.com/){:target="_blank"}, and download [faas-cli.exe](https://github.com/openfaas/faas-cli/releases){:target="_blank"}.
+
+You must also create a [Github](https://github.com/join){:target="_blank"} account. You need to verify your email with Github in order to contribute to the VEBA repository.
+
+# Quickstart for Contributing
+
+## Download the VEBA source code
+```bash
+git clone https://github.com/vmware-samples/vcenter-event-broker-appliance
+```
+
+## Configure git to sign code with your verified name and email
+```bash
+git config --global user.name "Your Name"
+git config --global user.email "youremail@domain.com"
+```
+
+## Contribute documentation changes
+
+Make the necessary changes and save your files.
+```bash
+git diff
+```
+
+This is sample output from git. It will show you files that have changed as well as all display all changes.
+```bash
+user@wrkst01 MINGW64 ~/Documents/git/vcenter-event-broker-appliance(master)
+$ git diff
+diff --git a/docs/kb/contribute-start.md b/docs/kb/contribute-start.md
+index 4245046..f86f09f 100644
+--- a/docs/kb/contribute-start.md
++++ b/docs/kb/contribute-start.md
+@@ -6,6 +6,32 @@ description: Getting Started
+ permalink: /kb/contribute-start
+ ---
+
++# Preqrequisites
++
++Three tools are required in order to contribute functions -
+(output truncated)
+```
+
+Commit the code and push your commit. -a commits all changed files, -s signs your commit, and -m is a commit message - a short description of your change.
+
+
+```bash
+git commit -a -s -m "Added prereq and git diff output to contribution page."
+git push
+```
+
+You can then submit a pull request (PR) to the VEBA maintainers - a step-by-step guide with screenshots is available [here](http://www.patrickkremer.com/2019/12/vcenter-event-broker-appliance-part-v-contributing-to-the-veba-project/){:target="_blank"}
+
+# Changing or contributing new functions
+
+The git commands are the same, but in order to change code, you must reference your own Docker image. The example YAML below comes from the [datastore-usage-email](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/development/examples/powercli/datastore-usage-email){:target="_blank"} sample function. Note that the `image:` references the `vmware` docker account. You must change this to your own docker account. As always the `gateway:` must point to your own local VEBA appliance
+
+
+```yaml
+provider:
+ name: openfaas
+ gateway: https://veba.primp-industries.com
+functions:
+ powershell-datastore-usage:
+ lang: powercli
+ handler: ./handler
+ image: vmware/veba-powercli-datastore-notification:latest
+ environment:
+ write_debug: true
+ read_debug: true
+ function_debug: false
+ secrets:
+ - vc-datastore-config
+ annotations:
+ topic: AlarmStatusChangedEvent
+
+```
+
+Once you've written or changed function code, you can push it to your local VEBA appliance for testing.
+```bash
+docker login
+faas-cli build -f stack.yml
+faas-cli push -f stack.yml
+faas-cli deploy -f stack.yml --tls-no-verify
+```
+If everything works as expected, you can then commit your code and file a pull request for inclusion into the project.
+
+## Submitting Bug Reports and Feature Requests
+
+Please submit bug reports and feature requests by using our GitHub [Issues](https://github.com/vmware-samples/vcenter-event-broker-appliance/issues){:target="_blank"} page.
+
+Before you submit a bug report about the code in the repository, please check the Issues page to see whether someone has already reported the problem. In the bug report, be as specific as possible about the error and the conditions under which it occurred. On what version and build did it occur? What are the steps to reproduce the bug?
+
+Feature requests should fall within the scope of the project.
+
+## Pull Requests
+
+Before submitting a pull request, please make sure that your change satisfies the following requirements:
+- VMware Event Broker Appliance can be built and deployed. See the getting started build guide [here](contribute-eventrouter.md).
+- The change is signed as described by the [Developer Certificate of Origin](https://cla.vmware.com/dco){:target="_blank"} doc.
+- The change is clearly documented and follows Git commit [best practices](https://chris.beams.io/posts/git-commit/){:target="_blank"}
+- Contributions to the [examples](/examples) contain a titled readme.
\ No newline at end of file
diff --git a/docs/kb/img/example1.png b/docs/kb/img/example1.png
new file mode 100644
index 00000000..07c8a5d9
Binary files /dev/null and b/docs/kb/img/example1.png differ
diff --git a/docs/kb/img/example2.png b/docs/kb/img/example2.png
new file mode 100644
index 00000000..59d6f3e8
Binary files /dev/null and b/docs/kb/img/example2.png differ
diff --git a/veba-appliance-diagram.png b/docs/kb/img/veba-appliance-diagram.png
similarity index 100%
rename from veba-appliance-diagram.png
rename to docs/kb/img/veba-appliance-diagram.png
diff --git a/docs/kb/img/veba-architecture.png b/docs/kb/img/veba-architecture.png
new file mode 100644
index 00000000..0e6ee828
Binary files /dev/null and b/docs/kb/img/veba-architecture.png differ
diff --git a/docs/kb/install-eventbridge.md b/docs/kb/install-eventbridge.md
new file mode 100644
index 00000000..7b5d0919
--- /dev/null
+++ b/docs/kb/install-eventbridge.md
@@ -0,0 +1,89 @@
+---
+layout: docs
+toc_id: install-eventbridge
+title: VMware Event Broker Appliance - EventBridge
+description: Deploying VMware Event Broker Appliance with AWS EventBridge
+permalink: /kb/install-eventbridge
+cta:
+ title: What Next?
+ description: At this point, you have successfully extended your SDDC to AWS. You can take advantage of the number of AWS resources that can be configured as targets for AWS EventBridge
+ actions:
+ - text: AWS EventBridge documentation - [here](https://docs.aws.amazon.com/eventbridge/latest/userguide/what-is-amazon-eventbridge.html).
+---
+
+# Deploy VMware Event Broker Appliance with AWS EventBridge
+
+Customers looking to seamlessly extend their vCenter through native AWS components (lambda, cloud watch etc) can get started quickly by deploying VMware Event Broker Appliance with AWS EventBridge as the Event Processor
+
+## Appliance Deployment Steps
+
+### Requirements
+
+* 2 vCPU and 8GB of memory for VMware Event Broker Appliance
+* vCenter Server 6.x or greater
+* Account to login to vCenter Server (readOnly is sufficient)
+* Access credentials for AWS account
+* Details about the AWS EventBridge setup (Rules, Region etc)
+
+### Step 1 - Download the VMware Event Broker Appliance (OVA) from the [VMware Fling site](https://flings.vmware.com/vcenter-event-broker-appliance){:target="_blank"}.
+
+### Step 2 - Deploy the VMware Event Broker Appliance OVA to your vCenter Server using the vSphere HTML5 Client. As part of the deployment you will be prompted to provide the following input:
+
+#### **Networking** (**Required**)
+
+ * Hostname - The FQDN of the VMware Event Broker Appliance. If you do not have DNS in your environment, make sure the hostname provide is resolvable from your desktop which may require you to manually add a hosts entry. Proper DNS resolution is recommended
+ * IP Address - The IP Address of the VMware Event Broker Appliance
+ * Network Prefix - Network CIDR Selection (e.g. 24 = 255.255.255.0)
+ * Gateway - The Network Gateway address
+ * DNS - DNS Server(s) that will be able to resolve to external sites such as Github for initial configuration. If you have multiple DNS Servers, input needs to be **space separated**.
+ * DNS Domain - The DNS domain of your network
+ * NTP Server - NTP Server(s) for proper time synchronization. If you have multiple DNS Servers, input needs to be **space separated**.
+
+#### **Proxy Settings** (Optional)
+ * HTTP Proxy Server - HTTP Proxy Server followed by the port and without typing http:// before (e.g. proxy.provider.com:3128)
+ * HTTPS Proxy - HTTPS Proxy Server followed by the port and without typing https:// before (e.g. proxy.provider.com:3128)
+ * Proxy Username - Optional Username for Proxy Server
+ * Proxy Password - Optional Password for Proxy Server
+ * No Proxy - Exclude internal domain suffix. Comma separated (localhost, 127.0.0.1, domain.local)
+
+#### **OS Credentials** (**Required**)
+ * Root Password - This is the OS root password for the VMware Event Broker Appliance
+
+#### vSphere* (**Required**)
+
+ * vCenter Server - This FQDN or IP Address of your vCenter Server that you wish to associate this VMware Event Broker Appliance to for Event subscription
+ * vCenter Username - The username to login to vCenter Server, as mentioned earlier, readOnly account is sufficient
+ * vCenter Password - The password to the vCenter Username
+ * Disable vCenter Server TLS Verification - If you have a self-signed SSL Certificate, you will need to check this box
+
+#### **Event Processor Configuration** (**Required**)
+ * Event Processor - Choose AWS EventBridge
+
+#### **AWS EventBridge Configuration**
+ * Access Key - A valid AWS Access Key to AWS EventBridge
+ * Access Secret - A valid AWS Access Secret to AWS EventBridge
+ * Event Bus Name - Name of the AWS Event Bus to use. If left blank, this defaults to "default" Bus name.
+ * Region - Region where Event Bus is running (e.g. us-west-2)
+ * Rule ARN - ID of the Rule ARN created in AWS EventBridge
+ * Advanced Settings - N/A, future use
+
+> For more information on using the OpenFaaS and AWS EventBridge Processor, please take a look at the [VMware Event Router documentation](https://github.com/vmware-samples/vcenter-event-broker-appliance/blob/development/vmware-event-router/README.MD){:target="_blank"}
+
+#### **zAdvanced** (Optional)
+ * Debugging - When enabled, this will output a more verbose log file that can be used to troubleshoot failed deployments
+ * POD CIDR Network - Customize POD CIDR Network (Default 10.99.0.0/20). Must not overlap with the appliance IP address.
+
+### Step 3 - Power On the VMware Event Broker Appliance after successful deployment. Depending on your external network connectivity, it can take a few minutes while the system is being setup. You can open the VM Console to view the progress. Once everything is completed, you should see an updated login banner for the various endpoints:
+
+```
+Appliance Status: https://[hostname]/status
+Install Logs: https://[hostname]/bootstrap
+Appliance Statistics: https://[hostname]/stats
+```
+
+> NOTE
+- When configured with the AWS EventBridge Processor, the OpenFaaS UI endpoint will not be available which is expected and is not shown in the login banner.
+- If you enable Debugging, the install logs endpoint will automatically contain the more verbose log entries.
+
+### Step 4 - You can verify that everything was deployed correctly by opening a web browser and accessing one of the endpoints along with the associated admin password you had specified as part of the OVA deployment.
+
diff --git a/docs/kb/install-openfaas.md b/docs/kb/install-openfaas.md
new file mode 100644
index 00000000..2b0d0b14
--- /dev/null
+++ b/docs/kb/install-openfaas.md
@@ -0,0 +1,83 @@
+---
+layout: docs
+toc_id: install-openfaas
+title: VMware Event Broker Appliance - OpenFaaS
+description: Deploying VMware Event Broker Appliance with OpenFaaS
+permalink: /kb/install-openfaas
+cta:
+ title: Deploy a Function
+ description: At this point, you have successfully deployed the VMware Event Broker Appliance and you are ready to start deploying your functions!
+ actions:
+ - text: Check the [pre-built Functions](/examples) to quickly get started
+ - text: Find the instruction to deploy a function - [here](use-functions).
+---
+# Deploy VMware Event Broker Appliance with OpenFaaS
+
+Customers looking to seamlessly extend their vCenter by either deploying our prebuilt functions or writing your own functions can get started quickly by deploying VMware Event Broker Appliance with OpenFaaS as the Event Processor
+
+## Appliance Deployment Steps
+
+### Requirements
+
+* 2 vCPU and 8GB of memory for VMware Event Broker Appliance
+* vCenter Server 6.x or greater
+* Account to login to vCenter Server (readOnly is sufficient)
+
+### Step 1 - Download the VMware Event Broker Appliance (OVA) from the [VMware Fling site](https://flings.vmware.com/vcenter-event-broker-appliance){:target="_blank"}.
+
+### Step 2 - Deploy the VMware Event Broker Appliance OVA to your vCenter Server using the vSphere HTML5 Client. As part of the deployment you will be prompted to provide the following input:
+
+#### **Networking** (**Required**)
+
+ * Hostname - The FQDN of the VMware Event Broker Appliance. If you do not have DNS in your environment, make sure the hostname provide is resolvable from your desktop which may require you to manually add a hosts entry. Proper DNS resolution is recommended
+ * IP Address - The IP Address of the VMware Event Broker Appliance
+ * Network Prefix - Network CIDR Selection (e.g. 24 = 255.255.255.0)
+ * Gateway - The Network Gateway address
+ * DNS - DNS Server(s) that will be able to resolve to external sites such as Github for initial configuration. If you have multiple DNS Servers, input needs to be **space separated**.
+ * DNS Domain - The DNS domain of your network
+ * NTP Server - NTP Server(s) for proper time synchronization. If you have multiple NTP Servers, input needs to be **space separated**.
+
+#### **Proxy Settings** (Optional)
+ * HTTP Proxy Server - HTTP Proxy Server followed by the port and without typing http:// before (e.g. proxy.provider.com:3128)
+ * HTTPS Proxy - HTTPS Proxy Server followed by the port and without typing https:// before (e.g. proxy.provider.com:3128)
+ * Proxy Username - Optional Username for Proxy Server
+ * Proxy Password - Optional Password for Proxy Server
+ * No Proxy - Exclude internal domain suffix. Comma separated (localhost, 127.0.0.1, domain.local)
+
+#### **OS Credentials** (**Required**)
+ * Root Password - This is the OS root password for the VMware Event Broker Appliance
+
+#### **vSphere** (**Required**)
+
+ * vCenter Server - This FQDN or IP Address of your vCenter Server that you wish to associate this VMware Event Broker Appliance to for Event subscription
+ * vCenter Username - The username to login to vCenter Server, as mentioned earlier, readOnly account is sufficient
+ * vCenter Password - The password to the vCenter Username
+ * Disable vCenter Server TLS Verification - If you have a self-signed SSL Certificate, you will need to check this box
+
+#### **Event Processor Configuration** (**Required**)
+ * Event Processor - Choose OpenFaaS (default)
+
+#### **OpenFaaS Configuration**
+ * Password - Password to login into OpenFaaS using "admin" account. Please use a secure password
+ * Advanced Settings - N/A, future use
+
+> For more information on using the OpenFaaS and AWS EventBridge Processor, please take a look at the [VMware Event Router documentation](https://github.com/vmware-samples/vcenter-event-broker-appliance/blob/development/vmware-event-router/README.MD){:target="_blank"}
+
+#### **zAdvanced** (Optional)
+ * Debugging - When enabled, this will output a more verbose log file that can be used to troubleshoot failed deployments
+ * POD CIDR Network - Customize POD CIDR Network (Default 10.99.0.0/20). Must not overlap with the appliance IP address
+
+### Step 3 - Power On the VMware Event Broker Appliance after successful deployment. Depending on your external network connectivity, it can take a few minutes while the system is being setup. You can open the VM Console to view the progress. Once everything is completed, you should see an updated login banner for the various endpoints:
+
+```
+Appliance Status: https://[hostname]/status
+Install Logs: https://[hostname]/bootstrap
+Appliance Statistics: https://[hostname]/stats
+OpenFaaS UI: https://[hostname]
+```
+
+> NOTE: If you enable Debugging, the install logs endpoint will automatically contain the more verbose log entries.
+
+
+### Step 4 - You can verify that everything was deployed correctly by opening a web browser and accessing one of the endpoints along with the associated admin password you had specified as part of the OVA deployment.
+
diff --git a/docs/kb/intro-about.md b/docs/kb/intro-about.md
new file mode 100644
index 00000000..6f5e32cd
--- /dev/null
+++ b/docs/kb/intro-about.md
@@ -0,0 +1,52 @@
+---
+layout: docs
+toc_id: intro-about
+title: VMware Event Broker Appliance - Introduction
+description: VMware Event Broker Appliance - Introduction
+permalink: /kb
+cta:
+ title: Getting Started
+ description: Get started with VMware Event Broker Appliance and extend your vSphere SDDC in under 60 minutes
+ actions:
+ - text: Install the [Appliance with OpenFaaS](install-openfaas) to extend your SDDC with our [community-sourced functions](/examples)
+ - text: Install the [Appliance with AWS EventBridge](install-eventbridge) to extend your SDDC leveraging native AWS capabilities.
+---
+
+# VMware Event Broker Appliance
+
+The [VMware Event Broker Appliance](https://flings.vmware.com/vcenter-event-broker-appliance#summary){:target="_blank"} Fling enables customers to unlock the hidden potential of events in the SDDC to easily create [event-driven automation](https://octo.vmware.com/vsphere-power-event-driven-automation/){:target="_blank"} and take vCenter Server Events to the next level! Extending vSphere by easily triggering custom or prebuilt actions to deliver powerful integrations within your datacenter across public cloud has never been more easier before.
+
+VMware Event Broker Appliance is provided as a virtual appliance that can be deployed to any vSphere-based infrastructure, including an on-premises and/or any public cloud environment running on vSphere such as VMware Cloud on AWS or VMware Cloud on DellEMC.
+
+With this appliance, end-users, partners and independent software vendors only have to write minimal business logic without going through a steep learning curve of understanding vSphere APIs. We believe this solution offers a better user experience in solving existing problems for vSphere operators. More importantly, it will enable new integration use cases and workflows to grow the vSphere ecosystem and community, similar to what AWS has achieved with AWS Lambda.
+
+## Use Cases
+
+VMware Event Broker Appliance enables customers to quickly get started with pre-built functions and enable the following use cases:
+
+### Notification:
+- Receive alerts and real time updates using your preferred communication channel such as SMS, Slack, Microsoft Teams, etc.
+- Real time updates for specific vSphere Inventory objects which matter to you and your organization
+- Monitor the health, availability & capacity of SDDC resources
+
+### Automation:
+- Apply configuration or customization changes based on specific VM or Host life cycle activities as an example within the SDDC (e.g. apply security settings to a VM or vSphere Tag to Host)
+- Scheduled jobs to validate health of an environment such as a long running snapshot on a VM
+
+### Integration:
+- Consume 2nd/3rd party solutions that provide remote APIs to associate with specific infrastructure events
+- Automated ticket creation using platforms such as Pager Duty, ServiceNow, Jira Service Desk, Salesforce based specific incidents such as workload and/or hardware failure as an example
+- Easily extend and consume public cloud services such as AWS EventBridge
+
+### Remediation:
+- Detect and automatically perform specific tasks based on certain types of events (e.g. add or request additional capacity)
+- Enables Operations and SRE teams to codify existing and well known run books for automated resolution
+
+### Audit:
+- Track all configuration changes for objects like a VM and automatically update a change management database (CMDB)
+- Forward all authentication and authorization events to your security team for compliance and/or intrusion detection
+- Replay configuration changes to aide in troubleshooting or debugging purposes
+
+### Analytics:
+- Reduce the number of connections and/or users to vCenter Server by providing access to events in an external system like CMDB or data warehouse solution
+- Enable teams to better understand workload and infrastructure behaviors by identifying trends observed in the events data including duration of events, users generating specific operations or the commonly used workflows
diff --git a/docs/kb/intro-architecture.md b/docs/kb/intro-architecture.md
new file mode 100644
index 00000000..066bde4e
--- /dev/null
+++ b/docs/kb/intro-architecture.md
@@ -0,0 +1,54 @@
+---
+layout: docs
+toc_id: intro-architecture
+title: VMware Event Broker Appliance - Architecture
+description: VMware Event Broker Appliance Architecture
+permalink: /kb/architecture
+cta:
+ title: Learn More
+ description: Find more about what makes VMware Event Broker Appliance possible
+ actions:
+ - text: Install the [Appliance with OpenFaaS](install-openfaas) to extend your SDDC with our [community-sourced functions](/examples)
+ - text: Install the [Appliance with AWS EventBridge](install-eventbridge) to extend your SDDC leveraging native AWS capabilities.
+ - text: Learn more about the [VMware Event Router](event-router) and supported Event Sources and Processors
+---
+
+# Architecture
+
+The VMware Event Broker Appliance follows a highly modular approach, using Kubernetes and containers as an abstraction layer between the base operating system ([Photon OS](https://github.com/vmware/photon)) and the required application services. Currently the following components are used in the appliance:
+
+- VMware Event Router ([Github](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/vmware-event-router){:target="_blank"})
+ - Supported Event Stream Sources:
+ - VMware vCenter ([Website](https://www.vmware.com/products/vcenter-server.html){:target="_blank"})
+ - Supported Event Stream Processors:
+ - OpenFaaS ([Website](https://www.openfaas.com/){:target="_blank"})
+ - AWS EventBridge ([Website](https://aws.amazon.com/eventbridge/){:target="_blank"})
+- Contour ([Github](https://github.com/projectcontour/contour){:target="_blank"})
+- Kubernetes ([Github](https://github.com/kubernetes/kubernetes){:target="_blank"})
+- Photon OS ([Github](https://github.com/vmware/photon){:target="_blank"})
+
+
+
+**[VMware Event Router](event-router)** implements the core functionality of the VMware Event Broker Appliance, that is connecting to event `streams` ("sources") and processing the events with a configurable event `processor` such as OpenFaaS or AWS EventBridge.
+
+**OpenFaaS®** makes it easy for developers to deploy event-driven functions and microservices to Kubernetes without repetitive, boiler-plate coding. Package your code or an existing binary in a Docker image to get a highly scalable endpoint with auto-scaling and metrics. In the VMware Event Broker Appliance, OpenFaaS powers the appliance-integrated Function-as-a-Service framework to **trigger custom functions based on vCenter events**. The OpenFaaS user interface provides an easy to use dashboard to deploy and monitor functions. Functions can be authored and also deployed via an easy to use [CLI](https://github.com/openfaas/faas-cli){:target="_blank"}.
+
+**Amazon EventBridge** is a serverless event bus that makes it easy to connect applications together using data from your own applications, integrated Software-as-a-Service (SaaS) applications, and AWS services. The VMware Event Broker Appliance offers native integration for **event forwarding to AWS EventBridge**. The only requirement is creating a dedicated IAM user (access_key) and associated EventBridge rule on the default (or custom) event bus in the AWS management console to be used by this appliance. Only events matching the specified event pattern (EventBridge rule) will be forwarded to limit outgoing network traffic and costs.
+
+**Contour** is an ingress controller for Kubernetes that works by deploying the Envoy proxy as a reverse proxy and load balancer. Contour supports dynamic configuration updates out of the box while maintaining a lightweight profile. In the VMware Event Broker Appliance, Contour provides **TLS termination for the various HTTP(S) endpoints** served.
+
+**Kubernetes** is an open source system for managing containerized applications across multiple hosts. It provides basic mechanisms for deployment, maintenance, and scaling of applications. For application and appliance developers, Kubernetes provides **powerful platform capabilities** such as application (container) self-healing, secrets and configuration management, resource management, and extensibility. Kubernetes lays the foundation for future improvements of the VMware Event Broker Appliance with regards to **high availability (n+1) and scalability (horizontal scale out)**.
+
+**Photon OS™** is an open source Linux container host optimized for cloud-native applications, cloud platforms, and VMware infrastructure. Photon OS provides a **secure runtime environment for efficiently running containers** and out of the box support for Kubernetes. Photon OS is the foundation for many appliances built for the vSphere platform and its ecosystem and thus the first choice for building the VMware Event Broker Appliance.
+
+# Architectural Considerations
+
+Even though the VMware Event Broker Appliance is instantiated as a single running virtual machine, internally its components follow a [microservices architecture](#architecture) running on Kubernetes. The individual services communicate via TCP/IP network sockets. Most of the communication is performed internally in the appliance so the chance of losing network packets is reduced.
+
+However, in case of a component becoming unavailable (crash-loop, overloaded,or slow to respond), communication might be impacted and; it's important to understand the consequences for event delivery, i.e. function invocation. To avoid the risk of blocking remote calls, which could render the whole system unusable, sensible default timeouts are applied, which can be fine-tuned if needed.
+
+Kubernetes is a great platform and foundation for building highly available distributed systems. Even though we currently don't make use of its multi-node clustering capabilities (i.e. scale out), Kubernetes provides a lot of benefits to developers and users. Its self-healing capabilities continuously watch the critical VMware Event Broker Appliance components and user-deployed functions and trigger restarts when necessary.
+
+Kubernetes and its dependencies, such as the Docker, are deployed as systemd units. This addresses the "who watches the watcher" problem in case the Kubernetes node agent (kubelet) or Docker container runtime crashes.
+
+> **Note:** We are considering to use Kubernetes' cluster capabilities in the future to provide increased resiliency (node crashes), scalability (scale out individual components to handle higher load) and durability (replication and persistency). The downside is the added complexity of deploying and managing a multi-node VMware Event Broker Appliance environment.
\ No newline at end of file
diff --git a/docs/kb/intro-event-router.md b/docs/kb/intro-event-router.md
new file mode 100644
index 00000000..2348f6ee
--- /dev/null
+++ b/docs/kb/intro-event-router.md
@@ -0,0 +1,125 @@
+---
+layout: docs
+toc_id: intro-event-router
+title: VMware Event Router - Introduction
+description: VMware Event Router Introduction
+permalink: /kb/event-router
+cta:
+ title: Get Started
+ description: Explore the capabilities that the VMware Event Router enables
+ actions:
+ - text: Install the [Appliance with OpenFaaS](install-openfaas) to extend your SDDC with our [community-sourced functions](/examples)
+ - text: Install the [Appliance with AWS EventBridge](install-eventbridge) to extend your SDDC leveraging native AWS capabilities.
+ - text: Learn more about the [Events in vCenter](vcenter-events) and how to find the right event for your usecase
+ - text: Learn more about Functions in this overview [here](functions).
+---
+
+# VMware Event Router
+
+The VMware Event Router is responsible for connecting to event `stream` sources, such as VMware vCenter, and forward events to an event `processor`. To allow for extensibility and different event sources/processors event sources and processors are abstracted via Go `interfaces`.
+
+Currently, one VMware Event Router is deployed per appliance (1:1 mapping). Only one vCenter event stream can be processed per appliance. Also, only one event stream (source) and one processor can be configured. The list of supported event sources and processors can be found below.We are evaluating options to support multiple event sources (vCenter servers) and processors per appliance (scale up) or alternatively support multi-node appliance deployments (scale out), which might be required in large deployments (performance, throughput).
+
+> **Note:** We have not done any extensive performance and scalability testing to understand the limits of the single appliance model.
+
+# Supported Event Sources
+- [VMware vCenter Server](https://www.vmware.com/products/vcenter-server.html){:target="_blank"}
+
+# Supported Event Processors
+- [OpenFaaS](https://www.openfaas.com/){:target="_blank"}
+- [AWS EventBridge](https://aws.amazon.com/eventbridge/?nc1=h_ls){:target="_blank"}
+
+# Event Handling
+
+As described in the [architecture section](intro-architecture.md), due to the microservices architecture used in the VMware Event Broker Appliance one always has to consider message delivery problems such as timeouts, delays, reordering, loss. These challenges are fundamental to [distributed systems](https://github.com/papers-we-love/papers-we-love/blob/master/distributed_systems/a-note-on-distributed-computing.pdf){:target="_blank"} and must be understood and considered by function authors.
+
+## Event Types supported
+
+For the supported event stream source, e.g. VMware vCenter, all events provided by that source can be used. Since event types are environment specific (vSphere version, extensions), a list of events for vCenter as an event source can be generated as described in this [blog post](https://www.virtuallyghetto.com/2019/12/listing-all-events-for-vcenter-server.html){:target="_blank"}.
+
+## Message Delivery Guarantees
+
+Consider the following most basic form of messaging between two systems:
+
+[PRODUCER]------[MESSAGE]----->[CONSUMER]
+[PRODUCER]<---[MESSAGE_ACK]---[CONSUMER]
+
+Even though this example looks simple, a lot of things can go wrong when transferring a message over the network (vs in-process communication):
+
+- The message might never be received by the consumer
+- The message might arrive out of order (previous message not shown here)
+- The message might be delayed during transport
+- The message might be duplicated during transport
+- The consumer might be slow acknowledging the message
+- The consumer might receive the message and then crash before acknowledging it
+- The consumer acknowledges the message but this message is lost/delayed/arrives out of order
+- The producer crashes immediately after receiving the acknowledgement
+
+> **Note:** For our example, it doesn't really matter whether the packet (message) actually leaves the machine or the destination (consumer) is on the same host. Of course, having a physical network in between the actors increases the chances of [messaging failures](https://queue.acm.org/detail.cfm?id=2655736){:target="_blank"}. The network protocol in use was intentionally left unspecified.
+
+One of the following message delivery semantics is typically used to describe the messaging characteristics of a distributed system such as the VMware Event Broker Appliance:
+
+- At most once semantics: a message will be delivered once or not at all to the consumer
+- At least once semantics: a message will be delivered once or multiple times to the consumer
+- Exactly once semantics: a message will be delivered exactly once to the consumer
+
+> **Note:** Exactly once semantics is not supported by all messaging systems as it requires significant engineering effort to implement. It is considered the gold standard in messaging while at the same time being a highly [debated](https://medium.com/@jaykreps/exactly-once-support-in-apache-kafka-55e1fdd0a35f){:target="_blank"} topic.
+
+As of today the VMware Event Broker Appliance guarantees at most once delivery. While this might sound like a huge limitation in the appliance (and it might be, depending on your use case) in practice the chances for message delivery failures are/can be reduced by:
+
+- Using TCP/IP as the underlying communication protocol which provides certain ordering (sequencing), back-pressure and retry capabilities at the transmission layer (default in the appliance)
+- Using asynchronous function [invocation](#invocation) (defaults to "off", i.e. "synchronous", in the appliance) which internally uses a message queue for event processing
+- Following [best practices](contribute-functions.md) for writing functions
+
+> **Note:** The VMware Event Broker Appliance currently does not persist (to disk) or retry event delivery in case of failure during function invocation or upstream (external system, such as Slack) communication issues. For introspection and debugging purposes invocations are logged to standard output by the OpenFaaS vcenter-connector ("sync" invocation mode) or OpenFaaS queue-worker ("async" invocation mode).
+
+We are currently investigating options to support at least once delivery semantics. However, this requires significant changes to the event router such as:
+
+- Tracking and checkpointing (to disk) successfully processed vCenter events (stream history position)
+- Buffering events in the connector (incl. queue management to protect from overflows)
+- Raising awareness (docs, tutorials) for function authors to deal with duplicated, delayed or out of order arriving event messages
+- High-availability deployments (active-active/active-passive) to continue to retrieve the event stream during appliance downtime (maintenance, crash)
+- Describe mitigation strategies for data loss in the appliance (snapshots, backups)
+
+## Invocation
+
+Functions in OpenFaaS can be invoked synchronously or asynchronously:
+
+`synchronous:` The function is called and the caller, e.g. OpenFaaS vcenter-connector, waits until the function returns (successful/error) or the timeout threshold is hit.
+
+`asynchronous:` The function is not directly called. Instead, HTTP status code 202 ("accepted") is returned and the request, including the event payload, is stored in a [NATS Streaming](https://docs.nats.io/nats-streaming-concepts/intro){:target="_blank"} queue. One or more "queue-workers" process the queue items.
+
+If you directly invoke your functions deployed in the appliance you can decide which invocation mode is used (per function). More details can be found [here](https://github.com/openfaas/workshop/blob/master/lab7.md){:target="_blank"}.
+
+The VMware Event Broker appliance by default uses synchronous invocation mode. If you experience performance issues due to long-running/slow/blocking functions, consider running the VMware Event Router in asynchronous mode by setting the `"async"` option to `"true"` (quotes required) in the configuration file for the VMware Event Router deployment:
+
+```json
+{
+ "type": "processor",
+ "provider": "openfaas",
+ "address": "http://127.0.0.1:8080",
+ "auth": {
+ ...skipped
+ }
+ },
+ "options": {
+ "async": "true"
+ }
+}
+```
+
+When the AWS EventBridge [event processor](#components) is used, events are only forwarded for the patterns configured in the AWS event rule ARN. For example, if the rule is configured with this event pattern:
+
+```json
+{
+ "detail": {
+ "subject": [
+ "VmPoweredOnEvent",
+ "VmPoweredOffEvent",
+ "VmReconfiguredEvent"
+ ]
+ }
+}
+```
+
+Only these three vCenter event types would be forwarded. Other events are discarded to save network bandwidth and costs.
\ No newline at end of file
diff --git a/docs/kb/intro-functions.md b/docs/kb/intro-functions.md
new file mode 100644
index 00000000..cc614c21
--- /dev/null
+++ b/docs/kb/intro-functions.md
@@ -0,0 +1,68 @@
+---
+layout: docs
+toc_id: intro-functions
+title: VMware Event Broker Appliance - Intro to Functions
+description: VMware Event Broker Appliance - Intro to Functions
+permalink: /kb/functions
+cta:
+ title: Get Started
+ description: Extend your vCenter seamlessly with our pre-built functions
+ actions:
+ - text: Install the [Appliance with OpenFaaS](install-openfaas) to extend your SDDC with our [community-sourced functions](/examples)
+ - text: Learn more about the [Events in vCenter](vcenter-events) and the [Event Specification](eventspec) used to send the events to a Function
+ - text: Find steps to deploy a function - [instructions](use-functions).
+---
+
+# Functions
+
+## Getting Started
+
+The VMware Event Broker Appliance deployed with OpenFaaS provides a Function-as-a-Service (FaaS) platform. Alex Ellis, the creator of OpenFaaS, and the community have put together comprehensive documentation and workshop materials to get you started with writing your first functions:
+
+- [OpenFaaS Workshop](https://docs.openfaas.com/tutorials/workshop/){:target="_blank"}
+- [Your first OpenFaaS Function with Python](https://docs.openfaas.com/tutorials/first-python-function/){:target="_blank"}
+- [Writing your first Serverless function](https://medium.com/@pkblah/writing-your-first-serverless-function-23508cb4ea11?source=friends_link&sk=90cbed9b0dadb67578cebe54a88df494){:target="_blank"}
+- [Serverless Function - Quickstart templates](https://medium.com/@pkblah/serverless-function-templates-available-2642bb92f58b?source=friends_link&sk=888a695eb9b4c1105f2bedc8478700b1){:target="_blank"}
+
+Users who directly want to jump into VMware vSphere-related function code might want to check out the examples we provide [here](/examples).
+
+## Naming and Version Control
+
+When it comes to authoring functions, it's important to understand how the different fields in the OpenFaaS function's stack definition, e.g. `stack.yml`, are used throughout the appliance. Let's take the following excerpt as an example:
+
+```yaml
+# stack.yaml snippet
+[...]
+functions:
+ pytag-fn:
+ lang: python3
+ handler: ./handler
+ image: embano1/pytag-fn:0.2
+```
+
+`pytag-fn:` The name of the function used by OpenFaaS as the canonical name and identifier throughout the lifecycle of the function. Internally this will be the name used by Kubernetes to run the function as a Kubernetes deployment.
+
+
+
+The value of this field:
+
+- must not conflict with an existing function
+- should not contain special characters, e.g. "$" or "/"
+- should represent the intent of the function, e.g. "tag" or "tagging"
+- may use a major version suffix, e.g. "pytag-fn-v3" in case of breaking changes/when multiple versions of the function need to run in parallel for backwards compatibility
+
+`image:` The name of the resulting container image following Docker naming conventions `"/:"`. OpenFaaS uses this field during the build and deployment phases, i.e. `faas-cli [build|deploy]`. Internally this will be the image pulled by Kubernetes when creating the function.
+
+The value of this field:
+
+- must resolve to a valid Docker container name (see convention above)
+- should reflect the name of the function for clarity
+- should use a tag other than `"latest"`, e.g. `":0.2"` or `":$GIT_COMMIT"`
+- should be updated whenever changes to the function logic are made (before `faas-cli [build|deploy]`)
+ - avoids overwriting the existing container image which ensures audibility and eases troubleshooting
+ - supports common CI/CD version control flows
+ - changing the tag is sufficient
+
+
+> **Note:** `functions` can contain multiple functions described as a list in YAML (not shown here).
+
diff --git a/docs/kb/troubleshoot-appliance.md b/docs/kb/troubleshoot-appliance.md
new file mode 100644
index 00000000..7208eed4
--- /dev/null
+++ b/docs/kb/troubleshoot-appliance.md
@@ -0,0 +1,264 @@
+---
+layout: docs
+toc_id: troubleshoot-appliance
+title: VMware Event Broker Appliance Troubleshooting
+description: Troubleshooting guide for general appliance issues
+permalink: /kb/troubleshoot-appliance
+cta:
+ title: Still having trouble?
+ description: Please submit bug reports and feature requests by using our GitHub [Issues](https://github.com/vmware-samples/vcenter-event-broker-appliance/issues){:target="_blank"} page or Join us on slack [#vcenter-event-broker-appliance](https://vmwarecode.slack.com/archives/CQLT9B5AA){:target="_blank"} on vmwarecode.slack.com.
+---
+# VMware Event Broker Appliance - Troubleshooting
+
+## Requirements
+
+You must log on to the VMware Event Broker appliance as root. You can do this from the console. If you want SSH access, execute the following command:
+
+```bash
+systemctl start sshd
+```
+
+This turns on the SSH daemon but does not enable it to start on appliance boot. You should now be able to SSH into the appliance.
+
+If you wish to disable the SSH daemon when you are done troubleshooting, execute the following command:
+
+```bash
+systemctl stop sshd
+```
+
+## Troubleshooting an initial deployment
+
+If the appliance is not working immediately after deployment, the first thing to do is check your Kubernetes pods.
+
+```bash
+kubectl get pods -A
+```
+
+Here is the command output:
+
+```
+NAMESPACE NAME READY STATUS RESTARTS AGE
+kube-system coredns-584795fc57-hcvxh 1/1 Running 1 4d15h
+kube-system coredns-584795fc57-hf72w 1/1 Running 1 4d15h
+kube-system etcd-veba02 1/1 Running 1 4d15h
+kube-system kube-apiserver-veba02 1/1 Running 1 4d15h
+kube-system kube-controller-manager-veba02 1/1 Running 1 4d15h
+kube-system kube-proxy-fj47p 1/1 Running 1 4d15h
+kube-system kube-scheduler-veba02 1/1 Running 1 4d15h
+kube-system weave-net-vs8ls 2/2 Running 4 4d15h
+projectcontour contour-5cddfc8f6-8hzd6 1/1 Running 1 4d15h
+projectcontour contour-5cddfc8f6-jq7d8 1/1 Running 1 4d15h
+projectcontour contour-certgen-f92l5 0/1 Completed 0 4d15h
+projectcontour envoy-gcmqt 1/1 Running 1 4d15h
+vmware tinywww-7fcfc6fb94-mfltm 1/1 Running 1 4d15h
+vmware vmware-event-router-5dd9c8f858-5c9mh 0/1 CrashLoopBackoff 6 4d13h
+```
+
+> **Note:** The status ```Completed``` of the container ```contour-certgen-f92l5``` is expected after successful appliance deployment.
+
+One of the first things to look for is whether a pod is in a crash state. In this case, the vmware-event-router pod is crashing. We need to look at the logs with this command:
+
+```bash
+kubectl logs vmware-event-router-5dd9c8f858-5c9mh -n vmware
+```
+
+> **Note:** The pod suffix ```-5dd9c8f858-5c9mh``` will be different in each environment
+
+Here is the command output:
+
+```
+ _ ____ ___ ______ __ ____ __
+| | / / |/ / ______ _________ / ____/ _____ ____ / /_ / __ \____ __ __/ /____ _____
+| | / / /|_/ / | /| / / __ / ___/ _ \ / __/ | | / / _ \/ __ \/ __/ / /_/ / __ \/ / / / __/ _ \/ ___/
+| |/ / / / /| |/ |/ / /_/ / / / __/ / /___ | |/ / __/ / / / /_ / _, _/ /_/ / /_/ / /_/ __/ /
+|___/_/ /_/ |__/|__/\__,_/_/ \___/ /_____/ |___/\___/_/ /_/\__/ /_/ |_|\____/\__,_/\__/\___/_/
+
+
+[VMware Event Router] 2020/03/10 18:59:47 connecting to vCenter https://vc01.labad.int/sdk
+[VMware Event Router] 2020/03/10 18:59:52 could not connect to vCenter: could not create vCenter client: ServerFaultCode: Cannot complete login due to an incorrect user name or password.
+```
+
+The error message shows us that we made a mistake when we configured our username or password. We must now edit the Event Router JSON configuration file to fix the mistake.
+
+```bash
+vi /root/event-router-config.json
+
+```
+
+Here is some of the JSON from the config file - you can see the mistake in the credentials. Fix the credentials and save the file.
+
+```json
+[
+ {
+ "type": "stream",
+ "provider": "vmware_vcenter",
+ "address": "https://vc01.labad.int/sdk",
+ "auth": {
+ "method": "user_password",
+ "secret": {
+ "username": "administrator@vsphere.local",
+ "password": "WrongPassword"
+ }
+ },
+ "options": {
+ "insecure": "true"
+ }
+ }
+]
+```
+
+We now fix the Kubernetes configuration with 3 commands - delete and recreate the secret file, then delete the broken pod. Kubernetes will automatically spin up a new pod with the new configuration. We need to do this because the JSON configuration file is not directly referenced by the event router. The JSON file is mounted into the event router pod as a Kubernetes secret.
+
+```
+kubectl -n vmware delete secret event-router-config
+kubectl -n vmware create secret generic event-router-config --from-file=event-router-config.json
+kubectl -n vmware delete pod vmware-event-router-5dd9c8f858-5c9mh
+```
+
+We get a pod list again to determine the name of the new pod.
+
+```
+kubectl get pods -A
+```
+
+Here is the command output:
+```
+NAMESPACE NAME READY STATUS RESTARTS AGE
+kube-system coredns-584795fc57-hcvxh 1/1 Running 1 4d19h
+kube-system coredns-584795fc57-hf72w 1/1 Running 1 4d19h
+kube-system etcd-veba02 1/1 Running 1 4d19h
+kube-system kube-apiserver-veba02 1/1 Running 1 4d19h
+kube-system kube-controller-manager-veba02 1/1 Running 1 4d19h
+kube-system kube-proxy-fj47p 1/1 Running 1 4d19h
+kube-system kube-scheduler-veba02 1/1 Running 1 4d19h
+kube-system weave-net-vs8ls 2/2 Running 4 4d19h
+projectcontour contour-5cddfc8f6-8hzd6 1/1 Running 1 4d19h
+projectcontour contour-5cddfc8f6-jq7d8 1/1 Running 1 4d19h
+projectcontour contour-certgen-f92l5 0/1 Completed 0 4d19h
+projectcontour envoy-gcmqt 1/1 Running 1 4d19h
+vmware tinywww-7fcfc6fb94-mfltm 1/1 Running 1 4d19h
+vmware vmware-event-router-5dd9c8f858-5c9mh 0/1 Terminating 40 3h9m
+vmware vmware-event-router-5dd9c8f858-wt64s 1/1 Running 0 28s
+```
+
+Now view the event router logs.
+
+```bash
+kubectl logs -n vmware vmware-event-router-5dd9c8f858-wt64s
+```
+
+Here is the command output:
+```
+
+ _ ____ ___ ______ __ ____ __
+| | / / |/ / ______ _________ / ____/ _____ ____ / /_ / __ \____ __ __/ /____ _____
+| | / / /|_/ / | /| / / __ / ___/ _ \ / __/ | | / / _ \/ __ \/ __/ / /_/ / __ \/ / / / __/ _ \/ ___/
+| |/ / / / /| |/ |/ / /_/ / / / __/ / /___ | |/ / __/ / / / /_ / _, _/ /_/ / /_/ / /_/ __/ /
+|___/_/ /_/ |__/|__/\__,_/_/ \___/ /_____/ |___/\___/_/ /_/\__/ /_/ |_|\____/\__,_/\__/\___/_/
+
+
+[VMware Event Router] 2020/03/10 20:37:28 connecting to vCenter https://vc01.labad.int/sdk/sdk
+[VMware Event Router] 2020/03/10 20:37:28 connecting to OpenFaaS gateway http://gateway.openfaas:8080 (async mode: false)
+[VMware Event Router] 2020/03/10 20:37:28 exposing metrics server on 0.0.0.0:8080 (auth: basic_auth)
+[Metrics Server] 2020/03/10 20:37:28 starting metrics server and listening on "http://0.0.0.0:8080/stats"
+2020/03/10 20:37:28 Syncing topic map
+[OpenFaaS] 2020/03/10 20:37:28 processing event [0] of type *types.UserLoginSessionEvent from source https://vc01.labad.int/sdk: &{SessionEvent:{Event:{DynamicData:{} Key:8755384 ChainId:8755384 CreatedTime:2020-03-10 20:36:19.594 +0000 UTC UserName:: ComputeResource: Host: Vm: Ds: Net: Dvs: FullFormattedMessage:User @10.46.144.4 logged in as VMware vim-java 1.0 ChangeTag:}} IpAddress:192.168.10.24 UserAgent:VMware vim-java 1.0 Locale:en SessionId:5254c9e5-4c2d-0af0-cae3-7fdebdc2eacb}
+```
+
+We now see that the Event Router came online, connected to vCenter, and successfully received an event.
+
+## Changing the vCenter service account
+
+If you need to change the account the appliance uses to connect to vCenter, use the following procedure.
+
+Open a console to the appliance. If you want to do the configuration via SSH, you must first enable the SSH daemon with the following command
+```bash
+systemcl start sshd
+```
+The SSH daemon will run but not automatically start with the next reboot. You can use the same command with `stop` instead of `start` when you are finished. Or you can type everything directly into the console if you do not want to use SSH.
+
+Edit the configuration file with vi
+```bash
+vi /root/event-router-config.json
+```
+
+The editor will open with output similar to this (truncated)
+```bash
+[{
+ "type": "stream",
+ "provider": "vmware_vcenter",
+ "address": "https://vc01.lab.int/sdk",
+ "auth": {
+ "method": "user_password",
+ "secret": {
+ "username": "administrator@vsphere.local",
+ "password": "KeepMeSecure123!"
+ }
+ },
+ "options": {
+ "insecure": "true"
+ }
+ },
+```
+
+Change the username and password, then save the file. Then delete and recreate the event router pod secret with the following commands:
+```bash
+kubectl -n vmware delete secret event-router-config
+kubectl -n vmware create secret generic event-router-config --from-file=/root/event-router-config.json
+```
+
+Now, restart the event router pod. Get the current pod name with the following command:
+```bash
+kubectl get pods -A
+```
+You will see output similar the following (trucnated):
+```bash
+projectcontour contour-certgen-7r9dl 0/1 Completed 0 22d
+projectcontour envoy-htrwv 1/1 Running 1 22d
+vmware tinywww-7fcfc6fb94-tv98j 1/1 Running 1 22d
+vmware vmware-event-router-5dd9c8f858-7htv5 1/1 Running 14 19d
+```
+
+Find the event router pod. Every environment will have a unique suffix on the pod name - in this example, it is `-5dd9c8f858-7htv5`. Delete the event router pod with the following command (make sure to match the pod name with the one in your environment):
+```bash
+kubectl -n vmware delete pod vmware-event-router-5dd9c8f858-7htv5
+```
+
+The pod will automatically recreate itself. You can repeatedly run the following command:
+```bash
+kubectl get pods -A
+```
+Various stages of the pod lifecycle may be shown. Here, we see the original pod terminating while the new pod is spinning up.
+```bash
+projectcontour envoy-htrwv 1/1 Running 1 22d
+vmware tinywww-7fcfc6fb94-tv98j 1/1 Running 1 22d
+vmware vmware-event-router-5dd9c8f858-7htv5 0/1 Terminating 14 19d
+vmware vmware-event-router-5dd9c8f858-l6gdj 0/1 ContainerCreating 0 4s
+```
+
+Eventually the old pod will disappear and the new pod will show as running:
+```bash
+projectcontour contour-certgen-7r9dl 0/1 Completed 0 22d
+projectcontour envoy-htrwv 1/1 Running 1 22d
+vmware tinywww-7fcfc6fb94-tv98j 1/1 Running 1 22d
+vmware vmware-event-router-5dd9c8f858-l6gdj 1/1 Running 0 92s
+```
+
+You can check the pod logs with the following command (make sure to add the correct suffix shown in your environment):
+```bash
+kubectl logs -n vmware kubectl logs -n vmware vmware-event-router-5dd9c8f858-n9pg6
+```
+You should see a successful connection to vCenter in the logs
+```bash
+ _ ____ ___ ______ __ ____ __
+| | / / |/ / ______ _________ / ____/ _____ ____ / /_ / __ \____ __ __/ /____ _____
+| | / / /|_/ / | /| / / __ / ___/ _ \ / __/ | | / / _ \/ __ \/ __/ / /_/ / __ \/ / / / __/ _ \/ ___/
+| |/ / / / /| |/ |/ / /_/ / / / __/ / /___ | |/ / __/ / / / /_ / _, _/ /_/ / /_/ / /_/ __/ /
+|___/_/ /_/ |__/|__/\__,_/_/ \___/ /_____/ |___/\___/_/ /_/\__/ /_/ |_|\____/\__,_/\__/\___/_/
+
+
+[VMware Event Router] 2020/04/08 05:07:11 connecting to vCenter https://vc01.lab.int/sdk
+[VMware Event Router] 2020/04/08 05:07:11 connecting to OpenFaaS gateway http://gateway.openfaas:8080 (async mode: false)
+[VMware Event Router] 2020/04/08 05:07:11 exposing metrics server on 0.0.0.0:8080 (auth: basic_auth)
+[Metrics Server] 2020/04/08 05:07:11 starting metrics server and listening on "http://0.0.0.0:8080/stats"
+```
\ No newline at end of file
diff --git a/docs/kb/troubleshoot-functions.md b/docs/kb/troubleshoot-functions.md
new file mode 100644
index 00000000..d83044fc
--- /dev/null
+++ b/docs/kb/troubleshoot-functions.md
@@ -0,0 +1,182 @@
+---
+layout: docs
+toc_id: troubleshoot-functions
+title: VMware Event Broker Function Troubleshooting
+description: Troubleshooting guide for general function issues
+permalink: /kb/troubleshoot-functions
+cta:
+ title: Still having trouble?
+ description: Please submit bug reports and feature requests by using our GitHub [Issues](https://github.com/vmware-samples/vcenter-event-broker-appliance/issues){:target="_blank"} page or Join us on slack [#vcenter-event-broker-appliance](https://vmwarecode.slack.com/archives/CQLT9B5AA){:target="_blank"} on vmwarecode.slack.com.
+---
+
+## OpenFaaS Function Troubleshooting
+
+If a function is not behaving as expected, you can look at the logs to troubleshoot. First, SSH or console to the appliance as shown in the Requirements section.
+
+List out the pods.
+
+```bash
+kubectl get pods -A
+```
+
+This is the function output:
+
+```
+NAMESPACE NAME READY STATUS RESTARTS AGE
+kube-system coredns-584795fc57-4bp2s 1/1 Running 1 6d4h
+kube-system coredns-584795fc57-76pwr 1/1 Running 1 6d4h
+kube-system etcd-veba01 1/1 Running 2 6d4h
+kube-system kube-apiserver-veba01 1/1 Running 2 6d4h
+kube-system kube-controller-manager-veba01 1/1 Running 3 6d4h
+kube-system kube-proxy-fvf2n 1/1 Running 2 6d4h
+kube-system kube-scheduler-veba01 1/1 Running 2 6d4h
+kube-system weave-net-v9jss 2/2 Running 6 6d4h
+openfaas-fn powercli-entermaint-d84fd8d85-sjdgl 1/1 Running 1 6d4h
+openfaas alertmanager-58f8d787d9-nqwm8 1/1 Running 1 6d4h
+openfaas basic-auth-plugin-dd49cd66b-rv6n7 1/1 Running 1 6d4h
+openfaas faas-idler-59ff9778fd-84szz 1/1 Running 4 6d4h
+openfaas gateway-74f6f9489b-btgz8 2/2 Running 5 6d4h
+openfaas nats-6dfbf45d77-9swph 1/1 Running 1 6d4h
+openfaas prometheus-5f5494b54f-srs2d 1/1 Running 1 6d4h
+openfaas queue-worker-59b67bf4-wqhm5 1/1 Running 4 6d4h
+projectcontour contour-5cddfc8f6-hpzn8 1/1 Running 1 6d4h
+projectcontour contour-5cddfc8f6-tdv2r 1/1 Running 2 6d4h
+projectcontour contour-certgen-wrgnb 0/1 Completed 0 6d4h
+projectcontour envoy-8mdhb 1/1 Running 2 6d4h
+vmware tinywww-7fcfc6fb94-v7ncj 1/1 Running 1 6d4h
+vmware vmware-event-router-5dd9c8f858-9g44h 1/1 Running 4 6d4h
+```
+
+First, we want to see if the event router is capturing events and forwarding them on to a function.
+
+Use this command to follow the live Event Router log.
+
+```bash
+kubectl logs -n vmware vmware-event-router-5dd9c8f858-9g44h --follow
+```
+
+For this sample troubleshooting, we have the sample hostmaintenance alarms function running. To see if the appliance is properly handling the event, we put a host into maintenance mode.
+
+When we look at the log output, we see various entries regarding EnteredMaintenanceModeEvent, ending with the following:
+
+```
+[OpenFaaS] 2020/03/11 22:15:09 invoking function(s) on topic: EnteredMaintenanceModeEvent
+[OpenFaaS] 2020/03/11 22:15:09 successfully invoked function powercli-entermaint for topic EnteredMaintenanceModeEvent
+```
+
+This lets us know that the function was invoked. If we still don't see the expected result, we need to look at the function logs.
+
+Each OpenFaaS function will have its own pod running in the openfaas-fn namespace. We can examine the logs with the following command.
+
+```bash
+kubectl logs -n openfaas-fn powercli-entermaint-d84fd8d85-sjdgl
+```
+
+We don't need the --follow switch because we are just trying to look at recent logs, but --follow would work too.
+Some other useful switches are `--since` and `--tail`.
+
+This command will show you the last 5 minutes worth of logs.
+
+```bash
+kubectl logs -n openfaas-fn powercli-entermaint-d84fd8d85-sjdgl --since=5m
+```
+
+This command will show you the last 20 lines of logs.
+
+```bash
+kubectl logs -n openfaas-fn powercli-entermaint-d84fd8d85-sjdgl --tail=20
+```
+
+Log output showing a succesful function invocation:
+
+```
+Connecting to vCenter Server ...
+
+Disabling alarm actions on host: esx01.labad.int
+Disconnecting from vCenter Server ...
+
+2020/03/11 22:15:15 Duration: 6.085448 seconds
+```
+
+An alternative way to troubleshoot OpenFaaS logs is to use `faas-cli`.
+
+This faas-cli command will show all available functions in the appliance. ```--tls-no-verify``` bypasses SSL certificate validation
+
+```bash
+faas-cli list --tls-no-verify
+```
+
+The command output is:
+
+```
+Function Invocations Replicas
+powercli-entermaint 3 1
+```
+
+We can look at the logs with this command.
+
+```bash
+faas-cli logs powercli-entermaint --tls-no-verify
+```
+
+The logs are the same:
+
+```
+2020-03-11T22:15:15Z Connecting to vCenter Server ...
+2020-03-11T22:15:15Z
+2020-03-11T22:15:15Z Disabling alarm actions on host: esx01.labad.int
+2020-03-11T22:15:15Z Disconnecting from vCenter Server ...
+2020-03-11T22:15:15Z
+2020-03-11T22:15:15Z 2020/03/11 22:15:15 Duration: 6.085448 seconds
+```
+
+All of the same switches shown in the kubectl commands such as `--tail` and `--since` work with `faas-cli`.
+
+> **Note:** `faas-cli` will stop tailing the log after a fixed period.
+
+## OpenFaaS Gateway not available
+
+### Self-signed certificate errors
+
+The most common issue with OpenFaaS is related to certificates. The appliance certificate is self-signed. Attempting to connect to it without ignoring certificate errors results in an error, shown below
+
+```bash
+faas-cli secret list
+```
+
+The command output is:
+```bash
+Cannot connect to OpenFaaS on URL: https://veba02.lab.int
+```
+
+Adding the switch `--tls-no-verify` allows you to bypass SSL errors
+```bash
+faas-cli secret list --tls-no-verify
+```
+
+The command output is:
+```bash
+NAME
+vc-hostmaint-config
+```
+Alternatively, you can replace the self-signed certificate with a signed certificate
+
+### Requirements
+* Root access to the appliance
+* A public/private key pair copied to a folder on the appliance filesystem
+
+Run the following commands
+
+```bash
+cd /path/to/your/cert/files
+CERT_NAME=eventrouter-tls
+KEY_FILE=yourkeyfile.pem
+CERT_FILE=yourcertfile.cer
+
+#recreate the tls secret
+kubectl --kubeconfig /root/.kube/config -n vmware delete secret ${CERT_NAME}
+kubectl --kubeconfig /root/.kube/config -n vmware create secret tls ${CERT_NAME} --key ${KEY_FILE} --cert ${CERT_FILE}
+
+#reapply the config to take the new certificate
+kubectl --kubeconfig /root/.kube/config apply -f /root/ingressroute-gateway.yaml
+```
diff --git a/docs/kb/use-eventspec.md b/docs/kb/use-eventspec.md
new file mode 100644
index 00000000..f0f7038c
--- /dev/null
+++ b/docs/kb/use-eventspec.md
@@ -0,0 +1,58 @@
+---
+layout: docs
+toc_id: use-eventspec
+title: VMware Event Broker Appliance - Architecture
+description: VMware Event Broker Appliance Architecture
+permalink: /kb/eventspec
+cta:
+ title: Get Started
+ description: Explore the capabilities that the VMware Event Router enables
+ actions:
+ - text: Get started quickly by deploying from the [community-sourced, pre-built functions](/examples)
+ - text: Deploy a function using these [instructions](use-functions) and learn how to [write your own function](contribute-functions).
+---
+
+# The Event Specification
+
+The event payload structure used by the VMware Event Broker Appliance follows the [CloudEvents](https://github.com/cloudevents/sdk-go/blob/master/pkg/cloudevents/eventcontext_v1.go){:target="_blank"} v1 specification for cross-cloud portability. The current data content type which is sent as payload to a supported event processor is JSON.
+
+The following example shows the event structure sent as JSON to a supported event processor (trimmed for better readability):
+
+```json
+{
+ "id": "08179137-b8e0-4973-b05f-8f212bf5003b",
+ "source": "https://vcenter-01:443/sdk",
+ "specversion": "1.0",
+ "type": "com.vmware.event.router/event",
+ "subject": "VmPoweredOffEvent",
+ "time": "2020-02-11T21:29:54.9052539Z",
+ "data": {
+ "Key": 9902,
+ "ChainId": 9895,
+ [.....]
+ },
+ "datacontenttype": "application/json"
+}
+```
+
+`id:` The unique ID ([UUID](https://tools.ietf.org/html/rfc4122){:target="_blank"}) of the event
+
+`source:` The vCenter emitting the embedded vSphere event (FQDN resolved when available)
+
+`specversion:` The event specification the appliances uses (can be used for schema handling)
+
+`type:` The canonical name of the event class in "." dot notation
+
+`subject:` The vCenter event name (CamelCase)
+
+`time:` Timestamp when this event was produced by the appliance
+
+`data:` Original vCenter event
+
+`data.Key:` Monotonically increasing value set by vCenter (the lower the key, the older the message as being created by vCenter)
+
+`data.CreatedTime:` When the embedded event was created by vCenter
+
+`datacontenttype:` Encoding used (JSON)
+
+Please see the section on function [best practices](contribute-functions.md) below how you can make use of these fields for advanced requirements.
diff --git a/docs/kb/use-functions.md b/docs/kb/use-functions.md
new file mode 100644
index 00000000..f364cb1f
--- /dev/null
+++ b/docs/kb/use-functions.md
@@ -0,0 +1,94 @@
+---
+layout: docs
+toc_id: use-functions
+title: VMware Event Broker Appliance - Using Functions
+description: VMware Event Broker Appliance - Using Functions
+permalink: /kb/use-functions
+cta:
+ title: What's next?
+ description: Extend your vCenter quickly with our pre-built functions
+ actions:
+ - text: See our complete list of prebuilt functions - [here](/examples)
+ - text: Learn how to write your own function - [here](contribute-functions).
+---
+
+# Getting started with using functions
+
+The steps below describe a generalized deployment step of a function on the VMware Event Broker Appliance configured with OpenFaaS as the Event Processor. For customers looking to get started quickly, please look at deploying from our growing list of [Prebuilt Functions](/examples). The functions are organized by the language that they are written in and have well-documented README.md files with detailed deployment steps.
+
+## Function deployment steps
+
+For this walk-through, the `host-maint-alarms` function from the example folder is used.
+
+### Prerequisites
+
+Before proceeding to deploy a function, you must have VMware Event Broker Appliance deployed and be able to login to OpenFaaS.
+
+```bash
+#Use your appliance URL and OpenFaaS password
+export OPENFAAS_URL='https://veba.primp-industries.com'
+faas-cli login -p YourPassword
+```
+> **NOTE:** You may have to use the `--tls-no-verify` flag as the appliance utilizes self-signed certificates by default. You can update the certificates following this guide [here](advanced-certificates)
+
+An alternative way to log in if you don't want your password showing up in command history is to put the password in a text file and use this command:
+```bash
+cat password.txt | faas-cli login --password-stdin
+```
+
+### Step 1 - Clone repo
+
+```
+git clone https://github.com/vmware-samples/vcenter-event-broker-appliance
+cd vcenter-event-broker-appliance/examples/powercli/hostmaint-alarms
+git checkout master
+```
+
+### Step 2 - Edit the configuration files
+
+* Edit `stack.yml` to update `gateway:` with the specific appliance URL in your environment. Notice event(s) next to `topics:` - all available events can be reviewed in the [vCenter Event Mapping](https://github.com/lamw/vcenter-event-mapping){:target="_blank"} document.
+
+```yaml
+version: 1.0
+provider:
+ name: openfaas
+ gateway: https://veba.primp-industries.com
+functions:
+ powercli-entermaint:
+ lang: powercli
+ handler: ./handler
+ image: vmware/veba-powercli-esx-maintenance:latest
+ environment:
+ write_debug: true
+ read_debug: true
+ function_debug: false
+ secrets:
+ - vc-hostmaint-config
+ annotations:
+ topic: EnteredMaintenanceModeEvent,ExitMaintenanceModeEvent
+```
+
+* Most functions also have a secrets configuration file that you must edit to match your environment. For the `hostmaint-alarms` function, the file is named `vc-hostmaint-config.json`
+```json
+{
+ "VC" : "https://veba.primp-industries.com",
+ "VC_USERNAME" : "veba@vsphere.local",
+ "VC_PASSWORD" : "FillMeIn"
+}
+```
+Then create the secret in OpenFaaS with this command:
+```bash
+faas-cli secret create vc-hostmaint-config --from-file=vc-hostmaint-config.json
+```
+
+
+### Step 3 - Deploy function to VMware Event Broker Appliance
+
+```
+faas-cli deploy -f stack.yml
+```
+
+### Step 4 - Test and Invoke your functions
+
+* Your function is now deployed to OpenFaaS and available for VMware Event Router to invoke when it sees a matching event
+* You can also test or invoke your functions using the http endpoint for the function that OpenFaaS makes available. Pass the expected CloudEvents to the function as the http request parameter
diff --git a/docs/kb/use-vcenter-events.md b/docs/kb/use-vcenter-events.md
new file mode 100644
index 00000000..15b5fa5a
--- /dev/null
+++ b/docs/kb/use-vcenter-events.md
@@ -0,0 +1,78 @@
+---
+layout: docs
+toc_id: use-vcenter-events
+title: VMware Event Broker Appliance - vCenter Events
+description: VMware Event Broker Appliance - vCenter Events
+permalink: /kb/vcenter-events
+cta:
+ title: Deploy Event-Driven Functions
+ description: Extend your vCenter seamlessly with our pre-built functions
+ actions:
+ - text: Get started quickly by deploying from the [community-sourced, pre-built functions](/examples)
+ - text: Learn more about the [Event Specification](eventspec) to understand how the events are sent to the Functions
+ - text: Deploy a function using these [instructions](use-functions) and learn how to [write your own function](contribute-functions).
+---
+
+# vCenter Events
+
+vCenter produces events that get generated in response to actions taken on an entity such as VM, Host, Datastore, etc. These events contain immutable facts documenting the entity state changes such as who initiated the change, what action was performed, which object was modified, and when was the change initiated.
+
+Events naturally serve as auditing and troubleshooting tools, allowing an administrator to retrieve details on a specific change. Event Driven Automation builds on the construct of events and enables advanced distributed design patterns driven through Events. VMware Event Broker Appliance aims to enable this for VMware SDDC by enabling VI Administrators to write lean functions (script or code) that are triggered by vCenter Events.
+
+## Overview of the vCenter events
+
+vCenter Events are categorized by the Objects and the actions that are allowed on these objects and are documented under the vSphere API [6.7U3 reference](https://code.vmware.com/apis/704/vsphere/vim.event.Event.html){:target="_blank"}.
+
+* Event
+ * ClusterEvent
+ * ClusterCreatedEvent, ClusterDestroyedEvent, ClusterOvercommittedEvent...
+ * DatastoreEvent
+ * DatastoreCapacityIncreasedEvent, DatastoreDestroyedEvent, DatastoreDuplicatedEvent...
+ * DatacenterEvent
+ * DatacenterCreatedEvent, DatacenterRenamedEvent
+ * HostEvent
+ * HostShutdownEvent, HostAddedEvent, EnteringMaintenanceModeEvent...
+ * VMEvent
+ * VmNoNetworkAccessEvent, VmOrphanedEvent, VmPoweredOffEvent...
+ * ...
+
+There are over 1650+ events available on an out of the box install of vCenter that are provided [here](https://github.com/lamw/vcenter-event-mapping/blob/master/vsphere-6.7-update-3.md){:target="_blank"} and [here](https://www.virten.net/vmware/vcenter-events/){:target="_blank"}. You can get the complete list of events for your vCenter using the powershell script below.
+
+```powershell
+$vcNames = "hostname"
+
+Connect-VIServer -Server $vcNames
+
+$vcenterVersion = ($global:DefaultVIServer.ExtensionData.Content.About.ApiVersion)
+
+$eventMgr = Get-View $global:DefaultVIServer.ExtensionData.Content.EventManager
+
+$results = @()
+foreach ($event in $eventMgr.Description.EventInfo) {
+ if($event.key -eq "EventEx" -or $event.key -eq "ExtendedEvent") {
+ #echo $event
+ $eventId = ($event.FullFormat.toString()) -replace "\|.*",""
+ $eventType = $event.key
+ } else {
+ $eventId = $event.key
+ $eventType = "Standard"
+ }
+ $eventCategory = $event.Category
+ $eventDescription = $event.Description
+
+ $tmp = [PSCustomObject] @{
+ EventId = $eventId;
+ EventCategory = $eventCategory
+ EventType = $eventType;
+ EventDescription = $($eventDescription.Replace("<","").Replace(">",""));
+ }
+
+ $results += $tmp
+}
+
+Write-Host "Number of Events: $($results.count)"
+$results | Sort-Object -Property EventId | ConvertTo-Csv | Out-File -FilePath vcenter-$vcenterVersion-events.csv
+
+Write-Host "Disconnecting from vCenter Server ..."
+Disconnect-VIServer * -Confirm:$false
+```
diff --git a/users-and-use-cases.md b/docs/site/casestudy-wip.md
similarity index 71%
rename from users-and-use-cases.md
rename to docs/site/casestudy-wip.md
index 1588b908..36b6a25e 100644
--- a/users-and-use-cases.md
+++ b/docs/site/casestudy-wip.md
@@ -1,6 +1,6 @@
# Users and Use Cases
-Please submit a [pull request](https://github.com/vmware-samples/vcenter-event-broker-appliance/pulls) if you would like to be included in the list below.
+Please submit a [pull request](https://github.com/vmware-samples/vcenter-event-broker-appliance/pulls){:target="_blank"} if you would like to be included in the list below.
| User | Use Case | Language | Contribution |
|------|----------|----------|--------------|
diff --git a/docs/site/community.md b/docs/site/community.md
new file mode 100644
index 00000000..d50681b2
--- /dev/null
+++ b/docs/site/community.md
@@ -0,0 +1,91 @@
+---
+layout: page
+id: community
+title: Join our Community
+description: Community Resources
+permalink: /community
+links:
+- title: Twitter
+ image: /assets/img/icons/twitter.svg
+ items:
+ - description: "Follow us at "
+ url: "https://twitter.com/VMWEventBroker"
+ label: "@VMWEventBroker"
+- title: Slack
+ image: /assets/img/icons/slack.svg
+ items:
+ - description: "Join us at"
+ url: "https://vmwarecode.slack.com/archives/CQLT9B5AA"
+ label: "#vcenter-event-broker-appliance"
+- title: Email
+ image: /assets/img/icons/email.svg
+ items:
+ - description: "Email us at "
+ url: "mailto:dl-veba@vmwarem.com"
+ label: dl-veba@vmware.com
+---
+
+
+The VMware Event Broker Appliance team welcomes contributions from the community and this page presents the guidelines for contributing to VMware Event Broker Appliance.
+
+# Guidelines
+
+Following the guidelines helps to make the contribution process easy, collaborative, and productive.
+
+Before you start working with the VMware Event Broker Appliance, please read our [Developer Certificate of Origin](https://cla.vmware.com/dco){:target="_blank"}. All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch.
+
+## Submitting Bug Reports and Feature Requests
+
+Please submit bug reports and feature requests by using our GitHub [Issues](https://github.com/vmware-samples/vcenter-event-broker-appliance/issues){:target="_blank"} page.
+
+Before you submit a bug report about the code in the repository, please check the Issues page to see whether someone has already reported the problem. In the bug report, be as specific as possible about the error and the conditions under which it occurred. On what version and build did it occur? What are the steps to reproduce the bug?
+
+Feature requests should fall within the scope of the project.
+
+## Pull Requests
+
+Before submitting a pull request, please make sure that your change satisfies the following requirements:
+- The change is signed as described by the [Developer Certificate of Origin](https://cla.vmware.com/dco){:target="_blank"} doc.
+- The change is clearly documented and follows Git commit [best practices](https://chris.beams.io/posts/git-commit/){:target="_blank"}
+
+### Contributions to the Appliance
+ - See the Build Appliance document [here](/kb/contribute-appliance)
+ - See the Build Event Router document [here](/kb/contribute-eventrouter)
+ - Requestor must verify that the VMware Event Broker Appliance can be built and deployed.
+
+### Contributions to the Functions
+ - See the Build Functions document [here](/kb/contribute-functions)
+ - PR should contain information on how the function was tested (environment, version etc)
+ - PR should contain a titled readme and the title is listed in the [Functions](/examples) page
+
+### Contributions to the Website
+ - See the Build Website document [here](/kb/contribute-functions)
+ - Requestor must verify that the website change was built and tested locally
+
+Get started quickly with your contributions with our [getting started](/kb/contribute-start) guide
+
+## Join the movement
+
+
+ {% include contributors.html %}
+
+
+## Get in touch
+
+
+ {% for link in page.links %}
+
+ {% endfor %}
+
+
\ No newline at end of file
diff --git a/docs/site/examples.md b/docs/site/examples.md
new file mode 100644
index 00000000..da07dcfe
--- /dev/null
+++ b/docs/site/examples.md
@@ -0,0 +1,145 @@
+---
+layout: page
+id: functions
+title: Prebuilt Functions
+description: Community-sourced and validated prebuilt functions for OpenFaaS with VEBA
+permalink: /examples
+images:
+ powercli: /assets/img/languages/powercli.png
+ python: /assets/img/languages/python.png
+ go: /assets/img/languages/go.png
+ powershell: /assets/img/languages/powershell.png
+examples:
+ - title: vSphere Tagging
+ usecases:
+ - item: automation
+ id: vsphere-tag
+ description: Automatically tag a VM upon a vCenter event (ex. a VM can be tagged during a poweron event)
+ links:
+ - language: python
+ image: {{ page.images.python }}
+ url: "/tree/master/examples/python/tagging"
+ - language: powercli
+ url: "/tree/master/examples/powercli/tagging"
+ - language: golang
+ url: "/tree/master/examples/go/tagging"
+
+ - title: Send VM Configuration Changes to Slack
+ usecases:
+ - item: integration
+ - item: notification
+ id: config-changes-to-slack
+ description: Notify a Slack channel upon a VM configuration change event
+ links:
+ - language: powercli
+ url: "/tree/master/examples/powercli/hwchange-slack"
+
+ - title: Disable Alarms for Host Maintenance
+ usecases:
+ - item: automation
+ id: disable-host-maintenance-alarms
+ description: Disable alarm actions on a host when it has entered maintenance mode and will re-enable alarm actions on a host after it has exited maintenance mode
+ links:
+ - language: powercli
+ url: "/tree/master/examples/powercli/hostmaint-alarms"
+
+ - title: ESX Maximum transmission unit fixer
+ usecases:
+ - item: automation
+ - item: remediation
+ id: esx-mtu-fixer
+ description: Remediation function which will be triggered when a VM is powered on to ensure that the Maximum Transmission Unit (MTU) of the VM Kernel Adapter on all ESX hosts is at least 1500
+ links:
+ - language: python
+ url: "/tree/master/examples/python/esx-mtu-fixer"
+
+ - title: Datastore Usage Notification
+ usecases:
+ - item: notification
+ id: datastore-usage-notification
+ description: Send an email notification when warning/error threshold is reach for Datastore Usage Alarm in vSphere
+ links:
+ - language: powercli
+ url: "/tree/master/examples/powercli/datastore-usage-email"
+
+ - title: vRealize Orchestrator
+ usecases:
+ - item: integration
+ - item: remediation
+ id: vrealize-workflow
+ description: Trigger vRealize Orchestrator workflow using vRO REST API
+ links:
+ - language: powershell
+ url: "/tree/master/examples/powershell/vro"
+
+ - title: Echo VEBA Event
+ usecases:
+ - item: other
+ id: echo-function
+ description: Function helps users understand the structure and data of a given vCenter Event which will be useful when creating brand new Functions.
+ links:
+ - language: powershell
+ url: "/tree/master/examples/python/echo"
+
+ - title: Trigger PagerDuty incident
+ usecases:
+ - item: integration
+ - item: notification
+ - item: remediation
+ id: invoke-pagerduty
+ description: Trigger a PagerDuty incident upon a vCenter Event
+ links:
+ - language: python
+ url: "/tree/master/examples/python/trigger-pagerduty-incident"
+
+ - title: POST to any REST API
+ usecases:
+ - item: automation
+ - item: integration
+ - item: notification
+ - item: remediation
+ id: post-res-api
+ description: Function allows making a single post api request to any endpoint - tested with Slack, ServiceNow and PagerDuty
+ links:
+ - language: python
+ url: "/tree/master/examples/python/invoke-rest-api"
+---
+
+A complete and updated list of ready to use functions curated by the VMware Event Broker community is listed below.
+
+# Get started with our prebuilt functions
+
+These functions are prebuilt, available in ready to deploy container and `stack.yml` files for you to deploy as is. Should you need to modify the functions to fit your needs, the `README.md` files provided within each function folder will provide all the information you need to customize, build and deploy the function on your VMware Event Broker appliance.
+
+> **Note:** These functions are provided and tested to be used with the VMware Event Broker Appliance deployed with [OpenFaaS](/kb/install-openfaas) as the event stream processor.
+
+
+
+
Functions
+ {% for ex in page.examples %}
+
+ {{ ex.description | markdownify }}
+
+ {% for usecase in ex.usecases %}
+ {{usecase.item}}
+ {% endfor %}
+
+
+ {% endfor %}
+
+
+## Contributions
+
+These functions serve as an easy way to use the appliance and as an inspiration for how to write functions in different languages. If you have an idea for a function and are looking to write your own, start with our documentation [here](/kb/contribute-functions).
+
+Check our [contributing guidelines](\community#contributing) and join [Team #VEBA](/#team-veba) by submitting a pull request for your function to be showcased on this list.
\ No newline at end of file
diff --git a/docs/site/faq.md b/docs/site/faq.md
new file mode 100644
index 00000000..394c20ed
--- /dev/null
+++ b/docs/site/faq.md
@@ -0,0 +1,67 @@
+---
+layout: page
+id: faq
+title: Frequently Asked Questions
+description: A compilation of frequently asked questions for VMware Event Broker Appliance
+permalink: /faq
+faqs:
+- title: Common Questions - Appliance
+ id: appliance
+ items:
+ - Q: "Can I connect to more than one vCenter per Appliance deployment?"
+ A: "No. The Appliance is currently designed to support one vCenter as the event source. Customers that are familiar with deploying the components on Kubernetes can deploy multiple instances of the VMware Event Router container. "
+ - Q: "Can the default TLS certificates that are being used on the Appliance be updated?"
+ A: "Yes! Follow the steps provided [here](/kb/advanced-certificates)"
+ - Q: What happens if vCenter and VMware Event Broker connectivity is lost?
+ A: VMware Event router streams vCenter Events as they get generated and being stateless, does not persist any event information. Events that occur during this connectivity loss are not seen by the VMware event router and is currently not designed to go back in time to replay past messages.
+ - Q: How long does it take for the functions to be invoked upon an event being generated?
+ A: Instantaneous to a few seconds! The function execution itself is not considered in this answer since that is dependent on the logic that is being implemented.
+ - Q: Can I setup the VMware Event Broker Appliance components on Kubernetes?
+ A: Yes! Follow the steps provided [here](/kb/advanced-deploy-k8s).
+- title: Common Questions - Functions
+ id: function
+ items:
+ - Q: How do I obtain the Events in the function?
+ A: >
+ Events are made available as stdin argument for the language that you are writing the function on. For example,
+ - In Powershell the event is made available using the `$args` variable as shown here `$json = $args | ConvertFrom-Json`
+ - In Python the event is made available with the `req` variable as shown here `cevent = json.loads(req)`
+ - Q: How do I obtain the config file within the function?
+ A: Configs are made available under `/var/etc/config/` within your container which you can read as a file within your function.
+ - Q: Can I reuse secrets that was created for another function?
+ A: Yes, if there is a config that you'd like different functions to share, create the secret and ensure your functions `stack.yml` references this secret.
+- title: Other
+ id: other
+ items:
+ - Q: How do I get support for VMware Event Broker Appliance?
+ A: VMware Event Broker Appliance is a Fling. While it is not supported by GSS, if you find an issue, you can always open a bug on the Flings website or create an issue on our Github. Our team is very responsive and will offer assistance based on impact and availability.
+---
+
+Find answers to the frequently asked questions about VMware Event Broker Appliance and Functions.
+
+
+ {% for faq in page.faqs %}
+
{{faq.title}}
+
+ {% for item in faq.items %}
+
+
+ {{forloop.index}}.
+ {{ item.Q | markdownify }}
+
+
+ >. {{ item.A | markdownify }}
+
+
+ {% endfor %}
+
+ {% endfor %}
+
+
+## Have more questions?
+- Explore our [documentation](/kb)
+- Feel free to reach out
+ - Email us at [dl-veba@vmware.com](mailto:dl-veba@vmware.com){:target="_blank"}
+ - Join us on slack [#vcenter-event-broker-appliance](https://vmwarecode.slack.com/archives/CQLT9B5AA){:target="_blank"} on vmwarecode.slack.com
+ - Tweet at us [@VMWEventBroker](https://twitter.com/VMWEventBroker){:target="_blank"}
+ - Explore our Github repository [here](https://github.com/vmware-samples/vcenter-event-broker-appliance){:target="_blank"}
\ No newline at end of file
diff --git a/docs/site/resources.md b/docs/site/resources.md
new file mode 100644
index 00000000..8976bbee
--- /dev/null
+++ b/docs/site/resources.md
@@ -0,0 +1,16 @@
+---
+layout: resources
+title: Additional Resources
+description: Update this
+permalink: /resources
+limit: 3
+---
+
+## Blog posts
+- https://rguske.github.io/post/event-driven-interactions-with-vsphere-using-functions-as-a-service/
+- https://octo.vmware.com/vsphere-power-event-driven-automation/
+- https://www.virtuallyghetto.com/2019/11/vcenter-event-broker-appliance-updates-vmworld-fling-community-open-source.html
+- https://www.opvizor.com/audit-vm-configuration-changes-using-the-vcenter-event-broker
+- https://doogleit.github.io/2019/11/automate-host-maintenance-with-the-vcenter-event-broker-appliance/
+- https://www.patrickkremer.com/veba/
+- https://www.virtuallyghetto.com/2019/12/listing-all-events-for-vcenter-server.html
\ No newline at end of file
diff --git a/DESIGN.md b/docs/zcleanup/DESIGN.md
similarity index 100%
rename from DESIGN.md
rename to docs/zcleanup/DESIGN.md
diff --git a/docs/zcleanup/advanced-deploy-k8s.md b/docs/zcleanup/advanced-deploy-k8s.md
new file mode 100644
index 00000000..ecee7098
--- /dev/null
+++ b/docs/zcleanup/advanced-deploy-k8s.md
@@ -0,0 +1,63 @@
+---
+layout: docs
+toc_id: advanced-deploy-k8s
+title: VMware Event Broker Appliance - Event Router Standalone
+description: Standalone Deployment of Event Router
+cta:
+ title: What's next?
+ description: Extend your vCenter seamlessly with our pre-built functions
+ actions:
+ - text: See our complete list of prebuilt functions - [here](/examples)
+ - text: Deploy a Function - [here](use-functions).
+---
+
+# Standalone Deployment of Event Router
+VMware Event Router can be deployed and run as standalone binary (see [below](#build-from-source)). However, it is designed to be run in a Kubernetes cluster for increased availability and ease of scaling out. The following steps describe the deployment of the VMware Event Router in **a Kubernetes cluster** for an existing OpenFaaS ("faas-netes") environment, respectively AWS EventBridge.
+
+> **Note:** Docker images are available [here](https://hub.docker.com/r/vmware/veba-event-router){:target="_blank"}.
+
+Create a namespace where the VMware Event Router will be deployed to:
+
+```bash
+kubectl create namespace vmware
+```
+
+Use one of the configuration files provided [here](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/development/vmware-event-router/deploy){:target="_blank"} to configure the router for **one** VMware vCenter Server event `stream` and **one** OpenFaaS **or** AWS EventBridge event stream `processor`. Change the values to match your environment. The following example will use the OpenFaaS config sample.
+
+> **Note:** Make sure your environment is up and running, i.e. Kubernetes and OpenFaaS (incl. a function for testing) up and running or AWS EventBridge correctly configured (IAM Role, event bus and pattern rule).
+
+After you made your changes to the configuration file, save it as `"event-router-config.json` in your current Git working directory.
+
+> **Note:** If you have changed the port of the metrics server in the configuration file (default: 8080) make sure to also change that value in the YAML manifest (under the Kubernetes service entry).
+
+Now, from your current Git working directory create a Kubernetes [secret](https://kubernetes.io/docs/concepts/configuration/secret/){:target="_blank"} from the configuration file:
+
+```bash
+kubectl -n vmware create secret generic event-router-config --from-file=event-router-config.json
+```
+
+> **Note:** You might want to delete the (local) configuration file to not leave behind sensitive information on your local machine.
+
+Now we can deploy the VMware Event Router:
+
+```bash
+kubectl -n vmware create -f deploy/event-router-k8s.yaml
+```
+
+Check the logs of the VMware Event Router to validate it started correctly:
+
+```bash
+kubectl -n vmware logs deploy/vmware-event-router -f
+```
+
+If you run into issues, the logs should give you a hint, e.g.:
+
+- configuration file not found -> file naming issue
+- connection to vCenter/OpenFaaS cannot be established -> check values in the configuration file
+- deployment/pod will not even come up -> check for resource issues, docker pull issues and other potential causes using the standard kubectl troubleshooting ways
+
+To delete the deployment and secret simply delete the namespace we created earlier:
+
+```bash
+kubectl delete namespace vmware
+```
\ No newline at end of file
diff --git a/getting-started-build.md b/docs/zcleanup/getting-started-build.md
similarity index 78%
rename from getting-started-build.md
rename to docs/zcleanup/getting-started-build.md
index dd615bf1..d10fa6d3 100644
--- a/getting-started-build.md
+++ b/docs/zcleanup/getting-started-build.md
@@ -28,7 +28,7 @@ Step 2 - Edit the `photon-builder.json` file to configure the vSphere endpoint f
}
```
-**Note:** If you need to change the default root password on the vCenter Event Broker Appliance, take a look at `photon-version.json`
+> **Note:** If you need to change the default root password on the vCenter Event Broker Appliance, take a look at `photon-version.json`
Step 3 - Start the build by running the build script
@@ -36,4 +36,4 @@ Step 3 - Start the build by running the build script
./build.sh
````
-If you wish to automatically deploy the vCenter Event Broker Appliance after successfully building the OVA. You can edit the `photon-dev.xml.template` file and change the `ovftool_deploy_*` variables and run `./build.sh dev` instead.
+If you wish to automatically deploy the vCenter Event Broker Appliance after successfully building the OVA, please take a look at the script samples located in the test directory.
diff --git a/getting-started.md b/docs/zcleanup/getting-started.md
similarity index 91%
rename from getting-started.md
rename to docs/zcleanup/getting-started.md
index 562f6c0c..e90c40f1 100644
--- a/getting-started.md
+++ b/docs/zcleanup/getting-started.md
@@ -58,7 +58,7 @@ For more information on using the OpenFaaS and AWS EventBridge Processor, please
*zAdvanced (Optional)*
* Debugging - When enabled, this will output a more verbose log file that can be used to troubleshoot failed deployments
- * POD CIDR Network - Customize POD CIDR Network (Default 10.99.0.0/20)
+ * POD CIDR Network - Customize POD CIDR Network (Default 10.99.0.0/20). This subnet must not overlap with the vCenter Event Broker IP address.
**Step 3** - Power On the vCenter Event Broker Appliance after successful deployment. Depending on your external network connectivity, it can take a few minutes while the system is being setup. You can open the VM Console to view the progress. Once everything is completed, you should see an updated login banner for the various endpoints:
@@ -71,8 +71,14 @@ OpenFaaS UI: https://[hostname]
If you are using the AWS EventBridge Processor, the OpenFaaS UI endpoint will not be available which is expected and is not shown in the login banner.
-**Note**: If you enable Debugging, the install logs endpoint will automatically contain the more verbose log entries.
+> **Note:** If you enable Debugging, the install logs endpoint will automatically contain the more verbose log entries.
**Step 4** - You can verify that everything was deployed correctly by opening a web browser and accessing one of the endpoints along with the associated admin password you had specified as part of the OVA deployment.
At this point, you have successfully deployed the vCenter Event Broker Appliance and you are ready to start deploying your functions! Check the [examples](./examples/README.md) to quickly get started.
+
+If the appliance does not appear to be working correctly, try some of the techniques in the [VEBA troubleshooting](./docs/8-veba-troubleshooting.md) guide.
+
+## Additional Learning
+
+[VEBA troubleshooting](./docs/8-veba-troubleshooting.md)
diff --git a/examples/README.md b/examples/README.md
index 740dde2a..95f7763e 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,14 +1,21 @@
# About the Example Functions
-This page lists ready to use functions curated by the vCenter Event Broker community. They serve as an easy way to use the appliance and as an inspiration for how to write functions in different languages.
-
-> **Note:** These functions are provided and tested to be used with the vCenter Event Broker Appliance deployed with [OpenFaaS](../DESIGN.md#components) as the event stream processor.
-
-| Use Cases | Python | PowerCLI |
-|-----------------|--------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|
-| vSphere Tagging | [Link](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/examples/python/tagging) | [Link](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/examples/powercli/tagging) |
-| Send VM Configuration Changes to Slack | | [Link](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/examples/powercli/hwchange-slack) |
-| Disable Alarms for Host Maintenance | | [Link](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/examples/powercli/hostmaint-alarms) |
-| ESX Maximum transmission unit fixer | [Link](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/examples/python/esx-mtu-fixer) | |
-| Datastore Usage Notification | | [Link](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/examples/powercli/datastore-usage-email) |
-| Echo VEBA Event | [Link](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/examples/python/echo)| |
\ No newline at end of file
+Example Functions serve as an easy way to use the appliance and as an inspiration for how to write functions in different languages.
+
+> **Note:** These functions are provided and tested to be used with the VMware Event Broker Appliance deployed with [OpenFaaS](https://vmweventbroker.io/kb/architecture) as the event stream processor.
+
+VMware Event Broker Appliance with OpenFaaS allows you to write functions in any language. These functions are organized by the language that they are written on as shown above
+
+When you are making a contribution, the [master list of functions](https://vmweventbroker.io/examples) should be updated by updating the yaml within [docs/site/examples.md](https://github.com/vmware-samples/vcenter-event-broker-appliance/blob/master/docs/site/examples.md). You must provide all required fields and the information must be in this particular format.
+
+```yaml
+ - title: POST to any REST API #short catchy title
+ usecases:
+ - item: automation
+ - item: #choose b/w analytics, audit, automation, integration, notification, remediation and other
+ id: post-rest-api #id for hyperlink anchoring
+ description: #description of the function purpose and what it does
+ links:
+ - language: python #use python for python3 as well
+ url: "/tree/master/examples/python/invoke-rest-api" #relative path to the function
+```
\ No newline at end of file
diff --git a/examples/go/tagging/.gitignore b/examples/go/tagging/.gitignore
new file mode 100644
index 00000000..80d71b9d
--- /dev/null
+++ b/examples/go/tagging/.gitignore
@@ -0,0 +1,2 @@
+template
+build
\ No newline at end of file
diff --git a/examples/go/tagging/README.MD b/examples/go/tagging/README.MD
new file mode 100644
index 00000000..a4b7e9e5
--- /dev/null
+++ b/examples/go/tagging/README.MD
@@ -0,0 +1,133 @@
+### Get the example function
+
+Clone this repository which contains the example functions.
+
+```bash
+git clone https://github.com/vmware-samples/vcenter-event-broker-appliance
+cd vcenter-event-broker-appliance/examples/go/tagging
+git checkout master
+```
+
+### Categories and tags
+
+For this exercise, we need to create a category and tag unless you want to use an existing tag to follow along.
+
+Create a category/tag to be attached to a VM when it is powered on. Since we need the unique tag ID (i.e. vSphere URN) we will use [govc](https://github.com/vmware/govmomi/tree/master/govc) for this job. You can also use vSphere APIs (REST/SOAP) to retrieve the URN.
+
+```bash
+# Test connection to vCenter, ignore TLS warnings
+export GOVC_INSECURE=true # only needed if vCenter certificates cannot be verified
+export GOVC_URL='https://vcuser:vcpassword@vcenter.ip' # replace with your environment details
+./govc tags.ls # should not error out, otherwise check parameters above
+
+# If the connection is successful create a demo category/tag to be used by the function
+./govc tags.category.create democat1
+urn:... # we don't need the category URN for this example
+./govc tags.create -c democat1 demotag1
+urn:vmomi:InventoryServiceTag:019c0a9e-0672-48f5-ac2a-e394669e2916:GLOBAL
+```
+
+Take a note of the `urn:...` for `demotag1` as we will need it for the next steps.
+
+### Customize the function
+
+For security reasons, do not expose sensitive data. We will create a Kubernetes [secret](https://kubernetes.io/docs/concepts/configuration/secret/) which will hold the vCenter credentials and tag information. This secret will be mounted (by the appliance) into the function during runtime. The secret will need to be created via `faas-cli`.
+
+First, change the configuration file [vcconfig.toml](vcconfig.toml) holding your secret vCenter information located in this folder:
+
+```toml
+# vcconfig.toml contents
+# Replace with your own values and use a dedicated user/service account with
+# permissions to tag VMs, if possible. Insecure indicates if TLS self-signed
+# certificates is being enforced. Insecure = true means TLS is not enforced.
+[vcenter]
+server = "VCENTER_FQDN/IP"
+user = "tagging-admin@vsphere.local"
+password = "DontUseThisPassword"
+insecure = true # by default, insecure = false
+
+[tag]
+urn = "urn:vmomi:InventoryServiceTag:019c0a9e-0672-48f5-ac2a-e394669e2916:GLOBAL" # replace with the one noted above
+action = "attach" # tagging action to perform, i.e. attach or detach tag
+```
+
+Store the vcconfig.toml configuration file as secret in the appliance using the following:
+
+```bash
+# set up faas-cli for first use
+export OPENFAAS_URL=https://VEBA_FQDN_OR_IP
+faas-cli login -p VEBA_OPENFAAS_PASSWORD --tls-no-verify # vCenter Event Broker Appliance is configured with authentication, pass in the password used during the vCenter Event Broker Appliance deployment process
+
+# now create the secret
+faas-cli secret create vcconfig --from-file=vcconfig.toml --tls-no-verify
+```
+
+**TIP:** If you need only to update the secret, the command is
+
+```bash
+faas-cli secret update vcconfig --from-file=vcconfig.toml --tls-no-verify
+```
+
+> **Note:** Delete the local `vcconfig.toml` after you're done with this exercise to not expose this sensitive information.
+
+Lastly, define the vCenter event which will trigger this function. Such function-specific settings are performed in the `stack.yml` file. Open and edit the `stack.yml` provided with in the examples/go/tagging directory. Change `gateway` and `topic` as per your environment/needs.
+
+> **Note:** A key-value annotation under `topic` defines which VM event should trigger the function. A list of VM events from vCenter can be found [here](https://code.vmware.com/doc/preview?id=4206#/doc/vim.event.VmEvent.html). A single topic can be written as `topic: VmPoweredOnEvent`. Multiple topics can be specified using a `","` delimiter syntax, e.g. "`topic: "VmPoweredOnEvent,VmPoweredOffEvent"`".
+
+```yaml
+version: 1.0
+provider:
+ name: openfaas
+ gateway: https://VEBA_FQDN_OR_IP # replace with your vCenter Event Broker Appliance environment
+functions:
+ gotag-fn:
+ lang: golang-http
+ handler: ./handler
+ image: vmware/veba-go-tagging:latest
+ environment:
+ write_debug: true
+ read_debug: true
+ secrets:
+ - vcconfig # leave as is unless you changed the name during the creation of the vCenter credentials secrets above
+ annotations:
+ topic: VmPoweredOnEvent
+```
+
+> **Note:** If you are running a vSphere DRS-enabled cluster the topic annotation above should be `DrsVmPoweredOnEvent`. Otherwise the function would never be triggered.
+
+### Deploy the function
+
+After you've performed the steps and modifications above, you can go ahead and deploy the function:
+
+```bash
+faas template store pull golang-http # only required during the first deployment
+faas-cli deploy -f stack.yml --tls-no-verify
+Deployed. 202 Accepted.
+```
+
+### Trigger the function
+
+Turn on a virtual machine, e.g. in vCenter or via `govc` CLI, to trigger the function via a `(DRS)VmPoweredOnEvent`. Verify the virtual machine was correctly tagged.
+
+> **Note:** If you don't see a tag being assigned verify that you correctly followed each step above, IPs/FQDNs and credentials are correct and see the [troubleshooting](#troubleshooting) section below.
+
+## Troubleshooting
+
+If your VM did not get the tag attached, verify:
+
+- vCenter IP/username/password
+- Permissions of the vCenter user
+- Whether the components can talk to each other (VMware Event Router to vCenter and OpenFaaS, function to vCenter)
+- Check the logs (`kubectl` is installed and configured locally on the appliance)):
+
+```bash
+faas-cli logs gotag-fn --follow --tls-no-verify
+
+# Successful log message in the OpenFaaS tagging function
+2019/01/25 23:48:55 Forking fprocess.
+2019/01/25 23:48:55 Query
+2019/01/25 23:48:55 Path /
+
+{"status": "200", "message": "successfully attached tag on VM: vm-267"}
+2019/01/25 23:48:56 Duration: 1.551482 seconds
+```
diff --git a/examples/go/tagging/handler/client.go b/examples/go/tagging/handler/client.go
new file mode 100644
index 00000000..013c1d80
--- /dev/null
+++ b/examples/go/tagging/handler/client.go
@@ -0,0 +1,64 @@
+package function
+
+import (
+ "context"
+ "fmt"
+ "net/url"
+
+ "github.com/vmware/govmomi"
+ "github.com/vmware/govmomi/vapi/rest"
+ "github.com/vmware/govmomi/vapi/tags"
+ "github.com/vmware/govmomi/vim25/types"
+)
+
+// vsClient is a client for vSphere.
+type vsClient struct {
+ govmomi *govmomi.Client
+ rest *rest.Client
+}
+
+func newClient(ctx context.Context, u url.URL, insecure bool) (*vsClient, error) {
+ var clt vsClient
+
+ gc, err := govmomi.NewClient(ctx, &u, insecure)
+ if err != nil {
+ return nil, fmt.Errorf("connecting to govmomi api failed: %w", err)
+ }
+ clt.govmomi = gc
+
+ clt.rest = rest.NewClient(clt.govmomi.Client)
+ err = clt.rest.Login(ctx, u.User)
+ if err != nil {
+ return nil, fmt.Errorf("log in to rest api failed: %w", err)
+ }
+
+ return &clt, nil
+}
+
+// moTag adds an existing tag to a VirtualMachine.
+func (clt *vsClient) moTag(ctx context.Context, vm types.ManagedObjectReference, tagID string) error {
+ // Get the tag manager which does the tagging.
+ m := tags.NewManager(clt.rest)
+
+ // Attach tag to VM.
+ err := m.AttachTag(ctx, tagID, vm)
+ if err != nil {
+ return fmt.Errorf("attach tag to VM failed: %w", err)
+ }
+
+ return nil
+}
+
+func (clt *vsClient) logout(ctx context.Context) error {
+ err := clt.govmomi.Logout(ctx)
+ if err != nil {
+ return fmt.Errorf("govmomi api logout failed: %w", err)
+ }
+
+ err = clt.rest.Logout(ctx)
+ if err != nil {
+ return fmt.Errorf("rest api logout failed: %w", err)
+ }
+
+ return nil
+}
diff --git a/examples/go/tagging/handler/go.mod b/examples/go/tagging/handler/go.mod
new file mode 100644
index 00000000..0d8ae3b1
--- /dev/null
+++ b/examples/go/tagging/handler/go.mod
@@ -0,0 +1,9 @@
+module github.com/vmware-samples/vcenter-event-broker-appliance/examples/go/tagging/handler
+
+go 1.13
+
+require (
+ github.com/openfaas-incubator/go-function-sdk v0.0.0-20191017092257-70701da50a91
+ github.com/pelletier/go-toml v1.6.0
+ github.com/vmware/govmomi v0.22.2
+)
diff --git a/examples/go/tagging/handler/go.sum b/examples/go/tagging/handler/go.sum
new file mode 100644
index 00000000..853295bc
--- /dev/null
+++ b/examples/go/tagging/handler/go.sum
@@ -0,0 +1,21 @@
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892/go.mod h1:CTDl0pzVzE5DEzZhPfvhY/9sPFMQIxaJ9VAMs9AagrE=
+github.com/google/uuid v0.0.0-20170306145142-6a5e28554805 h1:skl44gU1qEIcRpwKjb9bhlRwjvr96wLdvpTogCBBJe8=
+github.com/google/uuid v0.0.0-20170306145142-6a5e28554805/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/openfaas-incubator/go-function-sdk v0.0.0-20191017092257-70701da50a91 h1:18SEXx3EzxO9wdrcO+EKePNM0JCquzyLjiPYbgIfX7w=
+github.com/openfaas-incubator/go-function-sdk v0.0.0-20191017092257-70701da50a91/go.mod h1:F37Kp+hwdHP+o3UKjkGzikQg4weKiMvcegT9vCQjvjE=
+github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
+github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
+github.com/vmware/govmomi v0.22.2 h1:hmLv4f+RMTTseqtJRijjOWzwELiaLMIoHv2D6H3bF4I=
+github.com/vmware/govmomi v0.22.2/go.mod h1:Y+Wq4lst78L85Ge/F8+ORXIWiKYqaro1vhAulACy9Lc=
+github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728/go.mod h1:x9oS4Wk2s2u4tS29nEaDLdzvuHdB19CvSGJjPgkZJNk=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/examples/go/tagging/handler/handler.go b/examples/go/tagging/handler/handler.go
new file mode 100644
index 00000000..bdcb50dc
--- /dev/null
+++ b/examples/go/tagging/handler/handler.go
@@ -0,0 +1,244 @@
+package function
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+
+ handler "github.com/openfaas-incubator/go-function-sdk"
+ "github.com/pelletier/go-toml"
+ "github.com/vmware/govmomi/vim25/types"
+)
+
+const cfgPath = "/var/openfaas/secrets/vcconfig"
+
+// vcConfig represents the toml vcconfig file
+type vcConfig struct {
+ VCenter struct {
+ Server string
+ User string
+ Password string
+ Insecure bool
+ }
+ Tag struct {
+ URN string
+ Action string
+ }
+}
+
+// Incoming is a subsection of a Cloud Event.
+type incoming struct {
+ Data types.Event `json:"data,omitempty"`
+}
+
+var (
+ lock sync.Mutex // Lock protects client.
+ client *vsClient // Client persists vSphere connection.
+ once sync.Once // For handleSignal() to be called once.
+)
+
+// Handle a function invocation
+func Handle(req handler.Request) (handler.Response, error) {
+ ctx := context.Background()
+
+ // Load config every time, to ensure the most updated version is used.
+ cfg, err := loadTomlCfg(cfgPath)
+ if err != nil {
+ wrapErr := fmt.Errorf("loading of vcconfig failed: %w", err)
+ log.Println(wrapErr.Error())
+
+ return handler.Response{
+ Body: []byte(wrapErr.Error()),
+ StatusCode: http.StatusInternalServerError,
+ }, wrapErr
+ }
+
+ // Connect to vSphere govmomi API once and persist connection with global variable.
+ err = vsConnect(ctx, cfg)
+ if err != nil {
+ wrapErr := fmt.Errorf("connect to vSphere failed: %w", err)
+
+ if debug() {
+ log.Println(wrapErr)
+ }
+
+ return handler.Response{
+ Body: []byte(wrapErr.Error()),
+ StatusCode: http.StatusInternalServerError,
+ }, wrapErr
+ }
+
+ once.Do(func() {
+ // Set up os signal handling to log out of vSphere.
+ go handleSignal(ctx)
+ })
+
+ // Retrieve the Managed Object Reference from the event.
+ moRef, err := parseEventMoRef(req.Body)
+ if err != nil {
+ wrapErr := fmt.Errorf("retrieve managed reference object failed: %w", err)
+
+ if debug() {
+ log.Println(wrapErr)
+ }
+
+ return handler.Response{
+ Body: []byte(wrapErr.Error()),
+ StatusCode: http.StatusBadRequest,
+ }, wrapErr
+ }
+
+ err = client.moTag(ctx, *moRef, cfg.Tag.URN)
+ if err != nil {
+ wrapErr := fmt.Errorf("tagging managed reference object failed: %w", err)
+
+ if debug() {
+ log.Println(wrapErr)
+ }
+
+ return handler.Response{
+ Body: []byte(wrapErr.Error()),
+ StatusCode: http.StatusInternalServerError,
+ }, wrapErr
+ }
+
+ message := fmt.Sprintf("%v was tagged with %v", moRef.Value, cfg.Tag.URN)
+ log.Println(message)
+
+ return handler.Response{
+ Body: []byte(message),
+ StatusCode: http.StatusOK,
+ }, nil
+}
+
+// vsConnect connects to vSphere govmomi API using information from vcconfig.toml.
+func vsConnect(ctx context.Context, cfg *vcConfig) error {
+ lock.Lock()
+ defer lock.Unlock()
+
+ if client == nil {
+ u := url.URL{
+ Scheme: "https",
+ Host: cfg.VCenter.Server,
+ Path: "sdk",
+ }
+ u.User = url.UserPassword(cfg.VCenter.User, cfg.VCenter.Password)
+ insecure := cfg.VCenter.Insecure
+
+ if debug() {
+ log.Println("connect to vSphere")
+ }
+
+ c, err := newClient(ctx, u, insecure)
+ if err != nil {
+ return fmt.Errorf("connection to vSphere API failed: %w", err)
+ }
+
+ // Set global variable to persist connection.
+ client = c
+ }
+
+ return nil
+}
+
+func loadTomlCfg(path string) (*vcConfig, error) {
+ var cfg vcConfig
+
+ secret, err := toml.LoadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("unable to load vcconfig.toml: %w", err)
+ }
+
+ err = secret.Unmarshal(&cfg)
+ if err != nil {
+ return nil, fmt.Errorf("unable to unmarshal vcconfig.toml: %w", err)
+ }
+
+ err = validateConfig(cfg)
+ if err != nil {
+ return nil, fmt.Errorf("insufficient information in vcconfig.toml: %w", err)
+ }
+
+ return &cfg, nil
+}
+
+// ValidateConfig ensures the bare minimum of information is in the config file.
+func validateConfig(cfg vcConfig) error {
+ reqFields := map[string]string{
+ "vcenter server": cfg.VCenter.Server,
+ "vcenter user": cfg.VCenter.User,
+ "vcenter password": cfg.VCenter.Password,
+ "tag URN": cfg.Tag.URN,
+ "tag action": cfg.Tag.Action,
+ }
+
+ // Multiple fields may be missing, but err on the first encountered.
+ for k, v := range reqFields {
+ if v == "" {
+ return errors.New("required field(s) missing, including " + k)
+ }
+ }
+
+ return nil
+}
+
+// Debug determines verbose logging
+func debug() bool {
+ verbose := os.Getenv("write_debug")
+
+ if verbose == "true" {
+ return true
+ }
+
+ return false
+}
+
+func parseEventMoRef(req []byte) (*types.ManagedObjectReference, error) {
+ var event incoming
+ var moRef types.ManagedObjectReference
+
+ err := json.Unmarshal(req, &event)
+ if err != nil {
+ return nil, fmt.Errorf("parsing of request failed: %w", err)
+ }
+
+ if event.Data.Vm == nil || event.Data.Vm.Vm.Value == "" {
+ return nil, errors.New("empty managed reference object")
+ }
+
+ // Fill information in the request into a govmomi type.
+ moRef.Type = event.Data.Vm.Vm.Type
+ moRef.Value = event.Data.Vm.Vm.Value
+
+ return &moRef, nil
+}
+
+func handleSignal(ctx context.Context) {
+ var sigCh = make(chan os.Signal, 2)
+
+ signal.Notify(sigCh, syscall.SIGTERM, os.Interrupt)
+
+ s := <-sigCh
+ verbose := debug()
+
+ if verbose {
+ log.Printf("got signal: %v, log out of vSphere", s)
+ }
+
+ err := client.logout(ctx)
+ if verbose {
+ if err != nil {
+ log.Printf("vSphere logout failed: %v", err)
+ return
+ }
+ log.Println("logged out of govmomi and rest APIs")
+ }
+}
diff --git a/examples/go/tagging/handler/handler_test.go b/examples/go/tagging/handler/handler_test.go
new file mode 100644
index 00000000..5127bba2
--- /dev/null
+++ b/examples/go/tagging/handler/handler_test.go
@@ -0,0 +1,197 @@
+package function
+
+import (
+ "io/ioutil"
+ "testing"
+)
+
+const passMark = "\u2713"
+const failMark = "\u2717"
+
+// TestLoadTomlCfg shows valid vcconfig.toml files can be loaded and processed.
+func TestLoadTomlCfg(t *testing.T) {
+ var tests = []struct {
+ testDesc string
+ cfgPath string
+ expectErr bool
+ want *vcConfig
+ }{
+ {
+ "Test that toml file loads correctly",
+ "testdata/vcconfig.toml",
+ false,
+ &vcConfig{
+ struct {
+ Server string
+ User string
+ Password string
+ Insecure bool
+ }{
+ "veba.local.corp",
+ "admin@vsphere.local",
+ "password1234",
+ false,
+ },
+ struct {
+ URN string
+ Action string
+ }{
+ "urn:vmomi:InventoryServiceTag:11f16f36-f5c4-4c29-b7d3-d9c7d12babe6:GLOBAL",
+ "attach",
+ },
+ },
+ },
+ {
+ "Test that toml file loads, even with more info than needed, and defaults are set",
+ "testdata/vcconfig2.toml",
+ false,
+ &vcConfig{
+ struct {
+ Server string
+ User string
+ Password string
+ Insecure bool
+ }{
+ "veba.local.corp",
+ "admin@vsphere.local",
+ "password1234",
+ true,
+ },
+ struct {
+ URN string
+ Action string
+ }{
+ "urn:vmomi:InventoryServiceTag:11f16f36-f5c4-4c29-b7d3-d9c7d12babe6:GLOBAL",
+ "detach",
+ },
+ },
+ },
+ {
+ "Test that misconfigured toml file ends in error",
+ "testdata/vcconfigErr1.toml",
+ true,
+ nil,
+ },
+ {
+ "Test that vcconfig.toml missing essential information results in error.",
+ "testdata/vcconfigErr2.toml",
+ true,
+ nil,
+ },
+ {
+ "Test that missing toml file results in error",
+ "testdata/missing.toml",
+ true,
+ nil,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Logf("=========== %v ===========", tc.testDesc)
+ cfg, err := loadTomlCfg(tc.cfgPath)
+ if err != nil {
+ if tc.expectErr {
+ // An error is expected.
+ t.Logf("got an error, as expected: %v. %v", err, passMark)
+ } else {
+ t.Log(tc.testDesc, failMark, err)
+ t.Fail()
+ }
+ } else {
+ if *cfg == *tc.want {
+ t.Logf("got expected: %v. %v", tc.want, passMark)
+ } else {
+ t.Logf("expected: %v, got: %v. %v", tc.want, cfg, failMark)
+ t.Fail()
+ }
+ }
+
+ }
+}
+
+// TestParseEventMoRef ensures that managed object reference value and type are
+// obtained by the event json that meets Cloud Event specifications.
+func TestParseEventMoRef(t *testing.T) {
+ type vm struct {
+ vmType string
+ Value string
+ }
+
+ var tests = []struct {
+ testDesc string
+ jsonPath string
+ expectErr bool
+ want *vm
+ }{
+ {
+ "Test that event is readable",
+ "testdata/event.json",
+ false,
+ &vm{vmType: "VirtualMachine", Value: "vm-10000"},
+ },
+ {
+ "Event should be readable, even with minimal information",
+ "testdata/event2.json",
+ false,
+ &vm{vmType: "VirtualMachine", Value: "vm-2"},
+ },
+ {
+ "Event should return error if VM type and value are null",
+ "testdata/eventErr1.json",
+ true,
+ nil,
+ },
+ {
+ "Event should return error if VM info is null",
+ "testdata/eventErr2.json",
+ true,
+ nil,
+ },
+ {
+ "Event should return error if VM parent is null",
+ "testdata/eventErr3.json",
+ true,
+ nil,
+ },
+ {
+ "Event should return error if data is null",
+ "testdata/eventErr4.json",
+ true,
+ nil,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Logf("=========== %v ===========", tc.testDesc)
+ body, err := ioutil.ReadFile(tc.jsonPath)
+ if err != nil {
+ t.Fatal("Test failing due to improper test setup.", failMark, err)
+ }
+
+ moRef, err := parseEventMoRef(body)
+ if err != nil {
+ if tc.expectErr {
+ // An error is expected.
+ t.Logf("got an error, as expected: %v. %v", err, passMark)
+ } else {
+ t.Log(tc.testDesc, failMark, err)
+ t.Fail()
+ }
+ }
+
+ if err == nil {
+ if moRef.Type == tc.want.vmType {
+ t.Logf("got expected: '%s'. %v", moRef.Type, passMark)
+ } else {
+ t.Logf("expected: '%s', got: '%s'. %v", tc.want.vmType, moRef.Type, passMark)
+ t.Fail()
+ }
+
+ if moRef.Value == tc.want.Value {
+ t.Logf("got expected: '%s'. %v", moRef.Value, passMark)
+ } else {
+ t.Fatalf("expected: '%s', got: '%s'. %v", tc.want.Value, moRef.Value, passMark)
+ }
+ }
+ }
+}
diff --git a/examples/go/tagging/handler/testdata/event.json b/examples/go/tagging/handler/testdata/event.json
new file mode 100644
index 00000000..bb7e73e8
--- /dev/null
+++ b/examples/go/tagging/handler/testdata/event.json
@@ -0,0 +1,49 @@
+{
+ "id": "9f284e17-f688-408f-a439-e5e06f564c82",
+ "source": "https://10.10.10.1/sdk",
+ "specversion": "1.0",
+ "type": "com.vmware.event.router/event",
+ "subject": "VmPoweredOffEvent",
+ "time": "2020-03-13T21:11:53.867231Z",
+ "data": {
+ "Key": 14011,
+ "ChainId": 14010,
+ "CreatedTime": "2020-03-13T21:09:40.984999Z",
+ "UserName": "VSPHERE.LOCAL\\Administrator",
+ "Datacenter": {
+ "Name": "vcqaDC",
+ "Datacenter": {
+ "Type": "Datacenter",
+ "Value": "datacenter-2"
+ }
+ },
+ "ComputeResource": {
+ "Name": "cls",
+ "ComputeResource": {
+ "Type": "ClusterComputeResource",
+ "Value": "domain-c7"
+ }
+ },
+ "Host": {
+ "Name": "10.10.10.1",
+ "Host": {
+ "Type": "HostSystem",
+ "Value": "host-33"
+ }
+ },
+ "Vm": {
+ "Name": "standalone-8296aaf45-esx.3-vm.1",
+ "Vm": {
+ "Type": "VirtualMachine",
+ "Value": "vm-10000"
+ }
+ },
+ "Ds": null,
+ "Net": null,
+ "Dvs": null,
+ "FullFormattedMessage": "standalone-8296aaf45-esx.3-vm.1 on host 10.10.10.1 in vcqaDC is starting",
+ "ChangeTag": "",
+ "Template": false
+ },
+ "datacontenttype": "application/json"
+ }
diff --git a/examples/go/tagging/handler/testdata/event2.json b/examples/go/tagging/handler/testdata/event2.json
new file mode 100644
index 00000000..c655ff79
--- /dev/null
+++ b/examples/go/tagging/handler/testdata/event2.json
@@ -0,0 +1,10 @@
+{
+ "data": {
+ "Vm": {
+ "Vm" :{
+ "Value": "vm-2",
+ "Type": "VirtualMachine"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/go/tagging/handler/testdata/eventErr1.json b/examples/go/tagging/handler/testdata/eventErr1.json
new file mode 100644
index 00000000..a2cca394
--- /dev/null
+++ b/examples/go/tagging/handler/testdata/eventErr1.json
@@ -0,0 +1,10 @@
+{
+ "data": {
+ "Vm": {
+ "Vm" :{
+ "Value": null,
+ "Type": null
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/go/tagging/handler/testdata/eventErr2.json b/examples/go/tagging/handler/testdata/eventErr2.json
new file mode 100644
index 00000000..4a576f5b
--- /dev/null
+++ b/examples/go/tagging/handler/testdata/eventErr2.json
@@ -0,0 +1,7 @@
+{
+ "data": {
+ "Vm": {
+ "Vm" : null
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/go/tagging/handler/testdata/eventErr3.json b/examples/go/tagging/handler/testdata/eventErr3.json
new file mode 100644
index 00000000..731e3f1a
--- /dev/null
+++ b/examples/go/tagging/handler/testdata/eventErr3.json
@@ -0,0 +1,5 @@
+{
+ "data": {
+ "Vm": null
+ }
+}
\ No newline at end of file
diff --git a/examples/go/tagging/handler/testdata/eventErr4.json b/examples/go/tagging/handler/testdata/eventErr4.json
new file mode 100644
index 00000000..d1110fc2
--- /dev/null
+++ b/examples/go/tagging/handler/testdata/eventErr4.json
@@ -0,0 +1,3 @@
+{
+ "data": null
+}
\ No newline at end of file
diff --git a/examples/go/tagging/handler/testdata/vcconfig.toml b/examples/go/tagging/handler/testdata/vcconfig.toml
new file mode 100644
index 00000000..a1b494fe
--- /dev/null
+++ b/examples/go/tagging/handler/testdata/vcconfig.toml
@@ -0,0 +1,11 @@
+[vcenter]
+server = "veba.local.corp"
+user = "admin@vsphere.local"
+password = "password1234"
+
+[tag]
+urn = "urn:vmomi:InventoryServiceTag:11f16f36-f5c4-4c29-b7d3-d9c7d12babe6:GLOBAL"
+action = "attach"
+
+[debug]
+verbose = true
\ No newline at end of file
diff --git a/examples/go/tagging/handler/testdata/vcconfig2.toml b/examples/go/tagging/handler/testdata/vcconfig2.toml
new file mode 100644
index 00000000..911f4690
--- /dev/null
+++ b/examples/go/tagging/handler/testdata/vcconfig2.toml
@@ -0,0 +1,13 @@
+[vcenter]
+ server = "veba.local.corp"
+ user = "admin@vsphere.local"
+ password = "password1234"
+ insecure = true
+
+[tag]
+ urn = "urn:vmomi:InventoryServiceTag:11f16f36-f5c4-4c29-b7d3-d9c7d12babe6:GLOBAL"
+ action = "detach"
+
+[unneccessary]
+ info = "this is more than needed"
+
\ No newline at end of file
diff --git a/examples/go/tagging/handler/testdata/vcconfigErr1.toml b/examples/go/tagging/handler/testdata/vcconfigErr1.toml
new file mode 100644
index 00000000..c12184a8
--- /dev/null
+++ b/examples/go/tagging/handler/testdata/vcconfigErr1.toml
@@ -0,0 +1,12 @@
+(vcenter)
+ server = "veba.local.corp"
+ user = "admin@vsphere.local"
+ password = "password1234"
+ insecure = true
+
+(tag)
+ urn = "urn:vmomi:InventoryServiceTag:11f16f36-f5c4-4c29-b7d3-d9c7d12babe6:GLOBAL"
+ action = "detach"
+
+(debug)
+ verbose = false
\ No newline at end of file
diff --git a/examples/go/tagging/handler/testdata/vcconfigErr2.toml b/examples/go/tagging/handler/testdata/vcconfigErr2.toml
new file mode 100644
index 00000000..54220b07
--- /dev/null
+++ b/examples/go/tagging/handler/testdata/vcconfigErr2.toml
@@ -0,0 +1,10 @@
+[vcenter]
+ server = "veba.local.corp"
+ password = "password1234"
+ insecure = true
+
+[tag]
+ action = "detach"
+
+[debug]
+ verbose = true
\ No newline at end of file
diff --git a/examples/go/tagging/stack.yml b/examples/go/tagging/stack.yml
new file mode 100644
index 00000000..56bbb5a9
--- /dev/null
+++ b/examples/go/tagging/stack.yml
@@ -0,0 +1,16 @@
+version: 1.0
+provider:
+ name: openfaas
+ gateway: https://veba.yourdomain.com
+functions:
+ gotag-fn:
+ lang: golang-http
+ handler: ./handler
+ image: vmware/veba-go-tagging:latest
+ environment:
+ write_debug: true
+ read_debug: true
+ secrets:
+ - vcconfig
+ annotations:
+ topic: VmPoweredOnEvent
diff --git a/examples/go/tagging/vcconfig.toml b/examples/go/tagging/vcconfig.toml
new file mode 100644
index 00000000..e1daf942
--- /dev/null
+++ b/examples/go/tagging/vcconfig.toml
@@ -0,0 +1,8 @@
+[vcenter]
+server = "10.0.0.1"
+user = "administrator@vsphere.local"
+password = "DontUseThisPassword"
+
+[tag]
+urn = "urn:vmomi:InventoryServiceTag:6a7653a0-6fb0-407e-a4ec-a0196d9ea425:GLOBAL"
+action = "attach" # or detach
\ No newline at end of file
diff --git a/examples/powercli/datastore-usage-email/README.md b/examples/powercli/datastore-usage-email/README.md
index 518465b7..264966ca 100644
--- a/examples/powercli/datastore-usage-email/README.md
+++ b/examples/powercli/datastore-usage-email/README.md
@@ -16,9 +16,9 @@ git checkout master
Step 2 - Update `stack.yml` and `vc-datastore-config.json` with your environment information
-Note: leave SMTP_USERNAME and SMTP_PASSWORD blank if you do not want to use authenticated SMTP
+> **Note:** leave SMTP_USERNAME and SMTP_PASSWORD blank if you do not want to use authenticated SMTP
-Step 3 - Login to the OpenFaaS gateway on vCenter Event Broker Appliance
+Step 3 - Login to the OpenFaaS gateway on VMware Event Broker Appliance
```
VEBA_GATEWAY=https://veba.primp-industries.com
@@ -33,7 +33,7 @@ Step 4 - Create function secret (only required once)
faas-cli secret create vc-datastore-config --from-file=vc-datastore-config.json --tls-no-verify
```
-Step 5 - Deploy function to vCenter Event Broker Appliance
+Step 5 - Deploy function to VMware Event Broker Appliance
```
faas-cli deploy -f stack.yml --tls-no-verify
@@ -69,7 +69,7 @@ Step 5 - Push the function container to Docker Registry (default but can be chan
faas-cli push -f stack.yml
```
-Step 6 - Login to the OpenFaaS gateway on vCenter Event Broker Appliance
+Step 6 - Login to the OpenFaaS gateway on VMware Event Broker Appliance
```
VEBA_GATEWAY=https://veba.primp-industries.com
@@ -84,7 +84,7 @@ Step 7 - Create function secret (only required once)
faas-cli secret create vc-datastore-config --from-file=vc-datastore-config.json --tls-no-verify
```
-Step 8 - Deploy function to vCenter Event Broker Appliance
+Step 8 - Deploy function to VMware Event Broker Appliance
```
faas-cli deploy -f stack.yml --tls-no-verify
diff --git a/examples/powercli/hostmaint-alarms/README.md b/examples/powercli/hostmaint-alarms/README.md
index 2e7eb4e7..34cfbd49 100644
--- a/examples/powercli/hostmaint-alarms/README.md
+++ b/examples/powercli/hostmaint-alarms/README.md
@@ -2,7 +2,7 @@
## Description
-This example will disable alarm actions on a host when it has entered maintenance mode and will re-enable alarm actions on a host after it has exited maintenance mode. There is an accompanying blog post with more details: [Automate Host Maintenance with the vCenter Event Broker Appliance](https://doogleit.github.io/2019/11/automate-host-maintenance-with-the-vcenter-event-broker-appliance/)
+This example will disable alarm actions on a host when it has entered maintenance mode and will re-enable alarm actions on a host after it has exited maintenance mode. There is an accompanying blog post with more details: [Automate Host Maintenance with the VMware Event Broker Appliance](https://doogleit.github.io/2019/11/automate-host-maintenance-with-the-vcenter-event-broker-appliance/)
## Consume Function Instruction
@@ -16,7 +16,7 @@ git checkout master
Step 2 - Update `stack.yml` and `vc-hostmaint-config.json` with your environment information
-Step 3 - Login to the OpenFaaS gateway on vCenter Event Broker Appliance
+Step 3 - Login to the OpenFaaS gateway on VMware Event Broker Appliance
```
VEBA_GATEWAY=https://veba.primp-industries.com
@@ -31,7 +31,7 @@ Step 4 - Create function secret (only required once)
faas-cli secret create vc-hostmaint-config --from-file=vc-hostmaint-config.json --tls-no-verify
```
-Step 5 - Deploy function to vCenter Event Broker Appliance
+Step 5 - Deploy function to VMware Event Broker Appliance
```
faas-cli deploy -f stack.yml --tls-no-verify
@@ -67,7 +67,7 @@ Step 5 - Push the function container to Docker Registry (default but can be chan
faas-cli push -f stack.yml
```
-Step 6 - Login to the OpenFaaS gateway on vCenter Event Broker Appliance
+Step 6 - Login to the OpenFaaS gateway on VMware Event Broker Appliance
```
VEBA_GATEWAY=https://veba.primp-industries.com
@@ -82,7 +82,7 @@ Step 7 - Create function secret (only required once)
faas-cli secret create vc-hostmaint-config --from-file=vc-hostmaint-config.json --tls-no-verify
```
-Step 8 - Deploy function to vCenter Event Broker Appliance
+Step 8 - Deploy function to VMware Event Broker Appliance
```
faas-cli deploy -f stack.yml --tls-no-verify
diff --git a/examples/powercli/hwchange-slack/README.md b/examples/powercli/hwchange-slack/README.md
index 88401942..2921c675 100644
--- a/examples/powercli/hwchange-slack/README.md
+++ b/examples/powercli/hwchange-slack/README.md
@@ -4,7 +4,7 @@
This function demonstrates using PowerCLI to send VM configuration changes to Slack when the VM Reconfigure Event is triggered
-There is a blog post covering this example in detail: [Audit VM configuration changes using the vCenter Event Broker
+There is a blog post covering this example in detail: [Audit VM configuration changes using the VMware Event Broker
](https://www.opvizor.com/audit-vm-configuration-changes-using-the-vcenter-event-broker)
The custom PowerShell template for OpenFaaS is using [PSSlack](https://github.com/RamblingCookieMonster/PSSlack)
@@ -26,7 +26,7 @@ Make sure to create a channel for the notifications and a [Slack webhook](https:
Step 3 - Update `stack.yml` and `vc-slack-config.json` with your environment information
-Step 4 - Login to the OpenFaaS gateway on vCenter Event Broker Appliance
+Step 4 - Login to the OpenFaaS gateway on VMware Event Broker Appliance
```
VEBA_GATEWAY=https://veba.primp-industries.com
@@ -41,7 +41,7 @@ Step 5 - Create function secret (only required once)
faas-cli secret create vc-slack-config --from-file=vc-slack-config.json --tls-no-verify
```
-Step 6 - Deploy function to vCenter Event Broker Appliance
+Step 6 - Deploy function to VMware Event Broker Appliance
```
faas-cli deploy -f stack.yml --tls-no-verify
@@ -77,7 +77,7 @@ Step 5 - Push the function container to Docker Registry (default but can be chan
faas-cli push -f stack.yml
```
-Step 6 - Login to the OpenFaaS gateway on vCenter Event Broker Appliance
+Step 6 - Login to the OpenFaaS gateway on VMware Event Broker Appliance
```
VEBA_GATEWAY=https://veba.primp-industries.com
@@ -92,7 +92,7 @@ Step 7 - Create function secret (only required once)
faas-cli secret create vc-slack-config --from-file=vc-slack-config.json --tls-no-verify
```
-Step 8 - Deploy function to vCenter Event Broker Appliance
+Step 8 - Deploy function to VMware Event Broker Appliance
```
faas-cli deploy -f stack.yml --tls-no-verify
diff --git a/examples/powercli/tagging/README.md b/examples/powercli/tagging/README.md
index d856c024..3cc1c515 100644
--- a/examples/powercli/tagging/README.md
+++ b/examples/powercli/tagging/README.md
@@ -16,7 +16,7 @@ git checkout master
Step 2 - Update `stack.yml` and `vc-tag-config.json` with your environment information
-Step 3 - Login to the OpenFaaS gateway on vCenter Event Broker Appliance
+Step 3 - Login to the OpenFaaS gateway on VMware Event Broker Appliance
```
VEBA_GATEWAY=https://veba.primp-industries.com
@@ -31,7 +31,7 @@ Step 4 - Create function secret (only required once)
faas-cli secret create vc-tag-config --from-file=vc-tag-config.json --tls-no-verify
```
-Step 5 - Deploy function to vCenter Event Broker Appliance
+Step 5 - Deploy function to VMware Event Broker Appliance
```
faas-cli deploy -f stack.yml --tls-no-verify
@@ -67,7 +67,7 @@ Step 5 - Push the function container to Docker Registry (default but can be chan
faas-cli push -f stack.yml
```
-Step 6 - Login to the OpenFaaS gateway on vCenter Event Broker Appliance
+Step 6 - Login to the OpenFaaS gateway on VMware Event Broker Appliance
```
VEBA_GATEWAY=https://veba.primp-industries.com
@@ -82,7 +82,7 @@ Step 7 - Create function secret (only required once)
faas-cli secret create vc-tag-config --from-file=vc-tag-config.json --tls-no-verify
```
-Step 8 - Deploy function to vCenter Event Broker Appliance
+Step 8 - Deploy function to VMware Event Broker Appliance
```
faas-cli deploy -f stack.yml --tls-no-verify
diff --git a/examples/powershell/vro/README.md b/examples/powershell/vro/README.md
new file mode 100644
index 00000000..7b5e00a8
--- /dev/null
+++ b/examples/powershell/vro/README.md
@@ -0,0 +1,59 @@
+# vRealize Orchestrator Function
+
+## Description
+
+This function demonstrates using PowerShell to trigger vRealize Orchestrator workflow using vRO REST API
+
+## Prerequisites
+
+* You have deployed the example vSphere Tagging vRO Workflow package from https://github.com/kclinden/vro-vsphere-tagging
+* You have retrieved the required vRO Workflow ID (please see this blog post [here](https://www.virtuallyghetto.com/2020/03/using-vro-rest-api-to-execute-a-workflow-with-sdk-objects.html) for more details)
+
+## Instruction Consuming Function
+
+Step 1 - Initialize function, only required during the first deployment
+
+```
+faas-cli template pull
+```
+
+Step 2 - Update `stack.yml` and `vro-secrets.json` with your environment information
+
+> **Note:** If you are building your own function, you will need to update the `image:` property in the stack.yaml to point to your own Dockerhub account and Docker Image (e.g. `/`)
+
+Step 3 - Deploy function to VMware Event Broker Appliance
+
+```
+VEBA_GATEWAY=https://veba.primp-industries.com
+export OPENFAAS_URL=${VEBA_GATEWAY} # this is handy so you don't have to keep specifying OpenFaaS endpoint in command-line
+
+faas-cli login --username admin --password-stdin --tls-no-verify # login with your admin password
+faas-cli secret create vro-secrets --from-file=vro-secrets.json --tls-no-verify # create secret, only required once
+faas-cli deploy -f stack.yml --tls-no-verify
+```
+
+Step 4 - To remove the function and secret from VMware Event Broker Appliance
+
+```
+VEBA_GATEWAY=https://veba.primp-industries.com
+export OPENFAAS_URL=${VEBA_GATEWAY} # this is handy so you don't have to keep specifying OpenFaaS endpoint in command-line
+
+faas-cli remove -f stack.yml --tls-no-verify
+faas-cli secret remove vro-secrets --tls-no-verify
+```
+
+## Instruction Building Function
+
+Follow Step 1 from above and then any changes made to your function, you will need to run these additional two steps before proceeding to Step 2 from above.
+
+Step 1 - Build the function container
+
+```
+faas-cli build -f stack.yml
+```
+
+Step 2 - Push the function container to Docker Registry (default but can be changed to internal registry)
+
+```
+faas-cli push -f stack.yml
+```
diff --git a/examples/powershell/vro/handler/script.ps1 b/examples/powershell/vro/handler/script.ps1
new file mode 100644
index 00000000..a886d1db
--- /dev/null
+++ b/examples/powershell/vro/handler/script.ps1
@@ -0,0 +1,91 @@
+
+# Process function Secrets passed in
+$SECRETS_FILE = "/var/openfaas/secrets/vro-secrets"
+$SECRETS_CONFIG = (Get-Content -Raw -Path $SECRETS_FILE | ConvertFrom-Json)
+
+# Process payload sent from vCenter Server Event
+$json = $args | ConvertFrom-Json
+if($env:function_debug -eq "true") {
+ Write-Host "DEBUG: json=`"$($json | Format-List | Out-String)`""
+}
+
+$vcenter = ($json.source -replace "https://","" -replace "/sdk","");
+$vmMoRef = $json.data.vm.vm.value;
+$vm = $json.data.vm.name;
+
+if($vmMoRef -eq "" -or $vm -eq "") {
+ Write-Host "Unable to retrieve VM Object from Event payload, please ensure Event contains VM result"
+ exit
+}
+
+# e.g. mgmt-vcsa-01.cpbu.corp/vm-2660
+$vroVmId = "$vcenter/$vmMoRef"
+
+# assumes following vRO Workflow https://github.com/kclinden/vro-vsphere-tagging as an example
+$body = @"
+{
+ "parameters":
+ [
+ {
+ "value": {
+ "sdk-object":{
+ "type": "VC:VirtualMachine",
+ "id": "$($vroVmId)"}
+ },
+ "type": "VC:VirtualMachine",
+ "name": "vm",
+ "scope": "local"
+ },
+ {
+ "value": {
+ "string":{
+ "value": "$($SECRETS_CONFIG.TAG_CATEGORY_NAME)"
+ }
+ },
+ "type": "string",
+ "name": "categoryName",
+ "scope": "local"
+ },
+ {
+ "value": {
+ "string":{
+ "value": "$($SECRETS_CONFIG.TAG_NAME)"
+ }
+ },
+ "type": "string",
+ "name": "tagToFind",
+ "scope": "local"
+ }
+ ]
+}
+"@
+
+# Basic Auth for vRO execution
+$pair = "$($SECRETS_CONFIG.VRO_USERNAME):$($SECRETS_CONFIG.VRO_PASSWORD)"
+$bytes = [System.Text.Encoding]::ASCII.GetBytes($pair)
+$base64 = [System.Convert]::ToBase64String($bytes)
+$basicAuthValue = "Basic $base64"
+
+$headers = @{
+ "Authorization"="$basicAuthValue";
+ "Accept="="application/json";
+ "Content-Type"="application/json";
+}
+
+$vroUrl = "https://$($SECRETS_CONFIG.VRO_SERVER):443/vco/api/workflows/$($SECRETS_CONFIG.VRO_WORKFLOW_ID)/executions"
+
+if($env:function_debug -eq "true") {
+ Write-Host "DEBUG: vRoVmID=$vroVmId"
+ Write-Host "DEBUG: TagCategory=$($SECRETS_CONFIG.TAG_CATEGORY_NAME)"
+ Write-Host "DEBUG: TagName=$($SECRETS_CONFIG.TAG_NAME)"
+ Write-Host "DEBUG: vRoURL=`"$($vroUrl | Format-List | Out-String)`""
+ Write-Host "DEBUG: headers=`"$($headers | Format-List | Out-String)`""
+ Write-Host "DEBUG: body=$body"
+}
+
+Write-Host "Applying vSphere Tag: $($SECRETS_CONFIG.TAG_NAME) to VM: $vm ..."
+if($env:skip_vro_cert_check -eq "true") {
+ Invoke-Webrequest -Uri $vroUrl -Method POST -Headers $headers -SkipHeaderValidation -Body $body -SkipCertificateCheck
+} else {
+ Invoke-Webrequest -Uri $vroUrl -Method POST -Headers $headers -SkipHeaderValidation -Body $body
+}
diff --git a/examples/powershell/vro/stack.yml b/examples/powershell/vro/stack.yml
new file mode 100644
index 00000000..45025ec9
--- /dev/null
+++ b/examples/powershell/vro/stack.yml
@@ -0,0 +1,17 @@
+provider:
+ name: openfaas
+ gateway: https://veba.primp-industries.com
+functions:
+ powershell-vro:
+ lang: powercli
+ handler: ./handler
+ image: vmware/veba-powershell-vro:latest
+ environment:
+ write_debug: true
+ read_debug: true
+ function_debug: false
+ skip_vro_cert_check: true
+ secrets:
+ - vro-secrets
+ annotations:
+ topic: DrsVmPoweredOnEvent,VmPoweredOnEvent
diff --git a/examples/powershell/vro/template/powercli/Dockerfile b/examples/powershell/vro/template/powercli/Dockerfile
new file mode 100644
index 00000000..c5ab76f9
--- /dev/null
+++ b/examples/powershell/vro/template/powercli/Dockerfile
@@ -0,0 +1,28 @@
+FROM photon:3.0
+ENV TERM linux
+
+WORKDIR /root
+
+# Set terminal. If we don't do this, weird readline things happen.
+RUN echo "/usr/bin/pwsh" >> /etc/shells && \
+ echo "/bin/pwsh" >> /etc/shells && \
+ tdnf install -y powershell-6.2.3-1.ph3 unzip && \
+ pwsh -c "Set-PSRepository -Name PSGallery -InstallationPolicy Trusted" && \
+ find / -name "net45" | xargs rm -rf && \
+ tdnf erase -y unzip && \
+ tdnf clean all
+
+RUN echo "Pulling watchdog binary from Github." \
+ && curl -sSL https://github.com/openfaas/faas/releases/download/0.9.14/fwatchdog > /usr/bin/fwatchdog \
+ && chmod +x /usr/bin/fwatchdog \
+ && cp /usr/bin/fwatchdog /root
+
+# Populate example here - i.e. "cat", "sha512sum" or "node index.js"
+SHELL [ "pwsh", "-command" ]
+ENV fprocess="xargs pwsh ./function/script.ps1"
+COPY function function
+
+EXPOSE 8080
+
+HEALTHCHECK --interval=3s CMD [ -e /tmp/.lock ] || exit 1
+CMD [ "fwatchdog" ]
diff --git a/examples/powershell/vro/template/powercli/function/script.ps1 b/examples/powershell/vro/template/powercli/function/script.ps1
new file mode 100644
index 00000000..3e28cf60
--- /dev/null
+++ b/examples/powershell/vro/template/powercli/function/script.ps1
@@ -0,0 +1,2 @@
+Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -DisplayDeprecationWarnings $false -ParticipateInCeip $false -Scope AllUsers -Confirm:$false
+write-host "write your powercli code here"
diff --git a/examples/powershell/vro/template/powercli/template.yml b/examples/powershell/vro/template/powercli/template.yml
new file mode 100644
index 00000000..0f54f4e4
--- /dev/null
+++ b/examples/powershell/vro/template/powercli/template.yml
@@ -0,0 +1,3 @@
+language: powercli
+fprocess: xargs pwsh ./function/script.ps1
+
diff --git a/examples/powershell/vro/vro-secrets.json b/examples/powershell/vro/vro-secrets.json
new file mode 100644
index 00000000..19a83416
--- /dev/null
+++ b/examples/powershell/vro/vro-secrets.json
@@ -0,0 +1,8 @@
+{
+ "VRO_SERVER" : "vro.cpbu.corp",
+ "VRO_USERNAME": "vro-user@cpbu.corp",
+ "VRO_PASSWORD": "FILL-ME-IN",
+ "VRO_WORKFLOW_ID": "505e998a-2d3e-4608-b0bf-94c665dad8b5",
+ "TAG_CATEGORY_NAME": "Application",
+ "TAG_NAME" : "Photon"
+}
diff --git a/examples/python/echo/README.md b/examples/python/echo/README.md
index 31de1f92..419a5cd5 100644
--- a/examples/python/echo/README.md
+++ b/examples/python/echo/README.md
@@ -1,4 +1,4 @@
-# vCenter Event Broker Appliance Echo Event Function
+# VMware Event Broker Appliance Echo Event Function
## Description
@@ -14,7 +14,7 @@ git checkout master
Step 2 - Edit `stack.yml` and update the topic with the specific vCenter Server Event(s) from [vCenter Event Mapping](https://github.com/lamw/vcenter-event-mapping) document
-Step 3 - Login to the OpenFaaS gateway on vCenter Event Broker Appliance
+Step 3 - Login to the OpenFaaS gateway on VMware Event Broker Appliance
```
VEBA_GATEWAY=https://veba.primp-industries.com
@@ -23,7 +23,7 @@ export OPENFAAS_URL=${VEBA_GATEWAY}
faas-cli login --username admin --password-stdin --tls-no-verify
```
-Step 4 - Deploy function to vCenter Event Broker Appliance
+Step 4 - Deploy function to VMware Event Broker Appliance
```
faas-cli deploy -f stack.yml --tls-no-verify
diff --git a/examples/python/esx-mtu-fixer/README.md b/examples/python/esx-mtu-fixer/README.md
index 22e84100..c6d3466b 100644
--- a/examples/python/esx-mtu-fixer/README.md
+++ b/examples/python/esx-mtu-fixer/README.md
@@ -46,7 +46,9 @@ Before you start deploy arbitrary VM on one of your ESX hosts.
### Fix the MTU
-* Trigger `vm.powered.on` event, by powering on a VM.
-> Note for DRS-enabled clusters the event should be `drs.vm.powered.on`
+* Trigger a `VmPoweredOnEvent` by powering on a VM.
+
+> **Note:** for DRS-enabled clusters the event should be `DrsVmPoweredOnEvent`
+
* Navigate to the same place to see the MTU is back to `1500`
diff --git a/examples/python/invoke-rest-api/README.MD b/examples/python/invoke-rest-api/README.MD
new file mode 100644
index 00000000..da37c6cd
--- /dev/null
+++ b/examples/python/invoke-rest-api/README.MD
@@ -0,0 +1,199 @@
+# Function to make a POST request to any REST API
+
+This function aims to provide a easy way to make HTTP Post request to an API Endpoint that might help with a lot of integration scenarios. Eg. Post to a Slack channel, Create a PagerDuty or ServiceNow incident.
+
+> **NOTE:** This function currently supports endpoints that allow basic authentication (un/pwd) or token based authentication that can be passed with the headers
+
+- [Deploy](#deploy)
+ * [Get the example function](#get-the-example-function)
+ * [Customize the function](#customize-the-function)
+ + [Understanding the Metaconfig-[SYSTEM].json](#understanding-the-metaconfig--system-json)
+ - [Provide the API Details](#provide-the-api-details)
+ - [Mapping the Events and Request body](#mapping-the-events-and-request-body)
+ + [Updating the Stack.yml](#updating-the-stackyml)
+ + [Updating the Handler.py (advanced)](#updating-the-handlerpy--advanced-)
+ * [Deploy the function](#deploy-the-function)
+ + [Create the secret](#create-the-secret)
+ + [Build function (only if handler.py is changed)](#build-function--only-if-handlerpy-is-changed-)
+ + [Deploy the function](#deploy-the-function-1)
+ * [Trigger the function](#trigger-the-function)
+- [Troubleshooting](#troubleshooting)
+
+# Deploy
+
+## Get the example function
+
+Clone this repository which contains the example functions.
+
+```bash
+git clone https://github.com/vmware-samples/vcenter-event-broker-appliance
+cd vcenter-event-broker-appliance/examples/python/invoke-rest-api
+git checkout master
+```
+
+## Customize the function
+
+There are three key files, that you might have to modify if you are looking to customize this function and make a post api call to an external system.
+
+```bash
+ /invoke-rest-api/metaconfig-[SYSTEM].json #sample copies provided for PagerDuty, Slack, JIRA, Zendesk, ServiceDesk and ServiceNow
+ /invoke-rest-api/stack.yml
+ /invoke-rest-api/handler/handler.py
+```
+
+### Understanding the Metaconfig-[SYSTEM].json
+First, change the configuration file `metaconfig-[SYSTEM].json` (samples for Slack, PagerDuty and ServiceNow are provided with this example) holding both sensitive and configuration information needed for this function in this folder.
+
+The `metaconfig-[SYSTEM].json` (as you can see below) has inputs needed to make a POST API request such as - `url, auth, headers` and `body`. These fields are self explanatory for an API and here are some of the considerations for this functions
+
+#### Provide the API Details
+* **url** - Http(s) endpoint to the System's API Endpoint. The function only does a POST call and this endpoint should be able to accept POST http request
+* **auth** - Most APIs provide some sort of API key to use and don't seem to require authentication credentials. While some others require basic authentication which can be send in the header. Where the API requires username and password explicitly, i've added that as an option in the config. The function requires the presence of the auth key in the config (it can be an empty dict if the API does not require any username/password for authentication)
+* **headers** - JSON key value pair of any headers that the API requires
+* **body** - currently support only json body (which most APIs should support)
+
+> **NOTE:** All these keys are required attributes. They can be empty if not required but if they are missing in the config then the function will fail.
+
+
+#### Mapping the Events and Request body
+The `mapping` information provides a way to pull data from the Event that vCenter sends and replaces that within the request body before making the POST call.
+
+* `"pull": "data/FullFormattedMessage"` attempts to get the value of `FullFormattedMessage` within the `data` dict of the cloud Event that we receive from VMware Event Router
+* `"push": "payload/summary"` updates the `summary` field within the `payload` key of the request body as provided in this configuration file
+
+> **Note:** This function has been developed to handle VMPoweredOn(/Off)Event by default, which you can see in the provided samples. Please edit the mapping for other Events accordingly.
+
+```json
+{
+ "url": "https://events.pagerduty.com/v2/enqueue",
+ "auth": {
+ "un": "",
+ "pwd": ""
+ },
+ "headers": {
+ "Authorization":"Bearer ",
+ .....
+ },
+ "body": {
+ "payload": {
+ "summary": "Example alert on host1.example.com",
+ .....
+ }
+ },
+ "mappings": [
+ {
+ "push": "payload/summary",
+ "pull": "data/FullFormattedMessage"
+ }
+ .....
+ ]
+}
+```
+
+### Updating the Stack.yml
+Function-specific settings are performed in the `stack.yml` file such as gateway, image, environment variables, secrets(configs) and the topics(events) that this function will subscribe to. Open and edit the `stack.yml` provided to change as per your environment/needs.
+
+> **Note:** A key-value annotation under `topic` defines which VM event should trigger the function. A list of VM events from vCenter can be found [here](https://code.vmware.com/doc/preview?id=4206#/doc/vim.event.VmEvent.html). Multiple topics can be specified using a `","` delimiter syntax, e.g. "`topic: "VmPoweredOnEvent,VmPoweredOffEvent"`".
+
+```yaml
+provider:
+ name: openfaas
+ gateway: https://VEBA_FQDN_OR_IP # replace with your vCenter Event Broker Appliance URL
+functions:
+ restpost-fn:
+ lang: python3
+ handler: ./handler
+ image: vmware/veba-python-restpost:latest
+ environment:
+ write_debug: true # additional debugging messages are printed
+ combine_output: false # required to prevent debug messages from showing up in faas response
+ read_debug: true
+ insecure_ssl: true # set to false if you have a trusted TLS certificate on VEBA
+ secrets:
+ - metaconfig # leave as is, you will need to edit the function if this is changed
+ annotations:
+ topic: VmPoweredOnEvent,VmPoweredOffEvent # DrsVmPoweredOnEvent in a DRS-enabled cluster
+```
+
+> **Note:** If you are running a vSphere DRS-enabled cluster the topic annotation above should be `DrsVmPoweredOnEvent`. Otherwise the function would never be triggered.
+
+### Updating the Handler.py (advanced)
+You might have to edit this file if you are looking to possibly have multiple copies of this function running to make api calls to different system or to improve the function.
+
+To have multiple copies of this function running, you'll need multiple metaconfigs for each system and end up creating multiple secrets for each config. The `handler.py` for each function will have to be updated to reference their respective secret. You can do this by updating the below line in the file
+
+```
+META_CONFIG='/var/openfaas/secrets/metaconfig-[SYSTEM]'
+```
+
+For others that are looking to update the function and make improvements, please have at it!
+
+## Deploy the function
+
+For the most part (if you didn't have to edit the handler.py), you'll have to create the secret and deploy the function.
+
+### Create the secret
+Let's store the configuration file as secret in the appliance.
+
+```bash
+# set up faas-cli for first use
+export OPENFAAS_URL=https://VEBA_FQDN_OR_IP
+faas-cli login -p VEBA_OPENFAAS_PASSWORD --tls-no-verify # vCenter Event Broker Appliance is configured with authentication, pass in the password used during the vCenter Event Broker Appliance deployment process
+
+# now create the secret
+faas-cli secret create metaconfig --from-file=metaconfig-[SYSTEM].json --tls-no-verify
+```
+
+> **Note:** Delete the local `metaconfig-[SYSTEM].json` after you're done with this exercise to not expose any sensitive information.
+
+### Build function (only if handler.py is changed)
+
+> **NOTE:** This is only required if you changed the `handler.py`
+
+ Under the hoods, the functions are deployed as a container. Usually these containers are built and made readily available for you with the example function. However, when you make changes the function, you'll have to build the function and build the container.
+
+```bash
+faas-cli template pull
+
+faas-cli build
+
+faas-cli push #optional if you are pushing to DockerHub
+```
+
+> **NOTE:** Make sure the `image` tag in the `stack.yml` is updated to reference the correct image.
+
+### Deploy the function
+After you've performed the steps and modifications above, you can go ahead and deploy the function:
+
+```bash
+faas-cli deploy -f stack.yml --tls-no-verify
+Deployed. 202 Accepted.
+```
+
+## Trigger the function
+
+Turn on a virtual machine, e.g. in vCenter or via `govc` CLI, to trigger the function via a `(DRS)VmPoweredOnEvent`. Verify that the API was correctly called.
+
+> **Note:** If the API doesn't get called verify that you correctly followed each step above, IPs/FQDNs and credentials are correct and see the [troubleshooting](#troubleshooting) section below.
+
+# Troubleshooting
+
+If the API doesn't get called, verify:
+
+- Validate that the API call works with Postman or cURL
+- Validate the configurations provided within the `metaconfig-[SYSTEM].json`
+- Validate the `stack.yml` file and the topic being subscribed to
+- Verify if the components can talk to each other (VMware Event Router to vCenter and OpenFaaS, VMware Event Broker Appliance to API)
+- Check the logs:
+
+```bash
+faas-cli logs restpost-fn --follow --tls-no-verify
+
+# Successful log message in the OpenFaaS tagging function
+2019/01/25 23:48:55 Forking fprocess.
+2019/01/25 23:48:55 Query
+2019/01/25 23:48:55 Path /
+......
+{"status": "200", "message": "Successfully executed the REST API call"}
+2019/01/25 23:48:56 Duration: 1.551482 seconds
+```
\ No newline at end of file
diff --git a/examples/python/invoke-rest-api/handler/handler.py b/examples/python/invoke-rest-api/handler/handler.py
new file mode 100644
index 00000000..78020fe3
--- /dev/null
+++ b/examples/python/invoke-rest-api/handler/handler.py
@@ -0,0 +1,248 @@
+import sys, json, os
+import urllib3
+import requests
+import dpath.util
+import traceback
+
+# GLOBAL_VARS
+DEBUG=False
+class bgc:
+ HEADER = '\033[95m'
+ OKBLUE = '\033[94m'
+ OKGREEN = '\033[92m'
+ WARNING = '\033[93m'
+ FAIL = '\033[91m'
+ ENDC = '\033[0m'
+ BOLD = '\033[1m'
+ UNDERLINE = '\033[4m'
+
+if(os.getenv("insecure_ssl")):
+ # Surpress SSL warnings
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+if(os.getenv("write_debug")):
+ sys.stderr.write(f"{bgc.WARNING}WARNING!! DEBUG has been enabled for this function. Sensitive information could be printed to sysout{bgc.ENDC} \n")
+ DEBUG=True
+
+def debug(s):
+ if DEBUG:
+ sys.stderr.write(s+" \n") #Syserr only get logged on the console logs
+
+#
+### Paths and Endpoints
+### More examples and details here - https://v2.developer.pagerduty.com/docs/send-an-event-events-api-v2
+#
+META_CONFIG='/var/openfaas/secrets/metaconfig'
+
+class FaaSResponse:
+ """
+ FaaSResponse is a helper class to construct a properly formatted message returned by this function.
+ By default, OpenFaaS will marshal this response message as JSON.
+ """
+ def __init__(self, status, message):
+ """
+ Arguments:
+ status {str} -- the response status code
+ message {str} -- the response message
+ """
+ self.status=status
+ self.message=message
+
+class RESTful:
+ """
+ RESTful is a class which aims to make Rest API calls easily without writing any code
+ """
+
+ def __init__(self, conn, config, event):
+ """
+ Constructor for RESTful class
+
+ Arguments:
+ conn {session} -- [Request Connection]
+ config {dict} -- [Config with URL, Body and Mapping for Rest API call]
+ event {dict} -- [Cloud Event from vCenter]
+ """
+ self.session = conn
+ self.config = config
+ self.event = event
+
+ def geturl(self):
+ """
+ Getter for the URL to make the call
+
+ Returns:
+ [string] -- [URL for the Rest API endpoint]
+ """
+ url = self.config['url']
+ return url
+
+ def getheaders(self):
+ header = self.config['headers']
+ return header
+
+ def getauth(self):
+ ref = self.config['auth']
+ try:
+ auth = (ref['un'], ref['pwd'])
+ except (TypeError,KeyError) as err:
+ debug(f'Unexpected auth param provided, assuming no auth > "{ref}"')
+ auth = None
+ return auth
+
+ def getbody(self):
+ """
+ Getter for the Request Body to make the call
+
+ Returns:
+ [dict] -- [JSON constructed body]
+ """
+ body = self.config['body']
+ mappings = self.config['mappings']
+ for mapping in mappings:
+ pushvalue = mapping['push']
+ pullvalue = mapping['pull']
+ #debug(f'Attempting mapping of Body:{pushvalue} with CloudEvent:{pullvalue}')
+ #debug(f'Replacing >>> {dpath.util.get(body, pushvalue)} with ::: {dpath.util.get(self.event, pullvalue)}')
+ dpath.util.set(body, pushvalue, dpath.util.get(self.event, pullvalue))
+ return body
+
+ # PagerDuty REST API implementation
+ def post(self):
+ """
+ Function to make the POST call to the endpoint
+
+ Returns:
+ [FaaSResponse] -- [Formatted message for OpenFaaS]
+ """
+
+ urlPath = self.geturl()
+ debug(f'{bgc.OKBLUE}> URL: {bgc.ENDC}{urlPath}')
+ authObj = self.getauth()
+ #debug(f'{bgc.OKBLUE}> Auth: {bgc.ENDC}{authObj}') #don't want auth printed
+ headerObj = self.getheaders()
+ debug(f'{bgc.OKBLUE}> Headers: {bgc.ENDC}{json.dumps(headerObj, indent=4)}')
+ bodyObj = self.getbody()
+ debug(f'{bgc.OKBLUE}> Body: {bgc.ENDC}{json.dumps(bodyObj, indent=4)}')
+ try:
+ resp = self.session.post(urlPath, auth=authObj, json=bodyObj, headers=headerObj)
+ resp.raise_for_status()
+ try:
+ resp_body = json.loads(resp.text)
+ debug(f'{bgc.OKBLUE}> Response: {bgc.ENDC}{json.dumps(resp_body, indent=4, sort_keys=True)}')
+ except json.JSONDecodeError as err:
+ debug(f'{bgc.OKBLUE}> Response: {bgc.ENDC}{resp.text}') #some apis don't return json
+
+ return FaaSResponse('200', f'Response:{resp.text}')
+ except requests.HTTPError as err:
+ return FaaSResponse('500', 'Could not executed REST API > HTTPError: {0}'.format(err))
+
+def handle(req):
+
+ # Load the Events that function gets from vCenter through the Event Router
+ debug(f'{bgc.HEADER}Reading Cloud Event: {bgc.ENDC}')
+ debug(f'{bgc.OKBLUE}Event > {bgc.ENDC}{req}')
+ try:
+ cevent = json.loads(req)
+ except json.JSONDecodeError as err:
+ res = FaaSResponse('400','Invalid JSON > JSONDecodeError: {0}'.format(err))
+ print(json.dumps(vars(res)))
+ return
+
+ # Load the Config File
+ debug(f'{bgc.HEADER}Reading Configuration file: {bgc.ENDC}')
+ debug(f'{bgc.OKBLUE}Config File > {bgc.ENDC}{META_CONFIG}')
+ try:
+ with open(META_CONFIG, 'r') as prodconfig:
+ metaconfig = json.load(prodconfig)
+ except json.JSONDecodeError as err:
+ res = FaaSResponse('400','Invalid JSON > JSONDecodeError: {0}'.format(err))
+ print(json.dumps(vars(res)))
+ return
+ except OSError as err:
+ res = FaaSResponse('500','Could not read configuration > OSError: {0}'.format(err))
+ print(json.dumps(vars(res)))
+ return
+
+ #Validate CloudEvent and Configuration for mandatory fields
+ debug(f'{bgc.HEADER}Validating Input data and mapping: {bgc.ENDC}')
+ debug(f'{bgc.OKBLUE}Event > {bgc.ENDC}{json.dumps(cevent, indent=4, sort_keys=True)}')
+ debug(f'{bgc.OKBLUE}Config > {bgc.ENDC}{json.dumps(metaconfig, indent=4, sort_keys=True)}')
+ try:
+ #CloudEvent - simple validation
+ event = cevent['data']
+
+ #Config - checking for required fields
+ url = metaconfig['url'] #not validating if an actual URL is provided
+ auth = metaconfig['auth'] #auth can be empty for no auth but a required key
+ headers = metaconfig['headers'] #not validating sanctity of headers
+ body = metaconfig['body'] #json only supported
+ mappings = metaconfig['mappings'] #mapping can be empty array but the key needs to be present in the config
+
+ #supports 1-1 event-config mapping. next iteration can take complex 1-*(seperated by comma) mapping that will allow building out strings with values from the event
+ for mapping in mappings:
+ pushvalue = mapping['push']
+ debug(f'Config has key "{pushvalue}" >>> "{dpath.util.get(body, pushvalue)}"')
+ pullvalue = mapping['pull']
+ debug(f'Event has key "{pullvalue}" >>> "{dpath.util.get(cevent, pullvalue)}"')
+ except KeyError as err:
+ res = FaaSResponse('400','Invalid JSON, required key not found > KeyError: {0}'.format(err))
+ traceback.print_exc(limit=1, file=sys.stderr) #providing traceback since it helps debug the exact key that failed
+ print(json.dumps(vars(res)))
+ return
+ except ValueError as err:
+ res = FaaSResponse('400','Invalid mapping, multiple keys found > ValueError: {0}'.format(err))
+ traceback.print_exc(limit=1, file=sys.stderr) #providing traceback since it helps debug the exact key that failed
+ print(json.dumps(vars(res)))
+ return
+
+ # Make the Rest Api Call
+ s=requests.Session()
+ if(os.getenv("insecure_ssl")):
+ s.verify=False
+
+ # with the metaconfig - which is the configuration file with the URL and body to make the call
+ # and with the cloud event - which is the event generated from vCenter
+ # we are going to build the request body and make the rest api call
+ debug(f'{bgc.HEADER}Attemping HTTP POST: {bgc.ENDC}')
+ try:
+ restful = RESTful(s, metaconfig, cevent)
+ res = restful.post()
+ print(json.dumps(vars(res)))
+ except Exception as err:
+ res = FaaSResponse('500','Unexpected error occurred > Exception: {0}'.format(err))
+ traceback.print_exc(limit=1, file=sys.stderr) #providing traceback since it helps debug the exact key that failed
+ print(json.dumps(vars(res)))
+
+ s.close()
+
+ return
+
+#
+## Unit Test - helps testing the function locally
+## Uncomment META_CONFIG - update the path to the file accordingly
+## Uncomment handle('...') to test the function with the event samples provided below test without deploying to OpenFaaS
+#
+#META_CONFIG='metaconfig-pduty.json'
+
+#
+## FAILURE CASES :Invalid Inputs
+#
+#handle('')
+#handle('"test":"ok"')
+#handle('{"test":"ok"}')
+#handle('{"data":"ok"}')
+
+#
+## FAILURE CASES :Unhandled Events
+#
+# Standard : UserLogoutSessionEvent
+#handle('{"id":"17e1027a-c865-4354-9c21-e8da3df4bff9","source":"https://vcsa.pdotk.local/sdk","specversion":"1.0","type":"com.vmware.event.router/event","subject":"UserLogoutSessionEvent","time":"2020-04-14T00:28:36.455112549Z","data":{"Key":7775,"ChainId":7775,"CreatedTime":"2020-04-14T00:28:35.221698Z","UserName":"machine-b8eb9a7f","Datacenter":null,"ComputeResource":null,"Host":null,"Vm":null,"Ds":null,"Net":null,"Dvs":null,"FullFormattedMessage":"User machine-b8ebe7eb9a7f@127.0.0.1 logged out (login time: Tuesday, 14 April, 2020 12:28:35 AM, number of API invocations: 34, user agent: pyvmomi Python/3.7.5 (Linux; 4.19.84-1.ph3; x86_64))","ChangeTag":"","IpAddress":"127.0.0.1","UserAgent":"pyvmomi Python/3.7.5 (Linux; 4.19.84-1.ph3; x86_64)","CallCount":34,"SessionId":"52edf160927","LoginTime":"2020-04-14T00:28:35.071817Z"},"datacontenttype":"application/json"}')
+# Eventex : vim.event.ResourceExhaustionStatusChangedEvent
+#handle('{"id":"0707d7e0-269f-42e7-ae1c-18458ecabf3d","source":"https://vcsa.pdotk.local/sdk","specversion":"1.0","type":"com.vmware.event.router/eventex","subject":"vim.event.ResourceExhaustionStatusChangedEvent","time":"2020-04-14T00:20:15.100325334Z","data":{"Key":7715,"ChainId":7715,"CreatedTime":"2020-04-14T00:20:13.76967Z","UserName":"machine-bb9a7f","Datacenter":null,"ComputeResource":null,"Host":null,"Vm":null,"Ds":null,"Net":null,"Dvs":null,"FullFormattedMessage":"vCenter Log File System Resource status changed from Yellow to Green on vcsa.pdotk.local ","ChangeTag":"","EventTypeId":"vim.event.ResourceExhaustionStatusChangedEvent","Severity":"info","Message":"","Arguments":[{"Key":"resourceName","Value":"storage_util_filesystem_log"},{"Key":"oldStatus","Value":"yellow"},{"Key":"newStatus","Value":"green"},{"Key":"reason","Value":" "},{"Key":"nodeType","Value":"vcenter"},{"Key":"_sourcehost_","Value":"vcsa.pdotk.local"}],"ObjectId":"","ObjectType":"","ObjectName":"","Fault":null},"datacontenttype":"application/json"}')
+
+#
+## SUCCESS CASES
+#
+# Standard : VmPoweredOnEvent
+#handle('{"id":"453120cd-3d19-4c43-aadc-df0cdbce3887","source":"https://vcsa.pdotk.local/sdk","specversion":"1.0","type":"com.vmware.event.router/event","subject":"VmPoweredOnEvent","time":"2020-04-13T23:46:10.402531287Z","data":{"Key":7441,"ChainId":7438,"CreatedTime":"2020-04-13T23:46:09.387283Z","UserName":"Administrator","Datacenter":{"Name":"PKLAB","Datacenter":{"Type":"Datacenter","Value":"datacenter-3"}},"ComputeResource":{"Name":"esxi01.pdotk.local","ComputeResource":{"Type":"ComputeResource","Value":"domain-s29"}},"Host":{"Name":"esxi01.pdotk.local","Host":{"Type":"HostSystem","Value":"host-31"}},"Vm":{"Name":"Test VM","Vm":{"Type":"VirtualMachine","Value":"vm-33"}},"Ds":null,"Net":null,"Dvs":null,"FullFormattedMessage":"Test VM on esxi01.pdotk.local in PKLAB has powered on","ChangeTag":"","Template":false},"datacontenttype":"application/json"}')
+# Standard : VmPoweredOffEvent
+#handle('{"id":"d77a3767-1727-49a3-ac33-ddbdef294150","source":"https://vcsa.pdotk.local/sdk","specversion":"1.0","type":"com.vmware.event.router/event","subject":"VmPoweredOffEvent","time":"2020-04-14T00:33:30.838669841Z","data":{"Key":7825,"ChainId":7821,"CreatedTime":"2020-04-14T00:33:30.252792Z","UserName":"Administrator","Datacenter":{"Name":"PKLAB","Datacenter":{"Type":"Datacenter","Value":"datacenter-3"}},"ComputeResource":{"Name":"esxi01.pdotk.local","ComputeResource":{"Type":"ComputeResource","Value":"domain-s29"}},"Host":{"Name":"esxi01.pdotk.local","Host":{"Type":"HostSystem","Value":"host-31"}},"Vm":{"Name":"Test VM","Vm":{"Type":"VirtualMachine","Value":"vm-33"}},"Ds":null,"Net":null,"Dvs":null,"FullFormattedMessage":"Test VM on esxi01.pdotk.local in PKLAB is powered off","ChangeTag":"","Template":false},"datacontenttype":"application/json"}')
diff --git a/examples/python/invoke-rest-api/handler/requirements.txt b/examples/python/invoke-rest-api/handler/requirements.txt
new file mode 100644
index 00000000..c195d28b
--- /dev/null
+++ b/examples/python/invoke-rest-api/handler/requirements.txt
@@ -0,0 +1,3 @@
+urllib3==1.25.6
+requests==2.22.0
+dpath==2.0.1
diff --git a/examples/python/invoke-rest-api/metaconfig-jira.json b/examples/python/invoke-rest-api/metaconfig-jira.json
new file mode 100644
index 00000000..dfcc3fbd
--- /dev/null
+++ b/examples/python/invoke-rest-api/metaconfig-jira.json
@@ -0,0 +1,34 @@
+{
+ "url": "https://pk-sdesk.atlassian.net/rest/api/2/issue",
+ "headers": {
+ "Content-Type": "application/json; charset=UTF-8",
+ "Accept": "application/json; charset=UTF-8"
+ },
+ "auth": {
+ "un": "",
+ "pwd": ""
+ },
+ "body": {
+ "fields": {
+ "project":
+ {
+ "key": "VEBA"
+ },
+ "summary": "REST ye merry gentlemen.",
+ "description": "Creating of an issue using project keys and issue type names using the REST API",
+ "issuetype": {
+ "name": "Bug"
+ }
+ }
+ },
+ "mappings": [
+ {
+ "push": "fields/summary",
+ "pull": "data/FullFormattedMessage"
+ },
+ {
+ "push": "fields/description",
+ "pull": "data/FullFormattedMessage"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/python/invoke-rest-api/metaconfig-pduty.json b/examples/python/invoke-rest-api/metaconfig-pduty.json
new file mode 100644
index 00000000..59bff665
--- /dev/null
+++ b/examples/python/invoke-rest-api/metaconfig-pduty.json
@@ -0,0 +1,60 @@
+{
+ "url": "https://events.pagerduty.com/v2/enqueue",
+ "headers": {
+ "content-type": "application/json; charset=UTF-8"
+ },
+ "auth": {},
+ "body": {
+ "event_action": "trigger",
+ "client": "VMware Event Broker Appliance",
+ "client_url": "https://veba-dev",
+ "routing_key": "",
+ "payload": {
+ "summary": "Example alert on host1.example.com",
+ "timestamp": "2015-07-17T08:42:58.315+0000",
+ "source": "monitoringtool:cloudvendor:central-region-dc-01:852559987:cluster/api-stats-prod-003",
+ "severity": "info",
+ "component": "postgres",
+ "group": "prod-datapipe",
+ "class": "deploy",
+ "custom_details": {
+ "ping time": "1500ms",
+ "load avg": 0.75
+ }
+ }
+ },
+ "mappings": [
+ {
+ "push": "payload/summary",
+ "pull": "data/FullFormattedMessage"
+ },
+ {
+ "push": "payload/timestamp",
+ "pull": "time"
+ },
+ {
+ "push": "payload/source",
+ "pull": "source"
+ },
+ {
+ "push": "payload/component",
+ "pull": "data/Vm/Name"
+ },
+ {
+ "push": "payload/group",
+ "pull": "data/Host/Name"
+ },
+ {
+ "push": "payload/class",
+ "pull": "subject"
+ },
+ {
+ "push": "client",
+ "pull": "source"
+ },
+ {
+ "push": "client_url",
+ "pull": "source"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/python/invoke-rest-api/metaconfig-sdesk.json b/examples/python/invoke-rest-api/metaconfig-sdesk.json
new file mode 100644
index 00000000..7e11516f
--- /dev/null
+++ b/examples/python/invoke-rest-api/metaconfig-sdesk.json
@@ -0,0 +1,32 @@
+{
+ "url": "https://pk-sdesk.atlassian.net/rest/servicedeskapi/request",
+ "headers": {
+ "Content-Type": "application/json; charset=UTF-8",
+ "Accept": "application/json; charset=UTF-8"
+ },
+ "auth": {
+ "un": "",
+ "pwd": ""
+ },
+ "body": {
+ "serviceDeskId": "1",
+ "requestTypeId": "10007",
+ "requestFieldValues": {
+ "summary": "Request JSD help via REST",
+ "description": "I need a new mouse for my Mac"
+ },
+ "requestParticipants": [
+ "qm:2bce408d-160f-4209-a12d-4939013fba18:71b91e5d-b836-4078-bcae-a43d303fd3a6"
+ ]
+ },
+ "mappings": [
+ {
+ "push": "requestFieldValues/summary",
+ "pull": "data/FullFormattedMessage"
+ },
+ {
+ "push": "requestFieldValues/description",
+ "pull": "data/FullFormattedMessage"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/python/invoke-rest-api/metaconfig-slack.json b/examples/python/invoke-rest-api/metaconfig-slack.json
new file mode 100644
index 00000000..412ab9ab
--- /dev/null
+++ b/examples/python/invoke-rest-api/metaconfig-slack.json
@@ -0,0 +1,76 @@
+{
+ "url": "https://.slack.com/services/T024....FIe",
+ "headers": {
+ "content-type": "application/json; charset=UTF-8"
+ },
+ "auth": {},
+ "body": {
+ "text": "Title",
+ "blocks": [
+ {
+ "type": "section",
+ "text": {
+ "type": "mrkdwn",
+ "text": "Title"
+ }
+ },
+ {
+ "type": "section",
+ "block_id": "section567",
+ "text": {
+ "type": "mrkdwn",
+ "text": "Description"
+ },
+ "accessory": {
+ "type": "image",
+ "image_url": "https://clipartart.com/images/vmware-esxi-clipart-2.jpg",
+ "alt_text": "vCenter"
+ },
+ "fields": [
+ {
+ "type": "mrkdwn",
+ "text": "Additional Detail"
+ },
+ {
+ "type": "mrkdwn",
+ "text": "Additional Detail"
+ },
+ {
+ "type": "mrkdwn",
+ "text": "Additional Detail"
+ }
+ ]
+ }
+ ]
+ },
+ "mappings": [
+ {
+ "push": "text",
+ "pull": "subject"
+ },
+ {
+ "push": "blocks/[0]/text/text",
+ "pull": "subject"
+ },
+ {
+ "push": "blocks/[1]/text/text",
+ "pull": "data/FullFormattedMessage"
+ },
+ {
+ "push": "blocks/[1]/accessory/alt_text",
+ "pull": "source"
+ },
+ {
+ "push": "blocks/[1]/fields/[0]/text",
+ "pull": "data/Datacenter/Datacenter/Value"
+ },
+ {
+ "push": "blocks/[1]/fields/[1]/text",
+ "pull": "data/Host/Host/Value"
+ },
+ {
+ "push": "blocks/[1]/fields/[2]/text",
+ "pull": "data/Vm/Vm/Value"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/python/invoke-rest-api/metaconfig-snow.json b/examples/python/invoke-rest-api/metaconfig-snow.json
new file mode 100644
index 00000000..afea6015
--- /dev/null
+++ b/examples/python/invoke-rest-api/metaconfig-snow.json
@@ -0,0 +1,20 @@
+{
+ "url": "https://.service-now.com/api/now/table/incident?sysparm_limit=1",
+ "headers": {
+ "content-type": "application/json; charset=UTF-8"
+ },
+ "auth": {
+ "un": "",
+ "pwd": ""
+ },
+ "body": {
+ "short_description":"Test incident creation through REST",
+ "comments":"VEBA > Please validate this incident"
+ },
+ "mappings": [
+ {
+ "push": "short_description",
+ "pull": "data/FullFormattedMessage"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/python/invoke-rest-api/metaconfig-zdesk.json b/examples/python/invoke-rest-api/metaconfig-zdesk.json
new file mode 100644
index 00000000..d1653434
--- /dev/null
+++ b/examples/python/invoke-rest-api/metaconfig-zdesk.json
@@ -0,0 +1,25 @@
+{
+ "url": "https://pk-zen.zendesk.com/api/v2/tickets.json",
+ "headers": {
+ "Content-Type": "application/json; charset=UTF-8",
+ "Accept": "application/json; charset=UTF-8"
+ },
+ "auth": {
+ "un": "",
+ "pwd": ""
+ },
+ "body": {
+ "ticket": {
+ "subject": "My printer is on fire!",
+ "comment": {
+ "body": "VEBA: Please look into this ASAP"
+ }
+ }
+ },
+ "mappings": [
+ {
+ "push": "ticket/subject",
+ "pull": "data/FullFormattedMessage"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/python/invoke-rest-api/stack.yml b/examples/python/invoke-rest-api/stack.yml
new file mode 100644
index 00000000..0e360906
--- /dev/null
+++ b/examples/python/invoke-rest-api/stack.yml
@@ -0,0 +1,18 @@
+version: 1.0
+provider:
+ name: openfaas
+ gateway: https://VEBA_FQDN_OR_IP
+functions:
+ restpost-fn:
+ lang: python3
+ handler: ./handler
+ image: vmware/veba-python-restpost:latest
+ environment:
+ write_debug: true
+ read_debug: true
+ combine_output: false
+ insecure_ssl: true
+ secrets:
+ - metaconfig
+ annotations:
+ topic: VmPoweredOnEvent,VmPoweredOffEvent
\ No newline at end of file
diff --git a/examples/python/tagging/README.MD b/examples/python/tagging/README.MD
index 41af15be..b27c1837 100644
--- a/examples/python/tagging/README.MD
+++ b/examples/python/tagging/README.MD
@@ -59,7 +59,7 @@ faas-cli login -p VEBA_OPENFAAS_PASSWORD --tls-no-verify # vCenter Event Broker
faas-cli secret create vcconfig --from-file=vcconfig.toml --tls-no-verify
```
-**Note:** Delete the local `vcconfig.toml` after you're done with this exercise to not expose this sensitive information.
+> **Note:** Delete the local `vcconfig.toml` after you're done with this exercise to not expose this sensitive information.
Lastly, define the vCenter event which will trigger this function. Such function-specific settings are performed in the `stack.yml` file. Open and edit the `stack.yml` provided with in the Python tagging example code. Change `gateway` and `topic` as per your environment/needs.
@@ -83,7 +83,7 @@ functions:
topic: VmPoweredOnEvent # or DrsVmPoweredOnEvent in a DRS-enabled cluster
```
-**Note:** If you are running a vSphere DRS-enabled cluster the topic annotation above should be `DrsVmPoweredOnEvent`. Otherwise the function would never be triggered.
+> **Note:** If you are running a vSphere DRS-enabled cluster the topic annotation above should be `DrsVmPoweredOnEvent`. Otherwise the function would never be triggered.
### Deploy the function
@@ -99,7 +99,7 @@ Deployed. 202 Accepted.
Turn on a virtual machine, e.g. in vCenter or via `govc` CLI, to trigger the function via a `(DRS)VmPoweredOnEvent`. Verify the virtual machine was correctly tagged.
-**Note:** If you don't see a tag being assigned verify that you correctly followed each step above, IPs/FQDNs and credentials are correct and see the [troubleshooting](#troubleshooting) section below.
+> **Note:** If you don't see a tag being assigned verify that you correctly followed each step above, IPs/FQDNs and credentials are correct and see the [troubleshooting](#troubleshooting) section below.
## Troubleshooting
diff --git a/examples/python/trigger-pagerduty-incident/README.MD b/examples/python/trigger-pagerduty-incident/README.MD
new file mode 100644
index 00000000..8017ecad
--- /dev/null
+++ b/examples/python/trigger-pagerduty-incident/README.MD
@@ -0,0 +1,98 @@
+### Get the example function
+
+Clone this repository which contains the example functions.
+
+```bash
+git clone https://github.com/vmware-samples/vcenter-event-broker-appliance
+cd vcenter-event-broker-appliance/examples/python/trigger-pagerduty-incident
+git checkout master
+```
+
+### Customize the function
+
+For security reasons to not expose sensitive data we will create a Kubernetes [secret](https://kubernetes.io/docs/concepts/configuration/secret/) which will hold the routingkey information for the PagerDuty Event API v2. This secret will be mounted into the function during runtime and is taken care of by the appliance. We only have to create the secret with a simple command through `faas-cli`.
+
+First, change the configuration file [pdconfig.json](pdconfig.json) holding your sensitive pagerduty information in this folder:
+
+```json
+{
+ "routing_key": "",
+ "event_action": "trigger"
+}
+```
+
+Now go ahead and store this configuration file as secret in the appliance.
+
+```bash
+# set up faas-cli for first use
+export OPENFAAS_URL=https://VEBA_FQDN_OR_IP
+faas-cli login -p VEBA_OPENFAAS_PASSWORD --tls-no-verify # vCenter Event Broker Appliance is configured with authentication, pass in the password used during the vCenter Event Broker Appliance deployment process
+
+# now create the secret
+faas-cli secret create pdconfig --from-file=pdconfig.json --tls-no-verify
+```
+
+> **Note:** Delete the local `pdconfig.json` after you're done with this exercise to not expose this sensitive information.
+
+Lastly, define the vCenter event which will trigger this function. Such function-specific settings are performed in the `stack.yml` file. Open and edit the `stack.yml` provided with in the Python example code. Change `gateway` and `topic` as per your environment/needs.
+
+> **Note:** A key-value annotation under `topic` defines which VM event should trigger the function. A list of VM events from vCenter can be found [here](https://code.vmware.com/doc/preview?id=4206#/doc/vim.event.VmEvent.html). Multiple topics can be specified using a `","` delimiter syntax, e.g. "`topic: "VmPoweredOnEvent,VmPoweredOffEvent"`".
+
+```yaml
+provider:
+ name: openfaas
+ gateway: https://VEBA_FQDN_OR_IP # replace with your VMware Event Broker Appliance environment
+functions:
+ pdinvoke-fn:
+ lang: python3
+ handler: ./handler
+ image: vmware/veba-pagerduty-invoke:latest
+ environment:
+ write_debug: true #function writes verbose entries to the log when set to true, also requires combine_output to be set to false to avoid debug messages from showing up in the response
+ read_debug: true
+ combine_output: false #prevents error logs from showing up on the response output
+ insecure_ssl: true #set to true if you have a trusted TLS certificate on the gateway
+ secrets:
+ - pdconfig # update file with your Pagerduty integration key - https://v2.developer.pagerduty.com/docs/send-an-event-events-api-v2
+ annotations:
+ topic: VmPoweredOnEvent,VmPoweredOffEvent # or DrsVmPoweredOnEvent in a DRS-enabled cluster
+```
+
+> **Note:** If you are running a vSphere DRS-enabled cluster the topic annotation above should be `DrsVmPoweredOnEvent`. Otherwise the function would never be triggered.
+
+### Deploy the function
+
+After you've performed the steps and modifications above, you can go ahead and deploy the function:
+
+```bash
+faas-cli template pull # only required during the first deployment
+faas-cli deploy -f stack.yml --tls-no-verify
+Deployed. 202 Accepted.
+```
+
+### Trigger the function
+
+Turn on a virtual machine, e.g. in vCenter or via `govc` CLI, to trigger the function via a `(DRS)VmPoweredOnEvent`. You should now receive a PagerDuty notification through the configured notification channel
+
+> **Note:** If you are not seeing the PagerDuty alert upon the event being triggered, validate that the routing key in the `pdconfig.json` is correct and see the [troubleshooting](#troubleshooting) section below.
+
+## Troubleshooting
+
+If your PagerDuty event is not getting invoked, verify:
+
+- Routing Key in the `pdconfig.json`
+- Whether the components can talk to each other (VMware Event Router to vCenter and OpenFaaS, VMware Event Broker Appliance to PagerDuty)
+- If you have changed the `topic` in `stack.yml`, please ensure that the Function is also updated to handle the expected event data.
+- Check the logs:
+
+```bash
+faas-cli logs pdinvoke-fn --follow --tls-no-verify
+
+# Successful log message in the OpenFaaS function
+2019/01/25 23:48:55 Forking fprocess.
+2019/01/25 23:48:55 Query
+2019/01/25 23:48:55 Path /
+
+{"status": "200", "message": "successfully triggered event action with dedup_key: "}
+2019/01/25 23:48:56 Duration: 1.551482 seconds
+```
diff --git a/examples/python/trigger-pagerduty-incident/handler/handler.py b/examples/python/trigger-pagerduty-incident/handler/handler.py
new file mode 100644
index 00000000..19d1e754
--- /dev/null
+++ b/examples/python/trigger-pagerduty-incident/handler/handler.py
@@ -0,0 +1,216 @@
+import sys, json, os
+import urllib3
+import requests
+import traceback
+
+# GLOBAL_VARS
+DEBUG=False
+class bgc:
+ HEADER = '\033[95m'
+ OKBLUE = '\033[94m'
+ OKGREEN = '\033[92m'
+ WARNING = '\033[93m'
+ FAIL = '\033[91m'
+ ENDC = '\033[0m'
+ BOLD = '\033[1m'
+ UNDERLINE = '\033[4m'
+
+if(os.getenv("insecure_ssl")):
+ # Surpress SSL warnings
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+if(os.getenv("write_debug")):
+ sys.stderr.write(f"{bgc.WARNING}WARNING!! DEBUG has been enabled for this function. Sensitive information could be printed to sysout{bgc.ENDC} \n")
+ DEBUG=True
+
+def debug(s):
+ if DEBUG:
+ sys.stderr.write(s+" \n") #syserr only get logged on the console logs
+
+#
+### Paths and Endpoints
+### More examples and details here - https://v2.developer.pagerduty.com/docs/send-an-event-events-api-v2
+#
+PAGERDUTY_API_PATH='https://events.pagerduty.com/v2/enqueue'
+PD_CONFIG='/var/openfaas/secrets/pdconfig'
+class FaaSResponse:
+ """
+ FaaSResponse is a helper class to construct a properly formatted message returned by this function.
+ By default, OpenFaaS will marshal this response message as JSON.
+ """
+ def __init__(self, status, message):
+ """
+ Arguments:
+ status {str} -- the response status code
+ message {str} -- the response message
+ """
+ self.status=status
+ self.message=message
+
+class Pagerduty:
+ """
+ Pagerduty is a Class used to trigger Events in PagerDuty.
+ """
+
+ def __init__(self,conn):
+ """
+ Arguments:
+ conn {session} -- connection to PagerDuty REST API
+ """
+ self.session=conn
+
+ # PagerDuty REST API implementation
+ def invoke(self,obj):
+ """
+ Make a rest api call to Pagerduty Events API
+
+ Arguments:
+ obj {dict} -- Generated API Body for the PagerDuty Events API
+
+ Returns:
+ FaaSResponse -- status code and message
+ """
+ try:
+ resp = self.session.post(PAGERDUTY_API_PATH,json=obj)
+ resp.raise_for_status()
+ debug(f'{bgc.OKGREEN}HTTP POST Request successful{bgc.ENDC}')
+ debug(f'{bgc.OKBLUE}Response Body > {bgc.ENDC}{resp.text}')
+ resp_body = json.loads(resp.text)
+ return FaaSResponse('200', 'Successfully invoked PagerDuty API! dedup_key for this request: {0}'.format(resp_body['dedup_key']))
+ except requests.HTTPError as err:
+ return FaaSResponse('500', 'Could not invoke PagerDuty API > HTTPError: {0}'.format(err))
+
+def handle(req):
+
+ # Validate Event input
+ debug(f'{bgc.HEADER}---Validating CloudEvent--- {bgc.ENDC}')
+ debug(f'{bgc.OKBLUE}Event (raw) > {bgc.ENDC}{req}')
+ try:
+ cevent = json.loads(req)
+ debug(f'{bgc.OKGREEN}Successfully parsed Event into JSON!{bgc.ENDC}')
+ debug(f'{bgc.OKBLUE}Event (JSON) > {bgc.ENDC}{json.dumps(cevent, indent=4, sort_keys=True)}')
+ except json.JSONDecodeError as err:
+ res = FaaSResponse('400','Invalid JSON > JSONDecodeError: {0}'.format(err))
+ print(json.dumps(vars(res)))
+ return
+
+ # Validate Config file
+ debug(f'{bgc.HEADER}---Validating Config--- {bgc.ENDC}')
+ debug(f'{bgc.OKBLUE}Reading Config File > {bgc.ENDC}{PD_CONFIG}')
+ try:
+ with open(PD_CONFIG, 'r') as pdconfigfile:
+ pdconfig = json.load(pdconfigfile)
+ debug(f'{bgc.OKGREEN}Successfully parsed Configuration into JSON!{bgc.ENDC}')
+ debug(f'{bgc.OKBLUE}Configuration > {bgc.ENDC}{json.dumps(pdconfig,indent=4)}')
+ routingkey=pdconfig['routing_key']
+ event_action=pdconfig['event_action']
+ except json.JSONDecodeError as err:
+ res = FaaSResponse('400','Invalid JSON > JSONDecodeError: {0}'.format(err))
+ print(json.dumps(vars(res)))
+ return
+ except KeyError as err:
+ res = FaaSResponse('400','Required key not found in the provided configuration > KeyError: {0}'.format(err))
+ print(json.dumps(vars(res)))
+ return
+ except OSError as err:
+ res = FaaSResponse('500','Could not read PagerDuty configuration > OSError: {0}'.format(err))
+ print(json.dumps(vars(res)))
+ return
+
+ # Assert that the function is able to get the required information from the event and build the request body
+ # For debugging: validate the JSON blob we received - uncomment print statements if needed
+ # print(cevent)
+ debug(f'{bgc.HEADER}---Building HTTP Request body--- {bgc.ENDC}')
+ try:
+ # Map the CloudEvent data and build the PagerDuty Event API Request body
+ obj = {
+ 'routing_key': routingkey,
+ 'event_action': event_action,
+ 'client': 'VMware Event Broker Appliance',
+ 'client_url': cevent['source'],
+ 'payload': {
+ 'summary': cevent['data']['FullFormattedMessage'],
+ 'timestamp': cevent['data']['CreatedTime'],
+ 'source': cevent['source'],
+ 'severity': 'info',
+ 'component': cevent['data']['Vm']['Name'],
+ 'group': cevent['data']['Host']['Name'],
+ 'class': cevent['subject'],
+ 'custom_details': {
+ 'user': cevent['data']['UserName'],
+ 'Datacenter': cevent['data']['Datacenter'],
+ 'ComputeResource': cevent['data']['ComputeResource'],
+ 'Host': cevent['data']['Host'],
+ 'VM': cevent['data']['Vm']
+ }
+ }
+ #,
+ #'images': [{
+ # 'src': 'https://www.pagerduty.com/wp-content/uploads/2016/05/pagerduty-logo-green.png',
+ # 'href': 'https://example.com/',
+ # 'alt': cevent['data']['Vm']
+ # }],
+ # 'links': [{
+ # 'href': 'https://example.com/',
+ # 'text': 'Link to VM'
+ # }]
+ }
+ debug(f'{bgc.OKBLUE}Built API Request Body > {bgc.ENDC}{json.dumps(obj,indent=4)}')
+ except KeyError as err:
+ res = FaaSResponse('400','Invalid JSON, required key not found in the provided Event > KeyError: {0}'.format(err))
+ print(json.dumps(vars(res)))
+ traceback.print_exc(limit=1, file=sys.stderr) #providing traceback since it helps debug the exact key that failed
+ return
+ except TypeError as err:
+ res = FaaSResponse('400','Invalid JSON, missing required data in the provided Event > TypeError: {0}'.format(err))
+ print(json.dumps(vars(res)))
+ traceback.print_exc(limit=1, file=sys.stderr) #providing traceback since it helps debug the exact key that failed
+ return
+
+ # Make the Rest Api Call to PagerDuty
+ s=requests.Session()
+ if(os.getenv("insecure_ssl")):
+ s.verify=False
+ debug(f'{bgc.HEADER}---Attemping API Request to PagerDuty--- {bgc.ENDC}')
+ try:
+ pg = Pagerduty(s)
+ res = pg.invoke(obj)
+ print(json.dumps(vars(res)))
+ except Exception as err:
+ res = FaaSResponse('500','Unexpected Error occurred > Exception: {0}'.format(err))
+ print(json.dumps(vars(res)))
+
+ #Close session
+ s.close()
+
+ return
+
+#
+## Unit Test - helps with executing the function locally
+## Uncomment PDConfig - update the path to the file accordingly
+## Uncomment handle('... to test the function with the event samples provided below test without deploying to OpenFaaS
+#
+#PD_CONFIG='pdconfig.json'
+
+#
+## FAILURE CASES :Invalid Inputs
+#
+#handle('')
+#handle('"test":"ok"')
+#handle('{"test":"ok"}')
+#handle('{"data":"ok"}')
+
+#
+## FAILURE CASES :Unhandled Events
+#
+# Standard : UserLogoutSessionEvent
+#handle('{"id":"17e1027a-c865-4354-9c21-e8da3df4bff9","source":"https://vcsa.pdotk.local/sdk","specversion":"1.0","type":"com.vmware.event.router/event","subject":"UserLogoutSessionEvent","time":"2020-04-14T00:28:36.455112549Z","data":{"Key":7775,"ChainId":7775,"CreatedTime":"2020-04-14T00:28:35.221698Z","UserName":"machine-b8eb9a7f","Datacenter":null,"ComputeResource":null,"Host":null,"Vm":null,"Ds":null,"Net":null,"Dvs":null,"FullFormattedMessage":"User machine-b8ebe7eb9a7f@127.0.0.1 logged out (login time: Tuesday, 14 April, 2020 12:28:35 AM, number of API invocations: 34, user agent: pyvmomi Python/3.7.5 (Linux; 4.19.84-1.ph3; x86_64))","ChangeTag":"","IpAddress":"127.0.0.1","UserAgent":"pyvmomi Python/3.7.5 (Linux; 4.19.84-1.ph3; x86_64)","CallCount":34,"SessionId":"52edf160927","LoginTime":"2020-04-14T00:28:35.071817Z"},"datacontenttype":"application/json"}')
+# Eventex : vim.event.ResourceExhaustionStatusChangedEvent
+#handle('{"id":"0707d7e0-269f-42e7-ae1c-18458ecabf3d","source":"https://vcsa.pdotk.local/sdk","specversion":"1.0","type":"com.vmware.event.router/eventex","subject":"vim.event.ResourceExhaustionStatusChangedEvent","time":"2020-04-14T00:20:15.100325334Z","data":{"Key":7715,"ChainId":7715,"CreatedTime":"2020-04-14T00:20:13.76967Z","UserName":"machine-bb9a7f","Datacenter":null,"ComputeResource":null,"Host":null,"Vm":null,"Ds":null,"Net":null,"Dvs":null,"FullFormattedMessage":"vCenter Log File System Resource status changed from Yellow to Green on vcsa.pdotk.local ","ChangeTag":"","EventTypeId":"vim.event.ResourceExhaustionStatusChangedEvent","Severity":"info","Message":"","Arguments":[{"Key":"resourceName","Value":"storage_util_filesystem_log"},{"Key":"oldStatus","Value":"yellow"},{"Key":"newStatus","Value":"green"},{"Key":"reason","Value":" "},{"Key":"nodeType","Value":"vcenter"},{"Key":"_sourcehost_","Value":"vcsa.pdotk.local"}],"ObjectId":"","ObjectType":"","ObjectName":"","Fault":null},"datacontenttype":"application/json"}')
+
+#
+## SUCCESS CASES
+#
+# Standard : VmPoweredOnEvent
+#handle('{"id":"453120cd-3d19-4c43-aadc-df0cdbce3887","source":"https://vcsa.pdotk.local/sdk","specversion":"1.0","type":"com.vmware.event.router/event","subject":"VmPoweredOnEvent","time":"2020-04-13T23:46:10.402531287Z","data":{"Key":7441,"ChainId":7438,"CreatedTime":"2020-04-13T23:46:09.387283Z","UserName":"Administrator","Datacenter":{"Name":"PKLAB","Datacenter":{"Type":"Datacenter","Value":"datacenter-3"}},"ComputeResource":{"Name":"esxi01.pdotk.local","ComputeResource":{"Type":"ComputeResource","Value":"domain-s29"}},"Host":{"Name":"esxi01.pdotk.local","Host":{"Type":"HostSystem","Value":"host-31"}},"Vm":{"Name":"Test VM","Vm":{"Type":"VirtualMachine","Value":"vm-33"}},"Ds":null,"Net":null,"Dvs":null,"FullFormattedMessage":"Test VM on esxi01.pdotk.local in PKLAB has powered on","ChangeTag":"","Template":false},"datacontenttype":"application/json"}')
+# Standard : VmPoweredOffEvent
+#handle('{"id":"d77a3767-1727-49a3-ac33-ddbdef294150","source":"https://vcsa.pdotk.local/sdk","specversion":"1.0","type":"com.vmware.event.router/event","subject":"VmPoweredOffEvent","time":"2020-04-14T00:33:30.838669841Z","data":{"Key":7825,"ChainId":7821,"CreatedTime":"2020-04-14T00:33:30.252792Z","UserName":"Administrator","Datacenter":{"Name":"PKLAB","Datacenter":{"Type":"Datacenter","Value":"datacenter-3"}},"ComputeResource":{"Name":"esxi01.pdotk.local","ComputeResource":{"Type":"ComputeResource","Value":"domain-s29"}},"Host":{"Name":"esxi01.pdotk.local","Host":{"Type":"HostSystem","Value":"host-31"}},"Vm":{"Name":"Test VM","Vm":{"Type":"VirtualMachine","Value":"vm-33"}},"Ds":null,"Net":null,"Dvs":null,"FullFormattedMessage":"Test VM on esxi01.pdotk.local in PKLAB is powered off","ChangeTag":"","Template":false},"datacontenttype":"application/json"}')
diff --git a/examples/python/trigger-pagerduty-incident/handler/requirements.txt b/examples/python/trigger-pagerduty-incident/handler/requirements.txt
new file mode 100644
index 00000000..7c5a6ab8
--- /dev/null
+++ b/examples/python/trigger-pagerduty-incident/handler/requirements.txt
@@ -0,0 +1,2 @@
+urllib3==1.25.6
+requests==2.22.0
diff --git a/examples/python/trigger-pagerduty-incident/pdconfig.json b/examples/python/trigger-pagerduty-incident/pdconfig.json
new file mode 100644
index 00000000..7691b619
--- /dev/null
+++ b/examples/python/trigger-pagerduty-incident/pdconfig.json
@@ -0,0 +1,4 @@
+{
+ "routing_key": "",
+ "event_action": "trigger"
+}
\ No newline at end of file
diff --git a/examples/python/trigger-pagerduty-incident/stack.yml b/examples/python/trigger-pagerduty-incident/stack.yml
new file mode 100644
index 00000000..14b357eb
--- /dev/null
+++ b/examples/python/trigger-pagerduty-incident/stack.yml
@@ -0,0 +1,17 @@
+provider:
+ name: openfaas
+ gateway: https://VEBA_FQDN_OR_IP
+functions:
+ pdinvoke-fn:
+ lang: python3
+ handler: ./handler
+ image: vmware/veba-pagerduty-invoke:latest
+ environment:
+ write_debug: true
+ read_debug: true
+ combine_output: false
+ insecure_ssl: true
+ secrets:
+ - pdconfig
+ annotations:
+ topic: VmPoweredOnEvent,VmPoweredOffEvent
\ No newline at end of file
diff --git a/files/setup-01-os.sh b/files/setup-01-os.sh
index 33a1547b..817915dd 100755
--- a/files/setup-01-os.sh
+++ b/files/setup-01-os.sh
@@ -10,4 +10,18 @@ systemctl disable sshd
systemctl stop sshd
echo -e "\e[92mConfiguring OS Root password ..." > /dev/console
-echo "root:${ROOT_PASSWORD}" | /usr/sbin/chpasswd
\ No newline at end of file
+echo "root:${ROOT_PASSWORD}" | /usr/sbin/chpasswd
+
+if [ "${DOCKER_NETWORK_CIDR}" != "172.17.0.1/16" ]; then
+ echo -e "\e[92mConfiguring Docker Bridge Network ..." > /dev/console
+ cat > /etc/docker/daemon.json << EOF
+{
+ "bip": "${DOCKER_NETWORK_CIDR}"
+}
+EOF
+systemctl restart docker
+fi
+
+echo -e "\e[92mConfiguring IP Tables for Antrea ..." > /dev/console
+iptables -A INPUT -i gw0 -j ACCEPT
+iptables-save > /etc/systemd/scripts/ip4save
\ No newline at end of file
diff --git a/files/setup-04-kubernetes.sh b/files/setup-04-kubernetes.sh
index 19d23591..24053855 100755
--- a/files/setup-04-kubernetes.sh
+++ b/files/setup-04-kubernetes.sh
@@ -15,25 +15,31 @@ echo -e "\e[92mDisabling/Stopping IP Tables ..." > /dev/console
systemctl stop iptables
systemctl disable iptables
+# Customize the POD CIDR Network if provided or else default to 10.10.0.0/16
+if [ -z "${POD_NETWORK_CIDR}" ]; then
+ POD_NETWORK_CIDR="10.16.0.0/16"
+fi
+
+cat >> /root/config/kubeconfig.yml << EOF
+
+networking:
+ podSubnet: ${POD_NETWORK_CIDR}
+EOF
+
# Setup k8s
echo -e "\e[92mSetting up k8s ..." > /dev/console
+
+echo -e "\e[92mDeloying kubeadm ..." > /dev/console
HOME=/root
-kubeadm init --ignore-preflight-errors SystemVerification --skip-token-print --config /root/kubeconfig.yml
+kubeadm init --ignore-preflight-errors SystemVerification --skip-token-print --config /root/config/kubeconfig.yml
mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config
-echo -e "\e[92mDeloying kubeadm ..." > /dev/console
-
-# Customize the POD CIDR Network if provided or else default to 10.99.0.0/20
-if [ -z "${POD_NETWORK_CIDR}" ]; then
- POD_NETWORK_CIDR="10.99.0.0/20"
-fi
-
-sed -i "s#POD_NETWORK_CIDR#${POD_NETWORK_CIDR}#g" /root/weave.yaml
-
-kubectl --kubeconfig /root/.kube/config apply -f /root/weave.yaml
kubectl --kubeconfig /root/.kube/config taint nodes --all node-role.kubernetes.io/master-
+echo -e "\e[92mDeloying Antrea ..." > /dev/console
+kubectl --kubeconfig /root/.kube/config apply -f /root/download/antrea.yml
+
echo -e "\e[92mStarting k8s ..." > /dev/console
systemctl enable kubelet.service
diff --git a/files/setup-05-event-processor.sh b/files/setup-05-event-processor.sh
index a0f70628..4d8a92ed 100755
--- a/files/setup-05-event-processor.sh
+++ b/files/setup-05-event-processor.sh
@@ -14,20 +14,32 @@ kubectl --kubeconfig /root/.kube/config -n vmware create secret generic basic-au
--from-literal=basic-auth-password="${ROOT_PASSWORD}"
# Setup Event Processor Configuration File
-EVENT_ROUTER_CONFIG=/root/event-router-config.json
+EVENT_ROUTER_CONFIG=/root/config/event-router-config.json
+
+# Slicing of escaped variables needed to properly handle the double quotation issue with constructing vCenter Server URL
+ESCAPED_VCENTER_SERVER=$(echo -n ${VCENTER_SERVER} | python -c 'import sys,json;data=sys.stdin.read(); print json.dumps(data)[1:-1]')
+ESCAPED_VCENTER_USERNAME=$(echo -n ${VCENTER_USERNAME} | python -c 'import sys,json;data=sys.stdin.read(); print json.dumps(data)[1:-1]')
+ESCAPED_VCENTER_PASSWORD=$(echo -n ${VCENTER_PASSWORD} | python -c 'import sys,json;data=sys.stdin.read(); print json.dumps(data)[1:-1]')
+ESCAPED_ROOT_PASSWORD=$(echo -n ${ROOT_PASSWORD} | python -c 'import sys,json;data=sys.stdin.read(); print json.dumps(data)[1:-1]')
if [ "${EVENT_PROCESSOR_TYPE}" == "AWS EventBridge" ]; then
echo -e "\e[92mSetting up AWS Event Bridge Processor ..." > /dev/console
+
+ ESCAPED_AWS_EVENTBRIDGE_ACCESS_KEY=$(echo -n ${AWS_EVENTBRIDGE_ACCESS_KEY} | python -c 'import sys,json;data=sys.stdin.read(); print json.dumps(data)[1:-1]')
+ ESCAPED_AWS_EVENTBRIDGE_ACCESS_SECRET=$(echo -n ${AWS_EVENTBRIDGE_ACCESS_SECRET} | python -c 'import sys,json;data=sys.stdin.read(); print json.dumps(data)[1:-1]')
+ ESCAPED_AWS_EVENTBRIDGE_EVENT_BUS=$(echo -n ${VCENTER_USEAWS_EVENTBRIDGE_EVENT_BUSNAME} | python -c 'import sys,json;data=sys.stdin.read(); print json.dumps(data)[1:-1]')
+ ESCAPED_AWS_EVENTBRIDGE_RULE_ARN=$(echo -n ${AWS_EVENTBRIDGE_RULE_ARN} | python -c 'import sys,json;data=sys.stdin.read(); print json.dumps(data)[1:-1]')
+
cat > ${EVENT_ROUTER_CONFIG} << __AWS_EVENTBRIDGE_PROCESSOR__
[{
"type": "stream",
"provider": "vmware_vcenter",
- "address": "https://${VCENTER_SERVER}/sdk",
+ "address": "https://${ESCAPED_VCENTER_SERVER}/sdk",
"auth": {
"method": "user_password",
"secret": {
- "username": "${VCENTER_USERNAME}",
- "password": "${VCENTER_PASSWORD}"
+ "username": "${ESCAPED_VCENTER_USERNAME}",
+ "password": "${ESCAPED_VCENTER_PASSWORD}"
}
},
"options": {
@@ -40,14 +52,14 @@ if [ "${EVENT_PROCESSOR_TYPE}" == "AWS EventBridge" ]; then
"auth": {
"method": "access_key",
"secret": {
- "aws_access_key_id": "${AWS_EVENTBRIDGE_ACCESS_KEY}",
- "aws_secret_access_key": "${AWS_EVENTBRIDGE_ACCESS_SECRET}"
+ "aws_access_key_id": "${ESCAPED_AWS_EVENTBRIDGE_ACCESS_KEY}",
+ "aws_secret_access_key": "${ESCAPED_AWS_EVENTBRIDGE_ACCESS_SECRET}"
}
},
"options": {
"aws_region": "${AWS_EVENTBRIDGE_REGION}",
- "aws_eventbridge_event_bus": "${AWS_EVENTBRIDGE_EVENT_BUS}",
- "aws_eventbridge_rule_arn": "${AWS_EVENTBRIDGE_RULE_ARN}"
+ "aws_eventbridge_event_bus": "${ESCAPED_AWS_EVENTBRIDGE_EVENT_BUS}",
+ "aws_eventbridge_rule_arn": "${ESCAPED_AWS_EVENTBRIDGE_RULE_ARN}"
}
},
{
@@ -58,12 +70,13 @@ if [ "${EVENT_PROCESSOR_TYPE}" == "AWS EventBridge" ]; then
"method": "basic_auth",
"secret": {
"username": "admin",
- "password": "${ROOT_PASSWORD}"
+ "password": "${ESCAPED_ROOT_PASSWORD}"
}
}
}
]
__AWS_EVENTBRIDGE_PROCESSOR__
+echo "Processor: EventBridge" >> /etc/veba-release
else
# Setup OpenFaaS
echo -e "\e[92mSetting up OpenFaas Processor ..." > /dev/console
@@ -76,16 +89,18 @@ else
kubectl --kubeconfig /root/.kube/config create -f /root/download/faas-netes/yaml
+ ESCAPED_OPENFAAS_PASSWORD=$(echo -n ${OPENFAAS_PASSWORD} | python -c 'import sys,json;data=sys.stdin.read(); print json.dumps(data)[1:-1]')
+
cat > ${EVENT_ROUTER_CONFIG} << __OPENFAAS_PROCESSOR__
[{
"type": "stream",
"provider": "vmware_vcenter",
- "address": "https://${VCENTER_SERVER}/sdk",
+ "address": "https://${ESCAPED_VCENTER_SERVER}/sdk",
"auth": {
"method": "user_password",
"secret": {
- "username": "${VCENTER_USERNAME}",
- "password": "${VCENTER_PASSWORD}"
+ "username": "${ESCAPED_VCENTER_USERNAME}",
+ "password": "${ESCAPED_VCENTER_PASSWORD}"
}
},
"options": {
@@ -100,7 +115,7 @@ else
"method": "basic_auth",
"secret": {
"username": "admin",
- "password": "${OPENFAAS_PASSWORD}"
+ "password": "${ESCAPED_OPENFAAS_PASSWORD}"
}
},
"options": {
@@ -115,10 +130,11 @@ else
"method": "basic_auth",
"secret": {
"username": "admin",
- "password": "${ROOT_PASSWORD}"
+ "password": "${ESCAPED_ROOT_PASSWORD}"
}
}
}
]
__OPENFAAS_PROCESSOR__
+echo "Processor: OpenFaaS" >> /etc/veba-release
fi
\ No newline at end of file
diff --git a/files/setup-06-event-router.sh b/files/setup-06-event-router.sh
index 743c0ea9..830d7415 100755
--- a/files/setup-06-event-router.sh
+++ b/files/setup-06-event-router.sh
@@ -9,7 +9,10 @@ set -euo pipefail
echo -e "\e[92mDeploying VMware Event Router ..." > /dev/console
kubectl --kubeconfig /root/.kube/config -n vmware create secret generic event-router-config --from-file=${EVENT_ROUTER_CONFIG}
-cat > /root/event-router-k8s.yaml << __EVENT_ROUTER_CONFIG
+# Retrieve the version tag for VMware Event Router image
+EVENT_ROUTER_VERSION=$(awk '/Version:/ {print $2}' /etc/veba-release)
+
+cat > /root/config/event-router-k8s.yaml << __EVENT_ROUTER_CONFIG
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -27,7 +30,8 @@ spec:
app: vmware-event-router
spec:
containers:
- - image: vmware/veba-event-router:latest
+ - image: vmware/veba-event-router:${EVENT_ROUTER_VERSION}
+ imagePullPolicy: IfNotPresent
args: ["-config", "/etc/vmware-event-router/event-router-config.json", "-verbose"]
name: vmware-event-router
resources:
@@ -59,4 +63,4 @@ spec:
sessionAffinity: None
__EVENT_ROUTER_CONFIG
-kubectl --kubeconfig /root/.kube/config -n vmware create -f /root/event-router-k8s.yaml
\ No newline at end of file
+kubectl --kubeconfig /root/.kube/config -n vmware create -f /root/config/event-router-k8s.yaml
\ No newline at end of file
diff --git a/files/setup-07-tinywww.sh b/files/setup-07-tinywww.sh
index bff3c0b9..95cdb9db 100755
--- a/files/setup-07-tinywww.sh
+++ b/files/setup-07-tinywww.sh
@@ -7,7 +7,7 @@
set -euo pipefail
if [ ${VEBA_DEBUG} == "True" ]; then
- kubectl --kubeconfig /root/.kube/config apply -f /root/tinywww-debug.yml
+ kubectl --kubeconfig /root/.kube/config apply -f /root/config/tinywww-debug.yml
else
- kubectl --kubeconfig /root/.kube/config apply -f /root/tinywww.yml
+ kubectl --kubeconfig /root/.kube/config apply -f /root/config/tinywww.yml
fi
\ No newline at end of file
diff --git a/files/setup-08-ingress.sh b/files/setup-08-ingress.sh
index 55b25a91..9d015d5c 100755
--- a/files/setup-08-ingress.sh
+++ b/files/setup-08-ingress.sh
@@ -10,8 +10,8 @@ echo -e "\e[92mDeploying Contour ..." > /dev/console
kubectl --kubeconfig /root/.kube/config create -f /root/download/contour/examples/contour/
## Create SSL Certificate & Secret
-KEY_FILE=/root/eventrouter.key
-CERT_FILE=/root/eventrouter.crt
+KEY_FILE=/root/config/eventrouter.key
+CERT_FILE=/root/config/eventrouter.crt
CN_NAME=$(hostname -f)
CERT_NAME=eventrouter-tls
@@ -22,7 +22,7 @@ kubectl --kubeconfig /root/.kube/config -n vmware create secret tls ${CERT_NAME}
# Deploy Ingress Route
if [ "${EVENT_PROCESSOR_TYPE}" == "AWS EventBridge" ]; then
- cat << EOF > /root/ingressroute-gateway.yaml
+ cat << EOF > /root/config/ingressroute-gateway.yaml
apiVersion: contour.heptio.com/v1beta1
kind: IngressRoute
metadata:
@@ -54,7 +54,7 @@ spec:
port: 8080
EOF
else
- cat << EOF > /root/ingressroute-gateway.yaml
+ cat << EOF > /root/config/ingressroute-gateway.yaml
apiVersion: contour.heptio.com/v1beta1
kind: IngressRoute
metadata:
@@ -103,4 +103,4 @@ spec:
EOF
fi
-kubectl --kubeconfig /root/.kube/config create -f /root/ingressroute-gateway.yaml
\ No newline at end of file
+kubectl --kubeconfig /root/.kube/config create -f /root/config/ingressroute-gateway.yaml
\ No newline at end of file
diff --git a/files/setup-09-banner.sh b/files/setup-09-banner.sh
index 2c5e6bc6..eeff5c0f 100755
--- a/files/setup-09-banner.sh
+++ b/files/setup-09-banner.sh
@@ -12,20 +12,6 @@ if [ "${EVENT_PROCESSOR_TYPE}" == "OpenFaaS" ]; then
cat << EOF > /etc/issue
Welcome to the vCenter Event Broker Appliance
-Appliance Status: https://${HOSTNAME}/status
-Install Logs: https://${HOSTNAME}/bootstrap
-Appliance Statistics: https://${HOSTNAME}/stats
-OpenFaaS UI: https://${HOSTNAME}
-
-EOF
-else
- cat << EOF > /etc/issue
-Welcome to the vCenter Event Broker Appliance
-
-Appliance Status: https://${HOSTNAME}/status
-Install Logs: https://${HOSTNAME}/bootstrap
-Appliance Statistics: https://${HOSTNAME}/stats
-
EOF
fi
diff --git a/files/setup.sh b/files/setup.sh
index d40fc014..55c806ac 100755
--- a/files/setup.sh
+++ b/files/setup.sh
@@ -32,6 +32,7 @@ AWS_EVENTBRIDGE_EVENT_BUS=$(vmtoolsd --cmd "info-get guestinfo.ovfEnv" | grep "g
AWS_EVENTBRIDGE_REGION=$(vmtoolsd --cmd "info-get guestinfo.ovfEnv" | grep "guestinfo.aws_eb_region" | awk -F 'oe:value="' '{print $2}' | awk -F '"' '{print $1}')
AWS_EVENTBRIDGE_RULE_ARN=$(vmtoolsd --cmd "info-get guestinfo.ovfEnv" | grep "guestinfo.aws_eb_arn" | awk -F 'oe:value="' '{print $2}' | awk -F '"' '{print $1}')
AWS_EVENTBRIDGE_ADV_OPTION=$(vmtoolsd --cmd "info-get guestinfo.ovfEnv" | grep "guestinfo.aws_eb_advanced_options" | awk -F 'oe:value="' '{print $2}' | awk -F '"' '{print $1}')
+DOCKER_NETWORK_CIDR=$(vmtoolsd --cmd "info-get guestinfo.ovfEnv" | grep "guestinfo.docker_network_cidr" | awk -F 'oe:value="' '{print $2}' | awk -F '"' '{print $1}')
POD_NETWORK_CIDR=$(vmtoolsd --cmd "info-get guestinfo.ovfEnv" | grep "guestinfo.pod_network_cidr" | awk -F 'oe:value="' '{print $2}' | awk -F '"' '{print $1}')
if [ -e /root/ran_customization ]; then
diff --git a/files/veba-dcui b/files/veba-dcui
new file mode 100755
index 00000000..fd28eea3
Binary files /dev/null and b/files/veba-dcui differ
diff --git a/http/packages_minimal.json b/http/packages_minimal.json
index 655cc51a..49ebb596 100644
--- a/http/packages_minimal.json
+++ b/http/packages_minimal.json
@@ -1,7 +1,7 @@
{
"packages": [
"minimal",
- "linux",
+ "linux-esx",
"initramfs"
]
}
diff --git a/licenses/github.com/gizak/termui/LICENSE b/licenses/github.com/gizak/termui/LICENSE
new file mode 100644
index 00000000..b8beeb7f
--- /dev/null
+++ b/licenses/github.com/gizak/termui/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Zack Guo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/licenses/github.com/shirou/gopsutil/LICENSE b/licenses/github.com/shirou/gopsutil/LICENSE
new file mode 100644
index 00000000..f072612a
--- /dev/null
+++ b/licenses/github.com/shirou/gopsutil/LICENSE
@@ -0,0 +1,61 @@
+gopsutil is distributed under BSD license reproduced below.
+
+Copyright (c) 2014, WAKAYAMA Shirou
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of the gopsutil authors nor the names of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+-------
+internal/common/binary.go in the gopsutil is copied and modifid from golang/encoding/binary.go.
+
+
+
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/logo/README.md b/logo/README.md
new file mode 100644
index 00000000..d83d30ae
--- /dev/null
+++ b/logo/README.md
@@ -0,0 +1,5 @@
+## Hi! I'm Otto the Orca
+
+
+
+...and I'm the official mascot of the **VMware Event Broker Appliance**.
diff --git a/logo/veba_otto_the_orca_1024x1024.png b/logo/veba_otto_the_orca_1024x1024.png
new file mode 100644
index 00000000..41b6a320
Binary files /dev/null and b/logo/veba_otto_the_orca_1024x1024.png differ
diff --git a/logo/veba_otto_the_orca_320x320.png b/logo/veba_otto_the_orca_320x320.png
new file mode 100644
index 00000000..354cdd4d
Binary files /dev/null and b/logo/veba_otto_the_orca_320x320.png differ
diff --git a/logo/veba_otto_the_orca_3300x3300.png b/logo/veba_otto_the_orca_3300x3300.png
new file mode 100644
index 00000000..df12157f
Binary files /dev/null and b/logo/veba_otto_the_orca_3300x3300.png differ
diff --git a/logo/veba_otto_the_orca_640x640.png b/logo/veba_otto_the_orca_640x640.png
new file mode 100644
index 00000000..96c619e2
Binary files /dev/null and b/logo/veba_otto_the_orca_640x640.png differ
diff --git a/manual/add_ovf_properties.sh b/manual/add_ovf_properties.sh
index 8cca0a46..99626ba8 100755
--- a/manual/add_ovf_properties.sh
+++ b/manual/add_ovf_properties.sh
@@ -13,11 +13,13 @@ if [ "$(uname)" == "Darwin" ]; then
sed -i .bak2 "/ <\/vmw:BootOrderSection>/ r photon.xml" ${OUTPUT_PATH}/${VEBA_APPLIANCE_NAME}/${VEBA_APPLIANCE_NAME}.ovf
sed -i .bak3 '/^ //g' ${OUTPUT_PATH}/${VEBA_APPLIANCE_NAME}/${VEBA_APPLIANCE_NAME}.ovf
sed -i "/ <\/vmw:BootOrderSection>/ r photon.xml" ${OUTPUT_PATH}/${VEBA_APPLIANCE_NAME}/${VEBA_APPLIANCE_NAME}.ovf
sed -i '/^
- Information about the installed software
- vCenter Event Broker Appliance
- VMware
- {{VERSION}}
- https://github.com/vmware-samples/vcenter-event-broker-appliance
- https://www.vmware.com/
- Networking
-
- Hostname
- Hostname of system
-
-
- IP Address
- IP Address of the system
-
-
- Network CIDR Prefix
- Network CIDR Prefix
-
-
- Gateway
- Gateway of the system
-
-
- DNS
- DNS Servers (space separated)
-
-
- DNS Domain
- DNS Domain
-
-
- NTP
- NTP Servers (space separated)
-
- Proxy Settings (optional)
-
- HTTP Proxy
- Enter HTTP Proxy Server followed by the port and without typing "http://" before. Example: "proxy.provider.com:3128"
-
-
- HTTPS Proxy
- Enter HTTPS Proxy Server followed by the port and without typing "https://" before. Example: "proxy.provider.com:3128"
-
-
- Proxy Username (optional)
- Username for the Proxy Server
-
-
- Proxy Password (optional)
- Password for the Proxy User
-
-
- No Proxy
- No Proxy for e.g. your internal domain suffix. Comma separated (localhost, 127.0.0.1, domain.local)
-
- OS Credentials
-
- Root Password
- Password to login in as root. Please use a secure password
-
- vSphere
-
- vCenter Server
- IP Address or Hostname of vCenter Server
-
-
- vCenter Username
- Username to login to vCenter Server
-
-
- vCenter Password
- Password to login to vCenter Server
-
-
- Disable vCenter Server TLS Verification
- Disable TLS Verification for vCenter Server (required for self-sign certificate)
-
- Event Processor Configuration
-
- Event Processor
- Choose either OpenFaaS (default) or AWS EventBridge and only fill in the configuration for the select event processor below
-
- OpenFaaS Configuration
-
- Password
- Password to login into OpenFaaS. Please use a secure password
-
-
- Advanced Settings
- Opaque string for applying advanced configurations for OpenFaaS Processor. For advanced use cases only, please see documentation for more details
-
- AWS EventBridge Configuration
-
- Access Key
- A valid AWS Access Key to AWS EventBridge
-
-
- Access Secret
- A valid AWS Access Secret to AWS EventBridge
-
-
- Event Bus Name
- Name of the AWS Event Bus to use
-
-
- Region
- Region where Event Bus is running (e.g. us-west-2)
-
-
- Rule ARN
- ID of the Rule ARN created in AWS EventBridge
-
-
- Advanced Settings
- Opaque string for applying advanced configurations for AWS EventBridge Processor. For advanced use cases only, please see documentation for more details
-
- zAdvanced
-
- Debugging
- Enable Debugging
-
-
- POD CIDR Network
- Customize POD CIDR Network (Default 10.99.0.0/20)
-
-
diff --git a/manual/photon.xml.template b/manual/photon.xml.template
index d7c87132..9bd0cdab 100644
--- a/manual/photon.xml.template
+++ b/manual/photon.xml.template
@@ -121,8 +121,12 @@
Debugging
Enable Debugging
-
+
+ Docker Bridge CIDR Network
+ Customize Docker Bridge CIDR Network (Default 172.17.0.1/16)
+
+
POD CIDR Network
- Customize POD CIDR Network (Default 10.99.0.0/20)
+ Customize POD CIDR Network (Default 10.10.0.0/16)
diff --git a/photon-dev.json b/photon-dev.json
deleted file mode 100644
index 0da785ef..00000000
--- a/photon-dev.json
+++ /dev/null
@@ -1,187 +0,0 @@
-{
- "variables": {
- "veba_ovf_template": "photon-dev.xml.template",
- "ovftool_deploy_vcenter": "192.168.30.200",
- "ovftool_deploy_vcenter_username": "administrator@vsphere.local",
- "ovftool_deploy_vcenter_password": "VMware1!",
- "ovftool_deploy_datacenter": "Primp-Datacenter",
- "ovftool_deploy_cluster": "Supermicro-Cluster",
- "ovftool_deploy_vm_name": "PACKER-TEST-vCenter_Event_Broker_Appliance",
- "ovftool_deploy_vm_hostname": "veba.primp-industries.com",
- "ovftool_deploy_vm_ip_address": "192.168.30.170",
- "ovftool_deploy_vm_prefix": "24 (255.255.255.0)",
- "ovftool_deploy_vm_gateway": "192.168.30.1",
- "ovftool_deploy_vm_dns": "192.168.30.1",
- "ovftool_deploy_vm_dns_domain": "primp-industries.com",
- "ovftool_deploy_vm_ntp": "pool.ntp.org",
- "ovftool_deploy_vm_http_proxy": "",
- "ovftool_deploy_vm_https_proxy": "",
- "ovftool_deploy_vm_proxy_username": "",
- "ovftool_deploy_vm_proxy_password": "",
- "ovftool_deploy_vm_no_proxy": "",
- "ovftool_deploy_vm_root_password": "VMware1!",
- "ovftool_deploy_vm_openfaas_password": "VMware1!",
- "ovftool_deploy_vm_vcenter_server": "192.168.30.200",
- "ovftool_deploy_vm_vcenter_username": "administrator@vsphere.local",
- "ovftool_deploy_vm_vcenter_password": "VMware1!",
- "ovftool_deploy_vm_network": "VM Network",
- "ovftool_deploy_vm_datastore": "sm-vsanDatastore",
- "ovftool_deploy_vm_pod_network_cidr": "10.100.0.0/20",
- "ovftool_deploy_vm_event_processor_type": "OpenFaaS"
- },
- "builders": [
- {
- "type": "vmware-iso",
- "vm_name": "{{ user `vm_name` }}",
- "guest_os_type": "Other",
- "version": "13",
- "disk_size": "12288",
- "boot_command": [
- "",
- "vmlinuz initrd=initrd.img root=/dev/ram0 loglevel=3 ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/photon-kickstart.json photon.media=cdrom",
- ""
- ],
- "boot_wait": "10s",
- "headless": false,
- "vnc_disable_password": true,
- "iso_url": "{{ user `iso_url` }}",
- "iso_checksum": "{{ user `iso_checksum` }}",
- "iso_checksum_type": "{{ user `iso_checksum_type` }}",
- "http_directory": "http",
- "remote_type": "esx5",
- "remote_host": "{{ user `builder_host` }}",
- "remote_datastore": "{{ user `builder_host_datastore` }}",
- "remote_username": "{{ user `builder_host_username` }}",
- "remote_password": "{{ user `builder_host_password` }}",
- "ssh_username": "{{ user `guest_username` }}",
- "ssh_password": "{{ user `guest_password` }}",
- "ssh_port": 22,
- "format": "ovf",
- "shutdown_command": "/sbin/shutdown -h now",
- "shutdown_timeout": "1000s",
- "vmx_data": {
- "numvcpus": "{{ user `numvcpus` }}",
- "memsize": "{{ user `ramsize` }}",
-
- "ethernet0.networkName": "{{ user `builder_host_portgroup` }}",
- "ethernet0.present": "TRUE",
- "ethernet0.startConnected": "TRUE",
- "ethernet0.virtualDev": "vmxnet3",
- "ethernet0.addressType": "generated",
- "ethernet0.wakeOnPcktRcv": "FALSE",
- "annotation": "Version: {{ user `version` }}"
- }
- }
- ],
- "provisioners": [
- {
- "type": "shell",
- "expect_disconnect" : true,
- "scripts": [
- "scripts/photon-settings.sh",
- "scripts/photon-docker.sh"
- ]
- },
- {
- "type": "shell",
- "pause_before": "20s",
- "scripts": [
- "scripts/photon-containers.sh",
- "scripts/photon-cleanup.sh"
- ]
- },
- {
- "type": "file",
- "source": "files/rc.local",
- "destination": "/etc/rc.d/rc.local"
- },
- {
- "type": "file",
- "source": "files/setup.sh",
- "destination": "/root/setup/setup.sh"
- },
- {
- "type": "file",
- "source": "files/setup-01-os.sh",
- "destination": "/root/setup/setup-01-os.sh"
- },
- {
- "type": "file",
- "source": "files/setup-02-proxy.sh",
- "destination": "/root/setup/setup-02-proxy.sh"
- },
- {
- "type": "file",
- "source": "files/setup-03-network.sh",
- "destination": "/root/setup/setup-03-network.sh"
- },
- {
- "type": "file",
- "source": "files/setup-04-kubernetes.sh",
- "destination": "/root/setup/setup-04-kubernetes.sh"
- },
- {
- "type": "file",
- "source": "files/setup-05-event-processor.sh",
- "destination": "/root/setup/setup-05-event-processor.sh"
- },
- {
- "type": "file",
- "source": "files/setup-06-event-router.sh",
- "destination": "/root/setup/setup-06-event-router.sh"
- },
- {
- "type": "file",
- "source": "files/setup-07-tinywww.sh",
- "destination": "/root/setup/setup-07-tinywww.sh"
- },
- {
- "type": "file",
- "source": "files/setup-08-ingress.sh",
- "destination": "/root/setup/setup-08-ingress.sh"
- },
- {
- "type": "file",
- "source": "files/setup-09-banner.sh",
- "destination": "/root/setup/setup-09-banner.sh"
- },
- {
- "type": "file",
- "source": "files/tinywww.yml",
- "destination": "/root/tinywww.yml"
- },
- {
- "type": "file",
- "source": "files/tinywww-debug.yml",
- "destination": "/root/tinywww-debug.yml"
- },
- {
- "type": "file",
- "source": "files/kubeconfig.yml",
- "destination": "/root/kubeconfig.yml"
- }
- ],
- "post-processors": [
- {
- "type": "shell-local",
- "environment_vars": ["VEBA_VERSION={{ user `version` }}", "VEBA_APPLIANCE_NAME={{ user `vm_name` }}", "FINAL_VEBA_APPLIANCE_NAME={{ user `vm_name` }}_{{user `version`}}", "VEBA_OVF_TEMPLATE={{ user `veba_ovf_template` }}"],
- "inline": [
- "cd manual",
- "./add_ovf_properties.sh"
- ]
- },
- {
- "type": "shell-local",
- "inline": [
- "ovftool --powerOn --name={{ user `ovftool_deploy_vm_name` }} --net:'VM Network={{ user `ovftool_deploy_vm_network` }}' --datastore={{ user `ovftool_deploy_vm_datastore` }} --prop:guestinfo.hostname={{ user `ovftool_deploy_vm_hostname` }} --prop:guestinfo.ipaddress={{ user `ovftool_deploy_vm_ip_address` }} --prop:guestinfo.netmask={{ user `ovftool_deploy_vm_prefix` }} --prop:guestinfo.gateway={{ user `ovftool_deploy_vm_gateway` }} --prop:guestinfo.dns={{ user `ovftool_deploy_vm_dns` }} --prop:guestinfo.domain={{ user `ovftool_deploy_vm_dns_domain` }} --prop:guestinfo.ntp={{ user `ovftool_deploy_vm_ntp` }} --prop:guestinfo.http_proxy={{ user `ovftool_deploy_vm_http_proxy` }} --prop:guestinfo.https_proxy={{ user `ovftool_deploy_vm_https_proxy` }} --prop:guestinfo.proxy_username={{ user `ovftool_deploy_vm_proxy_username` }} --prop:guestinfo.proxy_password={{ user `ovftool_deploy_vm_proxy_password` }} --prop:guestinfo.no_proxy={{ user `ovftool_deploy_vm_no_proxy` }} --prop:guestinfo.root_password={{ user `vm_ovftool_deploy_root_password` }} --prop:guestinfo.event_processor_type={{ user `ovftool_deploy_vm_event_processor_type` }} --prop:guestinfo.openfaas_password={{ user `ovftool_deploy_vm_openfaas_password` }} --prop:guestinfo.vcenter_server={{ user `ovftool_deploy_vm_vcenter_server` }} --prop:guestinfo.vcenter_username={{ user `ovftool_deploy_vm_vcenter_username` }} --prop:guestinfo.vcenter_password={{ user `ovftool_deploy_vm_vcenter_password` }} --prop:guestinfo.vcenter_disable_tls_verification=True --prop:guestinfo.pod_network_cidr={{ user `ovftool_deploy_vm_pod_network_cidr` }} --prop:guestinfo.debug=True output-vmware-iso/{{ user `vm_name` }}_{{user `version`}}.ova 'vi://{{ user `ovftool_deploy_vcenter_username` }}:{{ user `ovftool_deploy_vcenter_password` }}@{{ user `ovftool_deploy_vcenter` }}/{{ user `ovftool_deploy_datacenter` }}/host/{{ user `ovftool_deploy_cluster` }}/'"
- ]
- },
- {
- "type": "shell-local",
- "inline": [
- "pwsh -F unregister_vm.ps1 {{ user `ovftool_deploy_vcenter` }} {{ user `ovftool_deploy_vcenter_username` }} {{ user `ovftool_deploy_vcenter_password` }} {{ user `vm_name` }}"
- ]
- }
- ]
-}
-
diff --git a/photon-version.json b/photon-version.json
index a29c6476..27068b60 100644
--- a/photon-version.json
+++ b/photon-version.json
@@ -1,10 +1,10 @@
{
- "version": "0.3.0",
+ "version": "0.4.0",
"description": "Photon Build for vCenter Event Broker Appliance",
"vm_name": "vCenter_Event_Broker_Appliance",
- "iso_checksum": "93d0cde8da51f9208713d895b5b85b86980d2a72e710f55f0e65bc82c299dd9a7ebedc8f30d5f4d18c1a389c76a961e8a14b02416692204d31d77e1e4792f37d",
+ "iso_checksum": "f6619bcff94cef63d0d6d7ead7dd3878816ebfa6a1ef5717175bb0d08d4ccc719e4ec7daa7db3c5dc07ea3547fc24412b4dc6827a4ac332ada9d5bfc842c4229",
"iso_checksum_type": "sha512",
- "iso_url": "http://dl.bintray.com/vmware/photon/3.0/GA/iso/photon-3.0-26156e2.iso",
+ "iso_url": "http://dl.bintray.com/vmware/photon/3.0/Rev2/iso/Update1/photon-3.0-a0f216d.iso",
"numvcpus": "2",
"ramsize": "8192",
"guest_username": "root",
diff --git a/photon.json b/photon.json
index fe86bf7a..f3e8e1f0 100644
--- a/photon.json
+++ b/photon.json
@@ -51,6 +51,10 @@
"provisioners": [
{
"type": "shell",
+ "environment_vars": [
+ "VEBA_VERSION={{ user `VEBA_VERSION` }}",
+ "VEBA_COMMIT={{ user `VEBA_COMMIT` }}"
+ ],
"expect_disconnect" : true,
"scripts": [
"scripts/photon-settings.sh",
@@ -59,6 +63,9 @@
},
{
"type": "shell",
+ "environment_vars": [
+ "VEBA_VERSION={{ user `VEBA_VERSION` }}"
+ ],
"pause_before": "20s",
"scripts": [
"scripts/photon-containers.sh",
@@ -123,23 +130,28 @@
{
"type": "file",
"source": "files/tinywww.yml",
- "destination": "/root/tinywww.yml"
+ "destination": "/root/config/tinywww.yml"
},
{
"type": "file",
"source": "files/tinywww-debug.yml",
- "destination": "/root/tinywww-debug.yml"
+ "destination": "/root/config/tinywww-debug.yml"
},
{
"type": "file",
"source": "files/kubeconfig.yml",
- "destination": "/root/kubeconfig.yml"
+ "destination": "/root/config/kubeconfig.yml"
+ },
+ {
+ "type": "file",
+ "source": "files/veba-dcui",
+ "destination": "/usr/bin/veba-dcui"
}
],
"post-processors": [
{
"type": "shell-local",
- "environment_vars": ["VEBA_VERSION={{ user `version` }}", "VEBA_APPLIANCE_NAME={{ user `vm_name` }}", "FINAL_VEBA_APPLIANCE_NAME={{ user `vm_name` }}_{{user `version`}}", "VEBA_OVF_TEMPLATE={{ user `veba_ovf_template` }}"],
+ "environment_vars": ["VEBA_VERSION={{ user `VEBA_VERSION` }}", "VEBA_APPLIANCE_NAME={{ user `vm_name` }}", "FINAL_VEBA_APPLIANCE_NAME={{ user `vm_name` }}_{{user `VEBA_VERSION`}}", "VEBA_OVF_TEMPLATE={{ user `veba_ovf_template` }}"],
"inline": [
"cd manual",
"./add_ovf_properties.sh"
diff --git a/scripts/photon-containers.sh b/scripts/photon-containers.sh
index f252e6d4..9ee21ee7 100644
--- a/scripts/photon-containers.sh
+++ b/scripts/photon-containers.sh
@@ -2,11 +2,6 @@
# Copyright 2019 VMware, Inc. All rights reserved.
# SPDX-License-Identifier: BSD-2
-echo '> Downloading weave.yaml'
-curl -L https://cloud.weave.works/k8s/net?k8s-version=Q2xpZW50IFZlcnNpb246IHZlcnNpb24uSW5mb3tNYWpvcjoiMSIsIE1pbm9yOiIxNCIsIEdpdFZlcnNpb246InYxLjE0LjYiLCBHaXRDb21taXQ6Ijk2ZmFjNWNkMTNhNWRjMDY0ZjdkOWY0ZjIzMDMwYTZhZWZhY2U2Y2MiLCBHaXRUcmVlU3RhdGU6ImFyY2hpdmUiLCBCdWlsZERhdGU6IjIwMTktMTAtMzFUMDY6MDQ6MDNaIiwgR29WZXJzaW9uOiJnbzEuMTMuMyIsIENvbXBpbGVyOiJnYyIsIFBsYXRmb3JtOiJsaW51eC9hbWQ2NCJ9ClNlcnZlciBWZXJzaW9uOiB2ZXJzaW9uLkluZm97TWFqb3I6IjEiLCBNaW5vcjoiMTQiLCBHaXRWZXJzaW9uOiJ2MS4xNC45IiwgR2l0Q29tbWl0OiI1MDBmNWFiYTgwZDcxMjUzY2MwMWFjNmE4NjIyYjgzNzdmNGE3ZWY5IiwgR2l0VHJlZVN0YXRlOiJjbGVhbiIsIEJ1aWxkRGF0ZToiMjAxOS0xMS0xM1QxMToxMzowNFoiLCBHb1ZlcnNpb246ImdvMS4xMi4xMiIsIENvbXBpbGVyOiJnYyIsIFBsYXRmb3JtOiJsaW51eC9hbWQ2NCJ9Cg -o /root/weave.yaml
-sed -i '/^ hostNetwork:.*/i \ imagePullPolicy: IfNotPresent' /root/weave.yaml
-sed -i '0,/^ env:/s// env:\n - name: IPALLOC_RANGE\n value: POD_NETWORK_CIDR/' /root/weave.yaml
-
echo '> Pre-Downloading Kubeadm Docker Containers'
CONTAINERS=(
@@ -17,8 +12,7 @@ k8s.gcr.io/kube-proxy:v1.14.9
k8s.gcr.io/pause:3.1
k8s.gcr.io/etcd:3.3.10
k8s.gcr.io/coredns:1.3.1
-docker.io/weaveworks/weave-kube:2.6.0
-docker.io/weaveworks/weave-npc:2.6.0
+antrea/antrea-ubuntu:v0.6.0
embano1/tinywww:latest
projectcontour/contour:v1.0.0-beta.1
openfaas/faas-netes:0.9.0
@@ -30,7 +24,7 @@ envoyproxy/envoy:v1.11.1
prom/prometheus:v2.11.0
prom/alertmanager:v0.18.0
nats-streaming:0.11.2
-vmware/veba-event-router:latest
+vmware/veba-event-router:${VEBA_VERSION}
)
for i in ${CONTAINERS[@]};
@@ -54,3 +48,8 @@ git checkout v1.0.0-beta.1
sed -i '/^---/i \ dnsPolicy: ClusterFirstWithHostNet\n hostNetwork: true' examples/contour/03-envoy.yaml
sed -i 's/imagePullPolicy: Always/imagePullPolicy: IfNotPresent/g' examples/contour/*.yaml
cd ..
+
+echo '> Downloading Antrea...'
+wget https://github.com/vmware-tanzu/antrea/releases/download/v0.6.0/antrea.yml -O /root/download/antrea.yml
+sed -i 's/image: antrea\/antrea-ubuntu:.*/image: antrea\/antrea-ubuntu:v0.6.0/g' /root/download/antrea.yml
+sed -i '/image:.*/i \ imagePullPolicy: IfNotPresent' /root/download/antrea.yml
\ No newline at end of file
diff --git a/scripts/photon-settings.sh b/scripts/photon-settings.sh
index 2663a604..5cc441d3 100644
--- a/scripts/photon-settings.sh
+++ b/scripts/photon-settings.sh
@@ -13,6 +13,7 @@ echo "net.ipv6.conf.all.disable_ipv6 = 1" >> /etc/sysctl.conf
echo '> Applying latest Updates...'
tdnf -y update
+#tdnf upgrade linux-esx
echo '> Installing Additional Packages...'
tdnf install -y \
@@ -26,8 +27,9 @@ tdnf install -y \
tar \
kubernetes-kubeadm
-echo '> Creating directory for setup scripts'
+echo '> Creating directory for setup scripts and configuration files'
mkdir -p /root/setup
+mkdir -p /root/config
echo '> Creating tools.conf to prioritize eth0 interface...'
cat > /etc/vmware-tools/tools.conf << EOF
@@ -39,4 +41,34 @@ low-priority-nics=weave,docker0
exclude-nics=veth*,vxlan*,datapath
EOF
+cat > /etc/veba-release << EOF
+Version: ${VEBA_VERSION}
+Commit: ${VEBA_COMMIT}
+EOF
+
+echo '> Creating VEBA DCUI systemd unit file...'
+mkdir -p /usr/lib/systemd/system/getty@tty1.service.d/
+cat > /usr/lib/systemd/system/getty@tty1.service.d/dcui_override.conf << EOF
+[Unit]
+Description=
+Description=VEBA DCUI
+After=
+After=network-online.target
+
+[Service]
+ExecStart=
+ExecStart=-/usr/bin/veba-dcui
+Restart=always
+RestartSec=1sec
+StandardOutput=tty
+StandardInput=tty
+StandardError=journal
+TTYPath=/dev/tty1
+TTYReset=yes
+TTYVHangup=yes
+KillMode=process
+EOF
+
+systemctl enable getty@tty1.service
+
echo '> Done'
diff --git a/test/deploy_veba_eventbridge_processor.sh b/test/deploy_veba_eventbridge_processor.sh
index 35de79b3..85b4672f 100755
--- a/test/deploy_veba_eventbridge_processor.sh
+++ b/test/deploy_veba_eventbridge_processor.sh
@@ -5,7 +5,7 @@
# Sample Shell Script to test deployment of VEBA w/AWS EventBridge Processor
OVFTOOL_BIN_PATH="/Applications/VMware OVF Tool/ovftool"
-VEBA_OVA="../output-vmware-iso/vCenter_Event_Broker_Appliance_0.3.0.ova"
+VEBA_OVA="../output-vmware-iso/vCenter_Event_Broker_Appliance_0.4.0-beta.ova"
# vCenter
DEPLOYMENT_TARGET_ADDRESS="192.168.30.200"
diff --git a/test/deploy_veba_openfaas_processor.sh b/test/deploy_veba_openfaas_processor.sh
index a9d5bc64..59743028 100755
--- a/test/deploy_veba_openfaas_processor.sh
+++ b/test/deploy_veba_openfaas_processor.sh
@@ -5,7 +5,7 @@
# Sample Shell Script to test deployment of VEBA w/OpenFaaS Processor
OVFTOOL_BIN_PATH="/Applications/VMware OVF Tool/ovftool"
-VEBA_OVA="../output-vmware-iso/vCenter_Event_Broker_Appliance_0.3.0.ova"
+VEBA_OVA="../output-vmware-iso/vCenter_Event_Broker_Appliance_0.4.0.ova"
# vCenter
DEPLOYMENT_TARGET_ADDRESS="192.168.30.200"
@@ -31,6 +31,7 @@ VEBA_VCENTER_USER="administrator@vsphere.local"
VEBA_VCENTER_PASS="VMware1!"
VEBA_VCENTER_DISABLE_TLS="True"
VEBA_OPENFAAS_PASS="VMware1!"
+VEBA_DOCKER_NETWORK="172.26.0.1/16"
### DO NOT EDIT BEYOND HERE ###
@@ -58,5 +59,6 @@ VEBA_OPENFAAS_PASS="VMware1!"
--prop:guestinfo.event_processor_type="OpenFaaS" \
--prop:guestinfo.openfaas_password=${VEBA_OPENFAAS_PASS} \
--prop:guestinfo.debug=${VEBA_DEBUG} \
+ --prop:guestinfo.docker_network_cidr=${VEBA_DOCKER_NETWORK} \
"${VEBA_OVA}" \
"vi://${DEPLOYMENT_TARGET_USERNAME}:${DEPLOYMENT_TARGET_PASSWORD}@${DEPLOYMENT_TARGET_ADDRESS}/${DEPLOYMENT_TARGET_DATACENTER}/host/${DEPLOYMNET_TARGET_CLUSTER}"
diff --git a/vmware-event-router/Makefile b/vmware-event-router/Makefile
index 7526bd8f..7a909460 100644
--- a/vmware-event-router/Makefile
+++ b/vmware-event-router/Makefile
@@ -5,8 +5,10 @@ IMAGE_NAME=$(IMAGE_REPO)/veba-event-router
DIST_FOLDER=dist
BINARY=vmware-event-router
BUILD_TAG=$(IMAGE_NAME):$(COMMIT)
+VERSION_TAG=$(IMAGE_NAME):$(VERSION)
LATEST_TAG=$(IMAGE_NAME):latest
+GIT_BRANCH := $(shell git branch --show-current)
GIT_NOT_CLEAN_CHECK := $(shell git status --porcelain)
export GO111MODULE=on
@@ -38,7 +40,7 @@ binary: test tidy
build: test tidy
$(info Make: Building image "$(IMAGE_NAME)".)
- $(if $(GIT_NOT_CLEAN_CHECK), $(error "Dirty Git repository."))
+ $(if $(GIT_NOT_CLEAN_CHECK), $(error "Dirty Git repository!"))
docker build -t $(BUILD_TAG) --build-arg COMMIT=$(COMMIT) --build-arg VERSION=$(VERSION) .
gofmt:
@@ -46,17 +48,24 @@ gofmt:
@test -z "$(shell gofmt -s -l -d -e ./cmd | tee /dev/stderr)"
test: gofmt
- GORACE=history_size=5 go test -race -timeout $(TIMEOUT)s $(TESTPKGS)
+ GORACE=history_size=5 go test -race -timeout $(TIMEOUT)s -cover $(TESTPKGS)
tag:
- $(info Make: Tagging image "$(IMAGE_NAME)" with "$(BUILD_TAG)" and "$(LATEST_TAG)".)
- docker tag $(BUILD_TAG) $(LATEST_TAG)
+ $(info Make: Tagging image "$(IMAGE_NAME)" with "$(BUILD_TAG)", "$(LATEST_TAG) and "$(VERSION_TAG)".)
+ @docker tag $(BUILD_TAG) $(LATEST_TAG)
+ @docker tag $(BUILD_TAG) $(VERSION_TAG)
push: tag
$(info Make: Pushing image "$(IMAGE_NAME)".)
+ifneq ($(GIT_BRANCH),master)
+ $(error "Not on master branch, won't push!")
+endif
+
docker push $(BUILD_TAG)
+ docker push $(VERSION_TAG)
docker push $(LATEST_TAG)
output: test
@echo Docker Image: $(BUILD_TAG)
+ @echo Docker Image: $(VERSION_TAG)
@echo Docker Image: $(LATEST_TAG)
diff --git a/vmware-event-router/README.MD b/vmware-event-router/README.MD
index 265de1ec..66bf05af 100644
--- a/vmware-event-router/README.MD
+++ b/vmware-event-router/README.MD
@@ -1,7 +1,7 @@
# VMware Event Router
-The VMware Event Router is used to connect to various VMware event `streams` (i.e. "sources") and forward these events to different `processors` (i.e. "sinks"). This project is currently used by the [*vCenter Event Broker Appliance*](https://github.com/vmware-samples/vcenter-event-broker-appliance) as the core logic to forward vCenter events to configurable event `processors` (see below).
+The VMware Event Router is used to connect to various VMware event `streams` (i.e. "sources") and forward these events to different `processors` (i.e. "sinks"). This project is currently used by the [*VMware Event Broker Appliance*](https://www.vmweventbroker.io/) as the core logic to forward vCenter events to configurable event `processors` (see below).
**Supported event sources:**
- [VMware vCenter Server](https://www.vmware.com/products/vcenter-server.html)
@@ -17,7 +17,6 @@ The VMware Event Router uses the [CloudEvents](https://cloudevents.io/) standard
- Only one event `stream` and one event `processor` can be configured at a time
- It is possible though to run **multiple instances** of the event router
- At-most-once delivery semantics are provided
- - See [this FAQ](https://github.com/vmware-samples/vcenter-event-broker-appliance/blob/development/FAQ.md) for a deeper understanding of messaging semantics
## Table of Contents
@@ -28,6 +27,8 @@ The VMware Event Router uses the [CloudEvents](https://cloudevents.io/) standard
- [Stream Processor: Configuration Details for AWS EventBridge](#stream-processor-configuration-details-for-aws-eventbridge)
- [Metrics Server: Configuration Details](#metrics-server-configuration-details)
- [Deployment](#deployment)
+ - [Assisted Deployment](#assisted-deployment)
+ - [Manual Deployment](#manual-deployment)
- [Build from Source](#build-from-source)
- [Example Event Structure](#example-event-structure)
@@ -264,6 +265,12 @@ VMware Event Router can be deployed and run as standalone binary (see [below](#b
> **Note:** Docker images are available [here](https://hub.docker.com/r/vmware/veba-event-router).
+#### Assisted Deployment
+
+For your convenience we provide an install script [here](hack/README.md).
+
+#### Manual Deployment
+
Create a namespace where the VMware Event Router will be deployed to:
```bash
diff --git a/vmware-event-router/hack/README.md b/vmware-event-router/hack/README.md
new file mode 100644
index 00000000..f464781a
--- /dev/null
+++ b/vmware-event-router/hack/README.md
@@ -0,0 +1,109 @@
+# Deploy VMware Event Broker Application to existing Kubernetes Cluster
+
+For customers with an existing Kubernetes ("K8s") cluster, you can deploy the underlying components that make up the VMware Event Broker Appliance. The instructions below will guide you in downloading the required files and using the `create_k8s_config.sh` shell script to aide in deploying the VEBA K8s application.
+
+The script will prompt users for the required input and automatically setup and deploy both OpenFaaS and the VMware Event Router components giving you a similar setup like the VMware Event Broke Appliance. If you have already deployed OpenFaaS, you can skip that step during the script input phase.
+
+## Pre-Req:
+* Ability to create namespaces, secrets and deployments in your K8s Cluster using kubectl
+* Outbound connectivity or access to private registry from the K8s Cluster to download the required containers to deploy OpenFaaS and/or VMware Event Router
+
+## Deploy VMware Event Router and OpenFaaS
+
+### Install
+
+Step 1 - Clone the OpenFaaS to your local system
+
+```
+git clone https://github.com/openfaas/faas-netes
+```
+
+Step 2 - Change into the `faas-netes` directory and checkout version `0.9.2` which has been tested with VEBA and then change back to previous working directory.
+
+
+```
+cd faas-netes
+git checkout 0.9.2
+cd ..
+```
+
+Step 3 - Download the `create_k8s_config.sh` script and ensure it has executable permission (`chmod +x create_k8s_config.sh`).
+
+Step 4 - Run the `create_k8s_config.sh` script which will prompt for vCenter Server address (FQDN/IP Address), the vCenter Server username and password which is authorized to retrieve vCenter Server Events (readOnly role is sufficient) and the admin password for OpenFaaS. Prior to deploying, you will be asked to confirm the input in case you need to change it.
+
+```
+./create_k8s_config.sh
+```
+
+Here is an example of what you should see if the deployment was successful:
+
+
+
+Step 5 - Ensure that all pods are running in both OpenFaaS and VMware namespace:
+
+```
+# kubectl get pods -n openfaas
+NAME READY STATUS RESTARTS AGE
+alertmanager-bdf9db7b9-ldwkz 1/1 Running 0 27s
+basic-auth-plugin-665bf4d59b-f87rm 1/1 Running 0 27s
+faas-idler-f4597f655-pr5tq 1/1 Running 0 27s
+gateway-cdf7b89fb-7589b 2/2 Running 1 27s
+nats-8455bfbb58-j4wpm 1/1 Running 0 27s
+prometheus-688d9cfbf7-wkvc9 1/1 Running 0 26s
+queue-worker-649bdf958f-k55g2 1/1 Running 0 27s
+```
+
+
+```
+# kubectl get pods -n vmware
+NAME READY STATUS RESTARTS AGE
+vmware-event-router-6744cc6447-xbpmn 1/1 Running 1 42s
+```
+
+To retrieve the OpenFaaS Gateway IP Address for function deployment, run the following command:
+
+```
+kubectl -n openfaas describe pods $(kubectl -n openfaas get pods | grep "gateway-" | awk '{print $1}') | grep "^Node:" | awk -F "/" '{print $2}'
+```
+
+**Note:** If you don't use an Ingress controller, load-balancer or other means to expose your Kubernetes deployments (services), then the default OpenFaaS endpoint is `http://:31112`
+
+### Uninstall
+
+To remove the VEBA and OpenFaaS K8s application, run the following commands:
+
+```
+kubectl delete ns vmware
+kubectl delete -f faas-netes/yaml
+kubectl delete -f faas-netes/namespaces.yml
+```
+
+## Deploy only VMware Event Router
+
+Step 1 - Download the `create_k8s_config.sh` script and ensure it has executable permission (`chmod +x create_k8s_config.sh`).
+
+Step 2 - Run the `create_k8s_config.sh` script which will prompt for vCenter Server address (FQDN/IP Address), the vCenter Server username and password which is authorized to retrieve vCenter Server Events (readOnly role is sufficient). Prior to deploying, you will be asked to confirm the input in case you need to change it.
+
+```
+./create_k8s_config.sh
+```
+
+Here is an example of what you should see if the deployment was successful:
+
+
+
+Step 3 - Ensure the VMware Event Router pod is running in the VMware namespace:
+
+```
+# kubectl get pods -n vmware
+NAME READY STATUS RESTARTS AGE
+vmware-event-router-6744cc6447-xbpmn 1/1 Running 1 42s
+```
+
+## Uninstall:
+
+To remove the VMware Event Router K8s application, run the following command:
+
+```
+kubectl delete ns vmware
+```
\ No newline at end of file
diff --git a/vmware-event-router/hack/create_k8s_config.sh b/vmware-event-router/hack/create_k8s_config.sh
new file mode 100755
index 00000000..207f3314
--- /dev/null
+++ b/vmware-event-router/hack/create_k8s_config.sh
@@ -0,0 +1,179 @@
+#!/bin/bash
+# Copyright 2019 VMware, Inc. All rights reserved.
+# SPDX-License-Identifier: BSD-2
+
+set -euo pipefail
+
+black='\E[30;40m'
+red='\E[31;40m'
+green='\E[32;40m'
+magenta='\E[35;40m'
+cyan='\E[36;40m'
+reset='\033[00m'
+
+cecho () {
+ local default_msg="No message passed."
+
+ message=${1:-$default_msg}
+ color=${2:-$black}
+
+ echo -e "$color"
+ echo -e "$message"
+ tput sgr0
+
+ return
+}
+
+cecho "Enter the following values for deployment:" $cyan
+
+echo -e "${magenta}"
+read -p "vCenter Server FQDN: " VCENTER_SERVER
+if [[ -z "${VCENTER_SERVER}" ]]; then
+ cecho "Please start over. No input entered." $red
+ exit 1
+fi
+
+echo -e "${magenta}"
+read -p "vCenter Server Username: " VCENTER_USERNAME
+if [[ -z "${VCENTER_USERNAME}" ]]; then
+ cecho "Please start over. No input entered." $red
+ exit 1
+fi
+
+echo -e "${magenta}"
+echo -n "vCenter Server Password: "
+read -s VCENTER_PASSWORD
+if [[ -z "${VCENTER_PASSWORD}" ]]; then
+ cecho "Please start over. No input entered." $red
+ exit 1
+fi
+echo ""
+
+echo -e "${magenta}"
+read -p "Deploy OpenFaaS: [y|n] " DEPLOY_OPENFAAS
+if [[ -z "${DEPLOY_OPENFAAS}" ]]; then
+ cecho "Please start over. No input entered." $red
+ exit 1
+fi
+
+if [ ${DEPLOY_OPENFAAS} == "y" ]; then
+ echo -e "${magenta}"
+ echo -n "OpenFaaS Admin Password: "
+ read -s OPENFAAS_PASSWORD
+ if [[ -z "${OPENFAAS_PASSWORD}" ]]; then
+ cecho "Please start over. No input entered." $red
+ exit 1
+ fi
+fi
+echo ""
+
+cecho "Please confirm the following settings are correct:\n" $cyan
+echo -e "\tVCENTER_SERVER=${VCENTER_SERVER}"
+echo -e "\tVCENTER_USERNAME=${VCENTER_USERNAME}"
+echo -e "\tVCENTER_PASSWORD=${VCENTER_PASSWORD}"
+echo -e "\tDEPLOY_OPENFAAS=${DEPLOY_OPENFAAS}"
+if [ ${DEPLOY_OPENFAAS} == "y" ]; then
+ echo -e "\tOPENFAAS_PASSWORD=${OPENFAAS_PASSWORD}"
+fi
+
+echo -e "${cyan}"
+read -p "Do you want to proceed with the VEBA K8s deployment [y]: " answer
+case $answer in
+ [Yy]* ) cecho "Starting Deployment ..." $green;;
+ [Nn]* ) cecho "Exiting ..." $red; exit;;
+ * ) cecho "Exiting ..." $red; exit;;
+esac
+
+cecho "Checking for K8s namespace creation permission ..." $green
+kubectl auth can-i create ns -q
+if [ $? -eq 1 ]; then
+ cecho "You do not have permission to create a new K8s namespace" $red
+ exit 1
+fi
+
+cecho "Checking for K8s deployments creation permission ..." $green
+kubectl auth can-i create deployments -q
+if [ $? -eq 1 ]; then
+ cecho "You do not have permission to create a new K8s deployments" $red
+ exit 1
+fi
+
+cecho "Checking for K8s secrets creation permission ..." $green
+kubectl auth can-i create secrets -q
+if [ $? -eq 1 ]; then
+ cecho "You do not have permission to create a new K8s secrets" $red
+ exit 1
+fi
+
+cecho "Creating vmware namespace ..." $green
+echo -e "\tkubectl create namespace vmware\n"
+kubectl create namespace vmware
+
+if [ $DEPLOY_OPENFAAS == "y" ]; then
+ cecho "Deploying OpenFaaS ..." $green
+ echo -e "\tkubectl create -f faas-netes/namespaces.yml"
+ echo -e "\tkubectl -n openfaas create secret generic basic-auth --from-literal=basic-auth-user=admin --from-literal=basic-auth-password=${OPENFAAS_PASSWORD}"
+ echo -e "\tkubectl create -f faas-netes/yaml"
+ kubectl create -f faas-netes/namespaces.yml
+ kubectl -n openfaas create secret generic basic-auth --from-literal=basic-auth-user=admin --from-literal=basic-auth-password="${OPENFAAS_PASSWORD}"
+ kubectl create -f faas-netes/yaml
+
+ OPENFAAS_GATEWAY=$(kubectl -n openfaas describe pods $(kubectl -n openfaas get pods | grep "gateway-" | awk '{print $1}') | grep "^Node:" | awk -F "/" '{print $2}')
+ while [ 1 ];
+ do
+ cecho "Waiting for OpenFaaS to be ready ..." $green
+ OPENFAAS_GATEWAY=$(kubectl -n openfaas describe pods $(kubectl -n openfaas get pods | grep "gateway-" | awk '{print $1}') | grep "^Node:" | awk -F "/" '{print $2}')
+ if [ ! -z ${OPENFAAS_GATEWAY} ]; then
+ break
+ fi
+ done
+fi
+
+cecho "Creating VEBA deployment files..." $green
+cat > event-router-config.json << EOF
+[{
+ "type": "stream",
+ "provider": "vmware_vcenter",
+ "address": "https://${VCENTER_SERVER}:443/sdk",
+ "auth": {
+ "method": "user_password",
+ "secret": {
+ "username": "${VCENTER_USERNAME}",
+ "password": "${VCENTER_PASSWORD}"
+ }
+ },
+ "options": {
+ "insecure": "true"
+ }
+ },
+ {
+ "type": "processor",
+ "provider": "openfaas",
+ "address": "http://gateway.openfaas:8080",
+ "auth": {
+ "method": "basic_auth",
+ "secret": {
+ "username": "admin",
+ "password": "${OPENFAAS_PASSWORD}"
+ }
+ },
+ "options": {
+ "async": "false"
+ }
+ },
+ {
+ "type": "metrics",
+ "provider": "internal",
+ "address": "0.0.0.0:8080",
+ "auth": {
+ "method": "none"
+ }
+ }
+]
+EOF
+
+cecho "Deploying VMware Event Router ..." $green
+echo -e "\tkubectl -n vmware create secret generic event-router-config --from-file=event-router-config.json"
+echo -e "\tkubectl -n vmware create -f https://raw.githubusercontent.com/vmware-samples/vcenter-event-broker-appliance/master/vmware-event-router/deploy/event-router-k8s.yaml"
+kubectl -n vmware create secret generic event-router-config --from-file=event-router-config.json
+kubectl -n vmware create -f https://raw.githubusercontent.com/vmware-samples/vcenter-event-broker-appliance/master/vmware-event-router/deploy/event-router-k8s.yaml
diff --git a/vmware-event-router/hack/example1.png b/vmware-event-router/hack/example1.png
new file mode 100644
index 00000000..07c8a5d9
Binary files /dev/null and b/vmware-event-router/hack/example1.png differ
diff --git a/vmware-event-router/hack/example2.png b/vmware-event-router/hack/example2.png
new file mode 100644
index 00000000..59d6f3e8
Binary files /dev/null and b/vmware-event-router/hack/example2.png differ
diff --git a/vmware-event-router/internal/processor/aws_event_bridge.go b/vmware-event-router/internal/processor/aws_event_bridge.go
index 0ccea60e..cca54d18 100644
--- a/vmware-event-router/internal/processor/aws_event_bridge.go
+++ b/vmware-event-router/internal/processor/aws_event_bridge.go
@@ -12,6 +12,7 @@ import (
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/eventbridge"
+ "github.com/aws/aws-sdk-go/service/eventbridge/eventbridgeiface"
"github.com/pkg/errors"
"github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/color"
"github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/connection"
@@ -27,12 +28,13 @@ const (
authMethodAWS = "access_key" // only this method is supported by the processor
resyncInterval = time.Minute * 5 // resync rule patterns after interval
pageLimit = 50 // max 50 results per page for list operations
+ batchSize = 10 // max 10 input events per batch sent to AWS
)
// awsEventBridgeProcessor implements the Processor interface
type awsEventBridgeProcessor struct {
session session.Session
- eventbridge.EventBridge
+ eventbridgeiface.EventBridgeAPI
source string
verbose bool
*log.Logger
@@ -100,7 +102,7 @@ func NewAWSEventBridgeProcessor(ctx context.Context, cfg connection.Config, sour
if ebSession == nil {
return nil, errors.Errorf("could not create AWS event bridge session")
}
- eventBridge.EventBridge = *ebSession
+ eventBridge.EventBridgeAPI = ebSession
var found bool
var nextToken *string
@@ -114,7 +116,7 @@ func NewAWSEventBridgeProcessor(ctx context.Context, cfg connection.Config, sour
return nil, errors.Wrap(err, "could not list event bridge rules")
}
- arnLoop:
+ arnLoop:
for _, rule := range rules.Rules {
switch {
case *rule.Arn == ruleARN:
@@ -144,7 +146,7 @@ func NewAWSEventBridgeProcessor(ctx context.Context, cfg connection.Config, sour
}
switch {
- case found: // return early
+ case found:
break
case rules.NextToken != nil: // try next batch of rules, if any
nextToken = rules.NextToken
@@ -172,36 +174,40 @@ func NewAWSEventBridgeProcessor(ctx context.Context, cfg connection.Config, sour
// throttling/batching
// https://docs.aws.amazon.com/eventbridge/latest/userguide/cloudwatch-limits-eventbridge.html#putevents-limits
func (awsEventBridge *awsEventBridgeProcessor) Process(moref types.ManagedObjectReference, baseEvent []types.BaseEvent) error {
- input, err := awsEventBridge.createPutEventsInput(baseEvent)
+ batchInput, err := awsEventBridge.createPutEventsInput(baseEvent)
if err != nil {
awsEventBridge.Printf("could not create PutEventsInput for event(s): %v", err)
return nil
}
// nothing to send
- if len(input.Entries) == 0 {
+ if len(batchInput) == 0 {
return nil
}
- // TODO: investigate limits on number/size of entries in a single put
- resp, err := awsEventBridge.PutEvents(&input)
- if err != nil {
- awsEventBridge.Printf("could not send event(s): %v", err)
- return nil
- }
- if awsEventBridge.verbose {
- awsEventBridge.Printf("successfully sent event(s) from source %s: %+v", awsEventBridge.source, resp)
+ for idx, input := range batchInput {
+ resp, err := awsEventBridge.PutEvents(&input)
+ if err != nil {
+ awsEventBridge.Printf("could not send event(s) for batch %d: %v", idx, err)
+ continue
+ }
+ if awsEventBridge.verbose {
+ awsEventBridge.Printf("successfully sent event(s) from source %s: %+v batch: %d",
+ awsEventBridge.source,
+ resp,
+ idx)
+ }
}
return nil
}
-func (awsEventBridge *awsEventBridgeProcessor) createPutEventsInput(baseEvent []types.BaseEvent) (eventbridge.PutEventsInput, error) {
- // TODO: Array Members: Minimum number of 1 item. Maximum number of 10 items. for []*eventbridge.PutEventsRequestEntry{}
- // https://github.com/pacedotdev/batch
+func (awsEventBridge *awsEventBridgeProcessor) createPutEventsInput(baseEvent []types.BaseEvent) ([]eventbridge.PutEventsInput, error) {
awsEventBridge.mu.Lock()
defer awsEventBridge.mu.Unlock()
- input := eventbridge.PutEventsInput{
+ batch := []eventbridge.PutEventsInput{}
+
+ tmpInput := eventbridge.PutEventsInput{
Entries: []*eventbridge.PutEventsRequestEntry{},
}
@@ -220,9 +226,8 @@ func (awsEventBridge *awsEventBridgeProcessor) createPutEventsInput(baseEvent []
cloudEvent := events.NewCloudEvent(event, eventInfo, awsEventBridge.source)
jsonBytes, err := json.Marshal(cloudEvent)
if err != nil {
- return eventbridge.PutEventsInput{}, errors.Wrapf(err, "could not marshal cloud event for vSphere event %d from source %s", event.GetEvent().Key, awsEventBridge.source)
+ return nil, errors.Wrapf(err, "could not marshal cloud event for vSphere event %d from source %s", event.GetEvent().Key, awsEventBridge.source)
}
-
jsonString := string(jsonBytes)
entry := eventbridge.PutEventsRequestEntry{
Detail: aws.String(jsonString),
@@ -230,13 +235,23 @@ func (awsEventBridge *awsEventBridgeProcessor) createPutEventsInput(baseEvent []
Source: aws.String(cloudEvent.Source),
DetailType: aws.String(cloudEvent.Subject),
}
- input.Entries = append(input.Entries, &entry)
+ tmpInput.Entries = append(tmpInput.Entries, &entry)
// update metrics
awsEventBridge.stats.Invocations[eventInfo.Name]++
+
+ if idx%batchSize == 0 && idx != 0 {
+ batch = append(batch, tmpInput)
+ tmpInput = eventbridge.PutEventsInput{
+ Entries: []*eventbridge.PutEventsRequestEntry{},
+ }
+ }
+ }
+ if len(tmpInput.Entries) > 0 {
+ batch = append(batch, tmpInput)
}
- return input, nil
+ return batch, nil
}
func (awsEventBridge *awsEventBridgeProcessor) syncPatternMap(ctx context.Context, eventbus string, ruleARN string) {
@@ -274,7 +289,7 @@ func (awsEventBridge *awsEventBridgeProcessor) syncRules(ctx context.Context, ev
return errors.Wrap(err, "could not list event bridge rules")
}
- arnLoop:
+ arnLoop:
for _, rule := range rules.Rules {
switch {
case *rule.Arn == ruleARN:
@@ -309,7 +324,7 @@ func (awsEventBridge *awsEventBridgeProcessor) syncRules(ctx context.Context, ev
switch {
case found: // return early
- break
+ return nil
case rules.NextToken != nil: // try next batch of rules, if any
nextToken = rules.NextToken
continue
diff --git a/vmware-event-router/internal/processor/aws_event_bridge_test.go b/vmware-event-router/internal/processor/aws_event_bridge_test.go
new file mode 100644
index 00000000..5e2b1d22
--- /dev/null
+++ b/vmware-event-router/internal/processor/aws_event_bridge_test.go
@@ -0,0 +1,143 @@
+package processor
+
+import (
+ "testing"
+
+ "github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/metrics"
+ "github.com/vmware/govmomi/vim25/types"
+)
+
+func Test_batching_createPutEventsInput(t *testing.T) {
+ tests := []struct {
+ title string
+ baseEvents []types.BaseEvent
+ desiredBatches int
+ desiredEntries int
+ desiredType string
+ }{
+ {
+ title: "13 VmPoweredOnEvent events 2 batches",
+ baseEvents: baseEventsMockVMPoweredOn(13),
+ desiredBatches: 2,
+ desiredEntries: 13,
+ desiredType: "VmPoweredOnEvent",
+ },
+ {
+ title: "10 VmPoweredOnEvent events 1 batch",
+ baseEvents: baseEventsMockVMPoweredOn(10),
+ desiredBatches: 1,
+ desiredEntries: 10,
+ desiredType: "VmPoweredOnEvent",
+ },
+ {
+ title: "3 VmPoweredOnEvent events 1 batch",
+ baseEvents: baseEventsMockVMPoweredOn(3),
+ desiredBatches: 1,
+ desiredEntries: 3,
+ desiredType: "VmPoweredOnEvent",
+ },
+ {
+ title: "23 VmPoweredOnEvent events 3 batches",
+ baseEvents: baseEventsMockVMPoweredOn(23),
+ desiredBatches: 3,
+ desiredEntries: 23,
+ desiredType: "VmPoweredOnEvent",
+ },
+ {
+ title: "0 events 0 batches unsubscribed event",
+ baseEvents: baseEventsMockCustomizedDVPortEvent(23),
+ desiredBatches: 0,
+ desiredEntries: 0,
+ desiredType: "",
+ },
+ {
+ title: "5 VmPoweredOnEvent events 1 batches",
+ baseEvents: baseEventsMockHalfVMPoweredOn(10),
+ desiredBatches: 1,
+ desiredEntries: 5,
+ desiredType: "VmPoweredOnEvent",
+ },
+ {
+ title: "0 events 0 batches",
+ baseEvents: []types.BaseEvent{},
+ desiredBatches: 0,
+ desiredEntries: 0,
+ desiredType: "",
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.title, func(t *testing.T) {
+ awsEventBridgeStub := createAWSObjectStubVMPoweredOn()
+ batchEvents, err := awsEventBridgeStub.createPutEventsInput(test.baseEvents)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err.Error())
+ }
+ actualBatches := 0
+ actualEntries := 0
+ for _, v := range batchEvents {
+ for _, entry := range v.Entries {
+ actualEntries++
+ if test.desiredType != *entry.DetailType {
+ t.Errorf("wanted entry type: %s got: %s",
+ test.desiredType,
+ *entry.DetailType)
+ }
+ }
+ actualBatches++
+ }
+ if test.desiredBatches != actualBatches {
+ t.Errorf("wanted: %v batches got: %v",
+ test.desiredBatches,
+ actualBatches)
+ }
+ if test.desiredEntries != actualEntries {
+ t.Errorf("wanted: %v entries got: %v",
+ test.desiredEntries,
+ actualEntries)
+ }
+ })
+ }
+}
+
+func baseEventsMockVMPoweredOn(numberOfEvents int) []types.BaseEvent {
+ baseEvents := []types.BaseEvent{}
+ for numberOfEvents > 0 {
+ numberOfEvents = numberOfEvents - 1
+ baseEvents = append(baseEvents, &types.VmPoweredOnEvent{})
+ }
+ return baseEvents
+}
+
+func baseEventsMockCustomizedDVPortEvent(numberOfEvents int) []types.BaseEvent {
+ baseEvents := []types.BaseEvent{}
+ for numberOfEvents > 0 {
+ numberOfEvents = numberOfEvents - 1
+ baseEvents = append(baseEvents, &types.VmPoweringOnWithCustomizedDVPortEvent{})
+ }
+ return baseEvents
+}
+
+func baseEventsMockHalfVMPoweredOn(numberOfEvents int) []types.BaseEvent {
+ baseEvents := []types.BaseEvent{}
+ switchFlag := true
+ for numberOfEvents > 0 {
+ if switchFlag {
+ baseEvents = append(baseEvents, &types.VmPoweringOnWithCustomizedDVPortEvent{})
+ switchFlag = false
+ } else {
+ baseEvents = append(baseEvents, &types.VmPoweredOnEvent{})
+ switchFlag = true
+ }
+ numberOfEvents = numberOfEvents - 1
+ }
+ return baseEvents
+}
+
+func createAWSObjectStubVMPoweredOn() awsEventBridgeProcessor {
+ return awsEventBridgeProcessor{
+ patternMap: map[string]string{"VmPoweredOnEvent": ""},
+ stats: metrics.EventStats{
+ Invocations: make(map[string]int),
+ },
+ }
+}