diff --git a/.devcontainer/features/k3s-on-host/devcontainer-feature.json b/.devcontainer/features/k3s-on-host/devcontainer-feature.json index 5ead9a6..1450138 100644 --- a/.devcontainer/features/k3s-on-host/devcontainer-feature.json +++ b/.devcontainer/features/k3s-on-host/devcontainer-feature.json @@ -39,11 +39,11 @@ ], "privileged": true, "containerEnv": { - "KUBECONFIG": "/k3s-on-host/k3s.devcontainer.yaml", + "KUBECONFIG": "/devfeature/k3s-on-host/k3s.devcontainer.yaml", "INSTALLDOCKERBUILDX": "false" }, "dependsOn":{ "ghcr.io/devcontainers/features/docker-outside-of-docker":{} }, - "updateContentCommand": "/k3s-on-host/updateContent.sh" + "updateContentCommand": "/devfeature/k3s-on-host/updateContent.sh" } \ No newline at end of file diff --git a/.devcontainer/features/k3s-on-host/install.sh b/.devcontainer/features/k3s-on-host/install.sh index 7854add..e697ece 100644 --- a/.devcontainer/features/k3s-on-host/install.sh +++ b/.devcontainer/features/k3s-on-host/install.sh @@ -10,6 +10,7 @@ K3S_VERSION="${K3SVERSION:-"latest"}" USE_CRI_DOCKERD="${CRIDOCKERD:-"true"}" HOST_INTERFACE_CONTAINER="host_interface" HOST_INTERFACE_CONTAINER_BASE="mcr.microsoft.com/devcontainers/base:ubuntu22.04" +CLUSTER_ENABLED="${CLUSTER_ENABLED:-"true"}" # Ensure apt is in non-interactive to avoid prompts export DEBIAN_FRONTEND=noninteractive @@ -20,7 +21,7 @@ set -e rm -rf /var/lib/apt/lists/* if [ "$(id -u)" -ne 0 ]; then - echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + echo -e 'Devcontainer feature requires root. Add "remoteuser":"root" to your devcontainer.json' exit 1 fi @@ -54,18 +55,18 @@ K3S_ARCHITECTURE=$ARCHITECTURE ############################################################ function build_dest_directory() { - echo "Creating /k3s-on-host directory..." - mkdir -p "/k3s-on-host" + echo "Creating /devfeature/k3s-on-host directory..." + mkdir -p "/devfeature/k3s-on-host" - mkdir -p "/host_var/tmp/k3s-on-host" + mkdir -p "/host_var/tmp/devfeature/k3s-on-host" - echo "Creating /k3s-on-host/.env file..." - tee /k3s-on-host/.env -a > /dev/null << UPDATE_END + echo "Creating /devfeature/k3s-on-host/.env file..." + tee /devfeature/k3s-on-host/.env -a > /dev/null << UPDATE_END export _REMOTE_USER=${_REMOTE_USER} export _REMOTE_USER_HOME=${_REMOTE_USER_HOME} export _CONTAINER_USER=${_CONTAINER_USER} +export CLUSTER_ENABLED=${CLUSTER_ENABLED} export K3S_VERSION=${K3S_VERSION} -export KUBECTL_VERSION=${KUBECTL_VERSION} export USE_CRI_DOCKERD=${USE_CRI_DOCKERD} export ARCHITECTURE=${ARCHITECTURE} export HOST_INTERFACE_CONTAINER=${HOST_INTERFACE_CONTAINER} @@ -74,14 +75,14 @@ export HOST_INTERFACE_CONTAINER_BASE=${HOST_INTERFACE_CONTAINER_BASE} UPDATE_END - echo "Copying scripts to /k3s-on-host/..." - cp ./*.sh /k3s-on-host/ + echo "Copying scripts to /devfeature/k3s-on-host/..." + cp ./*.sh /devfeature/k3s-on-host/ while read -r shellFile; do chmod +x ${shellFile} chmod 777 ${shellFile} - done < <(find "/k3s-on-host" -iname "*.sh") + done < <(find "/devfeature/k3s-on-host" -iname "*.sh") } diff --git a/.devcontainer/features/k3s-on-host/updateContent.sh b/.devcontainer/features/k3s-on-host/updateContent.sh index 27747ef..305912d 100755 --- a/.devcontainer/features/k3s-on-host/updateContent.sh +++ b/.devcontainer/features/k3s-on-host/updateContent.sh @@ -11,8 +11,12 @@ export DEBIAN_FRONTEND=noninteractive # Source /etc/os-release to get OS info source /etc/os-release -# Source our utilities script -source "/k3s-on-host/utils.sh" $@ +# Source the .env file so we can pull the options set by the user +source /devfeature/k3s-on-host/.env + +LOG_DIR="/var/log/devfeature/k3s-on-host" +SCRIPT_NAME=$(basename "$0") + exit_code=${PIPESTATUS[0]} @@ -21,6 +25,289 @@ if [[ $exit_code -gt 0 ]]; then return 1 fi + +############################################################ +# Helper function to run a script on the host via the host_interface container +# args +# position 1 : the command to run. i.e. "docker container ls" +# position 2 : the variable to return the results of the script to for further processing +# --ignore_error : allow the script to continue even if the return code is not 0 +# --disable_log : prevent the output from writing to the log and screen +############################################################ +function run_a_script_on_host() { + local timestamp=$(date "+%Y%m%d_%H%M%S") + if [[ "$#" -eq 0 ]]; then + exit_with_error "Missing run script to execute. Please use function like run_a_script 'ls /'" + fi + + local run_script="$1" + local __returnVar=$2 + RETURN_CODE="" + # We're passing flags and not a return value. Reset the return variable here + if [[ "${__returnVar:0:2}" == "--" ]]; then + __returnVar="" + fi + + local log_enabled=true + local ignore_error=false + local env_vars="" + local returnResult="" + + while [[ "$#" -gt 0 ]]; do + case $1 in + --ignore_error) + ignore_error=true + ;; + --disable_log) + log_enabled=false + ;; + --env) + shift + env_vars="${env_vars} --env $1" + ;; + esac + shift + done + + local run_cmd + + run_cmd="docker exec \ + ${env_vars} \ + -ti \ + $HOST_INTERFACE_CONTAINER \ + chroot /host bash -c \"${run_script}\"" + + + if [[ "${log_enabled}" == true ]]; then + trace_log "Running '${run_cmd}'..." + fi + + returnResult=$(eval "${run_cmd}" ) + + sub_exit_code=${PIPESTATUS[0]} + RETURN_CODE=${sub_exit_code} + if [[ -n ${__returnVar} ]]; then + eval $__returnVar="'$returnResult'" + fi + + if [[ "${log_enabled}" == true ]]; then + trace_log "...'${run_cmd}' Exit code: ${sub_exit_code}" + trace_log "...'${run_cmd}' Result: ${returnResult}" + fi + + if [[ "${ignore_error}" == true ]]; then + return + fi + + if [[ $sub_exit_code -gt 0 ]]; then + exit_with_error "Script failed. Received return code of '${sub_exit_code}'. Command ran: '${run_script}'. See previous errors and retry" + fi +} + + +############################################################ +# Helper function to run a script +# args +# position 1 : the command to run. i.e. "docker container ls" +# position 2 : the variable to return the results of the script to for further processing +# --ignore_error : allow the script to continue even if the return code is not 0 +# --disable_log : prevent the output from writing to the log and screen +############################################################ +function run_a_script() { + local timestamp=$(date "+%Y%m%d_%H%M%S") + if [[ "$#" -eq 0 ]]; then + exit_with_error "Missing run script to execute. Please use function like run_a_script 'ls /'" + fi + + local run_script="$1" + local __returnVar=$2 + RETURN_CODE="" + # We're passing flags and not a return value. Reset the return variable here + if [[ "${__returnVar:0:2}" == "--" ]]; then + __returnVar="" + fi + + local log_enabled=true + local ignore_error=false + local run_in_background=false + local returnResult="" + + while [[ "$#" -gt 0 ]]; do + case $1 in + --ignore_error) + ignore_error=true + ;; + --disable_log) + log_enabled=false + ;; + esac + shift + done + + local run_cmd + + run_cmd="${run_script}" + + + if [[ "${log_enabled}" == true ]]; then + trace_log "Running '${run_cmd}'..." + fi + + + returnResult=$(eval "${run_cmd}" ) + + sub_exit_code=${PIPESTATUS[0]} + RETURN_CODE=${sub_exit_code} + if [[ -n ${__returnVar} ]]; then + eval $__returnVar="'$returnResult'" + fi + + if [[ "${log_enabled}" == true ]]; then + trace_log "...'${run_cmd}' Exit code: ${sub_exit_code}" + trace_log "...'${run_cmd}' Result: ${returnResult}" + fi + + if [[ "${ignore_error}" == true ]]; then + return + fi + + if [[ $sub_exit_code -gt 0 ]]; then + exit_with_error "Script failed. Received return code of '${sub_exit_code}'. Command ran: '${run_script}'. See previous errors and retry" + fi +} + +############################################################ +# Reset the log file by renaming it with a timestamp and +# creating a new empty log file +############################################################ +function reset_log() { + local timestamp=$(date "+%Y%m%d_%H%M%S") + local logFile="${SCRIPT_NAME}.log" + + run_a_script "mkdir -p ${LOG_DIR}" --disable_log + + if [[ -f "${LOG_DIR}/${logFile}" ]]; then + run_a_script "mv ${LOG_DIR}/${logFile} ${LOG_DIR}/${logFile}.${timestamp}" --disable_log + fi + run_a_script "touch ${LOG_DIR}/${logFile}" --disable_log + run_a_script "chmod u=rw,g=rw,o=rw ${LOG_DIR}/${logFile}" --disable_log + + LOG_FILE="${LOG_DIR}/${logFile}" +} + +############################################################ +# Log a message to both stdout and the log file with a +# specified log level +############################################################ +function log() { + # log informational messages to stdout + local timestamp=$(date "+%Y-%m-%d %H:%M:%S") + local log_entry="${1}" + local received_log_level="INFO" + local full_log_entry="" + local log_raw=false + + if [[ -z ${log_entry} ]]; then + return + fi + + local configured_log_level=0 + case ${LOG_LEVEL^^} in + ERROR) + configured_log_level=4 + ;; + WARN) + configured_log_level=3 + ;; + INFO) + configured_log_level=2 + ;; + DEBUG) + configured_log_level=1 + ;; + *) + configured_log_level=0 + ;; + esac + + while [[ "$#" -gt 0 ]]; do + case $1 in + --info) + received_log_level="INFO" + received_log_level_int=2 + ;; + --debug) + received_log_level="DEBUG" + received_log_level_int=1 + ;; + --warn) + received_log_level="WARN" + received_log_level_int=3 + ;; + --error) + received_log_level="ERROR" + received_log_level_int=4 + ;; + --trace) + received_log_level="TRACE" + received_log_level_int=0 + ;; + --raw) + log_raw=true + ;; + esac + shift + done + + if [[ ${log_raw} == false ]]; then + full_log_entry="[${SCRIPT_NAME}] [${received_log_level}] ${timestamp}: ${log_entry}" + else + full_log_entry="${log_entry}" + fi + + # Our log level isn't high enough - don't write it to the screen + if [[ ${received_log_level_int} -lt ${configured_log_level} ]]; then + return + fi + + + if [[ -n "${LOG_FILE}" ]]; then + echo "${full_log_entry}" | tee -a "${LOG_FILE}" + fi +} + +# Log an informational message to stdout and the log file +function info_log() { + log "${1}" --info +} + +# Log a trace message to stdout and the log file +function trace_log() { + log "${1}" --trace +} + +# Log an debug message to stdout and the log file +function debug_log() { + log "${1}" --debug +} + +# Log an warning message to stdout and the log file +function warn_log() { + log "${1}" --warn +} + +# Log an error message to stdout and the log file +function error_log() { + log "${1}" --error +} + +# Log a critical error and exit the script with a non-zero return code +function exit_with_error() { + # log a message to stderr and exit 1 + error_log "${1}" + exit 1 +} + ############################################################ # Deploy container to interact with the host ############################################################ @@ -93,9 +380,9 @@ function install_k3s() { info_log "Latest k3s version is ${K3S_VERSION}" fi - if [[ ! -f "/host_var/tmp/k3s-on-host/k3s_install.sh" ]]; then + if [[ ! -f "/host_var/tmp/devfeature/k3s-on-host/k3s_install.sh" ]]; then debug_log "Downloading k3s install script..." - run_a_script "curl --silent --fail --create-dirs --output /host_var/tmp/k3s-on-host/k3s_install.sh -L https://get.k3s.io" + run_a_script "curl --silent --fail --create-dirs --output /host_var/tmp/devfeature/k3s-on-host/k3s_install.sh -L https://get.k3s.io" fi if [[ "${USE_CRI_DOCKERD}"==true ]]; then @@ -103,10 +390,10 @@ function install_k3s() { k3s_extra_commands="${k3s_extra_commands} --docker" fi - run_a_script "chmod +x /host_var/tmp/k3s-on-host/k3s_install.sh" --disable_log + run_a_script "chmod +x /host_var/tmp/devfeature/k3s-on-host/k3s_install.sh" --disable_log info_log "Installing k3s on host..." - run_a_script_on_host "/var/tmp/k3s-on-host/k3s_install.sh ${k3s_extra_commands}" --env INSTALL_K3S_VERSION=${K3S_VERSION} --env INSTALL_K3S_SYMLINK=force + run_a_script_on_host "/var/tmp/devfeature/k3s-on-host/k3s_install.sh ${k3s_extra_commands}" --env INSTALL_K3S_VERSION=${K3S_VERSION} --env INSTALL_K3S_SYMLINK=force } @@ -155,6 +442,7 @@ function gen_kubeconfig_for_devcontainer() { [[ -f "${KUBECONFIG}" ]] && run_a_script "rm -f ${KUBECONFIG}" + # Calculate the external ip of the host by checking the routes used to get to the internet debug_log "Calculating external ip..." run_a_script_on_host "ip route get 8.8.8.8" host_ip host_ip=${host_ip#*src } @@ -164,6 +452,7 @@ function gen_kubeconfig_for_devcontainer() { run_a_script_on_host "kubectl config view --flatten=true" kubeconfig --disable_log + # Update kubeconfig to use the external ip of the host instead of the default 127.0.0.1 kubeconfig="${kubeconfig/127.0.0.1/${host_ip}}" run_a_script "tee $KUBECONFIG > /dev/null << UPDATE_END @@ -191,8 +480,10 @@ function check_kubeconfig_in_bashrc(){ info_log "Checking '${HOME}/.bashrc' for 'export KUBECONFIG'..." + # Grep the .bashrc to see if the KUBECONFIG is already set run_a_script "grep 'export KUBECONFIG' ${HOME}/.bashrc" grep_results --ignore_error + # If the grep results are empty, then add the export KUBECONFIG to the .bashrc if [[ -z "${grep_results}" ]]; then info_log "...adding 'export KUBECONFIG=${KUBECONFIG}' to ${HOME}/.bashrc" run_a_script "tee -a ${HOME}/.bashrc > /dev/null << UPDATE_END @@ -208,6 +499,8 @@ UPDATE_END" function main() { + [[ "${CLUSTER_ENABLED}" == "false" ]] && return + reset_log host_interface_setup check_packages apt-transport-https curl jq yq @@ -218,5 +511,4 @@ function main() { } - main \ No newline at end of file diff --git a/.devcontainer/features/k3s-on-host/utils.sh b/.devcontainer/features/k3s-on-host/utils.sh deleted file mode 100755 index 5ad7b35..0000000 --- a/.devcontainer/features/k3s-on-host/utils.sh +++ /dev/null @@ -1,292 +0,0 @@ -source /k3s-on-host/.env - -LOG_DIR="/var/log/k3s-on-host" -SCRIPT_NAME=$(basename "$0") - -if [ "$(id -u)" -ne 0 ]; then - echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' - exit 1 -fi - -############################################################ -# Reset the log file by renaming it with a timestamp and -# creating a new empty log file -############################################################ -function reset_log() { - local timestamp=$(date "+%Y%m%d_%H%M%S") - local logFile="${SCRIPT_NAME}.log" - - run_a_script "mkdir -p ${LOG_DIR}" --disable_log - - if [[ -f "${LOG_DIR}/${logFile}" ]]; then - run_a_script "mv ${LOG_DIR}/${logFile} ${LOG_DIR}/${logFile}.${timestamp}" --disable_log - fi - run_a_script "touch ${LOG_DIR}/${logFile}" --disable_log - run_a_script "chmod u=rw,g=rw,o=rw ${LOG_DIR}/${logFile}" --disable_log - - LOG_FILE="${LOG_DIR}/${logFile}" -} - -############################################################ -# Log a message to both stdout and the log file with a -# specified log level -############################################################ -function log() { - # log informational messages to stdout - local timestamp=$(date "+%Y-%m-%d %H:%M:%S") - local log_entry="${1}" - local received_log_level="INFO" - local full_log_entry="" - local log_raw=false - - if [[ -z ${log_entry} ]]; then - return - fi - - local configured_log_level=0 - case ${LOG_LEVEL^^} in - ERROR) - configured_log_level=4 - ;; - WARN) - configured_log_level=3 - ;; - INFO) - configured_log_level=2 - ;; - DEBUG) - configured_log_level=1 - ;; - *) - configured_log_level=0 - ;; - esac - - while [[ "$#" -gt 0 ]]; do - case $1 in - --info) - received_log_level="INFO" - received_log_level_int=2 - ;; - --debug) - received_log_level="DEBUG" - received_log_level_int=1 - ;; - --warn) - received_log_level="WARN" - received_log_level_int=3 - ;; - --error) - received_log_level="ERROR" - received_log_level_int=4 - ;; - --trace) - received_log_level="TRACE" - received_log_level_int=0 - ;; - --raw) - log_raw=true - ;; - esac - shift - done - - if [[ ${log_raw} == false ]]; then - full_log_entry="[${SCRIPT_NAME}] [${received_log_level}] ${timestamp}: ${log_entry}" - else - full_log_entry="${log_entry}" - fi - - # Our log level isn't high enough - don't write it to the screen - if [[ ${received_log_level_int} -lt ${configured_log_level} ]]; then - return - fi - - - if [[ -n "${LOG_FILE}" ]]; then - echo "${full_log_entry}" | tee -a "${LOG_FILE}" - fi -} - -# Log an informational message to stdout and the log file -function info_log() { - log "${1}" --info -} - -# Log a trace message to stdout and the log file -function trace_log() { - log "${1}" --trace -} - -# Log an debug message to stdout and the log file -function debug_log() { - log "${1}" --debug -} - -# Log an warning message to stdout and the log file -function warn_log() { - log "${1}" --warn -} - -# Log an error message to stdout and the log file -function error_log() { - log "${1}" --error -} - -# Log a critical error and exit the script with a non-zero return code -function exit_with_error() { - # log a message to stderr and exit 1 - error_log "${1}" - exit 1 -} - - -############################################################ -# Helper function to run a script on the host -# args -# position 1 : the command to run. i.e. "docker container ls" -# position 2 : the variable to return the results of the script to for further processing -# --ignore_error : allow the script to continue even if the return code is not 0 -# --disable_log : prevent the output from writing to the log and screen -############################################################ -function run_a_script_on_host() { - local timestamp=$(date "+%Y%m%d_%H%M%S") - if [[ "$#" -eq 0 ]]; then - exit_with_error "Missing run script to execute. Please use function like run_a_script 'ls /'" - fi - - local run_script="$1" - local __returnVar=$2 - RETURN_CODE="" - # We're passing flags and not a return value. Reset the return variable here - if [[ "${__returnVar:0:2}" == "--" ]]; then - __returnVar="" - fi - - local log_enabled=true - local ignore_error=false - local env_vars="" - local returnResult="" - - while [[ "$#" -gt 0 ]]; do - case $1 in - --ignore_error) - ignore_error=true - ;; - --disable_log) - log_enabled=false - ;; - --env) - shift - env_vars="${env_vars} --env $1" - ;; - esac - shift - done - - local run_cmd - - run_cmd="docker exec \ - ${env_vars} \ - -ti \ - $HOST_INTERFACE_CONTAINER \ - chroot /host bash -c \"${run_script}\"" - - - if [[ "${log_enabled}" == true ]]; then - trace_log "Running '${run_cmd}'..." - fi - - returnResult=$(eval "${run_cmd}" ) - - sub_exit_code=${PIPESTATUS[0]} - RETURN_CODE=${sub_exit_code} - if [[ -n ${__returnVar} ]]; then - eval $__returnVar="'$returnResult'" - fi - - if [[ "${log_enabled}" == true ]]; then - trace_log "...'${run_cmd}' Exit code: ${sub_exit_code}" - trace_log "...'${run_cmd}' Result: ${returnResult}" - fi - - if [[ "${ignore_error}" == true ]]; then - return - fi - - if [[ $sub_exit_code -gt 0 ]]; then - exit_with_error "Script failed. Received return code of '${sub_exit_code}'. Command ran: '${run_script}'. See previous errors and retry" - fi -} - - -############################################################ -# Helper function to run a script -# args -# position 1 : the command to run. i.e. "docker container ls" -# position 2 : the variable to return the results of the script to for further processing -# --ignore_error : allow the script to continue even if the return code is not 0 -# --disable_log : prevent the output from writing to the log and screen -############################################################ -function run_a_script() { - local timestamp=$(date "+%Y%m%d_%H%M%S") - if [[ "$#" -eq 0 ]]; then - exit_with_error "Missing run script to execute. Please use function like run_a_script 'ls /'" - fi - - local run_script="$1" - local __returnVar=$2 - RETURN_CODE="" - # We're passing flags and not a return value. Reset the return variable here - if [[ "${__returnVar:0:2}" == "--" ]]; then - __returnVar="" - fi - - local log_enabled=true - local ignore_error=false - local run_in_background=false - local returnResult="" - - while [[ "$#" -gt 0 ]]; do - case $1 in - --ignore_error) - ignore_error=true - ;; - --disable_log) - log_enabled=false - ;; - esac - shift - done - - local run_cmd - - run_cmd="${run_script}" - - - if [[ "${log_enabled}" == true ]]; then - trace_log "Running '${run_cmd}'..." - fi - - - returnResult=$(eval "${run_cmd}" ) - - sub_exit_code=${PIPESTATUS[0]} - RETURN_CODE=${sub_exit_code} - if [[ -n ${__returnVar} ]]; then - eval $__returnVar="'$returnResult'" - fi - - if [[ "${log_enabled}" == true ]]; then - trace_log "...'${run_cmd}' Exit code: ${sub_exit_code}" - trace_log "...'${run_cmd}' Result: ${returnResult}" - fi - - if [[ "${ignore_error}" == true ]]; then - return - fi - - if [[ $sub_exit_code -gt 0 ]]; then - exit_with_error "Script failed. Received return code of '${sub_exit_code}'. Command ran: '${run_script}'. See previous errors and retry" - fi -} \ No newline at end of file diff --git a/README.md b/README.md index 3c3f47e..77919fd 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ kube-system metrics-server-557ff575fb-6t882 1/1 Running 0 "features": { "ghcr.io/microsoft/k3s-on-host":{ "k3sVersion": "latest", - "criDockerd": "true" + "criDockerd": "true", + "cluster_enabled": "true" } } ``` @@ -31,6 +32,7 @@ kube-system metrics-server-557ff575fb-6t882 1/1 Running 0 |-----|-----|-----|-----| | k3sVersion | Select or enter the k3s version | string | latest | | criDockerd | Deploy k3s with the cri-dockerd configured | boolean | true | +| cluster_enabled | Disable provisioning of the cluster. This is useful in CI/CD scenarios that reference this devcontainer feature, but doesn't always need the kubernetes cluster deployed. | boolean | true | ## Build and Deploying @@ -92,3 +94,4 @@ trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. +