diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 383ae8e3..25a80296 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -78,7 +78,7 @@ jobs: needs: [build] strategy: matrix: - containerd: ["1.7.27", "2.0.4"] + containerd: ["1.7.27", "2.1.3"] fail-fast: false timeout-minutes: 10 env: @@ -114,10 +114,10 @@ jobs: - name: Run opa e2e tests run: sudo -E make test-e2e-opa - name: Clean up Daemon socket - run: sudo rm /run/finch.sock && sudo rm /run/finch.pid + run: sudo rm /run/finch.sock && sudo rm /run/finch.pid && sudo rm /run/finch-credential.sock - name: Start finch-daemon - run: sudo bin/finch-daemon --debug --socket-owner $UID & + run: sudo cp bin/docker-credential-finch /usr/bin && sudo bin/finch-daemon --debug --socket-owner $UID & - name: Run e2e test run: sudo make test-e2e - name: Clean up Daemon socket - run: sudo rm /var/run/finch.sock && sudo rm /run/finch.pid + run: sudo rm /var/run/finch.sock && sudo rm /run/finch.pid && sudo rm /var/run/finch-credential.sock diff --git a/.github/workflows/finch-vm-test.yaml b/.github/workflows/finch-vm-test.yaml new file mode 100644 index 00000000..d6a8e947 --- /dev/null +++ b/.github/workflows/finch-vm-test.yaml @@ -0,0 +1,129 @@ +name: Finch VM +on: + push: + branches: + - main + paths-ignore: + - '**.md' + pull_request: + branches: + - main + paths-ignore: + - '**.md' + workflow_dispatch: +env: + GO_VERSION: '1.23.8' +jobs: + mac-test-e2e: + runs-on: codebuild-finch-daemon-arm64-2-instance-${{ github.run_id }}-${{ github.run_attempt }} + timeout-minutes: 30 + steps: + - name: Clean macOS runner workspace + run: | + rm -rf ${{ github.workspace }}/* + - name: Configure Git for ec2-user + run: | + git config --global --add safe.directory "*" + shell: bash + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: ${{ env.GO_VERSION }} + cache: false + + - name: Configure Go for ec2-user + run: | + # Ensure Go is properly configured for ec2-user + chown -R ec2-user:staff $GOPATH || true + chown -R ec2-user:staff $RUNNER_TOOL_CACHE/go || true + - name: Install Rosetta 2 + run: su ec2-user -c 'echo "A" | /usr/sbin/softwareupdate --install-rosetta --agree-to-license || true' + + - name: Configure Homebrew for ec2-user + run: | + echo "Creating .brewrc file for ec2-user..." + cat > /Users/ec2-user/.brewrc << 'EOF' + # Homebrew environment setup + export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$PATH" + export HOMEBREW_PREFIX="/opt/homebrew" + export HOMEBREW_CELLAR="/opt/homebrew/Cellar" + export HOMEBREW_REPOSITORY="/opt/homebrew" + export HOMEBREW_NO_AUTO_UPDATE=1 + EOF + chown ec2-user:staff /Users/ec2-user/.brewrc + + # Fix Homebrew permissions + echo "Setting permissions for Homebrew directories..." + mkdir -p /opt/homebrew/Cellar + chown -R ec2-user:staff /opt/homebrew + shell: bash + + # Install dependencies using ec2-user with custom environment + - name: Install dependencies + run: | + echo "Installing dependencies as ec2-user..." + # Run brew with custom environment + su ec2-user -c 'source /Users/ec2-user/.brewrc && brew install lz4 automake autoconf libtool yq' + shell: bash + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # We need to get all the git tags to make version injection work. See VERSION in Makefile for more detail. + fetch-depth: 0 + persist-credentials: false + submodules: recursive + + - name: Configure workspace for ec2-user + run: | + # Ensure workspace is properly owned by ec2-user + chown -R ec2-user:staff ${{ github.workspace }} + + # Install Finch + - name: Install Finch + run: | + echo "Installing Finch as ec2-user..." + + # Run brew with custom environment + su ec2-user -c 'source /Users/ec2-user/.brewrc && brew install finch --cask' + + # Verify installation + su ec2-user -c 'source /Users/ec2-user/.brewrc && brew list | grep finch || echo "finch not installed"' + mkdir -p /private/var/run/finch-lima + cat /etc/passwd + chown ec2-user:daemon /private/var/run/finch-lima + shell: bash + + # Build binaries + - name: Build binaries + run: | + echo "Building cross architecture binaries..." + su ec2-user -c 'cd ${{ github.workspace }} && STATIC=1 GOPROXY=direct GOOS=linux GOARCH=$(GOARCH) make' + su ec2-user -c 'finch vm remove -f' + cp -f ${{ github.workspace }}/bin/finch-daemon /Applications/Finch/finch-daemon/finch-daemon + shell: bash + + # Initialize VM and check version + - name: Check Finch version + run: | + echo "Initializing VM and checking version..." + su ec2-user -c 'finch vm init' + sleep 5 # Wait for services to be ready + echo "Checking Finch version..." + su ec2-user -c 'LIMA_HOME=/Applications/Finch/lima/data /Applications/Finch/lima/bin/limactl shell finch curl --unix-socket /var/run/finch.sock -X GET http:/v1.43/version' + shell: bash + + # Run e2e tests + - name: Run e2e tests + run: | + echo "Running e2e tests..." + su ec2-user -c 'make test-e2e-inside-vm' + shell: bash + + # Cleanup + - name: Stop Finch VM + run: | + echo "Stopping Finch VM as ec2-user..." + + # Stop VM using ec2-user with custom environment + su ec2-user -c "source /Users/ec2-user/.brewrc && HOME=/Users/ec2-user finch vm stop" + shell: bash + if: always() diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 9197fcfc..1f911381 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.17.2" + ".": "0.19.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 31c15cab..7dd28307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,50 @@ # Changelog +## [0.19.1](https://github.com/runfinch/finch-daemon/compare/v0.19.0...v0.19.1) (2025-07-28) + + +### Bug Fixes + +* downgrade nerdctl from v2.1.3 to v2.1.2 ([#289](https://github.com/runfinch/finch-daemon/issues/289)) ([a7487bf](https://github.com/runfinch/finch-daemon/commit/a7487bfd915424f7e643426f3e60614e347162f6)) + +## [0.19.0](https://github.com/runfinch/finch-daemon/compare/v0.18.1...v0.19.0) (2025-07-17) + + +### Features + +* Use nerdctl parsing logic for port publishing ([#265](https://github.com/runfinch/finch-daemon/issues/265)) ([1aec7ce](https://github.com/runfinch/finch-daemon/commit/1aec7cefe590a9f7d7857615f6ebebacd887545a)) + + +### Bug Fixes + +* restore update network settings for dockercompat ([#286](https://github.com/runfinch/finch-daemon/issues/286)) ([8fc3a1e](https://github.com/runfinch/finch-daemon/commit/8fc3a1eccf2ac5bc880dd184d8c13f58996836e7)) + +## [0.18.1](https://github.com/runfinch/finch-daemon/compare/v0.18.0...v0.18.1) (2025-07-11) + + +### Bug Fixes + +* verify release artifact ([20dc067](https://github.com/runfinch/finch-daemon/commit/20dc0677673c88f2bacbe7b7f94d307899918108)) +* verify release artifact docker-credential-finch ([#284](https://github.com/runfinch/finch-daemon/issues/284)) ([20dc067](https://github.com/runfinch/finch-daemon/commit/20dc0677673c88f2bacbe7b7f94d307899918108)) + +## [0.18.0](https://github.com/runfinch/finch-daemon/compare/v0.17.2...v0.18.0) (2025-07-11) + + +### Build System or External Dependencies + +* **deps:** Bump github.com/docker/docker from 28.0.2+incompatible to 28.2.2+incompatible ([#251](https://github.com/runfinch/finch-daemon/issues/251)) ([097fb7e](https://github.com/runfinch/finch-daemon/commit/097fb7ee7badd55f4d4cecd671eaaad290c6e92f)) +* **deps:** Bump github.com/go-viper/mapstructure/v2 ([0e28455](https://github.com/runfinch/finch-daemon/commit/0e2845588b0b5d922e539359f968c73fd271ac14)) +* **deps:** Bump github.com/go-viper/mapstructure/v2 from 2.2.1 to 2.3.0 ([#273](https://github.com/runfinch/finch-daemon/issues/273)) ([0e28455](https://github.com/runfinch/finch-daemon/commit/0e2845588b0b5d922e539359f968c73fd271ac14)) +* **deps:** Bump github.com/open-policy-agent/opa from 1.1.0 to 1.4.0 ([#268](https://github.com/runfinch/finch-daemon/issues/268)) ([7ec7d02](https://github.com/runfinch/finch-daemon/commit/7ec7d025266aa4cca0e137ee91390a16d1a21330)) + + +### Features + +* Add credential management for container build ([#275](https://github.com/runfinch/finch-daemon/issues/275)) ([47d2a65](https://github.com/runfinch/finch-daemon/commit/47d2a6560e6f5864b6aa4f4030e5ad8ebf977c5e)) +* migrate from golang gomock to uber gomock ([#264](https://github.com/runfinch/finch-daemon/issues/264)) ([bb9442a](https://github.com/runfinch/finch-daemon/commit/bb9442a022aeb392822a697f13a238b7f81b8af8)) +* Opa middleware support (Experimental) ([#156](https://github.com/runfinch/finch-daemon/issues/156)) ([91b9ac6](https://github.com/runfinch/finch-daemon/commit/91b9ac673ff13bcbe2a948d953481f5505245c4c)) + + ## [0.17.2](https://github.com/runfinch/finch-daemon/compare/v0.17.1...v0.17.2) (2025-06-06) diff --git a/Makefile b/Makefile index a567ebed..0272b6f2 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,11 @@ FINCH_DAEMON_PROJECT_ROOT ?= $(shell pwd) PREFIX ?= /usr/local BINDIR ?= $(PREFIX)/bin +CRED_HELPER_PREFIX ?= /usr +CRED_HELPER_BINDIR ?= $(CRED_HELPER_PREFIX)/bin + BINARY = $(addprefix bin/,finch-daemon) +CREDENTIAL_HELPER = $(addprefix bin/,docker-credential-finch) PACKAGE := github.com/runfinch/finch-daemon VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.modified' --always --tags) @@ -24,7 +28,10 @@ endif LDFLAGS_BASE := -X $(PACKAGE)/version.Version=$(VERSION) -X $(PACKAGE)/version.GitCommit=$(GITCOMMIT) $(EXTRA_LDFLAGS) .PHONY: build -build: +build: build-daemon build-credential-helper + +.PHONY: build-daemon +build-daemon: ifeq ($(STATIC),) @echo "Building Dynamic Binary" CGO_ENABLED=1 GOOS=linux go build \ @@ -38,6 +45,21 @@ else -v -o $(BINARY) $(PACKAGE)/cmd/finch-daemon endif +.PHONY: build-credential-helper +build-credential-helper: +ifeq ($(STATIC),) + @echo "Building Dynamic Credential Helper" + CGO_ENABLED=1 GOOS=linux go build \ + -ldflags "$(LDFLAGS_BASE)" \ + -v -o $(CREDENTIAL_HELPER) $(PACKAGE)/cmd/finch-credential-helper +else + @echo "Building Static Credential Helper" + CGO_ENABLED=0 GOOS=linux go build \ + -tags netgo \ + -ldflags "$(LDFLAGS_BASE) -extldflags '-static'" \ + -v -o $(CREDENTIAL_HELPER) $(PACKAGE)/cmd/finch-credential-helper +endif + clean: @rm -f $(BINARIES) @rm -rf $(BIN) @@ -63,9 +85,11 @@ start-debug: linux build $(DLV) unlink install: linux install -d $(DESTDIR)$(BINDIR) install $(BINARY) $(DESTDIR)$(BINDIR) + install $(CREDENTIAL_HELPER) $(DESTDIR)$(CRED_HELPER_BINDIR) uninstall: @rm -f $(addprefix $(DESTDIR)$(BINDIR)/,$(notdir $(BINARY))) + @rm -f $(addprefix $(DESTDIR)$(CRED_HELPER_BINDIR)/,$(notdir $(CREDENTIAL_HELPER))) # Unlink the unix socket if the link does not get cleaned up properly (or if finch-daemon is already running) .PHONY: unlink @@ -73,6 +97,9 @@ unlink: linux ifneq ("$(wildcard /run/finch.sock)","") sudo unlink /run/finch.sock endif +ifneq ("$(wildcard /run/finch/credential.sock)","") + sudo unlink /run/finch/credential.sock +endif .PHONY: gen-code gen-code: linux @@ -135,4 +162,26 @@ coverage: linux .PHONY: release release: linux @echo "$@" - @$(FINCH_DAEMON_PROJECT_ROOT)/scripts/create-releases.sh $(RELEASE_TAG) \ No newline at end of file + @$(FINCH_DAEMON_PROJECT_ROOT)/scripts/create-releases.sh $(RELEASE_TAG) + +.PHONY: macos +macos: +ifeq ($(shell uname), Darwin) + @echo "Running on macOS" +else + $(error This target can only be run on macOS!) +endif + + +DAEMON_DOCKER_HOST := "unix:///Applications/Finch/lima/data/finch/sock/finch.sock" +# DAEMON_ROOT + +.PHONY: test-e2e-inside-vm +test-e2e-inside-vm: macos + DOCKER_HOST=$(DAEMON_DOCKER_HOST) \ + DOCKER_API_VERSION="v1.41" \ + TEST_E2E=1 \ + go test ./e2e -test.v -ginkgo.v -ginkgo.randomize-all \ + --subject="finch" \ + --daemon-context-subject-prefix="/Applications/Finch/lima/bin/limactl shell finch sudo" \ + --daemon-context-subject-env="LIMA_HOME=/Applications/Finch/lima/data" diff --git a/api/auth/auth.go b/api/auth/auth.go index 77adafa1..ad5bf5cf 100644 --- a/api/auth/auth.go +++ b/api/auth/auth.go @@ -22,6 +22,10 @@ import ( // authorization credentials for registry operations (push/pull). const AuthHeader = "X-Registry-Auth" +// RegistryConfigHeader is the name of the header used to send encoded registry +// configuration for registry operations (build). +const RegistryConfigHeader = "X-Registry-Config" + // DecodeAuthConfig decodes base64url encoded (RFC4648, section 5) JSON // authentication information as sent through the X-Registry-Auth header. // @@ -61,3 +65,22 @@ func decodeAuthConfigFromReader(rdr io.Reader) (*dockertypes.AuthConfig, error) } return authConfig, nil } + +// DecodeRegistryConfig decodes base64url encoded JSON registry configuration. +func DecodeRegistryConfig(registryConfig string) (map[string]dockertypes.AuthConfig, error) { + if registryConfig == "" { + return map[string]dockertypes.AuthConfig{}, nil + } + + decoded, err := base64.URLEncoding.DecodeString(registryConfig) + if err != nil { + return nil, errors.Wrap(err, "failed to decode registry config") + } + + var registryConfigs map[string]dockertypes.AuthConfig + if err := json.Unmarshal(decoded, ®istryConfigs); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal registry config") + } + + return registryConfigs, nil +} diff --git a/api/credential-router/router.go b/api/credential-router/router.go new file mode 100644 index 00000000..3a6ed0ac --- /dev/null +++ b/api/credential-router/router.go @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package credentialrouter + +import ( + "net/http" + "os" + + ghandlers "github.com/gorilla/handlers" + "github.com/gorilla/mux" + + credentialhandler "github.com/runfinch/finch-daemon/api/credential" + "github.com/runfinch/finch-daemon/pkg/credential" + "github.com/runfinch/finch-daemon/pkg/flog" +) + +// CreateCredentialHandler creates a dedicated HTTP handler for the credential socket. +func CreateCredentialHandler(credentialService *credential.CredentialService, logger flog.Logger, authMiddleware func(http.Handler) http.Handler) (http.Handler, error) { + r := mux.NewRouter() + r.Use(authMiddleware) + + // Register the credential handler + credentialhandler.RegisterHandlers(r, credentialService, logger) + return ghandlers.LoggingHandler(os.Stderr, r), nil +} diff --git a/api/credential-router/router_test.go b/api/credential-router/router_test.go new file mode 100644 index 00000000..9e13b9df --- /dev/null +++ b/api/credential-router/router_test.go @@ -0,0 +1,83 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package credentialrouter + +import ( + "bytes" + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + dockertypes "github.com/docker/cli/cli/config/types" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" + + "github.com/runfinch/finch-daemon/mocks/mocks_logger" + "github.com/runfinch/finch-daemon/pkg/credential" +) + +// TestRouterFunctions is the entry point for unit tests in the router package. +func TestRouterFunctions(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "UnitTests - Router functions") +} + +// Unit tests for the CreateCredentialHandler function. +var _ = Describe("CreateCredentialHandler test", func() { + var ( + mockCtrl *gomock.Controller + mockLogger *mocks_logger.Logger + rr *httptest.ResponseRecorder + credCache *credential.CredentialCache + credentialService *credential.CredentialService + ) + + BeforeEach(func() { + mockCtrl = gomock.NewController(GinkgoT()) + mockLogger = mocks_logger.NewLogger(mockCtrl) + rr = httptest.NewRecorder() + + // Create a real credential service and cache for the handler + credCache = credential.NewCredentialCache() + credentialService = credential.NewCredentialService(mockLogger, credCache) + }) + + AfterEach(func() { + mockCtrl.Finish() + }) + + It("should set up credential routes correctly", func() { + // Store test credentials + buildID := "test-build-id" + serverAddr := "registry.example.com" + authConfig := dockertypes.AuthConfig{ + Username: "testuser", + Password: "testpass", + } + + err := credentialService.StoreAuthConfigs( + context.Background(), + buildID, + map[string]dockertypes.AuthConfig{serverAddr: authConfig}, + ) + Expect(err).Should(BeNil()) + mock_auth_middleware := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + }) + } + handler, err := CreateCredentialHandler(credentialService, mockLogger, mock_auth_middleware) + Expect(err).Should(BeNil()) + + requestBody := fmt.Sprintf(`{"buildID": "%s", "serverAddr": "%s"}`, buildID, serverAddr) + req, _ := http.NewRequest(http.MethodGet, "/finch/credentials", bytes.NewBufferString(requestBody)) + + handler.ServeHTTP(rr, req) + + Expect(rr.Code).Should(Equal(http.StatusOK)) + }) +}) diff --git a/api/credential/credential.go b/api/credential/credential.go new file mode 100644 index 00000000..0a3d7cb6 --- /dev/null +++ b/api/credential/credential.go @@ -0,0 +1,205 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package credential contains functions and structures related to credential management APIs +package credential + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "os" + "syscall" + + "github.com/gorilla/mux" + "github.com/runfinch/finch-daemon/api/response" + credService "github.com/runfinch/finch-daemon/pkg/credential" + "github.com/runfinch/finch-daemon/pkg/flog" + "github.com/shirou/gopsutil/v3/process" +) + +type handler struct { + service *credService.CredentialService + logger flog.Logger +} + +// BuildRequestAuthMiddleware checks peercreds for incoming request for build registry credentials. +func BuildRequestAuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + netConn := GetConn(r) + logger := flog.NewLogrus() + + // Validate the connection credentials + if err := validatePeerCreds(netConn); err != nil { + logger.Errorf("failed to get connection %v", err) + response.SendErrorResponse(w, http.StatusUnauthorized, fmt.Errorf("unauthorized access")) + return + } + + next.ServeHTTP(w, r) + }) +} + +// RegisterHandlers sets up the credential handlers. +func RegisterHandlers(r *mux.Router, service *credService.CredentialService, logger flog.Logger) { + h := newHandler(service, logger) + r.HandleFunc("/finch/credentials", h.getCredentials).Methods(http.MethodGet) +} + +func newHandler(service *credService.CredentialService, logger flog.Logger) *handler { + return &handler{ + service: service, + logger: logger, + } +} + +// getCredentials handles credential requests from the credential helper. +func (h *handler) getCredentials(w http.ResponseWriter, r *http.Request) { + // Parse the request + var req struct { + BuildID string `json:"buildID"` + ServerAddr string `json:"serverAddr"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + h.logger.Errorf("Failed to decode request") + response.SendErrorResponse(w, http.StatusBadRequest, fmt.Errorf("failed to decode request")) + return + } + + // Validate the request + if req.BuildID == "" { + h.logger.Errorf("Request rejected: missing build ID") + response.SendErrorResponse(w, http.StatusBadRequest, fmt.Errorf("request rejected: missing build ID")) + return + } + + if req.ServerAddr == "" { + h.logger.Errorf("Request rejected: missing server address") + response.SendErrorResponse(w, http.StatusBadRequest, fmt.Errorf("request rejected: missing server address")) + return + } + + // Get the credentials + authConfig, err := h.service.GetCredentials(r.Context(), req.BuildID, req.ServerAddr) + if err != nil { + h.logger.Errorf("Failed to get credentials") + response.SendErrorResponse(w, http.StatusNotFound, fmt.Errorf("failed to get credentials")) + return + } + + // Return the full AuthConfig object (which is already dockertypes.AuthConfig) + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(authConfig); err != nil { + h.logger.Errorf("Failed to encode response") + response.SendErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("failed to encode response")) + return + } +} + +// validatePeerCreds validates peer credentials of a connection. +// This function implements security check to ensure that credential requests +// are only accepted from authorized processes within the finch ecosystem: +// +// 1. UID/GID matching: Verifies the connecting process runs under the same user/group +// as the daemon, preventing cross-user access attempts. +// +// 2. Parent-parent PID matching: Implements process ancestry validation by checking that +// the parent parent's of the connecting process is the current daemon process. +// This works because the typical process hierarchy during a build is: +// finch-daemon (this process) -> buildkit -> credential-helper -> connection. +// +// By matching the parent's parent PID against our own PID, +// we ensure the request originates from a legitimate build process spawned by +// this daemon instance, preventing: +// - Processes from other UID/GID impersonating credential helpers. +// - Unauthorized access from unrelated processes with matching UID/GID. +func validatePeerCreds(conn net.Conn) error { + unixConn, ok := conn.(*net.UnixConn) + if !ok { + return errors.New("connection is not a Unix socket connection") + } + + rawConn, err := unixConn.SyscallConn() + if err != nil { + return err + } + + var ( + pid int + uid int + gid int + connErr error + ) + + err = rawConn.Control(func(fd uintptr) { + uid, gid, pid, connErr = getPeerCredentials(int(fd)) + }) + + if err != nil || connErr != nil { + return fmt.Errorf("failed to get peer credentials") + } + + currentUID := os.Getuid() + currentGID := os.Getgid() + + if uid != currentUID { + return errors.New("unauthorized access") + } + + if gid != currentGID { + return errors.New("unauthorized access") + } + + // Validate process ancestry: connecting process should be a descendant of this daemon + p, err := process.NewProcess(int32(pid)) + if err != nil { + return errors.New("internal error") + } + + // Get parent process (typically buildkit) + pp, err := p.Parent() + if err != nil { + return fmt.Errorf("internal error") + } + + // Get parent's parent process (should match current finch-daemon pid) + ppp, err := pp.Parent() + if err != nil { + return fmt.Errorf("internal error") + } + + // Verify the great-grandparent is this daemon process + if int(ppp.Pid) != os.Getpid() { + return errors.New("unauthorized access") + } + + return nil +} + +// getPeerCredentials gets the credentials from a socket connection. +func getPeerCredentials(fd int) (int, int, int, error) { + // GetsockoptUcred is used to get the uid, gid and pid of the established connection. + cred, err := syscall.GetsockoptUcred(fd, syscall.SOL_SOCKET, syscall.SO_PEERCRED) + if err != nil { + return 0, 0, 0, err + } + + return int(cred.Uid), int(cred.Gid), int(cred.Pid), nil +} + +type contextKey struct { + key string +} + +var ConnContextKey = &contextKey{"cred-conn-ctx"} + +func SetConn(ctx context.Context, c net.Conn) context.Context { + return context.WithValue(ctx, ConnContextKey, c) +} +func GetConn(r *http.Request) net.Conn { + return r.Context().Value(ConnContextKey).(net.Conn) +} diff --git a/api/handlers/builder/build.go b/api/handlers/builder/build.go index 4eceac90..f77faba8 100644 --- a/api/handlers/builder/build.go +++ b/api/handlers/builder/build.go @@ -12,7 +12,9 @@ import ( "github.com/containerd/containerd/v2/pkg/namespaces" "github.com/containerd/nerdctl/v2/pkg/api/types" + dockertypes "github.com/docker/cli/cli/config/types" + "github.com/runfinch/finch-daemon/api/auth" "github.com/runfinch/finch-daemon/api/response" "github.com/runfinch/finch-daemon/pkg/utility/maputility" ) @@ -20,6 +22,45 @@ import ( // build function is the http handler function for /build API. func (h *handler) build(w http.ResponseWriter, r *http.Request) { streamWriter := response.NewStreamWriter(w) + var buildID string + + registryConfig := r.Header.Get(auth.RegistryConfigHeader) + + if h.credentialSvc == nil { + h.logger.Warnf("Credential service is not initialized") + } else { + var authConfigs map[string]dockertypes.AuthConfig + + if registryConfig != "" { + var err error + authConfigs, err = auth.DecodeRegistryConfig(registryConfig) + if err != nil { + streamWriter.WriteError(http.StatusInternalServerError, + fmt.Errorf("failed to decode registry config: %w", err)) + return + } + } + + if len(authConfigs) > 0 { + var err error + buildID, err = h.credentialSvc.GenerateBuildID() + if err != nil { + streamWriter.WriteError(http.StatusInternalServerError, + fmt.Errorf("failed to generate build ID: %w", err)) + return + } + + defer func() { + h.credentialSvc.RemoveCredentials(buildID) + }() + + if err = h.credentialSvc.StoreAuthConfigs(r.Context(), buildID, authConfigs); err != nil { + streamWriter.WriteError(http.StatusInternalServerError, + fmt.Errorf("failed to store auth configs: %w", err)) + return + } + } + } // create the build options based on passed parameter buildOptions, err := h.getBuildOptions(w, r, streamWriter) @@ -30,7 +71,7 @@ func (h *handler) build(w http.ResponseWriter, r *http.Request) { // call the service to build ctx := namespaces.WithNamespace(r.Context(), h.Config.Namespace) - result, err := h.service.Build(ctx, buildOptions, r.Body) + result, err := h.service.Build(ctx, buildOptions, r.Body, buildID) if err != nil { streamWriter.WriteError(http.StatusInternalServerError, err) return @@ -138,11 +179,19 @@ func getQueryParamMap(r *http.Request, paramName string, defaultValue []string) return defaultValue, nil } + // First try to parse as map var parsedMap map[string]string err := json.Unmarshal([]byte(query), &parsedMap) - if err != nil { - return nil, fmt.Errorf("unable to parse %s query: %s", paramName, err) + if err == nil { + return maputility.Flatten(parsedMap, maputility.KeyEqualsValueFormat), nil + } + + // If that fails, try to parse as array + var parsedArray []string + err = json.Unmarshal([]byte(query), &parsedArray) + if err == nil { + return parsedArray, nil } - return maputility.Flatten(parsedMap, maputility.KeyEqualsValueFormat), nil + return nil, fmt.Errorf("unable to parse %s query: %s", paramName, err) } diff --git a/api/handlers/builder/build_test.go b/api/handlers/builder/build_test.go index 5a788edf..d4a18ccb 100644 --- a/api/handlers/builder/build_test.go +++ b/api/handlers/builder/build_test.go @@ -5,25 +5,40 @@ package builder import ( "bufio" + "context" + "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" + "strings" "github.com/containerd/nerdctl/v2/pkg/config" - "go.uber.org/mock/gomock" + dockertypes "github.com/docker/cli/cli/config/types" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" + "github.com/runfinch/finch-daemon/api/auth" "github.com/runfinch/finch-daemon/api/response" "github.com/runfinch/finch-daemon/api/types" "github.com/runfinch/finch-daemon/mocks/mocks_backend" "github.com/runfinch/finch-daemon/mocks/mocks_builder" "github.com/runfinch/finch-daemon/mocks/mocks_logger" + "github.com/runfinch/finch-daemon/pkg/credential" "github.com/runfinch/finch-daemon/pkg/errdefs" ) +// encodeRegistryConfig encodes auth configurations to a base64-encoded JSON string. +func encodeRegistryConfig(authConfigs map[string]dockertypes.AuthConfig) (string, error) { + configJSON, err := json.Marshal(authConfigs) + if err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(configJSON), nil +} + var _ = Describe("Build API", func() { var ( mockCtrl *gomock.Controller @@ -43,8 +58,11 @@ var _ = Describe("Build API", func() { logger = mocks_logger.NewLogger(mockCtrl) service = mocks_builder.NewMockService(mockCtrl) ncBuildSvc = mocks_backend.NewMockNerdctlBuilderSvc(mockCtrl) + // No need for credService in this test c := config.Config{} - h = newHandler(service, &c, logger, ncBuildSvc) + credCache := credential.NewCredentialCache() + credService := credential.NewCredentialService(logger, credCache) + h = newHandler(service, &c, logger, ncBuildSvc, credService) rr = httptest.NewRecorder() stream = response.NewStreamWriter(rr) req, _ = http.NewRequest(http.MethodPost, "/build", nil) @@ -63,8 +81,11 @@ var _ = Describe("Build API", func() { }) Context("handler", func() { It("should return 200 as success response", func() { - // service mock returns nil to mimic service built the image successfully. - service.EXPECT().Build(gomock.Any(), gomock.Any(), gomock.Any()).Return(result, nil) + // service mock returns build results to mimic service built the image successfully. + service.EXPECT().Build(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx interface{}, options interface{}, reader interface{}, buildID string) ([]types.BuildResult, error) { + return result, nil + }) ncBuildSvc.EXPECT().GetBuildkitHost().Return("mocked-value", nil).AnyTimes() h.build(rr, req) @@ -87,20 +108,17 @@ var _ = Describe("Build API", func() { It("should return 500 error", func() { // service mock returns not found error to mimic image build failed - service.EXPECT().Build(gomock.Any(), gomock.Any(), gomock.Any()).Return( - nil, errdefs.NewNotFound(fmt.Errorf("some error"))) + service.EXPECT().Build(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn( + func(ctx interface{}, options interface{}, reader interface{}, buildID string) ([]types.BuildResult, error) { + return nil, errdefs.NewNotFound(fmt.Errorf("some error")) + }) ncBuildSvc.EXPECT().GetBuildkitHost().Return("mocked-value", nil).AnyTimes() h.build(rr, req) Expect(rr).Should(HaveHTTPStatus(http.StatusInternalServerError)) Expect(rr.Body).Should(MatchJSON(`{"message": "some error"}`)) - }) - It("should return error due to buildkit failure", func() { - req = httptest.NewRequest(http.MethodPost, "/build", nil) - logger.EXPECT().Warnf("Failed to get buildkit host: %v", gomock.Any()) ncBuildSvc.EXPECT().GetBuildkitHost().Return("", fmt.Errorf("some error")).AnyTimes() h.build(rr, req) Expect(rr).Should(HaveHTTPStatus(http.StatusInternalServerError)) - Expect(rr.Body).Should(MatchJSON(`{"message": "some error"}`)) }) It("should set the buildkit host", func() { req = httptest.NewRequest(http.MethodPost, "/build", nil) @@ -167,7 +185,7 @@ var _ = Describe("Build API", func() { Expect(err).Should(BeNil()) Expect(buildOption.NoCache).Should(BeTrue()) }) - It("should set the CacheFrom query param", func() { + It("should set the CacheFrom query param as map", func() { ncBuildSvc.EXPECT().GetBuildkitHost().Return("mocked-value", nil).AnyTimes() req = httptest.NewRequest(http.MethodPost, "/build?cachefrom={\"image1\":\"tag1\",\"image2\":\"tag2\"}", nil) buildOption, err := h.getBuildOptions(rr, req, stream) @@ -175,6 +193,14 @@ var _ = Describe("Build API", func() { Expect(buildOption.CacheFrom).Should(ContainElements("image1=tag1", "image2=tag2")) }) + It("should set the CacheFrom query param as array", func() { + ncBuildSvc.EXPECT().GetBuildkitHost().Return("mocked-value", nil).AnyTimes() + req = httptest.NewRequest(http.MethodPost, "/build?cachefrom=[\"image1:tag1\",\"image2:tag2\"]", nil) + buildOption, err := h.getBuildOptions(rr, req, stream) + Expect(err).Should(BeNil()) + Expect(buildOption.CacheFrom).Should(ContainElements("image1:tag1", "image2:tag2")) + }) + It("should set the BuildArgs query param", func() { ncBuildSvc.EXPECT().GetBuildkitHost().Return("mocked-value", nil).AnyTimes() req = httptest.NewRequest(http.MethodPost, "/build?buildargs={\"ARG1\":\"value1\",\"ARG2\":\"value2\"}", nil) @@ -224,5 +250,123 @@ var _ = Describe("Build API", func() { Expect(buildOption.NetworkMode).Should(BeEmpty()) Expect(buildOption.Output).Should(BeEmpty()) }) + + It("should store credentials uniquely and clean up after build is complete", func() { + // Create a real credential cache and service + credCache := credential.NewCredentialCache() + credService := credential.NewCredentialService(logger, credCache) + + // Create build handler with the real credential service + h := newHandler(service, &config.Config{}, logger, ncBuildSvc, credService) + + // Setup auth configs for the test + authConfigs1 := map[string]dockertypes.AuthConfig{ + "registry1.example.com": { + Username: "user1", + Password: "pass1", + }, + "registry2.example.com": { + Username: "user2", + Password: "pass2", + }, + } + + authConfigs2 := map[string]dockertypes.AuthConfig{ + "registry1.example.com": { + Username: "different-user1", + Password: "different-pass1", + }, + "registry2.example.com": { + Username: "different-user2", + Password: "different-pass2", + }, + } + + buildId, err := credService.GenerateBuildID() + Expect(err).Should(BeNil()) + + err = credService.StoreAuthConfigs(context.TODO(), buildId, authConfigs2) + Expect(err).Should(BeNil()) + + registryAuthHeader1, err := encodeRegistryConfig(authConfigs1) + Expect(err).Should(BeNil()) + + registryAuthHeader2, err := encodeRegistryConfig(authConfigs2) + Expect(err).Should(BeNil()) + + Expect(len(credCache.Entries)).Should(Equal(1), "Credential cache should hold the entry made") + + // First build request + req1 := httptest.NewRequest(http.MethodPost, "/build", strings.NewReader("test-body-1")) + req1.Header.Set(auth.RegistryConfigHeader, registryAuthHeader1) + rr1 := httptest.NewRecorder() + + var capturedBuildID1 string + service.EXPECT().Build(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx interface{}, options interface{}, reader interface{}, buildID string) ([]types.BuildResult, error) { + capturedBuildID1 = buildID + Expect(buildID).ShouldNot(BeEmpty()) + + auth1, err := credService.GetCredentials(context.Background(), buildID, "registry1.example.com") + Expect(err).Should(BeNil()) + Expect(auth1.Username).Should(Equal("user1")) + Expect(auth1.Password).Should(Equal("pass1")) + + auth2, err := credService.GetCredentials(context.Background(), buildID, "registry2.example.com") + Expect(err).Should(BeNil()) + Expect(auth2.Username).Should(Equal("user2")) + Expect(auth2.Password).Should(Equal("pass2")) + + Expect(len(credCache.Entries)).Should(Equal(2), "Credential cache should contain both creds") + + return result, nil + }) + + ncBuildSvc.EXPECT().GetBuildkitHost().Return("mocked-value", nil).AnyTimes() + + h.build(rr1, req1) + Expect(rr1).Should(HaveHTTPStatus(http.StatusOK)) + Expect(len(credCache.Entries)).Should(Equal(1), "Credential cache should clear one cache entry after first build") + + // Second build request + req2 := httptest.NewRequest(http.MethodPost, "/build", strings.NewReader("test-body-2")) + req2.Header.Set(auth.RegistryConfigHeader, registryAuthHeader2) + rr2 := httptest.NewRecorder() + + var capturedBuildID2 string + service.EXPECT().Build(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx interface{}, options interface{}, reader interface{}, buildID string) ([]types.BuildResult, error) { + capturedBuildID2 = buildID + Expect(buildID).ShouldNot(BeEmpty()) + + // Verify credentials from second request + auth1, err := credService.GetCredentials(context.Background(), buildID, "registry1.example.com") + Expect(err).Should(BeNil()) + Expect(auth1.Username).Should(Equal("different-user1")) + Expect(auth1.Password).Should(Equal("different-pass1")) + + auth2, err := credService.GetCredentials(context.Background(), buildID, "registry2.example.com") + Expect(err).Should(BeNil()) + Expect(auth2.Username).Should(Equal("different-user2")) + Expect(auth2.Password).Should(Equal("different-pass2")) + + Expect(len(credCache.Entries)).Should(Equal(2), "Credential cache should contain both creds") + + return result, nil + }) + + h.build(rr2, req2) + Expect(rr2).Should(HaveHTTPStatus(http.StatusOK)) + + // Verify buildIDs are unique + Expect(capturedBuildID1).ShouldNot(BeEmpty()) + Expect(capturedBuildID2).ShouldNot(BeEmpty()) + Expect(capturedBuildID1).ShouldNot(Equal(capturedBuildID2), "Build IDs should be unique") + + Expect(len(credCache.Entries)).Should(Equal(1), "Credential cache should have one entry left") + + credService.RemoveCredentials(buildId) + Expect(len(credCache.Entries)).Should(Equal(0), "Credential cache should be clear now") + }) }) }) diff --git a/api/handlers/builder/builder.go b/api/handlers/builder/builder.go index 67bd9ec0..ad2fb643 100644 --- a/api/handlers/builder/builder.go +++ b/api/handlers/builder/builder.go @@ -14,6 +14,7 @@ import ( "github.com/runfinch/finch-daemon/api/types" "github.com/runfinch/finch-daemon/internal/backend" + "github.com/runfinch/finch-daemon/pkg/credential" "github.com/runfinch/finch-daemon/pkg/flog" ) @@ -23,8 +24,9 @@ func RegisterHandlers(r types.VersionedRouter, conf *config.Config, logger flog.Logger, ncBuildSvc backend.NerdctlBuilderSvc, + credService *credential.CredentialService, ) { - h := newHandler(service, conf, logger, ncBuildSvc) + h := newHandler(service, conf, logger, ncBuildSvc, credService) r.HandleFunc("/build", h.build, http.MethodPost) } @@ -32,22 +34,24 @@ func RegisterHandlers(r types.VersionedRouter, // //go:generate mockgen --destination=../../../mocks/mocks_builder/buildersvc.go -package=mocks_builder github.com/runfinch/finch-daemon/api/handlers/builder Service type Service interface { - Build(ctx context.Context, options *ncTypes.BuilderBuildOptions, tarBody io.ReadCloser) ([]types.BuildResult, error) + Build(ctx context.Context, options *ncTypes.BuilderBuildOptions, tarBody io.ReadCloser, buildID string) ([]types.BuildResult, error) } // newHandler creates the handler that serves all the container related APIs. -func newHandler(service Service, conf *config.Config, logger flog.Logger, ncBuildSvc backend.NerdctlBuilderSvc) *handler { +func newHandler(service Service, conf *config.Config, logger flog.Logger, ncBuildSvc backend.NerdctlBuilderSvc, credService *credential.CredentialService) *handler { return &handler{ - service: service, - Config: conf, - logger: logger, - ncBuildSvc: ncBuildSvc, + service: service, + Config: conf, + logger: logger, + ncBuildSvc: ncBuildSvc, + credentialSvc: credService, } } type handler struct { - service Service - Config *config.Config - logger flog.Logger - ncBuildSvc backend.NerdctlBuilderSvc + service Service + Config *config.Config + logger flog.Logger + ncBuildSvc backend.NerdctlBuilderSvc + credentialSvc *credential.CredentialService } diff --git a/api/handlers/builder/builder_test.go b/api/handlers/builder/builder_test.go index 2adcc0cd..eb4d23a7 100644 --- a/api/handlers/builder/builder_test.go +++ b/api/handlers/builder/builder_test.go @@ -10,10 +10,10 @@ import ( "testing" "github.com/containerd/nerdctl/v2/pkg/config" - "go.uber.org/mock/gomock" "github.com/gorilla/mux" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" "github.com/runfinch/finch-daemon/api/types" "github.com/runfinch/finch-daemon/mocks/mocks_backend" @@ -45,15 +45,16 @@ var _ = Describe("Build API ", func() { service = mocks_builder.NewMockService(mockCtrl) router = mux.NewRouter() ncBuildSvc := mocks_backend.NewMockNerdctlBuilderSvc(mockCtrl) - RegisterHandlers(types.VersionedRouter{Router: router}, service, &conf, logger, ncBuildSvc) + RegisterHandlers(types.VersionedRouter{Router: router}, service, &conf, logger, ncBuildSvc, nil) rr = httptest.NewRecorder() ncBuildSvc.EXPECT().GetBuildkitHost().Return("", nil).AnyTimes() }) Context("handler", func() { It("should call build method", func() { // setup mocks - service.EXPECT().Build(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error from build api")) + service.EXPECT().Build(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error from build api")) req, _ = http.NewRequest(http.MethodPost, "/build", nil) + logger.EXPECT().Warnf(gomock.Any()).Times(1) // call the API to check if it returns the error generated from the build method router.ServeHTTP(rr, req) Expect(rr).Should(HaveHTTPStatus(http.StatusInternalServerError)) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index 5ff17444..e93cb0fd 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -9,13 +9,13 @@ import ( "net/http" "os" "path/filepath" - "strconv" "strings" "github.com/containerd/containerd/v2/pkg/namespaces" gocni "github.com/containerd/go-cni" ncTypes "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/defaults" + "github.com/containerd/nerdctl/v2/pkg/portutil" "github.com/docker/go-connections/nat" "github.com/moby/moby/api/types/blkiodev" "github.com/sirupsen/logrus" @@ -320,7 +320,6 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { // #endregion } - portMappings, err := translatePortMappings(req.HostConfig.PortBindings) if err != nil { logrus.Debugf("failed to parse port mappings: %s", err) @@ -393,25 +392,30 @@ func translatePortMappings(portMappings nat.PortMap) ([]gocni.PortMapping, error if portMappings == nil { return ports, nil } + seenPortMappings := make(map[string]bool) for portName, portBindings := range portMappings { for _, portBinding := range portBindings { - hostPort, err := strconv.ParseInt(portBinding.HostPort, 10, 32) - if err != nil { - return []gocni.PortMapping{}, fmt.Errorf("failed to parse host port (%s) to integer: %w", portBinding.HostPort, err) + portStr := "" + if portBinding.HostIP != "" { + portStr += portBinding.HostIP + ":" } - // Cannot use portName.Int() because it assumes nat.NewPort() was used - // for error handling. - containerPort, err := strconv.ParseInt(portName.Port(), 10, 32) - if err != nil { - return []gocni.PortMapping{}, fmt.Errorf("failed to parse container port (%s) to integer: %w", portName, err) + + // Add host port (or empty string if not specified) + portStr += portBinding.HostPort + ":" + + // Add container port and protocol + portStr += portName.Port() + "/" + portName.Proto() + + if seenPortMappings[portStr] { + continue } - portMap := gocni.PortMapping{ - HostPort: int32(hostPort), - ContainerPort: int32(containerPort), - Protocol: portName.Proto(), - HostIP: portBinding.HostIP, + seenPortMappings[portStr] = true + + portMaps, err := portutil.ParseFlagP(portStr) + if err != nil { + return []gocni.PortMapping{}, fmt.Errorf("failed to parse port mapping %q: %w", portStr, err) } - ports = append(ports, portMap) + ports = append(ports, portMaps...) } } return ports, nil diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index 5920d9e1..65857b60 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -101,7 +101,7 @@ var _ = Describe("Container Create API ", func() { HostPort: 8001, ContainerPort: 8000, Protocol: "tcp", - HostIP: "", + HostIP: "0.0.0.0", }, { HostPort: 9001, @@ -140,7 +140,7 @@ var _ = Describe("Container Create API ", func() { "ExposedPorts": {"8000/tcp": {}}, "HostConfig": { "PortBindings": { - "8000/tcp": [{"HostIp": "", "HostPort": ""}], + "8000/tcp": [{"HostIp": "", "HostPort": "invalid-port"}] } } }`) diff --git a/api/handlers/image/push_test.go b/api/handlers/image/push_test.go index c7860012..7a1ab104 100644 --- a/api/handlers/image/push_test.go +++ b/api/handlers/image/push_test.go @@ -15,10 +15,10 @@ import ( "github.com/containerd/nerdctl/v2/pkg/config" dockertypes "github.com/docker/cli/cli/config/types" - "go.uber.org/mock/gomock" "github.com/gorilla/mux" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" "github.com/runfinch/finch-daemon/api/auth" "github.com/runfinch/finch-daemon/api/response" diff --git a/api/router/router.go b/api/router/router.go index b5e703e1..702bf63c 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -28,6 +28,7 @@ import ( "github.com/runfinch/finch-daemon/api/response" "github.com/runfinch/finch-daemon/api/types" "github.com/runfinch/finch-daemon/internal/backend" + "github.com/runfinch/finch-daemon/pkg/credential" "github.com/runfinch/finch-daemon/pkg/flog" "github.com/runfinch/finch-daemon/version" ) @@ -51,6 +52,7 @@ type Options struct { VolumeService volume.Service ExecService exec.Service DistributionService distribution.Service + CredentialService *credential.CredentialService RegoFilePath string // NerdctlWrapper wraps the interactions with nerdctl to build @@ -77,10 +79,11 @@ func New(opts *Options) (http.Handler, error) { image.RegisterHandlers(vr, opts.ImageService, opts.Config, logger) container.RegisterHandlers(vr, opts.ContainerService, opts.Config, logger) network.RegisterHandlers(vr, opts.NetworkService, opts.Config, logger) - builder.RegisterHandlers(vr, opts.BuilderService, opts.Config, logger, opts.NerdctlWrapper) + builder.RegisterHandlers(vr, opts.BuilderService, opts.Config, logger, opts.NerdctlWrapper, opts.CredentialService) volume.RegisterHandlers(vr, opts.VolumeService, opts.Config, logger) exec.RegisterHandlers(vr, opts.ExecService, opts.Config, logger) distribution.RegisterHandlers(vr, opts.DistributionService, opts.Config, logger) + return ghandlers.LoggingHandler(os.Stderr, r), nil } diff --git a/api/router/router_test.go b/api/router/router_test.go index 2b69ed2b..70996e65 100644 --- a/api/router/router_test.go +++ b/api/router/router_test.go @@ -13,9 +13,9 @@ import ( "testing" "github.com/containerd/nerdctl/v2/pkg/config" - "go.uber.org/mock/gomock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" "github.com/runfinch/finch-daemon/api/types" "github.com/runfinch/finch-daemon/mocks/mocks_system" diff --git a/cmd/finch-credential-helper/main.go b/cmd/finch-credential-helper/main.go new file mode 100644 index 00000000..4ae6e8d0 --- /dev/null +++ b/cmd/finch-credential-helper/main.go @@ -0,0 +1,116 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package main implements a credential helper for Finch daemon +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "os" + "time" + + dockertypes "github.com/docker/cli/cli/config/types" + "github.com/docker/docker-credential-helpers/credentials" + "github.com/runfinch/finch-daemon/pkg/config" + "github.com/runfinch/finch-daemon/pkg/flog" +) + +const ( + // ConnectionTimeout is the timeout for connecting to the credential socket. + ConnectionTimeout = 5 * time.Second +) + +// CredentialSocketPath is the path to the credential socket. +var CredentialSocketPath = config.DefaultCredentialAddr + +var log flog.Logger + +// FinchCredentialHelper implements the credentials.Helper interface. +type FinchCredentialHelper struct{} + +// Add is not implemented for Finch credential helper. +func (h FinchCredentialHelper) Add(*credentials.Credentials) error { + return fmt.Errorf("not implemented") +} + +// Delete is not implemented for Finch credential helper. +func (h FinchCredentialHelper) Delete(serverURL string) error { + return fmt.Errorf("not implemented") +} + +// List is not implemented for Finch credential helper. +func (h FinchCredentialHelper) List() (map[string]string, error) { + return nil, fmt.Errorf("not implemented") +} + +// Get retrieves credentials from the Finch daemon. +func (h FinchCredentialHelper) Get(serverURL string) (string, string, error) { + buildID := os.Getenv("FINCH_BUILD_ID") + if buildID == "" { + return "", "", credentials.NewErrCredentialsNotFound() + } + + credentialSocketPath := os.Getenv("FINCH_CREDENTIAL_SOCKET") + if credentialSocketPath == "" { + credentialSocketPath = CredentialSocketPath + } + + client := &http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", credentialSocketPath) + }, + }, + } + + // Create request with JSON body. + reqBody := struct { + BuildID string `json:"buildID"` + ServerAddr string `json:"serverAddr"` + }{BuildID: buildID, ServerAddr: serverURL} + + jsonData, _ := json.Marshal(reqBody) + req, _ := http.NewRequest(http.MethodGet, "http://localhost/finch/credentials", bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return "", "", fmt.Errorf("error sending request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", "", credentials.NewErrCredentialsNotFound() + } + + // Process successful response + responseBytes, err := io.ReadAll(resp.Body) + if err != nil { + log.Errorf("error reading response: %v", err) + return "", "", fmt.Errorf("error reading response: %v", err) + } + + var errorResponse struct { + Message string `json:"message,omitempty"` + } + if err := json.Unmarshal(responseBytes, &errorResponse); err == nil && errorResponse.Message != "" { + return "", "", credentials.NewErrCredentialsNotFound() + } + + var authConfig dockertypes.AuthConfig + if err := json.Unmarshal(responseBytes, &authConfig); err != nil { + return "", "", credentials.NewErrCredentialsNotFound() + } + + return authConfig.Username, authConfig.Password, nil +} + +func main() { + credentials.Serve(FinchCredentialHelper{}) +} diff --git a/cmd/finch-daemon/main.go b/cmd/finch-daemon/main.go index ed2f8129..330c5352 100644 --- a/cmd/finch-daemon/main.go +++ b/cmd/finch-daemon/main.go @@ -17,6 +17,7 @@ import ( "sync" "syscall" "time" + "os/exec" // #nosec // register HTTP handler for /debug/pprof on the DefaultServeMux. @@ -26,26 +27,25 @@ import ( "github.com/coreos/go-systemd/v22/daemon" "github.com/gofrs/flock" "github.com/moby/moby/pkg/pidfile" + credentialhandler "github.com/runfinch/finch-daemon/api/credential" + credentialRouter "github.com/runfinch/finch-daemon/api/credential-router" "github.com/runfinch/finch-daemon/api/router" "github.com/runfinch/finch-daemon/internal/fs/passwd" + "github.com/runfinch/finch-daemon/pkg/credential" "github.com/runfinch/finch-daemon/pkg/flog" "github.com/runfinch/finch-daemon/version" + "github.com/runfinch/finch-daemon/pkg/config" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -const ( - // Keep this value in sync with `guestSocket` in README.md. - defaultFinchAddr = "/run/finch.sock" - defaultNamespace = "finch" - defaultConfigPath = "/etc/finch/finch.toml" - defaultPidFile = "/run/finch.pid" -) - type DaemonOptions struct { debug bool socketAddr string + credentialAddr string socketOwner int + credentialSocketOwner int debugAddress string configPath string pidFile string @@ -64,12 +64,14 @@ func main() { RunE: runAdapter, SilenceUsage: true, } - rootCmd.Flags().StringVar(&options.socketAddr, "socket-addr", defaultFinchAddr, "server listening Unix socket address") + rootCmd.Flags().StringVar(&options.socketAddr, "socket-addr", config.DefaultFinchAddr, "server listening Unix socket address") + rootCmd.Flags().StringVar(&options.credentialAddr, "credential-socket-addr", config.DefaultCredentialAddr, "credential server listening Unix socket address") rootCmd.Flags().BoolVar(&options.debug, "debug", false, "turn on debug log level") rootCmd.Flags().IntVar(&options.socketOwner, "socket-owner", -1, "Uid and Gid of the server socket") + rootCmd.Flags().IntVar(&options.credentialSocketOwner, "credential-socket-owner", 0, "Uid and Gid of the server socket") rootCmd.Flags().StringVar(&options.debugAddress, "debug-addr", "", "") - rootCmd.Flags().StringVar(&options.configPath, "config-file", defaultConfigPath, "Daemon Config Path") - rootCmd.Flags().StringVar(&options.pidFile, "pidfile", defaultPidFile, "pid file location") + rootCmd.Flags().StringVar(&options.configPath, "config-file", config.DefaultConfigPath, "Daemon Config Path") + rootCmd.Flags().StringVar(&options.pidFile, "pidfile", config.DefaultPidFile, "pid file location") rootCmd.Flags().StringVar(&options.regoFilePath, "rego-file", "", "Rego Policy Path (requires --experimental flag)") rootCmd.Flags().BoolVar(&options.skipRegoPermCheck, "skip-rego-perm-check", false, "skip the rego file permission check (allows permissions more permissive than 0600)") rootCmd.Flags().BoolVar(&options.enableExperimental, "experimental", false, "enable experimental features") @@ -149,19 +151,80 @@ func run(options *DaemonOptions) error { } logger := flog.NewLogrus() - r, err := newRouter(options, logger) + credCache := credential.NewCredentialCache() + credService := credential.NewCredentialService(logger, credCache) + + r, err := newRouter(options, logger, credService) if err != nil { return fmt.Errorf("failed to create a router: %w", err) } serverWg := &sync.WaitGroup{} - serverWg.Add(1) + serverWg.Add(2) // Add 2 for both main socket and credential socket - if options.socketOwner >= 0 { - if err := defineDockerConfig(options.socketOwner); err != nil { + logger.Infof("setting docker config, socket owner %v", options.credentialSocketOwner) + if options.credentialSocketOwner >= 0 { + if err := defineDockerConfig(options.credentialSocketOwner); err != nil { return fmt.Errorf("failed to get finch config: %w", err) } + logger.Infof("setup done %v", os.Getenv("DOCKER_CONFIG")) + } else { + logger.Infof("skipping docker config setup") + } + + // Start of Build Credential Socket Implementation + // Set the credential address in the config package + config.SetCredentialAddr(options.credentialAddr) + + // Remove existing credential socket if it exists + if _, err := os.Stat(options.credentialAddr); err == nil { + if err := os.Remove(options.credentialAddr); err != nil { + return fmt.Errorf("failed to remove existing credential socket: %w", err) + } + } + + credListener, err := net.Listen("unix", options.credentialAddr) + if err != nil { + return fmt.Errorf("failed to listen on credential socket: %w", err) + } + + if err := os.Chmod(options.credentialAddr, 0600); err != nil { + return fmt.Errorf("failed to set permissions on credential socket: %w", err) + } + + if options.credentialSocketOwner >= 0 { + if err := os.Chown(options.credentialAddr, options.credentialSocketOwner, options.credentialSocketOwner); err != nil { + return fmt.Errorf("failed to chown the credential socket: %w", err) + } + } + + credhandler, err := credentialRouter.CreateCredentialHandler(credService, logger, credentialhandler.BuildRequestAuthMiddleware) + if err != nil { + return fmt.Errorf("failed to create credential router: %w", err) + } + + credServer := &http.Server{ + Handler: credhandler, + ConnContext: credentialhandler.SetConn, + ReadHeaderTimeout: 5 * time.Second, } + handleSignal(options.credentialAddr, credServer, logger) + + defer func() { + if err := credServer.Close(); err != nil { + logger.Warnf("Error closing credential server: %v", err) + } + os.Remove(options.credentialAddr) + }() + + go func() { + defer serverWg.Done() + logger.Infof("Serving credential endpoint on %s...", options.credentialAddr) + if err := credServer.Serve(credListener); err != nil && !errors.Is(err, http.ErrServerClosed) { + logger.Fatal(err) + } + }() + // End of Build Credential Socker Implementation listener, err := getListener(options) if err != nil { @@ -206,7 +269,7 @@ func run(options *DaemonOptions) error { return nil } -func newRouter(options *DaemonOptions, logger *flog.Logrus) (http.Handler, error) { +func newRouter(options *DaemonOptions, logger *flog.Logrus, credService *credential.CredentialService) (http.Handler, error) { conf, err := initializeConfig(options) if err != nil { return nil, err @@ -237,7 +300,7 @@ func newRouter(options *DaemonOptions, logger *flog.Logrus) (http.Handler, error logger.Info("experimental flag passed, but no experimental features enabled") } - opts := createRouterOptions(conf, clientWrapper, ncWrapper, logger, regoFilePath) + opts := createRouterOptions(conf, clientWrapper, ncWrapper, logger, regoFilePath, credService) newRouter, err := router.New(opts) if err != nil { return nil, err @@ -277,15 +340,46 @@ func sdNotify(state string, logger *flog.Logrus) { } // defineDockerConfig defines the DOCKER_CONFIG environment variable for the process -// to be $HOME/.finch. When building an image via finch-daemon, buildctl uses this variable +// to be $HOME/.finch-daemon. When building an image via finch-daemon, buildctl uses this variable // to load auth configs. -// -// This is a hack and should be fixed by passing the actual credentials that come in -// via the build API to buildctl instead. func defineDockerConfig(uid int) error { return passwd.Walk(func(e passwd.Entry) bool { if e.UID == uid { - os.Setenv("DOCKER_CONFIG", fmt.Sprintf("%s/.finch", e.Home)) + var configContent []byte + + // Check if docker-credential-finch exists in PATH + credentialHelperFound := false + _, err := exec.LookPath("docker-credential-finch") + if err == nil { + credentialHelperFound = true + } + + // This is to ensure if credhelper is not found builds function as per current architecture. + if !credentialHelperFound { + log.Printf("Warning: docker-credential-finch not found in PATH, credential store will not be configured") + os.Setenv("DOCKER_CONFIG", fmt.Sprintf("%s/.finch", e.Home)) + return false + } else { + configContent = []byte(`{ + "credsStore": "finch" +}`) + } + + dockerConfigDir := fmt.Sprintf("%s/.finch/.finch-daemon", e.Home) + + if err := os.MkdirAll(dockerConfigDir, 0700); err != nil { + return true + } + + configFilePath := filepath.Join(dockerConfigDir, "config.json") + + if err := os.WriteFile(configFilePath, configContent, 0600); err != nil { + log.Printf("failed to write docker config file: %v", err) + return true + } + + // Set the DOCKER_CONFIG environment variable + os.Setenv("DOCKER_CONFIG", dockerConfigDir) return false } return true diff --git a/cmd/finch-daemon/router_utils.go b/cmd/finch-daemon/router_utils.go index 839eccef..674076db 100644 --- a/cmd/finch-daemon/router_utils.go +++ b/cmd/finch-daemon/router_utils.go @@ -15,6 +15,8 @@ import ( "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/config" toml "github.com/pelletier/go-toml/v2" + + finchconfig "github.com/runfinch/finch-daemon/pkg/config" "github.com/runfinch/finch-daemon/api/router" "github.com/runfinch/finch-daemon/internal/backend" "github.com/runfinch/finch-daemon/internal/service/builder" @@ -26,6 +28,7 @@ import ( "github.com/runfinch/finch-daemon/internal/service/system" "github.com/runfinch/finch-daemon/internal/service/volume" "github.com/runfinch/finch-daemon/pkg/archive" + "github.com/runfinch/finch-daemon/pkg/credential" "github.com/runfinch/finch-daemon/pkg/ecc" "github.com/runfinch/finch-daemon/pkg/flog" "github.com/spf13/afero" @@ -65,7 +68,7 @@ func initializeConfig(options *DaemonOptions) (*config.Config, error) { conf.Debug = options.debug } if conf.Namespace == "" || conf.Namespace == namespaces.Default { - conf.Namespace = defaultNamespace + conf.Namespace = finchconfig.DefaultNamespace } return conf, nil @@ -99,6 +102,7 @@ func createRouterOptions( ncWrapper *backend.NerdctlWrapper, logger *flog.Logrus, regoFilePath string, + credService *credential.CredentialService, ) *router.Options { fs := afero.NewOsFs() tarCreator := archive.NewTarCreator(ecc.NewExecCmdCreator(), logger) @@ -110,12 +114,13 @@ func createRouterOptions( ImageService: image.NewService(clientWrapper, ncWrapper, logger), NetworkService: network.NewService(clientWrapper, ncWrapper, logger), SystemService: system.NewService(clientWrapper, ncWrapper, logger), - BuilderService: builder.NewService(clientWrapper, ncWrapper, logger, tarExtractor), + BuilderService: builder.NewService(clientWrapper, ncWrapper, logger, tarExtractor, credService), VolumeService: volume.NewService(ncWrapper, logger), ExecService: exec.NewService(clientWrapper, logger), DistributionService: distribution.NewService(clientWrapper, ncWrapper, logger), NerdctlWrapper: ncWrapper, RegoFilePath: regoFilePath, + CredentialService: credService, } } diff --git a/cmd/finch-daemon/router_utils_test.go b/cmd/finch-daemon/router_utils_test.go index b52f9069..b7fe8f8f 100644 --- a/cmd/finch-daemon/router_utils_test.go +++ b/cmd/finch-daemon/router_utils_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/containerd/nerdctl/v2/pkg/config" + finchconfig "github.com/runfinch/finch-daemon/pkg/config" "github.com/runfinch/finch-daemon/pkg/flog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,7 +23,7 @@ func TestInitializeConfig(t *testing.T) { require.NoError(t, err, "Initialization should succeed.") assert.True(t, cfg.Debug, "Debug mode should be enabled.") - assert.Equal(t, "finch", defaultNamespace, "check default namespace") + assert.Equal(t, "finch", finchconfig.DefaultNamespace, "check default namespace") } func TestHandleConfigOptions_FileNotFound(t *testing.T) { diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index b3918bed..25e36a5c 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -14,13 +14,13 @@ import ( "github.com/onsi/gomega" "github.com/runfinch/common-tests/command" "github.com/runfinch/common-tests/option" + "github.com/runfinch/finch-daemon/pkg/config" "github.com/runfinch/finch-daemon/e2e/tests" "github.com/runfinch/finch-daemon/e2e/util" ) const ( - defaultNamespace = "finch" testE2EEnv = "TEST_E2E" middlewareE2EEnv = "MIDDLEWARE_E2E" opaTestDescription = "Finch Daemon OPA E2E Tests" @@ -45,7 +45,7 @@ func TestRun(t *testing.T) { } func createTestOption() (*option.Option, error) { - return option.New([]string{*Subject, "--namespace", defaultNamespace}) + return option.New([]string{*Subject, "--namespace", config.DefaultNamespace}) } func setupTestSuite(opt *option.Option) { @@ -100,6 +100,7 @@ func runE2ETests(t *testing.T) { runImageTests(opt) runSystemTests(opt) runDistributionTests(opt) + runCredentialTests(opt, pOpt) }) runTests(t, e2eTestDescription) @@ -173,6 +174,11 @@ func runDistributionTests(opt *option.Option) { tests.DistributionInspect(opt) } +// functional test for credential helper. +func runCredentialTests(opt *option.Option, pOpt func([]string, ...option.Modifier) (*option.Option, error)) { + tests.CredentialHelper(opt, pOpt) +} + // parseTestFlags parses go test flags because pflag package ignores flags with '-test.' prefix // Related issues: // https://github.com/spf13/pflag/issues/63 diff --git a/e2e/tests/build_credential_management.go b/e2e/tests/build_credential_management.go new file mode 100644 index 00000000..63c2e61c --- /dev/null +++ b/e2e/tests/build_credential_management.go @@ -0,0 +1,369 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package tests + +import ( + b64 "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "time" + + dockertypes "github.com/docker/cli/cli/config/types" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/runfinch/common-tests/command" + "github.com/runfinch/common-tests/ffs" + "github.com/runfinch/common-tests/fnet" + "github.com/runfinch/common-tests/option" + + "github.com/runfinch/finch-daemon/e2e/client" + "github.com/runfinch/finch-daemon/pkg/archive" + "github.com/runfinch/finch-daemon/pkg/ecc" + "github.com/runfinch/finch-daemon/pkg/flog" +) + +func CredentialHelper(opt *option.Option, pOpt func([]string, ...option.Modifier) (*option.Option, error)) { + Describe("credential helper with authenticated registry", func() { + var ( + uClient *http.Client + version string + registry string + authImageTag string + buildContext string + testUser = "testUser" + testPassword = "testPassword" + registryImage = "public.ecr.aws/docker/library/registry:latest" + defaultImage = "public.ecr.aws/docker/library/alpine:latest" + ) + + BeforeEach(func() { + command.RemoveAll(opt) + uClient = client.NewClient(GetDockerHostUrl()) + version = GetDockerApiVersion() + }) + + BeforeEach(func() { + filename := "htpasswd" + // The htpasswd is generated by + // ` run --entrypoint htpasswd public.ecr.aws/docker/library/httpd:2 -Bbn testUser testPassword`. + htpasswd := "testUser:$2y$05$wE0sj3r9O9K9q7R0MXcfPuIerl/06L1IsxXkCuUr3QZ8lHWwicIdS" + htpasswdDir := filepath.Dir(ffs.CreateTempFile(filename, htpasswd)) + DeferCleanup(os.RemoveAll, htpasswdDir) + + // Set up authenticated registry + port := fnet.GetFreePort() + command.Run(opt, "run", + "-dp", fmt.Sprintf("%d:5000", port), + "--name", "registry", + "-v", fmt.Sprintf("%s:/auth", htpasswdDir), + "-e", "REGISTRY_AUTH=htpasswd", + "-e", "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm", + "-e", fmt.Sprintf("REGISTRY_AUTH_HTPASSWD_PATH=/auth/%s", filename), + registryImage) + registry = fmt.Sprintf(`localhost:%d`, port) + + command.Run(opt, "pull", defaultImage) + + authImageTag = fmt.Sprintf(`%s/test-login:tag`, registry) + buildContext = ffs.CreateBuildContext(fmt.Sprintf(`FROM %s + CMD ["echo", "credential-test"] + `, defaultImage)) + DeferCleanup(os.RemoveAll, buildContext) + + command.Run(opt, "build", "-t", authImageTag, buildContext) + + command.New(opt, "login", registry, "-u", testUser, "--password-stdin"). + WithStdin(gbytes.BufferWithBytes([]byte(testPassword))).Run() + DeferCleanup(func() { + command.Run(opt, "logout", registry) + }) + command.Run(opt, "push", authImageTag) + command.Run(opt, "rmi", authImageTag) + }) + + AfterEach(func() { + command.RemoveAll(opt) + }) + + It("should successfully build with registry credentials via API", func() { + buildFromAuthImage := ffs.CreateBuildContext(fmt.Sprintf(`FROM %s + CMD ["echo", "build-with-auth-successful"] + `, authImageTag)) + DeferCleanup(os.RemoveAll, buildFromAuthImage) + + tarReader, err := createTarFromBuildContext(buildFromAuthImage) + Expect(err).Should(BeNil()) + + authConfig := map[string]dockertypes.AuthConfig{ + registry: { + Username: testUser, + Password: testPassword, + }, + } + + authJSON, err := json.Marshal(authConfig) + Expect(err).Should(BeNil()) + authHeader := b64.StdEncoding.EncodeToString(authJSON) + + resultTag := "result-image:latest" + relativeUrl := client.ConvertToFinchUrl(version, "/build") + req, err := http.NewRequest(http.MethodPost, relativeUrl, tarReader) + Expect(err).Should(BeNil()) + + req.Header.Set("Content-Type", "application/x-tar") + req.Header.Set("X-Registry-Config", authHeader) + + q := req.URL.Query() + q.Add("t", resultTag) + req.URL.RawQuery = q.Encode() + + res, err := uClient.Do(req) + Expect(err).Should(BeNil()) + defer res.Body.Close() + + respBody, err := io.ReadAll(res.Body) + Expect(err).Should(BeNil()) + + Expect(res).To(HaveHTTPStatus(http.StatusOK)) + Expect(string(respBody)).ShouldNot(ContainSubstring("error")) + + imageShouldExist(opt, resultTag) + }) + + It("should fail to build without registry credentials", func() { + buildFromAuthImage := ffs.CreateBuildContext(fmt.Sprintf(`FROM %s + CMD ["echo", "build-without-auth-should-fail"] + `, authImageTag)) + DeferCleanup(os.RemoveAll, buildFromAuthImage) + + tarReader, err := createTarFromBuildContext(buildFromAuthImage) + Expect(err).Should(BeNil()) + + resultTag := "result-image:latest" + relativeUrl := client.ConvertToFinchUrl(version, "/build") + req, err := http.NewRequest(http.MethodPost, relativeUrl, tarReader) + Expect(err).Should(BeNil()) + + req.Header.Set("Content-Type", "application/x-tar") + + q := req.URL.Query() + q.Add("t", resultTag) + req.URL.RawQuery = q.Encode() + + res, err := uClient.Do(req) + Expect(err).Should(BeNil()) + defer res.Body.Close() + + respBody, err := io.ReadAll(res.Body) + Expect(err).Should(BeNil()) + + Expect(res).To(HaveHTTPStatus(http.StatusInternalServerError)) + Expect(string(respBody)).Should(ContainSubstring("")) + + // Verify image was not built + imageShouldNotExist(opt, resultTag) + }) + + It("should prevent credential sharing between parallel builds", func() { + slowBuildContext := ffs.CreateBuildContext(fmt.Sprintf(`FROM %s + RUN sleep 10 + CMD ["echo", "slow-build-with-auth-successful"] + `, authImageTag)) + DeferCleanup(os.RemoveAll, slowBuildContext) + + fastBuildContext := ffs.CreateBuildContext(fmt.Sprintf(`FROM %s + CMD ["echo", "quick-build-without-auth"] + `, authImageTag)) + DeferCleanup(os.RemoveAll, fastBuildContext) + + slowTarReader, err := createTarFromBuildContext(slowBuildContext) + Expect(err).Should(BeNil()) + quickTarReader, err := createTarFromBuildContext(fastBuildContext) + Expect(err).Should(BeNil()) + + authConfig := map[string]dockertypes.AuthConfig{ + registry: { + Username: testUser, + Password: testPassword, + }, + } + + authJSON, err := json.Marshal(authConfig) + Expect(err).Should(BeNil()) + authHeader := b64.StdEncoding.EncodeToString(authJSON) + + slowResultTag := "result-image-1:latest" + slowUrl := client.ConvertToFinchUrl(version, "/build") + slowReq, err := http.NewRequest(http.MethodPost, slowUrl, slowTarReader) + Expect(err).Should(BeNil()) + + slowReq.Header.Set("Content-Type", "application/x-tar") + slowReq.Header.Set("X-Registry-Config", authHeader) + + slowQ := slowReq.URL.Query() + slowQ.Add("t", slowResultTag) + slowReq.URL.RawQuery = slowQ.Encode() + + quickResultTag := "result-image-2:latest" + quickUrl := client.ConvertToFinchUrl(version, "/build") + quickReq, err := http.NewRequest(http.MethodPost, quickUrl, quickTarReader) + Expect(err).Should(BeNil()) + + quickReq.Header.Set("Content-Type", "application/x-tar") + + quickQ := quickReq.URL.Query() + quickQ.Add("t", quickResultTag) + quickReq.URL.RawQuery = quickQ.Encode() + + // Start the slow build (with auth) in a goroutine to have creds in store. + slowDone := make(chan struct{}) + var slowRes *http.Response + var slowRespBody []byte + var slowErr error + + go func() { + defer close(slowDone) + slowRes, slowErr = uClient.Do(slowReq) + if slowErr == nil { + defer slowRes.Body.Close() + slowRespBody, _ = io.ReadAll(slowRes.Body) + } + }() + + time.Sleep(1 * time.Second) + + quickRes, err := uClient.Do(quickReq) + Expect(err).Should(BeNil()) + defer quickRes.Body.Close() + + quickRespBody, err := io.ReadAll(quickRes.Body) + Expect(err).Should(BeNil()) + + Expect(quickRes).To(HaveHTTPStatus(http.StatusInternalServerError)) + Expect(string(quickRespBody)).Should(ContainSubstring("")) + imageShouldNotExist(opt, quickResultTag) + + <-slowDone + Expect(slowErr).Should(BeNil()) + Expect(slowRes).To(HaveHTTPStatus(http.StatusOK)) + Expect(string(slowRespBody)).ShouldNot(ContainSubstring("error")) + + imageShouldExist(opt, slowResultTag) + }) + + It("should return unauthorized error when requesting credentials from another parent process", func() { + // Create a slow build that will run for a few seconds + slowBuildContext := ffs.CreateBuildContext(fmt.Sprintf(`FROM %s + RUN sleep 5 + CMD ["echo", "slow-build-for-credential-test"] + `, defaultImage)) + DeferCleanup(os.RemoveAll, slowBuildContext) + + tarReader, err := createTarFromBuildContext(slowBuildContext) + Expect(err).Should(BeNil()) + + authConfig := map[string]dockertypes.AuthConfig{ + registry: { + Username: testUser, + Password: testPassword, + }, + } + + authJSON, err := json.Marshal(authConfig) + Expect(err).Should(BeNil()) + authHeader := b64.StdEncoding.EncodeToString(authJSON) + + slowResultTag := "result-image:latest" + slowUrl := client.ConvertToFinchUrl(version, "/build") + slowReq, err := http.NewRequest(http.MethodPost, slowUrl, tarReader) + Expect(err).Should(BeNil()) + + slowReq.Header.Set("Content-Type", "application/x-tar") + slowReq.Header.Set("X-Registry-Config", authHeader) + + slowQ := slowReq.URL.Query() + slowQ.Add("t", slowResultTag) + slowReq.URL.RawQuery = slowQ.Encode() + + go func() { + uClient.Timeout = 5 * time.Second + client, _ := uClient.Do(slowReq) + if client != nil { + defer client.Body.Close() + } + }() + + time.Sleep(1 * time.Second) + getCreds, _ := pOpt([]string{"sh", "-c", "echo \"serverurl\" | docker-credential-finch get"}) + getCreds.UpdateEnv("FINCH_BUILD_ID", "test") + out := command.RunWithoutSuccessfulExit(getCreds).Out + Expect(string(out.Contents())).Should(ContainSubstring("credentials not found in native keychain")) + }) + + It("should successfully build from a public registry image without authentication", func() { + // Create a build context using a public registry image + publicBuildContext := ffs.CreateBuildContext(fmt.Sprintf(`FROM %s + CMD ["echo", "public-registry-image-build-successful"] + `, defaultImage)) + DeferCleanup(os.RemoveAll, publicBuildContext) + + tarReader, err := createTarFromBuildContext(publicBuildContext) + Expect(err).Should(BeNil()) + + resultTag := "public-result-image:latest" + relativeUrl := client.ConvertToFinchUrl(version, "/build") + req, err := http.NewRequest(http.MethodPost, relativeUrl, tarReader) + Expect(err).Should(BeNil()) + + req.Header.Set("Content-Type", "application/x-tar") + + q := req.URL.Query() + q.Add("t", resultTag) + req.URL.RawQuery = q.Encode() + + res, err := uClient.Do(req) + Expect(err).Should(BeNil()) + defer res.Body.Close() + + respBody, err := io.ReadAll(res.Body) + Expect(err).Should(BeNil()) + + Expect(res).To(HaveHTTPStatus(http.StatusOK)) + Expect(string(respBody)).ShouldNot(ContainSubstring("error")) + + // Verify the image was built successfully + imageShouldExist(opt, resultTag) + }) + }) +} + +// createTarFromBuildContext creates a tar archive from the build context directory. +func createTarFromBuildContext(buildContextPath string) (io.Reader, error) { + logger := flog.NewLogrus() + eccCreator := ecc.NewExecCmdCreator() + tarCreator := archive.NewTarCreator(eccCreator, logger) + + cmd, err := tarCreator.CreateTarCommand(buildContextPath, true) + if err != nil { + return nil, err + } + + pr, pw := io.Pipe() + cmd.SetStdout(pw) + + go func() { + err := cmd.Run() + if err != nil { + logger.Errorf("Error running tar command: %v", err) + } + pw.Close() + }() + + return pr, nil +} diff --git a/e2e/tests/container_create.go b/e2e/tests/container_create.go index 2d93d107..bad1f4b5 100644 --- a/e2e/tests/container_create.go +++ b/e2e/tests/container_create.go @@ -210,6 +210,44 @@ func ContainerCreate(opt *option.Option, pOpt util.NewOpt) { Expect(portMap[udpPort][0]).Should(Equal(udpPortBinding)) }) + It("should create a container with automatic port allocation on the host", func() { + ctrPort := "8080" + + // define options with empty host port for automatic allocation + tcpPort := nat.Port(fmt.Sprintf("%s/tcp", ctrPort)) + tcpPortBinding := nat.PortBinding{HostIP: "0.0.0.0", HostPort: ""} + + options.Cmd = []string{"sleep", "Infinity"} + options.HostConfig.PortBindings = nat.PortMap{ + tcpPort: []nat.PortBinding{tcpPortBinding}, + } + + // create and start container + statusCode, ctr := createContainer(uClient, url, testContainerName, options) + Expect(statusCode).Should(Equal(http.StatusCreated)) + Expect(ctr.ID).ShouldNot(BeEmpty()) + command.Run(opt, "start", testContainerName) + + // inspect container + resp := command.Stdout(opt, "inspect", testContainerName) + var inspect []*dockercompat.Container + err := json.Unmarshal(resp, &inspect) + Expect(err).Should(BeNil()) + Expect(inspect).Should(HaveLen(1)) + + // verify port mappings with automatic allocation + Expect(inspect[0].NetworkSettings).ShouldNot(BeNil()) + portMap := *inspect[0].NetworkSettings.Ports + Expect(portMap).Should(HaveLen(1)) + Expect(portMap[tcpPort]).Should(HaveLen(1)) + Expect(portMap[tcpPort][0].HostIP).Should(Equal("0.0.0.0")) + Expect(portMap[tcpPort][0].HostPort).ShouldNot(BeEmpty()) + // Verify that a port was actually allocated (not empty string) + port, err := strconv.Atoi(portMap[tcpPort][0].HostPort) + Expect(err).Should(BeNil()) + Expect(port).Should(BeNumerically(">", 0)) + }) + // Volume Mounts It("should create a container with a directory mounted from the host", func() { diff --git a/go.mod b/go.mod index cfa37d2d..9dc4adf2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.8 require ( github.com/containerd/cgroups/v3 v3.0.5 github.com/containerd/containerd/api v1.9.0 - github.com/containerd/containerd/v2 v2.1.1 + github.com/containerd/containerd/v2 v2.1.3 github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 github.com/containerd/go-cni v1.1.12 @@ -17,8 +17,8 @@ require ( github.com/coreos/go-iptables v0.8.0 github.com/coreos/go-systemd/v22 v22.5.0 github.com/distribution/reference v0.6.0 - github.com/docker/cli v28.2.2+incompatible - github.com/docker/docker v28.2.2+incompatible + github.com/docker/cli v28.3.2+incompatible + github.com/docker/docker v28.3.3+incompatible github.com/docker/go-connections v0.5.0 github.com/docker/go-units v0.5.0 github.com/getlantern/httptest v0.0.0-20161025015934-4b40f4c7e590 @@ -26,7 +26,7 @@ require ( github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 github.com/moby/go-archive v0.1.0 - github.com/moby/moby v28.1.1+incompatible + github.com/moby/moby v28.3.2+incompatible github.com/moby/sys/user v0.4.0 github.com/onsi/ginkgo/v2 v2.23.4 github.com/onsi/gomega v1.37.0 @@ -36,6 +36,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 github.com/pkg/errors v0.9.1 github.com/runfinch/common-tests v0.9.4 + github.com/shirou/gopsutil/v3 v3.24.5 github.com/sirupsen/logrus v1.9.3 github.com/spf13/afero v1.14.0 github.com/spf13/cobra v1.9.1 @@ -43,8 +44,8 @@ require ( github.com/vishvananda/netlink v1.3.1 github.com/vishvananda/netns v0.0.5 go.uber.org/mock v0.5.2 - golang.org/x/net v0.40.0 - golang.org/x/sys v0.33.0 + golang.org/x/net v0.42.0 + golang.org/x/sys v0.34.0 google.golang.org/protobuf v1.36.6 ) @@ -53,36 +54,46 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-ini/ini v1.67.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect + github.com/moby/sys/capability v0.4.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_golang v1.22.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect - github.com/tchap/go-patricia/v2 v2.3.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.65.0 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/tchap/go-patricia/v2 v2.3.3 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect + github.com/vektah/gqlparser/v2 v2.5.30 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/sdk v1.35.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + sigs.k8s.io/yaml v1.5.0 // indirect ) require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/hcsshim v0.13.0 // indirect - github.com/cilium/ebpf v0.16.0 // indirect + github.com/cilium/ebpf v0.19.0 // indirect github.com/containerd/accelerated-container-image v1.3.0 // indirect github.com/containerd/console v1.0.5 // indirect github.com/containerd/continuity v0.4.5 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/go-runc v1.1.0 // indirect github.com/containerd/imgcrypt/v2 v2.0.1 // indirect - github.com/containerd/nydus-snapshotter v0.15.1 // indirect + github.com/containerd/nydus-snapshotter v0.15.2 // indirect github.com/containerd/plugin v1.0.0 // indirect github.com/containerd/stargz-snapshotter v0.16.3 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect @@ -93,27 +104,27 @@ require ( github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/djherbis/times v1.6.0 // indirect - github.com/docker/docker-credential-helpers v0.8.2 // indirect + github.com/docker/docker-credential-helpers v0.9.3 github.com/fahedouch/go-logrotate v0.3.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fluent/fluent-logger-golang v1.10.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/getlantern/mockconn v0.0.0-20200818071412-cb30d065a848 // indirect - github.com/go-jose/go-jose/v4 v4.0.5 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.1 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.3.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect + github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ipfs/go-cid v0.5.0 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/klauspost/cpuid/v2 v2.2.11 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect @@ -133,45 +144,44 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect - github.com/multiformats/go-multiaddr v0.13.0 // indirect + github.com/multiformats/go-multiaddr v0.16.0 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.0.7 // indirect - github.com/open-policy-agent/opa v1.4.0 - github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 // indirect + github.com/open-policy-agent/opa v1.6.0 + github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 // indirect github.com/opencontainers/selinux v1.12.0 // indirect - github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect - github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb // indirect + github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rootless-containers/bypass4netns v0.4.2 // indirect github.com/rootless-containers/rootlesskit/v2 v2.3.5 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect - github.com/smallstep/pkcs7 v0.1.1 // indirect + github.com/smallstep/pkcs7 v0.2.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect - github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect github.com/tinylib/msgp v1.3.0 // indirect - github.com/vbatts/tar-split v0.11.6 // indirect + github.com/vbatts/tar-split v0.12.1 // indirect github.com/yuchanns/srslog v1.1.0 // indirect go.opencensus.io v0.24.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/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/sync v0.14.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.25.0 // indirect - golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.31.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect - google.golang.org/grpc v1.72.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/term v0.33.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect + google.golang.org/grpc v1.73.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - lukechampine.com/blake3 v1.3.0 // indirect + lukechampine.com/blake3 v1.4.1 // indirect tags.cncf.io/container-device-interface v1.0.1 // indirect tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 3aee63df..03b1be27 100644 --- a/go.sum +++ b/go.sum @@ -6,14 +6,16 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg6 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -22,13 +24,13 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= -github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= +github.com/cilium/ebpf v0.19.0 h1:Ro/rE64RmFBeA9FGjcTc+KmCeY6jXmryu6FfnzPRIao= +github.com/cilium/ebpf v0.19.0/go.mod h1:fLCgMo3l8tZmAdM3B2XqdFzXBpwkcSTroaVqN08OWVY= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/compose-spec/compose-go/v2 v2.6.3 h1:zfW1Qp605ESySyth/zR+6yLr55XE0AiOAUlZLHKMoW0= @@ -41,8 +43,8 @@ github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/q github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= -github.com/containerd/containerd/v2 v2.1.1 h1:znnkm7Ajz8lg8BcIPMhc/9yjBRN3B+OkNKqKisKfwwM= -github.com/containerd/containerd/v2 v2.1.1/go.mod h1:zIfkQj4RIodclYQkX7GSSswSwgP8d/XxDOtOAoSDIGU= +github.com/containerd/containerd/v2 v2.1.3 h1:eMD2SLcIQPdMlnlNF6fatlrlRLAeDaiGPGwmRKLZKNs= +github.com/containerd/containerd/v2 v2.1.3/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -61,8 +63,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/nerdctl/v2 v2.1.2 h1:ykM+PtsTUWplav2vmf/YxyMwiAdEsaLM0FgkM4d95+c= github.com/containerd/nerdctl/v2 v2.1.2/go.mod h1:yMsrdkIWe73vaknK5vOPLHxo57Up1w48061VwxAde8s= -github.com/containerd/nydus-snapshotter v0.15.1 h1:huPj2d8J1BEx6mjm6h72BCo1kY5lTrfatnnujzpu6BA= -github.com/containerd/nydus-snapshotter v0.15.1/go.mod h1:FfwH2KBkNYoisK/e+KsmNr7xTU53DmnavQHMFOcXwfM= +github.com/containerd/nydus-snapshotter v0.15.2 h1:qsHI4M+Wwrf6Jr4eBqhNx8qh+YU0dSiJ+WPmcLFWNcg= +github.com/containerd/nydus-snapshotter v0.15.2/go.mod h1:FfwH2KBkNYoisK/e+KsmNr7xTU53DmnavQHMFOcXwfM= github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= @@ -106,12 +108,12 @@ 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/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= -github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A= -github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= -github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= -github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/docker/cli v28.3.2+incompatible h1:mOt9fcLE7zaACbxW1GeS65RI67wIJrTnqS3hP2huFsY= +github.com/docker/cli v28.3.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= 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= @@ -140,15 +142,18 @@ github.com/getlantern/mockconn v0.0.0-20200818071412-cb30d065a848 h1:2MhMMVBTnaH github.com/getlantern/mockconn v0.0.0-20200818071412-cb30d065a848/go.mod h1:+F5GJ7qGpQ03DBtcOEyQpM30ix4BLswdaojecFtsdy8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= 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-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= -github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= +github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= @@ -164,8 +169,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -186,23 +191,20 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -219,12 +221,14 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= -github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= +github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= +github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= @@ -239,19 +243,20 @@ github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dz github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mndrix/tap-go v0.0.0-20171203230836-629fa407e90b/go.mod h1:pzzDgJWZ34fGzaAZGFW22KVZDfyrYW+QABMrWnJBnSs= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/moby v28.1.1+incompatible h1:lyEaGTiUhIdXRUv/vPamckAbPt5LcPQkeHmwAHN98eQ= -github.com/moby/moby v28.1.1+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= +github.com/moby/moby v28.3.2+incompatible h1:K0SaQiU3VJxzMmHarwIa9MUyYFYC6FzCf0Qs9oQaFI4= +github.com/moby/moby v28.3.2+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= +github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/moby/sys/mount v0.3.4 h1:yn5jq4STPztkkzSKpZkLcmjue+bZJ0u2AuQY1iNI1Ww= github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= @@ -272,15 +277,14 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= -github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ= -github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII= +github.com/multiformats/go-multiaddr v0.16.0 h1:oGWEVKioVQcdIOBlYM8BH1rZDWOGJSqr9/BKl6zQ4qc= +github.com/multiformats/go-multiaddr v0.16.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= @@ -293,44 +297,45 @@ github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= -github.com/open-policy-agent/opa v1.4.0 h1:IGO3xt5HhQKQq2axfa9memIFx5lCyaBlG+fXcgHpd3A= -github.com/open-policy-agent/opa v1.4.0/go.mod h1:DNzZPKqKh4U0n0ANxcCVlw8lCSv2c+h5G/3QvSYdWZ8= +github.com/open-policy-agent/opa v1.6.0 h1:/S/cnNQJ2MUMNzizHPbisTWBHowmLkPrugY5jjkPlRQ= +github.com/open-policy-agent/opa v1.6.0/go.mod h1:zFmw4P+W62+CWGYRDDswfVYSCnPo6oYaktQnfIaRFC4= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opencontainers/runtime-spec v1.0.3-0.20220825212826-86290f6a00fb/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 h1:DmNGcqH3WDbV5k8OJ+esPWbqUOX5rMLR2PMvziDMJi0= -github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626/go.mod h1:BRHJJd0E+cx42OybVYSgUvZmU0B8P9gZuRXlZUP7TKI= -github.com/opencontainers/selinux v1.9.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 h1:2xZEHOdeQBV6PW8ZtimN863bIOl7OCW/X10K0cnxKeA= +github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2/go.mod h1:MXdPzqAA8pHC58USHqNCSjyLnRQ6D+NjbpP+02Z1U/0= github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8= github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= -github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= -github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb h1:3PrKuO92dUTMrQ9dx0YNejC6U/Si6jqKmyQ9vWjwqR4= +github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= -github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 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/rootless-containers/bypass4netns v0.4.2 h1:JUZcpX7VLRfDkLxBPC6fyNalJGv9MjnjECOilZIvKRc= @@ -342,11 +347,18 @@ github.com/runfinch/common-tests v0.9.4/go.mod h1:25UdRwKrGWnIzKHvceDaMpV3rz+41a github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU= -github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= +github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= +github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= @@ -362,28 +374,28 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= -github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= +github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= -github.com/urfave/cli v1.19.1/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= -github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= +github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= +github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -399,36 +411,42 @@ github.com/yuchanns/srslog v1.1.0/go.mod h1:HsLjdv3XV02C3kgBW2bTyW6i88OQE+VYJZIx github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 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/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= -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/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= +go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -436,12 +454,12 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -452,8 +470,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -470,8 +488,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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= @@ -483,16 +501,16 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -507,9 +525,9 @@ golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -518,9 +536,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -529,11 +547,11 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +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.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -546,8 +564,8 @@ 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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +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= @@ -558,17 +576,17 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= -google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -593,10 +611,10 @@ 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= -lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= -lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= +sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= +sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc= tags.cncf.io/container-device-interface v1.0.1/go.mod h1:JojJIOeW3hNbcnOH2q0NrWNha/JuHoDZcmYxAZwb2i0= tags.cncf.io/container-device-interface/specs-go v1.0.0 h1:8gLw29hH1ZQP9K1YtAzpvkHCjjyIxHZYzBAvlQ+0vD8= diff --git a/internal/backend/builder.go b/internal/backend/builder.go index f1213844..6cc631aa 100644 --- a/internal/backend/builder.go +++ b/internal/backend/builder.go @@ -5,22 +5,624 @@ package backend import ( "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/core/images/archive" + "github.com/containerd/errdefs" + "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/buildkitutil" - "github.com/containerd/nerdctl/v2/pkg/cmd/builder" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/containerutil" + "github.com/containerd/nerdctl/v2/pkg/platformutil" + "github.com/containerd/nerdctl/v2/pkg/referenceutil" + "github.com/containerd/nerdctl/v2/pkg/strutil" + "github.com/containerd/platforms" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/runfinch/finch-daemon/pkg/config" ) //go:generate mockgen --destination=../../mocks/mocks_backend/nerdctlbuildersvc.go -package=mocks_backend github.com/runfinch/finch-daemon/internal/backend NerdctlBuilderSvc type NerdctlBuilderSvc interface { - Build(ctx context.Context, client ContainerdClient, options types.BuilderBuildOptions) error + Build(ctx context.Context, client ContainerdClient, options types.BuilderBuildOptions, buildID string) error GetBuildkitHost() (string, error) } -func (*NerdctlWrapper) Build(ctx context.Context, client ContainerdClient, options types.BuilderBuildOptions) error { - return builder.Build(ctx, client.GetClient(), options) +func (w *NerdctlWrapper) Build(ctx context.Context, client ContainerdClient, options types.BuilderBuildOptions, buildID string) error { + return w.RunBuild(ctx, client.GetClient(), options, buildID) } func (w *NerdctlWrapper) GetBuildkitHost() (string, error) { return buildkitutil.GetBuildkitHost(w.globalOptions.Namespace) } + +type PlatformParser interface { + Parse(platform string) (platforms.Platform, error) + DefaultSpec() platforms.Platform +} + +type platformParser struct{} + +func (p platformParser) Parse(platform string) (platforms.Platform, error) { + return platforms.Parse(platform) +} + +func (p platformParser) DefaultSpec() platforms.Platform { + return platforms.DefaultSpec() +} + +// Following functions are copied from https://github.com/containerd/nerdctl/blob/ff9323859a8d7892d8d72380a17b99395ef9a516/pkg/cmd/builder/build.go. +// Copyright The Nerdctl Authors. +// Licensed under the Apache License, Version 2.0 +// NOTICE: https://github.com/containerd/nerdctl/blob/main/NOTICE + +// TODO: Make generateBuildctlArgs public in nerdctl library. +// source: https://github.com/containerd/nerdctl/blob/ff9323859a8d7892d8d72380a17b99395ef9a516/pkg/cmd/builder/build.go. +// +//nolint:stylecheck // adding a todo for exposing env in nerdctl library +func (w *NerdctlWrapper) RunBuild(ctx context.Context, client *containerd.Client, options types.BuilderBuildOptions, buildID string) error { + buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, err := generateBuildctlArgs(ctx, client, options) + if err != nil { + return err + } + if cleanup != nil { + defer cleanup() + } + + log.L.Debugf("running %s %v", buildctlBinary, buildctlArgs) + // #nosec G204 - buildctlBinary is validated by buildkitutil.BuildctlBinary() + buildctlCmd := exec.Command(buildctlBinary, buildctlArgs...) + buildctlCmd.Env = os.Environ() + buildctlCmd.Env = append(buildctlCmd.Env, fmt.Sprintf("FINCH_BUILD_ID=%s", buildID)) + buildctlCmd.Env = append(buildctlCmd.Env, fmt.Sprintf("FINCH_CREDENTIAL_SOCKET=%s",config.GetCredentialAddr())) + + var buildctlStdout io.Reader + if needsLoading { + buildctlStdout, err = buildctlCmd.StdoutPipe() + if err != nil { + return err + } + } else { + buildctlCmd.Stdout = options.Stdout + } + if !options.Quiet { + buildctlCmd.Stderr = options.Stderr + } + + if err := buildctlCmd.Start(); err != nil { + return err + } + + if needsLoading { + platMC, err := platformutil.NewMatchComparer(false, options.Platform) + if err != nil { + return err + } + if err = loadImage(ctx, buildctlStdout, options.GOptions.Namespace, options.GOptions.Address, options.GOptions.Snapshotter, options.Stdout, platMC, options.Quiet); err != nil { + return err + } + } + + if err = buildctlCmd.Wait(); err != nil { + return err + } + + if options.IidFile != "" { + id, err := getDigestFromMetaFile(metaFile) + if err != nil { + return err + } + if err := os.WriteFile(options.IidFile, []byte(id), 0644); err != nil { + return err + } + } + + if len(tags) > 1 { + log.L.Debug("Found more than 1 tag") + imageService := client.ImageService() + image, err := imageService.Get(ctx, tags[0]) + if err != nil { + return fmt.Errorf("unable to tag image: %w", err) + } + for _, targetRef := range tags[1:] { + image.Name = targetRef + if _, err := imageService.Create(ctx, image); err != nil { + // if already exists; skip. + if errors.Is(err, errdefs.ErrAlreadyExists) { + if err = imageService.Delete(ctx, targetRef, images.SynchronousDelete()); err != nil { + return err + } + if _, err = imageService.Create(ctx, image); err != nil { + return err + } + continue + } + return fmt.Errorf("unable to tag image: %w", err) + } + } + } + + return nil +} + +// TODO: This struct and `loadImage` are duplicated with the code in `cmd/load.go`, remove it after `load.go` has been refactor. +type readCounter struct { + io.Reader + N int +} + +func loadImage(ctx context.Context, in io.Reader, namespace, address, snapshotter string, output io.Writer, platMC platforms.MatchComparer, quiet bool) error { + // In addition to passing WithImagePlatform() to client.Import(), we also need to pass WithDefaultPlatform() to NewClient(). + // Otherwise unpacking may fail. + client, ctx, cancel, err := clientutil.NewClient(ctx, namespace, address, containerd.WithDefaultPlatform(platMC)) + if err != nil { + return err + } + defer func() { + cancel() + client.Close() + }() + r := &readCounter{Reader: in} + imgs, err := client.Import(ctx, r, containerd.WithDigestRef(archive.DigestTranslator(snapshotter)), containerd.WithSkipDigestRef(func(name string) bool { return name != "" }), containerd.WithImportPlatform(platMC)) + if err != nil { + if r.N == 0 { + // Avoid confusing "unrecognized image format" + return errors.New("no image was built") + } + if errors.Is(err, images.ErrEmptyWalk) { + err = fmt.Errorf("%w (Hint: set `--platform=PLATFORM` or `--all-platforms`)", err) + } + return err + } + for _, img := range imgs { + image := containerd.NewImageWithPlatform(client, img, platMC) + + // TODO: Show unpack status + if !quiet { + fmt.Fprintf(output, "unpacking %s (%s)...\n", img.Name, img.Target.Digest) + } + err = image.Unpack(ctx, snapshotter) + if err != nil { + return err + } + if quiet { + fmt.Fprintln(output, img.Target.Digest) + } else { + fmt.Fprintf(output, "Loaded image: %s\n", img.Name) + } + } + + return nil +} + +func generateBuildctlArgs(ctx context.Context, client *containerd.Client, options types.BuilderBuildOptions) (buildCtlBinary string, + buildctlArgs []string, needsLoading bool, metaFile string, tags []string, cleanup func(), err error) { + buildctlBinary, err := buildkitutil.BuildctlBinary() + if err != nil { + return "", nil, false, "", nil, nil, err + } + + output := options.Output + if output == "" { + info, err := client.Server(ctx) + if err != nil { + return "", nil, false, "", nil, nil, err + } + sharable, err := isImageSharable(options.BuildKitHost, options.GOptions.Namespace, info.UUID, options.GOptions.Snapshotter, options.Platform) + if err != nil { + return "", nil, false, "", nil, nil, err + } + if sharable { + output = "type=image,unpack=true" // ensure the target stage is unlazied (needed for any snapshotters) + } else { + output = "type=docker" + if len(options.Platform) > 1 { + // For avoiding `error: failed to solve: docker exporter does not currently support exporting manifest lists` + // TODO: consider using type=oci for single-options.Platform build too + output = "type=oci" + } + needsLoading = true + } + } else { + if !strings.Contains(output, "type=") { + // should accept --output as an alias of --output + // type=local,dest= + output = fmt.Sprintf("type=local,dest=%s", output) + } + if strings.Contains(output, "type=docker") || strings.Contains(output, "type=oci") { + if !strings.Contains(output, "dest=") { + needsLoading = true + } + } + } + if tags = strutil.DedupeStrSlice(options.Tag); len(tags) > 0 { + ref := tags[0] + parsedReference, err := referenceutil.Parse(ref) + if err != nil { + return "", nil, false, "", nil, nil, err + } + output += ",name=" + parsedReference.String() + + // pick the first tag and add it to output + for idx, tag := range tags { + parsedReference, err = referenceutil.Parse(tag) + if err != nil { + return "", nil, false, "", nil, nil, err + } + tags[idx] = parsedReference.String() + } + } else if len(tags) == 0 { + output = output + ",dangling-name-prefix=" + } + + buildctlArgs = buildkitutil.BuildctlBaseArgs(options.BuildKitHost) + + buildctlArgs = append(buildctlArgs, []string{ + "build", + "--progress=" + options.Progress, + "--frontend=dockerfile.v0", + "--local=context=" + options.BuildContext, + "--output=" + output, + }...) + + dir := options.BuildContext + file := buildkitutil.DefaultDockerfileName + if options.File != "" { + if options.File == "-" { + // Super Warning: this is a special trick to update the dir variable, Don't move this line!!!!!! + var err error + dir, err = buildkitutil.WriteTempDockerfile(options.Stdin) + if err != nil { + return "", nil, false, "", nil, nil, err + } + cleanup = func() { + os.RemoveAll(dir) + } + } else { + dir, file = filepath.Split(options.File) + } + + if dir == "" { + dir = "." + } + } + dir, file, err = buildkitutil.BuildKitFile(dir, file) + if err != nil { + return "", nil, false, "", nil, nil, err + } + + buildCtx, err := parseContextNames(options.ExtendedBuildContext) + if err != nil { + return "", nil, false, "", nil, nil, err + } + + for k, v := range buildCtx { + isURL := strings.HasPrefix(v, "https://") || strings.HasPrefix(v, "http://") + isDockerImage := strings.HasPrefix(v, "docker-image://") || strings.HasPrefix(v, "target:") + + if isURL || isDockerImage { + buildctlArgs = append(buildctlArgs, fmt.Sprintf("--opt=context:%s=%s", k, v)) + continue + } + + if isOCILayout := strings.HasPrefix(v, "oci-layout://"); isOCILayout { + args, err := parseBuildContextFromOCILayout(k, v) + if err != nil { + return "", nil, false, "", nil, nil, err + } + + buildctlArgs = append(buildctlArgs, args...) + continue + } + + path, err := filepath.Abs(v) + if err != nil { + return "", nil, false, "", nil, nil, err + } + buildctlArgs = append(buildctlArgs, fmt.Sprintf("--local=%s=%s", k, path)) + buildctlArgs = append(buildctlArgs, fmt.Sprintf("--opt=context:%s=local:%s", k, k)) + } + + buildctlArgs = append(buildctlArgs, "--local=dockerfile="+dir) + buildctlArgs = append(buildctlArgs, "--opt=filename="+file) + + if options.Target != "" { + buildctlArgs = append(buildctlArgs, "--opt=target="+options.Target) + } + + if len(options.Platform) > 0 { + buildctlArgs = append(buildctlArgs, "--opt=platform="+strings.Join(options.Platform, ",")) + } + + seenBuildArgs := make(map[string]struct{}) + for _, ba := range strutil.DedupeStrSlice(options.BuildArgs) { + arr := strings.Split(ba, "=") + seenBuildArgs[arr[0]] = struct{}{} + if len(arr) == 1 && len(arr[0]) > 0 { + // Avoid masking default build arg value from Dockerfile if environment variable is not set + // https://github.com/moby/moby/issues/24101 + val, ok := os.LookupEnv(arr[0]) + if ok { + buildctlArgs = append(buildctlArgs, fmt.Sprintf("--opt=build-arg:%s=%s", ba, val)) + } else { + log.L.Debugf("ignoring unset build arg %q", ba) + } + } else if len(arr) > 1 && len(arr[0]) > 0 { + buildctlArgs = append(buildctlArgs, "--opt=build-arg:"+ba) + + // Support `--build-arg BUILDKIT_INLINE_CACHE=1` for compatibility with `docker buildx build` + // https://github.com/docker/buildx/blob/v0.6.3/docs/reference/buildx_build.md#-export-build-cache-to-an-external-cache-destination---cache-to + if strings.HasPrefix(ba, "BUILDKIT_INLINE_CACHE=") { + bic := strings.TrimPrefix(ba, "BUILDKIT_INLINE_CACHE=") + bicParsed, err := strconv.ParseBool(bic) + if err == nil { + if bicParsed { + buildctlArgs = append(buildctlArgs, "--export-cache=type=inline") + } + } else { + log.L.WithError(err).Warnf("invalid BUILDKIT_INLINE_CACHE: %q", bic) + } + } + } else { + return "", nil, false, "", nil, nil, fmt.Errorf("invalid build arg %q", ba) + } + } + + // Propagate SOURCE_DATE_EPOCH from the client env + // https://github.com/docker/buildx/pull/1482 + if v := os.Getenv("SOURCE_DATE_EPOCH"); v != "" { + if _, ok := seenBuildArgs["SOURCE_DATE_EPOCH"]; !ok { + buildctlArgs = append(buildctlArgs, "--opt=build-arg:SOURCE_DATE_EPOCH="+v) + } + } + + for _, l := range strutil.DedupeStrSlice(options.Label) { + buildctlArgs = append(buildctlArgs, "--opt=label:"+l) + } + + if options.NoCache { + buildctlArgs = append(buildctlArgs, "--no-cache") + } + + if options.Pull != nil { + switch *options.Pull { + case true: + buildctlArgs = append(buildctlArgs, "--opt=image-resolve-mode=pull") + case false: + buildctlArgs = append(buildctlArgs, "--opt=image-resolve-mode=local") + } + } + + for _, s := range strutil.DedupeStrSlice(options.Secret) { + buildctlArgs = append(buildctlArgs, "--secret="+s) + } + + for _, s := range strutil.DedupeStrSlice(options.Allow) { + buildctlArgs = append(buildctlArgs, "--allow="+s) + } + + for _, s := range strutil.DedupeStrSlice(options.Attest) { + optAttestType, optAttestAttrs, _ := strings.Cut(s, ",") + if strings.HasPrefix(optAttestType, "type=") { + optAttestType := strings.TrimPrefix(optAttestType, "type=") + buildctlArgs = append(buildctlArgs, fmt.Sprintf("--opt=attest:%s=%s", optAttestType, optAttestAttrs)) + } else { + return "", nil, false, "", nil, nil, fmt.Errorf("attestation type not specified") + } + } + + for _, s := range strutil.DedupeStrSlice(options.SSH) { + buildctlArgs = append(buildctlArgs, "--ssh="+s) + } + + for _, s := range strutil.DedupeStrSlice(options.CacheFrom) { + if !strings.Contains(s, "type=") { + s = "type=registry,ref=" + s + } + buildctlArgs = append(buildctlArgs, "--import-cache="+s) + } + + for _, s := range strutil.DedupeStrSlice(options.CacheTo) { + if !strings.Contains(s, "type=") { + s = "type=registry,ref=" + s + } + buildctlArgs = append(buildctlArgs, "--export-cache="+s) + } + + if !options.Rm { + log.L.Warn("ignoring deprecated flag: '--rm=false'") + } + + if options.IidFile != "" { + file, err := os.CreateTemp("", "buildkit-meta-*") + if err != nil { + return "", nil, false, "", nil, cleanup, err + } + defer file.Close() + metaFile = file.Name() + buildctlArgs = append(buildctlArgs, "--metadata-file="+metaFile) + } + + if options.NetworkMode != "" { + switch options.NetworkMode { + case "none": + buildctlArgs = append(buildctlArgs, "--opt=force-network-mode="+options.NetworkMode) + case "host": + buildctlArgs = append(buildctlArgs, "--opt=force-network-mode="+options.NetworkMode, "--allow=network.host", "--allow=security.insecure") + case "", "default": + default: + log.L.Debugf("ignoring network build arg %s", options.NetworkMode) + } + } + + if len(options.ExtraHosts) > 0 { + extraHosts, err := containerutil.ParseExtraHosts(options.ExtraHosts, options.GOptions.HostGatewayIP, "=") + if err != nil { + return "", nil, false, "", nil, nil, err + } + buildctlArgs = append(buildctlArgs, "--opt=add-hosts="+strings.Join(extraHosts, ",")) + } + + return buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, nil +} + +func getDigestFromMetaFile(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + defer os.Remove(path) + + metadata := map[string]json.RawMessage{} + if err := json.Unmarshal(data, &metadata); err != nil { + log.L.WithError(err).Errorf("failed to unmarshal metadata file %s", path) + return "", err + } + digestRaw, ok := metadata["containerimage.digest"] + if !ok { + return "", errors.New("failed to find containerimage.digest in metadata file") + } + var digest string + if err := json.Unmarshal(digestRaw, &digest); err != nil { + log.L.WithError(err).Errorf("failed to unmarshal digset") + return "", err + } + return digest, nil +} + +func isMatchingRuntimePlatform(platform string, parser PlatformParser) bool { + p, err := parser.Parse(platform) + if err != nil { + return false + } + d := parser.DefaultSpec() + + if p.OS == d.OS && p.Architecture == d.Architecture && (p.Variant == "" || p.Variant == d.Variant) { + return true + } + + return false +} + +func isBuildPlatformDefault(platform []string, parser PlatformParser) bool { + if len(platform) == 0 { + return true + } else if len(platform) == 1 { + return isMatchingRuntimePlatform(platform[0], parser) + } + return false +} + +func isImageSharable(buildkitHost, namespace, uuid, snapshotter string, platform []string) (bool, error) { + labels, err := buildkitutil.GetWorkerLabels(buildkitHost) + if err != nil { + return false, err + } + log.L.Debugf("worker labels: %+v", labels) + executor, ok := labels["org.mobyproject.buildkit.worker.executor"] + if !ok { + return false, nil + } + containerdUUID, ok := labels["org.mobyproject.buildkit.worker.containerd.uuid"] + if !ok { + return false, nil + } + containerdNamespace, ok := labels["org.mobyproject.buildkit.worker.containerd.namespace"] + if !ok { + return false, nil + } + workerSnapshotter, ok := labels["org.mobyproject.buildkit.worker.snapshotter"] + if !ok { + return false, nil + } + // NOTE: It's possible that BuildKit doesn't download the base image of non-default platform (e.g. when the provided + // Dockerfile doesn't contain instructions require base images like RUN) even if `--output type=image,unpack=true` + // is passed to BuildKit. Thus, we need to use `type=docker` or `type=oci` when nerdctl builds non-default platform + // image using `platform` option. + parser := new(platformParser) + return executor == "containerd" && containerdUUID == uuid && containerdNamespace == namespace && workerSnapshotter == snapshotter && isBuildPlatformDefault(platform, parser), nil +} + +func parseContextNames(values []string) (map[string]string, error) { + if len(values) == 0 { + return nil, nil + } + result := make(map[string]string, len(values)) + for _, value := range values { + kv := strings.SplitN(value, "=", 2) + if len(kv) != 2 { + return nil, fmt.Errorf("invalid context value: %s, expected key=value", value) + } + result[kv[0]] = kv[1] + } + return result, nil +} + +var ( + ErrOCILayoutPrefixNotFound = errors.New("OCI layout prefix not found") + ErrOCILayoutEmptyDigest = errors.New("OCI layout cannot have empty digest") +) + +func parseBuildContextFromOCILayout(name, path string) ([]string, error) { + path, found := strings.CutPrefix(path, "oci-layout://") + if !found { + return []string{}, ErrOCILayoutPrefixNotFound + } + + abspath, err := filepath.Abs(path) + if err != nil { + return []string{}, err + } + + ociIndex, err := readOCIIndexFromPath(abspath) + if err != nil { + return []string{}, err + } + + var digest string + for _, manifest := range ociIndex.Manifests { + if images.IsManifestType(manifest.MediaType) { + digest = manifest.Digest.String() + } + } + + if digest == "" { + return []string{}, ErrOCILayoutEmptyDigest + } + + return []string{ + fmt.Sprintf("--oci-layout=parent-image-key=%s", abspath), + fmt.Sprintf("--opt=context:%s=oci-layout:parent-image-key@%s", name, digest), + }, nil +} + +func readOCIIndexFromPath(path string) (*ocispec.Index, error) { + ociIndexJSONFile, err := os.Open(filepath.Join(path, "index.json")) + if err != nil { + return nil, err + } + defer ociIndexJSONFile.Close() + + rawBytes, err := io.ReadAll(ociIndexJSONFile) + if err != nil { + return nil, err + } + + var ociIndex *ocispec.Index + err = json.Unmarshal(rawBytes, &ociIndex) + if err != nil { + return nil, err + } + return ociIndex, nil +} diff --git a/internal/backend/container.go b/internal/backend/container.go index 16d4c58c..2d066fea 100644 --- a/internal/backend/container.go +++ b/internal/backend/container.go @@ -68,12 +68,10 @@ func (w *NerdctlWrapper) CreateContainer(ctx context.Context, args []string, net func (w *NerdctlWrapper) InspectContainer(ctx context.Context, c containerd.Container, sizeFlag bool) (*dockercompat.Container, error) { var buf bytes.Buffer options := types.ContainerInspectOptions{ - Mode: "dockercompat", - Stdout: &buf, - Size: sizeFlag, - GOptions: types.GlobalCommandOptions{ - Snapshotter: w.globalOptions.Snapshotter, - }, + Mode: "dockercompat", + Stdout: &buf, + Size: sizeFlag, + GOptions: *w.globalOptions, } results, err := container.Inspect(ctx, w.clientWrapper.client, []string{c.ID()}, options) diff --git a/internal/service/builder/build.go b/internal/service/builder/build.go index d3c1630f..b336fc53 100644 --- a/internal/service/builder/build.go +++ b/internal/service/builder/build.go @@ -25,7 +25,7 @@ const shortLen = 12 var publishTagEventFunc = (*service).publishTagEvent // Build function builds an image using nerdctl function based on the BuilderBuildOptions. -func (s *service) Build(ctx context.Context, options *ncTypes.BuilderBuildOptions, tarBody io.ReadCloser) ([]types.BuildResult, error) { +func (s *service) Build(ctx context.Context, options *ncTypes.BuilderBuildOptions, tarBody io.ReadCloser, buildID string) ([]types.BuildResult, error) { tarCmd, err := s.tarExtractor.ExtractInTemp(tarBody, "build-context") if err != nil { s.logger.Warnf("Failed to extract build context. Error: %v", err) @@ -49,7 +49,7 @@ func (s *service) Build(ctx context.Context, options *ncTypes.BuilderBuildOption // update the build context and the docker file path with the temp dir. options.BuildContext = dir options.File = fmt.Sprintf("%s/%s", dir, options.File) - if err = s.nctlBuilderSvc.Build(ctx, s.client, *options); err != nil { + if err = s.nctlBuilderSvc.Build(ctx, s.client, *options, buildID); err != nil { return nil, err } diff --git a/internal/service/builder/build_test.go b/internal/service/builder/build_test.go index 5127b7b1..fa471759 100644 --- a/internal/service/builder/build_test.go +++ b/internal/service/builder/build_test.go @@ -12,9 +12,9 @@ import ( "os" "github.com/containerd/nerdctl/v2/pkg/api/types" - "go.uber.org/mock/gomock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" "github.com/runfinch/finch-daemon/api/events" "github.com/runfinch/finch-daemon/api/handlers/builder" @@ -59,21 +59,21 @@ var _ = Describe("Build API ", func() { mockCmd.EXPECT().GetDir().Return(fmt.Sprintf("%s/%s", os.TempDir(), cid)).AnyTimes() mockCmd.EXPECT().SetStderr(gomock.Any()).AnyTimes() - service = NewService(cdClient, mockNerdctlService{ncBuilderSvc, ncImgSvc}, logger, tarExtractor) + service = NewService(cdClient, mockNerdctlService{ncBuilderSvc, ncImgSvc}, logger, tarExtractor, nil) buildOption = types.BuilderBuildOptions{} req, _ = http.NewRequest(http.MethodPost, "/build", nil) }) Context("service", func() { It("should successfully build image", func() { // set up the mock - ncBuilderSvc.EXPECT().Build(gomock.Any(), gomock.Any(), gomock.Any()) + ncBuilderSvc.EXPECT().Build(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) tarExtractor.EXPECT().ExtractInTemp(gomock.Any(), gomock.Any()). Return(mockCmd, nil) tarExtractor.EXPECT().Cleanup(gomock.Any()) mockCmd.EXPECT().Run().Return(nil) // service should not return any error - _, err := service.Build(ctx, &buildOption, req.Body) + _, err := service.Build(ctx, &buildOption, req.Body, "test-build-id") Expect(err).Should(BeNil()) }) It("should fail building image due to temp folder creation failure", func() { @@ -82,7 +82,7 @@ var _ = Describe("Build API ", func() { tarExtractor.EXPECT().ExtractInTemp(gomock.Any(), gomock.Any()).Return(nil, mockErr) logger.EXPECT().Warnf("Failed to extract build context. Error: %v", mockErr) // service should return error - _, err := service.Build(ctx, &buildOption, req.Body) + _, err := service.Build(ctx, &buildOption, req.Body, "test-build-id") Expect(err).Should(Equal(mockErr)) }) It("should fail building image due to tar extraction failure", func() { @@ -93,7 +93,7 @@ var _ = Describe("Build API ", func() { logger.EXPECT().Warnf("Failed to extract build context in temp folder. Dir: %s, Error: %s, Stderr: %s", gomock.Any(), gomock.Any(), gomock.Any()) // service should return error - _, err := service.Build(ctx, &buildOption, req.Body) + _, err := service.Build(ctx, &buildOption, req.Body, "test-build-id") Expect(err).Should(Not(BeNil())) Expect(err.Error()).Should(Equal("failed to extract build context in temp folder")) }) @@ -105,18 +105,18 @@ var _ = Describe("Build API ", func() { logger.EXPECT().Warnf("Failed to extract build context in temp folder. Dir: %s, Error: %s, Stderr: %s", gomock.Any(), gomock.Any(), gomock.Any()) // service should return error - _, err := service.Build(ctx, &buildOption, req.Body) + _, err := service.Build(ctx, &buildOption, req.Body, "test-build-id") Expect(err.Error()).Should(Equal("failed to extract build context in temp folder")) }) It("should fail building image due build error from nerdctl", func() { // set up the mock mockCmd.EXPECT().Run().Return(nil) errExpected := fmt.Errorf("nerdctl error") - ncBuilderSvc.EXPECT().Build(gomock.Any(), gomock.Any(), gomock.Any()).Return(errExpected) + ncBuilderSvc.EXPECT().Build(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errExpected) tarExtractor.EXPECT().ExtractInTemp(gomock.Any(), gomock.Any()).Return(mockCmd, nil) tarExtractor.EXPECT().Cleanup(gomock.Any()) // service should return err - _, err := service.Build(ctx, &buildOption, req.Body) + _, err := service.Build(ctx, &buildOption, req.Body, "test-build-id") Expect(err).Should(Equal(errExpected)) }) It("should successfully tag image after build", func() { @@ -126,7 +126,7 @@ var _ = Describe("Build API ", func() { buildOption.Stdout = rr // set up mocks - ncBuilderSvc.EXPECT().Build(gomock.Any(), gomock.Any(), gomock.Any()) + ncBuilderSvc.EXPECT().Build(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) expectPublishTagEvent(mockCtrl, tag).Return(&events.Event{ID: imageId}, nil) tarExtractor.EXPECT().ExtractInTemp(gomock.Any(), gomock.Any()). Return(mockCmd, nil) @@ -134,7 +134,7 @@ var _ = Describe("Build API ", func() { mockCmd.EXPECT().Run().Return(nil) // service should not return any error - result, err := service.Build(ctx, &buildOption, req.Body) + result, err := service.Build(ctx, &buildOption, req.Body, "test-build-id") Expect(err).Should(BeNil()) Expect(result).Should(HaveLen(1)) Expect(result[0].ID).Should(Equal(imageId)) diff --git a/internal/service/builder/builder.go b/internal/service/builder/builder.go index 7db86f88..724c134a 100644 --- a/internal/service/builder/builder.go +++ b/internal/service/builder/builder.go @@ -8,6 +8,7 @@ import ( "github.com/runfinch/finch-daemon/api/handlers/builder" "github.com/runfinch/finch-daemon/internal/backend" "github.com/runfinch/finch-daemon/pkg/archive" + "github.com/runfinch/finch-daemon/pkg/credential" "github.com/runfinch/finch-daemon/pkg/flog" ) @@ -21,6 +22,7 @@ type service struct { nctlBuilderSvc NerdctlService logger flog.Logger tarExtractor archive.TarExtractor + credentialSvc *credential.CredentialService } // NewService creates a service struct for build APIs. @@ -29,11 +31,13 @@ func NewService( ncBuilderSvc NerdctlService, logger flog.Logger, tarExtractor archive.TarExtractor, + credService *credential.CredentialService, ) builder.Service { return &service{ client: client, nctlBuilderSvc: ncBuilderSvc, logger: logger, tarExtractor: tarExtractor, + credentialSvc: credService, } } diff --git a/internal/service/builder/builder_test.go b/internal/service/builder/builder_test.go index efe48fb7..5082ef6f 100644 --- a/internal/service/builder/builder_test.go +++ b/internal/service/builder/builder_test.go @@ -4,11 +4,14 @@ package builder import ( + "context" "testing" + "github.com/containerd/nerdctl/v2/pkg/api/types" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/runfinch/finch-daemon/internal/backend" "github.com/runfinch/finch-daemon/mocks/mocks_backend" ) @@ -17,6 +20,11 @@ type mockNerdctlService struct { *mocks_backend.MockNerdctlImageSvc } +// Build implements the Build method from NerdctlService interface with the buildID parameter. +func (m mockNerdctlService) Build(ctx context.Context, client backend.ContainerdClient, options types.BuilderBuildOptions, buildID string) error { + return m.MockNerdctlBuilderSvc.Build(ctx, client, options, buildID) +} + // TestContainerHandler function is the entry point of container service package's unit test using ginkgo. func TestContainerService(t *testing.T) { RegisterFailHandler(Fail) diff --git a/internal/service/container/inspect.go b/internal/service/container/inspect.go index 50d2f2bb..0d65b3cc 100644 --- a/internal/service/container/inspect.go +++ b/internal/service/container/inspect.go @@ -12,7 +12,6 @@ import ( "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/labels" - "github.com/runfinch/finch-daemon/api/types" ) diff --git a/internal/service/container/inspect_test.go b/internal/service/container/inspect_test.go index e8ef2b48..8c621698 100644 --- a/internal/service/container/inspect_test.go +++ b/internal/service/container/inspect_test.go @@ -86,10 +86,10 @@ var _ = Describe("Container Inspect API ", func() { ncClient.EXPECT().InspectContainer(gomock.Any(), con, sizeFlag).Return( &inspect, nil) - + con.EXPECT().Labels(gomock.Any()).Return(nil, nil) - result, err := service.Inspect(ctx, cid, sizeFlag) + Expect(*result).Should(Equal(ret)) Expect(err).Should(BeNil()) }) @@ -155,9 +155,8 @@ var _ = Describe("Container Inspect API ", func() { ncClient.EXPECT().InspectContainer(gomock.Any(), con, sizeFlag).Return( &inspectWithSize, nil) - + con.EXPECT().Labels(gomock.Any()).Return(nil, nil) - result, err := service.Inspect(ctx, cid, sizeFlag) Expect(err).Should(BeNil()) Expect(result.SizeRw).ShouldNot(BeNil()) @@ -174,7 +173,7 @@ var _ = Describe("Container Inspect API ", func() { ncClient.EXPECT().InspectContainer(gomock.Any(), con, sizeFlag).Return( &inspect, nil) - + con.EXPECT().Labels(gomock.Any()).Return(nil, nil) result, err := service.Inspect(ctx, cid, sizeFlag) Expect(err).Should(BeNil()) diff --git a/mocks/mocks_backend/nerdctlbuildersvc.go b/mocks/mocks_backend/nerdctlbuildersvc.go index 5aca1e6e..c48fa83c 100644 --- a/mocks/mocks_backend/nerdctlbuildersvc.go +++ b/mocks/mocks_backend/nerdctlbuildersvc.go @@ -43,17 +43,17 @@ func (m *MockNerdctlBuilderSvc) EXPECT() *MockNerdctlBuilderSvcMockRecorder { } // Build mocks base method. -func (m *MockNerdctlBuilderSvc) Build(ctx context.Context, client backend.ContainerdClient, options types.BuilderBuildOptions) error { +func (m *MockNerdctlBuilderSvc) Build(ctx context.Context, client backend.ContainerdClient, options types.BuilderBuildOptions, buildID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Build", ctx, client, options) + ret := m.ctrl.Call(m, "Build", ctx, client, options, buildID) ret0, _ := ret[0].(error) return ret0 } // Build indicates an expected call of Build. -func (mr *MockNerdctlBuilderSvcMockRecorder) Build(ctx, client, options any) *gomock.Call { +func (mr *MockNerdctlBuilderSvcMockRecorder) Build(ctx, client, options, buildID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Build", reflect.TypeOf((*MockNerdctlBuilderSvc)(nil).Build), ctx, client, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Build", reflect.TypeOf((*MockNerdctlBuilderSvc)(nil).Build), ctx, client, options, buildID) } // GetBuildkitHost mocks base method. diff --git a/mocks/mocks_builder/buildersvc.go b/mocks/mocks_builder/buildersvc.go index 863baded..72f33102 100644 --- a/mocks/mocks_builder/buildersvc.go +++ b/mocks/mocks_builder/buildersvc.go @@ -44,16 +44,16 @@ func (m *MockService) EXPECT() *MockServiceMockRecorder { } // Build mocks base method. -func (m *MockService) Build(ctx context.Context, options *types.BuilderBuildOptions, tarBody io.ReadCloser) ([]types0.BuildResult, error) { +func (m *MockService) Build(ctx context.Context, options *types.BuilderBuildOptions, tarBody io.ReadCloser, buildID string) ([]types0.BuildResult, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Build", ctx, options, tarBody) + ret := m.ctrl.Call(m, "Build", ctx, options, tarBody, buildID) ret0, _ := ret[0].([]types0.BuildResult) ret1, _ := ret[1].(error) return ret0, ret1 } // Build indicates an expected call of Build. -func (mr *MockServiceMockRecorder) Build(ctx, options, tarBody any) *gomock.Call { +func (mr *MockServiceMockRecorder) Build(ctx, options, tarBody, buildID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Build", reflect.TypeOf((*MockService)(nil).Build), ctx, options, tarBody) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Build", reflect.TypeOf((*MockService)(nil).Build), ctx, options, tarBody, buildID) } diff --git a/mocks/mocks_container/container.go b/mocks/mocks_container/container.go index e287d25b..95a8e8e6 100644 --- a/mocks/mocks_container/container.go +++ b/mocks/mocks_container/container.go @@ -193,7 +193,7 @@ func (m *MockContainer) Restore(arg0 context.Context, arg1 cio.Creator, arg2 str } // Restore indicates an expected call of Restore. -func (mr *MockContainerMockRecorder) Restore(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockContainerMockRecorder) Restore(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Restore", reflect.TypeOf((*MockContainer)(nil).Restore), arg0, arg1, arg2) } diff --git a/mocks/mocks_credential/credentialsvc.go b/mocks/mocks_credential/credentialsvc.go new file mode 100644 index 00000000..88bb200b --- /dev/null +++ b/mocks/mocks_credential/credentialsvc.go @@ -0,0 +1,124 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/runfinch/finch-daemon/api/handlers/credential (interfaces: Service) +// +// Generated by this command: +// +// mockgen --destination=../../../mocks/mocks_credential/credentialsvc.go -package=mocks_credential github.com/runfinch/finch-daemon/api/handlers/credential Service +// + +// Package mocks_credential is a generated GoMock package. +package mocks_credential + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockService is a mock of Service interface. +type MockService struct { + ctrl *gomock.Controller + recorder *MockServiceMockRecorder + isgomock struct{} +} + +// MockServiceMockRecorder is the mock recorder for MockService. +type MockServiceMockRecorder struct { + mock *MockService +} + +// NewMockService creates a new mock instance. +func NewMockService(ctrl *gomock.Controller) *MockService { + mock := &MockService{ctrl: ctrl} + mock.recorder = &MockServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockService) EXPECT() *MockServiceMockRecorder { + return m.recorder +} + +// GenerateBuildID mocks base method. +func (m *MockService) GenerateBuildID() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GenerateBuildID") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GenerateBuildID indicates an expected call of GenerateBuildID. +func (mr *MockServiceMockRecorder) GenerateBuildID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateBuildID", reflect.TypeOf((*MockService)(nil).GenerateBuildID)) +} + +// GetCredentials mocks base method. +func (m *MockService) GetCredentials(ctx context.Context, buildID, serverAddr string) (string, string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCredentials", ctx, buildID, serverAddr) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetCredentials indicates an expected call of GetCredentials. +func (mr *MockServiceMockRecorder) GetCredentials(ctx, buildID, serverAddr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCredentials", reflect.TypeOf((*MockService)(nil).GetCredentials), ctx, buildID, serverAddr) +} + +// RemoveCredentials mocks base method. +func (m *MockService) RemoveCredentials(ctx context.Context, buildID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveCredentials", ctx, buildID) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveCredentials indicates an expected call of RemoveCredentials. +func (mr *MockServiceMockRecorder) RemoveCredentials(ctx, buildID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveCredentials", reflect.TypeOf((*MockService)(nil).RemoveCredentials), ctx, buildID) +} + +// RemoveCredentialsAfterBuild mocks base method. +func (m *MockService) RemoveCredentialsAfterBuild(buildID string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RemoveCredentialsAfterBuild", buildID) +} + +// RemoveCredentialsAfterBuild indicates an expected call of RemoveCredentialsAfterBuild. +func (mr *MockServiceMockRecorder) RemoveCredentialsAfterBuild(buildID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveCredentialsAfterBuild", reflect.TypeOf((*MockService)(nil).RemoveCredentialsAfterBuild), buildID) +} + +// Shutdown mocks base method. +func (m *MockService) Shutdown() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Shutdown") +} + +// Shutdown indicates an expected call of Shutdown. +func (mr *MockServiceMockRecorder) Shutdown() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockService)(nil).Shutdown)) +} + +// StoreCredentials mocks base method. +func (m *MockService) StoreCredentials(ctx context.Context, buildID string, credentials map[string]string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StoreCredentials", ctx, buildID, credentials) + ret0, _ := ret[0].(error) + return ret0 +} + +// StoreCredentials indicates an expected call of StoreCredentials. +func (mr *MockServiceMockRecorder) StoreCredentials(ctx, buildID, credentials any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreCredentials", reflect.TypeOf((*MockService)(nil).StoreCredentials), ctx, buildID, credentials) +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 00000000..b35c7fa0 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,34 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package config provides shared configuration used throughout the application +package config + +import "sync" + +const ( + DefaultFinchAddr = "/run/finch.sock" + DefaultCredentialAddr = "/run/finch-credential.sock" + DefaultNamespace = "finch" + DefaultConfigPath = "/etc/finch/finch.toml" + DefaultPidFile = "/run/finch.pid" +) + +var ( + credentialAddr string = DefaultCredentialAddr + mu sync.RWMutex +) + +// SetCredentialAddr sets the credential address to be used at runtime. +func SetCredentialAddr(addr string) { + mu.Lock() + defer mu.Unlock() + credentialAddr = addr +} + +// GetCredentialAddr returns the current credential address. +func GetCredentialAddr() string { + mu.RLock() + defer mu.RUnlock() + return credentialAddr +} diff --git a/pkg/credential/credential.go b/pkg/credential/credential.go new file mode 100644 index 00000000..eaacc03e --- /dev/null +++ b/pkg/credential/credential.go @@ -0,0 +1,173 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package credential consists of definition of service structures and methods related to credential management +package credential + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "net/url" + "sync" + + dockertypes "github.com/docker/cli/cli/config/types" + "github.com/docker/docker-credential-helpers/registryurl" + "github.com/runfinch/finch-daemon/pkg/flog" +) + +// No constants needed as we're removing TTL-based expiration. +type CredentialCache struct { + Entries map[string]credentialEntry + Mutex sync.RWMutex +} + +// NewCredentialCache creates a new shared credential cache. +func NewCredentialCache() *CredentialCache { + return &CredentialCache{ + Entries: make(map[string]credentialEntry), + } +} + +// credentialEntry represents a set of credentials for a build. +type credentialEntry struct { + credentials map[string]dockertypes.AuthConfig +} + +// service implements the credential.Service interface. +// The service uses a shared cache that is passed in from main. +type CredentialService struct { + cache *CredentialCache + logger flog.Logger +} + +// NewCredentialService creates a new credential service with a shared cache. +func NewCredentialService(logger flog.Logger, cache *CredentialCache) *CredentialService { + return &CredentialService{ + cache: cache, + logger: logger, + } +} + +// GenerateBuildID creates a cryptographically secure random build ID using crypto/rand. +func (s *CredentialService) GenerateBuildID() (string, error) { + id := make([]byte, 32) + _, err := rand.Read(id) + if err != nil { + return "", err + } + return hex.EncodeToString(id), nil +} + +// StoreAuthConfigs stores AuthConfig objects for a build ID. +func (s *CredentialService) StoreAuthConfigs(ctx context.Context, buildID string, authConfigs map[string]dockertypes.AuthConfig) error { + s.cache.Mutex.Lock() + defer s.cache.Mutex.Unlock() + + s.cache.Entries[buildID] = credentialEntry{ + credentials: authConfigs, + } + return nil +} + +// GetCredentials retrieves credentials for a build ID and server address. +func (s *CredentialService) GetCredentials(ctx context.Context, buildID string, serverAddr string) (dockertypes.AuthConfig, error) { + s.cache.Mutex.Lock() + defer s.cache.Mutex.Unlock() + + entry, exists := s.cache.Entries[buildID] + if !exists { + return dockertypes.AuthConfig{}, fmt.Errorf("no credentials found") + } + + target, err := s.getTarget(serverAddr, entry.credentials) + if err != nil { + s.logger.Errorf("Error finding target for server: %v", err) + return dockertypes.AuthConfig{}, err + } + + if target == "" { + if _, fallbackExists := entry.credentials[serverAddr]; !fallbackExists { + s.logger.Errorf("No credentials found for server") + return dockertypes.AuthConfig{}, fmt.Errorf("no credentials found for server") + } + target = serverAddr + } + + authConfig, exists := entry.credentials[target] + if !exists { + s.logger.Errorf("No credentials found for matched target %s", target) + return dockertypes.AuthConfig{}, fmt.Errorf("no credentials found for matched target %s", target) + } + + return authConfig, nil +} + +// RemoveCredentials removes credentials for a build ID. +func (s *CredentialService) RemoveCredentials(buildID string) error { + s.cache.Mutex.Lock() + defer s.cache.Mutex.Unlock() + + if _, exists := s.cache.Entries[buildID]; !exists { + return fmt.Errorf("no credentials found") + } + + delete(s.cache.Entries, buildID) + + return nil +} + +func (s *CredentialService) getTarget(serverURL string, creds map[string]dockertypes.AuthConfig) (string, error) { + server, err := registryurl.Parse(serverURL) + if err != nil { + return serverURL, nil + } + + var targets []string + for cred := range creds { + targets = append(targets, cred) + } + + if target, found := s.findMatch(server, targets, s.exactMatch); found { + return target, nil + } + + if target, found := s.findMatch(server, targets, s.approximateMatch); found { + return target, nil + } + + return "", nil +} + +func (s *CredentialService) exactMatch(serverURL, target url.URL) bool { + return serverURL.String() == target.String() +} + +func (s *CredentialService) approximateMatch(serverURL, target url.URL) bool { + // Ignore scheme differences by using target's scheme. + serverURL.Scheme = target.Scheme + + if serverURL.Port() == "" && target.Port() != "" { + serverURL.Host = serverURL.Host + ":" + target.Port() + } + + if serverURL.Path == "" { + serverURL.Path = target.Path + } + return s.exactMatch(serverURL, target) +} + +// findMatch is a helper function that tries to match a serverURL against a list of targets using the provided matching function. +func (s *CredentialService) findMatch(serverUrl *url.URL, targets []string, matches func(url.URL, url.URL) bool) (string, bool) { + for _, target := range targets { + tURL, err := registryurl.Parse(target) + if err != nil { + continue + } + if matches(*serverUrl, *tURL) { + return target, true + } + } + return "", false +} diff --git a/pkg/credential/credential_test.go b/pkg/credential/credential_test.go new file mode 100644 index 00000000..7739ba3b --- /dev/null +++ b/pkg/credential/credential_test.go @@ -0,0 +1,343 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package credential + +import ( + "context" + "fmt" + "sync" + "testing" + + dockertypes "github.com/docker/cli/cli/config/types" + "github.com/runfinch/finch-daemon/mocks/mocks_logger" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestNewCredentialService(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + logger := mocks_logger.NewLogger(ctrl) + cache := NewCredentialCache() + service := NewCredentialService(logger, cache) + + assert.NotNil(t, service, "CredentialService should not be nil") + assert.NotNil(t, cache, "CredentialCache should not be nil") + assert.NotNil(t, cache.Entries, "Entries map should be initialized") + assert.Empty(t, cache.Entries, "Entries map should be empty") + assert.Equal(t, cache, service.cache, "Service cache should match the provided cache") +} + +func TestGenerateBuildID(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + logger := mocks_logger.NewLogger(ctrl) + cache := NewCredentialCache() + service := NewCredentialService(logger, cache) + + id1, err1 := service.GenerateBuildID() + assert.NoError(t, err1, "GenerateBuildID should not return an error") + assert.NotEmpty(t, id1, "Generated ID should not be empty") + + id2, err2 := service.GenerateBuildID() + assert.NoError(t, err2, "Second GenerateBuildID should not return an error") + assert.NotEmpty(t, id2, "Second generated ID should not be empty") + assert.NotEqual(t, id1, id2, "Generated IDs should be unique") +} + +func TestStoreAuthConfigs(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + logger := mocks_logger.NewLogger(ctrl) + cache := NewCredentialCache() + service := NewCredentialService(logger, cache) + ctx := context.Background() + buildID := "test-build-id" + + authConfigs := map[string]dockertypes.AuthConfig{ + "registry1.example.com": { + Username: "user1", + Password: "pass1", + }, + "registry2.example.com": { + Username: "user2", + Password: "pass2", + }, + } + + err := service.StoreAuthConfigs(ctx, buildID, authConfigs) + assert.NoError(t, err, "StoreAuthConfigs should not return an error") + + // Verify the configs were stored correctly + entry, exists := cache.Entries[buildID] + assert.True(t, exists, "Entry should exist for the build ID") + assert.Equal(t, authConfigs, entry.credentials, "Stored credentials should match the provided configs") + + // Test overwriting existing entry + newAuthConfigs := map[string]dockertypes.AuthConfig{ + "registry3.example.com": { + Username: "user3", + Password: "pass3", + }, + } + + err = service.StoreAuthConfigs(ctx, buildID, newAuthConfigs) + assert.NoError(t, err, "StoreAuthConfigs should not return an error when overwriting") + + // Verify the configs were updated + entry, exists = cache.Entries[buildID] + assert.True(t, exists, "Entry should still exist for the build ID") + assert.Equal(t, newAuthConfigs, entry.credentials, "Stored credentials should be updated") +} + +func TestGetCredentials(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + logger := mocks_logger.NewLogger(ctrl) + logger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + + cache := NewCredentialCache() + service := NewCredentialService(logger, cache) + ctx := context.Background() + buildID := "test-build-id" + + authConfigs := map[string]dockertypes.AuthConfig{ + "https://registry.example.com/v2/": { + Username: "user1", + Password: "pass1", + ServerAddress: "https://registry.example.com/v2/", + }, + "https://index.docker.io:443/v1/": { + Username: "user2", + Password: "pass2", + ServerAddress: "https://index.docker.io:443/v1/", + }, + "https://gcr.io/v1/": { + Username: "user3", + Password: "pass3", + ServerAddress: "https://gcr.io/v1/", + }, + } + err := service.StoreAuthConfigs(ctx, buildID, authConfigs) + assert.NoError(t, err, "StoreAuthConfigs should succeed") + + testCases := []struct { + name string + requestURL string + expectUser string + expectPass string + expectError bool + errorMessage string + }{ + { + name: "exact match", + requestURL: "https://registry.example.com/v2/", + expectUser: "user1", + expectPass: "pass1", + expectError: false, + }, + { + name: "different scheme", + requestURL: "http://registry.example.com/v2/", + expectUser: "user1", + expectPass: "pass1", + expectError: false, + }, + { + name: "missing scheme", + requestURL: "registry.example.com/v2/", + expectUser: "user1", + expectPass: "pass1", + expectError: false, + }, + { + name: "missing port", + requestURL: "https://index.docker.io/v1/", + expectUser: "user2", + expectPass: "pass2", + expectError: false, + }, + { + name: "missing path", + requestURL: "https://gcr.io", + expectUser: "user3", + expectPass: "pass3", + expectError: false, + }, + { + name: "no match", + requestURL: "https://quay.io", + expectUser: "", + expectPass: "", + expectError: true, + errorMessage: "no credentials found for server", + }, + } + + // Run test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // For the invalid build ID test case, use a different build ID + // Store the credentials + testBuildID := buildID + + auth, err := service.GetCredentials(ctx, testBuildID, tc.requestURL) + + if tc.expectError { + assert.Error(t, err) + if tc.errorMessage != "" { + assert.Contains(t, err.Error(), tc.errorMessage) + } + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expectUser, auth.Username) + assert.Equal(t, tc.expectPass, auth.Password) + } + }) + } +} + +func TestRemoveCredentials(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + logger := mocks_logger.NewLogger(ctrl) + cache := NewCredentialCache() + service := NewCredentialService(logger, cache) + ctx := context.Background() + buildID := "test-build-id" + + // Store some test credentials + authConfigs := map[string]dockertypes.AuthConfig{ + "registry.example.com": { + Username: "user1", + Password: "pass1", + }, + } + + err := service.StoreAuthConfigs(ctx, buildID, authConfigs) + assert.NoError(t, err, "StoreAuthConfigs should succeed") + + _, exists := cache.Entries[buildID] + assert.True(t, exists, "Entry should exist before removal") + + err = service.RemoveCredentials(buildID) + assert.NoError(t, err, "RemoveCredentials should succeed") + + _, exists = cache.Entries[buildID] + assert.False(t, exists, "Entry should not exist after removal") + + err = service.RemoveCredentials("non-existent-id") + assert.Error(t, err, "RemoveCredentials should fail for non-existent ID") + assert.Contains(t, err.Error(), "no credentials found") +} + +func TestParallelCredentialOperations(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + logger := mocks_logger.NewLogger(ctrl) + cache := NewCredentialCache() + service := NewCredentialService(logger, cache) + ctx := context.Background() + + const numBuildIDs = 1000 + const numRegistriesPerBuild = 5 + const numConcurrentOperations = 100 + + buildIDs := make([]string, numBuildIDs) + for i := 0; i < numBuildIDs; i++ { + id, err := service.GenerateBuildID() + assert.NoError(t, err) + buildIDs[i] = id + } + + var wg sync.WaitGroup + + t.Run("Parallel Store", func(t *testing.T) { + wg.Add(numBuildIDs) + + for i := 0; i < numBuildIDs; i++ { + go func(buildIdx int) { + defer wg.Done() + buildID := buildIDs[buildIdx] + + authConfigs := make(map[string]dockertypes.AuthConfig) + for j := 0; j < numRegistriesPerBuild; j++ { + registry := fmt.Sprintf("registry%d-%d.example.com", buildIdx, j) + authConfigs[registry] = dockertypes.AuthConfig{ + Username: fmt.Sprintf("user%d-%d", buildIdx, j), + Password: fmt.Sprintf("pass%d-%d", buildIdx, j), + ServerAddress: registry, + } + } + + err := service.StoreAuthConfigs(ctx, buildID, authConfigs) + assert.NoError(t, err) + }(i) + } + + wg.Wait() + + // Verify all entries were created + assert.Equal(t, numBuildIDs, len(cache.Entries), "All entries should be stored correctly") + }) + + // Add stress test for concurrent GetCredentials + t.Run("Parallel Get", func(t *testing.T) { + wg = sync.WaitGroup{} + wg.Add(numConcurrentOperations) + + for i := 0; i < numConcurrentOperations; i++ { + go func(operationIdx int) { + defer wg.Done() + + for j := 0; j < numBuildIDs/numConcurrentOperations; j++ { + buildIdx := (operationIdx*numBuildIDs/numConcurrentOperations + j) % numBuildIDs + buildID := buildIDs[buildIdx] + + for k := 0; k < numRegistriesPerBuild; k++ { + registry := fmt.Sprintf("registry%d-%d.example.com", buildIdx, k) + auth, err := service.GetCredentials(ctx, buildID, registry) + assert.NoError(t, err) + assert.Equal(t, fmt.Sprintf("user%d-%d", buildIdx, k), auth.Username) + assert.Equal(t, fmt.Sprintf("pass%d-%d", buildIdx, k), auth.Password) + } + } + }(i) + } + + wg.Wait() + }) + + // Add stress test for concurrent Remove operations + t.Run("Parallel Remove", func(t *testing.T) { + wg = sync.WaitGroup{} + wg.Add(numConcurrentOperations) + + for i := 0; i < numConcurrentOperations; i++ { + go func(operationIdx int) { + defer wg.Done() + + startIdx := operationIdx * numBuildIDs / numConcurrentOperations + endIdx := (operationIdx + 1) * numBuildIDs / numConcurrentOperations + + for buildIdx := startIdx; buildIdx < endIdx; buildIdx++ { + buildID := buildIDs[buildIdx] + err := service.RemoveCredentials(buildID) + assert.NoError(t, err) + } + }(i) + } + + wg.Wait() + + // Verify all entries were removed + assert.Equal(t, 0, len(cache.Entries), "All entries should be removed correctly") + }) +} diff --git a/scripts/verify-release-artifacts.sh b/scripts/verify-release-artifacts.sh index d90a9d35..96350b26 100755 --- a/scripts/verify-release-artifacts.sh +++ b/scripts/verify-release-artifacts.sh @@ -45,7 +45,7 @@ release_version=${release_tag/v/} pushd "$release_dir" || exit 1 tarballs=("finch-daemon-${release_version}-linux-${arch}.tar.gz" "finch-daemon-${release_version}-linux-${arch}-static.tar.gz") -expected_contents=("finch-daemon" "THIRD_PARTY_LICENSES") +expected_contents=("finch-daemon" "THIRD_PARTY_LICENSES" "docker-credential-finch") release_is_valid=true for t in "${tarballs[@]}"; do diff --git a/setup-test-env.sh b/setup-test-env.sh index c050c796..e216e13d 100755 --- a/setup-test-env.sh +++ b/setup-test-env.sh @@ -1,8 +1,8 @@ #!/bin/bash # Set versions -RUNC_VERSION=1.2.4 -NERDCTL_VERSION=2.0.4 -BUILDKIT_VERSION=0.18.1 +RUNC_VERSION=1.3.0 +NERDCTL_VERSION=2.1.2 +BUILDKIT_VERSION=0.23.2 CNI_VERSION=1.6.2 apt update && apt install -y make gcc linux-libc-dev libseccomp-dev pkg-config git