Fortinet TECWorkshop Template - MVP2

THIS IS A TEST

Change FYI, YOU WILL NOW NEED TO CHECKIN WITH YOUR EMAIL ADDRESS TO VIEW THIS GUIDE

This change helps with analytics and data gathering and is consistent with UserRepo guide (this repo) serving as the root of every new workshop (e.g. this guide is always on latest and greatest featureset which gets cloned into every new workshop)

This site will explain how to use the Hugo Web Framework & the “reLearn” Theme to effectively organize & deliver Fortinet TEC Workshops & Product demos in a consistent, simple, and visually appealing format.

The beauty of this framework lies in its simplicity. Within approx 30 mins, anybody can have a site up and running. Content is created via Markdown files, and the workshop guide layout is simply a directory structure.

Follow along on this simple tutorial to get up and running with a Hugo formatted site for your TEC Workshop/training/demo content today!

Learning Objectives

  • Setup UserRepo on your system & Build container with Hugo & CentralRepo
  • Learn to work in Hugo to create your content to display proper information flow for your TEC Workshop/demo/training
  • Publish your Hugo site to GitHub pages via a CI/CD model

Hugo and Fortinet TECWorkshops - Visually

  • The purpose of this workflow is to simplify creation of Fortinet TECWorkshop guides while providing an example CI/CD development environment with maximum re-usability
Copyright© 2026 Fortinet, Inc. All rights reserved. Fortinet®, FortiGate®, FortiCare® and FortiGuard®, and certain other marks are registered trademarks of Fortinet, Inc., and other Fortinet names herein may also be registered and/or common law trademarks of Fortinet. All other product or company names may be trademarks of their respective owners. Performance and other metrics contained herein were attained in internal lab tests under ideal conditions, and actual performance and other results may vary. Network variables, different network environments and other conditions may affect performance results. Nothing herein represents any binding commitment by Fortinet, and Fortinet disclaims all warranties, whether express or implied, except to the extent Fortinet enters a binding written contract, signed by Fortinet’s General Counsel, with a purchaser that expressly warrants that the identified product will perform according to certain expressly-identified performance metrics and, in such event, only the specific performance metrics expressly identified in such binding written contract shall be binding on Fortinet. For absolute clarity, any such warranty will be limited to performance in the same ideal conditions as in Fortinet’s internal lab tests. Fortinet disclaims in full any covenants, representations, and guarantees pursuant hereto, whether express or implied. Fortinet reserves the right to change, modify, transfer, or otherwise revise this publication without notice, and the most current version of the publication shall be applicable.

Subsections of TECWorkshop Template

What's New

This is a list of new features and/or changes in a given version


2025

Sept MVP 2.2 CURRENT VERSION

  • New Azure Lab Provisioning — launchdemoform now shows real progress, renders credentials on the page, and locks to one attempt per participant/lab/site, backed by a new Durable Function API instead of the old opaque Automation webhook.

May - MVP 2.1

  • Change FortiHugoRunner Tool will be the primary mechanism to work with our process going forward. It will continue to get iterative feature enhancements, so please update it often!
  • Update Hugo relearn theme update due to bug in compatability this Hugo versions 146 and later
  • Change Generate_toml runs on any container restart, so you never have to run it yourself and it will always update whenever you make changes

March - MVP 2.0

  • Change Require Checkin at each repo front page before browsing any further content.
    • Checkin data is used by Analytics gathering and issues a cookie valid for 5 days across our workshop estate. Collecting:
      • e-mail address
      • SMART Ticket
      • Marketing Code
    • New De-coupled analytics check-in functionality from Lab provisioning shortcode.
      • Check-in is automatically included on every repo.
      • Lab provisioning shortcodes should be included on labs featuring Azure automated provisioning scripts, and REMOVED from workshops which don’t require Azure provisioning.
    • Added Author mode so analytics check-in NOT REQUIRED while locally authoring workshops.
  • Change Introduced Go Utility to simplify workshop authoring from different systems
    • When this Utility and documentation is polished, it will eliminate the need for docker_build/_run scripts in each repo and the tax of maintaining/updating them.
  • New Created utility to upgrade older repos to latest feature set including automated conversion of config.toml to repoConfig.json/hugo.toml
  • Update Update gitHub action to use latest API versioned commands
  • Change Replaced Quizdown with CTF Quiz App quizframe shortcode. Check MD Page for usage instructions

2024

Oct - MVP 1.2

  • Change Switched to new container base image from hugomods
    • continuous support for latest Hugo version releases
    • Upgraded ReLearn theme to 6.0.0
  • Change Created a simplified/scripted procedure to convert workshop repos using older containers to the latest:
  • Update Theme updates
  • NewAnalytics
    • Add analytics capabilities to track site activity across entire Cloud TEC workshop catalog.

2023

June - MVP 1.1

  • reduce container size (using Alpine to get shell. BusyBox does not have shell)
  • autopublish action on GitHub (run our container as GitHub action to perform Hugo build w/ CentralRepo)
    • eliminates need to store Hugo static HTML (autopublish action directly publishes to GH pages)
  • Move Shortcodes from CentralRepo to UserRepo
  • Add CentralRepo/scripts/local_copy.sh to copy any local shortcodes or partials into container
  • Modify logo.html to read Params for logoBanner & logoBannerColor
  • Standardize themes and colors for Workshop, UseCase, Spotlight, Demo
  • Modify Banner Text and Subtext to match theme and be customizable
  • Add ability to run container run in “build”, “server”, or “shell” mode
  • Added Dev container env & workflow to stage and test changes before promoting to main/Prod

May - MVP 1.0

  • Separated UserRepo and Central Repo to allow maximum re-usability & future-proofing for style/format changes
    • Standard Repo has Fortinet reLearn theme Variant & all necessary customizations
  • Swap in Hugo ReLearn theme (actively community supported) and eliminate Learn & Notice themes (inactive development)
  • Ubuntu container:
    • Maintain consistent Hugo version and eliminate need for local installs
    • allow continuous improvement to our process via container improvements without adding burden to CSE Team
  • Use Container for Hugo Build & GitHub action to publish/refresh GitHub Pages
  • Containerizing our development efforts allows for a lightweight development area while eliminating redundant componentry every time we create a new repo/workshop/demo, and allowing simple and automated updates to existing workshops when the parent template changes.
    • Ultimately we’d like to have scripting copy/revise parent templates periodically and/or whenever we create new Workshops
  • First TECWorkshop re-published with this new workflow: https://fortinetcloudcse.github.io/FortiCNF/

March - MVP 0.2

  • created FortiCloudCSE GitHub Org
  • begin separation of UserRepo & Central Repo
  • begin investigation into container workflow

Jan - MVP 0.1

Ch 1 Getting Started with Repos, Containers, and Hugo

Change New

Setting up your Hugo environment (15 min)


Prereqs

  • Docker - older installs won’t work, so if you need to upgrade/reinstall
    • You can use your docker flavor of choice. Keep in mind Docker Desktop is no longer free for enterprise use.
    • We’ve tested Rancher Desktop, which works well. Caveat…your locally rendered version of the site may not update in real time as you modify content, so you may need to restart the server to see new content
  • GitHub keys (to clone GitHub repos using SSH which will eliminate Headaches later down the line with Authentication)
    • Follow the instructions here to generate a new SSH key pair
      Warning

      DO NOT USE A PASSPHRASE when you create the keys

    • Once created follow these directions to add the newly created Key to your GitHub account.

What is Hugo and how easy can this be?

  • The site you are viewing right now is built with Hugo.
  • You can navigate to specific chapters and tasks with the Left Navbar or the top banner table of contents
    • For a sequential step by step flow through this workshop, use the arrows in the upper right corner to go through each step individually

Development Environment Options

  • To start, you’ll request a new repo which is a clone of User Repo.
    • you’ll create the content for your TECWorkshop guides in this repo, and ultimately publish the Hugo built website to GitHub Pages
  • Once you have UserRepo, you can choose how to use Hugo
  1. NewOption 1 RECOMMENDED METHOD: Use the Go Utility flavored for your OS/Architecture to build and run our container seamlessly.
  2. Option 2: build a container with Hugo installed on it and a copy of all Fortinet specific customizations to the Hugo reLearn theme
    • Beyond providing an opportunity to learn the basics of container development, this option:
      • streamlines and simplifies the Hugo content creation process
      • minimizes local storage/upkeep of reusable componentry
      • reduces complications of version dependencies in development environment for Hugo or the reLearn theme
      • future-proofs the content created for any given TECWorkshop so that any Fortinet branding changes can be easily re-applied to all guides
  3. Option 3: THIS OPTION IS INCLUDED FOR POSTERITY ONLY Install Hugo locally on your laptop/workstation and clone CentralRepo
    • You’ll have to
      • maintain CentralRepo including submodules on your local workstation
      • ensure your final site is published to the /docs folder in your UserRepo

Subsections of Ch 1: Setup - NEW

Task 1 - Repo Creation

Repo creation request


Request a new repo for your TECWorkshop (this will be referred to as your UserRepo)

  • Send an email to fortinetcloudcse@fortinet.com to request a new GitHub repo and Jenkins Pipeline. Providing the following:
    • Repo Name
    • GitHub Usernames of collaborators
    • Your Fortinet email address that you use to log in to FortiCloud
  • Behind the scenes, a script is used to create your TECWorkshop repo with appropriate protections, features, and collaborators. Additionally a Jenkins pipeline will be setup to monitor changes to the repo and run things like
    • publishing the website
    • FortiDevSec scanning. To facilitate this, administrators will send you an application id that you must paste into fdevsec.yaml in order for the scans to run and tests to pass. Jenkins tests must pass in order for your feature branch to be merged into the main branch.
      • Along with the FortiDevSec application id, you will also be sent an account number which you can use to navigate to the FortiDevSec console to view the results of your scan. To do so, head to FortiCloud and click IAM Login on the left hand side of the page. Enter the provided account number along with your FortiCloud login credentials.
        forticloud-iam-login
  • You will use this repo to create and modify MD chapters & tasks to create your workshop Guide in Hugo format.

Repo Restrictions, Jenkins interactions, GitHub Actions

  • The only approved method to create repos in FortinetCloudCSE org is via request to mailto:fortinetcloudcse@fortinet.com
  • Only Authorized collaborators and admins are allowed to push to repos in FortinetCloudCSE org
    • We require pushes be made to a feature branch
  • Upon push to feature branch:
    • Jenkins clones the repo into its workspace on EC2 and runs FortiDevSec Scan which performs
      • SAST Scan
      • check for vulnerabilities in code, 3rd party libraries, and libraries pulled into Dockerfile
      • secrets scan
      • IaC Scan for misconfig
  • GitHub Pages deploys only on the main branch.
  • Requirements for merging feature to main branch:
    • FortiDevSec Tests Pass
    • PR request submitted and approved (automated approval)

Git repo setup

  • Once your TECWorkshop repo is created, clone the repo and change your working directory to the cloned repo

        git clone <provided link>
        cd <cloned repo directory>
  • The first thing you’ll want to do with the repo is create a Feature branch. There are branch protections in place on the repo preventing you from pushing to main, so you’ll have to follow our workflow described in Ch3

      git checkout -b Feature-<userid>-<shortDescr>
  • Full GitFlow & additional tips are available here

Hugo Frontmatter

First step on every new repo!

  • Due to the nature of Open source software, sometimes there are breaking changes. In this instance, Hugo has deprecated usage of the config.toml file in favor of a new file named Hugo.toml
  • Additionally, as part of the upgrade, we needed to modify some parameters in the file, so we took the opportunity to use a Jinaj2 template to generate the file for with proper parameters
    • This also allows us the ability to update the template in the future and re-generate hugo.toml as necessary
    • Rather than modify Hugo.toml directly, we will now maintain a JSON configuration file /scripts/repoConfig.json
  • TLDR Update scripts/repoConfig.json with parameters for your workshop
  • Every time the container runs it first executes a script generate_toml which will generate the hugo.toml file
    • Change you will no longer see config.toml or hugo.toml in your repo! It’s created automatically for you, and only exists while the container is running.

