diff --git a/root/etc/profile.d/getup.sh b/root/etc/profile.d/getup.sh index 1d8c71f..9a72c29 100755 --- a/root/etc/profile.d/getup.sh +++ b/root/etc/profile.d/getup.sh @@ -178,8 +178,13 @@ read_config() { local _name="${1}" local prompt="${2}" - local _opt_name="opt_$_name" + local _secret=${_secret:-false} + local read_opts='-e' + + if $_secret; then + read_opts+=' -s' + fi if [ -v "$_opt_name" ]; then local _opt_value="${!_opt_name}" @@ -194,11 +199,13 @@ read_config() else local _default="" fi - shift - if [ -n "$_default" ]; then - prompt+=" [$_default]" + if $_secret; then + prompt+=" [${_default//[^*]/*}]" + else + prompt+=" [$_default]" + fi fi if [ -n "$_opt_value" ]; then @@ -209,7 +216,7 @@ read_config() fi line - read -e -p "$(prompt "$prompt")" $_name + read $read_opts -p "$(prompt "$prompt")" $_name local _value="${!_name}" if [ -z "$_value" ]; then @@ -219,6 +226,29 @@ read_config() fi } +function get_tf_config() +{ + local sh_var_name=$1 + local tf_var_name=$2 + + if [ -v $sh_var_name ]; then + echo ${!sh_var_name} + return + elif [ -v TF_VAR_$tf_var_name ]; then + local v=TV_VAR_$tf_var_name + echo ${!v} + return + fi + + case "$(hcl2json "$TF_VARS_FILE" | jq -Mrc ".${tf_var_name}|type")" in + string|number|object) + hcl2json "$TF_VARS_FILE" | jq -Mrc ".${tf_var_name}" + ;; + array) + hcl2json "$TF_VARS_FILE" | jq -Mrc ".${tf_var_name}|join(\"\n\")" + esac +} + ask() { unset ask_response @@ -266,9 +296,41 @@ ask_any() # done #} +function fill_line() +{ + local cmd="$@" + local cmd_len=${#cmd} + local line_pre_fmt="------- [%s] " + local line_pre=$(printf -- "$line_pre_fmt" '') + local line_len=$[$(tput cols) - cmd_len - ${#line_pre}] + local line=$(printf -- '%*s' $line_len|tr ' ' -) + + printf -- "${COLOR_GREEN}${COLOR_BOLD}$line_pre_fmt%s${COLOR_RESET}\n" "$cmd" "$line" +} + +function execute_command_with_time_track() +{ + local _print_cmd="${_print_cmd:-$@}" + local TIMEFORMAT="${COLOR_CYAN}Command [$_print_cmd] took ${COLOR_BOLD}%2lR${COLOR_RESET}" + time "$@" +} + +function execute_command() +{ + if [ $# -eq 0 ]; then + return + fi + + local _print_cmd="${_print_cmd:-$@}" + + fill_line "$_print_cmd" + _print_cmd="${_print_cmd}" execute_command_with_time_track $@ +} + ask_execute_command() { local _default="${_default:-y}" + local _print_cmd="${_print_cmd:-$@}" if [ "$_default" == "n" ]; then local _sel="[y/N]" @@ -276,11 +338,7 @@ ask_execute_command() local _sel="[Y/n]" fi - if [ $BASH_VERSINFO -lt 5 ]; then - read -e -p "$(prompt COLOR_GREEN "Execute [${COLOR_BOLD}${@}${COLOR_RESET}${COLOR_GREEN}] now? $_sel")" res - else - read -e -p "$(prompt COLOR_GREEN "Execute [${COLOR_BOLD}${@@Q}${COLOR_RESET}${COLOR_GREEN}] now? $_sel")" res - fi + read -e -p "$(prompt COLOR_GREEN "Execute [${COLOR_BOLD}${_print_cmd}${COLOR_RESET}${COLOR_GREEN}] now? $_sel")" res if [ "$_default" == "n" ]; then res="${res:-n}" @@ -289,7 +347,8 @@ ask_execute_command() fi case "${res,,}" in - y|yes|s|sim) "$@" + y|yes|s|sim) + _print_cmd="${_print_cmd}" execute_command "$@" esac } diff --git a/root/usr/local/bin/check-unused-manifests b/root/usr/local/bin/check-unused-manifests index 50c1732..c63e957 100755 --- a/root/usr/local/bin/check-unused-manifests +++ b/root/usr/local/bin/check-unused-manifests @@ -18,7 +18,7 @@ except: dir = "/cluster/manifests/cluster/" unused = [] -print("Detecting unused resources: ", end='', flush=True) +#print("Detecting unused resources: ", end='', flush=True) def verify_dir(dir): dir = os.path.abspath(dir) @@ -51,6 +51,5 @@ if os.path.isdir(dir): verify_dir(dir) if unused: - print('\n', '\n'.join([ f'- {f}' for f in unused ]), sep='') -else: - print("none") + print(' '.join(unused)) + sys.exit(2) diff --git a/root/usr/local/bin/get-var b/root/usr/local/bin/get-var new file mode 100644 index 0000000..7abae8a --- /dev/null +++ b/root/usr/local/bin/get-var @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +import os +import sys +import json +import json +import glob +from hcl2.parser import hcl2 + +def flatten(l): + return [item for sublist in l for item in sublist] + +if len(sys.argv) > 1: + sources = sys.argv[1:] +else: + prefix = os.environ.get("CLUSTER_DIR", "./") + sources = [ os.path.join(prefix, "variable*.tf") ] + + +variables = [] +for f in flatten([ glob.glob(i) for i in sources ]): + with open(f, 'r') as f: + data = hcl2.parse(f.read()) + for v in data.get('variable', []): + variables.append(v) + +variables = sorted(variables, key=lambda a: tuple(a.keys())[0]) +print(json.dumps(variables)) + +def read_var(v): + if v['default'] != null: + return v + return v + +for v in variables: + v = read_var(v) + + diff --git a/root/usr/local/bin/kubeconfig-get b/root/usr/local/bin/kubeconfig-get index 23148a5..1c86e2e 100755 --- a/root/usr/local/bin/kubeconfig-get +++ b/root/usr/local/bin/kubeconfig-get @@ -8,48 +8,63 @@ if [ -v KUBECONFIG ]; then debug Using KUBECONFIG=$KUBECONFIG fi -if [ $# -gt 0 ]; then - cluster_type=$1 +while [ $# -gt 0 ]; do + case "$1" in + -n|--cluster-name) + shift + cluster_name=$1 + ;; + -t|--cluster-type) + shift + cluster_type=$1 + ;; + -f|--force) + read_config=":" + esac shift -fi - -if [ $# -gt 0 ]; then - cluster_name=$1 - shift -fi +done info Retrieving kubeconfig for $cluster_name/$cluster_type case $cluster_type in kind) - exec kind export kubeconfig --name $cluster_name "$@" + execute_command kind export kubeconfig --name $cluster_name "$@" ;; eks) - exec aws eks update-kubeconfig --name $cluster_name "$@" + export AWS_ACCESS_KEY_ID=$(get_tf_config AWS_ACCESS_KEY_ID aws_access_key_id) + export AWS_SECRET_ACCESS_KEY=$(get_tf_config AWS_SECRET_ACCESS_KEY aws_secret_access_key) + export AWS_DEFAULT_REGION=$(get_tf_config AWS_DEFAULT_REGION region) + + [ -n "$AWS_ACCESS_KEY_ID " ] || read_config AWS_ACCESS_KEY_ID 'AWS Access Key ID' + [ -n "$AWS_SECRET_ACCESS_KEY" ] || _secret=true read_config AWS_SECRET_ACCESS_KEY 'AWS Secret Access Key' + [ -n "$AWS_DEFAULT_REGION" ] || read_config AWS_DEFAULT_REGION 'AWS Default Region' + + execute_command aws eks update-kubeconfig --name $cluster_name "$@" ;; doks) if ! [ -e ~/.config/doctl/config.yaml ]; then - doctl auth init + execute_command doctl auth init fi - exec doctl kubernetes cluster kubeconfig save ${cluster_name} + execute_command doctl kubernetes cluster kubeconfig save ${cluster_name} ;; gke) + export GCP_PROJECT=$(get_tf_config GCP_PROJECT project_id) + export GCE_ZONE=$(get_tf_config GCE_ZONE zones | head -1) + + [ -n "$GCP_PROJECT" ] || read_config GCP_PROJECT "GCP Project" + [ -n "$GCE_ZONE" ] || read_config GCE_ZONE "GCE Zone" + if [ -e "$GOOGLE_APPLICATION_CREDENTIALS" ]; then - gcloud auth activate-service-account --key-file $GOOGLE_APPLICATION_CREDENTIALS - read_config GCP_PROJECT "GCP Project" - gcloud config set project $GCP_PROJECT - read_config GCE_ZONE "GCE Zone" - gcloud container clusters get-credentials --zone $GCE_ZONE --project $GCP_PROJECT $cluster_name - else - read_config GCP_PROJECT "GCP Project" - gcloud config set project $GCP_PROJECT - - read_config GCE_ZONE "GCE Zone" - gcloud container clusters get-credentials --zone $GCE_ZONE --project $GCP_PROJECT $cluster_name + execute_command gcloud auth activate-service-account --key-file $GOOGLE_APPLICATION_CREDENTIALS + else + warn "Missing service account file $GOOGLE_APPLICATION_CREDENTIALS (\$GOOGLE_APPLICATION_CREDENTIALS)" fi + + execute_command gcloud config set project $GCP_PROJECT + execute_command gcloud container clusters get-credentials --zone $GCE_ZONE --project $GCP_PROJECT $cluster_name ;; oke) @@ -82,7 +97,7 @@ case $cluster_type in oci ce cluster list --compartment-id $OCI_COMPARTMENT_ID --all --output table --query 'data[].name' done OCI_CLUSTER_ID=$(oci ce cluster list --name $OCI_CLUSTER_NAME --compartment-id $OCI_COMPARTMENT_ID | jq -r '.data[0].id') - oci ce cluster create-kubeconfig --cluster-id $OCI_CLUSTER_ID \ + execute_command oci ce cluster create-kubeconfig --cluster-id $OCI_CLUSTER_ID \ --file $CLUSTER_DIR/.kube/config --region $OCI_CLI_REGION \ --token-version 2.0.0 ;; @@ -98,20 +113,33 @@ case $cluster_type in ;; aks) - export ARM_CLIENT_ID=$(sed -ne 's/^client_id.*"\([^"]\+\)"/\1/p' /cluster/terraform.tfvars) - export ARM_CLIENT_SECRET=$(sed -ne 's/^client_secret.*"\([^"]\+\)"/\1/p' /cluster/terraform.tfvars) - export ARM_TENANT_ID=$(sed -ne 's/^tenant_id.*"\([^"]\+\)"/\1/p' /cluster/terraform.tfvars) - export ARM_SUBSCRIPTION_ID=$(sed -ne 's/^subscription_id.*"\([^"]\+\)"/\1/p' /cluster/terraform.tfvars) - export AKS_RESOURCE_GROUP_NAME=$(sed -ne 's/^resource_group_name.*"\([^"]\+\)"/\1/p' /cluster/terraform.tfvars) - - read_config ARM_CLIENT_ID "ARM_CLIENT_ID" - read_config ARM_CLIENT_SECRET "ARM_CLIENT_SECRET" - read_config ARM_TENANT_ID "ARM_TENANT_ID" - read_config ARM_SUBSCRIPTION_ID "AKS Subscription ID" - read_config AKS_RESOURCE_GROUP_NAME "AKS Resource Group Name" - - ask_execute_command az login --service-principal -u $ARM_CLIENT_ID -p $ARM_CLIENT_SECRET -t $ARM_TENANT_ID - az aks get-credentials --admin --name $cluster_name --resource-group $AKS_RESOURCE_GROUP_NAME --subscription $ARM_SUBSCRIPTION_ID + export ARM_CLIENT_ID=$(get_tf_config ARM_CLIENT_DIR client_id) + export ARM_CLIENT_SECRET=$(get_tf_config ARM_CLIENT_SECRET client_secret) + export ARM_TENANT_ID=$(get_tf_config ARM_TENANT_ID tenant_id) + export ARM_SUBSCRIPTION_ID=$(get_tf_config ARM_SUBSCRIPTION_ID subscription_id) + export AKS_RESOURCE_GROUP_NAME=$(get_tf_config AKS_RESOURCE_GROUP_NAME resource_group_name) + + [ -n "$ARM_CLIENT_ID" ] || read_config ARM_CLIENT_ID 'ARM Client ID' + [ -n "$ARM_CLIENT_SECRET" ] || _secret=true read_config ARM_CLIENT_SECRET 'ARM Client Secret' + [ -n "$ARM_TENANT_ID" ] || read_config ARM_TENANT_ID 'ARM Tenant ID' + [ -n "$ARM_SUBSCRIPTION_ID" ] || read_config ARM_SUBSCRIPTION_ID 'AKS Subscription ID' + [ -n "$AKS_RESOURCE_GROUP_NAME" ] || read_config AKS_RESOURCE_GROUP_NAME 'AKS Resource Group Name' + + _print_cmd="az login --service-principal -u $ARM_CLIENT_ID -p -t $ARM_TENANT_ID" \ + execute_command \ + az login --service-principal \ + -u $ARM_CLIENT_ID \ + -p $ARM_CLIENT_SECRET \ + -t $ARM_TENANT_ID \ + || exit + + execute_command \ + az aks get-credentials \ + --admin \ + --name $cluster_name \ + --resource-group $AKS_RESOURCE_GROUP_NAME \ + --subscription $ARM_SUBSCRIPTION_ID \ + --overwrite-existing ;; *) diff --git a/root/usr/local/bin/kubespray-copy b/root/usr/local/bin/kubespray-copy new file mode 100755 index 0000000..b4b1082 --- /dev/null +++ b/root/usr/local/bin/kubespray-copy @@ -0,0 +1,43 @@ +#!/bin/bash + +set -eu + +source /etc/profile.d/getup.sh + +user=$(get_tf_config SSH_USER ssh_user) + +if [ -z "$user" ]; then + user=centos +fi + +hosts=all +remote_src=false + +src=$1 +dest=$2 +shift 2 + +src_host=${src%%:*} +dest_host=${dest%%:*} +src=${src#*:} +dest=${dest#*:} + +if [ -n "$src_host" -a -n "$dest_host" ] || [ -z "$src_host$dest_host" ]; then + echo "Usage:" + echo " Upload: $0 local-file hosts:remote-file" + echo " Download: $0 host:remote-file local-file" + exit 1 +fi + +if [ -n "$src_host" ]; then + # downloading + hosts=$src_host + remote_src=true +elif [ -n "$dest_host" ]; then + # uploading + hosts=$dest_host +fi + +echo "See available flags in https://docs.ansible.com/ansible/latest/collections/ansible/builtin/copy_module.html" >&2 + +execute_command ansible $hosts -i $INVENTORY_FILE --become --user $user -m copy -a "src=$src dest=$dest remote_src=$remote_src $*" diff --git a/root/usr/local/bin/repo-push b/root/usr/local/bin/repo-push index e37ae46..cad2b4d 100755 --- a/root/usr/local/bin/repo-push +++ b/root/usr/local/bin/repo-push @@ -9,6 +9,17 @@ else fi source repo-git-config-setup +opt_force= +force=false +while [ $# -gt 0 ]; do + case "$1" in + -f|--force) + force=true + opt_force=$1 + esac + shift +done + info Switching to $REPO_DIR cd $REPO_DIR @@ -24,11 +35,15 @@ if [ "$origin" == https://github.com/getupcloud/managed-cluster.git ] \ fi if ! is_local_git_repo && ! repo-exists; then - ask_execute_command repo-setup + if $force; then + repo-setup $opt_force + else + ask_execute_command repo-setup + fi fi if ${INSIDE_CONTAINER:-false}; then - repo-validate + repo-validate $opt_force REPO_FILES="repo.conf clusters/$cluster_name/$cluster_type" COMMIT_MESSAGE="Automatic commit: clusters/$cluster_name/$cluster_type" else @@ -36,15 +51,18 @@ else COMMIT_MESSAGE="Automatic commit: repo/$customer_name" fi -FZF_PREVIEW=" - if [ {1} == '??' ]; then - [ -d {2} ] && tree {2} || cat {2} - else - git diff --color {2} - fi -" +if $force; then + git add ${REPO_FILES} + git commit -m "$COMMIT_MESSAGE" +elif ask "Select files to commit now? [Y/n]"; then + FZF_PREVIEW=" + if [ {1} == '??' ]; then + [ -d {2} ] && tree {2} || cat {2} + else + git diff --color {2} + fi + " -if ask "Select files to commit now? [Y/n]"; then FILES_TO_COMMIT=( $(git status --short $REPO_FILES | fzf -0 --ansi --reverse --multi --disabled \ --header='---[ Select files to commit (ctrl+a to select all) ]---' \ @@ -67,4 +85,8 @@ if ask "Select files to commit now? [Y/n]"; then fi fi -ask_execute_command git push origin main +if $force; then + git push origin main +else + ask_execute_command git push origin main +fi diff --git a/root/usr/local/bin/repo-ssh-key-exists b/root/usr/local/bin/repo-ssh-key-exists new file mode 100644 index 0000000..1da036e --- /dev/null +++ b/root/usr/local/bin/repo-ssh-key-exists @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -eu + +if ${INSIDE_CONTAINER:-false}; then + source /etc/profile.d/getup.sh +else + source $REPO_DIR/root/etc/profile.d/getup.sh +fi + +pub_key="identity.pub" +for dir in "${CLUSTER_DIR-:}" "$REPO_DIR"; do + if [ -n "$dir" ] && [ -e "$dir/$pub_key" ]; then + pub_key="$dir/$pub_key" + break + fi +done + +local_ssh_key_fingerprint=$(ssh-keygen -l -E sha256 -f $pub_key | awk '{print $2}') +remote_ssh_key_fingerprints=( + $(PAGER= gh api repos/$gh_owner/$gh_name/keys | jq '.[]|.key' -r | ssh-keygen -l -E sha256 -f - | awk '{print $2}') +) + +found=false +if [ ${#remote_ssh_key_fingerprints[*]} -gt 0 ]; then + for fp in ${remote_ssh_key_fingerprints[*]}; do + if [ "$fp" == "$local_ssh_key_fingerprint" ]; then + exit 0 + fi + done +fi + +exit 1 diff --git a/root/usr/local/bin/repo-validate b/root/usr/local/bin/repo-validate index afcb753..f5965d1 100755 --- a/root/usr/local/bin/repo-validate +++ b/root/usr/local/bin/repo-validate @@ -4,10 +4,34 @@ set -eu source /etc/profile.d/getup.sh +force=false +while [ $# -gt 0 ]; do + case "$1" in + -f|--force) + force=true + esac + shift +done + KUSTOMIZE_BUILD=/tmp/.kustomize_build.yaml +KUSTOMIZE_BUILD_CMD="kustomize build /cluster/manifests/cluster/ -o $KUSTOMIZE_BUILD" + +if $force; then + info Checking kustomize build: /cluster/manifests/cluster/ + $KUSTOMIZE_BUILD_CMD +else + ask_execute_command $KUSTOMIZE_BUILD_CMD +fi -if ask_execute_command kustomize build /cluster/manifests/cluster/ -o $KUSTOMIZE_BUILD; then - echo Passed +if [ $? -eq 0 ]; then + info Passed +else + warn Failed + exit 1 fi -check-unused-manifests || true +info Checking if there are unused manifests: /cluster/manifests/cluster/ +if ! UNUSED_MANIFESTS=$(check-unused-manifests); then + warn Manifests below are never referenced by /cluster/manifests/cluster/kustomization.yaml + printf '+ %s\n' $UNUSED_MANIFESTS +fi diff --git a/root/usr/local/bin/terraform-apply b/root/usr/local/bin/terraform-apply index 5acadcd..75af1bd 100755 --- a/root/usr/local/bin/terraform-apply +++ b/root/usr/local/bin/terraform-apply @@ -1,19 +1,53 @@ #!/usr/bin/env bash set -eu - source /etc/profile.d/getup.sh +tf_init=false +opt_force= +force=false + +while [ $# -gt 0 ]; do + case "$1" in + -f|--force) + force=true + opt_force=$1 + ;; + -i|--init) + tf_init=true + ;; + esac + shift +done + +if ! $force; then + tf_init=true +fi + info Switching to $CLUSTER_DIR cd $CLUSTER_DIR -ask_execute_command terraform init -ask_execute_command terraform validate -ask_execute_command terraform plan -out=$TF_PLAN_FILE -_default=n \ - ask_execute_command terraform apply $TF_PLAN_FILE -ask_execute_command terraform fmt -ask_execute_command repo-push -[ -e "$KUBECONFIG" ] && _default=n; \ - ask_execute_command kubeconfig-get +if $force; then + execute_command terraform init + execute_command terraform validate + execute_command terraform apply -auto-approve + execute_command terraform fmt + execute_command repo-push $opt_force + execute_command kubeconfig-get $opt_force + execute_command flux reconcile source git cluster +else + ask_execute_command terraform init + ask_execute_command terraform validate + ask_execute_command terraform plan -out=$TF_PLAN_FILE + _default=n \ + ask_execute_command terraform apply $TF_PLAN_FILE + ask_execute_command terraform fmt + ask_execute_command repo-push + + [ -e "$KUBECONFIG" ] && _default=n; \ + ask_execute_command kubeconfig-get +fi + +t=$(date -d@$SECONDS -u +%Hh%Mm%Ss) +echo -e "${COLOR_CYAN}Total execution time: ${COLOR_BOLD}${t}${COLOR_RESET}" diff --git a/root/usr/local/bin/terraform-setup.py b/root/usr/local/bin/terraform-setup.py new file mode 100644 index 0000000..7abae8a --- /dev/null +++ b/root/usr/local/bin/terraform-setup.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +import os +import sys +import json +import json +import glob +from hcl2.parser import hcl2 + +def flatten(l): + return [item for sublist in l for item in sublist] + +if len(sys.argv) > 1: + sources = sys.argv[1:] +else: + prefix = os.environ.get("CLUSTER_DIR", "./") + sources = [ os.path.join(prefix, "variable*.tf") ] + + +variables = [] +for f in flatten([ glob.glob(i) for i in sources ]): + with open(f, 'r') as f: + data = hcl2.parse(f.read()) + for v in data.get('variable', []): + variables.append(v) + +variables = sorted(variables, key=lambda a: tuple(a.keys())[0]) +print(json.dumps(variables)) + +def read_var(v): + if v['default'] != null: + return v + return v + +for v in variables: + v = read_var(v) + + diff --git a/templates/aks/README.md b/templates/aks/README.md new file mode 100644 index 0000000..d8119f5 --- /dev/null +++ b/templates/aks/README.md @@ -0,0 +1,69 @@ +# Required steps to create a cluster from scratch + +## Create a service principal + +```sh +az login +az account list +``` + +Choose `id` for your subscription and run + +```sh +ARM_SUBSCRIPTION_ID=<<.id from output above>> + +az account set --subscription=${ARM_SUBSCRIPTION_ID} +az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/${ARM_SUBSCRIPTION_ID}" +``` + +Alternativelly, use any of the methods describe in [Terraform Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#authenticating-to-azure). + +## Enable required provider features + +```sh +az provider register --namespace Microsoft.OperationalInsights +az provider register --namespace Microsoft.ContainerService +az provider register --namespace Microsoft.OperationsManagement +az provider register --namespace Microsoft.Compute +az provider register --namespace Microsoft.Quota +az provider register --namespace Microsoft.Insights +az rest --verbose --method POST \ + --url https://management.azure.com/subscriptions/${ARM_SUBSCRIPTION_ID}/providers/Microsoft.Features/providers/Microsoft.Compute/features/EncryptionAtHost/register?api-version=2015-12-01 +``` + +## Required resources + +### Resource Group + +``` +AZURE +az account list-locations | jq '.[].name' +AKS_LOCATION=<