diff --git a/.bazelignore b/.bazelignore deleted file mode 100644 index 1e107f5..0000000 --- a/.bazelignore +++ /dev/null @@ -1 +0,0 @@ -examples diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..9c597a2 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,4 @@ +# To update these lines, execute +# `bazel run @rules_bazel_integration_test//tools:update_deleted_packages` +build --deleted_packages=examples/customized,examples/customized/helper/authenticate,examples/customized/helper/cache,examples/customized/helper/helperfactory,examples/full +query --deleted_packages=examples/customized,examples/customized/helper/authenticate,examples/customized/helper/cache,examples/customized/helper/helperfactory,examples/full diff --git a/BUILD.bazel b/BUILD.bazel index bba7115..a42112c 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -35,6 +35,7 @@ filegroup( "//cmd:all_files", "//cmd/credential-helper:all_files", "//cmd/root:all_files", + "//examples/testing:all_files", "//helperfactory:all_files", "//helperfactory/fallback:all_files", "//installer:all_files", @@ -43,3 +44,12 @@ filegroup( ], visibility = ["//:__subpackages__"], ) + +test_suite( + name = "all_integration_tests", + tags = [ + "exclusive", + "manual", + ], + tests = ["//examples:integration_tests"], +) diff --git a/MODULE.bazel b/MODULE.bazel index bc2514e..35a047d 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -21,3 +21,25 @@ use_repo( "io_k8s_sigs_yaml", "org_golang_x_oauth2", ) + +# ✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂ +# only dev_dependencies below this line +# ✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂✂ + +bazel_dep( + name = "rules_bazel_integration_test", + version = "0.27.0", + dev_dependency = True, +) + +bazel_binaries = use_extension( + "@rules_bazel_integration_test//:extensions.bzl", + "bazel_binaries", + dev_dependency = True, +) +bazel_binaries.download(version = "7.0.0") +bazel_binaries.download(version = "7.4.1") +bazel_binaries.download(version = "8.0.0rc6") +bazel_binaries.download(version = "latest") +bazel_binaries.download(version = "last_green") +use_repo(bazel_binaries, "bazel_binaries", "bazel_binaries_bazelisk", "build_bazel_bazel_7_0_0", "build_bazel_bazel_7_4_1", "build_bazel_bazel_8_0_0rc6", "build_bazel_bazel_last_green", "build_bazel_bazel_latest") diff --git a/agent/locate/locate.go b/agent/locate/locate.go index 647d10a..1ba4cfa 100644 --- a/agent/locate/locate.go +++ b/agent/locate/locate.go @@ -15,19 +15,25 @@ func Base() (string, error) { return base, nil } - cacheDir, err := os.UserCacheDir() - if err != nil { - return "", err - } - - workspaceDirectory, ok := os.LookupEnv("BUILD_WORKSPACE_DIRECTORY") + // In Bazel integration tests we can't (and shouldn't) + // touch the user's home directory. + // Instead, we operate under $TEST_TMPDIR + var cacheDir string + var err error + cacheDir, ok := os.LookupEnv("TEST_TMPDIR") if !ok { - workspaceDirectory, err = os.Getwd() + // On a normal run, we want to operate in $HOME/.cache (or $XDG_CACHE_HOME) + cacheDir, err = os.UserCacheDir() if err != nil { return "", err } } + workspaceDirectory, err := workspaceDirectory() + if err != nil { + return "", err + } + return path.Join(cacheDir, "tweag-credential-helper", installBasePathComponent(workspaceDirectory)), nil } @@ -71,6 +77,20 @@ func AgentPaths() (string, string, error) { } if !haveSocketPathFromEnv { socketPath = path.Join(run, "agent.sock") + if len(socketPath) >= 108 { + // In many environments + // we are not allowed to use + // a socket path longer than 108 bytes + // + // in those cases, fall back to a unique + // abstract uds (prefixed with @ in Go) + workspaceDirectory, err := workspaceDirectory() + if err != nil { + return "", "", err + } + socketPath = "@" + installBasePathComponent(workspaceDirectory) + } + } if !havePidPathFromEnv { pidPath = path.Join(run, "agent.pid") @@ -82,3 +102,15 @@ func AgentPaths() (string, string, error) { func installBasePathComponent(workspaceDirectory string) string { return fmt.Sprintf("%x", md5.Sum([]byte(workspaceDirectory))) } + +func workspaceDirectory() (string, error) { + workspaceDirectory, ok := os.LookupEnv("BUILD_WORKSPACE_DIRECTORY") + if ok { + return workspaceDirectory, nil + } + workspaceDirectory, err := os.Getwd() + if err != nil { + return "", err + } + return workspaceDirectory, nil +} diff --git a/agent/service.go b/agent/service.go index c367f6d..bb3de36 100644 --- a/agent/service.go +++ b/agent/service.go @@ -9,6 +9,7 @@ import ( "net" "os" "path/filepath" + "strings" "sync" "sync/atomic" "syscall" @@ -28,7 +29,9 @@ type CachingAgent struct { func NewCachingAgent(socketPath string, agentLockPath string, cache api.Cache) (*CachingAgent, func() error, error) { _ = os.MkdirAll(filepath.Dir(agentLockPath), 0o755) - _ = os.MkdirAll(filepath.Dir(socketPath), 0o755) + if !strings.HasPrefix(socketPath, "@") { + _ = os.MkdirAll(filepath.Dir(socketPath), 0o755) + } agentLock, err := os.OpenFile(agentLockPath, os.O_RDWR|os.O_CREATE, 0o666) if err != nil { @@ -45,7 +48,9 @@ func NewCachingAgent(socketPath string, agentLockPath string, cache api.Cache) ( } // delete the socket file if it already exists from a previous, dead agent - _ = os.Remove(socketPath) + if !strings.HasPrefix(socketPath, "@") { + _ = os.Remove(socketPath) + } listener, err := net.Listen("unix", socketPath) if err != nil { diff --git a/cmd/root/root.go b/cmd/root/root.go index 9f42c28..7fe4f4a 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -125,6 +125,7 @@ func launchOrConnectAgent() (api.Cache, func() error, error) { if err != nil { return nil, func() error { return nil }, err } + logging.Debugf("connecting to agent at %s", sockPath) socketCache, err := cache.NewSocketCache(sockPath, time.Second) if err != nil { return nil, func() error { return nil }, err diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel new file mode 100644 index 0000000..f54d12a --- /dev/null +++ b/examples/BUILD.bazel @@ -0,0 +1,68 @@ +load("@bazel_binaries//:defs.bzl", "bazel_binaries") +load( + "@rules_bazel_integration_test//bazel_integration_test:defs.bzl", + "bazel_integration_test", + "bazel_integration_tests", + "default_test_runner", + "integration_test_utils", +) + +default_test_runner( + name = "default_test_runner", + bazel_cmds = [ + "info", + "run @tweag-credential-helper//installer", + "test //...", + ], +) + +default_test_runner( + name = "customized_installer_test_runner", + bazel_cmds = [ + "info", + "run //:custom_installer", + "test //...", + ], +) + +bazel_integration_tests( + name = "full_test", + additional_env_inherit = [ + "CREDENTIAL_HELPER_LOGGING", + "GH_TOKEN", + "GITHUB_TOKEN", + "GOOGLE_APPLICATION_CREDENTIALS", + ], + bazel_versions = bazel_binaries.versions.all, + test_runner = ":default_test_runner", + workspace_files = integration_test_utils.glob_workspace_files("full") + [ + "//:local_repository_files", + ], + workspace_path = "full", +) + +bazel_integration_tests( + name = "customized_test", + bazel_versions = bazel_binaries.versions.all, + # This integration test does not use real credentials. + # It is safe to enable debug logging by default. + env = {"CREDENTIAL_HELPER_LOGGING": "debug"}, + test_runner = ":customized_installer_test_runner", + workspace_files = integration_test_utils.glob_workspace_files("customized") + [ + "//:local_repository_files", + ], + workspace_path = "customized", +) + +test_suite( + name = "integration_tests", + tags = integration_test_utils.DEFAULT_INTEGRATION_TEST_TAGS, + tests = integration_test_utils.bazel_integration_test_names( + "customized_test", + bazel_binaries.versions.all, + ) + integration_test_utils.bazel_integration_test_names( + "full_test", + bazel_binaries.versions.all, + ), + visibility = ["//:__subpackages__"], +) diff --git a/examples/customized/.bazelrc b/examples/customized/.bazelrc index ad38de5..2806c4e 100644 --- a/examples/customized/.bazelrc +++ b/examples/customized/.bazelrc @@ -1,4 +1,2 @@ -common --credential_helper=storage.googleapis.com=%workspace%/tools/credential-helper -common --credential_helper=github.com=%workspace%/tools/credential-helper -common --credential_helper=s3.amazonaws.com=%workspace%/tools/credential-helper -common --credential_helper=*.s3.amazonaws.com=%workspace%/tools/credential-helper +common --credential_helper=httpbin.org=%workspace%/tools/credential-helper +common --test_output=errors diff --git a/examples/customized/BUILD.bazel b/examples/customized/BUILD.bazel index 6d49773..39dff09 100644 --- a/examples/customized/BUILD.bazel +++ b/examples/customized/BUILD.bazel @@ -23,3 +23,13 @@ installer( name = "custom_installer", credential_helper = ":custom_credential_helper", ) + +sh_test( + name = "check_httpbin_basic_auth", + srcs = ["@tweag-credential-helper//examples/testing:check_file_hash.sh"], + args = [ + "$(location @httpbin_basic_auth//file)", + "862df46c49d3993226614a265acc0460169eb9d16951d581e9f139153ad95138", + ], + data = ["@httpbin_basic_auth//file"], +) diff --git a/examples/customized/MODULE.bazel b/examples/customized/MODULE.bazel index d82b1e0..460ad4a 100644 --- a/examples/customized/MODULE.bazel +++ b/examples/customized/MODULE.bazel @@ -25,3 +25,9 @@ use_repo( "org_modernc_sqlite", ) +http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") +http_file( + name = "httpbin_basic_auth", + integrity = "sha256-hi30bEnTmTImYUomWswEYBaeudFpUdWB6fE5FTrZUTg=", + urls = ["https://httpbin.org/basic-auth/password/swordfish"], +) diff --git a/examples/customized/WORKSPACE.bazel b/examples/customized/WORKSPACE.bazel new file mode 100644 index 0000000..c154a63 --- /dev/null +++ b/examples/customized/WORKSPACE.bazel @@ -0,0 +1 @@ +# this example uses MODULE.bazel diff --git a/examples/customized/helper/authenticate/pathtoheader.go b/examples/customized/helper/authenticate/pathtoheader.go index 14a7331..28c6e81 100644 --- a/examples/customized/helper/authenticate/pathtoheader.go +++ b/examples/customized/helper/authenticate/pathtoheader.go @@ -2,7 +2,10 @@ package authenticate import ( "context" + "encoding/base64" + "fmt" "net/url" + "strings" "time" "github.com/tweag/credential-helper/api" @@ -36,10 +39,18 @@ func (PathToHeader) Get(ctx context.Context, req api.GetCredentialsRequest) (api return api.GetCredentialsResponse{}, error } + headers := map[string][]string{"X-Tweag-Credential-Helper-Path": {parsedURL.Path}} + + // implement httpbin basic-auth for testing + // parts: "", "basic-auth", username, password + bAuthUsernameAndPass := strings.Split(parsedURL.Path, "/") + if len(bAuthUsernameAndPass) == 4 && bAuthUsernameAndPass[1] == "basic-auth" { + bAuthCredentialsPlain := fmt.Sprintf("%s:%s", bAuthUsernameAndPass[2], bAuthUsernameAndPass[3]) + headers["Authorization"] = []string{"Basic " + base64.StdEncoding.EncodeToString([]byte(bAuthCredentialsPlain))} + } + return api.GetCredentialsResponse{ Expires: time.Now().Add(time.Hour * 24).UTC().Format(time.RFC3339), - Headers: map[string][]string{ - "X-Tweag-Credential-Helper-Path": {parsedURL.Path}, - }, + Headers: headers, }, nil } diff --git a/examples/customized/helper/cache/BUILD.bazel b/examples/customized/helper/cache/BUILD.bazel index 9e4edbe..02b6602 100644 --- a/examples/customized/helper/cache/BUILD.bazel +++ b/examples/customized/helper/cache/BUILD.bazel @@ -7,6 +7,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "@org_modernc_sqlite//:sqlite", + "@tweag-credential-helper//agent/locate", "@tweag-credential-helper//api", ], ) diff --git a/examples/customized/helper/cache/sqlitecache.go b/examples/customized/helper/cache/sqlitecache.go index d76a1e0..d29edd3 100644 --- a/examples/customized/helper/cache/sqlitecache.go +++ b/examples/customized/helper/cache/sqlitecache.go @@ -11,6 +11,7 @@ import ( "database/sql" + "github.com/tweag/credential-helper/agent/locate" "github.com/tweag/credential-helper/api" _ "modernc.org/sqlite" ) @@ -132,10 +133,10 @@ func dbPath() (string, error) { } func varDir() (string, error) { - cacheDir, err := os.UserCacheDir() + base, err := locate.Base() if err != nil { return "", err } - return path.Join(cacheDir, "credential-helper", "var"), nil + return path.Join(base, "var"), nil } diff --git a/examples/full/.bazelrc b/examples/full/.bazelrc index ad38de5..a931634 100644 --- a/examples/full/.bazelrc +++ b/examples/full/.bazelrc @@ -2,3 +2,4 @@ common --credential_helper=storage.googleapis.com=%workspace%/tools/credential-h common --credential_helper=github.com=%workspace%/tools/credential-helper common --credential_helper=s3.amazonaws.com=%workspace%/tools/credential-helper common --credential_helper=*.s3.amazonaws.com=%workspace%/tools/credential-helper +common --test_output=errors diff --git a/examples/full/BUILD.bazel b/examples/full/BUILD.bazel new file mode 100644 index 0000000..220ca0f --- /dev/null +++ b/examples/full/BUILD.bazel @@ -0,0 +1,9 @@ +sh_test( + name = "check_github_repo", + srcs = ["@tweag-credential-helper//examples/testing:check_file_hash.sh"], + args = [ + "$(location @private_github_repo//:hello.txt)", + "81e455883ec5c65337edd7bd1045b4fea50fef093b40636d96aa8001ebd54910", + ], + data = ["@private_github_repo//:hello.txt"], +) diff --git a/examples/full/MODULE.bazel b/examples/full/MODULE.bazel index f21d8c7..9fb24d5 100644 --- a/examples/full/MODULE.bazel +++ b/examples/full/MODULE.bazel @@ -15,23 +15,24 @@ local_path_override( http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") http_archive( - name = "private-repo", + name = "private_github_repo", build_file_content = """exports_files(glob(["**"]))""", - integrity = "sha256-cohWIonag4gZimD2Z/L8NKE+r5XnqV2VPbZt14QkuaE=", - strip_prefix = "private-repo-0.0.1", - urls = ["https://github.com/my-org/private-repo/archive/refs/tags/v0.0.1.tar.gz"], + integrity = "sha256-/M5gC3mcEjAUvQtByW3jYMWnwR7eD3NTs7ZWUp2QcAI=", + strip_prefix = "credential-helper-private-0.0.1", + urls = ["https://github.com/tweag/credential-helper-private/archive/refs/tags/v0.0.1.tar.gz"], ) -http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") - -http_file( - name = "hello_world_s3", - integrity = "sha256-N5gMM5Ud5rDkUMNwGyGb/u6TBURwX2N80RWLY4J7s5A=", - urls = ["https://malte-s3-bazel-test.s3.amazonaws.com/hello_world"], -) - -http_file( - name = "hello_world_gcs", - integrity = "sha256-N5gMM5Ud5rDkUMNwGyGb/u6TBURwX2N80RWLY4J7s5A=", - urls = ["https://storage.googleapis.com/rules_gcs/hello_world"], -) +# temporarily removed until CI has required access +# http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") +# +# http_file( +# name = "hello_world_s3", +# integrity = "sha256-N5gMM5Ud5rDkUMNwGyGb/u6TBURwX2N80RWLY4J7s5A=", +# urls = ["https://malte-s3-bazel-test.s3.amazonaws.com/hello_world"], +# ) +# +# http_file( +# name = "hello_world_gcs", +# integrity = "sha256-N5gMM5Ud5rDkUMNwGyGb/u6TBURwX2N80RWLY4J7s5A=", +# urls = ["https://storage.googleapis.com/rules_gcs/hello_world"], +# ) diff --git a/examples/full/WORKSPACE.bazel b/examples/full/WORKSPACE.bazel new file mode 100644 index 0000000..c154a63 --- /dev/null +++ b/examples/full/WORKSPACE.bazel @@ -0,0 +1 @@ +# this example uses MODULE.bazel diff --git a/examples/testing/BUILD.bazel b/examples/testing/BUILD.bazel new file mode 100644 index 0000000..b26aece --- /dev/null +++ b/examples/testing/BUILD.bazel @@ -0,0 +1,7 @@ +exports_files(["check_file_hash.sh"]) + +filegroup( + name = "all_files", + srcs = glob(["*"]), + visibility = ["//:__subpackages__"], +) diff --git a/examples/testing/check_file_hash.sh b/examples/testing/check_file_hash.sh new file mode 100755 index 0000000..2abadbc --- /dev/null +++ b/examples/testing/check_file_hash.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -euo pipefail +set -o errtrace + +# This script checks the hash of a file against a known hash. + +while [ $# -gt 0 ]; do + path="$1" + sha256="$2" + echo $sha256 "*$path" | shasum -a 256 -c - + shift 2 +done diff --git a/logging/logging.go b/logging/logging.go index ee1f665..32f93a3 100644 --- a/logging/logging.go +++ b/logging/logging.go @@ -69,7 +69,7 @@ func boundedLogLevel(numericLevel int) LogLevel { } func fPrintOut(format string, args ...any) { - fmt.Fprintf(os.Stdout, fmtWithNewline(format), args...) + fmt.Fprintf(os.Stderr, fmtWithNewline(format), args...) } func fmtWithNewline(format string) string { diff --git a/testdata/basic_auth.json b/testdata/basic_auth.json new file mode 100644 index 0000000..a3b06b4 --- /dev/null +++ b/testdata/basic_auth.json @@ -0,0 +1,3 @@ +{ + "uri": "https://httpbin.org/basic-auth/password/swordfish" +} diff --git a/tools/credential-helper b/tools/credential-helper index e40af24..ffc1474 100755 --- a/tools/credential-helper +++ b/tools/credential-helper @@ -1,4 +1,7 @@ #!/usr/bin/env bash +set -eo pipefail +set -o errtrace + # TODO: support platforms other than Linux # You can add hardcoded configuration here or use environment variables when running Bazel @@ -23,13 +26,17 @@ credential_helper_install_command="bazel run @tweag-credential-helper//installer" +cache_dir="${HOME}/.cache" +if [ -n "${TEST_TMPDIR}" ]; then + cache_dir="${TEST_TMPDIR}" +fi if [ -n "${CREDENTIAL_HELPER_INSTALL_BASE}" ]; then install_base="${CREDENTIAL_HELPER_INSTALL_BASE}" else # Bazel spawns the credential helper process using the workspace root as working directory. # We abuse this fact to obtain a stable, workspace specific install dir (just like Bazel's output base). - install_base=("$(echo -n $(pwd) | md5sum)") - install_base="${HOME}/.cache/tweag-credential-helper/${install_base}" + install_base_hash=($(echo -n $(pwd) | md5sum)) + install_base="${cache_dir}/tweag-credential-helper/${install_base_hash[0]}" fi if [ -n "${CREDENTIAL_HELPER_BIN}" ]; then