example of repoConfig.json. Replace each value with specific parameters for your repo:

  • repoName (your repo name from GitHub)
  • author
  • Workshop Title
  • themeVariant (options: [“Workshop”, “Demo”, “UseCase”, “Spotlight”, “Xperts2024”, “Xperts2025”, “CloudCSEMovie”] )
  • logoBannerText (whatever you want in top left Menu under Fortinet Logo. Leaving this field blank will default to the themeVariant name)
  • logoBannerSub Text (optional sub-banner text)
  • marketingCode (optional marketing code to be used as default in analytics gathering and provisioning form)
  • bannerLine1 (optional text for XPerts Content Header banner line 1)
  • bannerLine2 (optional text for XPerts Content Header banner line 2)
  • bannerLine3 (optional text for XPerts Content Header banner line 3)
  • quizUrl (optional URL for quiz to be used in the quiz shortcode)
  • videoHeaderSrc (optional, CloudCSEMovie theme only — URL path to the MP4, e.g. "/videos/header-bg.mp4". Hugo strips the static/ prefix — do NOT include it in the path. Leave blank to disable.)
  • videoHeaderInterval (optional, CloudCSEMovie theme only — total seconds between the start of each video play cycle. Defaults to 60. Must be greater than the video duration.)
  • Shortcuts (Helpful Resources Links in the left menu
  • DO NOT CHANGE THE FOLLOWING:
    • errorLevel
    • googleServicesID
{
  "repoName":"UserRepo",
  "author":"CSE Employee",
  "workshopTitle":"Hugo for Fortinet TECWorkshops",
  "themeVariant":"CloudCSEMovie",
  "logoBannerText":"",
  "logoBannerSubText":"",
  "errorLevel":"warning",
  "googleServicesID":"G-5RZBH288ST",
  "marketingCode": "MGO12345",
  "bannerLine1":"Workshop Type ID Eg: Public Cloud 101",
  "bannerLine2":"Workshop Title Here",
  "bannerLine3":"Subtitle,Description, or 3rd line Here",
  "quizUrl": "https://forms.gle/yourformhere",
  "videoHeaderSrc": "/videos/header-bg.mp4",
  "videoHeaderInterval": "60",
  "shortcuts": [
  {
    "text": "Fortinet Cloud CSE GitHub Org",
    "URL": "https://github.com/FortinetCloudCSE",
    "icon": "fa-graduation-cap",
    "weight":10
  },
  {
    "text": "Fortinet Standard Workshop Guide",
    "URL": "https://fortinetcloudcse.github.io/UserRepo",
    "icon": "fa-tools",
    "weight":20
  },
  {
    "text": "Fortinet Standard Workshop Template Repo",
    "URL": "https://github.com/FortinetCloudCSE/UserRepo",
    "icon": "fa-tools",
    "weight":30
  },
  {
    "text": "Fortinet Hugo reLearn theme - Guide",
    "URL": "https://mcshelby.github.io/hugo-theme-relearn/index.html",
    "icon": "fa-tools",
    "weight":40
  }
  ]
}

CloudCSEMovie Theme — Video Header Background

The CloudCSEMovie theme variant replaces the static header image with a looping MP4 video in the sidebar header area (the same region used by the Xperts background image).

How it works

  • The video plays automatically on page load (muted, no controls)
  • When the video ends, it pauses and replays after a configurable interval — for example, a 6-second video with a 60-second interval plays once, waits 54 seconds, then plays again
  • All other header content (logo, banner text, search bar) renders on top of the video

Setup steps

1. Add your MP4 to the repo

Place the video file in your repo’s static/videos/ folder:

static/
  videos/
    header-bg.mp4

2. Configure repoConfig.json

{
  "themeVariant": "CloudCSEMovie",
  "videoHeaderSrc": "/videos/header-bg.mp4",
  "videoHeaderInterval": 60
}
  • videoHeaderSrc — the URL path to your MP4, relative to the site root. Required for the video to appear.
  • videoHeaderInterval — total seconds from the start of one play to the start of the next. Optional; defaults to 60. Set this to a value greater than your video’s duration.

Tip Leave videoHeaderSrc as an empty string "" to use the CloudCSEMovie theme without a video (renders a solid color header like other themes).

Note The MP4 file is committed to your repo inside static/videos/. Keep it small — short clips under 5MB are ideal for fast page loads.

FortiHugoRunner Tool

New Go Utility for interacting with Container


The fortihugorunner CLI tool is a helper CLI tool that currently supports simplified workshop authoring on any Windows, MacOS, or Linux system with the following capabilities:

  • Same usage across different development platforms
  • Creates (builds) Fortinet Cloud CSE Docker development images
  • Launches local Docker container with Hugo web server and live updating view of rendered site as modifications are made and saved

You can download the binary for your OS and architecture specifications from the repo. Binaries are available for Windows, Mac, and Linux. The following sections will walk you through downloading the appropriate binary and setting up the tool in your environment.

Note

You must have Docker installed and running to use this tool. You also need to verify that the Docker API Client is reachable from the terminal environment where you’ll be running it. In Windows and Mac, Rancher Desktop is recommended for working with Docker. In Linux, installation instructions for various distributions may be found here.

Keep Docker Engine, Docker Desktop, or Rancher Desktop updated. FortiHugoRunner is a client that talks to your local Docker daemon; security fixes in FortiHugoRunner update the CLI binary and its bundled dependencies, but they do not patch the Docker daemon itself.

Some information on common issues you may run into while getting started with Docker can be found on the troubleshooting page. If you have any questions or need any help getting set up, please reach out to the FortinetCloudCSE team at fortinetcloudcse@fortinet.com.

fortihugorunner flows

Here’s a visual of what’s happening when we use this new tool:

ContainerFlow

System Architecture determination and binary download

Run this command from the Windows Command Prompt to find your OS Architecture

echo %PROCESSOR_ARCHITECTURE%

Depending on your output, download the appropriate Go Binary from the releases page:

If your command output is:then download:
AMD64fortihugorunner-windows-amd64.exe
x86fortihugorunner-windows-386.exe

Run the following to get your OS Architecture:

uname -m

Depending on the output, download the appropriate Go Binary from the releases page:

If your command output is:then download:
x86_64fortihugorunner-<darwin/linux>-amd64.exe
arm64fortihugorunner-<darwin/linux>-arm64.exe

Utility Setup

You should place the downloaded binary at the root of your Hugo development folder so it’s in a well-known location. This will make replacing and/or upgrading the utility easier.

For Example

If you use /home/ubuntu/pythonProjects/ for all of your Hugo development, and you clone repos into this folder structure, you should place the Go Utility here as your well-known location

Further, adding this well-known location to your system’s $PATH will enable you to run the utility on any repo you edit.

Tip

In the following examples, we’ll use C:\users\someUser\pythonProjects as our well-known-location

To show and change your system path using Windows CLI do the following:

echo %PATH%
setx PATH "%PATH%;C:\users\someUser\pythonProjects"
echo %PATH%
Warning

The echo %PATH% command lists your existing $PATH variable before and after the setx change. Be careful with setx as it replaces the PATH value, not appends (though %PATH% includes the existing value).

To show and change your system path using Windows Powershell do the following:

$env:Path
[Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\users\someUser\pythonProjects", [EnvironmentVariableTarget]::User)
$env:Path

To find your system path on Linux or MacOS:

echo $PATH

To add your well-known-location to the system path, edit the etc/environment file and append the well-known location for the Go Utility. The following commands show you how to do this using nano editor:

Tip

In the following examples, we’ll use /home/ubuntu/pythonProjects as our well-known-location

  1. Open the File

    sudo nano etc/environment
  2. Find the PATH line and append your well-known location where the Go Utility binary is stored, to the end of the existing line, making sure to retain the end "

    append /home/ubuntu/pythonProjects/ to the end of the following line as so:

    PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/ubuntu/pythonProjects/"
  3. Save file and exit nano editor with: CTRL+X, then Y, then Enter

  4. Apply the file changes to your current terminal session

    source /etc/environment
  5. Verify the change

    echo $PATH
  6. Finally, make sure the Go Binary is executable:

    chmod +x /home/ubuntu/pythonProjects fortihugorunner

Running the utility

Once the binary is downloaded, you can either run it from your workshop directory, or (recommended) copy it into your system path. If you copy it into your system path, it will be available throughout your system and you won’t need to copy the binary anywhere else to run it. You can use the following CLI arguments to modify the utility’s behavior. If you run the utility from an existing Hugo repo directory, leaving the CLI blank will run with defaults listed

The filename of the binary as downloaded will contain the OS and architecture it was compiled to, i.e. fortihugorunner-windows-amd64.exe. If you would like to trim the platform information from the binary, use the rename command:

fortihugorunner rename
# renamed fortihugo-runner-linux-amd64.exe to fortihugorunner.exe

General steps:

  1. Run the Utility to BUILD your container. This step is only necessary when we’ve added features or capabilities within the container. Rebuilding pulls the latest/greatest into your container image
  2. For Any repo you want to edit, run the utility with launch-server command to get a local live-updating view of your Hugo workshop site.

CLI options with defaults for Container BUILD

fortihugorunner build-image \
    --env admin-dev       # testing image, used for container/process development, named ```hugo-tester```
    --env author-dev      # daily-use image for workshop authoring, named ```fortinet-hugo```

CLI Options with defaults for Container RUN

fortihugorunner launch-server \

**OPTIONAL COMMANDS TO ALTER DEFAULT BEHAVIOR.  OMIT THESE OPTIONS FOR MOST SCENARIOS**

  --docker-image fortinet-hugo:latest \
  --host-port 1313 \
  --container-port 1313 \
  --watch-dir .
  --mount-toml

For each component (build-image, launch-server, etc.), there are help menus available which explain the various parameter flags available.

fortihugorunner -h

#Includes functions for facilitating Hugo app development with docker containers.

#Usage:
#  fortihugorunner [flags]
#  fortihugorunner [command]

#Available Commands:
#  build-image   Builds a Docker image programmatically using the Docker SDK
#  help          Help about any command
#  launch-server Launch the Hugo server container
#  version       Print fortihugorunner version.

#Flags:
#  -h, --help      help for fortihugorunner
#  -v, --version   fortihugorunner version information

#Use "fortihugorunner [command] --help" for more information about a command.

fortihugorunner build-image -h
fortihugorunner launch-server -h

There is also a version flag to display the current version of the tool being used. A CHANGELOG is available in the tool repository for information on version updates and features.

fortihugorunner -v

#Version: v0.3.2
#Date: 2025-05-13
#Platform: linux/amd64

To update your local binary executable to the latest version, use the update command.

fortihugorunner update
# Successfully updated to version 0.5.0! 

Use the update command periodically to pick up FortiHugoRunner security fixes and dependency updates. This updates the CLI binary only; keep your local Docker installation updated separately.

  1. Navigate to your workshop repo directory and run the utility (which is 1 level up in your development root/well-known-location).
  • In the example below, we are working on UserRepo located at C:\users\someUser\pythonProjects\UserRepo
  1. Build the Docker image
  2. Launch Hugo server in Author Mode
cd C:\users\someUser\pythonProjects\UserRepo
..\fortihugorunner.exe build-image --env author-dev
..\fortihugorunner.exe launch-server
  1. Navigate to your workshop repo directory and run the utility (which is 1 level up in your development root/well-known-location).
  • In the example below, we are working on UserRepo located at /home/ubuntu/pythonProjects/UserRepo
  1. Build the Docker image
  2. Launch Hugo server in Author Mode
cd /home/ubuntu/pythonProjects/UserRepo
../fortihugorunner build-image --env author-dev
../fortihugorunner launch-server

Important note on the Go Docker SDK

This utility leverages a Go Docker client from the Moby project, commonly referred to as the Docker SDK for Go to interact with the Docker daemon programmatically. The SDK doesn’t always initiate an implicit pull of missing base images when building from a Dockerfile as is the case when running docker build from the command line. If any base images needed or referenced in your workshop Dockerfile aren’t already present locally, the image build will fail with an error message along the lines of failed to resolve source metadata….

To resolve this, the utility explicitly checks for the presence of any base images before starting the build process and pulls them programmatically if they’re not present. At the moment, the only two required images are:

  • docker/dockerfile:1.5-labs
  • docker.io/hugomods/hugo:std

A parameter is available in the build-image component to change the tag of the hugomods image to pull if necessary. An example is shown below.

./fortihugorunner build-image --env ... --hugo-version 0.146.0

This argument is set to std by default. It is important to ensure that it matches the tag used in your workshop Dockerfile.

docker-base-image

Docker Helpful Hints

Useful Docker Commands to Know

docker images                                           #List all images
docker ps -a                                            #List all containers, both running and stopped
docker rmi <image-id>                                   #Remove an image
docker rmi $(docker images -aq)                         #Remove all images
docker rmi $(docker images --filter dangling=true -aq)  #Remove all images with tag <none>
docker rm <container-id>                                #Remove a container
docker rm $(docker ps -aq)                              #Remove all containers
docker builder prune                                    #Remove build cache

When running any of the above commands, if you get an error message indicating an image or container is being used or referenced in another image or container, you can issue the ‘-f’ flag to force remove.

Go CLI Tool -- Troubleshooting

Troubleshooting


This page outlines and attempts to offer solutions to various issues team members have run into while using the CLI tool. If you run into any issues not listed here, please email us at fortinetcloudcse@fortinet.com so we may research it and include information and solutions here.

General/Common Docker Permissions Issue in Linux

When working with Docker in Linux, you may encounter an error like the following:

docker images

# permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock

This issue indicates there is a permissions issue preventing the Docker CLI (api client) from communicating with the Docker daemon. To resolve it, ensure your docker.sock permissions look like the following:

ls -la /var/run/docker.sock

#srw-rw---- 1 root docker 0 May 28 09:41 /var/run/docker.sock=

If they don’t match what you see here, run:

sudo chmod 660 /var/run/docker.sock

sudo chown root:docker /var/run/docker.sock

Then, add your user account to the docker group:

sudo usermod -aG docker $USER

In order for the change to become effective, you will then need to log out of your terminal session and log back in. The quickest way to do this is to close your terminal window and re-open it. However, if working in a code editor such as VS Code, closing and re-opening may not be sufficient and you may need to completely log out of your Linux session and log back in (or just completely reboot).

After logging back in, check that your account has been added to the docker group:

groups
# ... group7 group8 docker

Windows Docker Issue

If you try to issue Docker commands in Windows specifically and you see this error:

...docker_engine/ping: open //./pipe/docker_engine: The system cannot find the file specified.

It means the Docker CLI is trying to connect to the daemon, but the pipe (communication channel) does not exist–usually because the Docker daemon isn’t running.

To resolve the issue, first ensure you have Docker running. If you’re using Rancher, ensure “Network Status” is online.

{{

rancher-image
}}

You can also check that the API client is available in your terminal environment by running docker images.

docker images

#REPOSITORY            TAG          IMAGE ID       CREATED         SIZE
#fortinet-hugo         latest       89d81e91a4c6   3 hours ago     597MB
#hugomods/hugo         std          2d5893e404be   2 days ago      498MB
....

If you can verify Docker is running and the API client is reachable and you’re still receiving the error message above, try the following:

  1. Try restarting your Docker Windows application (Rancher, etc.)

  2. Check your Docker Context:

docker context ls

docker context use rancher-desktop   # if using rancher, for example
  1. Try rebooting the machine.

Permissions Issues

MacOS

Some users have reported receiving system warnings such as the following when working with the tool in Mac:

{{

mac-go-binary-issue
}}

There is a resolution detailed here:

  1. Open System Settings.

  2. Click Privacy & Security, scroll down and look for the CLI tool binary. Click Open Anyway next to it.

  3. When the warning prompt reappears, click Open.

Windows

Some users have reported an issue in Windows where running the file results in the following warning from Windows Defender/AntiVirus:

{{

wacatac-warning-image
}}

This is uncommon and typically a false positive due to Go-compiled binaries often being misidentified by heuristic antivirus engines, especially if they are not often seen in the wild. If you run into this, please email us at fortinetcloudcse@fortinet.com.

Background Procedures - CentralRepo Maintenance

CentralRepo


CentralRepo contains all of the stuff Hugo needs to build a static website, including Fortinet Customizations to themes.

How it’s used

  • Generally TEC Program participants creating content for Workshops, Demos, User Cases, or Spotlights don’t need to worry about Central Repo at all
  • When you build your container, it inherently grabs the latest copy of CentralRepo:main from github, via this command in Dockerfile:
      ADD https://github.com/FortinetCloudCSE/CentralRepo.git#main /home/CentralRepo
  • This means that you can re-use the same container for every TEC workshop guide you create.
    Tip

    If you haven’t used your container in a while, it’s a good idea to rebuild it so it will grab the latest version of CentralRepo

Repo Maintenance

  • CentralRepo is maintained by the Fortinet Cloud CSE team, so work with us to make any changes necessary.

  • Review, modification, and testing process:

    • Anyone can fork CentralRepo and modify as necessary.
    • Once you’ve tested your modifications, submit a PR to Central Repo
    • Fortinet Cloud CSE team will merge your PR changes into branch CentralRepo:dev
    • Test using HugoDevContainer pointing to the merged branch rather than main
        docker_run_go build-image admin-dev
        docker_run_go launch-server --docker-image hugotester:latest
    Warning

    IMPORTANT If there is collaborative work while testing a PR, be sure to always pull latest from the PR Branch before starting new work!

         git checkout PRBranch
         git remote add <PR Label> git@github.com:FortinetCloudCSE/CentralRepo.git
         git pull dev
    • Upon successful testing, Fortinet Cloud CSE team will merge the branch to main and close PR
        git checkout dev
        git remote add <PR label> <remote Github ssh URL>
        git pull <PR label>
    
        <PERFORM TESTIGN ON CONTAINER, and make any changes as necessary on this branch>
    
        <UPON SUCCESSFUL TESTING>
        git checkout main
        git merge dev --ff-only
        git push 
    
        <Manually Close PR>      

Relearn theme update

Whenever we need to update the ReLearn Theme, perform the following ON CentralRepo where the theme reference is stored:

Alternatively, reference this (untested) method from McShelby: https://mcshelby.github.io/hugo-theme-relearn/introduction/upgrade/index.html

    cd CentralRepo
    git submodule init
    git submodule update --remote
    cd themes/hugo_theme_relearn
    git fetch
    git checkout <latest commit>
    cd ../..
    git submodule deinit --force themes/hugo-theme-relearn
    git add themes/hugo-theme-relearn
    git commit -m "Updating relearn theme to V<x.y.z>"
    git push

Ch 2 Hugo Content Structure

Learn to organize and create content in Hugo- estimated duration 20min

You now have a container running hugo webserver and tracking changes to the /content directory in your repo.

  • Create your TecWorkshop Guide including Chapters and tasks. You can use your favorite editor/IDE to create the markdown pages
  • As you make changes to the MD files, you’ll see LIVE changes to the your local copy of the workshop guide (which depending on your ENV setup may or may not be viewable at: http://localhost:1313/<your_repo_name>

With your FortinetHugo Container running, you can proceed to creating and editing your workshop content.

Hugo is incredibly powerful and allows many customizations, and we won’t cover most of theme here as they’ve already been set for Fortinet’s standard template

Generally, you only need to do 3 things:

  1. Set the folder structure for left hand menu bar navigation/topic structure, according to your chapters and tasks
  2. Create Markdown files for each Chapter and discrete task therein
  3. Adjust the site’s frontmatter settings via your_repo_name/scripts/repoConfig.json to reflect your TECWorkshop repo name, metadata, and leftnav URLs

Click the right arrow to go through each step individually

Subsections of Ch 2: Hugo Content

Task 1 - Chapter Directory Structure

Create a Folder structure correlating to the major topics/sections of the demonstration

  1. Browse to the content directory within your TECWorkshop repo (Locally on your machine)
  • The Left Hand navigation menu is driven by the folder structure you create
  • Folder naming drives site navigation, content hierarchy, and helps organize the overall site layout. Specifically, directory naming helps to identify what’s in each Chapter & task
  1. Within each folder there is an _index.md file (For Chapter/Section pages) or index.md (for regular/leaf pages) which will be used to hold content for that Chapter/Directory/Task
  • The metadata header on that page is used to set
    1. Title of the Page
    2. Link Title (as it appears in the leftNav bar)
    3. Weight the order of chapters or tasks in the leftNav bar
  • Notice the folder structure and file naming on the left and the resulting display on the right
    Info

    Note that the file and folder names only matter for ordering in your directory listing in your IDE. Lower numbered folders will appear first, but it’s really the _index.md weight metadata field which influences the chapter or task ordering. Only the “title” tag within each Markdown file will impact the resulting page view

image reference #Folder/File NamePage TypeWeight
1content/01GettingStarted/_index.htmlChapter Heading Page8 (Global Chapters)
2content/02Hugo/1_directory_structure/index.htmltask page (Hugo Page Bundle)10 (within Chapter 02)
chapterIndex
  1. Subsequent Markdown pages under each folder are used to explain tasks/steps within each chapter
ubuntu@ip-172-31-20-10:~/pythonProjects/UserRepo$ tree
.
├── README.md
├── content
│   ├── 00ChangeLog
│   │   └── _index.md
│   ├── 01GettingStarted
│   │   ├── 1_UserRepo
│   │   │   ├── forticloud-iam-login.png
│   │   │   └── index.md
│   │   ├── 2_HugoFrontmatter
│   │   │   └── index.md
│   │   ├── 3_FortiHugoRunner
│   │   │   ├── docker-base-image.png
│   │   │   └── index.md
│   │   ├── 4_Docker-helpful-hints
│   │   │   └── index.md
│   │   ├── 5_CLITroubleshooting
│   │   │   ├── index.md
│   │   │   ├── mac-go-binary-issue.png
│   │   ├── 6_CentralRepo
│   │   │   └── index.md
│   │   ├── _index.md
│   ├── 02Hugo
│   │   ├── 1_directory_structure
│   │   │   ├── chapterIndex.png
│   │   │   └── index.md
│   │   ├── 2_page_bundles
│   │   │   └── _index.md
│   │   ├── 3_create_md
│   │   │   ├── index.md
│   │   │   └── taskPage.png
│   │   ├── 4_images
│   │   │   └── index.md
│   │   ├── 5_shortcodes
│   │   │   └── index.md
│   │   ├── _index.md
│   │   └── config.png
│   ├── 03Chapter3
│   │   ├── 1_push_publish
│   │   │   └── index.md
│   │   ├── 2_gitflow
│   │   │   └── index.md
│   │   ├── 3_legacy
│   │   │   └── index.md
│   │   ├── _index.md
│   ├── _index.md
├── layouts
│   └── shortcodes
│       ├── someshortcode.html
└── scripts
  └── repoConfig.json

Task 2 - Hugo Page Bundles

Change New

What is a Hugo Page Bundle?

In an effort to better organize and stadardize image storage and display, and to simplify image references in Markdown, Hugo introduced Page Bundles.

A Hugo Page Bundle is a directory that contains a content file (e.g., index.md or _index.md) and its associated resources (e.g., images, videos, documents). This structure allows you to keep all related files together, making it easier to manage and reference them.

Why use Hugo Page Bundles?

  • Organization: Keeps related content and resources together, making it easier to manage.
  • Simplified References: Allows for simpler relative paths to resources, reducing the chances of broken links.
  • Portability: Makes it easier to move or copy content along with its resources.
  • Scalability: Facilitates the addition of new content and resources without cluttering the main content directory.

How to create a Hugo Page Bundle

  1. Create a directory for your content. The directory name can be anything, but it’s often helpful to use a descriptive name.
  2. Inside this directory, create an index.md file for regular pages or _index.md for chapter/section pages. This file will contain the content for that page or section.
  3. Add any associated resources (e.g., images, videos) to the same directory.
  4. Reference the resources in your Markdown file using relative paths.

Example of a Hugo Page Bundle structure

my-page-bundle/
├── index.md
├── image1.png
├── image2.jpg
└── document.pdf
  • For example, if you have an image named example.png in the same directory as your index.md, you can reference it like this:

    ![Example Image](csemascot.png)
  • Even better, use the figure shortcode to avoid unnecessary clickable images

      {{< figure src="csemascot.png" >}}

    Example Image Example Image

Task 2 - Create/Modify MD pages

Create or copy Markdown pages for each task within the chapter

  1. Each Chapter can have 1 or more tasks which should be completed by the participants

  2. To comply with Page Bundle standards, generally the md page should be named index.md if it’s a task page, or _index.md for section/chapter pages,

    Info

    The Title and Weight dictate the leftnav visual and ordering of the pages. Lower weight pages are displayed first

    taskPage

Warning

Because the file and folder names are very similar in our example repo, it can become confusing to know where to make edits. Make sure you’re editing the correct file in your IDE/editor

  1. This page contains several useful markdown shortcodes you can use for visual pop-outs on the site

Lots of shortcodes & Features available here

  • Badges:

    Important Version6.6.6 Captain InfoNew Awesome

  • Icons:

    ⭐ Tips this is a star 💡 this is a lightbulb

  • Notices

    Note

    this is a note box

    Tip

    this is a tip box

    Info

    this is a tip box

    Warning

    The examples and sample code provided in this workshop are intended to be consumed as instructional content. These will help you understand how various Fortinet and Azure services can be architected to build a solution while demonstrating best practices along the way. These examples are not intended for use in production environments without full understanding of how they operate.

  • Expandable sections":

    Expand me…

    Thank you!

  • Tabs

    hello.
    print("Hello World!")
    echo "Hello World!"
    printf"Hello World!");
  • Buttons:

    Get Hugo Get Hugo

  • Mermaid (diagrams & charts):

        graph LR;
            If --> Then
            Then --> Else
    %%{init:{"theme":"forest"}}%%
    graph LR;
        A[Hard edge] -->|Link text| B(Round edge)
        B --> C{<strong>Decision</strong>}
        C -->|One| D[Result one]
        C -->|Two| E[Result two]
    
%%{init:{"fontFamily":"monospace", "sequence":{"showSequenceNumbers":true}}}%%
sequenceDiagram
    Alice->>John: Hello John, how are you?
    loop Healthcheck
        John->>John: Fight against hypochondria
    end
    Note right of John: Rational thoughts!
    John-->>Alice: Great!
    John->>Bob: How about you?
    Bob-->>John: Jolly good!
  • Quizes with the CTF Quiz App (quizframe shortcode)

    {{< quizframe page="/your-quiz-page" height="600" >}}

    The page param is the path to the quiz in the CTF app configured via quizUrl in repoConfig.json. The shortcode passes fortiuser, fortiemail, and workshopID cookies automatically so attendees are pre-identified.

Task 4 - Images & Relative Paths Links

Image Storage

  • Optimal: Use Page Bundles (see previous task)
  • Acceptable: Store images in a common directory and reference them with relative paths (use this if an image is used in multiple places)
  • Not Recommended: Store images in the “static” directory and reference them with absolute paths (e.g., /images/foo.png)

Using Images in Markdown

  • Optimal: Use the Hugo figure shortcode to include images with optional attributes like alt text, title, width, and height.
    {{< figure src="image.png" alt="Description" title="Image Title" width="600" height="400" class="zoomable">}}
  • Acceptable: Use standard Markdown image syntax for simple images without additional attributes.
    ![Description](image.png)
    Note

    Markdown Image syntax results in images that are clickable and open the image file in the browser. This is often not the desired behavior. Use the `figure` shortcode instead and include class="zoomable" if you need to zoom in on a detailed image

  • Example: This page bundle is at content/02Hugo/4_images/index.md and you want to link to another page somewhere else in the site, you can reference it’s page bundle path

Task 5 - Shortcodes & Partials

Partials - Site customization (rarely used)

  • /layouts/partials are customizations used to tweak overall CSS parameters of the site. Generally these are reserved for use by ALL repos and can be changed by emailing Fortinet Cloud CSE team
  • If you absolutely must change something, use Hugo Partials as your guide
  • Partials are automatically included in the site’s CSS configuration

Shortcodes - custom HTML

Choose your deployment path
This page's steps are hidden until you pick one. The choice affects this page only, and you can switch at any time.
Docker Compose (example)Kubernetes / Helm (example)

Task 6 - Deployment Paths (the choice gate)

The problem this solves

If your workshop can be completed two ways — Docker Compose or Kubernetes, AWS or Azure, CLI or GUI — the obvious thing to do is put both on the page, side by side in tabs.

Participants then do both. Not some of them: enough of them that it is the single most common way a follow-along workshop goes wrong. Both sets of commands are visible, both look like steps, so both get run.

The deployment path feature fixes that by making the choice once, up front, and then showing that participant only their own path — in the page body, in the left-hand menu, in the next/previous buttons, and in search results.

Opt-in — nothing changes until you ask for it

A workshop that declares no deploymentPaths behaves exactly as it always has: no extra markup, no CSS, no JavaScript on any page. There is no default path vocabulary and no repo inherits another repo’s paths.

This page declares two example paths in its own front matter, so the feature is live on this page only — the controls above the title are real, and the working example further down is gated by them. Every other page of this guide is untouched.

Do you need it?

Use it when the same learning objective is reached by two or more mutually exclusive routes, and a participant only ever follows one.

SituationUse paths?
Docker Compose vs Kubernetes/Helm deployment of the same labYes
AWS vs Azure vs GCP versions of the same exerciseYes
“Advanced participants can also try…” — extra, optional materialNo — that is a notice or an expander
Two different products covered in sequenceNo — those are chapters
Showing the same command’s output on Linux and macOSNo — that is a normal tabs block

The test: if a participant should run exactly one of the variants and the other is wrong for them, use paths. If they might read both, use tabs.


Step 1 — declare your paths

Declare the vocabulary in one of two places. Which one you pick decides how far the gate reaches.

The whole workshop is gated — scripts/repoConfig.json

This is the normal case. Add deploymentPaths as a site param:

  "deploymentPaths": [
    { "key": "docker", "title": "Docker Compose" },
    { "key": "k8s",    "title": "Kubernetes / Helm" }
  ],

The participant chooses once and every page follows it: gated blocks, the left-hand menu, the next/previous buttons, search results, and a padlock line on every page so the choice is always visible and reversible.

One page gates itself — page front matter

Put the same list in a single page’s front matter instead:

deploymentPaths:
  - key: docker
    title: Docker Compose
  - key: k8s
    title: Kubernetes / Helm

Now only that page gates anything. No menu entry is hidden, no next/previous button is filtered, search is untouched, and the padlock line appears on that page alone. Reach for this when one page forks and the rest of the workshop does not — or when a page needs a self-contained demonstration, which is exactly what the example on this page is.

Either way, each entry needs:

  • key — the short identifier you will write in your markdown (path="docker"). Must start with a letter and contain only letters, digits, hyphens and underscores; it is used as part of a CSS class name as well as an attribute value.
  • title — the label participants see on the button and in the “you are on this path” banner. Write it the way you want it read: “Kubernetes / Helm”, not k8s.

Two or more entries is the normal case. There is no limit, but every path multiplies the content you have to write and keep correct — two is almost always the right answer.

Never both at once

Declaring deploymentPaths in a page’s front matter while the site param also exists is a hard build error. There is one stored choice per participant per site, so the page would write its own key into the slot every site-wide page reads back — and those pages would then quietly show nothing at all, which is also what they correctly do before a first choice. Nothing would look broken.

deploymentPath (singular, the front-matter param that scopes a whole page to one path) is a different thing and needs the site param, because what it gates — menu, next/previous, search — is site-wide by definition.

Order and titles are both load-bearing
  • The first entry is the default: it is the path shown to a participant with JavaScript disabled, and it is the tab the site marks active before anyone clicks. Prefer appending a new path over reordering the list.
  • Renaming a title silently resets every returning participant’s choice — the stored selection is keyed on the title text, so old selections stop matching. No build error, no warning; they are simply asked to choose again.
  • Renaming a key is the loud kind of change: every path="…" in your markdown must be updated to match, or the build fails with a clear error. That is the safer of the two to get wrong.

Step 2 — pick the right block for the job

There are three ways to scope content to a path. Use the smallest one that fits.

pathtabs / pathtab — parallel steps in the middle of a page

For a section where each path has its own version of the same step. The outer pathtabs block wraps one pathtab per path:

{{< pathtabs title="Deploy the stack" >}}
{{% pathtab path="docker" %}}
Bring the stack up on your own machine:

```bash
docker compose up -d
```
{{% /pathtab %}}
{{% pathtab path="k8s" %}}
Install the chart into your cluster:

```bash
helm upgrade --install mylab ./chart
```
{{% /pathtab %}}
{{< /pathtabs >}}
  • title is optional (default: Your path). It labels the banner that tells the participant which path they are on.
  • Use {{< pathtabs >}} with angle brackets for the outer block, and {{% pathtab %}} with percent signs for each path’s body — the percent form is what renders the markdown inside.
  • Every path in deploymentPaths must appear exactly once in every pathtabs block. This is enforced; see what the build refuses below.

pathonly — one path, no tab UI

For prose, a warning, or a whole section that only applies to one path and has no counterpart on the other. Nothing is shown to the other path, and no tab strip is drawn:

{{% pathonly path="k8s" %}}
Your cluster needs a default StorageClass before you continue. Check with
`kubectl get storageclass`.
{{% /pathonly %}}
pathonly must use the percent form

Write {{% pathonly %}}, never {{< pathonly >}}. With angle brackets, any heading inside the block never joins the page’s list of headings, so every in-page link to it breaks — silently, with no build error.

Leave a blank line above and below the opening and closing tags. Without them the markdown inside is treated as raw HTML and never rendered.

deploymentPath front matter — a whole page for one path

When an entire page belongs to one path, scope the page instead of wrapping its body. Add one line to the front matter:

---
title: "Kubernetes / Helm Setup"
linkTitle: "Kubernetes / Helm"
weight: 20
deploymentPath: k8s
---

That single line does four things automatically:

  1. Hides the page’s entry in the left-hand menu for participants on another path.
  2. Skips the page in the next/previous buttons, so following the arrows never walks into the other path.
  3. Removes the page from search results for participants on another path.
  4. If someone reaches it anyway — a bookmark, a shared link, a search from before they chose — shows a banner at the top explaining that the page belongs to the other path, with a button to switch.

Pair it with an opening notice that links to the equivalent page on the other path, so a participant who lands there has somewhere to go.


What the participant sees

Before choosing, at the very top of any page that gates content — above the page title:

Choose your deployment path This page’s steps are hidden until you pick one. Every other page then follows the same choice, and you can switch at any time from the header. [ Docker Compose ] [ Kubernetes / Helm ]

The gated steps below stay hidden until they click. That is deliberate — it is the whole point — but it means anything a participant needs in order to decide must live outside a path block. Requirements, comparisons, “choose this if…” guidance: put those in plain page content, above the blocks.

After choosing, the buttons are replaced by a padlock line naming their path with a Switch to … button beside it. With a site-wide declaration that line appears on every page, so the choice is always visible and always reversible; with a page-level declaration it appears on the declaring page only, which is the only page the choice affects.

Tell them the buttons are buttons

Participants read documentation, not interfaces. Add a short notice near the top of your first page saying that the controls above the title are clickable and that the choice can be changed at any time from any page. It costs three lines and saves the “where did the Kubernetes steps go?” question.

With JavaScript disabled, every path is shown, labelled, with a note that they are alternatives rather than a sequence. Nothing is lost — the gate degrades to the old behaviour.


Try it — this page is the example

Everything below this line is a real, working gate. It is driven entirely by this page’s own front matter — nothing in scripts/repoConfig.json:

deploymentPaths:
  - key: docker
    title: Docker Compose (example)
  - key: k8s
    title: Kubernetes / Helm (example)

That is the only configuration involved, and it is why the rest of this guide has no padlock line, no hidden menu entries and no gate markup at all. The markdown that produces the block below is exactly the escaped pathtabs example from Step 2, with the /* */ escapes removed.

Deploy the stack: Docker Compose (example)Kubernetes / Helm (example)
Locked in — this page follows this choice.

You are reading the Docker Compose version of this step. A participant on the Kubernetes path never sees it.

docker compose up -d

You are reading the Kubernetes / Helm version of this step. A participant on the Docker Compose path never sees it.

helm upgrade --install mylab ./chart

And a pathonly block, which has no counterpart on the other path and draws no tab strip at all — on the Docker Compose path there is simply nothing here:

Kubernetes path only. Your cluster needs a default StorageClass before you continue. Check with kubectl get storageclass. Nobody on the Docker Compose path is shown this paragraph, and no empty panel is left behind where it would have been.

Three things to notice while you are here:

  • The chooser is above the page title, not next to the blocks. Scroll up. That is where every participant makes the choice, which is why the guidance they need to choose has to sit in plain page content.
  • The padlock line and its Switch to … button are on this page only. Click through to another task and look above the title: nothing. That is the page-level declaration at work — a site-wide declaration would put that line on every page, which is what a real two-path workshop wants and what a guide like this one does not.
  • The rest of this page never changed. Only the two blocks above are gated; ungated content is unaffected by the choice.
Starting a new workshop from this template? You inherit nothing

This repo is cloned to start every new workshop, which is the whole reason the example lives in front matter. scripts/repoConfig.json declares no paths, so a fresh clone has the feature switched off — delete this page and the example goes with it.

Had the example been a site param instead, every new workshop would have started life with two paths called (example) and a padlock line on every page.


Editing existing blocks

Add a path to a workshop that already has two

  1. Append the new entry to deploymentPaths — append, don’t reorder.
  2. Add a matching pathtab to every pathtabs block in the repo. The build fails, by design, listing each block that is missing the new path.
  3. Review every pathonly block and every page with deploymentPath front matter: content that used to mean “not the other path” may now be wrong for the third one.
  4. Check anything that hardcodes your path list — scripts, workflows, generated handouts. Derive it from repoConfig.json instead.

Remove a path

Delete its entry from deploymentPaths first, then build: every stale path="…" becomes a build error naming the file, which is your to-do list. Delete the orphaned pathtab bodies and any pages scoped to that path.

Convert an existing tabs block into a gated one

Rename the wrapper tabs → pathtabs, rename each tab → pathtab, and replace title="…" on each tab with path="<key>". The tab titles now come from deploymentPaths, so they are consistent across every block in the workshop instead of being retyped per page.

Move content out of a block

The most common edit, and the most common mistake. Anything that is true for everyone — the explanation of why a step exists, a diagram, a link to reference material — should sit outside the path blocks. Content inside a block is invisible to participants on other paths and to anyone who has not chosen yet.

Rule of thumb: inside the block, only the commands and the words that differ. Everything else moves out.

Split a long page by path

If a page’s two variants have diverged past the point where tabs read well, promote each variant to its own page with deploymentPath front matter and let the menu and next/previous gating do the work. That is usually the right move for setup and prerequisite pages, which tend to diverge most.


What the build refuses

These are hard build failures, not warnings. Each one exists because the alternative is a silent gate that shows the wrong path’s steps — which nobody notices until a participant does.

The build fails whenWhy
pathtabs, pathtab or pathonly is used with no deploymentPaths in either placeThere is no path vocabulary to gate against, and no default to fall back on
A page declares deploymentPaths in front matter while the site param also existsTwo vocabularies behind one stored choice: the page’s key would be written into the slot every site-wide page reads, and those pages would show nothing while looking correct
A pathtabs block is missing one of the configured pathsA participant on the missing path would silently be shown another path’s steps
A pathtabs block defines the same path twiceAmbiguous — only one of them can win
A pathtab or pathonly body is emptyRenders an empty gated panel, which reads as “this path has nothing to do” rather than as the authoring mistake it is
path="…" names a key that is not in deploymentPathsTypo; the error lists the valid keys
A pathtab sits outside a pathtabs blockIts content would never be collected or gated
A pathonly is nested inside pathtabs/pathonlyThe enclosing block already restricts the path, so it is redundant or unreachable
A page’s deploymentPath names an unknown keyThe page would be hidden from every participant’s menu
A page combines deploymentPath with menuPageRef or menuUrlThose make the menu entry a crosslink, so the menu-hiding rule would match nothing and the page would stay visible to everyone with no error
A key does not start with a letter, or contains anything but letters, digits, hyphens and underscoresIt becomes part of a CSS class name, so anything else silently produces a selector that never matches

Every message names the offending file. Fix the file it names.


Gotchas worth knowing up front

  • Put decision-making information outside path blocks. If the prerequisites for each path live inside that path’s pathtab, a participant who has not chosen yet can see neither — the information they need to choose is hidden behind the choice.
  • If a page now opens with a notice, set description: in its front matter. Otherwise the site builds the page’s description, its link previews, and its search snippet out of that notice’s text.
  • A page scoped with deploymentPath is still reachable. Menu and next/previous gating cannot stop a bookmark or a shared link. The banner covers this — but it is another reason to keep an opening notice that points at the other path’s page.
  • Don’t hardcode your path list anywhere else. The deploymentPaths declaration is the single source. Scripts and workflows that need the list should read it from there, or they will quietly keep working with a stale list after you add a path. Printable handouts is a worked example of exactly this — its generator derives everything from this same declaration.
  • With a site-wide declaration the switch control is site-wide, not per page. A participant who picks wrongly on page one can fix it from any page — you do not need to repeat a chooser.
  • deploymentPath (singular) needs the site param. Scoping a whole page to one path gates the menu, the next/previous walk and search, all of which are site-wide; a page-level deploymentPaths declaration deliberately touches none of them.

Reference

  • Shortcode parameters and site params: CentralRepo README
  • A live workshop using all three mechanisms: ai-101 — see its 01Intro chapter for the chooser, the comparison written outside the blocks, and two path-scoped prerequisite pages
  • Printing a single path cleanly (the print-CSS gap this vocabulary feeds into): Task 7 — Printable Handouts

Task 7 - Printable Handouts

The problem this solves

Deployment paths show each participant only their own path in the browser: pathtabs renders one tab active, one hidden, and JavaScript swaps them on click. That is exactly what makes it useless on paper.

theme.css sets .tab-content { display: none } with .tab-content.active { display: block }. That is not a print-only rule — the print stylesheet only recolours tabs for the page, it does not un-hide anything. So a two-path page, printed or exported to PDF exactly as it renders, contains one path’s steps and zero indication the other path’s steps ever existed. No error, no missing-content notice — the page just looks complete. A participant who prints the Kubernetes path for offline reference silently gets a document with no helm commands, or vice versa.

The fix is not a print stylesheet — the tab that isn’t active isn’t in the printed DOM at all, so no @media print rule can recover it. The fix is a separate, generated page per path, flattened ahead of time so nothing needs to be hidden. That is what this tooling does.

Opt-in — nothing changes until you ask for it

The same rule as deployment paths itself: a workshop that declares no deploymentPaths gets no generated handouts, no extra CI time, and no content/09Reference/handouts/ directory. All four files below already sit in this repo, inert, as of this change — see How to opt in.

The four files

FileJob
scripts/gen_handouts.pyWalks content/ in site order and, per deployment path, writes one linear page to content/09Reference/handouts/ with every pathtabs/pathonly/tabs block flattened to that path’s body and the other path’s content dropped entirely — not collapsed, dropped, because collapsed is exactly the failure mode above. --check exits 1 if regenerating would change anything; --list-slugs prints the handout slugs and nothing else.
scripts/lint_paths.pyGuards the invariant the generator depends on: every path-specific instruction has to actually live inside a pathtabs/pathonly block (or a page-level deploymentPath) or the generator has no way to flatten it correctly. It imports its marker grammar and path vocabulary from gen_handouts.py rather than keeping a second copy. Its final check delegates to gen_handouts.py --check — a lint pass and a stale-handout are the same failure, reported together.
.github/workflows/path-lint.ymlRuns on every PR touching content: regenerates the handouts and fails loud if that produces a diff, then runs lint_paths.py — see the CI gate below. On a push to main (never on a PR — see below), the auto-fix commits and pushes the regenerated handouts instead of just failing.
.github/workflows/handout-pdf.ymlRuns a full Hugo build, serves it, and renders each handout to PDF with headless Chrome. PR-triggered so a real build failure (the actual incident this hardening was built after — a bad pathtabs block that passed lint but broke the Hugo build) is visible before merge, reusing the build step this workflow already ran, rather than adding a third copy of build-and-fail-on-error.

How to opt in

Declare deploymentPaths — site-wide in scripts/repoConfig.json, or page-scoped in a single page’s front matter — exactly as described in Step 1 of the deployment-paths guide. The moment a real path vocabulary exists:

  • gen_handouts.py starts producing pages under content/09Reference/handouts/, one per path.
  • lint_paths.py starts enforcing that every path-specific instruction sits inside a path block.
  • Both workflows above stop short-circuiting and start doing real work on every PR.

Nothing else changes. scripts/lint_paths.py’s own PATH_TITLE_RE, PATH_TOKENS and ALLOWLIST are empty in this repo’s copy — they carried ai-101’s docker/kubectl/helm vocabulary, which is that workshop’s content, not a generic default — so a repo that writes real path-specific lab content should repopulate them the way ai-101 did (see the comments at the top of scripts/lint_paths.py for the reuse recipe).

The CI gate

“Stale” means the committed pages under content/09Reference/handouts/ do not match what gen_handouts.py would write today — a source page changed and nobody re-ran the generator. --check (used by both lint_paths.py and handout-pdf.yml) catches this and fails loud, naming every changed and orphaned file.

path-lint.yml goes one step further, but only on a push to main, never on a pull request — before linting, it runs gen_handouts.py for real (not --check), and if that produces a diff, it commits and pushes the fix using a repo secret named HANDOUT_AUTOFIX_PAT — a fine-grained PAT (contents: write, scoped to that one repo), not the workflow’s own GITHUB_TOKEN. That distinction matters: a push made with GITHUB_TOKEN deliberately does not re-trigger other workflow runs, so a re-run against the corrected head wouldn’t happen automatically. The push is deliberately restricted to the push trigger: a pull_request run has just executed the PR head’s own (potentially untrusted) gen_handouts.py in the same job, and GitHub Actions runner state persists across steps in one job — combining that with a secret capable of pushing to the repo is a real credential-exfiltration risk, not a theoretical one. So a PR that needs its handouts regenerated has to be fixed locally (python3 scripts/gen_handouts.py + commit) or merged and left to self-heal on main afterward — it is never auto-fixed during review.

A repo without its own HANDOUT_AUTOFIX_PAT still gets the full safety guarantee — just not the auto-fix. The push step checks the secret is present (and that the trigger is push) before attempting anything; if either isn’t true, it reverts its own regeneration and leaves the stale files in place, and lint_paths.py fails with the same clear “handouts are stale, re-run gen_handouts.py” message it always has. Nothing pushes with an empty credential, nothing produces an opaque git-auth error — the mechanism degrades from self-healing to fail-loud-and-tell-you-how-to-fix-it, never to silently-wrong. This repo (UserRepo) does not provision the secret — there is nothing here to print — so this is the path every clone of this template actually exercises.

To get the auto-fix instead of just the fail-loud check, mint a fine-grained PAT the same way ai-101 did — contents: write, scoped to that one repo only — and store it as a repository secret named HANDOUT_AUTOFIX_PAT.

Live example

ai-101 is the one workshop currently using this end to end: deploymentPaths declared site-wide, real pathtabs content throughout, HANDOUT_AUTOFIX_PAT provisioned, and its own PATH_TITLE_RE/PATH_TOKENS/ALLOWLIST populated with its docker/kubectl/helm vocabulary. Its published handouts are the generator’s real output.

Reference

  • Deployment paths (the vocabulary this tooling reads): Task 6
  • A live workshop using the full mechanism: ai-101

Task 8 - Azure Lab Provisioning

The problem this solves

Workshops that need per-participant Azure accounts (a resource group, a portal sign-in, sometimes a VM) use the launchdemoform shortcode (CentralRepo) to trigger provisioning. Until now, that button POSTed to an Azure Automation webhook using mode:'no-cors' — the request went out, but the response was opaque by design, so the page could never show real status. Credentials arrived by a single Gmail-SMTP email, which routinely didn’t land in time for a live workshop.

launchdemoform now calls a real HTTP API (an Azure Durable Function, in fortinet-on-demand-labs-provisioning-and-tracking) that returns a real, readable response and a status endpoint the page polls. Participants see a progress bar and step name while provisioning runs, and credentials render directly on the page the moment they’re ready — email is now a secondary/audit channel, not the only delivery path.

Only add this shortcode where Azure provisioning is actually required

launchdemoform should appear on labs that use this repo’s Azure automation, and be removed from workshops that don’t need it — it has no effect and shows nothing useful otherwise. It requires a participant to have already checked in (the fortiuser/fortiemail cookies come from the site’s analytics check-in flow, unchanged by this feature).

Params

Shortcode params (unchanged by this rework):

ParamRequiredMeaning
lab (or labdefinition)YesThe lab definition to provision — matches a file name (minus .json) under lab-definitions/ in the backend repo, e.g. azure-102-odl.
debugNo, default falsetrue logs request/response/state-transition detail to the browser console.
customerNoOptional customer identifier passed through to the backend.
smartticketNoOptional SmartTicket reference passed through to the backend.

One new site param, in this repo’s scripts/repoConfig.json:

  "provisionApiBaseUrl": "https://<your-function-app>.azurewebsites.net"

Point it at the deployed Function App from the backend repo. Leave it unset until that Function is actually deployed and reachable — with no value, the shortcode renders normally (the button is still gated on check-in as before) but a click shows a clear “Provisioning is not configured for this workshop yet” message instead of failing against a URL that doesn’t exist.

Usage

{{< launchdemoform lab="azure-102-odl" >}}

Live example

What you’ll see here depends on your check-in/config state

This repo’s own scripts/repoConfig.json intentionally has no provisionApiBaseUrl set — the example below is live, but clicking it (once you’ve checked in) demonstrates the “not configured” state rather than a real provisioning run. A workshop with the site param set and a real backend behaves as described in What the participant sees below.

Provision your Portal Accounts
Required for hands-on portion of this workshop

What the participant sees

  1. Before checking in: the button is disabled with a message to check in first (unchanged behavior).
  2. Idle, checked in, no attempt yet, no reusable credential found: button reads “Provision Accounts” and is clickable.
  3. Idle, checked in, and a valid credential from an earlier workshop exists — either the same lab-definition, or a different one tagged into the same series (see Gotchas below) — the plain button is replaced by a choice: “We found existing credentials from {workshop}, issued {date}” with two buttons:
    • Reuse These Credentials — renders that credential immediately (username, TAP, resource group, a “Reused from” line naming the original workshop) with no new provisioning call at all.
    • Provision New For This Workshop — proceeds with the normal flow below as if no prior credential existed. This choice is remembered only for this page’s site+lab, not the whole series — a different workshop page later in the same series still offers the reuse choice fresh.
  4. In flight: the button disables (permanently, for this lab, until a terminal state), a progress bar fills, and a step label updates every few seconds — e.g. “Creating AD user — 20%”, “Assigning role — 60%”.
  5. On success: a credential card renders in place — username, sign-in info, resource group, expiry, and an “Open Azure Portal” link — and the button reads “Already Provisioned” and stays disabled.
  6. On failure: the button re-enables as “Retry Provisioning” and the status line shows the participant-safe failure reason from the backend.
  7. Reload, close the tab, or restart the browser — the page always reads its stored state first: it resumes polling an in-flight attempt, or renders the stored credential card directly, before ever showing the plain “Provision Accounts” button. State is localStorage, not session-only — it survives tab/browser closure by design, since a provisioned credential is only valid for the lab’s own duration regardless of how long a browser stays open. Once that duration has passed, the credential card still renders (as a record of what was issued) but shows an inline “access window has ended” warning instead of presenting a dead credential as still good.

Gotchas

  • Attempt state is scoped per Hugo site and per lab, not just per site. Two launchdemoform shortcodes with different lab values on the same or different pages of the same workshop track independent attempts; the same lab value anywhere on the site shares one lock. Scoping is keyed on the browser’s localStorage, prefixed with the site’s own base URL — a participant working two different workshop sites in the same browser never sees one site’s credentials or lock state on another.
  • The client-side single-attempt lock is a UX convenience, not the source of truth. The backend’s own idempotency (a deterministic key over participant email + lab definition + source site) is what actually prevents double-provisioning, and it has no expiry of its own — this matters if you’re debugging what looks like a stuck lock, or wondering why a participant can’t start a genuinely new attempt after their lab access has ended: clearing localStorage only resets what this browser remembers, not what the backend already did. Getting a real new attempt for the same email+lab+site is a backend-side question, out of scope for anything client-side.
  • Retry only appears after a hard Failed status from the backend, never just because a poll is slow or a tab was closed mid-provisioning. If a participant reports “it’s been stuck at X% forever,” that’s a backend-side question (check the Function’s Application Insights traces via the token), not a reason to expect a retry button to appear.
  • A missing or unreachable provisionApiBaseUrl fails safely. No value configured → the shortcode never attempts a network call and shows the “not configured” message on click. A value that’s set but wrong (typo, backend down) surfaces as a normal Failed attempt with a generic network-error reason, and the retry button appears once the URL is fixed.
  • Linking two workshops into a reusable credential “series” is a lab-definition change, not a shortcode param. A participant who already completed one workshop can be offered their existing credential on a later one (see What the participant sees, step 3) when either (a) both workshops’ launchdemoform calls use the literal same lab value (nothing to configure — this is how the K8s 101/201/202 trio works today), or (b) their lab-definitions are genuinely different but both set the same series string in the lab-definition JSON (in lab-definitions/ in the backend repo — not this repo). There’s no shortcode-side setup: once the backend lab-definitions share a series (or are the same lab), the reuse choice appears automatically. Only identity (the AD user/TAP) is reused across a series — a linked workshop’s own resource groups/storage/VM still provision fresh if it needs them.
  • This shortcode has no default backend URL any more. The previous version silently posted to a hardcoded Azure Automation webhook if site.Params.webhookUrl wasn’t set — and due to a separate bug, that site param was never actually wired into hugo.toml by generate_toml.py/hugo.jinja, so every real build used the hardcoded default regardless of what a repo configured. provisionApiBaseUrl is wired through the schema, the Jinja template, and hugo.toml generation, and has no hardcoded fallback — an unset value is a deliberate, visible “not configured” state instead of a silent default.

Admin: logs, credential recovery, and troubleshooting

Azure Portal access required

Everything below is in the Internal-Training subscription, resource group CloudCSE-WorkshopAccountProvisioning. You need at least Reader on that resource group to follow along.

The backend: Function App func-cse-lab-provisioning (Python, Durable Functions, EP1 plan). Search for that exact name in the Azure Portal to get to everything else — its Function App blade links to its Application Insights resource and its own Durable Functions instance list.

Every request is logged and correlatable by the orchestration instance ID — which is also the token the shortcode polls GET /api/provision/status/{token} with, so it’s visible in the participant’s browser dev tools (Network tab) or console (debug=true shortcode param logs it directly). All logs land in Application Insights appi-cse-lab-provisioning (backed by Log Analytics workspace appi-cse-lab-provisioning-law, 30-day retention). From the Function App blade → Application Insights → Logs, run:

traces
| where customDimensions.instance_id == "<token>"
| order by timestamp asc

This reconstructs the entire run for one participant — every step name, percent, and (on failure) the real exception with a stack trace, not just the generic participant-safe failure message the page shows. log_failure writes failures to traces/exceptions at error level; a stuck or failed provisioning is almost always diagnosable from this one query.

Finding a participant’s instance ID when you don’t have it (they lost the tab, the email never arrived, and they can’t give you the token): the orchestration’s stored input includes their email, but email is never logged into customDimensions directly, so traces can’t be full-text-searched by email. Instead use the Durable Functions instance list — either the Function App’s own Durable Functions instances view in the Portal, or the management REST API (.../runtime/webhooks/durabletask/instances?showInput=true&showOutput=true, auth code from the Function App’s host keys) — and filter the results for input.email. This lists every orchestration regardless of terminal state, which is also the fastest way to answer “is this participant’s attempt actually stuck, or did it fail/succeed and they just didn’t see it.”

Fastest path: check the Training Credentials channel in the Cloud Architects Team first. Every successful provisioning posts a formatted card — workshop name, participant email, username, password/TAP — to that Teams channel via a Power Automate webhook, at the same moment the participant’s credential email goes out. This is the intended first stop for “a participant says they never got their credentials” — faster than any of the lookups below, since it needs no instance ID and no Portal access.

The webhook posts through a Power Automate flow (fd0bf968c0df4299aa09ab673de41b04, same Power Platform environment as ManageTrainingUser.ps1’s original webhook) — open it in Power Automate if the destination ever needs to change.

This only covers labs that reach the send_email step successfully — a Failed attempt never posts (use the instance-ID/Application Insights lookups below for those), and the notification itself is best-effort: a webhook failure is logged (step: teams_webhook) but never blocks credential delivery or rolls back provisioning, so occasionally you’ll need to fall back to the lookups below even for a Completed attempt.

Recovering an already-issued credential depends on which lab path ran:

  • Bastion + VM labs (10 of 11 lab definitions): the VM’s local-admin password is a durable Key Vault secret, vm-password-<resource-group-name>, in the dedicated vault kv-cse-wksp-provision. Look up the resource group name from the orchestration’s output (same instance lookup as above), then read that secret directly — no time limit, it doesn’t expire or get purged.
  • AD user / Temporary Access Pass labs (azure-102-odl and similar): the TAP is not stored anywhere durable on purpose — it’s a Microsoft Entra credential, emailed once and returned once in the orchestration’s output. The orchestration’s output (via the same instance-ID lookup, show_output=true) still has the original TAP value as long as the Durable Task history hasn’t been purged — nothing currently purges it (no retention policy configured on the ProvisioningTaskHub storage), so in practice this works well past the 30-day Application Insights window. If the TAP has since expired (they’re short-lived) or truly can’t be found, don’t try to reuse it — issue a fresh one for that user directly in Entra ID (Users → the participant’s AD user → Authentication methods → Temporary Access Pass). The AD user itself persists across labs; only the TAP is single-use/time-boxed.

Checking whether a participant is rate-limited or “stuck” on a stale lock: the dedup/rate-limit state lives in a Durable Entity (ProvisionEntity), one instance per email:<lowercased-email> and one per dedup_key (email + lab + source site). Query entity state the same way as orchestration status (Durable Functions instance/entity list, entity ID @ProvisionEntity@email:<email> or @ProvisionEntity@<dedup_key>) if a participant reports the button won’t let them retry — this tells you whether the backend genuinely still considers them in-flight/already-completed, independent of whatever the browser’s localStorage shows.

Client-side state is not authoritative

Clearing a participant’s browser localStorage (or having them use a different browser) never unblocks a real backend-side lock or rate limit — it only resets what their browser remembers. Always confirm against the Durable Entity state above before telling a participant to “just try a different browser.”

Tracing a reused credential (a participant says “it gave me someone else’s/an old workshop’s credentials”): verify via the backend, never by guessing from what the page shows.

  1. Fastest check — the orchestration’s own output. Find the participant’s instance ID the same way as above (Durable Functions instance list filtered on input.email), then read that instance’s output. If the credential was reused, the output has a reusedFrom field naming the lab-definition the identity/TAP originally came from. No reusedFrom field means that specific attempt provisioned fresh — the participant’s complaint is about a different attempt, or a client-side display bug, not the reuse mechanism.
  2. Full per-participant history — every lab that email has ever completed, reused or not — lives in a separate Durable Entity, one instance per email, entity ID history:<sha256 of the lowercased, trimmed email> (hashed rather than the raw email — a read_entity_state HTTP lookup on a key containing @ doesn’t resolve; see this repo’s backend CLAUDE.md if you’re touching this code). Compute the key with:
    python3 -c "import hashlib; e='participant@example.com'.strip().lower(); print('history:' + hashlib.sha256(e.encode()).hexdigest())"
    Then look that entity up the same way as ProvisionEntity above (Durable Functions instance/entity list, @ProvisionEntity@history:<hash>). Its state is a list of every completion for that email — lab_definition_name, series, user_principal_name, completed_at, lab_duration_days — the authoritative record of what that participant actually has and where each credential came from.
  3. “It gave me an old workshop’s credentials” almost always means the reuse was correct and expected — the participant is confusing “the same account, still valid, from an earlier session today” with “wrong account.” Confirm by checking completed_at + lab_duration_days on the matching history entry actually covers now (an expired-but-still-Completed entry is never offered for reuse server-side — see the backend README’s “Credential reuse” section — so if they’re seeing stale credentials from outside that window, that’s a genuine bug, not the intended behavior, and worth escalating with the instance ID from step 1).

Reference

Ch 3 - Hugo Publish

Publish your content to GitHub pages via a GitHub action

Now that you have your content first draft, push your repo to gitHub, there’s already a GitHub action to automatically publish your content anytime you upload to the repo

Click the right arrow to go through each step individually

Subsections of Ch 3: Publish

Task 1 - Push content to your repo

Push your content to GitHubo repo

  1. When you’re satisfied with the look and feel of your workshop guide locally, from your local workstation CLI, push the newly created Hugo site up to GitHub to automatically publish your Hugo Site

      git add .
      git commit -m "<my commit message>"
      git push 
  2. Remember we’re always working in a Git Branch, so you should get in the habit of issuing a Pull request and merge using our GitFlow procedure

Info

This is mostly applicable when working in a collaborative environment where multiple people may be pushing to the repo with different branches/PR to main. Strictly speaking, if you’re the only person working on this repo and/or it’s your first push, this step isn’t 100% necessary

          # locally checkout the main branch
      git checkout main
          # pull the latest version of main from GitHub to your local repo 
      git pull
          # locally checkout your feature branch
      git checkout <branch>
          # locally perform an interactive rebase which locally pull commits from main into my branch
      git rebase main -i 
          # push my local branch (which now includes the latest changes from GH main) up to GitHub remote
      git push --force

      ########### WAIT FOR PR APPROVAL
  1. Create a PR on GitHub, being sure to select your branch to merge with main. Wait for approval

    {{

    PRScreenshot
    }} {{
    PRmergeblock
    }}

    • Once your PR is approved, checkout the main branch and perform a fast-forward merge and force push to complete the workflow.

            # locally checkout the main branch
        git checkout main
            # locally merge myFeatureBranch into main with a fast-forward merge scheme
        git merge <feature branch name> --ff-only
            # push local main (which now has myFeatureBranch merged into it) up to GitHub remote  
            # because this push includes the merge it will auto close the PullRequest
        git push
  2. After you push to main, the PR auto closes and the change to main triggers GitHub actions which automatically build and publish your workshop to GitHub Pages

GitHub Action to Auto Publish

  • The file workflows/static.yaml is already included in your repo and triggers a GitHub Action to build and publish your Hugo site every time you push content to GitHub.
  • Action:
    • Build a Hugo container with all of our customizations
    • Issue a Hugo Build command to create static HTML site
    • Publish resulting HTML to GitHub Pages for your repo
  • You can see action progress and errors in the Actions Tab on your repo

Git Flow

GitHub Repo Getting Started (General Workflow for GitHub Repos)

  1. Once your repo and pipeline have been created, you will be provided with the GitHub repo link which you can use to clone and begin content creation. First, navigate to a desired local directory and clone the repo with the provided link:

      cd <desired parent directory>
      git clone <provided link>
      cd <cloned repo directory>
  2. Create a feature branch to begin working on your desired changes.

      git checkout -b <FEATURE-username-ShortDescr>
  3. Check the repo status to verify the changes to be staged.

      git status
  4. When you have changes, Stage the desired files (or issue -A (or .) for all), commit, and push.

      git add -A {or} git add .
      git commit -m "<add a commit message here>"
      git push
    • If this is your first push to the branch, GitHub upstream doesn’t know about it. Just go ahead and use the provided command in this case to perform the push, which will create the upstream branch

      • To auto create new branches when you first push, update Git global config

         git config --global --add --bool push.autoSetupRemote true
    • Tip: If you have a number of small commits and don’t want them and their associated commit messages polluting the git log, you can squash your commits by performing a soft reset:

         git reset --soft <hash of the last commit you want to keep as is>
         git add -A
         git commit -m "<new commit message>"
         git log
      • You will see the new commit on top of the one you referenced in the git reset command.
  5. When you have completed your work and are ready to merge your changes into the main branch, ensure your branch is up-to-date with the main branch.

        # locally checkout the main branch
      git checkout main
        # pull the latest version of main from GitHub to your local repo 
      git pull
        # locally checkout your feature branch
      git checkout <branch>
        # locally perform an interactive rebase which locally pulls commits from main into my branch
      git rebase main -i 
        # push my local branch (which now includes the latest changes from GH main) up to GitHub remote
      git push --force
    • Create a PR on GitHub, being sure to select your branch to merge with main. Wait for approval

      PRScreenshot PRScreenshot

      • You will not be able to merge the PR until receiving approval from Jeff or Rob PRmergeblock PRmergeblock
  • Once your PR is approved, checkout the main branch and perform a fast-forward merge and force push to complete the workflow.

      # locally checkout the main branch
      git checkout main
      # locally merge myFeatureBranch into main with a fast-forward merge scheme
      git merge <feature branch name> --ff-only
      # push local main (which now has myFeatureBranch merged into it) up to GitHub remote  
      # because this push includes the merge it will auto close the PullRequest
      git push
  1. Branch cleanup - generally you can reuse your branch while actively developing. If you want to close your branch, use the following commands
      # delete the branch locally 
      git branch -D feature-branch
      # tell GH remote about branch deletion
      git push origin --delete feature-branch

Manual Hugo Build

Hugo Build

When you’re satisfied with Hugo view of your content in Hugo virtual server, issue a Hugo ‘build’ in the container CLI

    hugo --minify --cleanDestinationDir
  • This command “builds” your Hugo site into the container’s /public folder. We used a docker disk mount to map this folder back to your local /docs folder, so the Hugo website will automatically be copied back into your local repo
  • flag ‘–cleanDestinationDir’ tells hugo to re-write the entire output directory with its build, so it will clear out template files/anything else that may be in there
  • You can now exit the container with ctrl + cd, or command: ’exit’
  • When you exit the container, any files stored or changes you made to the container will be lost and cannot be recovered
    • Remember we edited the /content folder on our local OS, so those changes were not made to the container and will not be lost
    • Further, the disk mount from local’s /docs to Container’s public AUTOMATICALLY writes the hugo build to your local OS, so those changes will not be lost
    • If you need to continue editing, just run a new container from your built image, and run hugo’s webserver. Everything is linked properly so it should just work

Ch 4 - Appendix

Subsections of Ch 4: Appendix

Updating the Jenkins Personal Access Token

The Jenkins PAT

The Jenkins personal access token is used by Jenkins to retrieve Git repo information and send status checks during pipeline runs.

To generate a new token, navigate a web browser to the team Github, and click the {{

gh-logo
}} at the top right of the screen.

{{

UserIconClick
}}

Click ‘Settings’ in the dropdown.

{{

SettingsClick
}}

At the bottom left of the Settings page, click ‘Developer Settings.’

{{

DevSettingsClick
}}

Click the ‘Personal access tokens’ dropdown, and click ‘Tokens (classic)’.

{{

PATClick
}}

Generating a New Token

To generate a new token, follow these steps. If you just need to regenerate an existing token, skip down to the following section.

To generate a new token, click the ‘Generate new token’ dropdown near the top right of the Personal access tokens (classic) screen. In the dropdown, click ‘Generate new token (classic).’

{{

GNTDropdown
}}

You will be prompted to enter your FortiAuthenticator TOTP code. Enter it, and on the New personal access token (classic) page, ensure the following permissions are checked:

  • repo and all of its sub-options:

    {{

    RepoPerms
    }}

  • admin:repo_hook and all of its sub-options:

    {{

    AdminPerms
    }}

Choose an expiration date, optionally add a note, and click {{

gen-tkn
}} at the bottom of the screen.

Note: When the token appears on the next screen, ensure you copy and paste it somewhere where you can retrieve it before navigating away from the screen or closing the tab. Once you do, you will not be able to retrieve the token again and will need to generate another.

Regenerating an Existing Token

To regenerate an existing token, from the Personal access tokens (classic) screen, click the name of the token. For example:

{{

PATTokenExample
}}

On the following screen, ensure the requisite permissions are selected, and click ‘Regenerate token’, and copy the token someplace handy where you can retrieve it later.

{{

RegenToken
}}

Updating Jenkins

Navigate to the FortinetCloudCSE Jenkins server, and login with your credentials. To update a token, you’ll need admin permissions.

{{

Jenkins-login
}}

After logging in, click ‘Manage Jenkins’ on the left hand side of the screen.

{{

ManageJenkins
}}

On the Manage Jenkins screen, under the Security heading, click ‘Credentials’.

{{

ClickCredsJenkins
}}

On the Credentials page, click the name of the token you want to update. Then, click ‘Update’ on the left menu.

{{

UpdateToken
}}

Click ‘Change Password’, paste in the new token, and click {{

jenk-sv-btn
}}.

Confirm the token works

Click the ‘Manage Jenkins’ breadcrumb at the top of the screen.

{{

MJBreadcrumb
}}

Click ‘System’ under the System Configuration heading on the Manage Jenkins page.

{{

MJSystem
}}

Scroll down to the Github section towards the center of the page.

{{

GithubServers
}}

Select the credential that references the token you just updated, and click {{

test-conn-btn
}}.

If the token is valid and working, you should see a message appear such as Credentials verified for user… as in the image below.

{{

CredsVer
}}

Ch 1 Getting Started with Repos, Containers, Go Utility, and Hugo

Setting up your Hugo environment (15 min)

Pre-requisites

  • Docker - older installs won’t work, so if you need to upgrade/reinstall
    • You can use your docker flavor of choice. Keep in mind Docker Desktop is no longer free for enterprise use.
    • We’ve tested Rancher Desktop, which works well. Caveat…your locally rendered version of the site may not update in real time as you modify content, so you may need to restart the server to see new content
  • GitHub keys
    • Follow the instructions here to generate a new SSH key pair
      Warning

      DO NOT USE A PASSPHRASE when you create the keys

    • Once created follow these directions to add the newly created Key to your GitHub account.

What is Hugo and how easy can this be?

  • The site you are viewing right now is built with Hugo.
  • You can navigate to specific chapters and tasks with the Left Navbar or the top banner table of contents
    • For a sequential step by step flow through this workshop, use the arrows in the upper right corner to go through each step individually

Development Environment Options

  • To start, you’ll request a new repo which is a clone of User Repo.
    • you’ll create the content for your TECWorkshop guides in this repo, and ultimately publish the Hugo built website to GitHub Pages
  • Once you have UserRepo, you can choose how to use Hugo
  1. Option 1: build a container with Hugo installed on it and a copy of all Fortinet specific customizations to the Hugo reLearn theme
    • Beyond providing an opportunity to learn the basics of container development, this option:
      • streamlines and simplifies the Hugo content creation process
      • minimizes local storage/upkeep of reusable componentry
      • reduces complications of version dependencies in development environment for Hugo or the reLearn theme
      • future-proofs the content created for any given TECWorkshop so that any Fortinet branding changes can be easily re-applied to all guides
  2. Option 2: Install Hugo locally on your laptop/workstation and clone CentralRepo
    • You’ll have to
      • maintain CentralRepo including submodules on your local workstation
      • ensure your final site is published to the /docs folder in your UserRepo

Subsections of Legacy Setup - OLD

Task 1 - Repo Creation

Repo creation request

Request a new repo for your TECWorkshop (this will be referred to as your UserRepo)

  • Send an email to fortinetcloudcse@fortinet.com to request a new GitHub repo and Jenkins Pipeline. Providing the following:
    • Repo Name
    • GitHub Usernames of collaborators
    • Your Fortinet email address that you use to log in to FortiCloud
  • Behind the scenes, a script is used to create your TECWorkshop repo with appropriate protections, features, and collaborators. Additionally a Jenkins pipeline will be setup to monitor changes to the repo and run things like
    • publishing the website
    • FortiDevSec scanning. To facilitate this, administrators will send you an application id that you must paste into fdevsec.yaml in order for the scans to run and tests to pass. Jenkins tests must pass in order for your feature branch to be merged into the main branch.
      • Along with the FortiDevSec application id, you will also be sent an account number which you can use to navigate to the FortiDevSec console to view the results of your scan. To do so, head to FortiCloud and click IAM Login on the left hand side of the page. Enter the provided account number along with your FortiCloud login credentials. {{
        forticloud-iam-login
        }}
  • You will use this repo to create and modify MD chapters & tasks to create your workshop Guide in Hugo format.

Repo Restrictions, Jenkins interactions, GitHub Actions

  • The only approved method to create repos in FortinetCloudCSE org is via request to mailto:fortinetcloudcse@fortinet.com
  • Only Authorized collaborators and admins are allowed to push to repos in FortinetCloudCSE org
    • We require pushes be made to a feature branch
  • Upon push to feature branch:
    • Jenkins clones the repo into its workspace on EC2 and runs FortiDevSec Scan which performs
      • SAST Scan
      • check for vulnerabilities in code, 3rd party libraries, and libraries pulled into Dockerfile
      • secrets scan
      • IaC Scan for misconfig
    • GitHub Pages deploys only on the main branch.
    • Requirements for merging feature to main branch:
      • FortiDevSec Tests Pass
      • PR request submitted and approved by FortiCloudCSE admins (manual intervention)

Git repo setup

  • Once your TECWorkshop repo is created, clone the repo and change your working directory to the cloned repo

        git clone <provided link>
        cd <cloned repo directory>
  • The first thing you’ll want to do with the repo is create a Feature branch. There are branch protections in place on the repo preventing you from pushing to main, so you’ll have to follow our workflow described in Ch3

      git checkout -b Feature-<userid>-<shortDescr>
  • Full GitFlow & additional tips are available here



MVP0 (LEGACY STEPS only do this if MVP1 steps don’t work)

Info

You won’t be able to clone this repo into the FortinetCloudCSE Org, so using this route, you’ll have to clone to your own repo

Step 1 Clone this git repo
git clone https://github.com/FortinetCloudCSE/UserRepo.git

Task 2 - Container build

Task 2 Build a FortinetHugo container

  • You only need to build the container when you’re starting development

    • Container rebuild is necessary when:
      • CentralRepo has changed
      • You removed/deleted the previously built container
  • Once Built, you can re-run the container whenever you wish to keep creating content and reviewing your Hugo site

    Warning

    You can choose your own container name and it MUST be lowercase only . Our example uses fortinet-hugo

    Tip

    The Full commands and explanation for building and running docker are listed below. We’ve also included a shell script in this repo to perform the build

        ./scripts/docker_build.sh [prod | dev]
    • use prod (Production) Container for everyday usage
    • use dev (Development) Container for testing changes to CentralRepo or other background items

        docker build -t fortinet-hugo  . --target=prod
    Info

    If you get build errors, check you’re on a recent version of docker and upgrade if necessary

    • The container image is a point-in-time Ubuntu OS including a Hugo installation and a copy of CentralRepo so your Hugo formatting/themes/branding will always be up-to-date
      • CentralRepo contains necessary files, directories, and Fortinet-specific customizations to configure Hugo, it won’t change often
    • Command Line arguments (You can view our Docker build file here)
      • ‘-t’: container_image_name, must be lowercase
      • ‘.’: build the container in this folder
      • ‘–target=prod’: Prod is for general usage. We’re using a single Dockerfile for both dev and prod container images. Dev is used for testing changes to CentralRepo.
    • If you would prefer local Hugo install/development follow these directions
    • Container advantages:
      • no need to install/maintain Hugo locally
      • no need to clone/maintain Hugo “supporting files/directories”….your repo will be much larger and will get out-of-date quickly
      • same container can be used to preview and build EVERY TECWorkshop, and you could build/move it anywhere
      • no need to rename/modify Hugo’s public folder after builds

Task 3 - Run Container

Task 3: Run FortinetHugo container

  • Run the container with local disk mounts so you can review your markdown TECWorkshop Guide as you’re creating the content (repeat this procedure for any TECWorkshop you’re creating)
Tip: Simple container run

We’ve included a super simple shell script to run your container image with arguments

    ./scripts/docker_run.sh server [prod | dev]

Choose your CLI argument depending on what you want Hugo to do:

  • server argument to run Hugo’s interactive web server
  • shell to access the container CLI
  • build to perform a Hugo build
  • generate_toml to generate hugo.toml from parameters in scripts/repoConfig.json
  • update_scripts to update scripts to latest features
  • prod to use the Production container (for everyday usage with latest released container)
  • dev to use the Development container (for testing changes to Central Repo or other background items)
Full Container run command
  docker run --rm -it \
  -v $(pwd):/home/UserRepo \
  --mount type=bind,source=$(pwd)/hugo.toml,target=/home/CentralRepo/hugo.toml \
  -p 1313:1313 fortinet-hugo:latest server
  • ‘-rm’ flag removes the container after it exits…freeing up resources

  • ‘-it’ flag runs the container interactively providing prompt into the Container

  • ‘-v’ flag creates a disk mount to the local file system available within the container OS

  • ‘-p’ publishes container ports to the local OS (used to view the local Hugo Webserver)

  • CLI Options:

    • ‘shell’ subcommand for Hugo container to access the shell directly
    • ‘server’ subcommand for Hugo container to run the local/test webserver
    • ‘build’ subcommand for Hugo container to build the static website, this is the default
  • the ‘shell argument runs the container and logs you into the container Alpine Linux OS CLI (general Linux commands will work, but this is a lean image so doesn’t include everything)

    • Note the $(pwd) in Container OS, and list files. You’ll see you have everything needed to create your Hugo site!

      pwd
      ls -la 
Note

Notice the folders from your local repo are available in the container, from the disk mounts

  • /content
  • /layouts
  • hugo.toml

These disk mounts allow bidirectional read/write between container and local file system, and these disk mounts are the only directories that will be maintained when the container shuts down

  • Within the container shell, you can Run Hugo virtual server to get a live view of Hugo’s output

    hugo server --contentDir /home/UserRepo/content --bind=0.0.0.0

    In your local machine, browse to http://localhost:1313/UserRepo

    You’ll see a template hugo site served by Hugo’s local webserver. Now you’re ready to proceed building your TECWorkshop content in the next chapter!

  • To exit out of the Container OS: use ’exit’ or CTRL+d

    exit

Tip

We’re including some helpful docker commands here for reference. Use these if you’ve built LOTS of images and you need to get rid of the mess

Notes:

  • Inside the container, Central Repo (which is where we’ll make any template changes) is cloned and integrated with your repo.

  • Container (ideally) displays local version of Hugo site updating near real time as you create content

  • To run a container interactively (for troubleshooting or to see how they function)

    • Comment out any offending lines in the dockerfile
    • Build again using commands above.
    • to run the container interactively use the ‘-it’ flag:
      docker run --rm -it fortinet-hugo:latest
  • Container outputs /public folder which is the result from “Hugo build”

    • This /public(/docs) folder can be hosted anywhere. We’ll still use GH Pages to host the actual page.

Useful Docker Commands to Know

docker images                                           #List all images
docker ps -a                                            #List all containers, both running and stopped
docker rmi <image-id>                                   #Remove an image
docker rmi $(docker images -aq)                         #Remove all images
docker rmi $(docker images --filter dangling=true -aq)  #Remove all images with tag <none>
docker rm <container-id>                                #Remove a container
docker rm $(docker ps -aq)                              #Remove all containers
docker builder prune                                    #Remove build cache

When running any of the above commands, if you get an error message indicating an image or container is being used or referenced in another image or container, you can issue the ‘-f’ flag to force remove.

Upgrade Hugo - container and scripts Update

FYI - Upgrading Hugo and the container image

  • As a reminder, all the software we’re using in this CI/CD process is Open source, and there are active updates to Hugo and the reLearn theme periodically. As such, we may introduce new features and/or change how we work within this process.

  • To simplify the update process and not force you into the sausage making process, we have devised a scheme to update the container and scripts in each repo with minimal manual intervention.

  • This process is detailed below. Follow the tabs in order complete the upgrade (ON AN EXISTING REPO ONLY….this new process is built into any newly created repos)

    • Once you’ve done this conversion on an existing repo, you should periodically rebuild the container and run it with update_scripts to get any latest features

      Rebuild Old Container

      • Start in the root of your repo directory
      • Rebuild your hugo container with the command below.
        • This pulls the latest versions if our customized scripts into the existing/“older” container image (running older Hugo version), allowing us to copy some of them back to your repo directory.
      ./scripts/docker_build.sh

      Manually update Docker_run

      • Grab a copy of the latest docker run command scripts, located here
      • Once you have the latest file, overwrite the docker_run.sh script in your repo with the latest version
        • scripts/docker_run.sh
      Note

      This is a chicken-and-egg problem. We have everything we need in the refreshed container, but we can’t run any of the new features bc our docker_run script is limited to whatever was available when your repo was cloned.

      The new run command maps additional local directories to the container allowing us to make updates from the container in the future

      Use container script to update the rest of your local scripts

      Use the newly updated docker_run.sh script with update_scripts option to copy the latest scripts into your local environment/repo directory

      ./scripts/docker_run.sh update_scripts

      This command performs the following:

      • From the Container OS/file system, copy the following files into your local environment
        • docker_tester_run.sh –> scripts/docker_tester_run.sh
          • update the docker_tester_run script with latest command line options. The tester script is used for testing changes to CentralRepo and UserRepo before merging to main
        • docker_run.sh –> scripts/docker_run.sh
          • update the docker_run script with latest command line options
        • GitHub Action static.yaml –> .github/workflows/static.yaml
          • update GitHub action to reflect latest docker run commands
        • Dockerfile –> Dockerfile
          • update Dockerfile to use latest container image and any installed packages
        • IF THERE IS NO repoConfig.json file in scripts/
          • repoConfig.json –> scripts/repoConfig.json
          • We don’t overwrite this file if it’s already there :)
        • Echo ‘venv/’ to .gitignore
          • ignore Python virtual envs created during generate_toml
      Tip

      The most important update is to Dockerfile, which now uses a new base container image featuring rolling upgrades to Hugo. Our old container image was no longer actively supported, so it wasn’t getting the latest Hugo updates.

      All the rest of the updates are supporting cast to streamline our scripts, and to simplify future modifications as necessary.

      Build New Container

      Now that you have the latest Dockerfile (which directs your docker build command to use a new Container image with updated Hugo), we need to re-build the container again:

      ./scripts/docker_build.sh

      The previous container image we were using which included Hugo was no longer maintained, so we had to switch to a new one. We also install python on the container for use in the next step!

      Generate hugo.toml file

      • Due to the nature of Open source software, sometimes there are breaking changes. In this instance, Hugo is deprecating usage of the config.toml file in favor of a new file named Hugo.toml
      • Additionally, as part of the upgrade, we needed to modify some parameters in the file, so we took the opportunity to use a Jinaj2 template to generate the file for with proper parameters
        • This also allows us the ability to update the template in the future and re-generate hugo.toml as necessary
        • Rather than modify Hugo.toml directly, we will now maintain a JSON configuration file /scripts/repoConfig.json
      • Update scripts/repoConfig.json with parameters for your workshop
      • Run the container with generate_toml option
        • every time you run this command, a new hugo.toml file will be generated
      ./scripts/docker_run.sh generate_toml

      example of repoConfig.toml. Replace each value with specific parameters for your repo prior to using the generate_toml script:

      • repoName (your repo name from GitHub)
      • author
      • Workshop Title
      • themeVariant (options: [“Workshop”, “Demo”, “UseCase”, “Spotlight”, “Xperts2024”] )
      • logoBannerText (whatever you want in top left Menu under Fortinet Logo. Leaving this field blank will default to the themeVariant name)
      • logoBannerSub Text (optional sub-banner text)
      • marketingCode (optional marketing code to be used as default in analytics gathering and provisioning form)
      • Shortcuts (Helpful Resources Links in the left menu
      • DO NOT CHANGE THE FOLLOWING:
        • errorLevel
        • googleServicesID
      {
        "repoName":"UserRepo",
        "author":"CSE Employee",
        "workshopTitle":"Hugo for Fortinet TECWorkshops",
        "themeVariant":"Xperts-2024",
        "logoBannerText":"",
        "logoBannerSubText":"",
        "errorLevel":"warning",
        "googleServicesID":"G-5RZBH288ST",
        "marketingCode": "MGO12345",
        "shortcuts": [
        {
          "text": "Fortinet Cloud CSE GitHub Org",
          "URL": "https://github.com/FortinetCloudCSE",
          "icon": "fa-graduation-cap",
          "weight":10
        },
        {
          "text": "Fortinet Standard Workshop Guide",
          "URL": "https://fortinetcloudcse.github.io/UserRepo",
          "icon": "fa-tools",
          "weight":20
        },
        {
          "text": "Fortinet Standard Workshop Template Repo",
          "URL": "https://github.com/FortinetCloudCSE/UserRepo",
          "icon": "fa-tools",
          "weight":30
        },
        {
          "text": "Fortinet Hugo reLearn theme - Guide",
          "URL": "https://mcshelby.github.io/hugo-theme-relearn/index.html",
          "icon": "fa-tools",
          "weight":40
        }
        ]
      }

      Run hugo server

      Congratulations! You’ve updated everything required to use the latest Hugo version. Now all you need to do is run the container with server command like normal:

      ./scripts/docker_run.sh server

Container Flow Visual

ContainerFlow.html

Optional - Install Hugo

Tip

Hugo is installed on the container so it’s best to use it there. These instructions are included for legacy learning purposes

Hugo Local install

  • If you’re using a Mac, run the following brew install from your terminal

    brew install hugo 

    If you’re using Windows, install chocolatey for windows (follow directions here). Once installed run the following

    choco install hugo -confirm
  • Run Hugo webserver locally to see a local version of the rendered website

    From within the root of the repo you copied onto your system

    hugo server
  • Click on the URL presented after the above command finishes to view a local version of your first Hugo formatted website {{

    hugoServer
    }} hugoServer hugoServer

Clone Central Repo if not using a container

  • Because you’re not using a container, you need to clone and maintain fresh copy of CentralRepo
        git clone https://github.com/FortinetCloudCSE/CentralRepo.git --recursive LocalCopyCentralRepo          
  • To pull updates later
        cd LocalCopyCentralRepo
        git pull -r    

Running Hugo locally w/ CentralRepo + UserRepo

  • Start in Central Repo, and use hugo webserver, pointing to proper content directory and config files

      cd LocalCopyCentralRepo 
      hugo server --contentDir $(PWD)/../UserRepo/content --config $(PWD)/../UserRepo/hugo.toml -p 8080
    • Flags:
      • ‘–contentDir’: tell Hugo where the /content folder is
      • ‘–config’: tell Hugo where the frontmatter config file is
      • ‘-p’: tell Hugo webserver what port to use
    • Hugo will serve up a local version of the page at: http://localhost:8080/UserRepo/
  • Now that you have Hugo running locally, you can proceed to content creation with Hugo

  • When you’re ready to perform a final ‘hugo build’ on your site, be sure to use the ‘-d’ flag to write files back into your UserRepo

  hugo --minify -d $(PWD)/../UserRepo/docs --contentDir $(PWD)/../UserRepo/content --config $(PWD)/../UserRepo/hugo.toml --cleanDestinationDir
Warning

The examples and sample code provided in this workshop are intended to be consumed as instructional content. These will help you understand how various Fortinet services can be architected to build a solution while demonstrating best practices along the way. These examples are not intended for use in production environments without full understanding of how they operate.

Background Procedures - CentralRepo Maintenance

CentralRepo

CentralRepo contains all of the stuff Hugo needs to build a static website, including Fortinet Customizations to themes.

How it’s used

  • Generally TEC Program participants creating content for Workshops, Demos, User Cases, or Spotlights don’t need to worry about Central Repo at all
  • When you build your container, it inherently grabs the latest copy of CentralRepo:main from github, via this command in Dockerfile:
      ADD https://github.com/FortinetCloudCSE/CentralRepo.git#main /home/CentralRepo
  • This means that you can re-use the same container for every TEC workshop guide you create.
    Tip

    If you haven’t used your container in a while, it’s a good idea to rebuild it so it will grab the latest version of CentralRepo

Repo Maintenance

  • CentralRepo is maintained by the Fortinet Cloud CSE team, so work with us to make any changes necessary.

  • Review, modification, and testing process:

    • Anyone can fork CentralRepo and modify as necessary.
    • Once you’ve tested your modifications, submit a PR to Central Repo
    • Fortinet Cloud CSE team will merge your PR changes into branch CentralRepo:prreviewJune23
    • Test using HugoDevContainer pointing to the merged branch rather than main
        ./scripts/docker_tester_build.sh
        ./scripts/docker_tester_run.sh
    Warning

    IMPORTANT If there is collaborative work while testing a PR, be sure to always pull latest from the PR Branch before starting new work!

         git checkout PRBranch
         git remote add <PR Label> git@github.com:FortinetCloudCSE/CentralRepo.git
         git pull prreviewJune23
    • Upon successful testing, Fortinet Cloud CSE team will merge the branch to main and close PR
        git checkout prreviewJune23
        git remote add <PR label> <remote Github ssh URL>
        git pull <PR label>
    
        <PERFORM TESTIGN ON CONTAINER, and make any changes as necessary on this branch>
    
        <UPON SUCCESSFUL TESTING>
        git checkout main
        git merge prreviewJune23 --ff-only
        git push 
    
        <Manually Close PR>