From 0ee00cb602c54904d93649e1d3e9e377301f8755 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 5 Oct 2026 18:22:00 -0600 Subject: [PATCH 1/3] feat(driver): host external runtime lifecycle operations --- THIRD_PARTY_LICENSES.md | 226 ++++++------- cmd/internal/internal.go | 1 + cmd/internal/runtime_supervisor.go | 15 + cmd/internal/runtime_supervisor_test.go | 27 ++ go.mod | 5 + go.sum | 18 + hack/licenses/overrides.ndjson | 1 + pkg/driver/external/errors.go | 63 ++++ pkg/driver/external/host.go | 159 +++++++++ pkg/driver/external/host_test.go | 314 ++++++++++++++++++ .../external/internal/testfixture/main.go | 156 +++++++++ pkg/driver/external/lifecycle.go | 211 ++++++++++++ pkg/driver/external/session.go | 119 +++++++ .../docs/developing-providers/driver.mdx | 25 ++ 14 files changed, 1229 insertions(+), 111 deletions(-) create mode 100644 cmd/internal/runtime_supervisor.go create mode 100644 cmd/internal/runtime_supervisor_test.go create mode 100644 pkg/driver/external/errors.go create mode 100644 pkg/driver/external/host.go create mode 100644 pkg/driver/external/host_test.go create mode 100644 pkg/driver/external/internal/testfixture/main.go create mode 100644 pkg/driver/external/lifecycle.go create mode 100644 pkg/driver/external/session.go diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md index 20781ea40..6190cd407 100644 --- a/THIRD_PARTY_LICENSES.md +++ b/THIRD_PARTY_LICENSES.md @@ -13,12 +13,12 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | Dependency | Version | License | | ---------- | ------- | ------- | -| [al.essio.dev/pkg/shellescape](https://al.essio.dev/pkg/shellescape) | `v1.6.0` | MIT | +| [al.essio.dev/pkg/shellescape](https://al.essio.dev/pkg/shellescape) | `v1.6.1` | MIT | | [cel.dev/expr](https://cel.dev/expr) | `v0.25.2` | Apache-2.0 | | [charm.land/bubbles/v2](https://charm.land/bubbles/v2) | `v2.0.0` | MIT | | [charm.land/bubbletea/v2](https://charm.land/bubbletea/v2) | `v2.0.2` | MIT | | [charm.land/huh/v2](https://charm.land/huh/v2) | `v2.0.3` | MIT | -| [charm.land/lipgloss/v2](https://charm.land/lipgloss/v2) | `v2.0.5` | MIT | +| [charm.land/lipgloss/v2](https://charm.land/lipgloss/v2) | `v2.0.6` | MIT | | [cloud.google.com/go](https://cloud.google.com/go) | `v0.123.0` | Apache-2.0 | | [cloud.google.com/go/auth](https://cloud.google.com/go/auth) | `v0.22.0` | Apache-2.0 | | [cloud.google.com/go/auth/oauth2adapt](https://cloud.google.com/go/auth/oauth2adapt) | `v0.2.8` | Apache-2.0 | @@ -29,18 +29,18 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [cloud.google.com/go/monitoring](https://cloud.google.com/go/monitoring) | `v1.30.0` | Apache-2.0 | | [cloud.google.com/go/storage](https://cloud.google.com/go/storage) | `v1.63.1` | Apache-2.0 | | [code.gitea.io/sdk/gitea](https://code.gitea.io/sdk/gitea) | `v0.25.1` | MIT | -| [filippo.io/age](https://filippo.io/age) | `v1.3.1` | BSD-3-Clause | +| [filippo.io/age](https://filippo.io/age) | `v1.3.2` | BSD-3-Clause | | [filippo.io/edwards25519](https://filippo.io/edwards25519) | `v1.2.0` | BSD-3-Clause | | [filippo.io/hpke](https://filippo.io/hpke) | `v0.4.0` | BSD-3-Clause | | [github.com/42wim/httpsig](https://github.com/42wim/httpsig) | `v1.2.4` | BSD-3-Clause | | [github.com/AlecAivazis/survey/v2](https://github.com/AlecAivazis/survey) | `v2.3.7` | MIT | -| [github.com/Azure/azure-sdk-for-go/sdk/azcore](https://github.com/Azure/azure-sdk-for-go) | `v1.22.0` | MIT | -| [github.com/Azure/azure-sdk-for-go/sdk/azidentity](https://github.com/Azure/azure-sdk-for-go) | `v1.14.0` | MIT | +| [github.com/Azure/azure-sdk-for-go/sdk/azcore](https://github.com/Azure/azure-sdk-for-go) | `v1.23.1` | MIT | +| [github.com/Azure/azure-sdk-for-go/sdk/azidentity](https://github.com/Azure/azure-sdk-for-go) | `v1.14.1` | MIT | | [github.com/Azure/azure-sdk-for-go/sdk/internal](https://github.com/Azure/azure-sdk-for-go) | `v1.12.0` | MIT | | [github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys](https://github.com/Azure/azure-sdk-for-go) | `v1.5.0` | MIT | | [github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal](https://github.com/Azure/azure-sdk-for-go) | `v1.2.0` | MIT | | [github.com/Azure/go-ansiterm](https://github.com/Azure/go-ansiterm) | `v0.0.0-20250102033503-faa5f7b0171c` | MIT | -| [github.com/AzureAD/microsoft-authentication-library-for-go](https://github.com/AzureAD/microsoft-authentication-library-for-go) | `v1.7.2` | MIT | +| [github.com/AzureAD/microsoft-authentication-library-for-go](https://github.com/AzureAD/microsoft-authentication-library-for-go) | `v1.8.0` | MIT | | [github.com/BurntSushi/toml](https://github.com/BurntSushi/toml) | `v1.6.0` | MIT | | [github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go) | `v1.34.0` | Apache-2.0 | | [github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go) | `v0.58.0` | Apache-2.0 | @@ -54,28 +54,28 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/andybalholm/brotli](https://github.com/andybalholm/brotli) | `v1.1.1` | MIT | | [github.com/antlr4-go/antlr/v4](https://github.com/antlr4-go/antlr) | `v4.13.1` | BSD-3-Clause | | [github.com/atotto/clipboard](https://github.com/atotto/clipboard) | `v0.1.4` | BSD-3-Clause | -| [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) | `v1.43.0` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream](https://github.com/aws/aws-sdk-go-v2) | `v1.7.14` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) | `v1.32.31` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/credentials](https://github.com/aws/aws-sdk-go-v2) | `v1.19.30` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://github.com/aws/aws-sdk-go-v2) | `v1.18.31` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) | `v1.43.8` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream](https://github.com/aws/aws-sdk-go-v2) | `v1.7.18` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) | `v1.32.39` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/credentials](https://github.com/aws/aws-sdk-go-v2) | `v1.19.38` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://github.com/aws/aws-sdk-go-v2) | `v1.18.39` | Apache-2.0 | | [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) | `v1.22.34` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/internal/configsources](https://github.com/aws/aws-sdk-go-v2) | `v1.4.31` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://github.com/aws/aws-sdk-go-v2) | `v2.7.31` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/internal/v4a](https://github.com/aws/aws-sdk-go-v2) | `v1.4.32` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/internal/configsources](https://github.com/aws/aws-sdk-go-v2) | `v1.4.39` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://github.com/aws/aws-sdk-go-v2) | `v2.7.39` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/internal/v4a](https://github.com/aws/aws-sdk-go-v2) | `v1.4.40` | Apache-2.0 | | [github.com/aws/aws-sdk-go-v2/service/ecr](https://github.com/aws/aws-sdk-go-v2) | `v1.56.0` | Apache-2.0 | | [github.com/aws/aws-sdk-go-v2/service/ecrpublic](https://github.com/aws/aws-sdk-go-v2) | `v1.38.11` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding](https://github.com/aws/aws-sdk-go-v2) | `v1.13.13` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/internal/checksum](https://github.com/aws/aws-sdk-go-v2) | `v1.9.24` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://github.com/aws/aws-sdk-go-v2) | `v1.13.31` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/internal/s3shared](https://github.com/aws/aws-sdk-go-v2) | `v1.19.32` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding](https://github.com/aws/aws-sdk-go-v2) | `v1.13.18` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/internal/checksum](https://github.com/aws/aws-sdk-go-v2) | `v1.9.28` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://github.com/aws/aws-sdk-go-v2) | `v1.13.39` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/internal/s3shared](https://github.com/aws/aws-sdk-go-v2) | `v1.19.36` | Apache-2.0 | | [github.com/aws/aws-sdk-go-v2/service/kms](https://github.com/aws/aws-sdk-go-v2) | `v1.54.1` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) | `v1.106.0` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/signin](https://github.com/aws/aws-sdk-go-v2) | `v1.5.0` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/sso](https://github.com/aws/aws-sdk-go-v2) | `v1.33.0` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://github.com/aws/aws-sdk-go-v2) | `v1.38.0` | Apache-2.0 | -| [github.com/aws/aws-sdk-go-v2/service/sts](https://github.com/aws/aws-sdk-go-v2) | `v1.45.0` | Apache-2.0 | -| [github.com/aws/smithy-go](https://github.com/aws/smithy-go) | `v1.27.4` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) | `v1.106.5` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/signin](https://github.com/aws/aws-sdk-go-v2) | `v1.5.8` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/sso](https://github.com/aws/aws-sdk-go-v2) | `v1.33.8` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://github.com/aws/aws-sdk-go-v2) | `v1.38.8` | Apache-2.0 | +| [github.com/aws/aws-sdk-go-v2/service/sts](https://github.com/aws/aws-sdk-go-v2) | `v1.45.8` | Apache-2.0 | +| [github.com/aws/smithy-go](https://github.com/aws/smithy-go) | `v1.27.10` | Apache-2.0 | | [github.com/awslabs/amazon-ecr-credential-helper/ecr-login](https://github.com/awslabs/amazon-ecr-credential-helper) | `v0.12.0` | Apache-2.0 | | [github.com/beorn7/perks](https://github.com/beorn7/perks) | `v1.0.1` | MIT | | [github.com/blang/semver](https://github.com/blang/semver) | `v3.5.1+incompatible` | MIT | @@ -86,8 +86,8 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/cenkalti/backoff/v5](https://github.com/cenkalti/backoff) | `v5.0.3` | MIT | | [github.com/cespare/xxhash/v2](https://github.com/cespare/xxhash) | `v2.3.0` | MIT | | [github.com/charmbracelet/colorprofile](https://github.com/charmbracelet/colorprofile) | `v0.4.3` | MIT | -| [github.com/charmbracelet/ultraviolet](https://github.com/charmbracelet/ultraviolet) | `v0.0.0-20260205113103-524a6607adb8` | MIT | -| [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x) | `v0.11.7` | MIT | +| [github.com/charmbracelet/ultraviolet](https://github.com/charmbracelet/ultraviolet) | `v0.0.0-20260811164956-006e29f97886` | MIT | +| [github.com/charmbracelet/x/ansi](https://github.com/charmbracelet/x) | `v0.11.8` | MIT | | [github.com/charmbracelet/x/exp/ordered](https://github.com/charmbracelet/x) | `v0.1.0` | MIT | | [github.com/charmbracelet/x/exp/strings](https://github.com/charmbracelet/x) | `v0.0.0-20240722160745-212f7b056ed0` | MIT | | [github.com/charmbracelet/x/term](https://github.com/charmbracelet/x) | `v0.2.2` | MIT | @@ -98,15 +98,15 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/cloudflare/circl](https://github.com/cloudflare/circl) | `v1.6.4` | BSD-3-Clause | | [github.com/cncf/xds/go](https://github.com/cncf/xds) | `v0.0.0-20260202195803-dba9d589def2` | Apache-2.0 | | [github.com/coder/websocket](https://github.com/coder/websocket) | `v1.8.14` | ISC | -| [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) | `v2.14.0` | Apache-2.0 | +| [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) | `v2.15.0` | Apache-2.0 | | [github.com/containerd/console](https://github.com/containerd/console) | `v1.0.5` | Apache-2.0 | | [github.com/containerd/containerd/api](https://github.com/containerd/containerd) | `v1.11.1` | Apache-2.0 | -| [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) | `v2.3.3` | Apache-2.0 | +| [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) | `v2.3.4` | Apache-2.0 | | [github.com/containerd/continuity](https://github.com/containerd/continuity) | `v0.5.0` | Apache-2.0 | | [github.com/containerd/errdefs](https://github.com/containerd/errdefs) | `v1.0.0` | Apache-2.0 | | [github.com/containerd/errdefs/pkg](https://github.com/containerd/errdefs) | `v0.3.0` | Apache-2.0 | | [github.com/containerd/log](https://github.com/containerd/log) | `v0.1.0` | Apache-2.0 | -| [github.com/containerd/platforms](https://github.com/containerd/platforms) | `v1.0.0-rc.4` | Apache-2.0 | +| [github.com/containerd/platforms](https://github.com/containerd/platforms) | `v1.0.0-rc.5` | Apache-2.0 | | [github.com/containerd/ttrpc](https://github.com/containerd/ttrpc) | `v1.2.9` | Apache-2.0 | | [github.com/containerd/typeurl/v2](https://github.com/containerd/typeurl) | `v2.3.0` | Apache-2.0 | | [github.com/containers/image/v5](https://github.com/containers/image) | `v5.36.2` | Apache-2.0 | @@ -124,12 +124,13 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/devsy-org/admin-apis](https://github.com/devsy-org/admin-apis) | `v1.2.0` | MPL-2.0 | | [github.com/devsy-org/agentapi](https://github.com/devsy-org/agentapi) | `v1.0.1` | MPL-2.0 | | [github.com/devsy-org/api](https://github.com/devsy-org/api) | `v1.1.0` | MPL-2.0 | -| [github.com/devsy-org/apiserver](https://github.com/devsy-org/apiserver) | `v1.5.3` | Apache-2.0 | -| [github.com/devsy-org/ssh](https://github.com/devsy-org/ssh) | `v1.2.5` | BSD-3-Clause | +| [github.com/devsy-org/apiserver](https://github.com/devsy-org/apiserver) | `v1.5.4` | Apache-2.0 | +| [github.com/devsy-org/devsy-runtime-sdk](https://github.com/devsy-org/devsy-runtime-sdk) | `v1.2.0` | MPL-2.0 | +| [github.com/devsy-org/ssh](https://github.com/devsy-org/ssh) | `v1.2.9` | BSD-3-Clause | | [github.com/distribution/reference](https://github.com/distribution/reference) | `v0.6.0` | Apache-2.0 | -| [github.com/docker/cli](https://github.com/docker/cli) | `v29.7.1+incompatible` | Apache-2.0 | +| [github.com/docker/cli](https://github.com/docker/cli) | `v29.8.0+incompatible` | Apache-2.0 | | [github.com/docker/docker](https://github.com/docker/docker) | `v28.5.2+incompatible` | Apache-2.0 | -| [github.com/docker/docker-credential-helpers](https://github.com/docker/docker-credential-helpers) | `v0.9.8` | MIT | +| [github.com/docker/docker-credential-helpers](https://github.com/docker/docker-credential-helpers) | `v0.9.9` | MIT | | [github.com/docker/go-connections](https://github.com/docker/go-connections) | `v0.8.1` | Apache-2.0 | | [github.com/docker/go-units](https://github.com/docker/go-units) | `v0.5.0` | Apache-2.0 | | [github.com/dustin/go-humanize](https://github.com/dustin/go-humanize) | `v1.0.1` | MIT | @@ -141,7 +142,7 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/fatih/color](https://github.com/fatih/color) | `v1.19.0` | MIT | | [github.com/felixge/httpsnoop](https://github.com/felixge/httpsnoop) | `v1.1.0` | MIT | | [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify) | `v1.10.1` | BSD-3-Clause | -| [github.com/fxamacker/cbor/v2](https://github.com/fxamacker/cbor) | `v2.9.0` | MIT | +| [github.com/fxamacker/cbor/v2](https://github.com/fxamacker/cbor) | `v2.9.1` | MIT | | [github.com/gaissmai/bart](https://github.com/gaissmai/bart) | `v0.26.1` | MIT | | [github.com/getsops/gopgagent](https://github.com/getsops/gopgagent) | `v0.0.0-20241224165529-7044f28e491e` | Apache-2.0 | | [github.com/getsops/sops/v3](https://github.com/getsops/sops) | `v3.13.3` | MPL-2.0 | @@ -152,37 +153,36 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/go-logr/stdr](https://github.com/go-logr/stdr) | `v1.2.2` | Apache-2.0 | | [github.com/go-logr/zapr](https://github.com/go-logr/zapr) | `v1.3.0` | Apache-2.0 | | [github.com/go-ole/go-ole](https://github.com/go-ole/go-ole) | `v1.3.0` | MIT | -| [github.com/go-openapi/jsonpointer](https://github.com/go-openapi/jsonpointer) | `v0.23.1` | Apache-2.0 | -| [github.com/go-openapi/jsonreference](https://github.com/go-openapi/jsonreference) | `v0.21.6` | Apache-2.0 | -| [github.com/go-openapi/swag](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | -| [github.com/go-openapi/swag/cmdutils](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | -| [github.com/go-openapi/swag/conv](https://github.com/go-openapi/swag) | `v0.27.0` | Apache-2.0 | -| [github.com/go-openapi/swag/fileutils](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | -| [github.com/go-openapi/swag/jsonname](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | -| [github.com/go-openapi/swag/jsonutils](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | -| [github.com/go-openapi/swag/loading](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | -| [github.com/go-openapi/swag/mangling](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | -| [github.com/go-openapi/swag/netutils](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | -| [github.com/go-openapi/swag/stringutils](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | -| [github.com/go-openapi/swag/typeutils](https://github.com/go-openapi/swag) | `v0.27.0` | Apache-2.0 | -| [github.com/go-openapi/swag/yamlutils](https://github.com/go-openapi/swag) | `v0.26.1` | Apache-2.0 | +| [github.com/go-openapi/jsonpointer](https://github.com/go-openapi/jsonpointer) | `v1.0.0` | Apache-2.0 | +| [github.com/go-openapi/jsonreference](https://github.com/go-openapi/jsonreference) | `v1.0.0` | Apache-2.0 | +| [github.com/go-openapi/swag](https://github.com/go-openapi/swag) | `v0.27.1` | Apache-2.0 | +| [github.com/go-openapi/swag/cmdutils](https://github.com/go-openapi/swag) | `v0.27.1` | Apache-2.0 | +| [github.com/go-openapi/swag/conv](https://github.com/go-openapi/swag) | `v0.27.1` | Apache-2.0 | +| [github.com/go-openapi/swag/fileutils](https://github.com/go-openapi/swag) | `v0.27.1` | Apache-2.0 | +| [github.com/go-openapi/swag/jsonutils](https://github.com/go-openapi/swag) | `v0.27.1` | Apache-2.0 | +| [github.com/go-openapi/swag/loading](https://github.com/go-openapi/swag) | `v0.27.1` | Apache-2.0 | +| [github.com/go-openapi/swag/mangling](https://github.com/go-openapi/swag) | `v0.27.1` | Apache-2.0 | +| [github.com/go-openapi/swag/netutils](https://github.com/go-openapi/swag) | `v0.27.1` | Apache-2.0 | +| [github.com/go-openapi/swag/pools](https://github.com/go-openapi/swag) | `v0.27.1` | Apache-2.0 | +| [github.com/go-openapi/swag/stringutils](https://github.com/go-openapi/swag) | `v0.27.1` | Apache-2.0 | +| [github.com/go-openapi/swag/typeutils](https://github.com/go-openapi/swag) | `v0.27.1` | Apache-2.0 | +| [github.com/go-openapi/swag/yamlutils](https://github.com/go-openapi/swag) | `v0.27.1` | Apache-2.0 | | [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) | `v2.5.0` | MIT | | [github.com/goccy/go-json](https://github.com/goccy/go-json) | `v0.10.6` | MIT | | [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) | `v1.19.2` | MIT | | [github.com/godbus/dbus/v5](https://github.com/godbus/dbus) | `v5.2.2` | BSD-2-Clause | -| [github.com/gofrs/flock](https://github.com/gofrs/flock) | `v0.13.0` | BSD-3-Clause | -| [github.com/gogo/protobuf](https://github.com/gogo/protobuf) | `v1.3.2` | BSD-3-Clause | +| [github.com/gofrs/flock](https://github.com/gofrs/flock) | `v0.13.1` | BSD-3-Clause | | [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) | `v5.3.1` | MIT | | [github.com/golang/groupcache](https://github.com/golang/groupcache) | `v0.0.0-20241129210726-2c02b8208cf8` | Apache-2.0 | | [github.com/golang/protobuf](https://github.com/golang/protobuf) | `v1.5.4` | BSD-3-Clause | | [github.com/google/btree](https://github.com/google/btree) | `v1.1.3` | Apache-2.0 | -| [github.com/google/cel-go](https://github.com/google/cel-go) | `v0.27.0` | Apache-2.0 | +| [github.com/google/cel-go](https://github.com/google/cel-go) | `v0.29.2` | Apache-2.0 | | [github.com/google/gnostic-models](https://github.com/google/gnostic-models) | `v0.7.1` | Apache-2.0 | | [github.com/google/go-cmp](https://github.com/google/go-cmp) | `v0.7.0` | BSD-3-Clause | -| [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) | `v0.21.8` | Apache-2.0 | +| [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) | `v0.22.1` | Apache-2.0 | | [github.com/google/go-containerregistry/pkg/authn/kubernetes](https://github.com/google/go-containerregistry) | `v0.0.0-20260731170845-2ea098f4b134` | Apache-2.0 | | [github.com/google/go-github/v86](https://github.com/google/go-github) | `v86.0.0` | BSD-3-Clause | -| [github.com/google/go-github/v90](https://github.com/google/go-github) | `v90.0.0` | BSD-3-Clause | +| [github.com/google/go-github/v91](https://github.com/google/go-github) | `v91.0.0` | BSD-3-Clause | | [github.com/google/go-querystring](https://github.com/google/go-querystring) | `v1.2.0` | BSD-3-Clause | | [github.com/google/jsonschema-go](https://github.com/google/jsonschema-go) | `v0.4.3` | MIT | | [github.com/google/s2a-go](https://github.com/google/s2a-go) | `v0.1.9` | Apache-2.0 | @@ -198,7 +198,9 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/grpc-ecosystem/grpc-gateway/v2](https://github.com/grpc-ecosystem/grpc-gateway) | `v2.29.0` | BSD-3-Clause | | [github.com/hashicorp/errwrap](https://github.com/hashicorp/errwrap) | `v1.1.0` | MPL-2.0 | | [github.com/hashicorp/go-cleanhttp](https://github.com/hashicorp/go-cleanhttp) | `v0.5.2` | MPL-2.0 | +| [github.com/hashicorp/go-hclog](https://github.com/hashicorp/go-hclog) | `v1.6.3` | MIT | | [github.com/hashicorp/go-multierror](https://github.com/hashicorp/go-multierror) | `v1.1.1` | MPL-2.0 | +| [github.com/hashicorp/go-plugin](https://github.com/hashicorp/go-plugin) | `v1.8.0` | MPL-2.0 | | [github.com/hashicorp/go-retryablehttp](https://github.com/hashicorp/go-retryablehttp) | `v0.7.8` | MPL-2.0 | | [github.com/hashicorp/go-rootcerts](https://github.com/hashicorp/go-rootcerts) | `v1.0.2` | MPL-2.0 | | [github.com/hashicorp/go-secure-stdlib/parseutil](https://github.com/hashicorp/go-secure-stdlib) | `v0.2.0` | MPL-2.0 | @@ -208,6 +210,7 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/hashicorp/golang-lru/v2](https://github.com/hashicorp/golang-lru) | `v2.0.7` | MPL-2.0 | | [github.com/hashicorp/hcl](https://github.com/hashicorp/hcl) | `v1.0.1-vault-7` | MPL-2.0 | | [github.com/hashicorp/vault/api](https://github.com/hashicorp/vault) | `v1.23.0` | MPL-2.0 | +| [github.com/hashicorp/yamux](https://github.com/hashicorp/yamux) | `v0.1.2` | MPL-2.0 | | [github.com/hdevalence/ed25519consensus](https://github.com/hdevalence/ed25519consensus) | `v0.2.0` | BSD-3-Clause | | [github.com/huaweicloud/huaweicloud-sdk-go-v3](https://github.com/huaweicloud/huaweicloud-sdk-go-v3) | `v0.1.207` | BSD-2-Clause | | [github.com/huin/goupnp](https://github.com/huin/goupnp) | `v1.3.0` | BSD-2-Clause | @@ -218,15 +221,15 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/jsimonetti/rtnetlink](https://github.com/jsimonetti/rtnetlink) | `v1.4.1` | MIT | | [github.com/json-iterator/go](https://github.com/json-iterator/go) | `v1.1.13-0.20220915233716-71ac16282d12` | MIT | | [github.com/kballard/go-shellquote](https://github.com/kballard/go-shellquote) | `v0.0.0-20180428030007-95032a82bc51` | MIT | -| [github.com/klauspost/compress](https://github.com/klauspost/compress) | `v1.19.1` | Apache-2.0 | +| [github.com/klauspost/compress](https://github.com/klauspost/compress) | `v1.19.2` | Apache-2.0 | | [github.com/kr/fs](https://github.com/kr/fs) | `v0.1.0` | BSD-3-Clause | | [github.com/kylelemons/godebug](https://github.com/kylelemons/godebug) | `v1.1.0` | Apache-2.0 | | [github.com/lib/pq](https://github.com/lib/pq) | `v1.12.3` | MIT | | [github.com/liggitt/tabwriter](https://github.com/liggitt/tabwriter) | `v0.0.0-20181228230101-89fcab3d43de` | BSD-3-Clause | -| [github.com/lucasb-eyer/go-colorful](https://github.com/lucasb-eyer/go-colorful) | `v1.4.0` | MIT | +| [github.com/lucasb-eyer/go-colorful](https://github.com/lucasb-eyer/go-colorful) | `v1.4.1` | MIT | | [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable) | `v0.1.15` | MIT | | [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) | `v0.0.23` | MIT | -| [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) | `v0.0.23` | MIT | +| [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) | `v0.0.24` | MIT | | [github.com/mattn/go-shellwords](https://github.com/mattn/go-shellwords) | `v1.0.12` | MIT | | [github.com/mdlayher/netlink](https://github.com/mdlayher/netlink) | `v1.7.3-0.20250113171957-fbb4dce95f42` | MIT | | [github.com/mdlayher/socket](https://github.com/mdlayher/socket) | `v0.5.1` | MIT | @@ -236,7 +239,7 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/mitchellh/go-wordwrap](https://github.com/mitchellh/go-wordwrap) | `v1.0.1` | MIT | | [github.com/mitchellh/hashstructure/v2](https://github.com/mitchellh/hashstructure) | `v2.0.2` | MIT | | [github.com/mitchellh/mapstructure](https://github.com/mitchellh/mapstructure) | `v1.5.1-0.20231216201459-8508981c8b6c` | MIT | -| [github.com/moby/buildkit](https://github.com/moby/buildkit) | `v0.32.2` | Apache-2.0 | +| [github.com/moby/buildkit](https://github.com/moby/buildkit) | `v0.33.0` | Apache-2.0 | | [github.com/moby/docker-image-spec](https://github.com/moby/docker-image-spec) | `v1.3.1` | Apache-2.0 | | [github.com/moby/locker](https://github.com/moby/locker) | `v1.0.1` | Apache-2.0 | | [github.com/moby/patternmatcher](https://github.com/moby/patternmatcher) | `v0.6.1` | Apache-2.0 | @@ -252,8 +255,9 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/morikuni/aec](https://github.com/morikuni/aec) | `v1.1.0` | MIT | | [github.com/muesli/cancelreader](https://github.com/muesli/cancelreader) | `v0.2.2` | MIT | | [github.com/munnerz/goautoneg](https://github.com/munnerz/goautoneg) | `v0.0.0-20191010083416-a7dc8b61c822` | BSD-3-Clause | -| [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) | `v2.32.0` | MIT | -| [github.com/onsi/gomega](https://github.com/onsi/gomega) | `v1.42.1` | MIT | +| [github.com/oklog/run](https://github.com/oklog/run) | `v1.1.0` | Apache-2.0 | +| [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) | `v2.32.1` | MIT | +| [github.com/onsi/gomega](https://github.com/onsi/gomega) | `v1.43.0` | MIT | | [github.com/opencontainers/go-digest](https://github.com/opencontainers/go-digest) | `v1.0.0` | Apache-2.0 | | [github.com/opencontainers/image-spec](https://github.com/opencontainers/image-spec) | `v1.1.1` | Apache-2.0 | | [github.com/opencontainers/runtime-spec](https://github.com/opencontainers/runtime-spec) | `v1.3.0` | Apache-2.0 | @@ -263,11 +267,11 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/pkg/sftp](https://github.com/pkg/sftp) | `v1.13.11` | BSD-2-Clause | | [github.com/planetscale/vtprotobuf](https://github.com/planetscale/vtprotobuf) | `v0.6.1-0.20240319094008-0393e58bdf10` | BSD-3-Clause | | [github.com/pmezard/go-difflib](https://github.com/pmezard/go-difflib) | `v1.0.1-0.20181226105442-5d4384ee4fb2` | BSD-3-Clause | -| [github.com/posthog/posthog-go](https://github.com/posthog/posthog-go) | `v1.22.0` | MIT | -| [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) | `v1.23.2` | Apache-2.0 | +| [github.com/posthog/posthog-go](https://github.com/posthog/posthog-go) | `v1.25.1` | MIT | +| [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) | `v1.24.1` | Apache-2.0 | | [github.com/prometheus/client_model](https://github.com/prometheus/client_model) | `v0.6.2` | Apache-2.0 | -| [github.com/prometheus/common](https://github.com/prometheus/common) | `v0.69.0` | Apache-2.0 | -| [github.com/prometheus/procfs](https://github.com/prometheus/procfs) | `v0.20.1` | Apache-2.0 | +| [github.com/prometheus/common](https://github.com/prometheus/common) | `v0.70.1` | Apache-2.0 | +| [github.com/prometheus/procfs](https://github.com/prometheus/procfs) | `v0.21.1` | Apache-2.0 | | [github.com/rivo/uniseg](https://github.com/rivo/uniseg) | `v0.4.7` | MIT | | [github.com/russross/blackfriday/v2](https://github.com/russross/blackfriday) | `v2.1.0` | BSD-2-Clause | | [github.com/ryanuber/go-glob](https://github.com/ryanuber/go-glob) | `v1.0.0` | MIT | @@ -277,13 +281,13 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/segmentio/asm](https://github.com/segmentio/asm) | `v1.2.1` | MIT-0 | | [github.com/segmentio/encoding](https://github.com/segmentio/encoding) | `v0.5.4` | MIT | | [github.com/shibumi/go-pathspec](https://github.com/shibumi/go-pathspec) | `v1.3.0` | Apache-2.0 | -| [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) | `v4.26.7` | BSD-3-Clause | -| [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) | `v1.9.4` | MIT | +| [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) | `v4.26.8` | BSD-3-Clause | +| [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) | `v1.10.1` | MIT | | [github.com/skratchdot/open-golang](https://github.com/skratchdot/open-golang) | `v0.0.0-20200116055534-eef842397966` | MIT | | [github.com/spf13/cobra](https://github.com/spf13/cobra) | `v1.10.2` | Apache-2.0 | | [github.com/spf13/pflag](https://github.com/spf13/pflag) | `v1.0.10` | BSD-3-Clause | | [github.com/spiffe/go-spiffe/v2](https://github.com/spiffe/go-spiffe) | `v2.8.1` | Apache-2.0 | -| [github.com/stretchr/testify](https://github.com/stretchr/testify) | `v1.12.0` | MIT | +| [github.com/stretchr/testify](https://github.com/stretchr/testify) | `v1.12.1` | MIT | | [github.com/tailscale/certstore](https://github.com/tailscale/certstore) | `v0.1.1-0.20260409135935-3638fb84b77d` | MIT | | [github.com/tailscale/go-winio](https://github.com/tailscale/go-winio) | `v0.0.0-20231025203758-c4f33415bf55` | MIT | | [github.com/tailscale/hujson](https://github.com/tailscale/hujson) | `v0.0.0-20260727124030-b80ff77dac4f` | BSD-3-Clause | @@ -296,7 +300,7 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/tjfoc/gmsm](https://github.com/tjfoc/gmsm) | `v1.4.1` | Apache-2.0 | | [github.com/tklauser/go-sysconf](https://github.com/tklauser/go-sysconf) | `v0.3.16` | BSD-3-Clause | | [github.com/tklauser/numcpus](https://github.com/tklauser/numcpus) | `v0.11.0` | Apache-2.0 | -| [github.com/tonistiigi/fsutil](https://github.com/tonistiigi/fsutil) | `v0.0.0-20260717003753-6d9dc2ebad62` | MIT | +| [github.com/tonistiigi/fsutil](https://github.com/tonistiigi/fsutil) | `v0.0.0-20260819142231-83cac42c1c52` | MIT | | [github.com/tonistiigi/go-csvvalue](https://github.com/tonistiigi/go-csvvalue) | `v0.0.0-20240814133006-030d3b2625d0` | MIT | | [github.com/tonistiigi/units](https://github.com/tonistiigi/units) | `v0.0.0-20180711220420-6950e57a87ea` | MIT | | [github.com/tonistiigi/vt100](https://github.com/tonistiigi/vt100) | `v0.0.0-20240514184818-90bafcd6abab` | MIT | @@ -310,76 +314,76 @@ To regenerate this file after changing dependencies, run `task cli:licenses`. | [github.com/yusufpapurcu/wmi](https://github.com/yusufpapurcu/wmi) | `v1.2.4` | MIT | | [github.com/zalando/go-keyring](https://github.com/zalando/go-keyring) | `v0.2.8` | MIT | | [gitlab.com/gitlab-org/api/client-go](https://gitlab.com/gitlab-org/api/client-go) | `v1.46.0` | Apache-2.0 | -| [go.etcd.io/etcd/api/v3](https://go.etcd.io/etcd/api/v3) | `v3.6.8` | Apache-2.0 | -| [go.etcd.io/etcd/client/pkg/v3](https://go.etcd.io/etcd/client/pkg/v3) | `v3.6.8` | Apache-2.0 | -| [go.etcd.io/etcd/client/v3](https://go.etcd.io/etcd/client/v3) | `v3.6.8` | Apache-2.0 | +| [go.etcd.io/etcd/api/v3](https://go.etcd.io/etcd/api/v3) | `v3.7.0` | Apache-2.0 | +| [go.etcd.io/etcd/client/pkg/v3](https://go.etcd.io/etcd/client/pkg/v3) | `v3.7.0` | Apache-2.0 | +| [go.etcd.io/etcd/client/v3](https://go.etcd.io/etcd/client/v3) | `v3.7.0` | Apache-2.0 | | [go.mongodb.org/mongo-driver](https://go.mongodb.org/mongo-driver) | `v1.17.9` | Apache-2.0 | | [go.opentelemetry.io/auto/sdk](https://go.opentelemetry.io/auto/sdk) | `v1.2.1` | Apache-2.0 | | [go.opentelemetry.io/contrib/detectors/gcp](https://go.opentelemetry.io/contrib/detectors/gcp) | `v1.44.0` | Apache-2.0 | -| [go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc](https://go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc) | `v0.69.0` | Apache-2.0 | -| [go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace](https://go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace) | `v0.69.0` | Apache-2.0 | -| [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp) | `v0.69.0` | Apache-2.0 | -| [go.opentelemetry.io/otel](https://go.opentelemetry.io/otel) | `v1.44.0` | Apache-2.0 | -| [go.opentelemetry.io/otel/exporters/otlp/otlptrace](https://go.opentelemetry.io/otel/exporters/otlp/otlptrace) | `v1.44.0` | Apache-2.0 | -| [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc](https://go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc) | `v1.44.0` | Apache-2.0 | -| [go.opentelemetry.io/otel/metric](https://go.opentelemetry.io/otel/metric) | `v1.44.0` | Apache-2.0 | -| [go.opentelemetry.io/otel/sdk](https://go.opentelemetry.io/otel/sdk) | `v1.44.0` | Apache-2.0 | -| [go.opentelemetry.io/otel/sdk/metric](https://go.opentelemetry.io/otel/sdk/metric) | `v1.44.0` | Apache-2.0 | -| [go.opentelemetry.io/otel/trace](https://go.opentelemetry.io/otel/trace) | `v1.44.0` | Apache-2.0 | -| [go.opentelemetry.io/proto/otlp](https://go.opentelemetry.io/proto/otlp) | `v1.10.0` | Apache-2.0 | +| [go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc](https://go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc) | `v0.70.0` | Apache-2.0 | +| [go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace](https://go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace) | `v0.70.0` | Apache-2.0 | +| [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp) | `v0.70.0` | Apache-2.0 | +| [go.opentelemetry.io/otel](https://go.opentelemetry.io/otel) | `v1.45.0` | Apache-2.0 | +| [go.opentelemetry.io/otel/exporters/otlp/otlptrace](https://go.opentelemetry.io/otel/exporters/otlp/otlptrace) | `v1.45.0` | Apache-2.0 | +| [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc](https://go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc) | `v1.45.0` | Apache-2.0 | +| [go.opentelemetry.io/otel/metric](https://go.opentelemetry.io/otel/metric) | `v1.45.0` | Apache-2.0 | +| [go.opentelemetry.io/otel/sdk](https://go.opentelemetry.io/otel/sdk) | `v1.45.0` | Apache-2.0 | +| [go.opentelemetry.io/otel/sdk/metric](https://go.opentelemetry.io/otel/sdk/metric) | `v1.45.0` | Apache-2.0 | +| [go.opentelemetry.io/otel/trace](https://go.opentelemetry.io/otel/trace) | `v1.45.0` | Apache-2.0 | +| [go.opentelemetry.io/proto/otlp](https://go.opentelemetry.io/proto/otlp) | `v1.11.0` | Apache-2.0 | | [go.uber.org/atomic](https://go.uber.org/atomic) | `v1.11.0` | MIT | | [go.uber.org/goleak](https://go.uber.org/goleak) | `v1.3.0` | MIT | | [go.uber.org/multierr](https://go.uber.org/multierr) | `v1.11.0` | MIT | | [go.uber.org/zap](https://go.uber.org/zap) | `v1.28.0` | MIT | | [go.yaml.in/yaml/v2](https://go.yaml.in/yaml/v2) | `v2.4.4` | Apache-2.0 | -| [go.yaml.in/yaml/v3](https://go.yaml.in/yaml/v3) | `v3.0.4` | MIT | +| [go.yaml.in/yaml/v3](https://go.yaml.in/yaml/v3) | `v3.0.5` | MIT | | [go.yaml.in/yaml/v4](https://go.yaml.in/yaml/v4) | `v4.0.0-rc.6` | Apache-2.0 | | [go4.org/mem](https://go4.org/mem) | `v0.0.0-20240501181205-ae6ca9944745` | Apache-2.0 | | [go4.org/netipx](https://go4.org/netipx) | `v0.0.0-20231129151722-fdeea329fbba` | BSD-3-Clause | -| [golang.org/x/crypto](https://golang.org/x/crypto) | `v0.55.0` | BSD-3-Clause | +| [golang.org/x/crypto](https://golang.org/x/crypto) | `v0.57.0` | BSD-3-Clause | | [golang.org/x/exp](https://golang.org/x/exp) | `v0.0.0-20260603202125-055de637280b` | BSD-3-Clause | -| [golang.org/x/mod](https://golang.org/x/mod) | `v0.38.0` | BSD-3-Clause | -| [golang.org/x/net](https://golang.org/x/net) | `v0.57.1-0.20260729233039-99c3b0a8f463` | BSD-3-Clause | +| [golang.org/x/mod](https://golang.org/x/mod) | `v0.41.0` | BSD-3-Clause | +| [golang.org/x/net](https://golang.org/x/net) | `v0.58.0` | BSD-3-Clause | | [golang.org/x/oauth2](https://golang.org/x/oauth2) | `v0.36.0` | BSD-3-Clause | -| [golang.org/x/sync](https://golang.org/x/sync) | `v0.22.0` | BSD-3-Clause | -| [golang.org/x/sys](https://golang.org/x/sys) | `v0.47.0` | BSD-3-Clause | -| [golang.org/x/term](https://golang.org/x/term) | `v0.45.0` | BSD-3-Clause | -| [golang.org/x/text](https://golang.org/x/text) | `v0.41.0` | BSD-3-Clause | +| [golang.org/x/sync](https://golang.org/x/sync) | `v0.23.0` | BSD-3-Clause | +| [golang.org/x/sys](https://golang.org/x/sys) | `v0.48.0` | BSD-3-Clause | +| [golang.org/x/term](https://golang.org/x/term) | `v0.46.0` | BSD-3-Clause | +| [golang.org/x/text](https://golang.org/x/text) | `v0.42.0` | BSD-3-Clause | | [golang.org/x/time](https://golang.org/x/time) | `v0.15.0` | BSD-3-Clause | -| [golang.org/x/tools](https://golang.org/x/tools) | `v0.48.0` | BSD-3-Clause | +| [golang.org/x/tools](https://golang.org/x/tools) | `v0.49.0` | BSD-3-Clause | | [golang.zx2c4.com/wintun](https://golang.zx2c4.com/wintun) | `v0.0.0-20230126152724-0fa3db229ce2` | MIT | | [golang.zx2c4.com/wireguard/windows](https://golang.zx2c4.com/wireguard/windows) | `v0.5.3` | MIT | | [gomodules.xyz/jsonpatch/v2](https://gomodules.xyz/jsonpatch/v2) | `v2.4.0` | Apache-2.0 | | [google.golang.org/api](https://google.golang.org/api) | `v0.289.0` | BSD-3-Clause | | [google.golang.org/genproto](https://google.golang.org/genproto) | `v0.0.0-20260720171339-e059f2f05d78` | Apache-2.0 | -| [google.golang.org/genproto/googleapis/api](https://google.golang.org/genproto/googleapis/api) | `v0.0.0-20260720171339-e059f2f05d78` | Apache-2.0 | -| [google.golang.org/genproto/googleapis/rpc](https://google.golang.org/genproto/googleapis/rpc) | `v0.0.0-20260720171339-e059f2f05d78` | Apache-2.0 | -| [google.golang.org/grpc](https://google.golang.org/grpc) | `v1.83.0` | Apache-2.0 | -| [google.golang.org/protobuf](https://google.golang.org/protobuf) | `v1.36.12-0.20260120151049-f2248ac996af` | BSD-3-Clause | +| [google.golang.org/genproto/googleapis/api](https://google.golang.org/genproto/googleapis/api) | `v0.0.0-20260803160001-6ac0973c030d` | Apache-2.0 | +| [google.golang.org/genproto/googleapis/rpc](https://google.golang.org/genproto/googleapis/rpc) | `v0.0.0-20260803160001-6ac0973c030d` | Apache-2.0 | +| [google.golang.org/grpc](https://google.golang.org/grpc) | `v1.83.2` | Apache-2.0 | +| [google.golang.org/protobuf](https://google.golang.org/protobuf) | `v1.36.12` | BSD-3-Clause | | [gopkg.in/evanphx/json-patch.v4](https://gopkg.in/evanphx/json-patch.v4) | `v4.13.0` | BSD-3-Clause | | [gopkg.in/inf.v0](https://gopkg.in/inf.v0) | `v0.9.1` | BSD-3-Clause | | [gopkg.in/ini.v1](https://gopkg.in/ini.v1) | `v1.67.3` | Apache-2.0 | | [gopkg.in/yaml.v3](https://gopkg.in/yaml.v3) | `v3.0.1` | MIT | | [gvisor.dev/gvisor](https://gvisor.dev/gvisor) | `v0.0.0-20260224225140-573d5e7127a8` | Apache-2.0 | -| [k8s.io/api](https://github.com/kubernetes/api) | `v0.36.3` | Apache-2.0 | -| [k8s.io/apiextensions-apiserver](https://github.com/kubernetes/apiextensions-apiserver) | `v0.36.2` | Apache-2.0 | -| [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) | `v0.36.3` | Apache-2.0 | -| [k8s.io/apiserver](https://github.com/kubernetes/apiserver) | `v0.36.3` | Apache-2.0 | -| [k8s.io/cli-runtime](https://github.com/kubernetes/cli-runtime) | `v0.36.3` | Apache-2.0 | -| [k8s.io/client-go](https://github.com/kubernetes/client-go) | `v0.36.3` | Apache-2.0 | -| [k8s.io/component-base](https://github.com/kubernetes/component-base) | `v0.36.3` | Apache-2.0 | +| [k8s.io/api](https://github.com/kubernetes/api) | `v0.37.1` | Apache-2.0 | +| [k8s.io/apiextensions-apiserver](https://github.com/kubernetes/apiextensions-apiserver) | `v0.37.0` | Apache-2.0 | +| [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) | `v0.37.1` | Apache-2.0 | +| [k8s.io/apiserver](https://github.com/kubernetes/apiserver) | `v0.37.1` | Apache-2.0 | +| [k8s.io/cli-runtime](https://github.com/kubernetes/cli-runtime) | `v0.37.0` | Apache-2.0 | +| [k8s.io/client-go](https://github.com/kubernetes/client-go) | `v0.37.1` | Apache-2.0 | +| [k8s.io/component-base](https://github.com/kubernetes/component-base) | `v0.37.1` | Apache-2.0 | | [k8s.io/klog/v2](https://github.com/kubernetes/klog) | `v2.140.0` | Apache-2.0 | -| [k8s.io/kube-aggregator](https://github.com/kubernetes/kube-aggregator) | `v0.36.3` | Apache-2.0 | -| [k8s.io/kube-openapi](https://github.com/kubernetes/kube-openapi) | `v0.0.0-20260624041617-8f3fa4921821` | Apache-2.0 | -| [k8s.io/kubectl](https://github.com/kubernetes/kubectl) | `v0.36.3` | Apache-2.0 | -| [k8s.io/metrics](https://github.com/kubernetes/metrics) | `v0.36.3` | Apache-2.0 | -| [k8s.io/streaming](https://github.com/kubernetes/streaming) | `v0.36.3` | Apache-2.0 | +| [k8s.io/kube-aggregator](https://github.com/kubernetes/kube-aggregator) | `v0.37.1` | Apache-2.0 | +| [k8s.io/kube-openapi](https://github.com/kubernetes/kube-openapi) | `v0.0.0-20260721132016-d427ff9ee9ad` | Apache-2.0 | +| [k8s.io/kubectl](https://github.com/kubernetes/kubectl) | `v0.37.0` | Apache-2.0 | +| [k8s.io/metrics](https://github.com/kubernetes/metrics) | `v0.37.0` | Apache-2.0 | +| [k8s.io/streaming](https://github.com/kubernetes/streaming) | `v0.37.1` | Apache-2.0 | | [k8s.io/utils](https://github.com/kubernetes/utils) | `v0.0.0-20260707023825-cf1189d6abe3` | Apache-2.0 | -| [mvdan.cc/sh/v3](https://mvdan.cc/sh/v3) | `v3.13.1` | BSD-3-Clause | -| [sigs.k8s.io/apiserver-network-proxy/konnectivity-client](https://sigs.k8s.io/apiserver-network-proxy/konnectivity-client) | `v0.34.0` | Apache-2.0 | -| [sigs.k8s.io/controller-runtime](https://sigs.k8s.io/controller-runtime) | `v0.24.1` | Apache-2.0 | +| [mvdan.cc/sh/v3](https://mvdan.cc/sh/v3) | `v3.14.0` | BSD-3-Clause | +| [sigs.k8s.io/apiserver-network-proxy/konnectivity-client](https://sigs.k8s.io/apiserver-network-proxy/konnectivity-client) | `v0.36.0` | Apache-2.0 | +| [sigs.k8s.io/controller-runtime](https://sigs.k8s.io/controller-runtime) | `v0.25.0` | Apache-2.0 | | [sigs.k8s.io/json](https://sigs.k8s.io/json) | `v0.0.0-20250730193827-2d320260d730` | Apache-2.0 | | [sigs.k8s.io/randfill](https://sigs.k8s.io/randfill) | `v1.0.0` | Apache-2.0 | -| [sigs.k8s.io/structured-merge-diff/v6](https://sigs.k8s.io/structured-merge-diff/v6) | `v6.3.3` | Apache-2.0 | +| [sigs.k8s.io/structured-merge-diff/v6](https://sigs.k8s.io/structured-merge-diff/v6) | `v6.4.2` | Apache-2.0 | | [sigs.k8s.io/yaml](https://sigs.k8s.io/yaml) | `v1.6.0` | Apache-2.0 | -| [tailscale.com](https://tailscale.com) | `v1.102.2` | BSD-3-Clause | +| [tailscale.com](https://tailscale.com) | `v1.102.3` | BSD-3-Clause | diff --git a/cmd/internal/internal.go b/cmd/internal/internal.go index d7366c03b..65c448396 100644 --- a/cmd/internal/internal.go +++ b/cmd/internal/internal.go @@ -15,6 +15,7 @@ func NewInternalCmd(globalFlags *flags.GlobalFlags) *cobra.Command { Hidden: true, } cmd.AddCommand(NewAgentCmd(globalFlags)) + cmd.AddCommand(NewRuntimeSupervisorCmd()) cmd.AddCommand(NewDaemonLocalCmd(globalFlags)) cmd.AddCommand(NewLogsDaemonCmd(globalFlags)) cmd.AddCommand(NewRunUserCommandsCmd(globalFlags)) diff --git a/cmd/internal/runtime_supervisor.go b/cmd/internal/runtime_supervisor.go new file mode 100644 index 000000000..43de46a68 --- /dev/null +++ b/cmd/internal/runtime_supervisor.go @@ -0,0 +1,15 @@ +package cmdinternal + +import ( + "github.com/devsy-org/devsy-runtime-sdk/supervisor" + "github.com/spf13/cobra" +) + +func NewRuntimeSupervisorCmd() *cobra.Command { + return &cobra.Command{ + Use: "runtime-supervisor", Hidden: true, + DisableFlagParsing: true, + PersistentPreRunE: func(*cobra.Command, []string) error { return nil }, + Run: func(_ *cobra.Command, args []string) { supervisor.Main(args) }, + } +} diff --git a/cmd/internal/runtime_supervisor_test.go b/cmd/internal/runtime_supervisor_test.go new file mode 100644 index 000000000..f8d0b1a33 --- /dev/null +++ b/cmd/internal/runtime_supervisor_test.go @@ -0,0 +1,27 @@ +package cmdinternal + +import ( + "testing" + + "github.com/devsy-org/devsy/cmd/flags" + "github.com/stretchr/testify/suite" +) + +type RuntimeSupervisorSuite struct{ suite.Suite } + +func TestRuntimeSupervisorSuite(t *testing.T) { suite.Run(t, new(RuntimeSupervisorSuite)) } + +func (s *RuntimeSupervisorSuite) TestDedicatedPreRun() { + parent := NewInternalCmd(&flags.GlobalFlags{}) + const configFlag = "--config" + command, args, err := parent.Find( + []string{"runtime-supervisor", "--lease", "3", configFlag, "4"}, + ) + s.Require().NoError(err) + s.Equal("runtime-supervisor", command.Name()) + s.True(command.Hidden) + s.True(command.DisableFlagParsing) + s.Equal([]string{"--lease", "3", configFlag, "4"}, args) + s.NotNil(command.PersistentPreRunE) + s.Require().NoError(command.PersistentPreRunE(command, args)) +} diff --git a/go.mod b/go.mod index 6841e6698..05deda00f 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/devsy-org/agentapi v1.0.1 github.com/devsy-org/api v1.1.0 github.com/devsy-org/apiserver v1.5.4 + github.com/devsy-org/devsy-runtime-sdk v1.2.0 github.com/devsy-org/ssh v1.2.9 github.com/distribution/reference v0.6.0 github.com/docker/cli v29.8.0+incompatible @@ -43,6 +44,8 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/handlers v1.5.2 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 + github.com/hashicorp/go-hclog v1.6.3 + github.com/hashicorp/go-plugin v1.8.0 github.com/joho/godotenv v1.5.1 github.com/moby/buildkit v0.33.0 github.com/moby/patternmatcher v0.6.1 @@ -322,6 +325,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.1-vault-7 // indirect github.com/hashicorp/vault/api v1.23.0 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.207 // indirect @@ -398,6 +402,7 @@ require ( github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.0.7 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oklog/run v1.1.0 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect diff --git a/go.sum b/go.sum index 4092c0a37..bf6aebbd4 100644 --- a/go.sum +++ b/go.sum @@ -256,6 +256,8 @@ github.com/bluesky-social/indigo v0.0.0-20240813042137-4006c0eca043 h1:927VIkxPF github.com/bluesky-social/indigo v0.0.0-20240813042137-4006c0eca043/go.mod h1:dXjdzg6bhg1JKnKuf6EBJTtcxtfHYBFEe9btxX5YeAE= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/caarlos0/env/v11 v11.4.1 h1:fYwH0sWEsBSMPG7t4e/PEfTFzrWrpjyygXyUnWiSwEw= @@ -414,6 +416,8 @@ github.com/devsy-org/api v1.1.0 h1:l7T9k7RVwatwN4lxeDTF3iN6EYmfGZgR3ZTJMDGha1M= github.com/devsy-org/api v1.1.0/go.mod h1:mAZklKdnywJYiXDReBLte/H+3m69z6G7RHB3n1lI53Q= github.com/devsy-org/apiserver v1.5.4 h1:/bEPrSRSlfii2QHxc9qAiXlQawNJfIaPLraR3bYKHxg= github.com/devsy-org/apiserver v1.5.4/go.mod h1:sDFCTjCN13wAHhno4KX0Z756gCI8ttW/c7NwTzo16K0= +github.com/devsy-org/devsy-runtime-sdk v1.2.0 h1:yQ6yJq00ZOcdn0+IjYygK7Byk9qLBuZgSD1AIiA/FRo= +github.com/devsy-org/devsy-runtime-sdk v1.2.0/go.mod h1:MiBP/fiY83DAS0TueEiJZSHYZ1vq85UR4KJTMfHIM+E= github.com/devsy-org/ssh v1.2.9 h1:KHqX1xAplGFanm0FMSAojtiC9nV/UFxUeP/5jU5quak= github.com/devsy-org/ssh v1.2.9/go.mod h1:Uff10+cSSDZk3bG07u5D9+eQ8GMGqsgE70WCUJWcHv4= github.com/devsy-org/tailscale v1.102.2 h1:9SB6htvO+HmG8alal8WGCshHapfHR7dUFRSMYiFIzIM= @@ -485,6 +489,7 @@ github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQL github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= @@ -762,6 +767,8 @@ github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs= +github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= @@ -782,6 +789,8 @@ github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/vault/api v1.23.0 h1:gXgluBsSECfRWTSW9niY2jwg2e9mMJc4WoHNv4g3h6A= github.com/hashicorp/vault/api v1.23.0/go.mod h1:zransKiB9ftp+kgY8ydjnvCU7Wk8i9L0DYWpXeMj9ko= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= @@ -846,6 +855,8 @@ github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 h1:FWpSWRD8Fb github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7/go.mod h1:BMxO138bOokdgt4UaxZiEfypcSHX0t6SIFimVP1oRfk= github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= @@ -903,6 +914,8 @@ github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -1020,6 +1033,8 @@ github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0 github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo/v2 v2.32.1 h1:6tlvcDm/3sE8lGJbZ4+d4mO3RLy24/tQWOFzVSQNIfw= @@ -1193,6 +1208,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -1523,7 +1539,9 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc 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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/hack/licenses/overrides.ndjson b/hack/licenses/overrides.ndjson index 17a7db80f..cde76c094 100644 --- a/hack/licenses/overrides.ndjson +++ b/hack/licenses/overrides.ndjson @@ -1,2 +1,3 @@ {"name":"github.com/grpc-ecosystem/go-grpc-middleware/v2","licenceFile":"LICENSE","licenceType":"Apache-2.0"} {"name":"github.com/segmentio/asm","licenceFile":"LICENSE","licenceType":"MIT-0"} +{"name":"github.com/devsy-org/devsy-runtime-sdk","licenceType":"MPL-2.0"} diff --git a/pkg/driver/external/errors.go b/pkg/driver/external/errors.go new file mode 100644 index 000000000..1e4bc5471 --- /dev/null +++ b/pkg/driver/external/errors.go @@ -0,0 +1,63 @@ +package external + +import ( + "context" + "fmt" + + "github.com/devsy-org/devsy-runtime-sdk/runtimev1" + "github.com/devsy-org/devsy/pkg/log" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// RuntimeError preserves canonical and runtime error categories without keeping +// secret-bearing backend diagnostics in a caller-visible error. +type RuntimeError struct { + Operation string + Code codes.Code + Category runtimev1.RuntimeErrorCode + Retryable bool + Message string +} + +func (e *RuntimeError) Error() string { + return fmt.Sprintf("external runtime %s: %s", e.Operation, e.Message) +} + +func (e *RuntimeError) GRPCStatus() *status.Status { return status.New(e.Code, e.Message) } + +func (h *Host) operationError(ctx context.Context, operation string, err error) error { + if err == nil { + return nil + } + if ctx.Err() != nil { + return fmt.Errorf("external runtime %s: %w", operation, ctx.Err()) + } + rpcStatus, ok := status.FromError(err) + if !ok { + return fmt.Errorf("external runtime %s: %s", operation, h.redactor.Redact(err.Error())) + } + result := &RuntimeError{ + Operation: operation, Code: rpcStatus.Code(), + Message: h.redactor.Redact(rpcStatus.Message()), + } + for _, detail := range rpcStatus.Details() { + runtimeError, ok := detail.(*runtimev1.RuntimeError) + if !ok { + continue + } + if runtimeError.Message != "" { + result.Message = h.redactor.Redact(runtimeError.Message) + } + result.Category, result.Retryable = runtimeError.Code, runtimeError.Retryable + if runtimeError.RuntimeMessage != "" { + log.Debugf( + "External runtime %s diagnostic: %s", + operation, + h.redactor.Redact(runtimeError.RuntimeMessage), + ) + } + break + } + return result +} diff --git a/pkg/driver/external/host.go b/pkg/driver/external/host.go new file mode 100644 index 000000000..6a209b719 --- /dev/null +++ b/pkg/driver/external/host.go @@ -0,0 +1,159 @@ +// Package external hosts trusted Runtime Protocol v1 executables. +package external + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "slices" + "strings" + "time" + + "github.com/devsy-org/devsy-runtime-sdk/runtimev1" + "github.com/devsy-org/devsy/pkg/agent" + "github.com/devsy-org/devsy/pkg/provider" + "github.com/devsy-org/devsy/pkg/secrets" + "google.golang.org/protobuf/proto" +) + +const startupTimeout = 15 * time.Second + +// Host opens an owned plugin session per operation. It is not yet registered +// as a workspace driver; Exec and Logs are added in the next integration stage. +type Host struct { + config provider.ProviderAgentConfig + binariesDir string + supervisorBinary string + supervisorArgs []string + environment []string + redactor *secrets.Redactor + info *runtimev1.InfoResponse + timeout time.Duration +} + +// New discovers a prepared runtime without downloading it and negotiates Info. +// The running Devsy executable supplies the trusted supervisor entry point. +func New(ctx context.Context, workspace *provider.AgentWorkspaceInfo) (*Host, error) { + if workspace == nil { + return nil, errors.New("external runtime workspace is missing") + } + binariesDir, err := agent.GetAgentBinariesDirFromWorkspaceDir(workspace.Origin) + if err != nil { + return nil, fmt.Errorf("resolve runtime binaries directory: %w", err) + } + executable, err := os.Executable() + if err != nil { + return nil, fmt.Errorf("resolve Devsy supervisor executable: %w", err) + } + environment := provider.ToEnvironment( + workspace.Workspace, + workspace.Machine, + workspace.Options, + nil, + ) + return newHost(ctx, hostOptions{ + config: workspace.Agent, + directory: binariesDir, + supervisorBinary: executable, + supervisorArgs: []string{ + "internal", + "runtime-supervisor", + }, + environment: environment, + timeout: startupTimeout, + }) +} + +type hostOptions struct { + config provider.ProviderAgentConfig + directory, supervisorBinary string + supervisorArgs, environment []string + timeout time.Duration +} + +func newHost(ctx context.Context, options hostOptions) (*Host, error) { + h := &Host{ + config: options.config, + binariesDir: options.directory, + supervisorBinary: options.supervisorBinary, + supervisorArgs: slices.Clone(options.supervisorArgs), + environment: slices.Clone(options.environment), + timeout: options.timeout, + redactor: secrets.NewEnvironmentRedactor(options.environment), + } + // Freeze provider declarations so later caller mutation cannot change identity. + h.config.External.Args = slices.Clone(options.config.External.Args) + h.config.Binaries = make(map[string][]*provider.ProviderBinary, len(options.config.Binaries)) + for key, locations := range options.config.Binaries { + for _, binary := range locations { + if binary == nil { + h.config.Binaries[key] = append(h.config.Binaries[key], nil) + continue + } + copy := *binary + h.config.Binaries[key] = append(h.config.Binaries[key], ©) + } + } + infoContext, cancel := context.WithTimeout(ctx, options.timeout) + defer cancel() + err := h.call(infoContext, "Info", func(client runtimev1.RuntimeDriverClient) error { + info, err := client.Info(infoContext, &runtimev1.InfoRequest{}) + if err != nil { + return err + } + if err := runtimev1.ValidateInfo(info); err != nil { + return err + } + h.info = proto.Clone(info).(*runtimev1.InfoResponse) + return nil + }) + if err != nil { + return nil, err + } + return h, nil +} + +// Info returns a copy of the negotiated capabilities. +func (h *Host) Info() *runtimev1.InfoResponse { return proto.Clone(h.info).(*runtimev1.InfoResponse) } + +func (h *Host) executable() (string, error) { + path, err := provider.ResolveExternalRuntimeBinary(h.config, h.binariesDir) + if err != nil { + return "", err + } + for _, binary := range h.config.Binaries[h.config.External.Binary] { + if binary.OS != runtime.GOOS || binary.Arch != runtime.GOARCH || + filepath.IsAbs(binary.Path) { + continue + } + if err := containRealPath( + filepath.Join(h.binariesDir, strings.ToLower(h.config.External.Binary)), + path, + ); err != nil { + return "", err + } + } + return path, nil +} + +func containRealPath(directory, path string) error { + root, err := filepath.EvalSymlinks(directory) + if err != nil { + return err + } + resolved, err := filepath.EvalSymlinks(path) + if err != nil { + return err + } + relative, err := filepath.Rel(root, resolved) + if err != nil { + return err + } + if !filepath.IsLocal(relative) { + return errors.New("external runtime symlink escapes its binary directory") + } + return nil +} diff --git a/pkg/driver/external/host_test.go b/pkg/driver/external/host_test.go new file mode 100644 index 000000000..5d52071c8 --- /dev/null +++ b/pkg/driver/external/host_test.go @@ -0,0 +1,314 @@ +package external + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "testing" + "time" + + "github.com/devsy-org/devsy-runtime-sdk/conformance/fake" + "github.com/devsy-org/devsy-runtime-sdk/runtimev1" + "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/driver" + "github.com/devsy-org/devsy/pkg/log" + "github.com/devsy-org/devsy/pkg/provider" + "github.com/devsy-org/devsy/pkg/secrets" + "github.com/shirou/gopsutil/v4/process" + "github.com/stretchr/testify/suite" + "go.uber.org/zap/zapcore" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const ( + fixtureKey = "RUNTIME" + fixtureImage = "fixture" + fixtureWorkspace = "workspace" + delayedMode = "delayed" + workspaceCanary = "workspace-canary-value" +) + +type HostSuite struct { + suite.Suite + binary string + checksum string +} + +func TestHostSuite(t *testing.T) { suite.Run(t, new(HostSuite)) } + +func (s *HostSuite) SetupSuite() { + s.binary = filepath.Join(s.T().TempDir(), "runtime space é") + if runtime.GOOS == "windows" { + s.binary += ".exe" + } + // #nosec G204 -- Builds a checked-in fixture into a private test directory. + command := exec.Command("go", "build", "-o", s.binary, "./internal/testfixture") + output, err := command.CombinedOutput() + s.Require().NoError(err, string(output)) + // #nosec G304 -- Executable is built in this test's private temporary directory. + data, err := os.ReadFile(s.binary) + s.Require().NoError(err) + sum := sha256.Sum256(data) + s.checksum = hex.EncodeToString(sum[:]) +} + +func (s *HostSuite) TestLifecycleAcrossFreshProcesses() { + host := s.host(fake.Normal) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + s.Require().NoError(host.Preflight(ctx, driver.PreflightOptions{DisableAutoStart: true})) + s.Require().NoError(host.ProvisioningPreflight(ctx)) + found, err := host.FindDevContainer(ctx, fixtureWorkspace) + s.Require().NoError(err) + s.Nil(found) + s.Require(). + NoError(host.RunImage(ctx, &runtimev1.RunImageRequest{WorkspaceId: fixtureWorkspace, Image: fixtureImage})) + found, err = host.FindDevContainer(ctx, fixtureWorkspace) + s.Require().NoError(err) + s.Equal(config.ContainerStatusRunning, found.State.Status) + architecture, err := host.TargetArchitecture(ctx, fixtureWorkspace) + s.Require().NoError(err) + s.Contains([]string{"amd64", "arm64"}, architecture) + s.Require().NoError(host.StopDevContainer(ctx, fixtureWorkspace)) + found, err = host.FindDevContainer(ctx, fixtureWorkspace) + s.Require().NoError(err) + s.Equal(config.ContainerStatusExited, found.State.Status) + s.Require().NoError(host.StartDevContainer(ctx, fixtureWorkspace)) + s.Require().NoError(host.DeleteDevContainer(ctx, fixtureWorkspace)) + s.Require().NoError(host.DeleteDevContainer(ctx, fixtureWorkspace)) +} + +func (s *HostSuite) TestNegotiationFailures() { + for _, mode := range []string{ + fake.IncompatibleVersion, fake.MalformedInfo, + fake.CrashBeforeHandshake, fake.CrashAfterHandshake, + } { + s.Run(mode, func() { + _, err := s.newHost(context.Background(), mode, startupTimeout) + s.Error(err) + }) + } +} + +func (s *HostSuite) TestCancellationDuringHandshake() { + ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond) + defer cancel() + started := time.Now() + _, err := s.newHost(ctx, delayedMode, startupTimeout) + s.ErrorIs(err, context.DeadlineExceeded) + s.Less(time.Since(started), 5*time.Second) +} + +func (s *HostSuite) TestStartupTimeout() { + started := time.Now() + _, err := s.newHost(context.Background(), delayedMode, 200*time.Millisecond) + s.Error(err) + s.Less(time.Since(started), 5*time.Second) +} + +func (s *HostSuite) TestInfoNegotiationTimeout() { + _, err := s.newHost(context.Background(), "block-info", 500*time.Millisecond) + s.ErrorIs(err, context.DeadlineExceeded) +} + +func (s *HostSuite) TestStructuredFailureAndCapabilitiesCopy() { + host := s.host(fake.FailPreflight) + err := host.Preflight(context.Background(), driver.PreflightOptions{}) + s.Equal(codes.Unavailable, status.Code(err)) + var runtimeError *RuntimeError + s.True(errors.As(err, &runtimeError)) + s.Equal(runtimev1.RuntimeErrorCode_RUNTIME_ERROR_CODE_UNAVAILABLE, runtimeError.Category) + info := host.Info() + info.Capabilities.RecreateMode = runtimev1.RecreateMode_RECREATE_MODE_UNSPECIFIED + s.NotEqual(info.Capabilities.RecreateMode, host.Info().Capabilities.RecreateMode) +} + +func (s *HostSuite) TestRechecksExecutableBeforeEachOperation() { + host := s.host(fake.Normal) + host.config.Binaries[fixtureKey][0].Checksum = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + _, err := host.FindDevContainer(context.Background(), fixtureWorkspace) + s.ErrorContains(err, "checksum") +} + +func (s *HostSuite) TestRejectsInvalidIntentBeforeLaunch() { + host := s.host(fake.Normal) + s.Error(host.RunImage(context.Background(), nil)) + s.Error(host.RunImage(context.Background(), &runtimev1.RunImageRequest{ + WorkspaceId: fixtureWorkspace, Image: fixtureImage, + Mounts: []*runtimev1.Mount{{Type: runtimev1.MountType_MOUNT_TYPE_UNSPECIFIED}}, + })) + s.Error(host.StartDevContainer(context.Background(), "")) + _, err := host.FindDevContainer(context.Background(), "") + s.Error(err) +} + +func (s *HostSuite) TestContainerConversionRejectsInvalidState() { + for _, input := range []*runtimev1.ContainerDetails{ + nil, {Id: "id"}, {Id: "id", State: &runtimev1.ContainerState{Status: "unknown"}}, + } { + _, err := convertContainer(input) + s.Error(err) + } +} + +func (s *HostSuite) TestErrorRedactionPreservesStatus() { + host := s.host(fake.Normal) + host.redactor = secrets.NewRedactor([]string{"SECRET=private-canary-value"}) + err := host.operationError( + context.Background(), + "Start", + status.Error(codes.PermissionDenied, "denied private-canary-value"), + ) + s.Equal(codes.PermissionDenied, status.Code(err)) + s.NotContains(err.Error(), "private-canary-value") +} + +func (s *HostSuite) TestCancellationReapsUncooperativeChild() { + host := s.host("blocked") + directory := host.config.External.Args[3] + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + result := make(chan error, 1) + go func() { result <- host.Preflight(ctx, driver.PreflightOptions{}) }() + s.Require().Eventually(func() bool { + _, err := os.Stat(filepath.Join(directory, "ready")) + return err == nil + }, 5*time.Second, 10*time.Millisecond) + cancel() + select { + case err := <-result: + s.ErrorIs(err, context.Canceled) + case <-time.After(5 * time.Second): + s.FailNow("runtime cancellation did not return") + } + s.assertReaped(directory) +} + +func (s *HostSuite) TestInheritedEnvironmentAndTransportPrecedence() { + s.T().Setenv("FIXTURE_SETTING", "inherited") + s.T().Setenv("FIXTURE_EMPTY", "") + s.T().Setenv("FIXTURE_TOKEN", "private-canary-value") + s.T().Setenv("DEVSY_RUNTIME_PLUGIN", "provider-override") + host := s.host("environment") + s.Equal("inherited", host.Info().RuntimeVersion) +} + +func (s *HostSuite) TestSplitDiagnosticRedaction() { + var output bytes.Buffer + writer := &diagnosticWriter{ + writer: &output, + stream: secrets.NewStreamingRedactor( + secrets.NewRedactor([]string{"TOKEN=private-canary-value"}), + ), + } + _, err := writer.Write([]byte("diagnostic private-canary-")) + s.Require().NoError(err) + _, err = writer.Write([]byte("value tail\n")) + s.Require().NoError(err) + writer.close() + s.NotContains(output.String(), "private-canary-value") + s.Contains(output.String(), "diagnostic") + s.Contains(output.String(), "tail") +} + +func (s *HostSuite) TestWorkspaceEnvironmentRedactedFromRuntimeErrors() { + host := s.host("env-error") + observed := log.InitTestObserved(s.T(), zapcore.DebugLevel) + err := host.RunImage(context.Background(), &runtimev1.RunImageRequest{ + WorkspaceId: fixtureWorkspace, + Image: "fixture", + Environment: map[string]string{"CUSTOM_VALUE": workspaceCanary}, + }) + s.Require().Error(err) + s.NotContains(err.Error(), workspaceCanary) + s.Equal(codes.Unavailable, status.Code(err)) + var runtimeError *RuntimeError + s.True(errors.As(err, &runtimeError)) + s.True(runtimeError.Retryable) + for _, entry := range observed.All() { + s.NotContains(entry.Message, workspaceCanary) + } +} + +func (s *HostSuite) TestRealPathContainment() { + root := s.T().TempDir() + outside := filepath.Join(s.T().TempDir(), "outside") + s.Require().NoError(os.WriteFile(outside, nil, 0o600)) + inside := filepath.Join(root, "inside") + s.Require().NoError(os.WriteFile(inside, nil, 0o600)) + s.NoError(containRealPath(root, inside)) + link := filepath.Join(root, "escape") + err := os.Symlink(outside, link) + if err != nil && runtime.GOOS == "windows" { + s.T().Skip("symlink creation unavailable on this Windows host") + } + s.Require().NoError(err) + s.ErrorContains(containRealPath(root, link), "symlink escapes") +} + +func (s *HostSuite) TestMissingWorkspace() { + _, err := New(context.Background(), nil) + s.ErrorContains(err, "workspace is missing") +} + +func (s *HostSuite) assertReaped(directory string) { + for _, kind := range []string{"supervisor", "plugin", "child"} { + // #nosec G304 -- Fixture PID records live in a private test-owned temporary directory. + data, err := os.ReadFile(filepath.Join(directory, kind+"-pids")) + if os.IsNotExist(err) { + continue + } + s.Require().NoError(err) + for value := range strings.FieldsSeq(string(data)) { + pid, err := strconv.ParseInt(value, 10, 32) + s.Require().NoError(err) + s.Eventually(func() bool { + // #nosec G115 -- ParseInt above explicitly limits the value to 32 bits. + exists, err := process.PidExists(int32(pid)) + return err == nil && !exists + }, 5*time.Second, 10*time.Millisecond, "fixture %s PID %d survived session cleanup", kind, pid) + } + } +} + +func (s *HostSuite) host(mode string) *Host { + host, err := s.newHost(context.Background(), mode, startupTimeout) + s.Require().NoError(err) + return host +} + +func (s *HostSuite) newHost( + ctx context.Context, + mode string, + timeout time.Duration, +) (*Host, error) { + directory := s.T().TempDir() + s.T().Cleanup(func() { s.assertReaped(directory) }) + args := []string{"--mode", mode, "--state-dir", directory} + if mode == delayedMode { + args = append(args, "--delay", "1m") + } + agent := provider.ProviderAgentConfig{ + Driver: provider.ExternalDriver, + External: provider.ProviderExternalDriverConfig{Binary: fixtureKey, Args: args}, + Binaries: map[string][]*provider.ProviderBinary{ + fixtureKey: { + {OS: runtime.GOOS, Arch: runtime.GOARCH, Path: s.binary, Checksum: s.checksum}, + }, + }, + } + return newHost(ctx, hostOptions{ + config: agent, directory: s.T().TempDir(), + supervisorBinary: s.binary, supervisorArgs: []string{"supervise", directory}, + environment: os.Environ(), timeout: timeout, + }) +} diff --git a/pkg/driver/external/internal/testfixture/main.go b/pkg/driver/external/internal/testfixture/main.go new file mode 100644 index 000000000..dc53e7b6d --- /dev/null +++ b/pkg/driver/external/internal/testfixture/main.go @@ -0,0 +1,156 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "time" + + "github.com/devsy-org/devsy-runtime-sdk/conformance/fake" + "github.com/devsy-org/devsy-runtime-sdk/runtimev1" + "github.com/devsy-org/devsy-runtime-sdk/server" + "github.com/devsy-org/devsy-runtime-sdk/supervisor" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func main() { + if len(os.Args) > 1 && os.Args[1] == "supervise" { + recordPID(os.Args[2], "supervisor") + supervisor.Main(os.Args[3:]) + return + } + if len(os.Args) > 1 && os.Args[1] == "child" { + recordPID(os.Args[2], "child") + for { + time.Sleep(time.Hour) + } + } + mode := flag.String("mode", fake.Normal, "fixture mode") + state := flag.String("state-dir", "", "state directory") + delay := flag.Duration("delay", 0, "handshake delay") + flag.Parse() + recordPID(*state, "plugin") + serve(*mode, *state, *delay) +} + +func serve(mode, state string, delay time.Duration) { + if mode == fake.CrashBeforeHandshake { + os.Exit(22) + } + if delay > 0 { + time.Sleep(delay) + } + fixtureMode := mode + if fixtureMode == "blocked" || fixtureMode == "environment" || fixtureMode == "block-info" || + fixtureMode == "env-error" { + fixtureMode = fake.Normal + } + runtime, err := fake.New(fake.Config{StateDir: state, Mode: fixtureMode}) + if err != nil { + panic(err) + } + server.Serve(&fixture{Driver: runtime, mode: mode, directory: state}) +} + +type fixture struct { + *fake.Driver + mode, directory string +} + +func (f *fixture) Info( + ctx context.Context, + request *runtimev1.InfoRequest, +) (*runtimev1.InfoResponse, error) { + if f.mode == "block-info" { + <-ctx.Done() + return nil, ctx.Err() + } + response, err := f.Driver.Info(ctx, request) + if err != nil { + return nil, err + } + if f.mode == "environment" { + if os.Getenv("DEVSY_RUNTIME_PLUGIN") != "devsy-runtime-v1" || + os.Getenv("FIXTURE_EMPTY") != "" || + os.Getenv("FIXTURE_TOKEN") != "private-canary-value" { + return nil, fmt.Errorf("fixture environment was not preserved") + } + response.RuntimeVersion = os.Getenv("FIXTURE_SETTING") + } + return response, nil +} + +func (f *fixture) Preflight( + ctx context.Context, + request *runtimev1.PreflightRequest, +) (*runtimev1.PreflightResponse, error) { + if f.mode != "blocked" { + return f.Driver.Preflight(ctx, request) + } + executable, err := os.Executable() + if err != nil { + return nil, err + } + // #nosec G204 -- Relaunches this trusted test fixture; no shell or PATH lookup. + child := exec.Command(executable, "child", f.directory) + if err := child.Start(); err != nil { + return nil, err + } + go func() { _ = child.Wait() }() + for { + if _, err := os.Stat(filepath.Join(f.directory, "child-pids")); err == nil { + break + } + time.Sleep(time.Millisecond) + } + if err := os.WriteFile(filepath.Join(f.directory, "ready"), nil, 0o600); err != nil { + return nil, err + } + // #nosec G118 -- Deliberately ignores cancellation to prove supervisor forced cleanup. + for { + time.Sleep(time.Hour) + } +} + +func (f *fixture) RunImage( + ctx context.Context, + request *runtimev1.RunImageRequest, +) (*runtimev1.RunImageResponse, error) { + if f.mode != "env-error" { + return f.Driver.RunImage(ctx, request) + } + secret := request.Environment["CUSTOM_VALUE"] + failure, err := status.New(codes.Unavailable, "raw "+secret). + WithDetails(&runtimev1.RuntimeError{ + Code: runtimev1.RuntimeErrorCode_RUNTIME_ERROR_CODE_UNAVAILABLE, + Message: "failure " + secret, RuntimeMessage: "backend " + secret, Retryable: true, + }) + if err != nil { + return nil, err + } + return nil, failure.Err() +} + +func recordPID(directory, kind string) { + // #nosec G304 G703 -- Fixture writes only into the test-owned temporary directory. + file, err := os.OpenFile( + filepath.Join(directory, kind+"-pids"), + os.O_APPEND|os.O_CREATE|os.O_WRONLY, + 0o600, + ) + if err != nil { + panic(err) + } + _, err = fmt.Fprintln(file, strconv.Itoa(os.Getpid())) + if err != nil { + panic(err) + } + if err := file.Close(); err != nil { + panic(err) + } +} diff --git a/pkg/driver/external/lifecycle.go b/pkg/driver/external/lifecycle.go new file mode 100644 index 000000000..7c739e2b0 --- /dev/null +++ b/pkg/driver/external/lifecycle.go @@ -0,0 +1,211 @@ +package external + +import ( + "context" + "errors" + "fmt" + "slices" + + "github.com/devsy-org/devsy-runtime-sdk/runtimev1" + "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/driver" + "github.com/devsy-org/devsy/pkg/secrets" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (h *Host) Preflight(ctx context.Context, options driver.PreflightOptions) error { + return h.call(ctx, "Preflight", func(client runtimev1.RuntimeDriverClient) error { + _, err := client.Preflight( + ctx, + &runtimev1.PreflightRequest{DisableAutoStart: options.DisableAutoStart}, + ) + return err + }) +} + +func (h *Host) ProvisioningPreflight(ctx context.Context) error { + if !h.info.Capabilities.ProvisioningPreflight { + return ctx.Err() + } + return h.call(ctx, "ProvisioningPreflight", func(client runtimev1.RuntimeDriverClient) error { + _, err := client.ProvisioningPreflight(ctx, &runtimev1.ProvisioningPreflightRequest{}) + return err + }) +} + +func (h *Host) FindDevContainer( + ctx context.Context, + workspaceID string, +) (*config.ContainerDetails, error) { + if workspaceID == "" { + return nil, errors.New("workspace ID is required") + } + var container *config.ContainerDetails + err := h.call(ctx, "Find", func(client runtimev1.RuntimeDriverClient) error { + response, err := client.Find(ctx, &runtimev1.FindRequest{WorkspaceId: workspaceID}) + if err != nil { + return err + } + if response == nil { + return errors.New("runtime Find response is missing") + } + if !response.Found { + return nil + } + container, err = convertContainer(response.Container) + if container != nil { + container.State.Error = h.redactor.Redact(container.State.Error) + } + return err + }) + return container, err +} + +func (h *Host) TargetArchitecture(ctx context.Context, workspaceID string) (string, error) { + if workspaceID == "" { + return "", errors.New("workspace ID is required") + } + var architecture string + err := h.call(ctx, "TargetArchitecture", func(client runtimev1.RuntimeDriverClient) error { + response, err := client.TargetArchitecture( + ctx, + &runtimev1.TargetArchitectureRequest{WorkspaceId: workspaceID}, + ) + if err != nil { + return err + } + architecture = response.GetArchitecture() + if architecture != "amd64" && architecture != "arm64" { + return fmt.Errorf("invalid runtime target architecture %q", architecture) + } + return nil + }) + return architecture, err +} + +// RunImage accepts resolved protocol intent; driver conversion is a later stage. +func (h *Host) RunImage(ctx context.Context, request *runtimev1.RunImageRequest) error { + if err := h.validateRunImage(request); err != nil { + return err + } + operation := *h + environment := make([]string, 0, len(request.Environment)) + for key, value := range request.Environment { + environment = append(environment, key+"="+value) + } + operation.redactor = secrets.Combine(h.redactor, secrets.NewRedactor(environment)) + return operation.call(ctx, "RunImage", func(client runtimev1.RuntimeDriverClient) error { + _, err := client.RunImage(ctx, request) + return err + }) +} + +func (h *Host) validateRunImage(request *runtimev1.RunImageRequest) error { + if request == nil || request.WorkspaceId == "" || request.Image == "" { + return errors.New("runtime RunImage requires workspace ID and image") + } + mounts := slices.Clone(request.Mounts) + if request.WorkspaceMount != nil { + mounts = append(mounts, request.WorkspaceMount) + } + for _, mount := range mounts { + if mount == nil { + return errors.New("runtime RunImage mount is missing") + } + if !slices.Contains(h.info.Capabilities.MountTypes, mount.Type) { + return fmt.Errorf("runtime does not support requested mount type %s", mount.Type) + } + } + return nil +} + +func (h *Host) StartDevContainer(ctx context.Context, workspaceID string) error { + if workspaceID == "" { + return errors.New("workspace ID is required") + } + return h.call(ctx, "Start", func(client runtimev1.RuntimeDriverClient) error { + _, err := client.Start(ctx, &runtimev1.StartRequest{WorkspaceId: workspaceID}) + return err + }) +} + +func (h *Host) StopDevContainer(ctx context.Context, workspaceID string) error { + if workspaceID == "" { + return errors.New("workspace ID is required") + } + return h.call(ctx, "Stop", func(client runtimev1.RuntimeDriverClient) error { + _, err := client.Stop(ctx, &runtimev1.StopRequest{WorkspaceId: workspaceID}) + return err + }) +} + +func (h *Host) DeleteDevContainer(ctx context.Context, workspaceID string) error { + if workspaceID == "" { + return errors.New("workspace ID is required") + } + return h.call(ctx, "Delete", func(client runtimev1.RuntimeDriverClient) error { + _, err := client.Delete(ctx, &runtimev1.DeleteRequest{WorkspaceId: workspaceID}) + if status.Code(err) == codes.NotFound { + return nil + } + return err + }) +} + +func convertContainer(source *runtimev1.ContainerDetails) (*config.ContainerDetails, error) { + if source == nil || source.Id == "" || source.State == nil { + return nil, errors.New("runtime Find requires container ID and state") + } + state, err := convertState(source.State.Status) + if err != nil { + return nil, err + } + result := &config.ContainerDetails{ + ID: source.Id, Created: source.CreatedAt, + State: config.ContainerDetailsState{ + Status: state, StartedAt: source.State.StartedAt, + ExitCode: int(source.State.ExitCode), Error: source.State.Error, + }, + } + if source.Config != nil { + result.Config = config.ContainerDetailsConfig{ + Labels: source.Config.Labels, + WorkingDir: source.Config.WorkingDir, User: source.Config.User, + } + } + result.Mounts, err = convertMounts(source.Mounts) + if err != nil { + return nil, err + } + return result, nil +} + +func convertMounts(source []*runtimev1.ContainerMount) ([]config.ContainerMount, error) { + result := make([]config.ContainerMount, 0, len(source)) + for _, mount := range source { + if mount == nil { + return nil, errors.New("runtime container mount is missing") + } + result = append( + result, + config.ContainerMount{ + Type: mount.Type, + Source: mount.Source, + Destination: mount.Destination, + }, + ) + } + return result, nil +} + +func convertState(status string) (config.ContainerStatus, error) { + switch config.ToContainerStatus(status) { + case config.ContainerStatusRunning: + return config.ContainerStatusRunning, nil + case "stopped": + return config.ContainerStatusExited, nil + default: + return "", fmt.Errorf("invalid runtime container state %q", status) + } +} diff --git a/pkg/driver/external/session.go b/pkg/driver/external/session.go new file mode 100644 index 000000000..5171cb2d8 --- /dev/null +++ b/pkg/driver/external/session.go @@ -0,0 +1,119 @@ +package external + +import ( + "context" + "fmt" + "io" + "os/exec" + "slices" + "sync" + "time" + + sdkplugin "github.com/devsy-org/devsy-runtime-sdk/plugin" + "github.com/devsy-org/devsy-runtime-sdk/runtimev1" + "github.com/devsy-org/devsy-runtime-sdk/supervisor" + "github.com/devsy-org/devsy/pkg/log" + "github.com/devsy-org/devsy/pkg/secrets" + "github.com/hashicorp/go-hclog" + hplugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/go-plugin/runner" +) + +func (h *Host) call( + ctx context.Context, + operation string, + rpc func(runtimev1.RuntimeDriverClient) error, +) error { + if err := ctx.Err(); err != nil { + return err + } + binary, err := h.executable() + if err != nil { + return fmt.Errorf("external runtime %s: %w", operation, err) + } + output := log.Writer(log.LevelDebug) + diagnostic := &diagnosticWriter{ + writer: output, + stream: secrets.NewStreamingRedactor(h.redactor), + } + defer func() { diagnostic.close(); _ = output.Close() }() + owned := supervisor.Runner(supervisor.Options{ + SupervisorBinary: h.supervisorBinary, SupervisorArgs: h.supervisorArgs, + RuntimeBinary: binary, Args: h.config.External.Args, + Env: append(slices.Clone(h.environment), h.config.External.Binary+"="+binary), + }) + client := hplugin.NewClient(&hplugin.ClientConfig{ + HandshakeConfig: sdkplugin.Handshake(), + VersionedPlugins: map[int]hplugin.PluginSet{ + sdkplugin.ProtocolVersion: sdkplugin.ClientPlugins(), + }, + AllowedProtocols: []hplugin.Protocol{hplugin.ProtocolGRPC}, + StartTimeout: h.timeout, + Logger: hclog.New(&hclog.LoggerOptions{Output: diagnostic, Level: hclog.Debug}), + Stderr: diagnostic, + RunnerFunc: func(logger hclog.Logger, command *exec.Cmd, socketDir string) (runner.Runner, error) { + process, err := owned(logger, command, socketDir) + if err != nil { + return nil, err + } + return &contextRunner{Runner: process, caller: ctx}, nil + }, + }) + defer client.Kill() + started := time.Now() + transport, err := client.Client() + if err != nil { + return h.operationError(ctx, operation, err) + } + instance, err := transport.Dispense(sdkplugin.Name) + if err != nil { + return h.operationError(ctx, operation, err) + } + runtimeClient, ok := instance.(runtimev1.RuntimeDriverClient) + if !ok { + return fmt.Errorf("external runtime %s: invalid gRPC client", operation) + } + log.Debugf("External runtime %s startup completed in %s", operation, time.Since(started)) + return h.operationError(ctx, operation, rpc(runtimeClient)) +} + +// go-plugin holds its client lock throughout handshake, so Client.Kill cannot +// interrupt that phase. Cancel the leased runner directly instead. +type contextRunner struct { + runner.Runner + caller context.Context +} + +func (r *contextRunner) Start(ctx context.Context) error { + bounded, cancel := context.WithCancel(ctx) + stop := context.AfterFunc(r.caller, cancel) + defer stop() + defer cancel() + if err := r.Runner.Start(bounded); err != nil { + return err + } + stopOwnership := context.AfterFunc(r.caller, func() { _ = r.Kill(context.Background()) }) + // #nosec G118 -- Reaping must outlive caller cancellation; the lease is closed by the callback. + go func() { _ = r.Wait(context.Background()); stopOwnership() }() + return nil +} + +type diagnosticWriter struct { + mu sync.Mutex + writer io.Writer + stream *secrets.StreamingRedactor +} + +func (w *diagnosticWriter) Write(data []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + text := w.stream.RedactChunk(string(data)) + _, err := io.WriteString(w.writer, text) + return len(data), err +} + +func (w *diagnosticWriter) close() { + w.mu.Lock() + defer w.mu.Unlock() + _, _ = io.WriteString(w.writer, w.stream.Flush()) +} diff --git a/sites/docs-devsy-sh/content/docs/developing-providers/driver.mdx b/sites/docs-devsy-sh/content/docs/developing-providers/driver.mdx index 778ded779..cee4d02c0 100644 --- a/sites/docs-devsy-sh/content/docs/developing-providers/driver.mdx +++ b/sites/docs-devsy-sh/content/docs/developing-providers/driver.mdx @@ -57,6 +57,31 @@ not an image-building capability in the runtime plugin. For the protocol and SDK contract, see [Runtime Protocol v1](./runtime-protocol.mdx). +### Host adapter progress + +The internal host now negotiates Runtime Protocol v1 Info and performs preflight, +Find, TargetArchitecture, RunImage, Start, Stop, and Delete calls. Each operation +uses a fresh plugin process owned by the SDK supervisor, exposed through a +hidden helper in the running Devsy executable. Cancellation also covers startup +and terminates the leased process tree. This is a prerequisite implementation; +Exec, Logs, and workspace driver registration remain separate stages. + +The host inherits its environment for compatibility with proxy, certificate, +HOME/XDG, Docker, and runtime CLI settings. Provider environment overrides and +the verified runtime binary key are forwarded; plugin transport metadata keeps +precedence. Arguments are passed literally without shell expansion, and +text diagnostics are redacted before logging. + +Before every operation the host rechecks the prepared runtime's checksum and +permissions. Relative declarations also reject symlink targets outside their +binary directory; explicit absolute declarations retain their documented +behavior. These checks do not provide a filesystem sandbox or atomic +verification-to-execution guarantee. The runtime and its directories must be +trusted. Unix command descendants must remain in the supervised process group +and retain signalable privileges; detached or elevated services need their own +owner. Real-runtime compatibility and startup measurements against a complete +workspace launch remain gates before runtime cutover or session reuse. + ## Docker Driver The Docker driver is the default driver that Devsy uses to deploy the workspace container. From e2b762fba05b88c6f46d165a7a2e9f59158dcf62 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 5 Oct 2026 18:27:03 -0600 Subject: [PATCH 2/3] style(driver): avoid shadowing built-in copy --- pkg/driver/external/host.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/driver/external/host.go b/pkg/driver/external/host.go index 6a209b719..70c8e76c7 100644 --- a/pkg/driver/external/host.go +++ b/pkg/driver/external/host.go @@ -93,8 +93,8 @@ func newHost(ctx context.Context, options hostOptions) (*Host, error) { h.config.Binaries[key] = append(h.config.Binaries[key], nil) continue } - copy := *binary - h.config.Binaries[key] = append(h.config.Binaries[key], ©) + snapshot := *binary + h.config.Binaries[key] = append(h.config.Binaries[key], &snapshot) } } infoContext, cancel := context.WithTimeout(ctx, options.timeout) From e6444a63c61ce80b219d33ffc685441a110e80ea Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 5 Oct 2026 19:31:34 -0600 Subject: [PATCH 3/3] test(driver): exercise production runtime helper launch --- pkg/driver/external/production_test.go | 79 ++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 pkg/driver/external/production_test.go diff --git a/pkg/driver/external/production_test.go b/pkg/driver/external/production_test.go new file mode 100644 index 000000000..1903b065f --- /dev/null +++ b/pkg/driver/external/production_test.go @@ -0,0 +1,79 @@ +package external_test + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/devsy-org/devsy/cmd" + "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/driver" + "github.com/devsy-org/devsy/pkg/driver/external" + "github.com/devsy-org/devsy/pkg/provider" + "github.com/stretchr/testify/suite" +) + +func TestMain(m *testing.M) { + // New launches os.Executable; child invocations must use the same CLI entry + // point as main.go rather than the fixture's substitute supervisor command. + if len(os.Args) > 2 && os.Args[1] == "internal" && os.Args[2] == "runtime-supervisor" { + cmd.Execute() + return + } + os.Exit(m.Run()) +} + +type ProductionHostSuite struct{ suite.Suite } + +func TestProductionHostSuite(t *testing.T) { suite.Run(t, new(ProductionHostSuite)) } + +func (s *ProductionHostSuite) TestPreparedRuntimeThroughCLIHelper() { + s.T().Setenv(config.EnvHome, s.T().TempDir()) + origin := s.T().TempDir() + directory := filepath.Join(origin, "binaries", "runtime") + s.Require().NoError(os.MkdirAll(directory, 0o700)) + name := "runtime space é" + if runtime.GOOS == "windows" { + name += ".exe" + } + binary := filepath.Join(directory, name) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + // #nosec G204 -- Builds the checked-in runtime fixture in a private test directory. + build := exec.CommandContext(ctx, "go", "build", "-o", binary, "./internal/testfixture") + output, err := build.CombinedOutput() + s.Require().NoError(err, string(output)) + // #nosec G304 -- Reads only the executable just built in the test-owned directory. + data, err := os.ReadFile(binary) + s.Require().NoError(err) + checksum := sha256.Sum256(data) + workspace := &provider.AgentWorkspaceInfo{ + Origin: origin, + Agent: provider.ProviderAgentConfig{ + Driver: provider.ExternalDriver, + External: provider.ProviderExternalDriverConfig{ + Binary: "RUNTIME", Args: []string{"--state-dir", s.T().TempDir()}, + }, + Binaries: map[string][]*provider.ProviderBinary{ + "RUNTIME": {{ + OS: runtime.GOOS, Arch: runtime.GOARCH, Path: name, + Checksum: hex.EncodeToString(checksum[:]), + }}, + }, + }, + } + host, err := external.New(ctx, workspace) + s.Require().NoError(err) + s.NotEmpty(host.Info().RuntimeName) + s.Require().NoError(host.Preflight(ctx, driver.PreflightOptions{})) + + workspace.Origin = filepath.Join(origin, "missing") + _, err = external.New(ctx, workspace) + s.ErrorIs(err, os.ErrNotExist) +}