*: do not require env.sh

This removes the need to source env.{sh,fish} when working with hscloud.

This is done by:

 1. Implementing a Go library to reliably detect the location of the
    active hscloud checkout. That in turn is enabled by
    BUILD_WORKSPACE_DIRECTORY being now a thing in Bazel.
 2. Creating a tool `hscloud`, with a command `hscloud workspace` that
    returns the workspace path.
 3. Wrapping this tool to be accessible from Python and Bash.
 4. Bumping all users of hscloud_root to use either the Go library or
    one of the two implemented wrappers.

We also drive-by replace tools/install.sh to be a proper sh_binary, and
make it yell at people if it isn't being ran as `bazel run
//tools:install`.

Finally, we also drive-by delete cluster/tools/nixops.sh which was never used.

Change-Id: I7873714319bfc38bbb930b05baa605c5aa36470a
Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1169
Reviewed-by: informatic <informatic@hackerspace.pl>
changes/69/1169/4
q3k 2021-10-16 20:53:51 +00:00 committed by q3k
parent 20c6bcb730
commit 0f8e5a2132
20 changed files with 409 additions and 83 deletions

View File

@ -15,5 +15,6 @@ py_binary(
requirement("idna"),
requirement("six"),
"//tools:secretstore_lib",
"//tools/hscloud:python",
],
)

View File

@ -16,13 +16,12 @@ from cryptography.hazmat.backends import default_backend
import fabric
from tools import secretstore
from tools.hscloud import lib as hscloud
import ca
local_root = os.getenv('hscloud_root')
if local_root is None:
raise Exception("Please source env.sh")
local_root = hscloud.workspace_location()
cluster = 'k0.hswaw.net'

View File

@ -19,7 +19,10 @@ Before you can use `kubectl`, however, you will need to authenticate yourself. T
Enter SSO/LDAP password for q3k@hackerspace.pl:
Good evening professor. I see you have driven here in your Ferrari.
If `prodaccess` is not on your $PATH, ensure you have sourced `env.sh` from the root of hscloud and ran `tools/install.sh`.
If `prodaccess` is not on your $PATH:
$ bazel run //tools:install
$ . env.sh
By default, `prodaccess` will use your local user name to authenticate as `<user>@hackerspce.pl`. If your Hackerspace SSO name is different, specify it using the `-u` flag to prodaccess, eg. `prodaccess -u informatic`.

View File

