From d24fe0ffbff893faf9f899d339ca29937bd38d84 Mon Sep 17 00:00:00 2001 From: Admin <1+admin@noreply.git.dcntrlized.org> Date: Sat, 1 Aug 2026 20:39:27 +0100 Subject: [PATCH] feat: Implemented (un)install,start/stop flow and fixed actions flow --- docker-compose.yaml | 4 +- src/frontend/components.json | 10 +- src/frontend/src/app.tsx | 100 ++-- .../src/lib/components/installButton.tsx | 52 ++ .../src/lib/components/startButton.tsx | 44 ++ .../src/lib/components/stopButton.tsx | 45 ++ .../src/lib/components/uninstallButton.tsx | 45 ++ src/frontend/src/lib/maestro/containers.tsx | 56 ++ .../src/lib/maestro/installContainer.tsx | 17 - .../src/lib/shadcn/components/ui/spinner.tsx | 10 + src/frontend/src/lib/types.ts | 3 + src/go.mod | 153 ++++- src/go.sum | 542 +++++++++++++++++- src/handlers/actions/actions.go | 25 +- src/handlers/containers/install.go | 100 +++- src/handlers/containers/lifecycle.go | 82 +++ src/handlers/registry/registry.go | 85 +-- src/lib/containers/cache.go | 38 +- src/lib/containers/compose.go | 73 +++ src/lib/containers/install.go | 394 +++++++++++++ src/lib/containers/installed.go | 84 +++ src/lib/containers/name.go | 77 +++ src/lib/containers/start.go | 108 ++++ src/lib/containers/stop.go | 41 ++ src/lib/containers/uninstall.go | 97 ++++ src/lib/containers/watcher.go | 146 ++++- src/lib/types/docker.go | 17 +- src/lib/types/registry.go | 35 ++ src/main.go | 14 +- 29 files changed, 2331 insertions(+), 166 deletions(-) create mode 100644 src/frontend/src/lib/components/installButton.tsx create mode 100644 src/frontend/src/lib/components/startButton.tsx create mode 100644 src/frontend/src/lib/components/stopButton.tsx create mode 100644 src/frontend/src/lib/components/uninstallButton.tsx create mode 100644 src/frontend/src/lib/maestro/containers.tsx delete mode 100644 src/frontend/src/lib/maestro/installContainer.tsx create mode 100644 src/frontend/src/lib/shadcn/components/ui/spinner.tsx create mode 100644 src/handlers/containers/lifecycle.go create mode 100644 src/lib/containers/compose.go create mode 100644 src/lib/containers/installed.go create mode 100644 src/lib/containers/name.go create mode 100644 src/lib/containers/start.go create mode 100644 src/lib/containers/stop.go create mode 100644 src/lib/containers/uninstall.go create mode 100644 src/lib/types/registry.go diff --git a/docker-compose.yaml b/docker-compose.yaml index 048fcd7..f6c0883 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,13 +6,15 @@ services: - "8080:8080" volumes: - "/var/run/docker.sock:/var/run/docker.sock" + - "${XDG_DATA_HOME:-${HOME}}/.dcntrlized:/data/.dcntrlized" + - "${HOME}:/host-home:ro" networks: - dcntrlized_net build: context: . dockerfile: Dockerfile environment: - # Fill these in with your own tokens + - XDG_DATA_HOME=/data - GITEA_REGISTRY_RO_TOKEN= - GITEA_RW_PACKAGE_TOKEN= - GITEA_ORG= diff --git a/src/frontend/components.json b/src/frontend/components.json index 6f54fb7..00577b5 100644 --- a/src/frontend/components.json +++ b/src/frontend/components.json @@ -13,11 +13,11 @@ "iconLibrary": "lucide", "rtl": true, "aliases": { - "components": "src/lib/shadcn/components", - "utils": "src/lib/shadcn/lib/utils", - "ui": "src/lib/shadcn/components/ui", - "lib": "src/lib/shadcn/lib", - "hooks": "src/lib/shadcn/hooks" + "components": "@shadcn/components", + "utils": "@shadcn/lib/utils", + "ui": "@shadcn/components/ui", + "lib": "@shadcn/lib", + "hooks": "@shadcn/hooks" }, "menuColor": "default", "menuAccent": "subtle", diff --git a/src/frontend/src/app.tsx b/src/frontend/src/app.tsx index ae6983d..8ed6ec6 100644 --- a/src/frontend/src/app.tsx +++ b/src/frontend/src/app.tsx @@ -1,20 +1,22 @@ import './app.css' import { ConnectionBadge } from '@lib/components/connectionBadge' -import installContainer from '@lib/maestro/installContainer' import { registry } from '@lib/ws/registry' import { useEffect, useState, useMemo } from 'preact/hooks' -import { - type Container, - type RegistryItem, - STATUS_PRIORITY +import { + type Container, + type RegistryItem, + STATUS_PRIORITY } from '@lib/types' import semver from 'semver' // Shadcn UI Component Imports import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@shadcn/components/ui/card' -import { Button } from '@shadcn/components/ui/button' import { Input } from '@shadcn/components/ui/input' import { Badge } from '@shadcn/components/ui/badge' +import { InstallButton } from '@lib/components/installButton' +import { UninstallButton } from '@lib/components/uninstallButton' +import { StartButton } from '@lib/components/startButton' +import { StopButton } from '@lib/components/stopButton' // Helper to extract base image without tag or digest function getBaseImage(imageRef?: string): string { @@ -127,6 +129,7 @@ export function App() { id: `remote-${r.name}`, status: 'uninstalled', origin: 'remote', + version: '', latestVersion: r.version, remoteSource: r.source }) @@ -139,7 +142,7 @@ export function App() { const filteredContainers = useMemo(() => { return containers.filter((c) => { const searchStr = searchQuery.toLowerCase() - const matchesSearch = + const matchesSearch = c.name.toLowerCase().includes(searchStr) || c.image?.toLowerCase().includes(searchStr) || c.source?.toLowerCase().includes(searchStr) || @@ -194,8 +197,8 @@ export function App() { - ) : container.hasUpdate ? ( - - ) : null} +
+ { isRemoteOnly ? : null } + { container.hasUpdate ? : null} + { container.status === 'installed' ? <>: null} + { isRunning ? : null} + { container.status === 'stopped' || container.status === 'exited' ? <>: null} +
)} diff --git a/src/frontend/src/lib/components/installButton.tsx b/src/frontend/src/lib/components/installButton.tsx new file mode 100644 index 0000000..528334b --- /dev/null +++ b/src/frontend/src/lib/components/installButton.tsx @@ -0,0 +1,52 @@ +import { useState } from "react" +import { uninstallContainer, installContainer } from "@lib/maestro/containers" +import type { RegistryItem } from "@lib/types" +import { Button } from "@lib/shadcn/components/ui/button" +import { Spinner } from "@lib/shadcn/components/ui/spinner" +import { Download } from "lucide-react" + +interface InstallProps { + container: RegistryItem, + type?: "install" | "update" +} + +export function InstallButton({ container, type = 'install' }: InstallProps) { + const [isInstalling, setIsInstalling] = useState(false) + + const handleInstall = async () => { + if(type === 'update') uninstallContainer(container) + + setIsInstalling(true) + try { + await installContainer(container) + } catch (error) { + console.error(error) + } finally { + setIsInstalling(false) + } + } + + const buttonString = () => { + if(type === 'install') return isInstalling ? "Installing..." : "Install" + return isInstalling ? "Updating..." : "Update" + } + + return ( + + ) +} \ No newline at end of file diff --git a/src/frontend/src/lib/components/startButton.tsx b/src/frontend/src/lib/components/startButton.tsx new file mode 100644 index 0000000..5e71309 --- /dev/null +++ b/src/frontend/src/lib/components/startButton.tsx @@ -0,0 +1,44 @@ +import { useState } from "react" +import { startContainer } from "@lib/maestro/containers" +import type { RegistryItem } from "@lib/types" +import { Button } from "@lib/shadcn/components/ui/button" +import { Play } from "lucide-react" +import { Spinner } from "@lib/shadcn/components/ui/spinner" + +interface StartProps { + container: RegistryItem, +} + +export function StartButton({ container }: StartProps) { + const [isStarting, setIsStarting] = useState(false) + + const handleStart = async () => { + setIsStarting(true) + try { + await startContainer(container) + } catch (error) { + console.error(error) + } finally { + setIsStarting(false) + } + } + + return ( + + ) +} \ No newline at end of file diff --git a/src/frontend/src/lib/components/stopButton.tsx b/src/frontend/src/lib/components/stopButton.tsx new file mode 100644 index 0000000..2cc16dd --- /dev/null +++ b/src/frontend/src/lib/components/stopButton.tsx @@ -0,0 +1,45 @@ +import { useState } from "react" +import { stopContainer } from "@lib/maestro/containers" +import type { RegistryItem } from "@lib/types" +import { Button } from "@lib/shadcn/components/ui/button" +import { Square } from "lucide-react" +import { Spinner } from "@lib/shadcn/components/ui/spinner" + +interface StopProps { + container: RegistryItem, +} + +export function StopButton({ container }: StopProps) { + const [isStopping, setIsStopping] = useState(false) + + const handleStop = async () => { + setIsStopping(true) + try { + await stopContainer(container) + } catch (error) { + console.error(error) + } finally { + setIsStopping(false) + } + } + + return ( + + ) +} diff --git a/src/frontend/src/lib/components/uninstallButton.tsx b/src/frontend/src/lib/components/uninstallButton.tsx new file mode 100644 index 0000000..0113b44 --- /dev/null +++ b/src/frontend/src/lib/components/uninstallButton.tsx @@ -0,0 +1,45 @@ +import { useState } from "react" +import { uninstallContainer } from "@lib/maestro/containers" +import type { RegistryItem } from "@lib/types" +import { Button } from "@lib/shadcn/components/ui/button" +import { Trash2 } from "lucide-react" +import { Spinner } from "@lib/shadcn/components/ui/spinner" + +interface UninstallProps { + container: RegistryItem, +} + +export function UninstallButton({ container }: UninstallProps) { + const [isUninstalling, setIsUninstalling] = useState(false) + + const handleInstall = async () => { + setIsUninstalling(true) + try { + await uninstallContainer(container) + } catch (error) { + console.error(error) + } finally { + setIsUninstalling(false) + } + } + + return ( + + ) +} \ No newline at end of file diff --git a/src/frontend/src/lib/maestro/containers.tsx b/src/frontend/src/lib/maestro/containers.tsx new file mode 100644 index 0000000..8aba529 --- /dev/null +++ b/src/frontend/src/lib/maestro/containers.tsx @@ -0,0 +1,56 @@ +import type { RegistryItem } from "@lib/types"; + +export function installContainer(remoteItem: RegistryItem): Promise { + // For now simply call /api/containers/install with body { item: RemoteItem } to install the container + return fetch('/api/containers/install', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(remoteItem), + }).then(response => { + if (!response.ok) { + throw new Error(`Failed to install container ${remoteItem.name}: ${response.statusText}`); + } + }); +} + +export function uninstallContainer(remoteItem: RegistryItem): Promise { + return fetch("/api/containers/uninstall", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(remoteItem), + }).then(response => { + if (!response.ok) { + throw new Error(`Failed to uninstall container ${remoteItem.name}: ${response.statusText}`) + } + }) +} + +export function startContainer(remoteItem: RegistryItem): Promise { + return fetch("/api/containers/start", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(remoteItem), + }).then(response => { + if (!response.ok) { + throw new Error(`Failed to start container ${remoteItem.name}: ${response.statusText}`) + } + })} + +export function stopContainer(remoteItem: RegistryItem): Promise { + return fetch("/api/containers/stop", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(remoteItem), + }).then(response => { + if (!response.ok) { + throw new Error(`Failed to stop container ${remoteItem.name}: ${response.statusText}`) + } + })} diff --git a/src/frontend/src/lib/maestro/installContainer.tsx b/src/frontend/src/lib/maestro/installContainer.tsx deleted file mode 100644 index 36ff43e..0000000 --- a/src/frontend/src/lib/maestro/installContainer.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { RegistryItem } from "@lib/types"; - - -export default function installContainer(remoteItem: RegistryItem): Promise { - // For now simply call /api/containers/install with body { item: RemoteItem } to install the container - return fetch('/api/containers/install', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ item: remoteItem }), - }).then(response => { - if (!response.ok) { - throw new Error(`Failed to install container ${remoteItem.name}: ${response.statusText}`); - } - }); -} diff --git a/src/frontend/src/lib/shadcn/components/ui/spinner.tsx b/src/frontend/src/lib/shadcn/components/ui/spinner.tsx new file mode 100644 index 0000000..92bc765 --- /dev/null +++ b/src/frontend/src/lib/shadcn/components/ui/spinner.tsx @@ -0,0 +1,10 @@ +import { cn } from "@shadcn/lib/utils" +import { Loader2Icon } from "lucide-react" + +function Spinner({ className, ...props }: React.ComponentProps<"svg">) { + return ( + + ) +} + +export { Spinner } diff --git a/src/frontend/src/lib/types.ts b/src/frontend/src/lib/types.ts index 571147a..528dbaf 100644 --- a/src/frontend/src/lib/types.ts +++ b/src/frontend/src/lib/types.ts @@ -35,6 +35,8 @@ export const STATUS_PRIORITY: Record = { starting: 2, stopped: 1, exited: 1, + created: 1, + installed: 1, } // --- SINGLE UNIFIED CONTAINER TYPE --- @@ -44,6 +46,7 @@ export type ContainerOrigin = 'local' | 'remote' | 'both'; export interface Container { id: string; name: string; + service_name?: string; // Status can be from Docker ("running", "stopped") or our UI ("uninstalled") status: string; diff --git a/src/go.mod b/src/go.mod index d58f9da..1ab1956 100644 --- a/src/go.mod +++ b/src/go.mod @@ -3,15 +3,163 @@ module git.dcntrlized.org/Dcntrlized/Maestro go 1.26.4 require ( + github.com/compose-spec/compose-go/v2 v2.9.1 + github.com/docker/cli v28.5.1+incompatible + github.com/docker/compose/v2 v2.40.3 github.com/docker/docker v28.5.2+incompatible github.com/gorilla/websocket v1.5.3 ) require ( - github.com/docker/cli v29.5.3+incompatible // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect + github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect + github.com/aws/smithy-go v1.20.3 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/buger/goterm v1.0.4 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect + github.com/containerd/console v1.0.5 // indirect + github.com/containerd/containerd/api v1.9.0 // indirect + github.com/containerd/containerd/v2 v2.1.4 // indirect + github.com/containerd/continuity v0.4.5 // indirect + github.com/containerd/platforms v1.0.0-rc.1 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect + github.com/containerd/ttrpc v1.2.7 // indirect + github.com/containerd/typeurl/v2 v2.2.3 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/buildx v0.29.1 // indirect + github.com/docker/cli-docs-tool v0.10.0 // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect + github.com/docker/go-metrics v0.0.1 // indirect + github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fsnotify/fsevents v0.2.0 // indirect + github.com/fvbommel/sortorder v1.1.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gofrs/flock v0.12.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/in-toto/in-toto-golang v0.9.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-runewidth v0.0.27 // indirect + github.com/mattn/go-shellwords v1.0.12 // indirect + github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/moby/buildkit v0.25.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/sys/capability v0.4.0 // indirect + github.com/moby/sys/mountinfo v0.7.2 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/signal v0.7.1 // indirect + github.com/moby/sys/symlink v0.3.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect + github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect + github.com/shibumi/go-pathspec v1.3.0 // indirect + github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/theupdateframework/notary v0.7.0 // indirect + github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect + github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect + github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f // indirect + github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect + github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect + github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect + github.com/vbatts/tar-split v0.12.1 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xhit/go-str2duration/v2 v2.1.0 // indirect + github.com/zclconf/go-cty v1.17.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.21.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc v1.81.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.32.3 // indirect + k8s.io/apimachinery v0.32.3 // indirect + k8s.io/client-go v0.32.3 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect + tags.cncf.io/container-device-interface v1.0.1 // indirect ) require ( @@ -26,7 +174,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/go-containerregistry v0.21.7 + github.com/google/go-containerregistry v0.20.6 github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect @@ -46,5 +194,4 @@ require ( golang.org/x/mod v0.38.0 golang.org/x/sys v0.46.0 // indirect golang.org/x/time v0.11.0 // indirect - gotest.tools/v3 v3.5.2 // indirect ) diff --git a/src/go.sum b/src/go.sum index 7f7792f..81c0167 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,84 +1,505 @@ +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0= +github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= +github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= +github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d h1:hi6J4K6DKrR4/ljxn6SF6nURyu785wKMuQcjt7H3VCQ= +github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= +github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= +github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= +github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= +github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= +github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= +github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY= +github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE= +github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0 h1:s7+5BfS4WFJoVF9pnB8kBk03S7pZXRdKamnV0FOl5Sc= +github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ= +github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= +github.com/compose-spec/compose-go/v2 v2.9.1 h1:8UwI+ujNU+9Ffkf/YgAm/qM9/eU7Jn8nHzWG721W4rs= +github.com/compose-spec/compose-go/v2 v2.9.1/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE= +github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= +github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= +github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= +github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= +github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= +github.com/containerd/containerd/v2 v2.1.4 h1:/hXWjiSFd6ftrBOBGfAZ6T30LJcx1dBjdKEeI8xucKQ= +github.com/containerd/containerd/v2 v2.1.4/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM= +github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= +github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= +github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/containerd/nydus-snapshotter v0.15.2 h1:qsHI4M+Wwrf6Jr4eBqhNx8qh+YU0dSiJ+WPmcLFWNcg= +github.com/containerd/nydus-snapshotter v0.15.2/go.mod h1:FfwH2KBkNYoisK/e+KsmNr7xTU53DmnavQHMFOcXwfM= +github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= +github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= +github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= +github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= +github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= +github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= +github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= +github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= +github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs= -github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/buildx v0.29.1 h1:58hxM5Z4mnNje3G5NKfULT9xCr8ooM8XFtlfUK9bKaA= +github.com/docker/buildx v0.29.1/go.mod h1:J4EFv6oxlPiV1MjO0VyJx2u5tLM7ImDEl9zyB8d4wPI= +github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY= +github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU= +github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09fzRHP4aX1qwp1U= +github.com/docker/compose/v2 v2.40.3 h1:XeYkQu1svDtyfZPv5nTwFryQ25ZJMkIlc4pz9HalMPI= +github.com/docker/compose/v2 v2.40.3/go.mod h1:iNY1tvoHTyN3C3QHCuWAgj3OjR2T6mGkk/qxfbBF/4M= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= +github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= +github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= +github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c= +github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= +github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-sql-driver/mysql v1.3.0 h1:pgwjLi/dvffoP9aabwkT3AKpXQM93QARkjFhDDqC1UE= +github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93 h1:jc2UWq7CbdszqeH6qu1ougXMIUBfSy8Pbh/anURYbGI= +github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU= -github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE= +github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= +github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +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-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= +github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= +github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= +github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE= +github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= +github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc= +github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0= +github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/buildkit v0.25.1 h1:j7IlVkeNbEo+ZLoxdudYCHpmTsbwKvhgc/6UJ/mY/o8= +github.com/moby/buildkit v0.25.1/go.mod h1:phM8sdqnvgK2y1dPDnbwI6veUCXHOZ6KFSl6E164tkc= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= +github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= +github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= +github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrNU= +github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= +github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8= +github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/secure-systems-lab/go-securesystemslib v0.6.0 h1:T65atpAVCJQK14UA57LMdZGpHi4QYSH/9FZyNGqMYIA= +github.com/secure-systems-lab/go-securesystemslib v0.6.0/go.mod h1:8Mtpo9JKks/qhPG4HGZ2LGMvrPbzuxwfz/f/zLfEWkk= +github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU= +github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc= +github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= +github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk= +github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= +github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94 h1:JmfC365KywYwHB946TTiQWEb8kqPY+pybPLoGE9GgVk= +github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= +github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431 h1:XTHrT015sxHyJ5FnQ0AeemSspZWaDq7DoTRW0EVsDCE= +github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c h1:2EejZtjFjKJGk71ANb+wtFK5EjUzUkEM3R0xnp559xg= +github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= +github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= +github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA= +github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g= +github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4= +github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY= +github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f h1:MoxeMfHAe5Qj/ySSBfL8A7l1V+hxuluj8owsIEEZipI= +github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98= +github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE= +github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= +github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0= +github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk= +github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw= +github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= +github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= +github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= +github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 h1:SUplec5dp06reu1zaXmOXdvqH398taqrDXqUl99jxSc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0/go.mod h1:ho2g4N+ane+swq5I/VBkKWnRDY4kUINH3FuqyZqX/Ug= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= @@ -87,27 +508,136 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/ go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII= +gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM= +gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= +k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= +k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= +k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= +k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc= +tags.cncf.io/container-device-interface v1.0.1/go.mod h1:JojJIOeW3hNbcnOH2q0NrWNha/JuHoDZcmYxAZwb2i0= diff --git a/src/handlers/actions/actions.go b/src/handlers/actions/actions.go index 1dcdc63..8fd8d73 100644 --- a/src/handlers/actions/actions.go +++ b/src/handlers/actions/actions.go @@ -7,6 +7,7 @@ import ( "net/http" "git.dcntrlized.org/Dcntrlized/Maestro/lib/docker" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/logger" ) func RunAction(w http.ResponseWriter, r *http.Request) { @@ -14,6 +15,8 @@ func RunAction(w http.ResponseWriter, r *http.Request) { actionName := r.PathValue("actionName") containerId := r.PathValue("containerId") + logger.Debug("Received request to run action %s on container id %s", actionName, containerId) + // Get IP address of the container using containerId dockerClient, err := docker.NewDockerClient() if err != nil { @@ -34,16 +37,36 @@ func RunAction(w http.ResponseWriter, r *http.Request) { } // Construct the URL for the action endpoint - actionURL := fmt.Sprintf("http://%s:%d/%s", network.IPAddress, network.Ports[0].Host, actionName) // Assuming network.Ports[0] is the main port. Might change in the future + actionURL := fmt.Sprintf("http://%s:%d/action/%s", network.IPAddress.Address, network.Ports[0].Host, actionName) // Assuming network.Ports[0] is the main port. Might change in the future // Forward the request to the container's action endpoint resp, err := http.Get(actionURL) if err != nil { + logger.Debug("Failed to forward action request %s to container %s: %w", actionName, containerId, err) http.Error(w, "Failed to forward request to container", http.StatusInternalServerError) return } defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + logger.Debug("Failed to read container error response body: %v", err) + } + + responseBodyString := string(bodyBytes) + + if resp.StatusCode == 404 { + logger.Info("The requested action does not exist") + http.Error(w, "The requested action does not exist", http.StatusNotFound) + } + + logger.Info("Container responded with non-2xx code") + logger.Debug("Container: %s | Called URL: %s | Status: %d | Response: %s", + containerId, actionURL, resp.StatusCode, responseBodyString) + http.Error(w, "Container responded with non-2xx code", http.StatusInternalServerError) + return + } // Copy the response from the container back to the original response writer w.WriteHeader(resp.StatusCode) _, err = io.Copy(w, resp.Body) diff --git a/src/handlers/containers/install.go b/src/handlers/containers/install.go index c2a3a8a..fb15fac 100644 --- a/src/handlers/containers/install.go +++ b/src/handlers/containers/install.go @@ -1,10 +1,104 @@ package containers import ( + "encoding/json" "net/http" + + libcontainers "git.dcntrlized.org/Dcntrlized/Maestro/lib/containers" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/logger" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/types" ) -func InstallContainer(w http.ResponseWriter, r *http.Request) { - // Stub, to implement, receives a container via frontend code, triggers it's install automagically and registers it in a local database - http.Error(w, "Action not implemented", http.StatusNotImplemented) +// InstallContainerHandler installs a registry container (downloads the compose +// file, pulls the image) and registers it in the cache so it shows up immediately. +func InstallContainerHandler(cache *libcontainers.ContainerCache) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req types.RegistryContainer + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + logger.Error("Failed to decode install request payload: %v", err) + http.Error(w, "Invalid JSON payload", http.StatusBadRequest) + return + } + + if req.Name == "" || req.Image == "" { + http.Error(w, "Missing required fields (name, image)", http.StatusBadRequest) + return + } + + logger.Info("Received Install Container Request for '%s' (%s)", req.Name, req.Image) + + if err := libcontainers.InstallContainer(r.Context(), req); err != nil { + logger.Error("Error installing container '%s': %v", req.Name, err) + http.Error(w, "Failed to install container: "+err.Error(), http.StatusInternalServerError) + return + } + + // Register in the cache so the frontend sees the installed state right away, + // without requiring a Maestro restart. + if existing, ok := cache.GetService(req.Name); !ok || existing.Status != "running" { + cache.AddService(types.Container{ + ServiceName: req.Name, + Name: req.Name, + Version: req.Version, + Image: req.Image, + Status: "installed", + CreatedAt: req.CreatedAt, + Title: req.Title, + Vendor: req.Vendor, + Author: req.Author, + Description: req.Description, + Source: req.Source, + }) + } + + respondSuccess(w, "Container downloaded and image pulled successfully") + } +} + +// UninstallContainerHandler uninstalls a container and removes it from the cache. +func UninstallContainerHandler(cache *libcontainers.ContainerCache) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req types.RegistryContainer + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + logger.Error("Failed to decode uninstall request payload: %v", err) + http.Error(w, "Invalid JSON payload", http.StatusBadRequest) + return + } + + if req.Name == "" { + http.Error(w, "Missing required field (name)", http.StatusBadRequest) + return + } + + logger.Info("Received Uninstall Container Request for '%s'", req.Name) + + if err := libcontainers.UninstallContainer(r.Context(), req); err != nil { + logger.Error("Error uninstalling container '%s': %v", req.Name, err) + http.Error(w, "Failed to uninstall container: "+err.Error(), http.StatusInternalServerError) + return + } + + cache.RemoveService(types.Container{ServiceName: req.Name}) + + respondSuccess(w, "Container uninstalled successfully") + } +} + +func respondSuccess(w http.ResponseWriter, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{ + "status": "success", + "message": message, + }) } diff --git a/src/handlers/containers/lifecycle.go b/src/handlers/containers/lifecycle.go new file mode 100644 index 0000000..a6bf138 --- /dev/null +++ b/src/handlers/containers/lifecycle.go @@ -0,0 +1,82 @@ +package containers + +import ( + "encoding/json" + "net/http" + + libcontainers "git.dcntrlized.org/Dcntrlized/Maestro/lib/containers" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/logger" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/types" +) + +// StartContainerHandler starts a previously installed service container. +func StartContainerHandler(cache *libcontainers.ContainerCache) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req types.RegistryContainer + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + logger.Error("Failed to decode start request payload: %v", err) + http.Error(w, "Invalid JSON payload", http.StatusBadRequest) + return + } + + if req.Name == "" { + http.Error(w, "Missing required field (name)", http.StatusBadRequest) + return + } + + logger.Info("Received Start Container Request for '%s'", req.Name) + + if err := libcontainers.StartContainer(r.Context(), req); err != nil { + logger.Error("Error starting container '%s': %v", req.Name, err) + http.Error(w, "Failed to start container: "+err.Error(), http.StatusInternalServerError) + return + } + + // Optimistically mark the service as starting; the watcher flips it to + // running once the container's start event is observed. + if existing, ok := cache.GetService(req.Name); ok && existing.Status != "running" { + existing.Status = "starting" + cache.UpdateService(existing) + } + + respondSuccess(w, "Container started successfully") + } +} + +// StopContainerHandler stops a running service container. +func StopContainerHandler(cache *libcontainers.ContainerCache) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req types.RegistryContainer + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + logger.Error("Failed to decode stop request payload: %v", err) + http.Error(w, "Invalid JSON payload", http.StatusBadRequest) + return + } + + if req.Name == "" { + http.Error(w, "Missing required field (name)", http.StatusBadRequest) + return + } + + logger.Info("Received Stop Container Request for '%s'", req.Name) + + if err := libcontainers.StopContainer(r.Context(), req); err != nil { + logger.Error("Error stopping container '%s': %v", req.Name, err) + http.Error(w, "Failed to stop container: "+err.Error(), http.StatusInternalServerError) + return + } + + // The watcher sets the status to "stopped" on the container's die event. + respondSuccess(w, "Container stopped successfully") + } +} diff --git a/src/handlers/registry/registry.go b/src/handlers/registry/registry.go index 6753aee..38d3402 100644 --- a/src/handlers/registry/registry.go +++ b/src/handlers/registry/registry.go @@ -10,6 +10,7 @@ import ( "time" "git.dcntrlized.org/Dcntrlized/Maestro/lib/logger" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/types" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" @@ -17,29 +18,6 @@ import ( "golang.org/x/mod/semver" ) -// RegistryContainer represents a clean API payload specifically for container registry items. -type RegistryContainer struct { - Name string `json:"name"` - Version string `json:"version"` - Image string `json:"image"` - Title string `json:"title,omitempty"` - Vendor string `json:"vendor,omitempty"` - Author string `json:"author,omitempty"` - Description string `json:"description,omitempty"` - Source string `json:"source,omitempty"` - CreatedAt string `json:"created_at,omitempty"` -} - -type RawPackage struct { - Name string `json:"name"` - Version string `json:"version"` - CreatedAt string `json:"created_at"` - HTMLURL string `json:"html_url"` - Repository *struct { - HTMLURL string `json:"html_url"` - } `json:"repository"` -} - func GetRegistry(w http.ResponseWriter, r *http.Request) { // 1. Query Gitea REST API using the exact Org name apiURL := fmt.Sprintf("https://git.dcntrlized.org/api/v1/packages/%s?type=container", os.Getenv("GITEA_ORG")) @@ -51,7 +29,7 @@ func GetRegistry(w http.ResponseWriter, r *http.Request) { return } - logger.Debug(fmt.Sprintf("%s", os.Getenv("GITEA_RW_PACKAGE_TOKEN"))) + logger.Debug("Querying Gitea package API at %s", apiURL) req.Header.Set("Authorization", "token "+os.Getenv("GITEA_RW_PACKAGE_TOKEN")) client := &http.Client{Timeout: 10 * time.Second} @@ -69,7 +47,7 @@ func GetRegistry(w http.ResponseWriter, r *http.Request) { return } - var rawPackages []RawPackage + var rawPackages []types.RawPackage if err := json.NewDecoder(resp.Body).Decode(&rawPackages); err != nil { logger.Error("Failed to decode registry response: %v", err) http.Error(w, "Failed to decode registry response", http.StatusInternalServerError) @@ -77,7 +55,7 @@ func GetRegistry(w http.ResponseWriter, r *http.Request) { } // 2. Group by Name and filter out "latest" & "sha256:" - groupedPackages := make(map[string][]RawPackage) + groupedPackages := make(map[string][]types.RawPackage) for _, pkg := range rawPackages { if pkg.Version == "latest" || strings.HasPrefix(pkg.Version, "sha256:") { continue @@ -94,7 +72,7 @@ func GetRegistry(w http.ResponseWriter, r *http.Request) { } // 3. For each container name, find the package entry with the highest semver - var latestPackages []RawPackage + var latestPackages []types.RawPackage for _, pkgs := range groupedPackages { if len(pkgs) == 0 { continue @@ -118,23 +96,24 @@ func GetRegistry(w http.ResponseWriter, r *http.Request) { ociAuth := remote.WithAuth(&authn.Basic{ Username: os.Getenv("GITEA_USER"), - Password: os.Getenv("GITEA_RO_TOKEN"), + Password: os.Getenv("GITEA_RW_PACKAGE_TOKEN"), }) ociOrg := strings.ToLower(os.Getenv("GITEA_ORG")) - var registryItems []RegistryContainer + var registryItems []types.RegistryContainer // 4. Fetch OCI metadata only for the highest version of each package for _, pkg := range latestPackages { pkgNameLower := strings.ToLower(pkg.Name) imageRef := fmt.Sprintf("git.dcntrlized.org/%s/%s:%s", ociOrg, pkgNameLower, pkg.Version) + // Default fallback source from Gitea REST API source := pkg.HTMLURL if pkg.Repository != nil && pkg.Repository.HTMLURL != "" { source = pkg.Repository.HTMLURL } - item := RegistryContainer{ + item := types.RegistryContainer{ Name: pkg.Name, Version: pkg.Version, Image: imageRef, @@ -156,7 +135,12 @@ func GetRegistry(w http.ResponseWriter, r *http.Request) { continue } - // Extract annotations from OCI manifest + // Fetch image digest (sha256:...) + if digest, err := img.Digest(); err == nil { + item.Digest = digest.String() + } + + // 1. Extract standard OCI annotations from OCI Manifest if manifest, err := img.Manifest(); err == nil && manifest != nil { if title, ok := manifest.Annotations["org.opencontainers.image.title"]; ok { item.Title = title @@ -170,11 +154,30 @@ func GetRegistry(w http.ResponseWriter, r *http.Request) { if desc, ok := manifest.Annotations["org.opencontainers.image.description"]; ok { item.Description = desc } + if src, ok := manifest.Annotations["org.opencontainers.image.source"]; ok && src != "" { + item.Source = src + } + if rev, ok := manifest.Annotations["org.opencontainers.image.revision"]; ok { + item.Revision = rev + } + if lic, ok := manifest.Annotations["org.opencontainers.image.licenses"]; ok { + item.Licenses = lic + } + if url, ok := manifest.Annotations["org.opencontainers.image.url"]; ok { + item.URL = url + } + if doc, ok := manifest.Annotations["org.opencontainers.image.documentation"]; ok { + item.Documentation = doc + } + if created, ok := manifest.Annotations["org.opencontainers.image.created"]; ok && created != "" { + item.CreatedAt = created + } } - // Fallback to Image Config labels + // 2. Fallback to Image Config Labels (where docker buildx puts them when targeting v2 registries) if configFile, err := img.ConfigFile(); err == nil && configFile != nil { labels := configFile.Config.Labels + if item.Title == "" { item.Title = labels["org.opencontainers.image.title"] } @@ -187,6 +190,24 @@ func GetRegistry(w http.ResponseWriter, r *http.Request) { if item.Description == "" { item.Description = labels["org.opencontainers.image.description"] } + if src, ok := labels["org.opencontainers.image.source"]; ok && src != "" { + item.Source = src + } + if item.Revision == "" { + item.Revision = labels["org.opencontainers.image.revision"] + } + if item.Licenses == "" { + item.Licenses = labels["org.opencontainers.image.licenses"] + } + if item.URL == "" { + item.URL = labels["org.opencontainers.image.url"] + } + if item.Documentation == "" { + item.Documentation = labels["org.opencontainers.image.documentation"] + } + if item.CreatedAt == "" && labels["org.opencontainers.image.created"] != "" { + item.CreatedAt = labels["org.opencontainers.image.created"] + } } registryItems = append(registryItems, item) diff --git a/src/lib/containers/cache.go b/src/lib/containers/cache.go index 2a97567..4f0f074 100644 --- a/src/lib/containers/cache.go +++ b/src/lib/containers/cache.go @@ -1,11 +1,23 @@ package containers import ( + "strings" "sync" "git.dcntrlized.org/Dcntrlized/Maestro/lib/types" ) +func normalizeName(name string) string { + return strings.ToLower(strings.TrimSpace(name)) +} + +func serviceKey(c types.Container) string { + if s := normalizeName(c.ServiceName); s != "" { + return s + } + return normalizeName(c.Name) +} + type ContainerCache struct { sync.RWMutex // Maps Container ID -> Container @@ -25,18 +37,30 @@ func (cache *ContainerCache) AddService(c types.Container) { if cache.services == nil { cache.services = make(map[string]types.Container) } - cache.services[c.ID] = c + cache.services[serviceKey(c)] = c cache.Unlock() // Trigger update AFTER unlocking to prevent holding locks during channel sends cache.TriggerUpdate() } +func (cache *ContainerCache) UpdateService(c types.Container) { + cache.Lock() + if cache.services == nil { + cache.services = make(map[string]types.Container) + } + cache.services[serviceKey(c)] = c + cache.Unlock() + + cache.TriggerUpdate() +} + func (cache *ContainerCache) RemoveService(c types.Container) { cache.Lock() - _, exists := cache.services[c.ID] + key := serviceKey(c) + _, exists := cache.services[key] if exists { - delete(cache.services, c.ID) + delete(cache.services, key) } cache.Unlock() @@ -64,3 +88,11 @@ func (cache *ContainerCache) GetServices() []types.Container { } return services } + +func (cache *ContainerCache) GetService(name string) (types.Container, bool) { + cache.RLock() + defer cache.RUnlock() + + service, ok := cache.services[normalizeName(name)] + return service, ok +} diff --git a/src/lib/containers/compose.go b/src/lib/containers/compose.go new file mode 100644 index 0000000..fb50d8d --- /dev/null +++ b/src/lib/containers/compose.go @@ -0,0 +1,73 @@ +package containers + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + "git.dcntrlized.org/Dcntrlized/Maestro/lib/docker" + "github.com/compose-spec/compose-go/v2/cli" + "github.com/compose-spec/compose-go/v2/types" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/flags" + "github.com/docker/compose/v2/pkg/api" + "github.com/docker/compose/v2/pkg/compose" +) + +// newComposeService returns a compose API service backed by the provided Docker client. +func newComposeService(dc *docker.DockerClient) (api.Compose, error) { + dockerCli, err := command.NewDockerCli() + if err != nil { + return nil, fmt.Errorf("failed to create docker cli: %w", err) + } + if err := dockerCli.Initialize(flags.NewClientOptions(), command.WithAPIClient(dc.Client)); err != nil { + return nil, fmt.Errorf("failed to initialize docker cli: %w", err) + } + return compose.NewComposeService(dockerCli), nil +} + +// loadComposeProject loads the docker-compose.yaml for the given service from its data dir. +func loadComposeProject(ctx context.Context, serviceName string) (*types.Project, error) { + dir, err := getContainerDataDir(serviceName) + if err != nil { + return nil, err + } + + composePath := filepath.Join(dir, "docker-compose.yaml") + projectOptions, err := cli.NewProjectOptions( + []string{composePath}, + cli.WithName(serviceName), + cli.WithWorkingDirectory(dir), + cli.WithOsEnv, + cli.WithDotEnv, + ) + if err != nil { + return nil, fmt.Errorf("failed to build compose project options: %w", err) + } + + project, err := cli.ProjectFromOptions(ctx, projectOptions) + if err != nil { + return nil, fmt.Errorf("failed to load compose project %s: %w", serviceName, err) + } + + // Replicate the docker compose CLI's project decoration: the compose API relies + // on these labels to match containers back to the project (e.g. start() filters + // on the project + oneoff labels). Without them, Up() creates containers but the + // subsequent start() step fails to find them. Also normalize the dcntrlized + // service marker to the lowercase project name so label lookups are consistent. + for name, s := range project.Services { + s.CustomLabels = map[string]string{ + api.ProjectLabel: project.Name, + api.ServiceLabel: name, + api.VersionLabel: api.ComposeVersion, + api.WorkingDirLabel: project.WorkingDir, + api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","), + api.OneoffLabel: "False", + "org.dcntrlized.service": project.Name, + } + project.Services[name] = s + } + + return project, nil +} diff --git a/src/lib/containers/install.go b/src/lib/containers/install.go index 0c09d3e..1b1e623 100644 --- a/src/lib/containers/install.go +++ b/src/lib/containers/install.go @@ -1 +1,395 @@ package containers + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" + "time" + + "git.dcntrlized.org/Dcntrlized/Maestro/lib/docker" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/logger" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/types" + "github.com/docker/docker/api/types/image" + registrytypes "github.com/docker/docker/api/types/registry" +) + +// InstallContainer downloads the raw docker-compose.yaml and pulls the Docker image. +func InstallContainer(ctx context.Context, item types.RegistryContainer) error { + // 1. Determine storage path (XDG_DATA_HOME or ~/.dcntrlized/[name]/) + targetDir, err := getContainerDataDir(item.Name) + if err != nil { + return fmt.Errorf("failed to determine data directory: %w", err) + } + + if err := os.MkdirAll(targetDir, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", targetDir, err) + } + + // 2. Fetch raw docker-compose.yaml from the Git URL + rawURL := fmt.Sprintf("%s/raw/branch/master/docker-compose.yaml", item.URL) + composePath := filepath.Join(targetDir, "docker-compose.yaml") + + logger.Info("Fetching compose file from %s...", rawURL) + if err := downloadFile(ctx, rawURL, composePath); err != nil { + return fmt.Errorf("failed to download docker-compose.yaml: %w", err) + } + + // 2.5. Persist install metadata so installed-but-not-running state can be tracked without Docker lookups + metadata := types.InstalledMetadata{ + RegistryContainer: item, + InstalledAt: time.Now().UTC().Format(time.RFC3339), + } + metadataPath := filepath.Join(targetDir, "metadata.json") + if err := writeMetadata(metadataPath, metadata); err != nil { + return fmt.Errorf("failed to write metadata: %w", err) + } + + // 3. Pull the Docker Image with Registry Authentication + logger.Info("Pulling docker image %s...", item.Image) + cli, err := docker.NewDockerClient() + if err != nil { + return fmt.Errorf("failed to initialize docker client: %w", err) + } + defer cli.Close() + + pullOpts := image.PullOptions{} + + // Build auth payload if git credentials or registry auth env vars exist + authHeader, err := buildRegistryAuthHeader() + if err != nil { + return fmt.Errorf("failed to create registry auth: %w", err) + } + if authHeader != "" { + pullOpts.RegistryAuth = authHeader + } + + reader, err := cli.ImagePull(ctx, item.Image, pullOpts) + if err != nil { + return fmt.Errorf("failed to pull docker image %s: %w", item.Image, err) + } + defer reader.Close() + + // Drain reader stream to ensure Docker finishes downloading layers + _, _ = io.Copy(io.Discard, reader) + + logger.Info("Successfully installed container service: %s", item.Name) + return nil +} + +// buildRegistryAuthHeader builds the base64-encoded auth header required by Docker SDK +func buildRegistryAuthHeader() (string, error) { + username := os.Getenv("REGISTRY_USER") + token := os.Getenv("GIT_TOKEN") + if token == "" { + token = os.Getenv("GITHUB_TOKEN") + } + + // For Gitea/Forgejo container registries, token auth works using "token" or your username as the user, and GIT_TOKEN as password. + if username == "" { + username = "oauth2" // Fallback username when using personal access tokens + } + + if token == "" { + return "", nil // No token set; proceed unauthenticated + } + + authConfig := registrytypes.AuthConfig{ + Username: username, + Password: token, + } + + encodedJSON, err := json.Marshal(authConfig) + if err != nil { + return "", err + } + + return base64.URLEncoding.EncodeToString(encodedJSON), nil +} + +// getDataRoot resolves the directory that stores installed container metadata, +// using the first variable that is set. DCNTRLIZED_INSTALL_DIR is used as-is; +// the others are base dirs with ".dcntrlized" appended. +func getDataRoot() (string, error) { + if dir := os.Getenv("DCNTRLIZED_INSTALL_DIR"); dir != "" { + return dir, nil + } + + if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" { + return filepath.Join(xdg, ".dcntrlized"), nil + } + + if xdgHome := os.Getenv("XDG_HOME_DIR"); xdgHome != "" { + return filepath.Join(xdgHome, ".dcntrlized"), nil + } + + if home := os.Getenv("HOME"); home != "" { + return filepath.Join(home, ".dcntrlized"), nil + } + + return filepath.Join("/data", ".dcntrlized"), nil +} + +// EnsureDataRoot creates the data root directory if it does not exist yet. +// This gives a clear, early error when the install directory is missing or not +// writable (e.g. the container's bind mount source was removed) instead of a +// confusing "no such file or directory" when installing a container. +// +// When started as root inside the container it hands the data root over to the +// host user (discovered at runtime, or overridden via DCNTRLIZED_UID/GID) and +// drops privileges, so every file created afterwards stays user-owned. +func EnsureDataRoot() error { + root, err := getDataRoot() + if err != nil { + return err + } + if err := os.MkdirAll(root, 0755); err != nil { + return fmt.Errorf("failed to create data root %s (is the host directory present and writable?): %w", root, err) + } + if os.Geteuid() == 0 { + identity, err := resolveRuntimeIdentity(root) + if err != nil { + return err + } + if err := os.Chown(root, identity.uid, identity.gid); err != nil { + return fmt.Errorf("failed to chown data root %s to %d:%d: %w", root, identity.uid, identity.gid, err) + } + if err := dropPrivileges(identity, root); err != nil { + return err + } + } + return nil +} + +// runtimeIdentity is the user the container should run as, plus any +// supplementary groups needed to keep working (e.g. the docker socket group). +type runtimeIdentity struct { + username string + hostDir string + uid, gid int + groups []int +} + +// resolveRuntimeIdentity determines the user to hand the data root to. +// An explicit DCNTRLIZED_UID/GID wins; otherwise the host user is discovered +// from the bind-mounted home directory, falling back to the data root's owner +// (self-heals upgrades) and finally to uid 1000. +func resolveRuntimeIdentity(dataRoot string) (runtimeIdentity, error) { + username := filepath.Base(hostPath(mountSource("/host-home"))) + if username == "" || username == "." || username == "/" { + username = "user" + } + hostDir := hostPath(mountSource(dataRoot)) + if hostDir == "" { + hostDir = "(unknown)" + } + + var id runtimeIdentity + if uidStr := os.Getenv("DCNTRLIZED_UID"); uidStr != "" { + uid, err := strconv.Atoi(uidStr) + if err != nil { + return runtimeIdentity{}, fmt.Errorf("invalid DCNTRLIZED_UID %q: %w", uidStr, err) + } + gid, err := strconv.Atoi(os.Getenv("DCNTRLIZED_GID")) + if err != nil { + gid = uid + } + id = runtimeIdentity{uid: uid, gid: gid, groups: dockerSocketGroups(uid)} + } else { + uid, gid, err := discoverHostUser(dataRoot) + if err != nil { + return runtimeIdentity{}, err + } + id = runtimeIdentity{uid: uid, gid: gid, groups: dockerSocketGroups(uid)} + } + + id.username, id.hostDir = username, hostDir + logger.Info("User: %s (%d, %d)", id.username, id.uid, id.gid) + return id, nil +} + +// discoverHostUser finds the host user's uid/gid, preferring the read-only +// bind mount of the host home directory, then the data root's owner. +func discoverHostUser(dataRoot string) (uid, gid int, err error) { + if uid, gid, ok := statOwner("/host-home"); ok && uid != 0 { + logger.Debug("Host user resolved from /host-home") + return uid, gid, nil + } + if uid, gid, ok := statOwner(dataRoot); ok && uid != 0 { + logger.Debug("Host user resolved from data root owner") + return uid, gid, nil + } + logger.Warn("Could not determine host user (no /host-home or owned data root); defaulting to uid 1000") + return 1000, 1000, nil +} + +// statOwner returns the numeric uid/gid owning the given path. +func statOwner(path string) (uid, gid int, ok bool) { + st, err := os.Stat(path) + if err != nil { + return 0, 0, false + } + sys, ok := st.Sys().(*syscall.Stat_t) + if !ok { + return 0, 0, false + } + return int(sys.Uid), int(sys.Gid), true +} + +// mountSource returns the host source path of the bind mount at mountPoint by +// parsing /proc/self/mountinfo, or "" when the mount does not exist. +func mountSource(mountPoint string) string { + data, err := os.ReadFile("/proc/self/mountinfo") + if err != nil { + return "" + } + for _, line := range strings.Split(string(data), "\n") { + fields := strings.Fields(line) + if len(fields) < 5 { + continue + } + // Format: ... + // For bind mounts, is the source path on the host. + if fields[4] == mountPoint { + return unescapeMountPath(fields[3]) + } + } + return "" +} + +// unescapeMountPath decodes the octal escapes used in mountinfo paths (e.g. \040). +func unescapeMountPath(s string) string { + if !strings.Contains(s, `\`) { + return s + } + var b strings.Builder + for i := 0; i < len(s); i++ { + if s[i] == '\\' && i+3 < len(s) { + if v, err := strconv.ParseUint(s[i+1:i+4], 8, 8); err == nil { + b.WriteByte(byte(v)) + i += 3 + continue + } + } + b.WriteByte(s[i]) + } + return b.String() +} + +// hostPath converts a mount source into a host-visible path. Btrfs encodes +// subvolumes as "@name/..." (e.g. "@home/franciscofadigas" mounted at +// "/home/franciscofadigas"); ordinary sources already start with "/". +func hostPath(source string) string { + if source == "" { + return "" + } + if strings.HasPrefix(source, "/@") { + return "/" + strings.TrimPrefix(source, "/@") + } + if strings.HasPrefix(source, "@") { + return "/" + strings.TrimPrefix(source, "@") + } + return source +} + +// dockerSocketPath is where the host's docker socket is mounted into the container. +const dockerSocketPath = "/var/run/docker.sock" + +// dockerSocketGroups returns the group of the mounted docker socket when it is +// group-accessible, so the container can keep talking to it after dropping +// privileges without hardcoding the host's docker group id. +func dockerSocketGroups(runAsUID int) []int { + st, err := os.Stat(dockerSocketPath) + if err != nil { + return nil + } + if st.Mode()&0o020 == 0 { + return nil + } + sys, ok := st.Sys().(*syscall.Stat_t) + if !ok || sys.Gid == 0 || int(sys.Gid) == runAsUID { + return nil + } + return []int{int(sys.Gid)} +} + +// dropPrivileges switches the process to the given uid/gid, keeping the +// supplementary groups needed for the docker socket. +func dropPrivileges(id runtimeIdentity, guestDir string) error { + if err := syscall.Setgroups(id.groups); err != nil { + return fmt.Errorf("failed to set supplementary groups %v: %w", id.groups, err) + } + if err := syscall.Setgid(id.gid); err != nil { + return fmt.Errorf("failed to set gid to %d: %w", id.gid, err) + } + if err := syscall.Setuid(id.uid); err != nil { + return fmt.Errorf("failed to set uid to %d: %w", id.uid, err) + } + if os.Geteuid() != id.uid { + return fmt.Errorf("failed to drop privileges: still running as euid %d (want %d)", os.Geteuid(), id.uid) + } + logger.Info("Handoff %s (%s) %d:%d", guestDir, id.hostDir, id.uid, id.gid) + return nil +} + +func getContainerDataDir(serviceName string) (string, error) { + root, err := getDataRoot() + if err != nil { + return "", err + } + return filepath.Join(root, serviceName), nil +} + +func downloadFile(ctx context.Context, url string, destPath string) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return err + } + + token := os.Getenv("GIT_TOKEN") + if token == "" { + token = os.Getenv("GITHUB_TOKEN") + } + + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden { + return fmt.Errorf("authentication failed (HTTP %d): ensure GIT_TOKEN is set and has access to the repository", resp.StatusCode) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected HTTP status fetching compose file: %s", resp.Status) + } + + out, err := os.Create(destPath) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} + +func writeMetadata(path string, metadata types.InstalledMetadata) error { + data, err := json.MarshalIndent(metadata, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, data, 0644) +} diff --git a/src/lib/containers/installed.go b/src/lib/containers/installed.go new file mode 100644 index 0000000..35d1efd --- /dev/null +++ b/src/lib/containers/installed.go @@ -0,0 +1,84 @@ +package containers + +import ( + "encoding/json" + "os" + "path/filepath" + + "git.dcntrlized.org/Dcntrlized/Maestro/lib/logger" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/types" +) + +// LoadInstalledContainers scans the data root and registers every installed (but not +// necessarily running) container in the cache under its install-dir name. +func (cache *ContainerCache) LoadInstalledContainers() error { + root, err := getDataRoot() + if err != nil { + return err + } + + entries, err := os.ReadDir(root) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + name := entry.Name() + metadata, err := readInstalledMetadata(filepath.Join(root, name)) + if err != nil { + logger.Warn("Skipping install dir %s: %v", name, err) + continue + } + + cache.AddService(types.Container{ + ServiceName: name, + Name: metadata.Name, + Version: metadata.Version, + Image: metadata.Image, + Status: "installed", + CreatedAt: metadata.CreatedAt, + Title: metadata.Title, + Vendor: metadata.Vendor, + Author: metadata.Author, + Description: metadata.Description, + Source: metadata.Source, + }) + logger.Info("Loaded installed container: %s", name) + } + + return nil +} + +// readInstalledMetadata reads and parses the metadata.json of an install dir. +func readInstalledMetadata(dir string) (*types.InstalledMetadata, error) { + data, err := os.ReadFile(filepath.Join(dir, "metadata.json")) + if err != nil { + return nil, err + } + + var metadata types.InstalledMetadata + if err := json.Unmarshal(data, &metadata); err != nil { + return nil, err + } + return &metadata, nil +} + +// IsInstalled reports whether the named service has an install record on disk. +func IsInstalled(name string) bool { + if name == "" { + return false + } + dir, err := getContainerDataDir(name) + if err != nil { + return false + } + _, err = os.Stat(filepath.Join(dir, "metadata.json")) + return err == nil +} diff --git a/src/lib/containers/name.go b/src/lib/containers/name.go new file mode 100644 index 0000000..21b3294 --- /dev/null +++ b/src/lib/containers/name.go @@ -0,0 +1,77 @@ +package containers + +import ( + "context" + "os" + "path/filepath" + "strings" + + "git.dcntrlized.org/Dcntrlized/Maestro/lib/docker" + "github.com/docker/docker/api/types/container" +) + +// resolveServiceName maps a possibly display name (e.g. "TestService" from a +// container manifest) back to the compose project name used for data dirs and +// docker labels ("testservice"). It prefers the container's compose project +// label (authoritative and lowercase), then the install data dir, then a +// lowercased fallback. +func resolveServiceName(ctx context.Context, dc *docker.DockerClient, displayName string) string { + if dirExists(displayName) { + return displayName + } + if project := lookupProjectName(ctx, dc, displayName); project != "" { + return project + } + lower := strings.ToLower(displayName) + if dirExists(lower) { + return lower + } + return lower +} + +// dirExists reports whether a service install directory exists for the name. +func dirExists(serviceName string) bool { + dir, err := getContainerDataDir(serviceName) + if err != nil { + return false + } + info, err := os.Stat(dir) + return err == nil && info.IsDir() +} + +// lookupProjectName finds the compose project name of a container matching the +// display name. The com.docker.compose.project label is authoritative; the +// org.dcntrlized.service label is a fallback for containers not started +// through compose. +func lookupProjectName(ctx context.Context, dc *docker.DockerClient, displayName string) string { + containers, err := dc.ContainerList(ctx, container.ListOptions{All: true}) + if err != nil { + return "" + } + for _, c := range containers { + if !matchesDisplayName(c, displayName) { + continue + } + if project := c.Labels["com.docker.compose.project"]; project != "" { + return project + } + if label := c.Labels["org.dcntrlized.service"]; label != "" { + return label + } + } + return "" +} + +// matchesDisplayName reports whether the container belongs to the service that +// presents the given (possibly display) name. +func matchesDisplayName(c container.Summary, displayName string) bool { + if label := c.Labels["org.dcntrlized.service"]; label != "" && strings.EqualFold(label, displayName) { + return true + } + for _, n := range c.Names { + if strings.EqualFold(filepath.Base(n), displayName) { + return true + } + } + return false +} diff --git a/src/lib/containers/start.go b/src/lib/containers/start.go new file mode 100644 index 0000000..916ea8d --- /dev/null +++ b/src/lib/containers/start.go @@ -0,0 +1,108 @@ +package containers + +import ( + "context" + "fmt" + "strings" + + "git.dcntrlized.org/Dcntrlized/Maestro/lib/docker" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/logger" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/types" + composetypes "github.com/compose-spec/compose-go/v2/types" + "github.com/docker/compose/v2/pkg/api" +) + +// StartContainer creates and starts the service containers defined in the +// service's docker-compose.yaml. +func StartContainer(ctx context.Context, item types.RegistryContainer) error { + dc, err := docker.NewDockerClient() + if err != nil { + return fmt.Errorf("failed to initialize docker client: %w", err) + } + defer dc.Close() + + serviceName := resolveServiceName(ctx, dc, item.Name) + project, err := loadComposeProject(ctx, serviceName) + if err != nil { + return err + } + + // The compose file may reference the image by a short name (e.g. + // "testservice") while install pulled it under its full registry reference. + // Make sure those short names exist so Up() can find them. + installedImage := item.Image + if installedImage == "" { + if dir, dirErr := getContainerDataDir(serviceName); dirErr == nil { + if metadata, metaErr := readInstalledMetadata(dir); metaErr == nil { + installedImage = metadata.Image + } + } + } + ensureServiceImages(ctx, dc, project, installedImage) + + composeService, err := newComposeService(dc) + if err != nil { + return err + } + + logger.Info("Starting service %s...", serviceName) + err = composeService.Up(ctx, project, api.UpOptions{ + Create: api.CreateOptions{ + Recreate: api.RecreateDiverged, + AssumeYes: true, + }, + Start: api.StartOptions{}, + }) + if err != nil { + return fmt.Errorf("failed to start service %s: %w", serviceName, err) + } + + logger.Info("Successfully started service: %s", serviceName) + return nil +} + +// ensureServiceImages makes sure every image referenced by the compose project +// exists locally. Registries install the image under its full reference (e.g. +// git.../testservice:3.4) while the service's compose file often references it +// by a short name (e.g. "testservice"); when the short name is missing locally +// and its base name matches the installed image, it is tagged so start works. +func ensureServiceImages(ctx context.Context, dc *docker.DockerClient, project *composetypes.Project, installedImage string) { + if installedImage == "" { + return + } + for name, s := range project.Services { + ref := s.Image + if ref == "" || ref == installedImage { + continue + } + if imageExists(ctx, dc, ref) { + continue + } + if baseImageName(ref) != baseImageName(installedImage) { + continue + } + logger.Info("Tagging %s as %s for compose service %s", installedImage, ref, name) + if err := dc.Client.ImageTag(ctx, installedImage, ref); err != nil { + logger.Warn("Failed to tag %s as %s: %v", installedImage, ref, err) + } + } +} + +// imageExists reports whether an image reference exists locally. +func imageExists(ctx context.Context, dc *docker.DockerClient, ref string) bool { + _, _, err := dc.Client.ImageInspectWithRaw(ctx, ref) + return err == nil +} + +// baseImageName strips the registry/namespace and tag from an image reference, +// returning the repository name (e.g. "testservice"). +func baseImageName(ref string) string { + base := ref + if i := strings.LastIndex(base, "/"); i >= 0 { + base = base[i+1:] + } + if i := strings.LastIndex(base, ":"); i >= 0 { + base = base[:i] + } + return base +} diff --git a/src/lib/containers/stop.go b/src/lib/containers/stop.go new file mode 100644 index 0000000..07846a1 --- /dev/null +++ b/src/lib/containers/stop.go @@ -0,0 +1,41 @@ +package containers + +import ( + "context" + "fmt" + + "git.dcntrlized.org/Dcntrlized/Maestro/lib/docker" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/logger" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/types" + "github.com/docker/compose/v2/pkg/api" +) + +// StopContainer stops the running service containers defined in the service's +// docker-compose.yaml. +func StopContainer(ctx context.Context, item types.RegistryContainer) error { + dc, err := docker.NewDockerClient() + if err != nil { + return fmt.Errorf("failed to initialize docker client: %w", err) + } + defer dc.Close() + + serviceName := resolveServiceName(ctx, dc, item.Name) + project, err := loadComposeProject(ctx, serviceName) + if err != nil { + return err + } + + composeService, err := newComposeService(dc) + if err != nil { + return err + } + + logger.Info("Stopping service %s...", serviceName) + err = composeService.Stop(ctx, project.Name, api.StopOptions{Project: project}) + if err != nil { + return fmt.Errorf("failed to stop service %s: %w", serviceName, err) + } + + logger.Info("Successfully stopped service: %s", serviceName) + return nil +} diff --git a/src/lib/containers/uninstall.go b/src/lib/containers/uninstall.go new file mode 100644 index 0000000..f9a9986 --- /dev/null +++ b/src/lib/containers/uninstall.go @@ -0,0 +1,97 @@ +package containers + +import ( + "context" + "fmt" + "os" + "strings" + + "git.dcntrlized.org/Dcntrlized/Maestro/lib/docker" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/logger" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/types" + "github.com/docker/compose/v2/pkg/api" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/image" +) + +// UninstallContainer stops and removes the service container, removes its data +// directory and removes the pulled image. +func UninstallContainer(ctx context.Context, item types.RegistryContainer) error { + dc, err := docker.NewDockerClient() + if err != nil { + return fmt.Errorf("failed to initialize docker client: %w", err) + } + defer dc.Close() + + serviceName := resolveServiceName(ctx, dc, item.Name) + targetDir, err := getContainerDataDir(serviceName) + if err != nil { + return fmt.Errorf("failed to determine data directory: %w", err) + } + + if _, err := os.Stat(targetDir); err == nil { + // Stop and remove the project containers/networks via compose down when + // the compose file is available. + if project, loadErr := loadComposeProject(ctx, serviceName); loadErr == nil { + if composeService, newErr := newComposeService(dc); newErr == nil { + if err := composeService.Down(ctx, project.Name, api.DownOptions{Project: project}); err != nil { + logger.Warn("Compose down failed for %s: %v", serviceName, err) + } + } + } + + // Fallback: force-remove any container still labeled with this service. + stopAndRemoveByLabel(ctx, dc, serviceName) + } + + if item.Image == "" { + if metadata, err := readInstalledMetadata(targetDir); err == nil { + item.Image = metadata.Image + } + } + + if err := os.RemoveAll(targetDir); err != nil { + // Leftover files (e.g. permission issues) shouldn't abort the uninstall: + // the container and image are already gone, and failing here would leave + // the cache entry stuck so the UI keeps showing the service as installed. + logger.Warn("Failed to remove data directory %s: %v", targetDir, err) + } + + if item.Image != "" { + if _, err := dc.ImageRemove(ctx, item.Image, image.RemoveOptions{PruneChildren: true}); err != nil { + // Image may still be in use by another container; uninstall still succeeds. + logger.Warn("Failed to remove image %s: %v", item.Image, err) + } + } + + logger.Info("Successfully uninstalled container service: %s", serviceName) + return nil +} + +// stopAndRemoveByLabel force-stops and removes every container carrying the +// org.dcntrlized.service label for the given service. +func stopAndRemoveByLabel(ctx context.Context, dc *docker.DockerClient, serviceName string) { + filter := filters.NewArgs() + filter.Add("label", "org.dcntrlized.service") + + containers, err := dc.ContainerList(ctx, container.ListOptions{All: true, Filters: filter}) + if err != nil { + logger.Warn("Failed to list containers for service %s: %v", serviceName, err) + return + } + + for _, c := range containers { + // The label may carry the display name (e.g. "TestService"); compare + // case-insensitively against the resolved project name. + if label := c.Labels["org.dcntrlized.service"]; label != "" && !strings.EqualFold(label, serviceName) { + continue + } + if err := dc.ContainerStop(ctx, c.ID, container.StopOptions{}); err != nil { + logger.Warn("Failed to stop container %s: %v", c.ID, err) + } + if err := dc.ContainerRemove(ctx, c.ID, container.RemoveOptions{Force: true}); err != nil { + logger.Warn("Failed to remove container %s: %v", c.ID, err) + } + } +} diff --git a/src/lib/containers/watcher.go b/src/lib/containers/watcher.go index 4af6b09..70db30f 100644 --- a/src/lib/containers/watcher.go +++ b/src/lib/containers/watcher.go @@ -5,10 +5,12 @@ import ( "fmt" "log" "os" + "strings" "time" "git.dcntrlized.org/Dcntrlized/Maestro/lib/docker" "git.dcntrlized.org/Dcntrlized/Maestro/lib/types" + "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" ) @@ -41,7 +43,7 @@ func (cache *ContainerCache) WatchContainers(ctx context.Context, dc *docker.Doc } continue } - _ = serviceLabel + key := normalizeName(serviceLabel) containerID := event.Actor.ID containerName := event.Actor.Attributes["name"] @@ -61,52 +63,70 @@ func (cache *ContainerCache) WatchContainers(ctx context.Context, dc *docker.Doc netInfo, shouldCache, err := docker.ExtractNetworkInfo(ctx, dc, inspectData) if err != nil { log.Printf("Error extracting network details: %v", err) + } else if !shouldCache { + log.Printf("Bypassing special network mode: %s", inspectData.HostConfig.NetworkMode) + } else { + containerNetwork = netInfo } - if !shouldCache { - fmt.Printf("Bypassing special network mode: %s\n", inspectData.HostConfig.NetworkMode) - cache.Unlock() - return - } - containerNetwork = netInfo } // Get Versions - ver, err := docker.GetContainerVersion(ctx, dc, containerID) + ver, _ := docker.GetContainerVersion(ctx, dc, containerID) - // Fetch Manifest + // Fetch Manifest (best effort; fall back to basic info if it fails) host := containerID[:12] port := event.Actor.Attributes["org.dcntrlized.port"] manifest, err := docker.FetchManifestWithRetry(ctx, host, port, 20, 50*time.Millisecond) if err != nil { log.Printf("Error fetching manifest for container %s: %v", containerID, err) - cache.Unlock() - continue } // Save to Cache log.Printf("Service started. Adding container %s - %s to cache", host, containerName) - cache.services[containerID] = types.Container{ - ID: containerID, - Name: manifest.Name, - DockerName: containerName, - Image: event.Actor.Attributes["image"], - CreatedAt: fmt.Sprint(event.TimeNano), - Network: containerNetwork, - Status: "running", - Version: ver, + existing, _ := cache.services[key] + container := types.Container{ + ID: containerID, + ServiceName: serviceLabel, + DockerName: containerName, + Image: event.Actor.Attributes["image"], + CreatedAt: fmt.Sprint(event.TimeNano), + Network: containerNetwork, + Status: "running", + Version: ver, + Title: existing.Title, + Vendor: existing.Vendor, + Author: existing.Author, + Description: existing.Description, + Source: existing.Source, } + if manifest != nil { + container.Name = manifest.Name + } else { + container.Name = containerName + } + cache.services[key] = container case "die": - if container, exists := cache.services[containerID]; exists { - log.Printf("Service stopping: %s", containerName) - container.Status = "stopping" - cache.services[containerID] = container + if container, exists := cache.services[key]; exists { + log.Printf("Service stopped: %s", containerName) + container.Status = "stopped" + cache.services[key] = container } case "destroy": - if container, exists := cache.services[containerID]; exists { + if container, exists := cache.services[key]; exists { log.Printf("Service destroyed: %s", containerName) - cache.RemoveService(container) + if IsInstalled(serviceLabel) || IsInstalled(container.ServiceName) || IsInstalled(container.Name) { + container.Status = "installed" + container.ID = "" + container.DockerName = "" + container.Network = types.Network{} + cache.services[key] = container + } else { + // We already hold the lock; delete directly instead of + // calling RemoveService (which re-locks and deadlocks). + delete(cache.services, key) + } } } @@ -115,3 +135,77 @@ func (cache *ContainerCache) WatchContainers(ctx context.Context, dc *docker.Doc } } } + +// imageTag returns the tag portion of an image reference, defaulting to latest. +func imageTag(ref string) string { + base := ref + if i := strings.LastIndex(base, "/"); i >= 0 { + base = base[i+1:] + } + if i := strings.LastIndex(base, ":"); i >= 0 { + return base[i+1:] + } + return "latest" +} + +// normalizeDockerState maps raw Docker container states onto the smaller status +// vocabulary used by the frontend (running / starting / stopped / installed). +func normalizeDockerState(state string) string { + switch state { + case "running": + return "running" + case "restarting", "starting": + return "starting" + case "exited", "stopped", "paused", "dead", "removing": + return "stopped" + case "created": + return "installed" + default: + return "installed" + } +} + +// ReconcileContainers lists all dcntrlized-labeled containers (running or not) and +// syncs their status into the cache, so state survives a Maestro restart. +func (cache *ContainerCache) ReconcileContainers(ctx context.Context, dc *docker.DockerClient) { + filter := filters.NewArgs() + filter.Add("label", "org.dcntrlized.service") + options := container.ListOptions{ + All: true, + Filters: filter, + } + + existing, err := dc.Client.ContainerList(ctx, options) + if err != nil { + log.Printf("Failed to list existing containers during reconciliation: %v", err) + return + } + + for _, c := range existing { + serviceLabel := c.Labels["org.dcntrlized.service"] + if serviceLabel == "" || len(c.Names) == 0 { + continue + } + + key := normalizeName(serviceLabel) + cache.Lock() + entry, has := cache.services[key] + if !has { + entry = types.Container{ + ServiceName: serviceLabel, + Name: strings.TrimPrefix(c.Names[0], "/"), + Image: c.Image, + Version: imageTag(c.Image), + Status: "installed", + } + } + entry.Status = normalizeDockerState(c.State) + entry.ID = c.ID + entry.DockerName = strings.TrimPrefix(c.Names[0], "/") + cache.services[key] = entry + cache.Unlock() + } + + cache.TriggerUpdate() + log.Printf("Reconciled %d existing containers with Docker", len(existing)) +} diff --git a/src/lib/types/docker.go b/src/lib/types/docker.go index 9c9d90e..7146b28 100644 --- a/src/lib/types/docker.go +++ b/src/lib/types/docker.go @@ -25,14 +25,15 @@ type Network struct { } type Container struct { - ID string `json:"id"` - Name string `json:"name"` - DockerName string `json:"docker_name"` - Image string `json:"image"` - Version string `json:"version"` - Status string `json:"status"` - CreatedAt string `json:"created_at"` - Network Network `json:"network"` + ID string `json:"id"` + Name string `json:"name"` + ServiceName string `json:"service_name,omitempty"` + DockerName string `json:"docker_name"` + Image string `json:"image"` + Version string `json:"version"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` + Network Network `json:"network"` Title string `json:"title,omitempty"` Vendor string `json:"vendor,omitempty"` diff --git a/src/lib/types/registry.go b/src/lib/types/registry.go new file mode 100644 index 0000000..b2b1172 --- /dev/null +++ b/src/lib/types/registry.go @@ -0,0 +1,35 @@ +package types + +// RegistryContainer represents a clean API payload specifically for container registry items. +type RegistryContainer struct { + Name string `json:"name"` + Version string `json:"version"` + Image string `json:"image"` + Digest string `json:"digest,omitempty"` + Title string `json:"title,omitempty"` + Vendor string `json:"vendor,omitempty"` + Author string `json:"author,omitempty"` + Description string `json:"description,omitempty"` + Source string `json:"source,omitempty"` + Revision string `json:"revision,omitempty"` + Licenses string `json:"licenses,omitempty"` + URL string `json:"url,omitempty"` + Documentation string `json:"documentation,omitempty"` + CreatedAt string `json:"created_at,omitempty"` +} + +type RawPackage struct { + Name string `json:"name"` + Version string `json:"version"` + CreatedAt string `json:"created_at"` + HTMLURL string `json:"html_url"` + Repository *struct { + HTMLURL string `json:"html_url"` + } `json:"repository"` +} + +// InstalledMetadata is the persisted install record written to the container data dir at install time. +type InstalledMetadata struct { + RegistryContainer + InstalledAt string `json:"installed_at,omitempty"` +} diff --git a/src/main.go b/src/main.go index 38d1deb..467f5c8 100644 --- a/src/main.go +++ b/src/main.go @@ -34,6 +34,13 @@ func main() { log.Fatalf("Failed to init docker client: %v", err) } defer dc.Close() + if err := libcontainers.EnsureDataRoot(); err != nil { + log.Fatalf("Data root check failed: %v", err) + } + if err := cache.LoadInstalledContainers(); err != nil { + log.Fatalf("Failed to load installed containers: %v", err) + } + cache.ReconcileContainers(ctx, dc) go cache.WatchContainers(ctx, dc) } @@ -42,8 +49,11 @@ func main() { mux := http.NewServeMux() mux.HandleFunc("/api/containers", containers.NewListContainersHandler(cache)) mux.HandleFunc("/api/ws", wsHub.HandleWS) - mux.HandleFunc("/api/containers/install", containers.InstallContainer) - mux.HandleFunc("/api/actions/{actionName}/{containerId}", actions.RunAction) + mux.HandleFunc("/api/containers/install", containers.InstallContainerHandler(cache)) + mux.HandleFunc("/api/containers/uninstall", containers.UninstallContainerHandler(cache)) + mux.HandleFunc("/api/containers/start", containers.StartContainerHandler(cache)) + mux.HandleFunc("/api/containers/stop", containers.StopContainerHandler(cache)) + mux.HandleFunc("/api/actions/{containerId}/{actionName}", actions.RunAction) mux.HandleFunc("/api/registry", handlers.GetRegistry) if os.Getenv("DEV_MODE") != "true" {