diff --git a/.goreleaser.yaml b/.goreleaser.yaml index a5772cd..1b69004 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -3,6 +3,7 @@ version: 2 before: hooks: - go mod tidy + - ./scripts/package-managed-runtime-bundle.sh - "sed -i.bak 's/\"version\": \".*\"/\"version\": \"{{ .Version }}\"/' plugin.json && rm -f plugin.json.bak" builds: @@ -28,6 +29,8 @@ archives: - plugin.json - plugin.contracts.json - runtime-adapters.json + - managed-runtime-bundles.json + - .release/managed-runtime-bundles/* - LICENSE checksum: diff --git a/README.md b/README.md index 71f610d..cc7e6f5 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ registry allowlists, proof/reward mutation, and worker binding. Runtime adapter metadata is published in `runtime-adapters.json` and referenced from `plugin.json` through `runtimeAdaptersRef`. +Managed runtime bundle metadata is published in `managed-runtime-bundles.json` +and referenced from `plugin.json` through `managedRuntimeBundlesRef`. + ## Runtime Backend Probes The `container` package can probe Docker-compatible runtimes through @@ -29,6 +32,23 @@ reports are only emitted after the runtime executable is found, version probing works, and the conformance command succeeds. Degraded and unsupported reports do not advertise executor providers or executor refs. +The `ManagedContainerdRuntimeProbes` helper builds target-aware probes from the +managed runtime bundle catalog and an explicit agent-managed install root. The +initial managed bundle target is Linux amd64 and is pinned to +`containerd/nerdctl` `v2.3.1` `nerdctl-full-2.3.1-linux-amd64.tar.gz`. Agents +must only advertise this backend after the bundle descriptor validates for the +current OS/architecture, the pinned runtime has been downloaded and extracted +into the agent-managed install root, the helper resolves an absolute +`bin/nerdctl` path inside that root, and the same conformance checks pass. The +managed backend uses an opaque nerdctl namespace so workloads run outside the +host's default container namespace. + +Managed runtime release packaging verifies the upstream `SHA256SUMS` and +`SHA256SUMS.asc` digests and emits a source manifest in the plugin release +archive. Agent-side download, extraction, signature verification, and lifecycle +wiring are intentionally left to the workflow-compute host integration phase so +the plugin does not advertise a host-installed `nerdctl` as bundled. + The default conformance image reference is the Debian 13 distroless static index: ```text diff --git a/container/adapters.go b/container/adapters.go index 0f17766..eec32e2 100644 --- a/container/adapters.go +++ b/container/adapters.go @@ -228,6 +228,7 @@ type RuntimeBackendCatalogEntry struct { BackendID string `json:"backend_id"` Families []core.RuntimeBackendFamily `json:"families"` Tools []core.ContainerRuntimeTool `json:"tools"` + SupportedTargets []core.ManagedRuntimeTarget `json:"supported_targets,omitempty"` IsolationModes []core.RuntimeIsolationMode `json:"isolation_modes"` InstallBurdens []core.RuntimeInstallBurden `json:"install_burdens"` RuntimeProfiles []core.RuntimeProfile `json:"runtime_profiles"` diff --git a/container/managed_runtime_bundle.go b/container/managed_runtime_bundle.go new file mode 100644 index 0000000..91133da --- /dev/null +++ b/container/managed_runtime_bundle.go @@ -0,0 +1,115 @@ +package container + +import ( + "errors" + "fmt" + "slices" + "strconv" + "strings" + "time" + + core "github.com/GoCodeAlone/workflow-plugin-compute-core/protocol" +) + +type ManagedRuntimeBundleCatalog struct { + ReleaseTag string `json:"release_tag,omitempty"` + SourceBaseURL string `json:"source_base_url,omitempty"` + GeneratedAt time.Time `json:"generated_at"` + Bundles []core.ManagedRuntimeBundleDescriptor `json:"bundles"` + BlockedVersions []string `json:"blocked_versions,omitempty"` + RevokedKeyIDs []string `json:"revoked_key_ids,omitempty"` + MinimumVersion string `json:"minimum_version,omitempty"` + StableSigningKey string `json:"stable_signing_key,omitempty"` +} + +func (c ManagedRuntimeBundleCatalog) Bundle(bundleID string, now time.Time) (core.ManagedRuntimeBundleDescriptor, error) { + if strings.TrimSpace(bundleID) == "" { + return core.ManagedRuntimeBundleDescriptor{}, errors.New("bundle_id is required") + } + if now.IsZero() { + return core.ManagedRuntimeBundleDescriptor{}, errors.New("validation time is required") + } + for _, bundle := range c.Bundles { + if bundle.BundleID != bundleID { + continue + } + if err := validateManagedRuntimeBundlePolicy(c, bundle, now); err != nil { + return core.ManagedRuntimeBundleDescriptor{}, err + } + return bundle, nil + } + return core.ManagedRuntimeBundleDescriptor{}, fmt.Errorf("managed runtime bundle %q not found", bundleID) +} + +func (c ManagedRuntimeBundleCatalog) BundleForTarget(bundleID, targetOS, targetArch string, now time.Time) (core.ManagedRuntimeBundleDescriptor, error) { + bundle, err := c.Bundle(bundleID, now) + if err != nil { + return core.ManagedRuntimeBundleDescriptor{}, err + } + if !managedRuntimeBundleSupportsTarget(bundle, targetOS, targetArch) { + return core.ManagedRuntimeBundleDescriptor{}, fmt.Errorf("managed runtime bundle %q does not support %s/%s", bundleID, targetOS, targetArch) + } + return bundle, nil +} + +func validateManagedRuntimeBundlePolicy(c ManagedRuntimeBundleCatalog, bundle core.ManagedRuntimeBundleDescriptor, now time.Time) error { + if err := bundle.ValidateAt(now); err != nil { + return err + } + if slices.Contains(c.BlockedVersions, bundle.Version) || + slices.Contains(bundle.UpdatePolicy.BlockedVersions, bundle.Version) || + slices.Contains(bundle.CVEPolicy.BlockedVersions, bundle.Version) { + return fmt.Errorf("managed runtime bundle version %q is blocked", bundle.Version) + } + if slices.Contains(c.RevokedKeyIDs, bundle.SignatureKeyID) || + slices.Contains(bundle.CVEPolicy.RevokedKeyIDs, bundle.SignatureKeyID) { + return fmt.Errorf("managed runtime bundle signing key %q is revoked", bundle.SignatureKeyID) + } + minVersion := firstNonEmpty(c.MinimumVersion, bundle.UpdatePolicy.MinSupportedVersion) + if minVersion != "" && semverLess(bundle.Version, minVersion) { + return fmt.Errorf("managed runtime bundle version %q is below minimum %q", bundle.Version, minVersion) + } + if c.StableSigningKey != "" && bundle.SignatureKeyID != c.StableSigningKey { + return fmt.Errorf("managed runtime bundle signing key %q does not match stable signing key", bundle.SignatureKeyID) + } + return nil +} + +func managedRuntimeBundleSupportsTarget(bundle core.ManagedRuntimeBundleDescriptor, targetOS, targetArch string) bool { + targetOS = strings.TrimSpace(targetOS) + targetArch = strings.TrimSpace(targetArch) + if targetOS == "" || targetArch == "" { + return false + } + for _, target := range bundle.SupportedTargets { + if target.OS == targetOS && target.Arch == targetArch { + return true + } + } + return false +} + +func semverLess(left, right string) bool { + l := parseVersionParts(left) + r := parseVersionParts(right) + for i := range 3 { + if l[i] != r[i] { + return l[i] < r[i] + } + } + return false +} + +func parseVersionParts(version string) [3]int { + version = strings.TrimPrefix(strings.TrimSpace(version), "v") + parts := strings.Split(version, ".") + var out [3]int + for i := 0; i < len(parts) && i < 3; i++ { + value, err := strconv.Atoi(parts[i]) + if err != nil { + return [3]int{} + } + out[i] = value + } + return out +} diff --git a/container/runtime_probe.go b/container/runtime_probe.go index 529d7e1..6ea8247 100644 --- a/container/runtime_probe.go +++ b/container/runtime_probe.go @@ -63,10 +63,12 @@ type RuntimeBackendProbeOptions struct { ConformanceImage string ConformanceCommand []string ConformanceWorkspace string + RuntimeScopeArgs []string IsolationMode core.RuntimeIsolationMode InstallBurden core.RuntimeInstallBurden RuntimeProfiles []core.RuntimeProfile ConformanceProfiles []string + ManagedBundle *core.ManagedRuntimeBundleDescriptor GeneratedAt time.Time } @@ -75,6 +77,28 @@ type RuntimeBackendProbe struct { Runner RuntimeCommandRunner } +type ManagedRuntimeInstallation struct { + Bundle core.ManagedRuntimeBundleDescriptor + Root string + CommandPath string +} + +func NewManagedContainerdRuntimeInstallation(bundle core.ManagedRuntimeBundleDescriptor, root string) (ManagedRuntimeInstallation, error) { + root = strings.TrimSpace(root) + if root == "" { + return ManagedRuntimeInstallation{}, errors.New("managed runtime root is required") + } + root, err := filepath.Abs(root) + if err != nil { + return ManagedRuntimeInstallation{}, err + } + return ManagedRuntimeInstallation{ + Bundle: bundle, + Root: root, + CommandPath: filepath.Join(root, "bin", "nerdctl"), + }, nil +} + func DockerCompatibleRuntimeProbes(runner RuntimeCommandRunner, generatedAt time.Time) []RuntimeBackendProbe { if runner == nil { runner = ExecRuntimeCommandRunner{} @@ -135,6 +159,77 @@ func DockerCompatibleRuntimeProbes(runner RuntimeCommandRunner, generatedAt time } } +func ManagedContainerdRuntimeProbe(installation *ManagedRuntimeInstallation, runner RuntimeCommandRunner, generatedAt time.Time) RuntimeBackendProbe { + if runner == nil { + runner = ExecRuntimeCommandRunner{} + } + profiles := []core.RuntimeProfile{core.RuntimeProfileSandboxedOCI, core.RuntimeProfileContainerBuild} + backendID := "managed-containerd" + command := "" + var bundle *core.ManagedRuntimeBundleDescriptor + if installation != nil { + bundle = &installation.Bundle + command = installation.CommandPath + } + if bundle != nil && bundle.BundleID != "" { + backendID = bundle.BundleID + } + profile := defaultConformanceProfile + if bundle != nil && bundle.ConformanceProfile != "" { + profile = bundle.ConformanceProfile + } + return RuntimeBackendProbe{ + Options: RuntimeBackendProbeOptions{ + BackendID: backendID, + Family: core.RuntimeBackendFamilyContainerd, + Tool: core.ContainerRuntimeNerdctl, + Command: command, + VersionArgs: []string{"version", "--format", "{{.Client.Version}}"}, + ConformanceImage: defaultRuntimeConformanceImage(), + ConformanceCommand: []string{defaultConformanceCommand}, + RuntimeScopeArgs: ManagedRuntimeScopeArgs(backendID, profile), + IsolationMode: core.RuntimeIsolationUserNamespace, + InstallBurden: core.RuntimeInstallBundled, + RuntimeProfiles: profiles, + ConformanceProfiles: []string{profile}, + ManagedBundle: bundle, + GeneratedAt: generatedAt, + }, + Runner: runner, + } +} + +func ManagedContainerdRuntimeProbes(catalog ManagedRuntimeBundleCatalog, installRoot string, runner RuntimeCommandRunner, generatedAt time.Time) []RuntimeBackendProbe { + if strings.TrimSpace(installRoot) == "" { + return nil + } + if generatedAt.IsZero() { + generatedAt = time.Now().UTC() + } + probes := make([]RuntimeBackendProbe, 0, len(catalog.Bundles)) + for _, candidate := range catalog.Bundles { + bundle, err := catalog.BundleForTarget(candidate.BundleID, runtime.GOOS, runtime.GOARCH, generatedAt) + if err != nil { + continue + } + installation, err := NewManagedContainerdRuntimeInstallation(bundle, filepath.Join(installRoot, bundle.BundleID)) + if err != nil { + continue + } + probes = append(probes, ManagedContainerdRuntimeProbe(&installation, runner, generatedAt)) + } + return probes +} + +func ManagedRuntimeScopeArgs(parts ...string) []string { + return []string{"--namespace", ManagedRuntimeScopeName(parts...)} +} + +func ManagedRuntimeScopeName(parts ...string) string { + digest := digestForRuntimeClaim(append(parts, "scope")...) + return "wfcompute-" + digest[7:23] +} + func (p RuntimeBackendProbe) Probe(ctx context.Context) core.RuntimeBackendReport { opts := p.Options.withDefaults() runner := p.Runner @@ -155,6 +250,35 @@ func (p RuntimeBackendProbe) Probe(ctx context.Context) core.RuntimeBackendRepor ConformanceProfiles: slices.Clone(opts.ConformanceProfiles), GeneratedAt: opts.GeneratedAt, } + if opts.InstallBurden == core.RuntimeInstallBundled { + if opts.ManagedBundle == nil { + report.Reason = "managed runtime bundle is unavailable" + return report + } + if !filepath.IsAbs(opts.Command) { + report.Reason = "managed runtime executable is unavailable" + return report + } + if err := opts.ManagedBundle.ValidateAt(opts.GeneratedAt); err != nil { + report.Status = core.RuntimeBackendDegraded + report.Reason = redactRuntimeProbeDetail(fmt.Sprintf("managed runtime bundle validation failed: %v", err)) + return report + } + report.Bundle = opts.ManagedBundle + report.Version = opts.ManagedBundle.Version + if opts.ManagedBundle.Family != "" { + report.Family = opts.ManagedBundle.Family + } + if opts.ManagedBundle.Tool != "" { + report.Tool = opts.ManagedBundle.Tool + } + if opts.ManagedBundle.OS != "" { + report.OS = opts.ManagedBundle.OS + } + if opts.ManagedBundle.Arch != "" { + report.Arch = opts.ManagedBundle.Arch + } + } if _, err := runner.LookPath(opts.Command); err != nil { report.Reason = "runtime executable is unavailable" return report @@ -165,7 +289,9 @@ func (p RuntimeBackendProbe) Probe(ctx context.Context) core.RuntimeBackendRepor report.Reason = redactRuntimeProbeDetail(fmt.Sprintf("runtime version probe failed: %v", err)) return report } - report.Version = sanitizeRuntimeVersion(string(versionResult.Stdout)) + if report.Version == "" { + report.Version = sanitizeRuntimeVersion(string(versionResult.Stdout)) + } workspace, cleanup, err := runtimeBackendConformanceWorkspace(opts) if err != nil { report.Status = core.RuntimeBackendDegraded @@ -173,16 +299,17 @@ func (p RuntimeBackendProbe) Probe(ctx context.Context) core.RuntimeBackendRepor return report } defer cleanup() - conformanceArgs := []string{ + conformanceArgs := append([]string(nil), opts.RuntimeScopeArgs...) + conformanceArgs = append(conformanceArgs, "run", "--rm", "--network", SandboxNetworkNone, - "-v", workspace + ":/workspace", + "-v", workspace+":/workspace", "-w", "/workspace", "-e", "WFCOMPUTE_RUNTIME_PROBE=1", "--read-only", opts.ConformanceImage, - } + ) conformanceArgs = append(conformanceArgs, opts.ConformanceCommand...) conformanceResult, err := runner.Run(ctx, opts.Command, conformanceArgs...) if err != nil { @@ -334,6 +461,12 @@ func runtimeBackendConformanceEvidence(stdout []byte, report core.RuntimeBackend if len(evidence.Details) == 0 { evidence.Details = []string{firstNonEmpty(report.ConformanceProfiles...)} } + if report.InstallBurden == core.RuntimeInstallBundled && report.Bundle != nil { + expectedScope := "runtime-scope:" + ManagedRuntimeScopeName(report.BackendID, firstNonEmpty(report.ConformanceProfiles...)) + if !slices.Contains(evidence.Details, expectedScope) { + return core.RuntimeBackendEvidence{}, fmt.Errorf("missing managed runtime scope evidence") + } + } return evidence, nil } diff --git a/container/runtime_probe_test.go b/container/runtime_probe_test.go index a2f269e..8d519ff 100644 --- a/container/runtime_probe_test.go +++ b/container/runtime_probe_test.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "slices" "strings" "testing" @@ -186,6 +187,142 @@ func TestDockerCompatibleRuntimeProbeOnlyAdvertisesRequestedProfiles(t *testing. } } +func TestManagedRuntimeBundleCatalogRejectsBlockedStaleOrUnsignedBundle(t *testing.T) { + catalog := validManagedRuntimeBundleCatalog() + catalog.BlockedVersions = []string{"v2.3.1"} + if _, err := catalog.Bundle("managed-containerd-linux-amd64", time.Unix(1_700_000_000, 0).UTC()); err == nil || !strings.Contains(err.Error(), "blocked") { + t.Fatalf("expected blocked bundle to fail, got %v", err) + } + + catalog = validManagedRuntimeBundleCatalog() + catalog.Bundles[0].ValidUntil = time.Unix(1_600_000_000, 0).UTC() + if _, err := catalog.Bundle("managed-containerd-linux-amd64", time.Unix(1_700_000_000, 0).UTC()); err == nil || !strings.Contains(err.Error(), "valid_until") { + t.Fatalf("expected stale bundle to fail, got %v", err) + } + + catalog = validManagedRuntimeBundleCatalog() + catalog.Bundles[0].SignatureDigest = "" + if _, err := catalog.Bundle("managed-containerd-linux-amd64", time.Unix(1_700_000_000, 0).UTC()); err == nil || !strings.Contains(err.Error(), "signature") { + t.Fatalf("expected unsigned bundle to fail, got %v", err) + } +} + +func TestManagedRuntimeBundleCatalogRequiresMatchingTarget(t *testing.T) { + catalog := validManagedRuntimeBundleCatalog() + + if _, err := catalog.BundleForTarget("managed-containerd-linux-amd64", "windows", "amd64", time.Unix(1_700_000_000, 0).UTC()); err == nil || !strings.Contains(err.Error(), "does not support") { + t.Fatalf("expected mismatched target to fail, got %v", err) + } + if _, err := catalog.BundleForTarget("managed-containerd-linux-amd64", "linux", "amd64", time.Unix(1_700_000_000, 0).UTC()); err != nil { + t.Fatalf("expected linux/amd64 bundle lookup to pass: %v", err) + } +} + +func TestManagedContainerdRuntimeProbeRequiresBundleBeforeSupport(t *testing.T) { + probe := ManagedContainerdRuntimeProbe(nil, RuntimeCommandRunner(nil), time.Unix(1_700_000_000, 0).UTC()) + + report := probe.Probe(context.Background()) + + if report.Status != core.RuntimeBackendUnsupported { + t.Fatalf("status=%q want unsupported: %+v", report.Status, report) + } + if len(report.ExecutorProviders) != 0 || len(report.Executors) != 0 { + t.Fatalf("missing managed bundle advertised executors: %+v", report) + } + if err := report.Validate(); err != nil { + t.Fatalf("unsupported managed bundle report invalid: %v", err) + } +} + +func TestManagedContainerdRuntimeProbeAttachesBundleAndScopedNamespace(t *testing.T) { + var calls []string + bundle := validManagedRuntimeBundleDescriptor() + installation, err := NewManagedContainerdRuntimeInstallation(bundle, filepath.Join(t.TempDir(), "managed-containerd-linux-amd64")) + if err != nil { + t.Fatalf("managed runtime installation: %v", err) + } + probe := ManagedContainerdRuntimeProbe(&installation, &fakeRuntimeCommandRunner{ + path: installation.CommandPath, + calls: &calls, + results: map[string]fakeRuntimeCommandResult{ + installation.CommandPath + " version --format {{.Client.Version}}": {stdout: "1.7.7\n"}, + managedRuntimeConformanceCommandKey(installation.CommandPath): { + stdout: validManagedRuntimeEvidenceJSON(), + }, + }, + }, time.Unix(1_700_000_000, 0).UTC()) + probe.Options.ConformanceWorkspace = "/tmp/wfcompute-runtime-probe-test" + + report := probe.Probe(context.Background()) + + if report.Status != core.RuntimeBackendSupported { + t.Fatalf("status=%q want supported: %+v", report.Status, report) + } + if report.InstallBurden != core.RuntimeInstallBundled || report.Bundle == nil { + t.Fatalf("supported managed report missing bundled descriptor: %+v", report) + } + if err := report.ValidateAt(time.Unix(1_700_000_000, 0).UTC()); err != nil { + t.Fatalf("supported managed report invalid: %v", err) + } + for _, call := range calls { + if strings.HasPrefix(call, installation.CommandPath+" --namespace ") { + if !strings.Contains(call, "wfcompute-") || strings.Contains(call, "pool") || strings.Contains(call, "worker") { + t.Fatalf("managed runtime scope is not opaque: %q", call) + } + return + } + } + t.Fatalf("managed probe did not use scoped nerdctl args: %+v", calls) +} + +func TestManagedContainerdRuntimeProbeRequiresScopedEvidence(t *testing.T) { + bundle := validManagedRuntimeBundleDescriptor() + installation, err := NewManagedContainerdRuntimeInstallation(bundle, filepath.Join(t.TempDir(), "managed-containerd-linux-amd64")) + if err != nil { + t.Fatalf("managed runtime installation: %v", err) + } + probe := ManagedContainerdRuntimeProbe(&installation, &fakeRuntimeCommandRunner{ + path: installation.CommandPath, + results: map[string]fakeRuntimeCommandResult{ + installation.CommandPath + " version --format {{.Client.Version}}": {stdout: "1.7.7\n"}, + managedRuntimeConformanceCommandKey(installation.CommandPath): { + stdout: validRuntimeEvidenceJSON(), + }, + }, + }, time.Unix(1_700_000_000, 0).UTC()) + probe.Options.ConformanceWorkspace = "/tmp/wfcompute-runtime-probe-test" + + report := probe.Probe(context.Background()) + + if report.Status != core.RuntimeBackendDegraded { + t.Fatalf("status=%q want degraded without scoped evidence: %+v", report.Status, report) + } + if len(report.ExecutorProviders) != 0 || len(report.Executors) != 0 { + t.Fatalf("managed runtime without scoped evidence advertised executors: %+v", report) + } +} + +func TestManagedContainerdRuntimeProbesRequireInstallRootAndCurrentTarget(t *testing.T) { + catalog := validManagedRuntimeBundleCatalog() + if probes := ManagedContainerdRuntimeProbes(catalog, "", &fakeRuntimeCommandRunner{}, time.Unix(1_700_000_000, 0).UTC()); len(probes) != 0 { + t.Fatalf("managed probes without install root = %+v", probes) + } + installRoot := t.TempDir() + probes := ManagedContainerdRuntimeProbes(catalog, installRoot, &fakeRuntimeCommandRunner{}, time.Unix(1_700_000_000, 0).UTC()) + if runtime.GOOS == "linux" && runtime.GOARCH == "amd64" { + if len(probes) != 1 || probes[0].Options.BackendID != "managed-containerd-linux-amd64" { + t.Fatalf("managed probes = %+v", probes) + } + if !filepath.IsAbs(probes[0].Options.Command) || !strings.HasPrefix(probes[0].Options.Command, installRoot) { + t.Fatalf("managed probe command is not rooted in install root: %q", probes[0].Options.Command) + } + return + } + if len(probes) != 0 { + t.Fatalf("unexpected managed probes for %s/%s: %+v", runtime.GOOS, runtime.GOARCH, probes) + } +} + func TestRuntimeBackendProbeRunsConformanceWithWorkspaceEnvAndReadOnlyRoot(t *testing.T) { var calls []string probe := RuntimeBackendProbe{ @@ -354,6 +491,82 @@ func validRuntimeEvidenceJSON() string { return `{"workspace":true,"network":true,"env":true,"proof":true,"cleanup":true}` } +func validManagedRuntimeEvidenceJSON() string { + return `{"workspace":true,"network":true,"env":true,"proof":true,"cleanup":true,"details":["runtime-scope:wfcompute-06d10e1aef1733a0"]}` +} + +func validManagedRuntimeBundleCatalog() ManagedRuntimeBundleCatalog { + return ManagedRuntimeBundleCatalog{ + ReleaseTag: "v2.3.1", + SourceBaseURL: "https://github.com/containerd/nerdctl/releases/download/v2.3.1", + GeneratedAt: time.Unix(1_700_000_000, 0).UTC(), + MinimumVersion: "v2.3.1", + StableSigningKey: "containerd-nerdctl-release", + Bundles: []core.ManagedRuntimeBundleDescriptor{validManagedRuntimeBundleDescriptor()}, + } +} + +func validManagedRuntimeBundleDescriptor() core.ManagedRuntimeBundleDescriptor { + return core.ManagedRuntimeBundleDescriptor{ + ProtocolVersion: core.Version, + BundleID: "managed-containerd-linux-amd64", + Family: core.RuntimeBackendFamilyContainerd, + Tool: core.ContainerRuntimeNerdctl, + Version: "v2.3.1", + OS: "linux", + Arch: "amd64", + ArtifactName: "nerdctl-full-2.3.1-linux-amd64.tar.gz", + ArtifactDigest: "sha256:7a0d8efcf55b10b57d831541266adb9c6ec3d55b44ec041c95f6eb994d1faab9", + ChecksumName: "SHA256SUMS", + ChecksumDigest: "sha256:8a0586ff11d4d5a5d19d59494a10af8c6d41dd95ca72ff347f62d5288bc5131a", + SignatureName: "SHA256SUMS.asc", + SignatureDigest: "sha256:f87400e0923e22eab251328bd210bb9e8d3bba2b58dbbb84699622474344d68c", + SignatureIssuer: "containerd/nerdctl release", + SignatureKeyID: "containerd-nerdctl-release", + TrustRootDigest: "sha256:6fad18923304aba73378965a8bac49bf44a3a22da73df42ca6a081c726c36b34", + SignatureSubject: core.ManagedRuntimeSignatureSubject{ + ArtifactDigest: "sha256:7a0d8efcf55b10b57d831541266adb9c6ec3d55b44ec041c95f6eb994d1faab9", + RuntimeFamily: core.RuntimeBackendFamilyContainerd, + OS: "linux", + Arch: "amd64", + Version: "v2.3.1", + Channel: "stable", + ConformanceProfile: "distroless-static-v1", + ScopedStorePolicyDigest: "sha256:311ab6244d878cf7280a5927f5af6063337ec262e35fd7c84c6579d07591337e", + }, + ValidUntil: time.Date(2026, 12, 31, 0, 0, 0, 0, time.UTC), + UpdatePolicy: core.ManagedRuntimeUpdatePolicy{ + Channel: "stable", + MinSupportedVersion: "v2.3.1", + }, + CVEPolicy: core.ManagedRuntimeCVEPolicy{ + PolicyDigest: "sha256:5bc9d3baf40fe716e68bfc469c53040351288caaaa048650aeadd6320ca6d7c1", + UpdatedByVersion: "v2.3.1", + }, + ScopedStore: core.ManagedRuntimeScopedStorePolicy{ + Required: true, + NamespaceStrategy: "opaque-worker-pool-scope", + StoreStrategy: "workflow-owned-content-store", + PolicyDigest: "sha256:311ab6244d878cf7280a5927f5af6063337ec262e35fd7c84c6579d07591337e", + CleanupRequired: true, + HostGlobalVisibilityForbidden: true, + }, + SupportedTargets: []core.ManagedRuntimeTarget{{ + OS: "linux", + Arch: "amd64", + }}, + ConformanceProfile: "distroless-static-v1", + InstallBurden: core.RuntimeInstallBundled, + } +} + +func managedRuntimeConformanceCommandKey(command string) string { + digest := digestForRuntimeClaim("managed-containerd-linux-amd64", "distroless-static-v1", "scope") + return command + " --namespace wfcompute-" + digest[7:23] + + " run --rm --network none -v /tmp/wfcompute-runtime-probe-test:/workspace -w /workspace -e WFCOMPUTE_RUNTIME_PROBE=1 --read-only " + + defaultConformanceImageRef + " " + defaultConformanceCommand +} + func TestRuntimeProbeRedactsAuthFailures(t *testing.T) { report := RuntimeBackendProbe{ Options: podmanProbeOptions([]core.RuntimeProfile{core.RuntimeProfileSandboxedOCI}), diff --git a/go.mod b/go.mod index 2f46d7d..48f1d22 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.0 require ( github.com/GoCodeAlone/workflow v0.62.0 - github.com/GoCodeAlone/workflow-plugin-compute-core v0.5.0 + github.com/GoCodeAlone/workflow-plugin-compute-core v0.6.0 ) require ( diff --git a/go.sum b/go.sum index 90a55e3..250a8c9 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/GoCodeAlone/modular/modules/eventbus/v2 v2.8.0 h1:buYs0TGNbAZgtTq1Qb+ github.com/GoCodeAlone/modular/modules/eventbus/v2 v2.8.0/go.mod h1:329flAKmwrPq2JEwu9iltWv6A83H/Di82Xze+kvdKDw= github.com/GoCodeAlone/workflow v0.62.0 h1:emFkTomDpVmBcEfw7quRO4V/J4qDsWNx/CrBdlGqkfg= github.com/GoCodeAlone/workflow v0.62.0/go.mod h1:659GGDrw3QJ7b625y9rf8QhKIpt1VCoEG0MxKu5tGQs= -github.com/GoCodeAlone/workflow-plugin-compute-core v0.5.0 h1:nHci8DsOetdjDF9ihieWCSH7f6gKcQnn6GmTaZbqEtE= -github.com/GoCodeAlone/workflow-plugin-compute-core v0.5.0/go.mod h1:1T6uCpUWPCNk6XPYgKq5CL/7LkD24MphKYsYVzF4jnI= +github.com/GoCodeAlone/workflow-plugin-compute-core v0.6.0 h1:khmZdWjqybwuXlUHmxDIP3Ce6ce6F77QXXvV4HAox7w= +github.com/GoCodeAlone/workflow-plugin-compute-core v0.6.0/go.mod h1:1T6uCpUWPCNk6XPYgKq5CL/7LkD24MphKYsYVzF4jnI= github.com/GoCodeAlone/yaegi v0.17.2 h1:WK6Y6e0t1a6U7r+S2dN3CGWW1PizYD3zO0zneToZPxM= github.com/GoCodeAlone/yaegi v0.17.2/go.mod h1:z5Pr6Wse6QJcQvpgxTxzMAevFarH0N37TG88Y9dprx0= github.com/IBM/sarama v1.47.0 h1:GcQFEd12+KzfPYeLgN69Fh7vLCtYRhVIx0rO4TZO318= diff --git a/internal/plugin_test.go b/internal/plugin_test.go index cee0fe9..5c6b884 100644 --- a/internal/plugin_test.go +++ b/internal/plugin_test.go @@ -33,8 +33,9 @@ func TestPluginJSONReferencesRuntimeAdapters(t *testing.T) { t.Fatal(err) } var manifest struct { - RuntimeAdaptersRef string `json:"runtimeAdaptersRef"` - Dependencies []struct { + RuntimeAdaptersRef string `json:"runtimeAdaptersRef"` + ManagedRuntimeBundlesRef string `json:"managedRuntimeBundlesRef"` + Dependencies []struct { Name string `json:"name"` Constraint string `json:"constraint"` } `json:"dependencies"` @@ -49,7 +50,7 @@ func TestPluginJSONReferencesRuntimeAdapters(t *testing.T) { for _, dependency := range manifest.Dependencies { if dependency.Name == "workflow-plugin-compute-core" { coreDependencyFound = true - if dependency.Constraint != ">=0.5.0" { + if dependency.Constraint != ">=0.6.0" { t.Fatalf("compute-core dependency constraint = %q", dependency.Constraint) } } @@ -71,13 +72,14 @@ func TestPluginJSONReferencesRuntimeAdapters(t *testing.T) { if len(catalog.Adapters) != 2 { t.Fatalf("runtime adapter catalog incomplete: %+v", catalog) } - if len(catalog.RuntimeBackends) != 3 { + if len(catalog.RuntimeBackends) != 4 { t.Fatalf("runtime backend catalog incomplete: %+v", catalog) } wantBackends := map[string]core.ContainerRuntimeTool{ - "podman-rootless": core.ContainerRuntimePodman, - "docker-desktop": core.ContainerRuntimeDocker, - "nerdctl-containerd": core.ContainerRuntimeNerdctl, + "podman-rootless": core.ContainerRuntimePodman, + "docker-desktop": core.ContainerRuntimeDocker, + "nerdctl-containerd": core.ContainerRuntimeNerdctl, + "managed-containerd-linux-amd64": core.ContainerRuntimeNerdctl, } for _, backend := range catalog.RuntimeBackends { wantTool, ok := wantBackends[backend.BackendID] @@ -89,11 +91,52 @@ func TestPluginJSONReferencesRuntimeAdapters(t *testing.T) { !slices.Contains(backend.ExecutorProviders, container.SandboxedContainerBuildProviderName) { t.Fatalf("runtime backend catalog missing expected entries: %+v", backend) } + if backend.BackendID == "managed-containerd-linux-amd64" { + if len(backend.SupportedTargets) != 1 || + backend.SupportedTargets[0].OS != "linux" || + backend.SupportedTargets[0].Arch != "amd64" { + t.Fatalf("managed runtime backend target constraints missing: %+v", backend) + } + } delete(wantBackends, backend.BackendID) } if len(wantBackends) != 0 { t.Fatalf("runtime backend catalog missing entries: %+v", wantBackends) } + if manifest.ManagedRuntimeBundlesRef != "managed-runtime-bundles.json" { + t.Fatalf("managedRuntimeBundlesRef = %q", manifest.ManagedRuntimeBundlesRef) + } + bundlesData, err := os.ReadFile(filepath.Join(root, manifest.ManagedRuntimeBundlesRef)) + if err != nil { + t.Fatal(err) + } + var bundleCatalog container.ManagedRuntimeBundleCatalog + if err := json.Unmarshal(bundlesData, &bundleCatalog); err != nil { + t.Fatal(err) + } + if _, err := bundleCatalog.Bundle("managed-containerd-linux-amd64", bundleCatalog.GeneratedAt); err != nil { + t.Fatalf("managed runtime bundle catalog invalid: %v", err) + } + if bundleCatalog.ReleaseTag != "v2.3.1" || + bundleCatalog.SourceBaseURL != "https://github.com/containerd/nerdctl/releases/download/v2.3.1" { + t.Fatalf("managed runtime bundle source metadata drifted: %+v", bundleCatalog) + } + if len(bundleCatalog.Bundles) != 1 { + t.Fatalf("managed runtime bundle catalog = %+v", bundleCatalog) + } + bundle := bundleCatalog.Bundles[0] + if bundle.ArtifactName != "nerdctl-full-2.3.1-linux-amd64.tar.gz" || + bundle.ArtifactDigest != "sha256:7a0d8efcf55b10b57d831541266adb9c6ec3d55b44ec041c95f6eb994d1faab9" || + bundle.ChecksumDigest != "sha256:8a0586ff11d4d5a5d19d59494a10af8c6d41dd95ca72ff347f62d5288bc5131a" || + bundle.SignatureDigest != "sha256:f87400e0923e22eab251328bd210bb9e8d3bba2b58dbbb84699622474344d68c" { + t.Fatalf("managed runtime bundle descriptor is not pinned to the expected upstream asset: %+v", bundle) + } + bundlesText := string(bundlesData) + if strings.Contains(bundlesText, strings.Repeat("1", 32)) || + strings.Contains(bundlesText, strings.Repeat("2", 32)) || + strings.Contains(bundlesText, strings.Repeat("3", 32)) { + t.Fatalf("managed runtime bundle catalog contains placeholder digest material") + } for _, adapter := range catalog.Adapters { contract := adapter.Contract(core.RuntimeDescriptor{ Name: adapter.AdapterID, @@ -131,9 +174,29 @@ func TestPluginContractsAdvertiseRuntimeBackendReports(t *testing.T) { typ.Wire == "json" && typ.GoType == "github.com/GoCodeAlone/workflow-plugin-compute-core/protocol.RuntimeBackendReport" && typ.ProtocolVersion == core.Version && - slices.Contains(typ.ProducedBy, "DockerCompatibleRuntimeProbes") { + slices.Contains(typ.ProducedBy, "DockerCompatibleRuntimeProbes") && + slices.Contains(typ.ProducedBy, "ManagedContainerdRuntimeProbe") { return } } t.Fatalf("RuntimeBackendReport protocol type not advertised: %+v", contracts.ProtocolTypes) } + +func TestManagedRuntimeBundlePackagingScriptMatchesCatalogPins(t *testing.T) { + script, err := os.ReadFile(filepath.Join("..", "scripts", "package-managed-runtime-bundle.sh")) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{ + "release_tag=\"v2.3.1\"", + "artifact_name=\"nerdctl-full-2.3.1-linux-amd64.tar.gz\"", + "artifact_digest=\"7a0d8efcf55b10b57d831541266adb9c6ec3d55b44ec041c95f6eb994d1faab9\"", + "checksum_digest=\"8a0586ff11d4d5a5d19d59494a10af8c6d41dd95ca72ff347f62d5288bc5131a\"", + "signature_digest=\"f87400e0923e22eab251328bd210bb9e8d3bba2b58dbbb84699622474344d68c\"", + "grep -F \"${artifact_digest} ${artifact_name}\"", + } { + if !strings.Contains(string(script), want) { + t.Fatalf("managed runtime packaging script missing %q", want) + } + } +} diff --git a/managed-runtime-bundles.json b/managed-runtime-bundles.json new file mode 100644 index 0000000..8a5d4b9 --- /dev/null +++ b/managed-runtime-bundles.json @@ -0,0 +1,62 @@ +{ + "release_tag": "v2.3.1", + "source_base_url": "https://github.com/containerd/nerdctl/releases/download/v2.3.1", + "generated_at": "2026-06-11T00:00:00Z", + "minimum_version": "v2.3.1", + "stable_signing_key": "containerd-nerdctl-release", + "bundles": [ + { + "protocol_version": "compute.v1alpha1", + "bundle_id": "managed-containerd-linux-amd64", + "family": "containerd", + "tool": "nerdctl", + "version": "v2.3.1", + "os": "linux", + "arch": "amd64", + "artifact_name": "nerdctl-full-2.3.1-linux-amd64.tar.gz", + "artifact_digest": "sha256:7a0d8efcf55b10b57d831541266adb9c6ec3d55b44ec041c95f6eb994d1faab9", + "checksum_name": "SHA256SUMS", + "checksum_digest": "sha256:8a0586ff11d4d5a5d19d59494a10af8c6d41dd95ca72ff347f62d5288bc5131a", + "signature_name": "SHA256SUMS.asc", + "signature_digest": "sha256:f87400e0923e22eab251328bd210bb9e8d3bba2b58dbbb84699622474344d68c", + "signature_issuer": "containerd/nerdctl release", + "signature_key_id": "containerd-nerdctl-release", + "trust_root_digest": "sha256:6fad18923304aba73378965a8bac49bf44a3a22da73df42ca6a081c726c36b34", + "signature_subject": { + "artifact_digest": "sha256:7a0d8efcf55b10b57d831541266adb9c6ec3d55b44ec041c95f6eb994d1faab9", + "runtime_family": "containerd", + "os": "linux", + "arch": "amd64", + "version": "v2.3.1", + "channel": "stable", + "conformance_profile": "distroless-static-v1", + "scoped_store_policy_digest": "sha256:311ab6244d878cf7280a5927f5af6063337ec262e35fd7c84c6579d07591337e" + }, + "valid_until": "2026-12-31T00:00:00Z", + "update_policy": { + "channel": "stable", + "min_supported_version": "v2.3.1" + }, + "cve_policy": { + "policy_digest": "sha256:5bc9d3baf40fe716e68bfc469c53040351288caaaa048650aeadd6320ca6d7c1", + "updated_by_version": "v2.3.1" + }, + "scoped_store": { + "required": true, + "namespace_strategy": "opaque-worker-pool-scope", + "store_strategy": "workflow-owned-content-store", + "policy_digest": "sha256:311ab6244d878cf7280a5927f5af6063337ec262e35fd7c84c6579d07591337e", + "cleanup_required": true, + "host_global_visibility_forbidden": true + }, + "supported_targets": [ + { + "os": "linux", + "arch": "amd64" + } + ], + "conformance_profile": "distroless-static-v1", + "install_burden": "bundled" + } + ] +} diff --git a/plugin.contracts.json b/plugin.contracts.json index aab871d..adbb358 100644 --- a/plugin.contracts.json +++ b/plugin.contracts.json @@ -7,7 +7,14 @@ "wire": "json", "goType": "github.com/GoCodeAlone/workflow-plugin-compute-core/protocol.RuntimeBackendReport", "protocolVersion": "compute.v1alpha1", - "producedBy": ["DockerCompatibleRuntimeProbes"] + "producedBy": ["DockerCompatibleRuntimeProbes", "ManagedContainerdRuntimeProbe"] + }, + { + "name": "ManagedRuntimeBundleCatalog", + "wire": "json", + "goType": "github.com/GoCodeAlone/workflow-plugin-compute-container/container.ManagedRuntimeBundleCatalog", + "protocolVersion": "compute.v1alpha1", + "producedBy": ["managed-runtime-bundles.json"] } ] } diff --git a/plugin.json b/plugin.json index 46b2fc0..46df614 100644 --- a/plugin.json +++ b/plugin.json @@ -19,9 +19,10 @@ "dependencies": [ { "name": "workflow-plugin-compute-core", - "constraint": ">=0.5.0" + "constraint": ">=0.6.0" } ], + "managedRuntimeBundlesRef": "managed-runtime-bundles.json", "capabilities": { "configProvider": false, "moduleTypes": [], diff --git a/runtime-adapters.json b/runtime-adapters.json index 26abb0b..718cbb0 100644 --- a/runtime-adapters.json +++ b/runtime-adapters.json @@ -51,6 +51,22 @@ "runtime_profiles": ["sandboxed-oci-v1", "container-build-v1"], "executor_providers": ["sandboxed-command", "sandboxed-container-build"], "conformance_profiles": ["distroless-static-v1"] + }, + { + "backend_id": "managed-containerd-linux-amd64", + "families": ["containerd"], + "tools": ["nerdctl"], + "supported_targets": [ + { + "os": "linux", + "arch": "amd64" + } + ], + "isolation_modes": ["user-namespace"], + "install_burdens": ["bundled"], + "runtime_profiles": ["sandboxed-oci-v1", "container-build-v1"], + "executor_providers": ["sandboxed-command", "sandboxed-container-build"], + "conformance_profiles": ["distroless-static-v1"] } ], "host_owned_responsibilities": [ diff --git a/scripts/package-managed-runtime-bundle.sh b/scripts/package-managed-runtime-bundle.sh new file mode 100755 index 0000000..271bed9 --- /dev/null +++ b/scripts/package-managed-runtime-bundle.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail +export LC_ALL=C +export LANG=C + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/.." && pwd)" +cd "$repo_root" + +out_dir=".release/managed-runtime-bundles" +bundle_id="managed-containerd-linux-amd64" +release_tag="v2.3.1" +source_base_url="https://github.com/containerd/nerdctl/releases/download/${release_tag}" +artifact_name="nerdctl-full-2.3.1-linux-amd64.tar.gz" +artifact_digest="7a0d8efcf55b10b57d831541266adb9c6ec3d55b44ec041c95f6eb994d1faab9" +checksum_digest="8a0586ff11d4d5a5d19d59494a10af8c6d41dd95ca72ff347f62d5288bc5131a" +signature_digest="f87400e0923e22eab251328bd210bb9e8d3bba2b58dbbb84699622474344d68c" +manifest="${out_dir}/${bundle_id}.source.json" + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + return + fi + shasum -a 256 "$1" | awk '{print $1}' +} + +require_digest() { + local file="$1" + local want="$2" + local got + got="$(sha256_file "$file")" + if [ "$got" != "$want" ]; then + printf 'managed runtime bundle digest mismatch for %s: got %s want %s\n' "$file" "$got" "$want" >&2 + exit 1 + fi +} + +rm -rf "$out_dir" +mkdir -p "$out_dir" +curl -fsSL -o "${out_dir}/SHA256SUMS" "${source_base_url}/SHA256SUMS" +curl -fsSL -o "${out_dir}/SHA256SUMS.asc" "${source_base_url}/SHA256SUMS.asc" +require_digest "${out_dir}/SHA256SUMS" "$checksum_digest" +require_digest "${out_dir}/SHA256SUMS.asc" "$signature_digest" +if ! grep -F "${artifact_digest} ${artifact_name}" "${out_dir}/SHA256SUMS" >/dev/null; then + printf 'managed runtime artifact %s with digest %s not found in SHA256SUMS\n' "$artifact_name" "$artifact_digest" >&2 + exit 1 +fi + +cat > "$manifest" < "${manifest}.sha256" +cp managed-runtime-bundles.json "${out_dir}/managed-runtime-bundles.json"