@ -13,6 +13,7 @@ go_library(
"//cluster/certs:go_default_library",
"//cluster/prodvider/proto:go_default_library",
"//go/pki:go_default_library",
"//go/workspace:go_default_library",
"@com_github_golang_glog//:go_default_library",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_grpc//credentials:go_default_library",

View File

@ -14,17 +14,18 @@ import (
"github.com/golang/glog"
pb "code.hackerspace.pl/hscloud/cluster/prodvider/proto"
"code.hackerspace.pl/hscloud/go/workspace"
)
func kubernetesPaths() (string, string, string) {
localRoot := os.Getenv("hscloud_root")
if localRoot == "" {
glog.Exitf("Please source env.sh")
ws, err := workspace.Get()
if err != nil {
glog.Exitf("%v", err)
}
localKey := path.Join(localRoot, ".kubectl", fmt.Sprintf("%s.key", flagUsername))
localCert := path.Join(localRoot, ".kubectl", fmt.Sprintf("%s.crt", flagUsername))
localCA := path.Join(localRoot, ".kubectl", fmt.Sprintf("ca.crt"))
localKey := path.Join(ws, ".kubectl", fmt.Sprintf("%s.key", flagUsername))
localCert := path.Join(ws, ".kubectl", fmt.Sprintf("%s.crt", flagUsername))
localCA := path.Join(ws, ".kubectl", fmt.Sprintf("ca.crt"))
return localKey, localCert, localCA
}

View File

@ -22,7 +22,14 @@ copy_go_binary(
sh_binary(
name = "calicoctl",
srcs = ["calicoctl.sh"],
data = [":calicoctl.bin", "//tools:secretstore"],
data = [
":calicoctl.bin",
"//tools:secretstore",
"//tools/hscloud",
],
deps = [
"//tools/hscloud:shell",
],
)
copy_go_binary(

View File

@ -1,23 +1,22 @@
#!/usr/bin/env bash
# A wrapper around the real calicoctl to configure etcd access...
# A wrapper around the real calicoctl to configure etcd access.
if [ -z "$hscloud_root" ]; then
echo 2>&1 "Please source env.sh"
exit 1
fi
source tools/hscloud/lib.sh || exit 1
ETCD_ENDPOINTS="https://bc01n01.hswaw.net:2379,https://bc01n01.hswaw.net:2379,https://bc01n01.hswaw.net:2379"
ETCD_KEY_FILE="$hscloud_root/cluster/secrets/plain/etcd-calico.key"
ETCD_CERT_FILE="$hscloud_root/cluster/certs/etcd-calico.cert"
ETCD_CA_CERT_FILE="$hscloud_root/cluster/certs/ca-etcd.crt"
function main() {
local ws=$(hscloud::workspace_location)
if [ ! -f "$ETCD_KEY_FILE" ] ; then
secretstore decrypt "$hscloud_root/cluster/secrets/cipher/etcd-calico.key" > "$ETCD_KEY_FILE"
fi
export ETCD_ENDPOINTS="https://bc01n01.hswaw.net:2379,https://bc01n01.hswaw.net:2379,https://bc01n01.hswaw.net:2379"
export ETCD_KEY_FILE="$ws/cluster/secrets/plain/etcd-calico.key"
export ETCD_CERT_FILE="$ws/cluster/certs/etcd-calico.cert"
export ETCD_CA_CERT_FILE="$ws/cluster/certs/ca-etcd.crt"
export ETCD_ENDPOINTS
export ETCD_KEY_FILE
export ETCD_CERT_FILE
export ETCD_CA_CERT_FILE
calicoctl.bin "$@"
if [ ! -f "$ETCD_KEY_FILE" ] ; then
$(hscloud::must_rlocation hscloud/tools/secretstore) decrypt "$ws/cluster/secrets/cipher/etcd-calico.key" "$ETCD_KEY_FILE"
fi
"$(hscloud::must_rlocation hscloud/cluster/tools/calicoctl.bin)" "$@"
}
main "$@"

View File

@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -e -o pipefail
if [ -z "$hscloud_root" ]; then
echo 2>&1 "Please first source env.sh"
exit 1
fi
cd "${hscloud_root}"
bazel build \
//cluster/tools:kubectl \
//cluster/tools:kubecfg \
//cluster/tools:calicoctl \
//cluster/tools:cfssl

View File

@ -1,22 +0,0 @@
#!/usr/bin/env bash
# A wrapper around real nixops to decrypt GCP secret.
if [ -z "$hscloud_root" ]; then
echo 2>&1 "Please source env.sh"
exit 1
fi
for f in sa.json sa.pem; do
plain="$hscloud_root/gcp/secrets/plain/$f"
cipher="$hscloud_root/gcp/secrets/cipher/$f"
if [ ! -f "$plain" ]; then
secretstore decrypt "$cipher" > "$plain"
fi
done
export GCE_PROJECT="hscloud"
export GCE_SERVICE_ACCOUNT="nixops@hscloud.iam.gserviceaccount.com"
export ACCESS_KEYPATH="$hscloud_root/gcp/secrets/plain/sa.pem"
./external/nixops/bin/nixops "$@"

View File

@ -11,6 +11,8 @@ if not string match -q $hscloud_path $PATH
set -x PATH $hscloud_path $PATH
end
# Leftover junk. This should be removed, as env.fish is now optional.
# Do _not_ add more aliases!
function gpg-unlock
echo "test" | gpg2 --sign --batch --no-tty -o /dev/null
end

13
env.sh
View File

@ -1,23 +1,24 @@
# source me to have all the nice things
# Source this file to have hscloud tools available in your PATH after running
# `bazel run //tools:install`.
if [ "$0" == "$BASH_SOURCE" ]; then
echo "You should be sourcing this."
exit 1
fi
export hscloud_root="$( cd "$(dirname "$BASH_SOURCE")"; pwd -P )"
hscloud_root="$( cd "$(dirname "$BASH_SOURCE")"; pwd -P )"
if [ ! -f "$hscloud_root/WORKSPACE" ]; then
echo "Could not find WORKSPACE"
exit 1
fi
hscloud_path="$hscloud_root/bazel-bin/tools:$hscloud_root/bazel-bin/cluster/tools"
[[ ":$PATH:" != *":$hscloud_path:"* ]] && PATH="$hscloud_path:${PATH}"
unset -f hscloud_root
unset -f hscloud_path
# Leftover junk. This should be removed, as env.sh is now optional.
# Do _not_ add more aliases!
alias bajzel=bazel
gpg-unlock() {
echo "test" | gpg2 --sign --batch --no-tty -o /dev/null
}

8
go/workspace/BUILD.bazel Normal file
View File

@ -0,0 +1,8 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["workspace.go"],
importpath = "code.hackerspace.pl/hscloud/go/workspace",
visibility = ["//visibility:public"],
)

101
go/workspace/workspace.go Normal file
View File

@ -0,0 +1,101 @@
package workspace
import (
"fmt"
"os"
"path"
"sync"
"syscall"
)
// isWorkspace returns whether a given string is a valid path pointing to a
// Bazel workspace directory.
func isWorkspace(dir string) bool {
w := path.Join(dir, "WORKSPACE")
if _, err := os.Stat(w); err == nil {
return true
}
return false
}
// getPathFSID returns an opaque filesystem identifier for a given path.
func getPathFSID(dir string) (uint64, error) {
st, err := os.Stat(dir)
if err != nil {
// No need to wrap err, as stat errors are already quite explicit
// (they also include the stat'd path).
return 0, err
}
switch x := st.Sys().(type) {
case *syscall.Stat_t:
return x.Dev, nil
default:
return 0, fmt.Errorf("unsupported operating system (got stat type %+v)", st.Sys())
}
}
// lookForWorkspace recurses up a directory until it finds a WORKSPACE, the
// root, or crosses a filesystem boundary.
func lookForWorkspace(dir string) (string, error) {
fsid, err := getPathFSID(dir)
if err != nil {
return "", fmt.Errorf("could not get initial FSID: %w", err)
}
for {
if dir == "." || dir == "/" || dir == "" {
return "", fmt.Errorf("got up to root before finding workspace")
}
fsid2, err := getPathFSID(dir)
if err != nil {
return "", fmt.Errorf("could not get parent FWID: %w", err)
}
if fsid2 != fsid {
return "", fmt.Errorf("crossed filesystem boundaries before finding workspace")
}
if isWorkspace(dir) {
return dir, nil
}
dir = path.Dir(dir)
}
}
// Get returns the workspace directory from which a given
// command line tool is running. This handles the following
// cases:
//
// 1. The command line tool was invoked via `bazel run`.
// 2. The command line tool was started in the workspace directory or a
// subdirectory.
//
// If the workspace directory path cannot be inferred based on the above
// assumptions, an error is returned.
func Get() (string, error) {
workspaceOnce.Do(func() {
workspace, workspaceErr = get()
})
return workspace, workspaceErr
}
var (
workspace string
workspaceErr error
workspaceOnce sync.Once
)
func get() (string, error) {
if p := os.Getenv("BUILD_WORKSPACE_DIRECTORY"); p != "" && isWorkspace(p) {
return p, nil
}
wd, err := os.Getwd()
if err != nil {
return "", err
}
p, err := lookForWorkspace(wd)
if err != nil {
return "", fmt.Errorf("not invoked from `bazel run` and could not find workspace root by traversing upwards: %w", err)
}
return p, nil
}

View File

@ -10,7 +10,7 @@ in with hscloud.pkgs; let
''
source /etc/profile
source ${toString ./.}/env.sh
${toString ./.}/tools/install.sh
bazel run //tools:install
# Fancy colorful PS1 to make people notice easily they're in hscloud.
PS1='\[\033]0;\u/hscloud:\w\007\]'

View File

@ -1,6 +1,17 @@
load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar", "pkg_deb")
load("//bzl:rules.bzl", "copy_go_binary")
sh_binary(
name = "install",
srcs = [
"install.sh",
],
visibility = ["//visibility:public"],
deps = [
"//tools/hscloud:shell",
],
)
py_library(
name = "secretstore_lib",
srcs = ["secretstore.py"],

46
tools/hscloud/BUILD.bazel Normal file
View File

@ -0,0 +1,46 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "hscloud_lib",
srcs = ["main.go"],
importpath = "code.hackerspace.pl/hscloud/tools/hscloud",
visibility = ["//visibility:private"],
deps = [
"//go/workspace:go_default_library",
"@com_github_spf13_cobra//:go_default_library",
],
)
go_binary(
name = "hscloud",
embed = [":hscloud_lib"],
visibility = ["//visibility:public"],
)
sh_library(
name = "shell",
srcs = [
"lib.sh",
],
deps = [
"@bazel_tools//tools/bash/runfiles",
],
data = [
":hscloud",
],
visibility = ["//visibility:public"],
)
py_library(
name = "python",
srcs = [
"lib.py",
],
deps = [
"@rules_python//python/runfiles",
],
data = [
":hscloud",
],
visibility = ["//visibility:public"],
)

45
tools/hscloud/lib.py Normal file
View File

@ -0,0 +1,45 @@
# Library to interact with the active hscloud checkout. This supersedes the
# hscloud_root environment variable once used in hscloud.
#
# Some of this could be implemented in Python instead of shelling out to a Go
# binary - but that way we have a single source of truth, even if it's janky.
#
# To use:
#
# from tools.hscloud import lib as hscloud
#
# And specify deps = [ "//tools/hscloud:python" ] in py_binary.
import subprocess
from rules_python.python.runfiles import runfiles
r = runfiles.Create()
def tool_location():
"""
Return an absolute path to a built //tools/hscloud binary, ready to run.
"""
rloc = r.Rlocation("hscloud/tools/hscloud/hscloud_/hscloud")
if rloc is None:
raise Exception("Could not find location of hscloud - are you in a valid checkout?")
return rloc
def workspace_location():
"""Return an absolute path to the hscloud checkout."""
return subprocess.check_output([tool_location(), "workspace"]).decode()
def must_rlocation(runfile):
"""Return an absolute path to a runfile, eg. a data depndency in sh_binary."""
rloc = r.Rlocation(runfile)
if rloc is None:
msg = f"Could not find runfile {runfile}"
manifest = os.environ.get("RUNFILES_MANIFEST_FILE", "")
if manifest != "":
msg += f"; manifest file: {manifest}"
raise Exception(msg)
return rloc

65
tools/hscloud/lib.sh Normal file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Top-level 'universal' shell library for hscloud. All sh_binary targets should
# depend on this and import it as follows:
#
# #!/usr/bin/env bash
# source tools/hscloud/lib.sh || exit 1
#
# And by specifying deps = [ "//tools/hscloud:shell" ] in sh_binary.
set -e -u -o pipefail
function hscloud::_prepare_runfiles() {
if [[ $(type -t rlocation) == function ]]; then
return
fi
# --- begin runfiles.bash initialization v2 ---
# Mostly copy-pasted from the Bazel Bash runfiles library v2.
local f=bazel_tools/tools/bash/runfiles/runfiles.bash
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
source "$0.runfiles/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
{ echo>&2 "ERROR: cannot find $f - are you in a valid checkout and running the script via bazel?"; exit 1; }
# --- end runfiles.bash initialization v2 ---
}
# Return an absolute path to a built //tools/hscloud binary, ready to run.
#
# This will fail if we're not in a hscloud checkout.
function hscloud::tool_location() {
rloc="$(hscloud::rlocation "hscloud/tools/hscloud/hscloud_/hscloud")"
if [ -z "$rloc" ]; then
echo "Could not find location of hscloud - are you in a valid checkout and running the script via bazel?" >&2
exit 1
fi
echo "$rloc"
}
# Return an absolute path to the hscloud checkout.
function hscloud::workspace_location() {
$(hscloud::tool_location) workspace
}
# Return an absolute path to a runfile, eg. a data dependency in sh_binary.
function hscloud::rlocation() {
hscloud::_prepare_runfiles
echo "$(rlocation "$1")"
}
# Return an absolute path to a runfile, eg. a data dependency in sh_binary.
#
# This will fail if the runfile is not found.
function hscloud::must_rlocation() {
rloc="$(hscloud::rlocation $1)"
if [ -z "$rloc" ]; then
echo "Could not find runfile $1" >&2
if [ ! -z "${RUNFILES_MANIFEST_FILE:-}" ]; then
echo "Manifest file: $RUNFILES_MANIFEST_FILE" >&2
fi
exit 1
fi
echo "$rloc"
}

37
tools/hscloud/main.go Normal file
View File

@ -0,0 +1,37 @@
package main
import (
"fmt"
"os"
"code.hackerspace.pl/hscloud/go/workspace"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "hscloud",
Short: "hscloud kitchesink tool",
Long: `A single entrypoint tool to interact with a hscloud git checkout.`,
}
var workspaceCmd = &cobra.Command{
Use: "workspace",
Short: "Print root path of hscloud checkuot",
Long: `This returns the directory path containing WORKSPACE. It works both from 'bazel run', when invoked as a tool in bazel or when called manually. Feel free to use this in your sh_binary scripts.`,
Run: func(cmd *cobra.Command, args []string) {
wd, err := workspace.Get()
if err != nil {
fmt.Fprintf(os.Stderr, "%v", err)
os.Exit(1)
}
fmt.Println(wd)
},
}
func main() {
rootCmd.AddCommand(workspaceCmd)
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

View File

@ -1,14 +1,52 @@
#!/usr/bin/env bash
source tools/hscloud/lib.sh || exit 1
set -e -o pipefail
function main() {
# If we're not running from `bazel run/buld`, complain and re-execute
# ourselves.
#
# We do the check fairly low level, as //tools/hscloud:lib.sh will just
# fail in this case. We want to be nice.
#
# This is all mostly copied from the runfiles.bash snippet in
# tools/hscloud/lib.sh.
f=bazel_tools/tools/bash/runfiles/runfiles.bash
if [ ! -e "${RUNFILES_DIR:-/dev/null}/$f" ] && \
[ ! -e "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" ] && \
[ ! -e "$0.runfiles/$f" ] && \
[ ! -e "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" ] && \
[ ! -e "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" ]; then
echo "Uh-oh, looks like you didn't run this as 'bazel run //tools:install'.">&2
echo "Let me fix that for you in a few seconds - but work on your muscle memory, as we'll stop supporting this by some point.">&2
sleep 2
bazel run //tools:install -- "$@"
exit $?
fi
if [ -z "$hscloud_root" ]; then
echo 2>&1 "Please first source env.sh"
exit 1
fi
cd $(hscloud::workspace_location)
echo "Building hscloud tools and cluster tools..."
bazel build //tools/... //cluster/tools/...
cd "${hscloud_root}"
local path_missing=""
local path="$(hscloud::workspace_location)/bazel-bin/tools"
if [[ ":$PATH:" == *":$path:"* ]]; then
path_missing="$path"
fi
local path="$(hscloud::workspace_location)/bazel-bin/cluster/tools"
if [[ ":$PATH:" == *":$path:"* ]]; then
if [ -z "$path_missing" ]; then
path_missing="$path"
else
path_missing="$path_missing:$path"
fi
fi
if [ -z "$path_missing" ]; then
echo "Tools built correctly, but your PATH should be updated to access them:">&2
echo ' PATH="$PATH:'$path_missing'"'
echo 'Add the above line to your shell profile, or source env.sh from the root of hscloud.'
else
echo "Tools built correctly and in PATH. Happy hsclouding!"
fi
}
bazel build //tools/...
cluster/tools/install.sh
main "$@"