From d06b19b6cd904da52c78cbd301bc26ffa4ea55af Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Sun, 27 Jul 2025 13:17:26 +0200 Subject: [PATCH 1/3] Global refactoring before new integration tests --- .gitattributes | 4 + .goreleaser.yaml | 2 +- Makefile | 165 ++++++++++++++++++++++--- cmd/launchr/{launchr.go => main.go} | 0 gen.go | 2 +- go.mod | 49 ++++---- go.sum | 110 +++++++++-------- internal/launchr/env.go | 66 ++++++++++ internal/launchr/filepath.go | 81 +++++++++++- internal/launchr/filepath_test.go | 27 +++- internal/launchr/filepath_unix.go | 24 ++-- internal/launchr/filepath_windows.go | 26 ++++ internal/launchr/lockedfile_windows.go | 2 +- internal/launchr/sensitive.go | 59 ++++++++- internal/launchr/tools.go | 9 ++ internal/launchr/types.go | 48 +------ internal/launchr/version.go | 42 +++++-- pkg/action/action.go | 77 +++++++++++- pkg/action/action.input.go | 3 +- pkg/action/action_test.go | 25 ++-- pkg/action/discover.go | 23 +++- pkg/action/discover_test.go | 6 + pkg/action/jsonschema.go | 3 +- pkg/action/loader.go | 114 ++++++++--------- pkg/action/loader_test.go | 18 ++- pkg/action/runtime.container.go | 45 +++---- pkg/action/runtime.container_test.go | 15 +-- pkg/action/runtime.go | 10 +- pkg/action/runtime.shell.go | 26 ++-- pkg/action/test_utils.go | 4 + pkg/action/utils.go | 88 +++++++++++++ pkg/action/utils_unix.go | 52 ++++++++ pkg/action/utils_windows.go | 132 ++++++++++++++++++++ pkg/action/yaml.discovery.go | 5 +- pkg/action/yaml_const_test.go | 2 +- pkg/archive/tar.go | 8 ++ pkg/driver/docker.go | 11 +- pkg/driver/iostream.go | 17 +-- pkg/driver/type.go | 1 - plugins/actionscobra/cobra.go | 29 +++-- plugins/builder/builder.go | 21 ++-- plugins/builder/environment.go | 62 ++++++---- plugins/builder/plugin.go | 17 +-- plugins/verbosity/plugin.go | 2 + 44 files changed, 1165 insertions(+), 367 deletions(-) create mode 100644 .gitattributes rename cmd/launchr/{launchr.go => main.go} (100%) create mode 100644 internal/launchr/env.go create mode 100644 pkg/action/utils_unix.go create mode 100644 pkg/action/utils_windows.go diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2f6b218 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Set default behavior to automatically normalize line endings +* text=auto eol=lf +*.txtar text eol=lf +*.sh text eol=lf diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 49d5070..0b963be 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -3,7 +3,7 @@ project_name: launchr before: hooks: - go mod download - - go test ./... + - go test -short ./... builds: - main: ./cmd/launchr env: diff --git a/Makefile b/Makefile index 2e9a3cb..cad5821 100644 --- a/Makefile +++ b/Makefile @@ -25,51 +25,184 @@ LOCAL_BIN:=$(CURDIR)/bin GOLANGCI_BIN:=$(LOCAL_BIN)/golangci-lint GOLANGCI_TAG:=1.64.5 +GOTESTFMT_BIN:=$(GOBIN)/gotestfmt + +# Color definitions +RED=\033[0;31m +GREEN=\033[0;32m +YELLOW=\033[0;33m +BLUE=\033[0;34m +MAGENTA=\033[0;35m +CYAN=\033[0;36m +WHITE=\033[0;37m +BOLD=\033[1m +RESET=\033[0m + +# Disable colors on Windows. +ifeq ($(OS),Windows_NT) + RED= + GREEN= + YELLOW= + BLUE= + MAGENTA= + CYAN= + WHITE= + BOLD= + RESET= +endif + +# Print functions +define print_header + @echo "$(BOLD)$(CYAN)╔═════════════════════════════════════════════════════════════╗$(RESET)" + @echo "$(BOLD)$(CYAN)║ LAUNCHR ║$(RESET)" + @echo "$(BOLD)$(CYAN)╚═════════════════════════════════════════════════════════════╝$(RESET)" +endef + +define print_success + @echo "$(BOLD)$(GREEN)✅ $(1)$(RESET)" + @echo +endef + +define print_info + @echo "$(BOLD)$(BLUE)📋 $(1)$(RESET)" + @echo +endef + +define print_warning + @echo "$(BOLD)$(YELLOW)⚠️ $(1)$(RESET)" + @echo +endef + +define print_error + @echo "$(BOLD)$(RED)❌ $(1)$(RESET)" + @echo +endef + +define print_step + @echo "$(BOLD)$(MAGENTA)🔧 $(1)$(RESET)" +endef + .PHONY: all -all: deps test build +all: banner deps test-short build + $(call print_success,"🎉 All tasks completed successfully!") + +.PHONY: banner +banner: + $(call print_header) + @echo "$(BOLD)$(WHITE)📦 Version: $(APP_VERSION)$(RESET)" + @echo "$(BOLD)$(WHITE)🌿 Branch: $(GIT_BRANCH)$(RESET)" + @echo "$(BOLD)$(WHITE)🔗 Hash: $(GIT_HASH)$(RESET)" + @echo # Install go dependencies .PHONY: deps deps: - $(info Installing go dependencies...) - go mod download + $(call print_step,"Installing go dependencies...") + @go mod download + $(call print_success,"Dependencies installed successfully!") # Run all tests .PHONY: test -test: - $(info Running tests...) - go test ./... +test: .install-gotestfmt + $(call print_step,"Running all tests...") + @go test -json -v ./... | $(GOTESTFMT_BIN) -hide all && \ + echo "$(BOLD)$(GREEN)🧪 ✅ All tests passed$(RESET)" || \ + echo "$(BOLD)$(RED)🧪 ❌ Some tests failed$(RESET)" + @echo + +# Run short tests +.PHONY: test-short +test-short: .install-gotestfmt + $(call print_step,"Running short tests...") + @go test -json -short -v ./... | $(GOTESTFMT_BIN) -hide all && \ + echo "$(BOLD)$(GREEN)🧪 ✅ All short tests passed$(RESET)" || \ + echo "$(BOLD)$(RED)🧪 ❌ Some short tests failed$(RESET)" + @echo # Build launchr .PHONY: build build: - $(info Building launchr...) + $(call print_step,"Building launchr...") # Application related information available on build time. $(eval LDFLAGS:=-X '$(GOPKG).name=launchr' -X '$(GOPKG).version=$(APP_VERSION)' $(LDFLAGS_EXTRA)) $(eval BIN?=$(LOCAL_BIN)/launchr) - go generate ./... - $(BUILD_ENVPARMS) go build -ldflags "$(LDFLAGS)" $(BUILD_OPTS) -o $(BIN) ./cmd/launchr + @go generate ./... + @$(BUILD_ENVPARMS) go build -ldflags "$(LDFLAGS)" $(BUILD_OPTS) -o $(BIN) ./cmd/launchr + $(call print_success,"🔨 Build completed: $(BIN)") # Install launchr .PHONY: install install: all -install: - $(info Installing launchr to GOPATH...) - cp $(LOCAL_BIN)/launchr $(GOBIN)/launchr + $(call print_step,"Installing launchr to GOPATH...") + @cp $(LOCAL_BIN)/launchr $(GOBIN)/launchr + $(call print_success,"🚀 launchr installed to $(GOBIN)/launchr") # Install and run linters .PHONY: lint -lint: .install-lint .lint +lint: .install-lint .lint-fix # Install golangci-lint binary .PHONY: .install-lint .install-lint: ifeq ($(wildcard $(GOLANGCI_BIN)),) - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(LOCAL_BIN) v$(GOLANGCI_TAG) + $(call print_step,"Installing golangci-lint v$(GOLANGCI_TAG)...") + @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(LOCAL_BIN) v$(GOLANGCI_TAG) + $(call print_success,"golangci-lint installed!") +endif + +# Install gotestfmt binary +.PHONY: .install-gotestfmt +.install-gotestfmt: +ifeq ($(wildcard $(GOTESTFMT_BIN)),) + $(call print_step,"Installing gotestfmt...") + @go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest + $(call print_success,"gotestfmt installed!") endif # Runs linters +.PHONY: .lint-fix +.lint-fix: + $(call print_step,"Running linters with auto-fix...") + @$(GOLANGCI_BIN) run --fix ./... && \ + echo "$(BOLD)$(GREEN)🔍 ✅ All linting checks passed$(RESET)" || \ + echo "$(BOLD)$(YELLOW)🔍 ⚠️ Some linting issues found - please review$(RESET)" + @echo + .PHONY: .lint .lint: - $(info Running lint...) - $(GOLANGCI_BIN) run --fix ./... + $(call print_step,"Running linters...") + @$(GOLANGCI_BIN) run && \ + echo "$(BOLD)$(GREEN)🔍 ✅ All linting checks passed$(RESET)" || \ + echo "$(BOLD)$(YELLOW)🔍 ⚠️ Some linting issues found - please review$(RESET)" + @echo + +# Clean build artifacts +.PHONY: clean +clean: + $(call print_step,"Cleaning build artifacts...") + @rm -rf $(LOCAL_BIN) + $(call print_success,"🧹 Cleanup completed!") + +# Show help +.PHONY: help +help: + $(call print_header) + @echo "$(BOLD)$(WHITE)Available targets:$(RESET)" + @echo "" + @echo " $(BOLD)$(GREEN)all$(RESET) 🎯 Run deps, test, and build" + @echo " $(BOLD)$(GREEN)deps$(RESET) 📦 Install go dependencies" + @echo " $(BOLD)$(GREEN)test$(RESET) 🧪 Run all tests" + @echo " $(BOLD)$(GREEN)test-short$(RESET) ⚡ Run short tests only" + @echo " $(BOLD)$(GREEN)build$(RESET) 🔨 Build launchr binary" + @echo " $(BOLD)$(GREEN)install$(RESET) 🚀 Install launchr to GOPATH" + @echo " $(BOLD)$(GREEN)lint$(RESET) 🔍 Run linters with auto-fix" + @echo " $(BOLD)$(GREEN)clean$(RESET) 🧹 Clean build artifacts" + @echo " $(BOLD)$(GREEN)help$(RESET) ❓ Show this help message" + @echo "" + @echo "$(BOLD)$(CYAN)Environment variables:$(RESET)" + @echo " $(BOLD)$(YELLOW)DEBUG=1$(RESET) Enable debug build" + @echo " $(BOLD)$(YELLOW)BIN=path$(RESET) Custom binary output path" + @echo "" + +# Default target shows help +.DEFAULT_GOAL := help diff --git a/cmd/launchr/launchr.go b/cmd/launchr/main.go similarity index 100% rename from cmd/launchr/launchr.go rename to cmd/launchr/main.go diff --git a/gen.go b/gen.go index ddc8103..a836804 100644 --- a/gen.go +++ b/gen.go @@ -20,7 +20,7 @@ func (app *appImpl) gen() error { // Set absolute paths. config.WorkDir = launchr.MustAbs(config.WorkDir) config.BuildDir = launchr.MustAbs(config.BuildDir) - // Change working directory to the selected. + // Change the working directory to the selected. err = os.Chdir(config.WorkDir) if err != nil { return err diff --git a/go.mod b/go.mod index 028714f..56823e6 100644 --- a/go.mod +++ b/go.mod @@ -5,24 +5,26 @@ go 1.24.0 toolchain go1.24.1 require ( - github.com/docker/docker v28.1.1+incompatible + github.com/containerd/errdefs v1.0.0 + github.com/docker/docker v28.3.2+incompatible github.com/knadh/koanf v1.5.0 github.com/moby/go-archive v0.1.0 github.com/moby/sys/signal v0.7.1 github.com/moby/term v0.5.2 - github.com/pterm/pterm v0.12.80 - github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 + github.com/pterm/pterm v0.12.81 + github.com/rogpeppe/go-internal v1.14.1 + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/spf13/cobra v1.9.1 - github.com/spf13/pflag v1.0.6 + github.com/spf13/pflag v1.0.7 github.com/stretchr/testify v1.10.0 - go.uber.org/mock v0.5.1 - golang.org/x/mod v0.24.0 - golang.org/x/sys v0.32.0 - golang.org/x/text v0.24.0 + go.uber.org/mock v0.5.2 + golang.org/x/mod v0.26.0 + golang.org/x/sys v0.34.0 + golang.org/x/text v0.27.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.33.0 - k8s.io/apimachinery v0.33.0 - k8s.io/client-go v0.33.0 + k8s.io/api v0.33.1 + k8s.io/apimachinery v0.33.1 + k8s.io/client-go v0.33.1 ) require ( @@ -31,7 +33,8 @@ require ( atomicgo.dev/schedule v0.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/containerd/console v1.0.4 // indirect + github.com/containerd/console v1.0.5 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect @@ -40,7 +43,7 @@ require ( github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.8.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect @@ -83,16 +86,18 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/oauth2 v0.29.0 // indirect - golang.org/x/term v0.31.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/term v0.33.0 // indirect golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.34.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect google.golang.org/protobuf v1.36.6 // indirect @@ -100,9 +105,9 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect + k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/yaml v1.5.0 // indirect ) diff --git a/go.sum b/go.sum index 0185208..4f67e3e 100644 --- a/go.sum +++ b/go.sum @@ -59,8 +59,12 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= -github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= +github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -76,8 +80,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= -github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA= +github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -108,8 +112,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= @@ -364,21 +368,21 @@ github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEej github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= -github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg= -github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo= +github.com/pterm/pterm v0.12.81 h1:ju+j5I2++FO1jBKMmscgh5h5DPFDFMB7epEjSoKehKA= +github.com/pterm/pterm v0.12.81/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= @@ -391,8 +395,9 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -419,29 +424,33 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3 go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs= -go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= +go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= @@ -461,8 +470,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -482,13 +491,13 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= -golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -539,15 +548,15 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -557,8 +566,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= @@ -575,8 +584,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -643,18 +652,18 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= -k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= -k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= -k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= -k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= +k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= +k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= +k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= +k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= +k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= -k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg= +k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= @@ -663,5 +672,6 @@ sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxO sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= +sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= diff --git a/internal/launchr/env.go b/internal/launchr/env.go new file mode 100644 index 0000000..8ccc5aa --- /dev/null +++ b/internal/launchr/env.go @@ -0,0 +1,66 @@ +package launchr + +import ( + "os" + "strings" + "syscall" +) + +// Application environment variables. +const ( + // EnvVarRootParentPID defines parent process id. May be used by forked processes. + EnvVarRootParentPID = EnvVar("root_ppid") + // EnvVarActionsPath defines path where to search for actions. + EnvVarActionsPath = EnvVar("actions_path") + // EnvVarLogLevel defines currently set log level, see --log-level or -v flag. + EnvVarLogLevel = EnvVar("log_level") + // EnvVarLogFormat defines currently set log format, see --log-format flag. + EnvVarLogFormat = EnvVar("log_format") + // EnvVarQuietMode defines if the application should output anything, see --quiet flag. + EnvVarQuietMode = EnvVar("quiet_mode") +) + +// EnvVar defines an environment variable and provides an interface to interact with it +// by prefixing the current app name. +// For example, if "my_var" is given as the variable name and the app name is "launchr", +// the accessed environment variable will be "LAUNCHR_MY_VAR". +type EnvVar string + +// String implements [fmt.Stringer] interface. +func (key EnvVar) String() string { + return strings.ToUpper(name + "_" + string(key)) +} + +// EnvString returns an os string of env variable with a value val. +func (key EnvVar) EnvString(val string) string { + return key.String() + "=" + val +} + +// Get returns env variable value. +func (key EnvVar) Get() string { + return os.Getenv(key.String()) +} + +// Set sets env variable. +func (key EnvVar) Set(val string) error { + return os.Setenv(key.String(), val) +} + +// Unset unsets env variable. +func (key EnvVar) Unset() error { + return os.Unsetenv(key.String()) +} + +// Getenv is an environment variable expand callback. +func Getenv(key string) string { + if key == "$" { + return "$" + } + // Replace all subexpressions. + if strings.Contains(key, "$") { + key = os.Expand(key, Getenv) + } + // @todo implement ${var-$DEFAULT}, ${var:-$DEFAULT}, ${var+$DEFAULT}, ${var:+$DEFAULT}, + v, _ := syscall.Getenv(key) + return v +} diff --git a/internal/launchr/filepath.go b/internal/launchr/filepath.go index 6a675f6..c9384c1 100644 --- a/internal/launchr/filepath.go +++ b/internal/launchr/filepath.go @@ -7,6 +7,8 @@ import ( osuser "os/user" "path/filepath" "reflect" + "regexp" + "strings" ) // MustAbs returns absolute filepath and panics on error. @@ -63,7 +65,20 @@ func EnsurePath(parts ...string) error { // IsHiddenPath checks if a path is hidden path. func IsHiddenPath(path string) bool { - return isHiddenPath(path) + return isDotPath(path) || isHiddenPath(path) +} + +func isDotPath(path string) bool { + if path == "." { + return false + } + dirs := strings.Split(filepath.ToSlash(path), "/") + for _, v := range dirs { + if v[0] == '.' { + return true + } + } + return false } // IsSystemPath checks if a path is a system path. @@ -116,7 +131,10 @@ func MkdirTemp(pattern string) (string, error) { if err != nil { u = &osuser.User{} } + baseCand := []string{ + // User defined. + strings.TrimSpace(os.Getenv("GOTMPDIR")), // Linux tmpfs paths. "/dev/shm", // Should be available for all. "/run/user/" + u.Uid, // User specific. @@ -125,6 +143,9 @@ func MkdirTemp(pattern string) (string, error) { // It will be used for Windows and macOS. os.TempDir(), } + if baseCand[0] == "" { + baseCand = baseCand[1:] + } basePath := "" dirPath := "" for _, cand := range baseCand { @@ -158,6 +179,16 @@ func MkdirTemp(pattern string) (string, error) { if dirPath == "" { return "", fmt.Errorf("failed to create temp directory") } + return dirPath, nil +} + +// MkdirTempWithCleanup creates a temporary directory with MkdirTemp. +// The temp directory is removed when the app terminates. +func MkdirTempWithCleanup(pattern string) (string, error) { + dirPath, err := MkdirTemp(pattern) + if err != nil { + return "", err + } // Make sure the dir is cleaned on finish. RegisterCleanupFn(func() error { @@ -166,3 +197,51 @@ func MkdirTemp(pattern string) (string, error) { return dirPath, nil } + +// EscapePathString escapes characters that may be +// incorrectly treated as a string like backslash "\" in a Windows path. +func EscapePathString(s string) string { + if filepath.Separator == '/' { + return s + } + return strings.Replace(s, "\\", "\\\\", -1) +} + +// ConvertWindowsPath converts Windows paths to Docker-compatible paths +func ConvertWindowsPath(windowsPath string) string { + // Regular expression to match Windows drive letters (C:, D:, etc.) + driveRegex := regexp.MustCompile(`^([A-Za-z]):[\\/](.*)`) + + // Check if it's a Windows absolute path with drive letter + if matches := driveRegex.FindStringSubmatch(windowsPath); matches != nil { + driveLetter := strings.ToLower(matches[1]) + restOfPath := matches[2] + + // Convert backslashes to forward slashes + restOfPath = strings.ReplaceAll(restOfPath, "\\", "/") + + // Return Docker-style path: /c/path/to/file + if restOfPath == "" { + return fmt.Sprintf("/%s/", driveLetter) + } + return fmt.Sprintf("/%s/%s", driveLetter, restOfPath) + } + + // Handle root drive paths like "C:\" + rootDriveRegex := regexp.MustCompile(`^([A-Za-z]):\\?$`) + if matches := rootDriveRegex.FindStringSubmatch(windowsPath); matches != nil { + driveLetter := strings.ToLower(matches[1]) + return fmt.Sprintf("/%s/", driveLetter) + } + + // Handle UNC paths (\\server\share\path) + if strings.HasPrefix(windowsPath, "\\\\") { + // Remove leading \\ and convert backslashes to forward slashes + uncPath := strings.TrimPrefix(windowsPath, "\\\\") + uncPath = strings.ReplaceAll(uncPath, "\\", "/") + return "//" + uncPath + } + + // Handle relative paths and other cases - just convert backslashes to forward slashes + return strings.ReplaceAll(windowsPath, "\\", "/") +} diff --git a/internal/launchr/filepath_test.go b/internal/launchr/filepath_test.go index 8128b55..27ff330 100644 --- a/internal/launchr/filepath_test.go +++ b/internal/launchr/filepath_test.go @@ -11,7 +11,7 @@ import ( func TestMkdirTemp(t *testing.T) { t.Parallel() - dir, err := MkdirTemp("test") + dir, err := MkdirTempWithCleanup("test") require.NoError(t, err) require.NotEmpty(t, dir) stat, err := os.Stat(dir) @@ -54,3 +54,28 @@ func TestFsRealpath(t *testing.T) { path = FsRealpath(subfs) assert.Equal(t, "", path) } + +func TestConvertWindowsPath(t *testing.T) { + t.Parallel() + tests := []struct { + name string + inp string + exp string + }{ + {"C drive path", `C:\Users\john\Documents`, `/c/Users/john/Documents`}, + {"D drive path", `D:\Projects\myapp`, `/d/Projects/myapp`}, + {"Root C drive", `C:\`, `/c/`}, + {"UNC path", `\\server\share\folder`, `//server/share/folder`}, + {"Relative path", `.\relative\path`, `./relative/path`}, + {"Already Unix path", `/already/unix/path`, `/already/unix/path`}, + {"Program Files path", `C:\Program Files\Docker`, `/c/Program Files/Docker`}} + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := ConvertWindowsPath(tt.inp) + assert.Equal(t, tt.exp, result) + }) + } +} diff --git a/internal/launchr/filepath_unix.go b/internal/launchr/filepath_unix.go index 9d0241b..6109712 100644 --- a/internal/launchr/filepath_unix.go +++ b/internal/launchr/filepath_unix.go @@ -4,7 +4,6 @@ package launchr import ( "path/filepath" - "strings" ) var skipRootDirs = []string{ @@ -46,17 +45,7 @@ var skipUserDirs = []string{ } func isHiddenPath(path string) bool { - if path == "." { - return false - } - dirs := strings.Split(path, string(filepath.Separator)) - for _, v := range dirs { - if v[0] == '.' { - return true - } - } - - return false + return isDotPath(path) } func isRootPath(path string) bool { @@ -69,3 +58,14 @@ func isUserHomeDir(path string) bool { macOs, _ := filepath.Match("/Users/*/*", abs) return linux || macOs } + +// KnownBashPaths returns paths where bash can be found. Used when PATH is not available. +func KnownBashPaths() []string { + return []string{ + "/bin/bash", + "/usr/bin/bash", + "/usr/local/bin/bash", + "/bin/ash", + "/bin/sh", // Fallback to sh. + } +} diff --git a/internal/launchr/filepath_windows.go b/internal/launchr/filepath_windows.go index c4491ae..48ba69e 100644 --- a/internal/launchr/filepath_windows.go +++ b/internal/launchr/filepath_windows.go @@ -3,6 +3,7 @@ package launchr import ( + "os" "path/filepath" "syscall" ) @@ -54,3 +55,28 @@ func isUserHomeDir(path string) bool { win, _ := filepath.Match(`C:\Users\*\*`, abs) return win } + +// KnownBashPaths returns paths where bash can be found. Used when PATH is not available. +func KnownBashPaths() []string { + // System-wide installation paths + paths := []string{ + "C:\\msys64\\usr\\bin\\bash.exe", + "C:\\msys32\\usr\\bin\\bash.exe", + "C:\\Program Files\\Git\\bin\\bash.exe", + "C:\\Program Files (x86)\\Git\\bin\\bash.exe", + "C:\\cygwin64\\bin\\bash.exe", + "C:\\cygwin\\bin\\bash.exe", + } + + // Get user's home directory + userHome, err := os.UserHomeDir() + if err == nil { + // User-specific installation paths + paths = append([]string{ + filepath.Join(userHome, "scoop", "apps", "git", "current", "bin", "bash.exe"), + filepath.Join(userHome, "AppData", "Local", "Programs", "Git", "bin", "bash.exe"), + filepath.Join(userHome, ".gitbash", "bin", "bash.exe"), + }, paths...) + } + return paths +} diff --git a/internal/launchr/lockedfile_windows.go b/internal/launchr/lockedfile_windows.go index 8113bae..419c6c1 100644 --- a/internal/launchr/lockedfile_windows.go +++ b/internal/launchr/lockedfile_windows.go @@ -31,7 +31,7 @@ func (f *LockedFile) unlock() { ol := new(windows.Overlapped) err := windows.UnlockFileEx(windows.Handle(f.file.Fd()), 0, allBytes, allBytes, ol) if err != nil { - Log().Warn("unlock is called on a not locked file: %s", err) + Log().Warn("unlock is called on a not locked file", "err", err) } f.locked = false } diff --git a/internal/launchr/sensitive.go b/internal/launchr/sensitive.go index 148da98..ea44aed 100644 --- a/internal/launchr/sensitive.go +++ b/internal/launchr/sensitive.go @@ -58,8 +58,11 @@ func (m *MaskingWriter) Write(p []byte) (n int, err error) { // If no complete sensitive data was found, keep everything in the buf. // Write the buffer periodically if the input slice `p` is less than its capacity. - if len(p) < cap(p) && m.buf.Len() > 0 { - // Write all remaining buffer content after masking. + // Check if we should flush based on content + shouldFlush := m.shouldFlush(p) + + // If we should flush AND there's no potential sensitive data at the end, flush + if shouldFlush && !m.hasPotentialSensitiveData() { if _, writeErr := m.w.Write(m.buf.Bytes()); writeErr != nil { return 0, writeErr } @@ -70,6 +73,58 @@ func (m *MaskingWriter) Write(p []byte) (n int, err error) { return len(p), nil } +// shouldFlush determines if we should flush based on the content +func (m *MaskingWriter) shouldFlush(p []byte) bool { + // Flush on newlines (most common for terminal output) + if bytes.Contains(p, []byte{'\n'}) { + return true + } + + // Flush on other natural boundaries + if bytes.Contains(p, []byte{'\r'}) || bytes.Contains(p, []byte{'\t'}) { + return true + } + + // Flush if buffer is getting large (safety valve) + if m.buf.Len() > 4096 { + return true + } + + return false +} + +// hasPotentialSensitiveData checks if buffer might contain partial sensitive data +func (m *MaskingWriter) hasPotentialSensitiveData() bool { + if len(m.mask.strings) == 0 { + return false + } + + bufData := m.buf.Bytes() + bufLen := len(bufData) + + // Check if any sensitive string could be partially present at the end + for _, sensitive := range m.mask.strings { + sensitiveLen := len(sensitive) + if sensitiveLen <= 1 { + continue // Skip very short patterns + } + + // Check if any prefix of the sensitive string matches the end of our buffer + maxCheck := sensitiveLen - 1 + if maxCheck > bufLen { + maxCheck = bufLen + } + + for i := 1; i <= maxCheck; i++ { + if bytes.HasSuffix(bufData, sensitive[:i]) { + return true + } + } + } + + return false +} + // Close flushes any remaining data in the buf. func (m *MaskingWriter) Close() error { if m.buf.Len() > 0 { diff --git a/internal/launchr/tools.go b/internal/launchr/tools.go index 5f527bc..7881196 100644 --- a/internal/launchr/tools.go +++ b/internal/launchr/tools.go @@ -163,3 +163,12 @@ func GetRandomString(length int) string { } return string(b) } + +// Executable returns the path name for the executable that started. +func Executable() string { + currentBin, err := os.Executable() + if err != nil { + return name + } + return currentBin +} diff --git a/internal/launchr/types.go b/internal/launchr/types.go index 446703f..a851666 100644 --- a/internal/launchr/types.go +++ b/internal/launchr/types.go @@ -7,26 +7,11 @@ import ( "os" "path/filepath" "strconv" - "strings" "text/template" "github.com/spf13/cobra" ) -// Application environment variables. -const ( - // EnvVarRootParentPID defines parent process id. May be used by forked processes. - EnvVarRootParentPID = EnvVar("root_ppid") - // EnvVarActionsPath defines path where to search for actions. - EnvVarActionsPath = EnvVar("actions_path") - // EnvVarLogLevel defines currently set log level, see --log-level or -v flag. - EnvVarLogLevel = EnvVar("log_level") - // EnvVarLogFormat defines currently set log format, see --log-format flag. - EnvVarLogFormat = EnvVar("log_format") - // EnvVarQuietMode defines if the application should output anything, see --quiet flag. - EnvVarQuietMode = EnvVar("quiet_mode") -) - // PkgPath is a main module path. const PkgPath = "github.com/launchrctl/launchr" @@ -87,10 +72,12 @@ type AppVersion struct { Version string OS string Arch string + Debug bool BuiltWith string CoreVersion string CoreReplace string Plugins []string + PluginsRepl []string } // PluginInfo provides information about the plugin and is used as a unique data to identify a plugin. @@ -268,34 +255,3 @@ func (e ExitError) Error() string { func (e ExitError) ExitCode() int { return e.code } - -// EnvVar defines an environment variable and provides an interface to interact with it -// by prefixing the current app name. -// For example, if "my_var" is given as the variable name and the app name is "launchr", -// the accessed environment variable will be "LAUNCHR_MY_VAR". -type EnvVar string - -// String implements [fmt.Stringer] interface. -func (key EnvVar) String() string { - return strings.ToUpper(name + "_" + string(key)) -} - -// EnvString returns an os string of env variable with a value val. -func (key EnvVar) EnvString(val string) string { - return key.String() + "=" + val -} - -// Get returns env variable value. -func (key EnvVar) Get() string { - return os.Getenv(key.String()) -} - -// Set sets env variable. -func (key EnvVar) Set(val string) error { - return os.Setenv(key.String(), val) -} - -// Unset unsets env variable. -func (key EnvVar) Unset() error { - return os.Unsetenv(key.String()) -} diff --git a/internal/launchr/version.go b/internal/launchr/version.go index 8f3a20f..bfbded4 100644 --- a/internal/launchr/version.go +++ b/internal/launchr/version.go @@ -43,22 +43,25 @@ func NewVersion(name, ver, bwith string, plugins PluginsMap) *AppVersion { buildInfo, _ := debug.ReadBuildInfo() // Add self as a dependency to get version for it also. buildInfo.Deps = append(buildInfo.Deps, &buildInfo.Main) - // Check core version when built or used in a plugin. + // Check a core version when built or used in a plugin. var coreRep string coreVer, coreRep := getCoreInfo(ver, buildInfo) if bwith == "" { ver = coreVer } + plver, plrepl := getPluginModules(plugins, buildInfo) return &AppVersion{ Name: name, Version: ver, OS: runtime.GOOS, Arch: runtime.GOARCH, + Debug: isDebugAvailable(buildInfo), CoreVersion: coreVer, CoreReplace: coreRep, BuiltWith: bwith, - Plugins: getPluginModules(plugins, buildInfo), + Plugins: plver, + PluginsRepl: plrepl, } } @@ -101,12 +104,13 @@ func getCoreInfo(v string, bi *debug.BuildInfo) (ver string, repl string) { return } -func getPluginModules(plugins PluginsMap, bi *debug.BuildInfo) []string { +func getPluginModules(plugins PluginsMap, bi *debug.BuildInfo) (res []string, repl []string) { if bi == nil { - return nil + return } - res := make([]string, 0, len(plugins)) + res = make([]string, 0, len(plugins)) + repl = make([]string, 0, len(plugins)) for pi := range plugins { if strings.HasPrefix(pi.pkgPath, PkgPath) { // Do not include info about the default package. @@ -116,15 +120,32 @@ func getPluginModules(plugins PluginsMap, bi *debug.BuildInfo) []string { // Path may be empty on "go run". if d.Path != "" && strings.HasPrefix(pi.pkgPath, d.Path) { s := fmt.Sprintf("%s %s", pi.pkgPath, d.Version) + r := s if d.Replace != nil { - s = fmt.Sprintf("%s => %s %s", s, d.Replace.Path, d.Replace.Version) + r = fmt.Sprintf("%s => %s %s", r, d.Replace.Path, d.Replace.Version) } res = append(res, s) + repl = append(res, r) } } } sort.Strings(res) - return res + sort.Strings(repl) + return +} + +// isDebugAvailable checks if the current binary has debug headers available. +func isDebugAvailable(bi *debug.BuildInfo) bool { + // Check for debug-related build settings + for _, setting := range bi.Settings { + if setting.Key == "-gcflags" { + // Check if gcflags contains debug-related flags + if strings.Contains(setting.Value, "-N") && strings.Contains(setting.Value, "-l") { + return true + } + } + } + return false } var versionTmpl = template.Must(template.New("version").Parse(versionTmplStr)) @@ -134,15 +155,18 @@ const versionTmplStr = ` {{- if .BuiltWith}} Built with {{.BuiltWith}} {{- end}} +{{- if .Debug}} +Built with debug headers +{{- end}} {{- if ne .CoreVersion .Version}} Core version: {{.CoreVersion}} {{- end}} {{- if .CoreReplace}} Core replace: {{.CoreReplace}} {{- end}} -{{- if .Plugins}} +{{- if .PluginsRepl}} Plugins: - {{- range .Plugins}} + {{- range .PluginsRepl}} - {{.}} {{- end}} {{end}}` diff --git a/pkg/action/action.go b/pkg/action/action.go index 5d3cc3f..4edd280 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -146,12 +146,12 @@ func (a *Action) syncToDisk() (err error) { // Export to a temporary path. // Make sure the path doesn't have semicolons, because Docker bind doesn't like it. tmpDirName := strings.Replace(a.ID, ":", "_", -1) - tmpDir, err := launchr.MkdirTemp(tmpDirName) + tmpDir, err := launchr.MkdirTempWithCleanup(tmpDirName) if err != nil { return } // We use subpath if there are multiple directories in the FS. - fsys, err := fs.Sub(a.fs.fs, filepath.Dir(a.Filepath())) + fsys, err := fs.Sub(a.fs.fs, filepath.ToSlash(filepath.Dir(a.Filepath()))) if err != nil { return } @@ -162,6 +162,7 @@ func (a *Action) syncToDisk() (err error) { } // Set a new filesystem to a cached path. a.fs = NewDiscoveryFS(os.DirFS(tmpDir), a.fs.wd) + a.fpath = filepath.Base(a.fpath) return } @@ -271,3 +272,75 @@ func (a *Action) Execute(ctx context.Context) error { } return a.runtime.Execute(ctx, a) } + +func (a *Action) getTemplateVars() *actionVars { + return newPredefinedVars(a) +} + +type actionVars struct { + a *Action + + uid string + gid string + actionID string + currentBin string // Get the path of the executable on the host. + actionWD string // app working directory + actionDir string // directory of action file + discoveryDir string // root directory where the action was found +} + +func newPredefinedVars(a *Action) *actionVars { + pv := &actionVars{a: a} + pv.init() + return pv +} + +func (v *actionVars) init() { + cuser := getCurrentUser() + v.currentBin = launchr.Executable() + v.uid = cuser.UID + v.gid = cuser.GID + if v.a != nil { + v.actionID = v.a.ID + v.actionWD = v.a.WorkDir() + v.actionDir = v.a.Dir() + v.discoveryDir = v.a.fs.Realpath() + } +} + +func (v *actionVars) templateData() map[string]string { + return map[string]string{ + "current_uid": v.uid, + "current_gid": v.gid, + "current_bin": v.currentBin, + "current_working_dir": v.actionWD, + "action_dir": v.actionDir, + "actions_base_dir": v.discoveryDir, + } +} + +func (v *actionVars) envData() map[string]string { + return map[string]string{ + "UID": v.uid, + "GID": v.gid, + "CBIN": v.currentBin, + "ACTION_ID": v.actionID, + "ACTION_WD": v.actionWD, + "ACTION_DIR": v.actionDir, + "DISCOVERY_DIR": v.discoveryDir, + } +} + +func (v *actionVars) envStrings() []string { + res := make([]string, 0, len(v.envData())) + for key, val := range v.envData() { + res = append(res, key+"="+val) + } + return res +} + +func (v *actionVars) getenv(key string) (string, bool) { + env := v.envData() + res, ok := env[key] + return res, ok +} diff --git a/pkg/action/action.input.go b/pkg/action/action.input.go index a8edfc7..20fde00 100644 --- a/pkg/action/action.input.go +++ b/pkg/action/action.input.go @@ -264,7 +264,8 @@ func CastSliceTypedToAny(slice any) []any { func CastSliceAnyToTyped[T any](orig []any) []T { res := make([]T, len(orig)) for i := 0; i < len(orig); i++ { - res[i] = orig[i].(T) + // Ignore the convert error if the value is not of the specified type. + res[i], _ = orig[i].(T) } return res } diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 3fe7fa9..ad575b9 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -3,7 +3,6 @@ package action import ( "context" "fmt" - "io/fs" "os" "path/filepath" "strings" @@ -30,12 +29,13 @@ func Test_Action(t *testing.T) { require.NotEmpty(actions) act := actions[0] // Override the real path to skip [Action.syncToDisc]. - act.fs.real = "/fstest/" + act.fs.real = filepath.FromSlash("/fstest/") // Test dir assert.Equal(act.fs.real+filepath.Dir(act.fpath), act.Dir()) - act.fpath = "test/file/path/action.yaml" - assert.Equal(act.fs.real+"test/file/path", act.Dir()) + // Test dir when fpath changed. + act.fpath = filepath.FromSlash("test/file/path/action.yaml") + assert.Equal(filepath.FromSlash(act.fs.real+"test/file/path"), act.Dir()) // Test arguments and options. inputArgs := InputParams{"arg1": "arg1", "arg2": "arg2", "arg-1": "arg-1", "arg_12": "arg_12_enum1"} @@ -103,20 +103,11 @@ func Test_Action(t *testing.T) { func Test_Action_NewYAMLFromFS(t *testing.T) { t.Parallel() - // Prepare FS. - fsys := genFsTestMapActions(1, validFullYaml, genPathTypeArbitrary) - // Get first key to make subdir. - var key string - for key = range fsys { - // There is only 1 entry, we get the only key. - break - } - - // Create action. - subfs, _ := fs.Sub(fsys, filepath.Dir(key)) - a, err := NewYAMLFromFS("test", subfs) - require.NotNil(t, a) + // Create action in memory FS. + fsys := genFsTestMapActions(1, validFullYaml, genPathTypeRoot) + a, err := NewYAMLFromFS("test", fsys) require.NoError(t, err) + require.NotNil(t, a) assert.Equal(t, "test", a.ID) require.NoError(t, a.EnsureLoaded()) assert.Equal(t, "Title", a.ActionDef().Title) diff --git a/pkg/action/discover.go b/pkg/action/discover.go index 445aebb..c0951f6 100644 --- a/pkg/action/discover.go +++ b/pkg/action/discover.go @@ -7,7 +7,6 @@ import ( "os" "path/filepath" "sort" - "strings" "sync" "time" @@ -16,7 +15,7 @@ import ( const actionsDirname = "actions" -var actionsSubdir = strings.Join([]string{"", actionsDirname, ""}, string(filepath.Separator)) +var actionsSubdir = filepath.FromSlash("/" + actionsDirname + "/") // DiscoveryPlugin is a launchr plugin to discover actions. type DiscoveryPlugin interface { @@ -183,6 +182,19 @@ func (ad *Discovery) Discover(ctx context.Context) ([]*Action, error) { // Traverse the FS. chFiles, chErr := ad.findFiles(ctx) + + // Check traversing the tree didn't have error. + // Usually no error, because we check for permissions. + var discoverErr error + errDone := make(chan struct{}) + go func() { + defer close(errDone) + if err := <-chErr; err != nil { + discoverErr = err + } + }() + + // Process files. for f := range chFiles { wg.Add(1) go func(f string) { @@ -196,10 +208,9 @@ func (ad *Discovery) Discover(ctx context.Context) ([]*Action, error) { } wg.Wait() - // Check traversing the tree didn't have error. - // Usually no error, because we check for permissions. - if err := <-chErr; err != nil { - return nil, err + // Wait for error handling to complete + if <-errDone; discoverErr != nil { + return nil, discoverErr } // Sort alphabetically. diff --git a/pkg/action/discover_test.go b/pkg/action/discover_test.go index b98bddd..37d1572 100644 --- a/pkg/action/discover_test.go +++ b/pkg/action/discover_test.go @@ -120,6 +120,12 @@ func Test_Discover_isValid(t *testing.T) { {"incorrect hidden subdir path", "1/2/.github/actions/3/action.yml", false}, // Invalid hidden subdirectory. {"nested action", "1/2/actions/3/4/5/action.yaml", false}, // There is a deeper nesting in actions directory. {"root action", "actions/verb/action.yaml", true}, // Actions are located in root. + {"special chars action in root 1", "actions/foo bar/action.yaml", false}, // Actions are located in root and with special characters. + {"special chars action in root 2", "actions/foo!bar/action.yaml", false}, // Actions are located in root and with special characters. + {"special chars action 1", "?/actions/foo/action.yaml", false}, // Actions with special characters. + {"special chars action 2", "foo/actions/foo<>bar/action.yaml", false}, // Actions with special characters. + {"special chars action 3", "foo/!<>/actions/foo\\bar/action.yaml", false}, // Actions with special characters. + {"special chars action 4", "foo bar/actions/baz/action.yaml", false}, // Actions with special characters. {"root myactions", "myactions/verb/action.yaml", false}, // Actions are located in dir ending with actions. {"dir", "1/2/actions/3", false}, // A directory is given. } diff --git a/pkg/action/jsonschema.go b/pkg/action/jsonschema.go index fe88575..527dfa4 100644 --- a/pkg/action/jsonschema.go +++ b/pkg/action/jsonschema.go @@ -3,6 +3,7 @@ package action import ( "fmt" "maps" + "path/filepath" "github.com/launchrctl/launchr/pkg/jsonschema" ) @@ -33,7 +34,7 @@ func (a *Action) JSONSchema() jsonschema.Schema { // Set ID to a filepath. It's not exactly JSON Schema, but some canonical path. // It's better to override the value, if the ID is needed by a validator. // In launchr, the id is overridden on loader, in web plugin with a server url. - s.ID = a.Filepath() + s.ID = "file:///" + filepath.ToSlash(a.Filepath()) // For plugin defined actions, filepath may be empty. if s.ID == "" { s.ID = a.ID diff --git a/pkg/action/loader.go b/pkg/action/loader.go index e3b1799..6303799 100644 --- a/pkg/action/loader.go +++ b/pkg/action/loader.go @@ -6,7 +6,6 @@ import ( "os" "regexp" "strings" - "syscall" "text/template" "github.com/launchrctl/launchr/internal/launchr" @@ -59,24 +58,19 @@ func (p *pipeProcessor) Process(ctx LoadContext, b []byte) ([]byte, error) { type envProcessor struct{} -func (p envProcessor) Process(_ LoadContext, b []byte) ([]byte, error) { +func (p envProcessor) Process(ctx LoadContext, b []byte) ([]byte, error) { + pv := newPredefinedVars(ctx.Action) + getenv := func(key string) string { + v, ok := pv.getenv(key) + if ok { + return v + } + return launchr.Getenv(key) + } s := os.Expand(string(b), getenv) return []byte(s), nil } -func getenv(key string) string { - if key == "$" { - return "$" - } - // Replace all subexpressions. - if strings.Contains(key, "$") { - key = os.Expand(key, getenv) - } - // @todo implement ${var-$DEFAULT}, ${var:-$DEFAULT}, ${var+$DEFAULT}, ${var:+$DEFAULT}, - v, _ := syscall.Getenv(key) - return v -} - type inputProcessor struct{} var rgxTplVar = regexp.MustCompile(`{{.*?\.([a-zA-Z][a-zA-Z0-9_]*).*?}}`) @@ -186,22 +180,13 @@ func (p inputProcessor) Process(ctx LoadContext, b []byte) ([]byte, error) { tpl := template.New(a.ID).Funcs(actionTplFuncs(a.Input())) _, err := tpl.Parse(string(b)) + // Check if variables have dashes to show the error properly. + err = checkDashErr(err, data) if err != nil { - // Check if variables have dashes to show the error properly. - hasDash := false - for k := range data { - if strings.Contains(k, "-") { - hasDash = true - break - } - } - if hasDash && strings.Contains(err.Error(), "bad character U+002D '-'") { - return nil, fmt.Errorf(`unexpected '-' symbol in a template variable. -Action definition is correct, but dashes are not allowed in templates, replace "-" with "_" in {{ }} blocks`) - } return nil, err } + // Execute template. buf := bytes.NewBuffer(make([]byte, 0, len(b))) err = tpl.Execute(buf, data) if err != nil { @@ -209,21 +194,10 @@ Action definition is correct, but dashes are not allowed in templates, replace " } // Find if some vars were used but not defined in arguments or options. - miss := make(map[string]struct{}) res := buf.Bytes() - if bytes.Contains(res, []byte("")) { - matches := rgxTplVar.FindAllSubmatch(b, -1) - for _, m := range matches { - k := string(m[1]) - if _, ok := data[k]; !ok { - miss[k] = struct{}{} - } - } - // If we don't have parameter names here, it means that all parameters are defined but the values were nil. - // It's ok, users will be able to identify missing parameters. - if len(miss) != 0 { - return nil, errMissingVar{miss} - } + err = findMissingVars(b, res, data) + if err != nil { + return nil, err } // Remove all lines containing [tokenRmLine]. @@ -264,22 +238,48 @@ func collectInputVars(values map[string]any, params InputParams, def ParametersL } func addPredefinedVariables(data map[string]any, a *Action) { - cuser := getCurrentUser() - // Set zeros for running in environments like Windows - data["current_uid"] = 0 - data["current_gid"] = 0 - if cuser != "" { - s := strings.Split(cuser, ":") - data["current_uid"] = s[0] - data["current_gid"] = s[1] + // TODO: Deprecated, use env variables. + pv := newPredefinedVars(a) + for k, v := range pv.templateData() { + data[k] = v } - data["current_working_dir"] = a.wd // app working directory - data["actions_base_dir"] = a.fs.Realpath() // root directory where the action was found - data["action_dir"] = a.Dir() // directory of action file - // Get the path of the executable on the host. - bin, err := os.Executable() - if err != nil { - bin = launchr.Version().Name +} + +func checkDashErr(err error, data map[string]any) error { + if err == nil { + return nil + } + // Check if variables have dashes to show the error properly. + hasDash := false + for k := range data { + if strings.Contains(k, "-") { + hasDash = true + break + } + } + if hasDash && strings.Contains(err.Error(), "bad character U+002D '-'") { + return fmt.Errorf(`unexpected '-' symbol in a template variable. +Action definition is correct, but dashes are not allowed in templates, replace "-" with "_" in {{ }} blocks`) + } + return err +} + +func findMissingVars(orig, repl []byte, data map[string]any) error { + miss := make(map[string]struct{}) + if !bytes.Contains(repl, []byte("")) { + return nil + } + matches := rgxTplVar.FindAllSubmatch(orig, -1) + for _, m := range matches { + k := string(m[1]) + if _, ok := data[k]; !ok { + miss[k] = struct{}{} + } + } + // If we don't have parameter names here, it means that all parameters are defined but the values were nil. + // It's ok, users will be able to identify missing parameters. + if len(miss) != 0 { + return errMissingVar{miss} } - data["current_bin"] = bin + return nil } diff --git a/pkg/action/loader_test.go b/pkg/action/loader_test.go index befbb5b..8dfda1a 100644 --- a/pkg/action/loader_test.go +++ b/pkg/action/loader_test.go @@ -1,10 +1,13 @@ package action import ( + "fmt" "os" "strings" "testing" + "github.com/launchrctl/launchr/internal/launchr" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -34,12 +37,24 @@ func testLoaderAction() *Action { func Test_EnvProcessor(t *testing.T) { t.Parallel() + act := testLoaderAction() proc := envProcessor{} + defer func() { + _ = os.Unsetenv("TEST_ENV1") + _ = os.Unsetenv("TEST_ENV2") + }() _ = os.Setenv("TEST_ENV1", "VAL1") _ = os.Setenv("TEST_ENV2", "VAL2") s := "$TEST_ENV1$TEST_ENV1,${TEST_ENV2},$$TEST_ENV1,${TEST_ENV_UNDEF},${TODO-$TEST_ENV1},${TODO:-$TEST_ENV1},${TODO+$TEST_ENV1},${TODO:+$TEST_ENV1}" - res, _ := proc.Process(LoadContext{}, []byte(s)) + res, err := proc.Process(LoadContext{Action: act}, []byte(s)) + assert.NoError(t, err) assert.Equal(t, "VAL1VAL1,VAL2,$TEST_ENV1,,,,,", string(res)) + // Test action predefined env variables. + s = "$CBIN,$ACTION_ID,$ACTION_WD,$ACTION_DIR,$DISCOVERY_DIR" + res, err = proc.Process(LoadContext{Action: act}, []byte(s)) + exp := fmt.Sprintf("%s,%s,%s,%s,%s", launchr.Executable(), act.ID, act.WorkDir(), act.Dir(), act.fs.Realpath()) + assert.NoError(t, err) + assert.Equal(t, exp, string(res)) } func Test_InputProcessor(t *testing.T) { @@ -138,6 +153,7 @@ func Test_PipeProcessor(t *testing.T) { ) _ = os.Setenv("TEST_ENV1", "VAL1") + defer os.Unsetenv("TEST_ENV1") input := NewInput(act, InputParams{"arg1": "arg1"}, InputParams{"optStr": "optVal1"}, nil) input.SetValidated(true) err := act.SetInput(input) diff --git a/pkg/action/runtime.container.go b/pkg/action/runtime.container.go index d078ee0..8e197d1 100644 --- a/pkg/action/runtime.container.go +++ b/pkg/action/runtime.container.go @@ -5,9 +5,9 @@ import ( "errors" "fmt" "os" - osuser "os/user" "path/filepath" "runtime" + "strconv" "strings" "github.com/launchrctl/launchr/internal/launchr" @@ -371,20 +371,6 @@ func (c *runtimeContainer) Execute(ctx context.Context, a *Action) (err error) { return err } -func getCurrentUser() string { - curuser := "" - // If running in a container native environment, run container as a current user. - // @todo review, it won't work with a remote context. - switch runtime.GOOS { - case "linux", "darwin": - u, err := osuser.Current() - if err == nil { - curuser = u.Uid + ":" + u.Gid - } - } - return curuser -} - func (c *runtimeContainer) Close() error { if c.crt == nil { return nil @@ -404,7 +390,6 @@ func (c *runtimeContainer) imageRemove(ctx context.Context, a *Action) error { } func (c *runtimeContainer) isRebuildRequired(bi *driver.BuildDefinition) (bool, error) { - // @todo test image cache resolution somehow. if c.imgccres == nil || bi == nil || !c.rebuildImage { return false, nil } @@ -492,7 +477,7 @@ func (c *runtimeContainer) imageEnsure(ctx context.Context, a *Action) error { // Output docker status only in Debug. err = status.Progress.Stream(streams.Out()) if err != nil { - c.Term().Error().Println("Error occurred while building the image %q", image) + c.Term().Error().Printfln("Error occurred while building the image %q", image) log.Error("error while building the image", "error", err) } } @@ -538,7 +523,7 @@ func (c *runtimeContainer) createContainerDef(a *Action, cname string) driver.Co WorkingDir: containerHostMount, ExtraHosts: runDef.Container.ExtraHosts, Env: runDef.Container.Env, - User: getCurrentUser(), + User: getCurrentUser().String(), Entrypoint: entrypoint, Streams: driver.ContainerStreamsOptions{ Stdin: !streams.In().IsDiscard(), @@ -556,8 +541,8 @@ func (c *runtimeContainer) createContainerDef(a *Action, cname string) driver.Co ) } else { createOpts.Binds = []string{ - launchr.MustAbs(a.WorkDir()) + ":" + containerHostMount + c.volumeFlags, - launchr.MustAbs(a.Dir()) + ":" + containerActionMount + c.volumeFlags, + normalizeContainerMountPath(a.WorkDir()) + ":" + containerHostMount + c.volumeFlags, + normalizeContainerMountPath(a.Dir()) + ":" + containerActionMount + c.volumeFlags, } } return createOpts @@ -575,7 +560,6 @@ func (c *runtimeContainer) copyAllToContainer(ctx context.Context, cid string, a if !c.isRemote() { return nil } - // @todo test somehow. launchr.Term().Info().Printfln(`Running in the remote environment. Copying the working directory and action directory inside the container.`) // Copy dir to a container to have the same owner in the destination directory. // Copying only the content of the dir will not override the parent dir ownership. @@ -610,12 +594,22 @@ func (c *runtimeContainer) copyAllFromContainer(ctx context.Context, cid string, // copyToContainer copies dir/file to a container. Directory will be copied as a subdirectory. func (c *runtimeContainer) copyToContainer(ctx context.Context, cid, srcPath, dstPath, rebaseName string) error { + var tarOpts *archive.TarOptions + copyOpts := driver.CopyToContainerOptions{} // Prepare destination copy info by stat-ing the container path. dstStat, err := c.crt.ContainerStatPath(ctx, cid, dstPath) if err != nil { return err } + // Set UID explicitly when run on Windows because files are copied as root. + if runtime.GOOS == "windows" { //nolint:goconst + user := getCurrentUser() + uid, _ := strconv.Atoi(user.UID) + gid, _ := strconv.Atoi(user.GID) + tarOpts = &archive.TarOptions{ChownOpts: &archive.ChownOpts{UID: uid, GID: gid}} + } + arch, err := archive.Tar( archive.CopyInfo{ Path: srcPath, @@ -626,7 +620,7 @@ func (c *runtimeContainer) copyToContainer(ctx context.Context, cid, srcPath, ds Exists: true, IsDir: dstStat.Mode.IsDir(), }, - nil, + tarOpts, ) if err != nil { return err @@ -637,11 +631,8 @@ func (c *runtimeContainer) copyToContainer(ctx context.Context, cid, srcPath, ds if !dstStat.Mode.IsDir() { dstDir = filepath.Base(dstPath) } - options := driver.CopyToContainerOptions{ - AllowOverwriteDirWithFile: false, - CopyUIDGID: false, - } - return c.crt.CopyToContainer(ctx, cid, dstDir, arch, options) + + return c.crt.CopyToContainer(ctx, cid, dstDir, arch, copyOpts) } func (c *runtimeContainer) copyFromContainer(ctx context.Context, cid, srcPath, dstPath, rebaseName string) (err error) { diff --git a/pkg/action/runtime.container_test.go b/pkg/action/runtime.container_test.go index 04af250..7a741fc 100644 --- a/pkg/action/runtime.container_test.go +++ b/pkg/action/runtime.container_test.go @@ -275,11 +275,12 @@ func Test_ContainerExec_createContainerDef(t *testing.T) { "env1=var1", "env2=var2", }, - User: getCurrentUser(), + User: getCurrentUser().String(), } defaultCmd := []string{"my", "cmd"} var defaultEntrypoint []string - actionDir := launchr.MustAbs("my/action/test") + actionDir := normalizeContainerMountPath("my/action/test") + workingDir := normalizeContainerMountPath("./") tts := []testCase{ { @@ -298,7 +299,7 @@ func Test_ContainerExec_createContainerDef(t *testing.T) { Command: defaultCmd, Entrypoint: defaultEntrypoint, Binds: []string{ - launchr.MustAbs("./") + ":" + containerHostMount, + workingDir + ":" + containerHostMount, actionDir + ":" + containerActionMount, }, Streams: driver.ContainerStreamsOptions{ @@ -324,7 +325,7 @@ func Test_ContainerExec_createContainerDef(t *testing.T) { Command: defaultCmd, Entrypoint: defaultEntrypoint, Binds: []string{ - launchr.MustAbs("../myactiondir") + ":" + containerHostMount, + normalizeContainerMountPath("../myactiondir") + ":" + containerHostMount, actionDir + ":" + containerActionMount, }, Streams: driver.ContainerStreamsOptions{ @@ -411,12 +412,12 @@ func Test_ContainerExec(t *testing.T) { Image: runConf.Image, ExtraHosts: runConf.ExtraHosts, Binds: []string{ - launchr.MustAbs(act.WorkDir()) + ":" + containerHostMount, - launchr.MustAbs(act.Dir()) + ":" + containerActionMount, + normalizeContainerMountPath(act.WorkDir()) + ":" + containerHostMount, + normalizeContainerMountPath(act.Dir()) + ":" + containerActionMount, }, WorkingDir: containerHostMount, Env: runConf.Env, - User: getCurrentUser(), + User: getCurrentUser().String(), } errImgEns := errors.New("image ensure error") diff --git a/pkg/action/runtime.go b/pkg/action/runtime.go index 1e1482a..a09e248 100644 --- a/pkg/action/runtime.go +++ b/pkg/action/runtime.go @@ -66,6 +66,9 @@ func (c *WithLogger) SetLogger(l *launchr.Logger) { // Log implements [RuntimeLoggerAware] interface func (c *WithLogger) Log() *launchr.Logger { + if c.logWith == nil { + return launchr.Log() + } return c.logger } @@ -76,8 +79,8 @@ func (c *WithLogger) LogWith(attrs ...any) *launchr.Logger { } return &launchr.Logger{ - Slog: c.logger.With(c.logWith...), - LogOptions: c.logger.LogOptions, + Slog: c.Log().With(c.logWith...), + LogOptions: c.Log().LogOptions, } } @@ -102,6 +105,9 @@ func (c *WithTerm) SetTerm(t *launchr.Terminal) { // Term implements [RuntimeTermAware] interface func (c *WithTerm) Term() *launchr.Terminal { + if c.term == nil { + return launchr.Term() + } return c.term } diff --git a/pkg/action/runtime.shell.go b/pkg/action/runtime.shell.go index 8edf9f3..a5d3a2c 100644 --- a/pkg/action/runtime.shell.go +++ b/pkg/action/runtime.shell.go @@ -6,11 +6,16 @@ import ( "fmt" "os" "os/exec" - "runtime" "github.com/launchrctl/launchr/internal/launchr" ) +type shellContext struct { + Shell string + Env []string + Script string +} + type runtimeShell struct { WithLogger } @@ -25,9 +30,6 @@ func (r *runtimeShell) Clone() Runtime { } func (r *runtimeShell) Init(_ context.Context, _ *Action) (err error) { - if runtime.GOOS == "windows" { - return fmt.Errorf("shell runtime is not supported in Windows") - } return nil } @@ -36,15 +38,16 @@ func (r *runtimeShell) Execute(ctx context.Context, a *Action) (err error) { log.Debug("starting execution of the action") streams := a.Input().Streams() - rt := a.RuntimeDef() - defaultShell := os.Getenv("SHELL") - if defaultShell == "" { - defaultShell = "/bin/bash" + shctx, err := createRTShellBashContext(a) + if err != nil { + return err } + log.Debug("using shell", "shell", shctx.Shell, "env", shctx.Env, "script", shctx.Script) - cmd := exec.CommandContext(ctx, defaultShell, "-l", "-c", rt.Shell.Script) //nolint:gosec // G204 user script is expected. + // Execute the script file directly + cmd := exec.CommandContext(ctx, shctx.Shell, shctx.Script) //nolint:gosec // G204 user script is expected. cmd.Dir = a.WorkDir() - cmd.Env = append(os.Environ(), rt.Shell.Env...) + cmd.Env = shctx.Env cmd.Stdout = streams.Out() cmd.Stderr = streams.Err() // Do no attach stdin, as it may not work as expected. @@ -53,6 +56,7 @@ func (r *runtimeShell) Execute(ctx context.Context, a *Action) (err error) { if err != nil { return err } + log.Debug("started process", "pid", cmd.Process.Pid) // If we attached with TTY, all signals will be processed by a child process. sigc := launchr.NotifySignals() @@ -72,7 +76,7 @@ func (r *runtimeShell) Execute(ctx context.Context, a *Action) (err error) { exitCode = 130 msg = fmt.Sprintf("action %q was interrupted, finished with exit code %d", a.ID, exitCode) } - log.Info("action finished with the exit code", "exit_code", exitCode) + log.Info("action finished with exit code", "exit_code", exitCode) return launchr.NewExitError(exitCode, msg) } return cmdErr diff --git a/pkg/action/test_utils.go b/pkg/action/test_utils.go index 4aa9492..342f5d7 100644 --- a/pkg/action/test_utils.go +++ b/pkg/action/test_utils.go @@ -19,9 +19,13 @@ const ( genPathTypeValid genPathType = iota // genPathTypeValid is a valid actions path genPathTypeArbitrary // genPathTypeArbitrary is a random string without actions directory. genPathTypeGHActions // genPathTypeGHActions is an incorrect hidden path but with actions directory. + genPathTypeRoot // genPathTypeRoot is a path in root. ) func genActionPath(d int, pathType genPathType) string { + if pathType == genPathTypeRoot { + d = 0 + } elems := make([]string, 0, d+3) for i := 0; i < d; i++ { elems = append(elems, launchr.GetRandomString(4)) diff --git a/pkg/action/utils.go b/pkg/action/utils.go index b9240a5..a1a97d2 100644 --- a/pkg/action/utils.go +++ b/pkg/action/utils.go @@ -2,12 +2,26 @@ package action import ( "fmt" + "os" + "os/exec" + "path/filepath" "strings" "sync" + "github.com/launchrctl/launchr/internal/launchr" + "gopkg.in/yaml.v3" ) +type userInfo struct { + UID string + GID string +} + +func (u userInfo) String() string { + return u.UID + ":" + u.GID +} + func yamlTypeError(s string) *yaml.TypeError { return &yaml.TypeError{Errors: []string{s}} } @@ -123,3 +137,77 @@ func collectAllNodes(n *yaml.Node) []*yaml.Node { } return res } + +// EnvVarRuntimeShellBash defines path to bash shell. +var EnvVarRuntimeShellBash = launchr.EnvVar("runtime_shell_bash") + +func createRTShellBashContext(a *Action) (*shellContext, error) { + path, err := getBashPath() + if err != nil { + return nil, err + } + return prepareShellContext(a, path) +} + +func getBashPath() (string, error) { + path, err := getRuntimeShellBashFromEnv() + if err != nil { + launchr.Log().Warn("failed to get shell from "+EnvVarRuntimeShellBash.String()+". Fallback to PATH lookup", "err", err) + } + if path != "" { + return path, nil + } + path, err = exec.LookPath("bash") + if err != nil { + // Try to find bash. + for _, path = range launchr.KnownBashPaths() { + if _, err := os.Stat(path); err == nil { + return path, nil + } + } + return "", err + } + return path, nil +} + +var errPathNotExecutable = fmt.Errorf("file is not executable") + +func getRuntimeShellBashFromEnv() (string, error) { + shell := EnvVarRuntimeShellBash.Get() + if shell == "" { + return "", nil + } + + // Check if the file is executable. + if err := isExecutable(shell); err != nil { + return "", fmt.Errorf("runtime shell %q is not executable: %w", shell, err) + } + return shell, nil +} + +// exportScriptToFile exports the shell script to a temporary file and returns the file path +func exportScriptToFile(script string) (string, error) { + // Create temporary file with action-specific naming + tmpDir, err := launchr.MkdirTempWithCleanup("runtime_shell_") + if err != nil { + return "", fmt.Errorf("failed to create temp directory: %w", err) + } + path := filepath.Join(tmpDir, "action.sh") + scriptFile, err := os.Create(path) //nolint:gosec // G304 We create the path. + if err != nil { + return "", fmt.Errorf("failed to create temp script file: %w", err) + } + defer scriptFile.Close() + + // Write the script content to the file + if _, err := scriptFile.WriteString(script); err != nil { + return "", fmt.Errorf("failed to write script to file: %w", err) + } + + // Make the script executable + if err := scriptFile.Chmod(0755); err != nil { + return "", fmt.Errorf("failed to make script executable: %w", err) + } + + return path, nil +} diff --git a/pkg/action/utils_unix.go b/pkg/action/utils_unix.go new file mode 100644 index 0000000..67af26a --- /dev/null +++ b/pkg/action/utils_unix.go @@ -0,0 +1,52 @@ +//go:build unix + +package action + +import ( + "os" + osuser "os/user" + + "github.com/launchrctl/launchr/internal/launchr" +) + +func getCurrentUser() userInfo { + // If running in a container native environment, run container as a current user. + curuser := userInfo{} + u, err := osuser.Current() + if err == nil { + curuser.UID = u.Uid + curuser.GID = u.Gid + } + return curuser +} + +func normalizeContainerMountPath(path string) string { + return launchr.MustAbs(path) +} + +func prepareShellContext(a *Action, shell string) (*shellContext, error) { + rt := a.RuntimeDef() + env := os.Environ() + env = append(env, a.getTemplateVars().envStrings()...) + env = append(env, rt.Shell.Env...) + scriptPath, err := exportScriptToFile(rt.Shell.Script) + if err != nil { + return nil, err + } + return &shellContext{ + Shell: shell, + Env: env, + Script: scriptPath, + }, nil +} + +func isExecutable(path string) error { + info, err := os.Stat(path) + if err != nil { + return err + } + if info.Mode()&0111 == 0 { + return errPathNotExecutable + } + return nil +} diff --git a/pkg/action/utils_windows.go b/pkg/action/utils_windows.go new file mode 100644 index 0000000..bc83af8 --- /dev/null +++ b/pkg/action/utils_windows.go @@ -0,0 +1,132 @@ +//go:build windows + +package action + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + + "github.com/launchrctl/launchr/internal/launchr" +) + +func getCurrentUser() userInfo { + // Use neutral 1000 when we can't get UID on Windows. + const defaultUID = "1000" + const defaultGID = "1000" + return userInfo{ + UID: defaultUID, + GID: defaultGID, + } +} + +func normalizeContainerMountPath(path string) string { + path = launchr.MustAbs(path) + // Convert windows paths C:\my\path -> /c/my/path for docker daemon. + return "/mnt" + launchr.ConvertWindowsPath(path) +} + +func isWSLShell(ctx context.Context, shell string) bool { + checkWslCmd := exec.CommandContext(ctx, shell, "-c", "uname -r") + wslOut := &strings.Builder{} + checkWslCmd.Stdout = wslOut + err := checkWslCmd.Run() + if err != nil { + return false + } + return strings.Contains(wslOut.String(), "WSL") +} + +func prepareShellContext(a *Action, shell string) (*shellContext, error) { + rt := a.RuntimeDef() + isWsl := isWSLShell(context.Background(), shell) + var convert func(string) string + if isWsl { + convert = normalizeContainerMountPath + } else { + convert = launchr.ConvertWindowsPath + } + + // Filter Windows-style paths from environment variables + vars := convertWindowsVarsPaths(a.getTemplateVars(), convert) + env := os.Environ() + env = append(env, vars.envStrings()...) + env = append(env, rt.Shell.Env...) + script := rt.Shell.Script + if isWsl { + // Prepend environment variables to script + script = prependEnvToScript(script, env) + } + scriptPath, err := exportScriptToFile(script) + if err != nil { + return nil, err + } + + return &shellContext{ + Shell: shell, + Env: env, + Script: convert(scriptPath), + }, nil +} + +func filterWindowsEnv(env []string) []string { + filtered := make([]string, 0, len(env)) + prefix := []string{ + "HOMEDRIVE=", + "PATHEXT=", + "UID=", + "SystemDrive=", + "Chocolatey", + "$=$", + ":=;", + } + for _, e := range env { + hasPrefix := slices.ContainsFunc(prefix, func(p string) bool { + return strings.HasPrefix(e, p) + }) + if !hasPrefix && !strings.Contains(e, `\`) { + filtered = append(filtered, e) + } + } + return filtered +} + +func convertWindowsVarsPaths(v *actionVars, convert func(string) string) *actionVars { + c := *v + c.currentBin = convert(v.currentBin) + c.actionWD = convert(v.actionWD) + c.actionDir = convert(v.actionDir) + c.discoveryDir = convert(v.discoveryDir) + return &c +} + +func isExecutable(path string) error { + // On Windows, check by file extension + ext := strings.ToLower(filepath.Ext(path)) + extExec := os.Getenv("PATHEXT") + if extExec == "" { + extExec = ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC" + } + extExec = strings.ToUpper(extExec) + if slices.Contains(strings.Split(extExec, ";"), strings.ToUpper(ext)) { + return nil + } + return errPathNotExecutable +} + +// prependEnvToScript prepends all environment variables to the script file +func prependEnvToScript(script string, env []string) string { + env = filterWindowsEnv(env) + envStr := "" + // Write environment variables first + for _, envVar := range env { + envStr += "export " + envVar + "\n" + } + + envStr += "\n" + + return envStr + script +} diff --git a/pkg/action/yaml.discovery.go b/pkg/action/yaml.discovery.go index f633d8d..b124e0f 100644 --- a/pkg/action/yaml.discovery.go +++ b/pkg/action/yaml.discovery.go @@ -4,13 +4,14 @@ import ( "bufio" "bytes" "io" + "path/filepath" "regexp" "sync" ) var ( // rgxYamlFilepath is a regex for a yaml path with unix and windows support. - rgxYamlFilepath = regexp.MustCompile(`(^actions|.*[\\/]actions)[\\/][^\\/]+[\\/]action\.y(a)?ml$`) + rgxYamlFilepath = regexp.MustCompile(`^(actions|[^\s!<>:"|?*]+/actions)/[^\s!<>:"|?*/]+/action\.y(a)?ml$`) // rgxYamlRootFile is a regex for a yaml file located in root dir only. rgxYamlRootFile = regexp.MustCompile(`^action\.y(a)?ml$`) ) @@ -28,7 +29,7 @@ type YamlDiscoveryStrategy struct { // IsValid implements [DiscoveryStrategy]. func (y YamlDiscoveryStrategy) IsValid(path string) bool { - return y.TargetRgx.MatchString(path) + return y.TargetRgx.MatchString(filepath.ToSlash(path)) } // Loader implements [DiscoveryStrategy]. diff --git a/pkg/action/yaml_const_test.go b/pkg/action/yaml_const_test.go index a1c18cc..df9decf 100644 --- a/pkg/action/yaml_const_test.go +++ b/pkg/action/yaml_const_test.go @@ -9,7 +9,7 @@ action: const validFullYaml = ` version: "1" -working_directory: "{{ .current_working_dir }}" +working_directory: '{{ .current_working_dir }}' action: title: Title description: Description diff --git a/pkg/archive/tar.go b/pkg/archive/tar.go index aa6fcbf..314b02b 100644 --- a/pkg/archive/tar.go +++ b/pkg/archive/tar.go @@ -16,6 +16,9 @@ import ( // Compression is the state represents if compressed or not. type Compression compression.Compression +// ChownOpts has ownership of files in the archive. +type ChownOpts archive.ChownOpts + // Compressions types. const ( Uncompressed = Compression(archive.Uncompressed) // Uncompressed represents the uncompressed. @@ -32,6 +35,7 @@ type TarOptions struct { Compression Compression RebaseNames map[string]string SrcInfo CopyInfo + ChownOpts *ChownOpts } // CopyInfo holds basic info about the source @@ -69,6 +73,10 @@ func Tar(src CopyInfo, dst CopyInfo, opts *TarOptions) (io.ReadCloser, error) { sourceDir, sourceBase := archive.SplitPathDirEntry(srcInfo.Path) tarOpts := archive.TarResourceRebaseOpts(sourceBase, srcInfo.RebaseName) tarOpts.ExcludePatterns = opts.ExcludePatterns + if opts.ChownOpts != nil { + chownOpts := archive.ChownOpts(*opts.ChownOpts) + tarOpts.ChownOpts = &chownOpts + } maps.Insert(tarOpts.RebaseNames, maps.All(opts.RebaseNames)) tarOpts.Compression = compression.Compression(opts.Compression) slices.AppendSeq(tarOpts.IncludeFiles, slices.Values(opts.IncludeFiles)) diff --git a/pkg/driver/docker.go b/pkg/driver/docker.go index b9d6c7a..b4552c5 100644 --- a/pkg/driver/docker.go +++ b/pkg/driver/docker.go @@ -6,12 +6,12 @@ import ( "fmt" "io" - dockertypes "github.com/docker/docker/api/types" + cerrdefs "github.com/containerd/errdefs" + "github.com/docker/docker/api/types/build" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" - "github.com/docker/docker/errdefs" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/stdcopy" @@ -34,7 +34,6 @@ type dockerRuntime struct { // NewDockerRuntime creates a docker runtime. func NewDockerRuntime() (ContainerRunner, error) { - // @todo it doesn't work with Colima or with non-default context. c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { @@ -107,7 +106,7 @@ func (d *dockerRuntime) ImageEnsure(ctx context.Context, imgOpts ImageOptions) ( // Check if the image already exists. insp, err := d.cli.ImageInspect(ctx, imgOpts.Name) if err != nil { - if !errdefs.IsNotFound(err) { + if !cerrdefs.IsNotFound(err) { return nil, err } } @@ -126,8 +125,8 @@ func (d *dockerRuntime) ImageEnsure(ctx context.Context, imgOpts ImageOptions) ( return nil, errTar } defer buildContext.Close() - resp, errBuild := d.cli.ImageBuild(ctx, buildContext, dockertypes.ImageBuildOptions{ - Tags: []string{imgOpts.Name}, + resp, errBuild := d.cli.ImageBuild(ctx, buildContext, build.ImageBuildOptions{ + Tags: imgOpts.Build.Tags, BuildArgs: imgOpts.Build.Args, Dockerfile: imgOpts.Build.Buildfile, NoCache: imgOpts.NoCache, diff --git a/pkg/driver/iostream.go b/pkg/driver/iostream.go index 11cc9c8..3f266a6 100644 --- a/pkg/driver/iostream.go +++ b/pkg/driver/iostream.go @@ -79,6 +79,16 @@ func (h *ioStreamer) stream(ctx context.Context) error { outputDone := h.beginOutputStream(restoreInput) inputDone := h.beginInputStream(restoreInput) + // Close input. + defer func() { + if conn, ok := h.io.In.(interface{ CloseWrite() error }); ok { + err := conn.CloseWrite() + if err != nil { + launchr.Log().Debug("couldn't send EOF", "error", err) + } + } + }() + select { case err := <-outputDone: return err @@ -189,13 +199,6 @@ func (h *ioStreamer) beginInputStream(restoreInput func()) (doneC <-chan struct{ } } - if conn, ok := h.io.In.(interface{ CloseWrite() error }); ok { - err := conn.CloseWrite() - if err != nil { - launchr.Log().Debug("couldn't send EOF", "error", err) - } - } - close(inputDone) }() diff --git a/pkg/driver/type.go b/pkg/driver/type.go index bbe4412..fd35e8d 100644 --- a/pkg/driver/type.go +++ b/pkg/driver/type.go @@ -191,7 +191,6 @@ type ContainerDefinition struct { Command []string WorkingDir string - // @todo review binds and volumes, because binds won't work for remote environments. Binds []string Volumes []ContainerVolume diff --git a/plugins/actionscobra/cobra.go b/plugins/actionscobra/cobra.go index 68f2f22..a9856f7 100644 --- a/plugins/actionscobra/cobra.go +++ b/plugins/actionscobra/cobra.go @@ -139,26 +139,37 @@ func setFlag(flags *pflag.FlagSet, param *action.DefParameter) (any, error) { if err != nil { return nil, err } + + // Skip silently incorrect or duplicated shorthands. + shorthand := param.Shorthand + if len(shorthand) > 1 { + launchr.Log().Warn("incorrect shorthand definition for cobra flag, skipping", "parameter", param.Name, "shorthand", shorthand) + shorthand = "" + } + if flags.ShorthandLookup(shorthand) != nil { + launchr.Log().Warn("duplicate shorthand definition for cobra flag, skipping", "parameter", param.Name, "shorthand", shorthand) + shorthand = "" + } switch param.Type { case jsonschema.String: - val = flags.StringP(param.Name, param.Shorthand, dval.(string), desc) + val = flags.StringP(param.Name, shorthand, dval.(string), desc) case jsonschema.Integer: - val = flags.IntP(param.Name, param.Shorthand, dval.(int), desc) + val = flags.IntP(param.Name, shorthand, dval.(int), desc) case jsonschema.Number: - val = flags.Float64P(param.Name, param.Shorthand, dval.(float64), desc) + val = flags.Float64P(param.Name, shorthand, dval.(float64), desc) case jsonschema.Boolean: - val = flags.BoolP(param.Name, param.Shorthand, dval.(bool), desc) + val = flags.BoolP(param.Name, shorthand, dval.(bool), desc) case jsonschema.Array: dslice := dval.([]any) switch param.Items.Type { case jsonschema.String: - val = flags.StringSliceP(param.Name, param.Shorthand, action.CastSliceAnyToTyped[string](dslice), desc) + val = flags.StringSliceP(param.Name, shorthand, action.CastSliceAnyToTyped[string](dslice), desc) case jsonschema.Integer: - val = flags.IntSliceP(param.Name, param.Shorthand, action.CastSliceAnyToTyped[int](dslice), desc) + val = flags.IntSliceP(param.Name, shorthand, action.CastSliceAnyToTyped[int](dslice), desc) case jsonschema.Number: - val = flags.Float64SliceP(param.Name, param.Shorthand, action.CastSliceAnyToTyped[float64](dslice), desc) + val = flags.Float64SliceP(param.Name, shorthand, action.CastSliceAnyToTyped[float64](dslice), desc) case jsonschema.Boolean: - val = flags.BoolSliceP(param.Name, param.Shorthand, action.CastSliceAnyToTyped[bool](dslice), desc) + val = flags.BoolSliceP(param.Name, shorthand, action.CastSliceAnyToTyped[bool](dslice), desc) default: // @todo use flags.Var() and define a custom value, jsonschema accepts "any". return nil, fmt.Errorf("json schema array type %q is not implemented", param.Items.Type) @@ -196,6 +207,8 @@ func derefOpt(v any) any { return *v case *[]int: return *v + case *[]float64: + return *v case *[]bool: return *v default: diff --git a/plugins/builder/builder.go b/plugins/builder/builder.go index c1e1604..6d1e2a0 100644 --- a/plugins/builder/builder.go +++ b/plugins/builder/builder.go @@ -81,7 +81,7 @@ type buildVars struct { Cwd string } -// NewBuilder creates build environment. +// NewBuilder creates a build environment. func NewBuilder(opts *BuildOptions) (*Builder, error) { wd, err := os.Getwd() if err != nil { @@ -94,25 +94,19 @@ func NewBuilder(opts *BuildOptions) (*Builder, error) { } // Build prepares build environment, generates go files and build the binary. -func (b *Builder) Build(ctx context.Context, streams launchr.Streams) error { +func (b *Builder) Build(ctx context.Context) error { b.Term().Info().Printfln("Starting to build %s", b.PkgName) - // Prepare build environment dir and go executable. + // Prepare a build environment dir and go executable. var err error - b.env, err = newBuildEnvironment(streams) + b.env, err = newBuildEnvironment(b) if err != nil { return err } - b.env.SetLogger(b.Log()) - b.env.SetTerm(b.Term()) - - // Delete temp files in case of error. - defer func() { - if err != nil { - _ = b.Close() - } - }() b.Log().Debug("creating build environment", "temp_dir", b.env.wd, "env", b.env.env) + if b.Debug { + b.Term().Warning().Printfln("Debug flag is set. The build temporary directory will not be deleted: %s", b.env.wd) + } // Write files to dir and generate go mod. b.Term().Info().Println("Creating the project files and fetching dependencies") @@ -195,6 +189,7 @@ func (b *Builder) goBuild(ctx context.Context) error { // Include or trim debug information. if b.Debug { args = append(args, "-gcflags", "all=-N -l") + b.Log().Debug("binary will be built with debug headers for delve debuggin") } else { ldflags = append(ldflags, "-s", "-w") args = append(args, "-trimpath") diff --git a/plugins/builder/environment.go b/plugins/builder/environment.go index a9a2937..ba8509b 100644 --- a/plugins/builder/environment.go +++ b/plugins/builder/environment.go @@ -3,6 +3,7 @@ package builder import ( "context" + "io" "os" "os/exec" "path/filepath" @@ -57,13 +58,18 @@ type buildEnvironment struct { action.WithLogger action.WithTerm - wd string - env envVars - streams launchr.Streams + wd string + env envVars } -func newBuildEnvironment(streams launchr.Streams) (*buildEnvironment, error) { - tmpDir, err := launchr.MkdirTemp("build_") +func newBuildEnvironment(b *Builder) (*buildEnvironment, error) { + var err error + var tmpDir string + if !b.Debug { + tmpDir, err = launchr.MkdirTempWithCleanup("build_") + } else { + tmpDir, err = launchr.MkdirTemp("build_") + } if err != nil { return nil, err } @@ -72,12 +78,13 @@ func newBuildEnvironment(streams launchr.Streams) (*buildEnvironment, error) { return nil, err } - env := envFromOs() - return &buildEnvironment{ - wd: tmpDir, - env: env, - streams: streams, - }, nil + env := &buildEnvironment{ + wd: tmpDir, + env: envFromOs(), + } + env.SetLogger(b.Log()) + env.SetTerm(b.Term()) + return env, nil } func (env *buildEnvironment) CreateModFile(ctx context.Context, opts *BuildOptions) error { @@ -107,18 +114,9 @@ func (env *buildEnvironment) CreateModFile(ctx context.Context, opts *BuildOptio } // Download core. - var coreRepl bool - for repl := range opts.ModReplace { - if strings.HasPrefix(opts.CorePkg.Path, repl) { - coreRepl = true - break - } - } - if !coreRepl { - err = env.execGoGet(ctx, opts.CorePkg.String()) - if err != nil { - return err - } + err = env.execGoGet(ctx, opts.CorePkg.String()) + if err != nil { + return err } // Download plugins. @@ -126,7 +124,7 @@ nextPlugin: for _, p := range opts.Plugins { // Do not get plugins of module subpath. for repl := range opts.ModReplace { - if strings.HasPrefix(p.Path, repl) { + if p.Path != repl && strings.HasPrefix(p.Path, repl) { continue nextPlugin } } @@ -148,23 +146,33 @@ func (env *buildEnvironment) NewCommand(ctx context.Context, command string, arg cmd := exec.CommandContext(ctx, command, args...) cmd.Dir = env.wd cmd.Env = env.env - cmd.Stdout = env.streams.Out() - cmd.Stderr = env.streams.Err() + cmd.Stdout = env.Term() + cmd.Stderr = env.Term() return cmd } func (env *buildEnvironment) execGoMod(ctx context.Context, args ...string) error { cmd := env.NewCommand(ctx, env.Go(), append([]string{"mod"}, args...)...) + // Don't output go output unless some verbosity is requested. + if env.Log().Level() != launchr.LogLevelDebug { + cmd.Stdout = io.Discard + cmd.Stderr = io.Discard + } return env.RunCmd(ctx, cmd) } func (env *buildEnvironment) execGoGet(ctx context.Context, args ...string) error { cmd := env.NewCommand(ctx, env.Go(), append([]string{"get"}, args...)...) + // Don't output go output unless some verbosity is requested. + if env.Log().Level() == launchr.LogLevelDisabled { + cmd.Stdout = io.Discard + cmd.Stderr = io.Discard + } return env.RunCmd(ctx, cmd) } func (env *buildEnvironment) RunCmd(ctx context.Context, cmd *exec.Cmd) error { - env.Log().Debug("executing shell", "cmd", cmd) + env.Log().Debug("executing shell", "cmd", cmd, "pwd", cmd.Dir) err := cmd.Start() if err != nil { return err diff --git a/plugins/builder/plugin.go b/plugins/builder/plugin.go index 59793cf..07c1046 100644 --- a/plugins/builder/plugin.go +++ b/plugins/builder/plugin.go @@ -23,9 +23,7 @@ func init() { } // Plugin is a [launchr.Plugin] to build launchr application. -type Plugin struct { - app launchr.App -} +type Plugin struct{} // PluginInfo implements [launchr.Plugin] interface. func (p *Plugin) PluginInfo() launchr.PluginInfo { @@ -51,8 +49,7 @@ type builderInput struct { // OnAppInit implements [launchr.OnAppInitPlugin] interface. func (p *Plugin) OnAppInit(app launchr.App) error { - p.app = app - actionYaml = bytes.Replace(actionYaml, []byte("DEFAULT_NAME_PLACEHOLDER"), []byte(p.app.Name()), 1) + actionYaml = bytes.Replace(actionYaml, []byte("DEFAULT_NAME_PLACEHOLDER"), []byte(app.Name()), 1) return nil } @@ -85,7 +82,7 @@ func (p *Plugin) DiscoverActions(_ context.Context) ([]*action.Action, error) { } flags.SetTerm(term) - return Execute(ctx, p.app.Streams(), &flags) + return Execute(ctx, &flags) })) return []*action.Action{a}, nil } @@ -103,7 +100,7 @@ func (p *Plugin) Generate(config launchr.GenerateConfig) error { } // Execute runs launchr and executes build of launchr. -func Execute(ctx context.Context, streams launchr.Streams, flags *builderInput) error { +func Execute(ctx context.Context, flags *builderInput) error { // Set build timeout. timeout, err := time.ParseDuration(flags.timeout) if err != nil { @@ -156,7 +153,11 @@ func Execute(ctx context.Context, streams launchr.Streams, flags *builderInput) builder.WithTerm = flags.WithTerm defer builder.Close() - return builder.Build(ctx, streams) + err = builder.Build(ctx) + if err == context.DeadlineExceeded { + err = fmt.Errorf("build timed out after %v", timeout) + } + return err } func corePkgInfo() UsePluginInfo { diff --git a/plugins/verbosity/plugin.go b/plugins/verbosity/plugin.go index 5938937..afc8786 100644 --- a/plugins/verbosity/plugin.go +++ b/plugins/verbosity/plugin.go @@ -146,6 +146,8 @@ func (p Plugin) OnAppInit(app launchr.App) error { logFormat := LogFormatPretty if pflags.Changed("log-format") { logFormat = logFormatStr + } else if launchr.EnvVarLogFormat.Get() != "" { + logFormat = LogFormat(launchr.EnvVarLogFormat.Get()) } streams := app.Streams() From 388b7a2d1151ebb43ed1a64db49fbbe686362280 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Sun, 27 Jul 2025 15:37:54 +0200 Subject: [PATCH 2/3] Integration tests and Github CI workflows --- internal/launchr/filepath.go | 21 +++++++++++---------- internal/launchr/filepath_test.go | 2 +- pkg/action/action.go | 2 +- pkg/action/utils.go | 2 +- plugins/builder/environment.go | 8 +------- 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/internal/launchr/filepath.go b/internal/launchr/filepath.go index c9384c1..e3136c9 100644 --- a/internal/launchr/filepath.go +++ b/internal/launchr/filepath.go @@ -123,9 +123,7 @@ func existsInSlice[T comparable](slice []T, el T) bool { return false } -// MkdirTemp creates a temporary directory. -// It tries to create a directory in memory (tmpfs). -func MkdirTemp(pattern string) (string, error) { +func mkdirTemp(pattern string) (string, error) { var err error u, err := osuser.Current() if err != nil { @@ -182,18 +180,21 @@ func MkdirTemp(pattern string) (string, error) { return dirPath, nil } -// MkdirTempWithCleanup creates a temporary directory with MkdirTemp. +// MkdirTemp creates a temporary directory. +// It tries to create a directory in memory (tmpfs). // The temp directory is removed when the app terminates. -func MkdirTempWithCleanup(pattern string) (string, error) { - dirPath, err := MkdirTemp(pattern) +func MkdirTemp(pattern string, keep bool) (string, error) { + dirPath, err := mkdirTemp(pattern) if err != nil { return "", err } - // Make sure the dir is cleaned on finish. - RegisterCleanupFn(func() error { - return os.RemoveAll(dirPath) - }) + if !keep { + // Remove dir on finish. + RegisterCleanupFn(func() error { + return os.RemoveAll(dirPath) + }) + } return dirPath, nil } diff --git a/internal/launchr/filepath_test.go b/internal/launchr/filepath_test.go index 27ff330..881bfeb 100644 --- a/internal/launchr/filepath_test.go +++ b/internal/launchr/filepath_test.go @@ -11,7 +11,7 @@ import ( func TestMkdirTemp(t *testing.T) { t.Parallel() - dir, err := MkdirTempWithCleanup("test") + dir, err := MkdirTemp("test", false) require.NoError(t, err) require.NotEmpty(t, dir) stat, err := os.Stat(dir) diff --git a/pkg/action/action.go b/pkg/action/action.go index 4edd280..e59768c 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -146,7 +146,7 @@ func (a *Action) syncToDisk() (err error) { // Export to a temporary path. // Make sure the path doesn't have semicolons, because Docker bind doesn't like it. tmpDirName := strings.Replace(a.ID, ":", "_", -1) - tmpDir, err := launchr.MkdirTempWithCleanup(tmpDirName) + tmpDir, err := launchr.MkdirTemp(tmpDirName, false) if err != nil { return } diff --git a/pkg/action/utils.go b/pkg/action/utils.go index a1a97d2..e7cc8a4 100644 --- a/pkg/action/utils.go +++ b/pkg/action/utils.go @@ -188,7 +188,7 @@ func getRuntimeShellBashFromEnv() (string, error) { // exportScriptToFile exports the shell script to a temporary file and returns the file path func exportScriptToFile(script string) (string, error) { // Create temporary file with action-specific naming - tmpDir, err := launchr.MkdirTempWithCleanup("runtime_shell_") + tmpDir, err := launchr.MkdirTemp("runtime_shell_", false) if err != nil { return "", fmt.Errorf("failed to create temp directory: %w", err) } diff --git a/plugins/builder/environment.go b/plugins/builder/environment.go index ba8509b..61d5412 100644 --- a/plugins/builder/environment.go +++ b/plugins/builder/environment.go @@ -63,13 +63,7 @@ type buildEnvironment struct { } func newBuildEnvironment(b *Builder) (*buildEnvironment, error) { - var err error - var tmpDir string - if !b.Debug { - tmpDir, err = launchr.MkdirTempWithCleanup("build_") - } else { - tmpDir, err = launchr.MkdirTemp("build_") - } + tmpDir, err := launchr.MkdirTemp("build_", b.Debug) if err != nil { return nil, err } From 6f3fdede3f2a37ddd88180ea4cf24aad6fd4e355 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Wed, 30 Jul 2025 23:45:25 +0200 Subject: [PATCH 3/3] Update golanglint to v2 --- .golangci.yaml | 93 ++++++++++++++++++------------------ Makefile | 2 +- internal/launchr/filepath.go | 2 +- internal/launchr/log.go | 2 +- internal/launchr/streams.go | 8 ++-- pkg/action/action.go | 2 +- pkg/action/loader_test.go | 8 ++-- pkg/action/process_test.go | 2 +- pkg/action/yaml.discovery.go | 4 +- pkg/archive/tar.go | 2 +- 10 files changed, 64 insertions(+), 61 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 98f1742..a8b9b80 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,55 +1,56 @@ -# More info on config here: https://github.com/golangci/golangci-lint#config-file -run: - deadline: 10s - issues-exit-code: 1 - tests: true - +# yaml-language-server: $schema=https://golangci-lint.run/jsonschema/golangci.jsonschema.json +version: "2" output: formats: - - format: colored-line-number - print-issued-lines: true - print-linter-name: true - -linters-settings: - govet: - shadow: true - golint: - min-confidence: 0 - dupl: - threshold: 100 - goconst: - min-len: 2 - min-occurrences: 2 - + text: + path: stdout + print-linter-name: true + print-issued-lines: true linters: - disable-all: true + default: none enable: - - revive - - govet - - errcheck - - unused - - ineffassign - - typecheck - dupl + - errcheck - goconst - gosec - - goimports - - gosimple + - govet + - ineffassign + - revive - staticcheck - unused - -issues: - exclude-use-default: false - exclude-dirs: - - bin - - vendor - - var - - tmp - exclude-files: - - \.pb\.go$ - - \.pb\.goclay\.go$ - exclude: -# # _ instead of err checks -# - G104 - # errcheck: Almost all programs ignore errors on these functions and in most cases it's ok - - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv|.*Rollback). is not checked + settings: + dupl: + threshold: 100 + goconst: + min-len: 2 + min-occurrences: 2 + exclusions: + generated: lax + rules: + - path: (.+)\.go$ + text: Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv|.*Rollback). is not checked + paths: + - \.pb\.go$ + - \.pb\.goclay\.go$ + - bin + - vendor + - var + - tmp + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - goimports + exclusions: + generated: lax + paths: + - \.pb\.go$ + - \.pb\.goclay\.go$ + - bin + - vendor + - var + - tmp + - third_party$ + - builtin$ + - examples$ diff --git a/Makefile b/Makefile index cad5821..44656ab 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ LOCAL_BIN:=$(CURDIR)/bin # Linter config. GOLANGCI_BIN:=$(LOCAL_BIN)/golangci-lint -GOLANGCI_TAG:=1.64.5 +GOLANGCI_TAG:=2.3.0 GOTESTFMT_BIN:=$(GOBIN)/gotestfmt diff --git a/internal/launchr/filepath.go b/internal/launchr/filepath.go index e3136c9..a62920d 100644 --- a/internal/launchr/filepath.go +++ b/internal/launchr/filepath.go @@ -205,7 +205,7 @@ func EscapePathString(s string) string { if filepath.Separator == '/' { return s } - return strings.Replace(s, "\\", "\\\\", -1) + return strings.ReplaceAll(s, "\\", "\\\\") } // ConvertWindowsPath converts Windows paths to Docker-compatible paths diff --git a/internal/launchr/log.go b/internal/launchr/log.go index a94beb5..6131d05 100644 --- a/internal/launchr/log.go +++ b/internal/launchr/log.go @@ -137,7 +137,7 @@ func (o *slogOpts) Level() LogLevel { func (o *slogOpts) SetLevel(l LogLevel) { o.LogLevel = l - o.LevelVar.Set(o.mapLevel(l)) + o.Set(o.mapLevel(l)) } func (o *slogOpts) SetOutput(w io.Writer) { diff --git a/internal/launchr/streams.go b/internal/launchr/streams.go index 0e590be..1e550e5 100644 --- a/internal/launchr/streams.go +++ b/internal/launchr/streams.go @@ -75,10 +75,10 @@ func (o *Out) Write(p []byte) (int, error) { // SetRawTerminal sets raw mode on the input terminal. func (o *Out) SetRawTerminal() (err error) { - if !o.commonStream.IsTerminal() { + if !o.IsTerminal() { return nil } - o.commonStream.state, err = mobyterm.SetRawTerminalOutput(o.commonStream.fd) + o.state, err = mobyterm.SetRawTerminalOutput(o.fd) return err } @@ -148,10 +148,10 @@ func (i *In) Close() error { // SetRawTerminal sets raw mode on the input terminal. func (i *In) SetRawTerminal() (err error) { - if !i.commonStream.IsTerminal() { + if !i.IsTerminal() { return nil } - i.commonStream.state, err = mobyterm.SetRawTerminal(i.commonStream.fd) + i.state, err = mobyterm.SetRawTerminal(i.fd) return err } diff --git a/pkg/action/action.go b/pkg/action/action.go index e59768c..6a2fde7 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -145,7 +145,7 @@ func (a *Action) syncToDisk() (err error) { } // Export to a temporary path. // Make sure the path doesn't have semicolons, because Docker bind doesn't like it. - tmpDirName := strings.Replace(a.ID, ":", "_", -1) + tmpDirName := strings.ReplaceAll(a.ID, ":", "_") tmpDir, err := launchr.MkdirTemp(tmpDirName, false) if err != nil { return diff --git a/pkg/action/loader_test.go b/pkg/action/loader_test.go index 8dfda1a..f49878d 100644 --- a/pkg/action/loader_test.go +++ b/pkg/action/loader_test.go @@ -6,10 +6,10 @@ import ( "strings" "testing" - "github.com/launchrctl/launchr/internal/launchr" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/launchrctl/launchr/internal/launchr" ) func testLoaderAction() *Action { @@ -153,7 +153,9 @@ func Test_PipeProcessor(t *testing.T) { ) _ = os.Setenv("TEST_ENV1", "VAL1") - defer os.Unsetenv("TEST_ENV1") + defer func() { + _ = os.Unsetenv("TEST_ENV1") + }() input := NewInput(act, InputParams{"arg1": "arg1"}, InputParams{"optStr": "optVal1"}, nil) input.SetValidated(true) err := act.SetInput(input) diff --git a/pkg/action/process_test.go b/pkg/action/process_test.go index 1addbcf..f775427 100644 --- a/pkg/action/process_test.go +++ b/pkg/action/process_test.go @@ -174,7 +174,7 @@ func addTestValueProcessors(am Manager) { procReplace := GenericValueProcessor[procTestReplaceOptions]{ Types: []jsonschema.Type{jsonschema.String}, Fn: func(v any, opts procTestReplaceOptions, _ ValueProcessorContext) (any, error) { - return strings.Replace(v.(string), opts.Fields.O, opts.Fields.N, -1), nil + return strings.ReplaceAll(v.(string), opts.Fields.O, opts.Fields.N), nil }, } procErr := GenericValueProcessor[ValueProcessorOptionsEmpty]{ diff --git a/pkg/action/yaml.discovery.go b/pkg/action/yaml.discovery.go index b124e0f..dcd0440 100644 --- a/pkg/action/yaml.discovery.go +++ b/pkg/action/yaml.discovery.go @@ -155,8 +155,8 @@ func (p escapeYamlTplCommentsProcessor) Process(_ LoadContext, b []byte) ([]byte if i := bytes.IndexByte(l, '#'); i != -1 { // Check the comment symbol is not inside a string. // Multiline strings are not supported for now. - if !(bytes.LastIndexByte(l[:i], '"') != -1 && bytes.IndexByte(l[i:], '"') != -1 || - bytes.LastIndexByte(l[:i], '\'') != -1 && bytes.IndexByte(l[i:], '\'') != -1) { + if (bytes.LastIndexByte(l[:i], '"') == -1 || bytes.IndexByte(l[i:], '"') == -1) && + (bytes.LastIndexByte(l[:i], '\'') == -1 || bytes.IndexByte(l[i:], '\'') == -1) { // Strip data after comment symbol. l = l[:i] } diff --git a/pkg/archive/tar.go b/pkg/archive/tar.go index 314b02b..c385807 100644 --- a/pkg/archive/tar.go +++ b/pkg/archive/tar.go @@ -79,7 +79,7 @@ func Tar(src CopyInfo, dst CopyInfo, opts *TarOptions) (io.ReadCloser, error) { } maps.Insert(tarOpts.RebaseNames, maps.All(opts.RebaseNames)) tarOpts.Compression = compression.Compression(opts.Compression) - slices.AppendSeq(tarOpts.IncludeFiles, slices.Values(opts.IncludeFiles)) + tarOpts.IncludeFiles = slices.AppendSeq(tarOpts.IncludeFiles, slices.Values(opts.IncludeFiles)) r, err := archive.TarWithOptions(sourceDir, tarOpts) if err != nil {