mirror of
https://github.com/psviderski/uncloud.git
synced 2026-11-11 05:04:16 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3482eeefab | ||
|
|
ace8cbf974 | ||
|
|
45d33d87dd | ||
|
|
5c33e4be86 | ||
|
|
88dda435e7 | ||
|
|
ae940c3003 | ||
|
|
e525bdeb7c | ||
|
|
bc09954cbd |
@@ -44,3 +44,4 @@ node_modules/
|
||||
.devcontainer/
|
||||
|
||||
.zed/
|
||||
research/
|
||||
|
||||
+30
-17
@@ -27,10 +27,12 @@ type GetCaddyConfigResponse struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The generated Caddyfile content.
|
||||
// The saved Caddyfile content.
|
||||
Caddyfile string `protobuf:"bytes,1,opt,name=caddyfile,proto3" json:"caddyfile,omitempty"`
|
||||
// Timestamp when the config was last modified.
|
||||
ModifiedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=modified_at,json=modifiedAt,proto3" json:"modified_at,omitempty"`
|
||||
// Error from the latest unsuccessful reconciliation attempt.
|
||||
LastReconciliationError string `protobuf:"bytes,3,opt,name=last_reconciliation_error,json=lastReconciliationError,proto3" json:"last_reconciliation_error,omitempty"`
|
||||
}
|
||||
|
||||
func (x *GetCaddyConfigResponse) Reset() {
|
||||
@@ -79,6 +81,13 @@ func (x *GetCaddyConfigResponse) GetModifiedAt() *timestamppb.Timestamp {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetCaddyConfigResponse) GetLastReconciliationError() string {
|
||||
if x != nil {
|
||||
return x.LastReconciliationError
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_api_pb_caddy_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_api_pb_caddy_proto_rawDesc = []byte{
|
||||
@@ -87,22 +96,26 @@ var file_api_pb_caddy_proto_rawDesc = []byte{
|
||||
0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
||||
0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x73, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x43, 0x61,
|
||||
0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65, 0x12,
|
||||
0x3b, 0x0a, 0x0b, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
|
||||
0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x74, 0x32, 0x49, 0x0a, 0x05,
|
||||
0x43, 0x61, 0x64, 0x64, 0x79, 0x12, 0x40, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66,
|
||||
0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69,
|
||||
0x2e, 0x47, 0x65, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75,
|
||||
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69,
|
||||
0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62,
|
||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xaf, 0x01, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x43,
|
||||
0x61, 0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65,
|
||||
0x12, 0x3b, 0x0a, 0x0b, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
||||
0x70, 0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3a, 0x0a,
|
||||
0x19, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x69, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x17, 0x6c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x69, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x32, 0x49, 0x0a, 0x05, 0x43, 0x61, 0x64,
|
||||
0x64, 0x79, 0x12, 0x40, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
|
||||
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
||||
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65,
|
||||
0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
|
||||
0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e,
|
||||
0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
+3
-1
@@ -13,8 +13,10 @@ service Caddy {
|
||||
}
|
||||
|
||||
message GetCaddyConfigResponse {
|
||||
// The generated Caddyfile content.
|
||||
// The saved Caddyfile content.
|
||||
string caddyfile = 1;
|
||||
// Timestamp when the config was last modified.
|
||||
google.protobuf.Timestamp modified_at = 2;
|
||||
// Error from the latest unsuccessful reconciliation attempt.
|
||||
string last_reconciliation_error = 3;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/alecthomas/chroma/v2/quick"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/internal/cli/completion"
|
||||
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -66,5 +67,10 @@ func runConfig(ctx context.Context, uncli *cli.CLI, opts configOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
if config.LastReconciliationError != "" {
|
||||
tui.PrintWarning(fmt.Sprintf("last Caddy config load failed: %s\nShowing the last saved Caddyfile.",
|
||||
config.LastReconciliationError))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ func NewRunCommand(groupID string) *cobra.Command {
|
||||
"Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where\n"+
|
||||
"the volume is located. Can be specified multiple times.\n"+
|
||||
"Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]\n"+
|
||||
"Host and container paths support runtime templates such as {{.Container.Name}}.\n"+
|
||||
"Examples:\n"+
|
||||
" -v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container\n"+
|
||||
" -v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container\n"+
|
||||
|
||||
@@ -61,27 +61,14 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go/auth v0.20.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/bigmod v0.1.0 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // 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/KimMachineGun/automemlimit v0.7.5 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.13.0 // indirect
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.17 // indirect
|
||||
@@ -101,9 +88,7 @@ require (
|
||||
github.com/buger/goterm v1.0.4 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.5 // indirect
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 // indirect
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
|
||||
@@ -111,10 +96,8 @@ require (
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/containerd/cgroups/v3 v3.0.5 // indirect
|
||||
github.com/containerd/console v1.0.5 // indirect
|
||||
github.com/containerd/containerd/api v1.9.0 // indirect
|
||||
@@ -127,13 +110,8 @@ require (
|
||||
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/coreos/go-oidc/v3 v3.17.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
github.com/dgraph-io/ristretto v0.2.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/distribution/distribution/v3 v3.0.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.12.0 // indirect
|
||||
@@ -151,14 +129,11 @@ require (
|
||||
github.com/fsnotify/fsevents v0.2.0 // indirect
|
||||
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
@@ -166,14 +141,9 @@ require (
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/cel-go v0.28.1 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
|
||||
github.com/gorilla/handlers v1.5.2 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
@@ -184,15 +154,11 @@ require (
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/hashicorp/golang-lru/arc/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 // 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/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
@@ -205,7 +171,6 @@ require (
|
||||
github.com/libdns/libdns v1.1.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||
@@ -215,11 +180,8 @@ require (
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.6 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.2 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/buildkit v0.25.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
@@ -245,9 +207,7 @@ require (
|
||||
github.com/opencontainers/runtime-spec v1.2.1 // indirect
|
||||
github.com/opencontainers/selinux v1.12.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pires/go-proxyproto v0.12.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
@@ -261,28 +221,17 @@ require (
|
||||
github.com/redis/go-redis/v9 v9.7.3 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // 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/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
||||
github.com/slackhq/nebula v1.10.3 // indirect
|
||||
github.com/smallstep/certificates v0.30.2 // indirect
|
||||
github.com/smallstep/cli-utils v0.12.2 // indirect
|
||||
github.com/smallstep/linkedca v0.25.0 // indirect
|
||||
github.com/smallstep/nosql v0.8.0 // indirect
|
||||
github.com/smallstep/pkcs7 v0.2.1 // indirect
|
||||
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect
|
||||
github.com/smallstep/truststore v0.13.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/spf13/afero v1.14.0 // indirect
|
||||
github.com/spf13/viper v1.12.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 // indirect
|
||||
github.com/subosito/gotenv v1.4.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
|
||||
@@ -290,7 +239,6 @@ require (
|
||||
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/urfave/cli v1.22.17 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
@@ -298,7 +246,6 @@ require (
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/zclconf/go-cty v1.17.0 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect
|
||||
@@ -325,29 +272,22 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.step.sm/crypto v0.81.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.28.0 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/time v0.16.0 // indirect
|
||||
golang.org/x/tools v0.50.0 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
google.golang.org/api v0.277.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // 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.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
k8s.io/api v0.32.3 // indirect
|
||||
k8s.io/apimachinery v0.32.3 // indirect
|
||||
k8s.io/client-go v0.32.3 // indirect
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
||||
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
||||
charm.land/huh/v2 v2.0.1 h1:9vhBjlIDuikdPKH+qnoG++GERVxqY0Lkv14xW57lj98=
|
||||
@@ -7,26 +5,8 @@ charm.land/huh/v2 v2.0.1/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
|
||||
charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
|
||||
charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
|
||||
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U=
|
||||
cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY=
|
||||
cloud.google.com/go/kms v1.31.0 h1:LS8N92OxFDgOLg5NCo3OmbvjtQAIVT5gUHVLKIDHaFE=
|
||||
cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U=
|
||||
cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY=
|
||||
cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E=
|
||||
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8=
|
||||
filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
@@ -34,30 +14,19 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
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/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/DeRuina/timberjack v1.4.2 h1:4bKlzhKdsR+2oNkgef9mqb4n11ICow8VK88RfzJPzN8=
|
||||
github.com/DeRuina/timberjack v1.4.2/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
|
||||
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/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
|
||||
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
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/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
@@ -66,9 +35,6 @@ github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWS
|
||||
github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
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=
|
||||
@@ -83,15 +49,10 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||
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/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
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/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
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/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
|
||||
@@ -112,8 +73,6 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZL
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.51.1 h1:zuSf4olLKZW8cF/W9Y5wvGT+/0raY/3kVp49KsGs0QY=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.51.1/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
|
||||
@@ -158,15 +117,11 @@ github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7l
|
||||
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
|
||||
github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=
|
||||
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@@ -194,15 +149,6 @@ github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2
|
||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
|
||||
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
@@ -210,8 +156,6 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
|
||||
github.com/clipperhouse/uax29/v2 v2.7.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/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
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=
|
||||
@@ -247,14 +191,8 @@ github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRq
|
||||
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/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
@@ -269,17 +207,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc
|
||||
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
|
||||
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
|
||||
github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o=
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=
|
||||
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
||||
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
||||
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
|
||||
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM=
|
||||
@@ -317,7 +244,6 @@ 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/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
|
||||
@@ -332,8 +258,6 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
|
||||
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
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=
|
||||
@@ -343,8 +267,6 @@ github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQ
|
||||
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
|
||||
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
@@ -403,13 +325,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM=
|
||||
github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
|
||||
github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
|
||||
@@ -422,33 +337,20 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
|
||||
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm-tools v0.4.8 h1:V4oIYyAD3BykOycwYQzO29WefDouQMTsYZqmG3HxOfM=
|
||||
github.com/google/go-tpm-tools v0.4.8/go.mod h1:4DfiOtiS1KppJjwf1+tqtW4K3PrCJjAAqFKj/TYTJKg=
|
||||
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
|
||||
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
|
||||
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/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
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.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
|
||||
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
|
||||
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
@@ -473,7 +375,6 @@ github.com/hashicorp/golang-lru/arc/v2 v2.0.7 h1:QxkVTxwColcduO+LP7eJO56r2hFiG8z
|
||||
github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
@@ -481,8 +382,6 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
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=
|
||||
@@ -492,15 +391,6 @@ github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaB
|
||||
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
|
||||
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 h1:i2fYnDurfLlJH8AyyMOnkLHnHeP8Ff/DDpuZA/D3bPo=
|
||||
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
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=
|
||||
@@ -524,7 +414,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
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.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
@@ -533,7 +422,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
|
||||
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.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
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=
|
||||
@@ -559,13 +447,10 @@ github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfs
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.0/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/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
@@ -598,20 +483,13 @@ github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ=
|
||||
github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
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/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
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.1.2/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/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/buildkit v0.25.0 h1:cRgh74ymzyHxS5a/lsYT4OCyVU8iC3UgkwasIEUi0og=
|
||||
github.com/moby/buildkit v0.25.0/go.mod h1:phM8sdqnvgK2y1dPDnbwI6veUCXHOZ6KFSl6E164tkc=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
@@ -682,18 +560,10 @@ github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0ht
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
|
||||
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
|
||||
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=
|
||||
@@ -701,8 +571,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
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/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
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=
|
||||
@@ -749,25 +617,16 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
|
||||
github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=
|
||||
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/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/siderolabs/grpc-proxy v0.5.1 h1:WTZYLMPTZPt43BzEJ02LT9kYA9qAfquWwCezc6NPPYE=
|
||||
github.com/siderolabs/grpc-proxy v0.5.1/go.mod h1:EQwE87LiWxhiIUPBeWmpjJb9DIWxWID8R6ARtdTC+8A=
|
||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
@@ -777,53 +636,24 @@ 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/slackhq/nebula v1.10.3 h1:EstYj8ODEcv6T0R9X5BVq1zgWZnyU5gtPzk99QF1PMU=
|
||||
github.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs=
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
|
||||
github.com/smallstep/certificates v0.30.2 h1:1G3xBi8sJ740iA1mMPW2Svv7EIZKJ4Zf/iQtA5QlN0Y=
|
||||
github.com/smallstep/certificates v0.30.2/go.mod h1:oyaE/aEYUGDr+YiCZLAxxP22bOQqcSHTeDgp8Vv2rlY=
|
||||
github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=
|
||||
github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
|
||||
github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo=
|
||||
github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=
|
||||
github.com/smallstep/nosql v0.8.0 h1:FBTCUfKPmWYbrozW+RBKu+fnvbn+zr5rVli/XB4Jp4A=
|
||||
github.com/smallstep/nosql v0.8.0/go.mod h1:5dUpNotHLHhOUapP0PLBVVfp3tG1DFC31VRccg+Cqwo=
|
||||
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
|
||||
github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=
|
||||
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA=
|
||||
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492/go.mod h1:QQhwLqCS13nhv8L5ov7NgusowENUtXdEzdytjmJHdZQ=
|
||||
github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4=
|
||||
github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
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/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
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/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3/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/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
|
||||
github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -831,7 +661,6 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
@@ -839,20 +668,13 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
|
||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
||||
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 h1:RnBbFMmodYzhC6adOjTbtUQXyzV8dcvKYbolzs6Qch0=
|
||||
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747/go.mod h1:ejPAJui3kVK4u5TgMtqtXlWf5HnKh9fLy5kvpaeuas0=
|
||||
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=
|
||||
@@ -867,11 +689,8 @@ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/
|
||||
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/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/unlabs-dev/bubbletea/v2 v2.0.8-uncloud.1 h1:898LVw5QvmRX/y+pgzCo2pE/NbN9CZfdpiVnzAeAFDY=
|
||||
github.com/unlabs-dev/bubbletea/v2 v2.0.8-uncloud.1/go.mod h1:DGW2q8gvzHnOpMpZTORs0aySVHCox5C+2Svk0fci1qs=
|
||||
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
|
||||
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
|
||||
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/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
@@ -884,7 +703,6 @@ github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
@@ -896,8 +714,6 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
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=
|
||||
@@ -952,10 +768,6 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.step.sm/crypto v0.81.0 h1:e+ouzpNt3Xm4dp7HGXhgYB5y4iFik3vh3phHKWmvugU=
|
||||
go.step.sm/crypto v0.81.0/go.mod h1:fsTizqQeASjTXnbv9O00XtRlIuXRkCdoRiJNyXGQujc=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
@@ -974,7 +786,6 @@ go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/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=
|
||||
@@ -982,14 +793,8 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh
|
||||
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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
@@ -999,10 +804,6 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
|
||||
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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1019,11 +820,6 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -1036,23 +832,15 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190222072716-a9d3bda3a223/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-20190626221950-04f50cda93cb/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=
|
||||
@@ -1061,44 +849,24 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
@@ -1112,9 +880,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
|
||||
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1127,15 +892,11 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvY
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk=
|
||||
google.golang.org/api v0.277.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM=
|
||||
@@ -1148,8 +909,6 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
|
||||
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -1166,7 +925,6 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
|
||||
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-20190902080502-41f04d3bba15/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=
|
||||
@@ -1181,7 +939,6 @@ 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.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
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=
|
||||
@@ -1195,8 +952,6 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
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=
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
@@ -57,14 +58,12 @@ https://{{$hostname}} {
|
||||
log
|
||||
}{{end}}
|
||||
`
|
||||
caddyfileUnavailabeFooter = `# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
||||
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
|
||||
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
|
||||
# and its logs for more details (uc logs caddy).
|
||||
`
|
||||
bootstrapMarker = "# Uncloud bootstrap for Caddy container "
|
||||
legacyBootstrapMarker = "# NOTE: User-defined configs for services were skipped because Caddy is not running"
|
||||
)
|
||||
|
||||
// CaddyfileGenerator generates a Caddyfile configuration for the Caddy reverse proxy.
|
||||
// It combines generated routes from published ports with user-defined Caddyfile snippets from service specs (x-caddy).
|
||||
type CaddyfileGenerator struct {
|
||||
// machineID is the unique identifier of the machine where the controller is running.
|
||||
machineID string
|
||||
@@ -74,11 +73,18 @@ type CaddyfileGenerator struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// CaddyfileValidator is an interface for validating Caddyfile configurations.
|
||||
// CaddyfileValidator checks a candidate Caddyfile without loading it. Validate returns InvalidCaddyfileError only
|
||||
// when the candidate is known to be invalid. Other errors mean the check could not be completed. A successful check
|
||||
// does not guarantee that the configuration will load.
|
||||
type CaddyfileValidator interface {
|
||||
Validate(ctx context.Context, caddyfile string) error
|
||||
}
|
||||
|
||||
// InvalidCaddyfileError means validation completed and the candidate Caddyfile was rejected.
|
||||
type InvalidCaddyfileError struct{ Message string }
|
||||
|
||||
func (e *InvalidCaddyfileError) Error() string { return e.Message }
|
||||
|
||||
func NewCaddyfileGenerator(
|
||||
machineID, machineName string, validator CaddyfileValidator, log *slog.Logger,
|
||||
) *CaddyfileGenerator {
|
||||
@@ -93,35 +99,41 @@ func NewCaddyfileGenerator(
|
||||
}
|
||||
}
|
||||
|
||||
// Generate creates a Caddyfile configuration based on the provided service containers.
|
||||
// The Caddyfile is generated from the service ports of the healthy containers.
|
||||
// If a 'caddy' service container is running on this machine and defines a custom Caddy config (x-caddy) in its service
|
||||
// spec, it will be validated and prepended to the generated Caddyfile. Custom Caddy configs (x-caddy) defined in other
|
||||
// service specs are validated and appended to the generated Caddyfile. Invalid configs are logged and skipped to ensure
|
||||
// the generated Caddyfile remains valid.
|
||||
// Generate creates a Caddyfile for the local Caddy container from the supplied healthy application containers.
|
||||
// Application service ports provide the generated sites. The Caddy container's custom Caddy config (x-caddy), if
|
||||
// defined, provides the global block even when that container is unhealthy. When application custom Caddy configs
|
||||
// are included, the newest container for each service provides its config.
|
||||
//
|
||||
// The final Caddyfile structure includes:
|
||||
// The resulting Caddyfile has this structure:
|
||||
//
|
||||
// [caddy x-caddy (global config)]
|
||||
// [generated Caddyfile from all service ports]
|
||||
// [service-a x-caddy]
|
||||
// [caddy custom Caddy config (global)]
|
||||
// [generated sites from application service ports]
|
||||
// [service-a custom Caddy config]
|
||||
// ...
|
||||
// [service-z x-caddy]
|
||||
// [service-z custom Caddy config]
|
||||
//
|
||||
// If includeCustom is false, custom Caddy configs (x-caddy) are not included in the generated Caddyfile.
|
||||
// Bootstrap mode keeps the global custom Caddy config and generated sites, but omits application custom Caddy configs.
|
||||
// It skips validation because Caddy may not be running yet. In this case, Caddy validates the config when it starts.
|
||||
// Global template errors still stop generation. In full mode, invalid globals stop generation, while invalid
|
||||
// application custom Caddy configs are logged and skipped.
|
||||
func (g *CaddyfileGenerator) Generate(
|
||||
ctx context.Context, records []store.ContainerRecord, includeCustom bool,
|
||||
ctx context.Context,
|
||||
caddyCtr api.ServiceContainer,
|
||||
records []store.ContainerRecord,
|
||||
bootstrap bool,
|
||||
) (string, error) {
|
||||
records = slices.Clone(records)
|
||||
// Sort records by local machine first, then by service name and creation time. Placing containers on the local
|
||||
// machine first lets user-defined Caddy configs pair this ordering with the "first" lb_policy to always send
|
||||
// traffic to the same-host replica (skipping the cross-machine hop) and only fall back to remote upstreams when
|
||||
// the local one is unhealthy.
|
||||
// The service name and creation time tiebreakers keep the generated Caddyfile stable across regenerations.
|
||||
// The service name, creation time, and container ID tiebreakers keep the file stable across regenerations.
|
||||
slices.SortStableFunc(records, func(a, b store.ContainerRecord) int {
|
||||
return cmp.Or(
|
||||
g.localMachineRank(a.MachineID)-g.localMachineRank(b.MachineID),
|
||||
strings.Compare(a.Container.ServiceName(), b.Container.ServiceName()),
|
||||
a.Container.CreatedTime().Compare(b.Container.CreatedTime()),
|
||||
strings.Compare(a.Container.ID, b.Container.ID),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -136,26 +148,11 @@ func (g *CaddyfileGenerator) Generate(
|
||||
}
|
||||
|
||||
caddyfileHeader := fmt.Sprintf(caddyfileHeaderFmt, g.machineName, time.Now().UTC().Format(time.RFC3339))
|
||||
if !includeCustom {
|
||||
return fmt.Sprintf("%s\n%s\n%s", caddyfileHeader, caddyfile, caddyfileUnavailabeFooter), nil
|
||||
}
|
||||
|
||||
upstreams := serviceUpstreams(containers)
|
||||
// Track validation errors for reporting.
|
||||
var configErrors []string
|
||||
|
||||
// Find the 'caddy' service container on this machine. Use the most recent one if multiple exist.
|
||||
var caddyCtr *api.ServiceContainer
|
||||
for _, cr := range records {
|
||||
if cr.MachineID == g.machineID && cr.Container.ServiceName() == CaddyServiceName &&
|
||||
(caddyCtr == nil || cr.Container.CreatedTime().Compare(caddyCtr.CreatedTime()) > 0) {
|
||||
caddyCtr = &cr.Container
|
||||
}
|
||||
}
|
||||
|
||||
// If the caddy container is running on this machine and has a custom Caddy config (global),
|
||||
// prepend it to the generated Caddyfile and validate it.
|
||||
if caddyCtr != nil && caddyCtr.ServiceSpec.CaddyConfig() != "" {
|
||||
if caddyCtr.ServiceSpec.CaddyConfig() != "" {
|
||||
// Render the custom global Caddy config as a Go template with the upstreams.
|
||||
tmplCtx := templateContext{
|
||||
Name: caddyCtr.ServiceName(),
|
||||
@@ -163,23 +160,23 @@ func (g *CaddyfileGenerator) Generate(
|
||||
}
|
||||
renderedConfig, err := renderCaddyfile(tmplCtx, caddyCtr.ServiceSpec.CaddyConfig())
|
||||
if err != nil {
|
||||
g.log.Error("Failed to render template directives in user-defined global Caddy config, skipping it.",
|
||||
"service", caddyCtr.ServiceName(), "container", caddyCtr.ID, "err", err)
|
||||
configErrors = append(configErrors,
|
||||
fmt.Sprintf("service '%s': failed to render template: %v", caddyCtr.ServiceName(), err))
|
||||
} else {
|
||||
caddyfileCandidate := fmt.Sprintf("# User-defined global config from service '%s'.\n%s\n\n%s",
|
||||
caddyCtr.ServiceName(), renderedConfig, caddyfile)
|
||||
|
||||
return "", fmt.Errorf(
|
||||
"render template directives in user-defined global Caddy config from Caddy container %s: %w",
|
||||
caddyCtr.ShortID(), err)
|
||||
}
|
||||
caddyfileCandidate := fmt.Sprintf("# User-defined global config from service '%s'.\n%s\n\n%s",
|
||||
caddyCtr.ServiceName(), renderedConfig, caddyfile)
|
||||
if !bootstrap && g.validator != nil {
|
||||
if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil {
|
||||
g.log.Error("User-defined global Caddy config is invalid, skipping it.",
|
||||
"service", caddyCtr.ServiceName(), "container", caddyCtr.ID, "err", err)
|
||||
configErrors = append(configErrors,
|
||||
fmt.Sprintf("service '%s': validation failed: %v", caddyCtr.ServiceName(), err))
|
||||
} else {
|
||||
caddyfile = caddyfileCandidate
|
||||
return "", fmt.Errorf("validate user-defined global Caddy config from Caddy container %s: %w",
|
||||
caddyCtr.ShortID(), err)
|
||||
}
|
||||
}
|
||||
caddyfile = caddyfileCandidate
|
||||
}
|
||||
|
||||
if bootstrap {
|
||||
return caddyfileHeader + bootstrapMarker + caddyCtr.ID + "\n\n" + caddyfile, nil
|
||||
}
|
||||
|
||||
// There could be multiple service containers for the same service with different custom Caddy configs, for example,
|
||||
@@ -200,7 +197,7 @@ func (g *CaddyfileGenerator) Generate(
|
||||
// Append a custom Caddy config for each service to the Caddyfile and validate it. If the config for a service
|
||||
// is invalid, skip it but continue processing other services to ensure the Caddyfile remains valid.
|
||||
for _, serviceName := range sortedServiceNames {
|
||||
// Skip the caddy container as we already processed it.
|
||||
// Skip the caddy container as we already processed it as the global config.
|
||||
if serviceName == CaddyServiceName {
|
||||
continue
|
||||
}
|
||||
@@ -226,16 +223,22 @@ func (g *CaddyfileGenerator) Generate(
|
||||
|
||||
caddyfileCandidate := fmt.Sprintf("%s\n# User-defined config for service '%s'.\n%s\n",
|
||||
caddyfile, serviceName, renderedConfig)
|
||||
if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil {
|
||||
g.log.Error("User-defined Caddy config for service is invalid, skipping it.",
|
||||
"service", serviceName, "err", err)
|
||||
configErrors = append(configErrors, fmt.Sprintf("service '%s': validation failed: %v", serviceName, err))
|
||||
} else {
|
||||
caddyfile = caddyfileCandidate
|
||||
if g.validator != nil {
|
||||
if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil {
|
||||
if _, ok := errors.AsType[*InvalidCaddyfileError](err); !ok {
|
||||
return "", fmt.Errorf("validate Caddy config for service '%s': %w", serviceName, err)
|
||||
}
|
||||
g.log.Error("User-defined Caddy config for service is invalid, skipping it.",
|
||||
"service", serviceName, "err", err)
|
||||
configErrors = append(configErrors,
|
||||
fmt.Sprintf("service '%s': validation failed: %v", serviceName, err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
caddyfile = caddyfileCandidate
|
||||
}
|
||||
|
||||
// Append error summary as comment if there were any invalid configs.
|
||||
// Keep skipped-snippet errors in the saved file so an operator can see why routes are missing after a restart.
|
||||
if len(configErrors) > 0 {
|
||||
var errorsComment strings.Builder
|
||||
errorsComment.WriteString("# Skipped invalid user-defined configs:\n")
|
||||
@@ -249,8 +252,7 @@ func (g *CaddyfileGenerator) Generate(
|
||||
return caddyfileHeader + "\n" + caddyfile, nil
|
||||
}
|
||||
|
||||
// localMachineRank returns 0 if the given machineID matches the local machine and 1 otherwise.
|
||||
// Useful for sorting containers running locally first.
|
||||
// localMachineRank is a sorting function for containers that puts running on the local machine first.
|
||||
func (g *CaddyfileGenerator) localMachineRank(machineID string) int {
|
||||
if g.machineID == machineID {
|
||||
return 0
|
||||
|
||||
@@ -208,10 +208,11 @@ http://app.example.com {
|
||||
ctx := context.Background()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Validator is not expected to be called in these tests.
|
||||
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", nil, nil)
|
||||
validator := NewMockCaddyfileValidator(t)
|
||||
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
|
||||
|
||||
config, err := generator.Generate(ctx, tt.containers, true)
|
||||
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.1", "", "", time.Now()).Container
|
||||
config, err := generator.Generate(ctx, caddyCtr, tt.containers, false)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
@@ -332,7 +333,7 @@ bad.template.com {
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "caddy service with invalid global config is skipped",
|
||||
name: "caddy service with invalid global config aborts",
|
||||
containers: []store.ContainerRecord{
|
||||
newContainerRecordWithCaddyConfig(
|
||||
"caddy",
|
||||
@@ -345,10 +346,7 @@ localhost {
|
||||
time.Now(),
|
||||
),
|
||||
},
|
||||
want: testCaddyfileHeader + `
|
||||
# Skipped invalid user-defined configs:
|
||||
# - service 'caddy': validation failed: invalid config detected
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "caddy service on different machine is ignored",
|
||||
@@ -786,7 +784,7 @@ web-v2.example.com {
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "multiple errors: invalid global, template error, and validation error",
|
||||
name: "invalid global aborts before application configs",
|
||||
containers: []store.ContainerRecord{
|
||||
newContainerRecordWithCaddyConfig(
|
||||
"caddy",
|
||||
@@ -827,17 +825,7 @@ invalid.example.com {
|
||||
time.Now(),
|
||||
),
|
||||
},
|
||||
want: testCaddyfileHeader + `
|
||||
# User-defined config for service 'valid'.
|
||||
valid.example.com {
|
||||
respond "Valid config"
|
||||
}
|
||||
|
||||
# Skipped invalid user-defined configs:
|
||||
# - service 'caddy': validation failed: invalid config detected
|
||||
# - service 'broken-template': failed to render template: parse config as Go template: template: Caddyfile:2: unterminated quoted string
|
||||
# - service 'invalid': validation failed: invalid config detected
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -846,7 +834,7 @@ valid.example.com {
|
||||
validator.EXPECT().Validate(mock.Anything, mock.Anything).RunAndReturn(
|
||||
func(ctx context.Context, caddyfile string) error {
|
||||
if strings.Contains(caddyfile, "# test:invalid") {
|
||||
return errors.New("invalid config detected")
|
||||
return &InvalidCaddyfileError{Message: "invalid config detected"}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -855,7 +843,8 @@ valid.example.com {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
|
||||
|
||||
config, err := generator.Generate(ctx, tt.containers, true)
|
||||
caddyCtr := caddyContainerFromTestContainers(tt.containers)
|
||||
config, err := generator.Generate(ctx, caddyCtr, tt.containers, false)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
@@ -868,6 +857,167 @@ valid.example.com {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaddyfileGeneratorBootstrap(t *testing.T) {
|
||||
// Bootstrap keeps the local Caddy container's globals and generated routes, but omits application custom configs.
|
||||
tests := []struct {
|
||||
name string
|
||||
containers []store.ContainerRecord
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "global config retained and application custom configs skipped",
|
||||
containers: []store.ContainerRecord{
|
||||
newContainerRecordWithCaddyConfig(
|
||||
"caddy",
|
||||
"10.210.0.1",
|
||||
`# Global config
|
||||
{
|
||||
global directive
|
||||
}`,
|
||||
"test-machine-id",
|
||||
time.Now(),
|
||||
),
|
||||
newContainerRecordWithCaddyConfig(
|
||||
"web",
|
||||
"10.210.0.2",
|
||||
`web.example.com {
|
||||
reverse_proxy web:3000
|
||||
}`,
|
||||
"test-machine-id",
|
||||
time.Now(),
|
||||
),
|
||||
newContainerRecordWithPorts(
|
||||
"api",
|
||||
"10.210.0.3",
|
||||
[]string{"api.example.com:8080/http"},
|
||||
"test-machine-id",
|
||||
),
|
||||
},
|
||||
want: strings.Replace(testCaddyfileHeader, "\n\n# Health check endpoint", `
|
||||
# Uncloud bootstrap for Caddy container caddy-10.210.0.1
|
||||
|
||||
# User-defined global config from service 'caddy'.
|
||||
# Global config
|
||||
{
|
||||
global directive
|
||||
}
|
||||
|
||||
# Health check endpoint`, 1) + `
|
||||
# Sites generated from service ports.
|
||||
|
||||
http://api.example.com {
|
||||
reverse_proxy 10.210.0.3:8080 {
|
||||
import common_proxy
|
||||
}
|
||||
log
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "no containers with x-caddy configs",
|
||||
containers: []store.ContainerRecord{
|
||||
newContainerRecordWithPorts(
|
||||
"api",
|
||||
"10.210.0.3",
|
||||
[]string{"api.example.com:8080/http"},
|
||||
"test-machine-id",
|
||||
),
|
||||
},
|
||||
want: strings.Replace(testCaddyfileHeader, "\n\n# Health check endpoint", `
|
||||
# Uncloud bootstrap for Caddy container caddy-10.210.0.1
|
||||
|
||||
# Health check endpoint`, 1) + `
|
||||
# Sites generated from service ports.
|
||||
|
||||
http://api.example.com {
|
||||
reverse_proxy 10.210.0.3:8080 {
|
||||
import common_proxy
|
||||
}
|
||||
log
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
validator := NewMockCaddyfileValidator(t)
|
||||
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
|
||||
|
||||
caddyContainer := caddyContainerFromTestContainers(tt.containers)
|
||||
config, err := generator.Generate(ctx, caddyContainer, tt.containers, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaddyfileGenerator_ValidatorTransportErrorAbortsGeneration(t *testing.T) {
|
||||
validator := NewMockCaddyfileValidator(t)
|
||||
validator.EXPECT().Validate(mock.Anything, mock.Anything).Return(errors.New("socket disappeared"))
|
||||
|
||||
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
|
||||
app := newContainerRecordWithCaddyConfig(
|
||||
"web",
|
||||
"10.210.0.2",
|
||||
"web.example.com { respond ok }",
|
||||
"test-machine-id",
|
||||
time.Now(),
|
||||
)
|
||||
|
||||
_, err := generator.Generate(context.Background(), caddyContainerFromTestContainers(nil),
|
||||
[]store.ContainerRecord{app}, false)
|
||||
require.ErrorContains(t, err, "socket disappeared")
|
||||
}
|
||||
|
||||
func newContainer(ip string, ports ...string) api.ServiceContainer {
|
||||
portsLabel := strings.Join(ports, ",")
|
||||
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
State: &container.State{
|
||||
Running: true,
|
||||
},
|
||||
},
|
||||
NetworkSettings: &container.NetworkSettings{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
docker.NetworkName: {
|
||||
IPAddress: ip,
|
||||
},
|
||||
},
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
api.LabelServicePorts: portsLabel,
|
||||
},
|
||||
},
|
||||
}}}
|
||||
}
|
||||
|
||||
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
||||
portsLabel := strings.Join(ports, ",")
|
||||
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
State: &container.State{
|
||||
Running: true,
|
||||
},
|
||||
},
|
||||
NetworkSettings: &container.NetworkSettings{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"other-network": {
|
||||
IPAddress: "172.17.0.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
api.LabelServicePorts: portsLabel,
|
||||
},
|
||||
},
|
||||
}}}
|
||||
}
|
||||
|
||||
func newContainerRecord(ctr api.ServiceContainer, machineID string) store.ContainerRecord {
|
||||
return store.ContainerRecord{
|
||||
Container: ctr,
|
||||
@@ -911,100 +1061,6 @@ func newContainerRecordWithCaddyConfig(serviceName, ip, caddyConfig, machineID s
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaddyfileGeneratorWithoutCustomConfigs(t *testing.T) {
|
||||
// Test that when includeCustom is false (Caddy not available), x-caddy configs are skipped.
|
||||
tests := []struct {
|
||||
name string
|
||||
containers []store.ContainerRecord
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "x-caddy configs are skipped",
|
||||
containers: []store.ContainerRecord{
|
||||
newContainerRecordWithCaddyConfig(
|
||||
"caddy",
|
||||
"10.210.0.1",
|
||||
`# Global config
|
||||
{
|
||||
global directive
|
||||
}`,
|
||||
"test-machine-id",
|
||||
time.Now(),
|
||||
),
|
||||
newContainerRecordWithCaddyConfig(
|
||||
"web",
|
||||
"10.210.0.2",
|
||||
`web.example.com {
|
||||
reverse_proxy web:3000
|
||||
}`,
|
||||
"test-machine-id",
|
||||
time.Now(),
|
||||
),
|
||||
newContainerRecordWithPorts(
|
||||
"api",
|
||||
"10.210.0.3",
|
||||
[]string{"api.example.com:8080/http"},
|
||||
"test-machine-id",
|
||||
),
|
||||
},
|
||||
want: testCaddyfileHeader + `
|
||||
# Sites generated from service ports.
|
||||
|
||||
http://api.example.com {
|
||||
reverse_proxy 10.210.0.3:8080 {
|
||||
import common_proxy
|
||||
}
|
||||
log
|
||||
}
|
||||
|
||||
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
||||
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
|
||||
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
|
||||
# and its logs for more details (uc logs caddy).
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "no containers with x-caddy configs",
|
||||
containers: []store.ContainerRecord{
|
||||
newContainerRecordWithPorts(
|
||||
"api",
|
||||
"10.210.0.3",
|
||||
[]string{"api.example.com:8080/http"},
|
||||
"test-machine-id",
|
||||
),
|
||||
},
|
||||
want: testCaddyfileHeader + `
|
||||
# Sites generated from service ports.
|
||||
|
||||
http://api.example.com {
|
||||
reverse_proxy 10.210.0.3:8080 {
|
||||
import common_proxy
|
||||
}
|
||||
log
|
||||
}
|
||||
|
||||
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
|
||||
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
|
||||
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
|
||||
# and its logs for more details (uc logs caddy).
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Validator is not expected to be called in these tests.
|
||||
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", nil, nil)
|
||||
|
||||
config, err := generator.Generate(ctx, tt.containers, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newContainerRecordWithPorts(serviceName, ip string, ports []string, machineID string) store.ContainerRecord {
|
||||
portsLabel := strings.Join(ports, ",")
|
||||
return store.ContainerRecord{
|
||||
@@ -1037,3 +1093,18 @@ func newContainerRecordWithPorts(serviceName, ip string, ports []string, machine
|
||||
MachineID: machineID,
|
||||
}
|
||||
}
|
||||
|
||||
// caddyContainerFromTestContainers returns the local Caddy container from the provided test containers,
|
||||
// or creates a default one if not found.
|
||||
func caddyContainerFromTestContainers(records []store.ContainerRecord) api.ServiceContainer {
|
||||
if caddyCtr := selectLocalCaddyContainer(records, "test-machine-id"); caddyCtr != nil {
|
||||
return *caddyCtr
|
||||
}
|
||||
return newContainerRecordWithCaddyConfig(
|
||||
"caddy",
|
||||
"10.210.0.1",
|
||||
"",
|
||||
"test-machine-id",
|
||||
time.Now(),
|
||||
).Container
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package caddyconfig
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -26,15 +25,15 @@ func NewCaddyAdminClient(socketPath string) *CaddyAdminClient {
|
||||
client: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||
return net.Dial("unix", socketPath)
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return (&net.Dialer{}).DialContext(ctx, "unix", socketPath)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// IsAvailable checks if the local Caddy instance is listening on the admin socket.
|
||||
// IsAvailable checks whether a Caddy process accepts connections at the admin socket.
|
||||
func (c *CaddyAdminClient) IsAvailable() bool {
|
||||
conn, err := net.DialTimeout("unix", c.socketPath, 1*time.Second)
|
||||
// A stale socket file left over from a crashed Caddy container returns ECONNREFUSED so this is correctly handled
|
||||
@@ -80,15 +79,16 @@ func (c *CaddyAdminClient) Adapt(ctx context.Context, caddyfile string) (string,
|
||||
// If the response is a 400 Bad Request, try to parse the error message from it.
|
||||
if resp.StatusCode == http.StatusBadRequest {
|
||||
var apiError caddy.APIError
|
||||
if err = json.Unmarshal(body, &apiError); err == nil {
|
||||
return "", errors.New(apiError.Message)
|
||||
if err = json.Unmarshal(body, &apiError); err == nil && apiError.Message != "" {
|
||||
return "", &InvalidCaddyfileError{Message: apiError.Message}
|
||||
}
|
||||
return "", &InvalidCaddyfileError{Message: string(body)}
|
||||
}
|
||||
|
||||
return "", errors.New(string(body))
|
||||
return "", fmt.Errorf("adapt request failed: HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Load loads a Caddyfile configuration into the Caddy instance running on the machine.
|
||||
// Load loads a Caddyfile configuration into the Caddy instance.
|
||||
// Due to a Caddy bug (https://github.com/caddyserver/caddy/issues/7246), we first adapt the Caddyfile to JSON
|
||||
// and then load the JSON config to get proper error handling.
|
||||
func (c *CaddyAdminClient) Load(ctx context.Context, caddyfile string) error {
|
||||
@@ -96,7 +96,11 @@ func (c *CaddyAdminClient) Load(ctx context.Context, caddyfile string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("adapt Caddyfile to JSON config: %w", err)
|
||||
}
|
||||
return c.LoadJSON(ctx, jsonConfig)
|
||||
}
|
||||
|
||||
// LoadJSON loads a JSON configuration into the Caddy instance.
|
||||
func (c *CaddyAdminClient) LoadJSON(ctx context.Context, jsonConfig string) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", "http://localhost/load", strings.NewReader(jsonConfig))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create load request: %w", err)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package caddyconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testAdminServer(t *testing.T, handler http.HandlerFunc) string {
|
||||
t.Helper()
|
||||
// macOS limits Unix socket paths to 104 bytes. t.TempDir can exceed that.
|
||||
dir, err := os.MkdirTemp("/tmp", "uc-caddy-")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = os.RemoveAll(dir)
|
||||
})
|
||||
|
||||
socketPath := filepath.Join(dir, "admin.sock")
|
||||
listener, err := net.Listen("unix", socketPath)
|
||||
require.NoError(t, err)
|
||||
server := httptest.NewUnstartedServer(handler)
|
||||
_ = server.Listener.Close()
|
||||
server.Listener = listener
|
||||
server.Start()
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
return socketPath
|
||||
}
|
||||
|
||||
func TestCaddyAdminClient_Validate(t *testing.T) {
|
||||
var status atomic.Int64
|
||||
status.Store(http.StatusBadRequest)
|
||||
socket := testAdminServer(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(int(status.Load()))
|
||||
_, _ = w.Write([]byte(`{"message":"invalid config"}`))
|
||||
})
|
||||
client := NewCaddyAdminClient(socket)
|
||||
err := client.Validate(context.Background(), "invalid")
|
||||
var invalid *InvalidCaddyfileError
|
||||
assert.ErrorAs(t, err, &invalid)
|
||||
|
||||
status.Store(http.StatusInternalServerError)
|
||||
err = client.Validate(context.Background(), "invalid")
|
||||
assert.Error(t, err)
|
||||
assert.NotErrorAs(t, err, &invalid)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package caddyconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/psviderski/uncloud/internal/fs"
|
||||
"github.com/psviderski/uncloud/internal/machine/store"
|
||||
@@ -20,27 +21,40 @@ const (
|
||||
CaddyServiceName = "caddy"
|
||||
CaddyGroup = "uncloud"
|
||||
VerifyPath = "/.uncloud-verify"
|
||||
// periodicReconciliationInterval is the interval at which the controller reconciles the Caddyfile
|
||||
// even if no container changes are observed.
|
||||
periodicReconciliationInterval = 30 * time.Second
|
||||
)
|
||||
|
||||
// Controller monitors container changes in the cluster store and generates a configuration file for Caddy reverse
|
||||
// proxy. The generated configuration allows Caddy to route external traffic to service containers across the internal
|
||||
// network.
|
||||
// Controller keeps the local Caddy reverse proxy in sync with container state from the cluster store. It writes a
|
||||
// Caddyfile that routes external traffic to healthy service containers across the internal network.
|
||||
//
|
||||
// Current Caddy deployments share one Caddyfile and one admin socket per machine. The controller must preserve a
|
||||
// saved full Caddyfile when Caddy is unavailable because that file may be needed to restart or roll back a container.
|
||||
// It writes a reduced bootstrap config only when no full file exists, including when an older offline file must be
|
||||
// replaced. The shared layout cannot give overlapping Caddy revisions separate configurations.
|
||||
type Controller struct {
|
||||
machineID string
|
||||
caddyfilePath string
|
||||
service *Service
|
||||
generator *CaddyfileGenerator
|
||||
client *CaddyAdminClient
|
||||
store *store.Store
|
||||
log *slog.Logger
|
||||
// lastFingerprint caches the fingerprint of the containers used to generate the latest successfully loaded
|
||||
// Caddyfile. nil means it hasn't been loaded yet or the last load failed.
|
||||
// Tests use their own group so they can exercise real file publication without the daemon's uncloud group.
|
||||
fileGroup string
|
||||
// lastFingerprint records the healthy application-container inputs used for the last full Caddyfile that was
|
||||
// successfully loaded into Caddy and saved to disk. It excludes the Caddy container, whose identity, start time,
|
||||
// and global config are tracked separately below.
|
||||
lastFingerprint []containerFingerprint
|
||||
// lastCaddyfile caches the last generated Caddyfile.
|
||||
lastCaddyfile string
|
||||
lastCaddyCtrID string
|
||||
lastStartedAt string
|
||||
lastGlobal string
|
||||
lastSavedBody string
|
||||
}
|
||||
|
||||
// containerFingerprint is the subset of container data that the Caddyfile generator depends on.
|
||||
// Comparing fingerprints lets the controller skip no-op regenerations.
|
||||
// containerFingerprint contains container identity and routing inputs that can change the generated Caddyfile.
|
||||
// It excludes incidental Docker state so unrelated container updates do not cause a Caddy reload.
|
||||
type containerFingerprint struct {
|
||||
ID string
|
||||
IP netip.Addr
|
||||
@@ -56,7 +70,8 @@ func (f containerFingerprint) Equal(other containerFingerprint) bool {
|
||||
f.CaddyConfig == other.CaddyConfig
|
||||
}
|
||||
|
||||
func NewController(machineID, configDir, adminSock string, store *store.Store) (*Controller, error) {
|
||||
func NewController(machineID string, service *Service, adminSock string, store *store.Store) (*Controller, error) {
|
||||
configDir := service.configDir
|
||||
if err := os.MkdirAll(configDir, 0o750); err != nil {
|
||||
return nil, fmt.Errorf("create directory for Caddy configuration '%s': %w", configDir, err)
|
||||
}
|
||||
@@ -67,16 +82,21 @@ func NewController(machineID, configDir, adminSock string, store *store.Store) (
|
||||
log := slog.With("component", "caddy-controller")
|
||||
client := NewCaddyAdminClient(adminSock)
|
||||
|
||||
// generator is initialised by Run() once the machine name is resolved from the store.
|
||||
// The generator is initialised by Run() after resolving the machine name from the store.
|
||||
return &Controller{
|
||||
machineID: machineID,
|
||||
caddyfilePath: filepath.Join(configDir, "Caddyfile"),
|
||||
service: service,
|
||||
client: client,
|
||||
store: store,
|
||||
log: log,
|
||||
fileGroup: CaddyGroup,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Run reconciles on container changes and every periodicReconciliationInterval. Failed work is retried every second,
|
||||
// but repeated retry failures are logged only during the periodic check.
|
||||
// The periodic check also covers missed events and outstanding failures after the daemon restarts.
|
||||
func (c *Controller) Run(ctx context.Context) error {
|
||||
// Default the machine name to the machine ID so the Caddyfile header still carries a stable identifier if
|
||||
// the store lookup fails.
|
||||
@@ -95,14 +115,35 @@ func (c *Controller) Run(ctx context.Context) error {
|
||||
}
|
||||
c.log.Info("Subscribed to container changes in the cluster to generate Caddy configuration.")
|
||||
|
||||
containers = filterHealthyContainers(containers)
|
||||
c.generateAndLoadCaddyfile(ctx, containers)
|
||||
var retryC <-chan time.Time
|
||||
periodic := time.NewTicker(periodicReconciliationInterval)
|
||||
defer periodic.Stop()
|
||||
|
||||
// TODO: left for backward compatibility, remove later.
|
||||
if err = c.generateJSONConfig(containers); err != nil {
|
||||
c.log.Error("Failed to generate Caddy JSON configuration to disk.", "err", err)
|
||||
handleResult := func(err error, logFailure bool) {
|
||||
c.service.setReconciliationResult(err)
|
||||
if err == nil {
|
||||
retryC = nil
|
||||
return
|
||||
}
|
||||
if logFailure {
|
||||
c.log.Error("Failed to reconcile Caddy configuration, will retry.", "err", err)
|
||||
}
|
||||
retryC = time.After(time.Second)
|
||||
}
|
||||
|
||||
reconcile := func(logFailure bool) {
|
||||
ctrs, err := c.store.ListContainers(ctx, store.ListOptions{})
|
||||
if err != nil {
|
||||
err = fmt.Errorf("list containers: %w", err)
|
||||
} else {
|
||||
err = c.generateAndLoadCaddyfile(ctx, ctrs)
|
||||
}
|
||||
handleResult(err, logFailure)
|
||||
}
|
||||
|
||||
// The subscription supplies an initial snapshot, so the first attempt need not wait for a change event.
|
||||
handleResult(c.generateAndLoadCaddyfile(ctx, containers), true)
|
||||
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-changes:
|
||||
@@ -111,25 +152,18 @@ func (c *Controller) Run(ctx context.Context) error {
|
||||
}
|
||||
c.log.Debug("Cluster containers changed, regenerating Caddy configuration.")
|
||||
|
||||
containers, err = c.store.ListContainers(ctx, store.ListOptions{})
|
||||
if err != nil {
|
||||
c.log.Error("Failed to list containers.", "err", err)
|
||||
continue
|
||||
}
|
||||
containers = filterHealthyContainers(containers)
|
||||
c.generateAndLoadCaddyfile(ctx, containers)
|
||||
|
||||
// TODO: left for backward compatibility, remove later.
|
||||
if err = c.generateJSONConfig(containers); err != nil {
|
||||
c.log.Error("Failed to generate Caddy JSON configuration to disk.", "err", err)
|
||||
}
|
||||
reconcile(true)
|
||||
case <-periodic.C:
|
||||
reconcile(true)
|
||||
case <-retryC:
|
||||
reconcile(false)
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filterHealthyContainers filters out unhealthy and hook containers.
|
||||
// filterHealthyContainers filters out unhealthy, hook, and caddy containers.
|
||||
// TODO: Filters out containers from this machine that are likely unavailable. The availability can be determined
|
||||
// by the cluster membership state of the machine that the container is running on. Implement machine membership
|
||||
// check using Corrossion Admin client.
|
||||
@@ -139,6 +173,9 @@ func filterHealthyContainers(containers []store.ContainerRecord) []store.Contain
|
||||
if cr.Container.IsHook() {
|
||||
continue
|
||||
}
|
||||
if cr.Container.ServiceName() == CaddyServiceName {
|
||||
continue
|
||||
}
|
||||
if cr.Container.Healthy() {
|
||||
healthy = append(healthy, cr)
|
||||
}
|
||||
@@ -146,65 +183,96 @@ func filterHealthyContainers(containers []store.ContainerRecord) []store.Contain
|
||||
return healthy
|
||||
}
|
||||
|
||||
// generateAndLoadCaddyfile regenerates the Caddyfile from the given containers and loads it into the local Caddy
|
||||
// if available.
|
||||
func (c *Controller) generateAndLoadCaddyfile(ctx context.Context, containers []store.ContainerRecord) {
|
||||
// Check if Caddy is available before attempting to generate and load config.
|
||||
caddyAvailable := c.client.IsAvailable()
|
||||
|
||||
// Skip regeneration when Caddy is available and the containers since the last successful load haven't changed.
|
||||
// When Caddy is unavailable we still regenerate to keep the Caddyfile on disk updated.
|
||||
fingerprint := fingerprintContainers(containers)
|
||||
if caddyAvailable && slices.EqualFunc(fingerprint, c.lastFingerprint, containerFingerprint.Equal) {
|
||||
c.log.Debug("Caddy configuration is unchanged.", "path", c.caddyfilePath)
|
||||
return
|
||||
// generateAndLoadCaddyfile reconciles the shared Caddyfile for the selected local Caddy container. A full candidate
|
||||
// reaches disk only after Caddy accepts it. Without an admin endpoint, the controller keeps a saved full file intact
|
||||
// and writes a bootstrap config only when the file is absent or already a bootstrap.
|
||||
func (c *Controller) generateAndLoadCaddyfile(ctx context.Context, containers []store.ContainerRecord) error {
|
||||
caddyCtr := selectLocalCaddyContainer(containers, c.machineID)
|
||||
if caddyCtr == nil {
|
||||
// Caddy is not running locally which means there is no reliable source for the global config, skipping.
|
||||
return nil
|
||||
}
|
||||
|
||||
caddyfile, err := c.generator.Generate(ctx, containers, caddyAvailable)
|
||||
if err != nil {
|
||||
c.log.Error("Failed to generate Caddyfile configuration.", "err", err)
|
||||
return
|
||||
saved, readErr := os.ReadFile(c.caddyfilePath)
|
||||
if readErr != nil && !errors.Is(readErr, os.ErrNotExist) {
|
||||
return fmt.Errorf("read saved Caddyfile: %w", readErr)
|
||||
}
|
||||
haveSaved := readErr == nil
|
||||
|
||||
if !caddyAvailable {
|
||||
// Caddy is not running so the generated Caddyfile should not include user-defined configs thus must be valid.
|
||||
// It's safe to write the config to disk so that when Caddy is deployed on this machine, it can pick it up.
|
||||
if err = c.writeCaddyfileIfChanged(caddyfile); err != nil {
|
||||
c.log.Error("Failed to write Caddyfile to disk.", "err", err)
|
||||
return
|
||||
healthyCtrs := filterHealthyContainers(containers)
|
||||
fingerprint := fingerprintContainers(healthyCtrs)
|
||||
|
||||
if !caddyCtr.State.Running || !c.client.IsAvailable() {
|
||||
if haveSaved && !isBootstrapCaddyfile(string(saved)) {
|
||||
// Caddy may need this file to restart or roll back. A hosts-only replacement could drop
|
||||
// storage or other global settings while the admin API is unavailable.
|
||||
if caddyCtr.State.Running {
|
||||
return fmt.Errorf("caddy admin socket unavailable, preserving saved Caddyfile")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
c.log.Debug("Caddy is not running on this machine, skipping configuration load.", "path", c.caddyfilePath)
|
||||
return
|
||||
|
||||
bootstrap, err := c.generator.Generate(ctx, *caddyCtr, healthyCtrs, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate Caddy bootstrap config: %w", err)
|
||||
}
|
||||
if err = c.writeCaddyfileIfChanged(bootstrap); err != nil {
|
||||
return fmt.Errorf("save Caddy bootstrap config: %w", err)
|
||||
}
|
||||
if caddyCtr.State.Running {
|
||||
return fmt.Errorf("caddy admin socket unavailable, bootstrap config saved")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Caddy is available, try to load the config which may fail if the config is invalid. Generally, a config can
|
||||
// pass the adaptation/validation step but still fail to load, for example, if it references resources that are
|
||||
// not available.
|
||||
if haveSaved &&
|
||||
caddyfileBody(string(saved)) == c.lastSavedBody &&
|
||||
!isBootstrapCaddyfile(string(saved)) &&
|
||||
c.lastCaddyCtrID == caddyCtr.ID &&
|
||||
c.lastStartedAt == caddyCtr.State.StartedAt &&
|
||||
c.lastGlobal == caddyCtr.ServiceSpec.CaddyConfig() &&
|
||||
slices.EqualFunc(fingerprint, c.lastFingerprint, containerFingerprint.Equal) {
|
||||
// No changes to the inputs that affect the generated Caddyfile, so no reload is needed.
|
||||
return nil
|
||||
}
|
||||
|
||||
caddyfile, err := c.generator.Generate(ctx, *caddyCtr, healthyCtrs, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate Caddyfile: %w", err)
|
||||
}
|
||||
// Try to load the config which may fail if the config is invalid. Generally, a config can pass
|
||||
// the adaptation/validation step but still fail to load, for example, if it references resources
|
||||
// that are not available.
|
||||
if err = c.client.Load(ctx, caddyfile); err != nil {
|
||||
c.log.Error("Failed to load new Caddy configuration into local Caddy instance.",
|
||||
"err", err, "path", c.caddyfilePath)
|
||||
// Mark the cache stale so the next container change retries the load even if the container set is unchanged.
|
||||
c.lastFingerprint = nil
|
||||
// Don't write invalid config to disk.
|
||||
return
|
||||
return fmt.Errorf("load Caddyfile: %w", err)
|
||||
}
|
||||
if err = c.writeCaddyfileIfChanged(caddyfile); err != nil {
|
||||
return fmt.Errorf("save loaded Caddyfile: %w", err)
|
||||
}
|
||||
c.lastFingerprint = fingerprint
|
||||
|
||||
// Config loaded successfully, now write it to disk.
|
||||
if err = c.writeCaddyfileIfChanged(caddyfile); err != nil {
|
||||
c.log.Error("Failed to write Caddyfile to disk after successful load.", "err", err)
|
||||
// Config is already loaded in Caddy, so this is not critical. The next regeneration retries the disk write.
|
||||
return
|
||||
}
|
||||
|
||||
c.lastCaddyCtrID = caddyCtr.ID
|
||||
c.lastStartedAt = caddyCtr.State.StartedAt
|
||||
c.lastGlobal = caddyCtr.ServiceSpec.CaddyConfig()
|
||||
c.lastSavedBody = caddyfileBody(caddyfile)
|
||||
c.log.Info("New Caddy configuration loaded into local Caddy instance.", "path", c.caddyfilePath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isBootstrapCaddyfile distinguishes a reduced startup file from a full file that must survive Caddy outage.
|
||||
// It also recognizes the older daemon's offline file. That file omitted every x-caddy block, including globals,
|
||||
// so treating it as a protected full file would preserve the configuration-loss bug:
|
||||
// https://github.com/psviderski/uncloud/issues/412
|
||||
func isBootstrapCaddyfile(caddyfile string) bool {
|
||||
return strings.Contains(caddyfile, bootstrapMarker) || strings.Contains(caddyfile, legacyBootstrapMarker)
|
||||
}
|
||||
|
||||
// fingerprintContainers returns a fingerprint of containers that the Caddyfile generator depends on.
|
||||
func fingerprintContainers(containers []store.ContainerRecord) []containerFingerprint {
|
||||
fingerprints := make([]containerFingerprint, len(containers))
|
||||
for i, cr := range containers {
|
||||
// Ignore ports parsing error as not much we can do about it. The generator just logs them and continues.
|
||||
// Ignore ports parsing error as not much we can do about it. The generator just logs them and skips
|
||||
// the container.
|
||||
ports, _ := cr.Container.ServicePorts()
|
||||
fingerprints[i] = containerFingerprint{
|
||||
ID: cr.Container.ID,
|
||||
@@ -220,21 +288,71 @@ func fingerprintContainers(containers []store.ContainerRecord) []containerFinger
|
||||
return fingerprints
|
||||
}
|
||||
|
||||
// writeCaddyfileIfChanged writes the Caddyfile content to disk with proper permissions only if its body differs
|
||||
// from the last successfully written content. The first line of the Caddyfile carries a generation timestamp that
|
||||
// changes on every regeneration, so it's excluded from the comparison to avoid redundant writes.
|
||||
// selectLocalCaddyContainer chooses which local Caddy container supplies global Caddy config. A running container
|
||||
// takes precedence over a newer stopped replacement, even if the running container is unhealthy. If two revisions
|
||||
// run at once, the shared admin socket does not identify its owner, so this choice remains best effort.
|
||||
func selectLocalCaddyContainer(records []store.ContainerRecord, machineID string) *api.ServiceContainer {
|
||||
var selected *api.ServiceContainer
|
||||
for _, cr := range records {
|
||||
ctr := cr.Container
|
||||
if cr.MachineID != machineID || ctr.ServiceName() != CaddyServiceName || ctr.IsHook() {
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
selected = &ctr
|
||||
continue
|
||||
}
|
||||
if ctr.State.Running != selected.State.Running {
|
||||
if ctr.State.Running {
|
||||
selected = &ctr
|
||||
}
|
||||
continue
|
||||
}
|
||||
if ctr.CreatedTime().Compare(selected.CreatedTime()) > 0 {
|
||||
selected = &ctr
|
||||
}
|
||||
}
|
||||
|
||||
return selected
|
||||
}
|
||||
|
||||
// writeCaddyfileIfChanged atomically replaces the saved Caddyfile only if its body differs from the new content.
|
||||
func (c *Controller) writeCaddyfileIfChanged(caddyfile string) error {
|
||||
if caddyfileBody(caddyfile) == caddyfileBody(c.lastCaddyfile) {
|
||||
saved, err := os.ReadFile(c.caddyfilePath)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("read Caddyfile '%s': %w", c.caddyfilePath, err)
|
||||
}
|
||||
if err == nil && caddyfileBody(caddyfile) == caddyfileBody(string(saved)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.WriteFile(c.caddyfilePath, []byte(caddyfile), 0o640); err != nil {
|
||||
return fmt.Errorf("write Caddyfile to file '%s': %w", c.caddyfilePath, err)
|
||||
dir := filepath.Dir(c.caddyfilePath)
|
||||
tmp, err := os.CreateTemp(dir, ".Caddyfile-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create temporary Caddyfile: %w", err)
|
||||
}
|
||||
if err := fs.Chown(c.caddyfilePath, "", CaddyGroup); err != nil {
|
||||
return fmt.Errorf("change owner of Caddyfile '%s': %w", c.caddyfilePath, err)
|
||||
defer os.Remove(tmp.Name())
|
||||
defer tmp.Close()
|
||||
|
||||
if err = tmp.Chmod(0o640); err != nil {
|
||||
return fmt.Errorf("set temporary Caddyfile permissions: %w", err)
|
||||
}
|
||||
if err = fs.Chown(tmp.Name(), "", c.fileGroup); err != nil {
|
||||
return fmt.Errorf("change owner of temporary Caddyfile: %w", err)
|
||||
}
|
||||
|
||||
if _, err = tmp.WriteString(caddyfile); err != nil {
|
||||
return fmt.Errorf("write temporary Caddyfile: %w", err)
|
||||
}
|
||||
if err = tmp.Sync(); err != nil {
|
||||
return fmt.Errorf("sync temporary Caddyfile: %w", err)
|
||||
}
|
||||
if err = tmp.Close(); err != nil {
|
||||
return fmt.Errorf("close temporary Caddyfile: %w", err)
|
||||
}
|
||||
if err = os.Rename(tmp.Name(), c.caddyfilePath); err != nil {
|
||||
return fmt.Errorf("replace Caddyfile '%s': %w", c.caddyfilePath, err)
|
||||
}
|
||||
c.lastCaddyfile = caddyfile
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -247,30 +365,3 @@ func caddyfileBody(caddyfile string) string {
|
||||
}
|
||||
return caddyfile
|
||||
}
|
||||
|
||||
func (c *Controller) generateJSONConfig(containers []store.ContainerRecord) error {
|
||||
serviceContainers := make([]api.ServiceContainer, len(containers))
|
||||
for i, cr := range containers {
|
||||
serviceContainers[i] = cr.Container
|
||||
}
|
||||
|
||||
config, err := GenerateJSONConfig(serviceContainers, c.machineID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configBytes, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal Caddy configuration: %w", err)
|
||||
}
|
||||
configPath := filepath.Join(filepath.Dir(c.caddyfilePath), "caddy.json")
|
||||
|
||||
if err = os.WriteFile(configPath, configBytes, 0o640); err != nil {
|
||||
return fmt.Errorf("write Caddy configuration to file '%s': %w", configPath, err)
|
||||
}
|
||||
if err = fs.Chown(configPath, "", CaddyGroup); err != nil {
|
||||
return fmt.Errorf("change owner of Caddy configuration file '%s': %w", configPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,14 +1,488 @@
|
||||
package caddyconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/psviderski/uncloud/internal/machine/store"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testController(t *testing.T, socketPath string) *Controller {
|
||||
t.Helper()
|
||||
group, err := user.LookupGroupId(strconv.Itoa(os.Getegid()))
|
||||
require.NoError(t, err)
|
||||
path := filepath.Join(t.TempDir(), "Caddyfile")
|
||||
client := NewCaddyAdminClient(socketPath)
|
||||
return &Controller{
|
||||
machineID: "test-machine-id",
|
||||
caddyfilePath: path,
|
||||
generator: NewCaddyfileGenerator("test-machine-id", "test-machine", client, nil),
|
||||
client: client,
|
||||
log: slog.Default(),
|
||||
fileGroup: group.Name,
|
||||
}
|
||||
}
|
||||
|
||||
type testCaddyAdmin struct {
|
||||
mu sync.Mutex
|
||||
adaptRequests []string
|
||||
loadCount int
|
||||
rejectAdaptContaining string
|
||||
rejectLoad bool
|
||||
}
|
||||
|
||||
func (a *testCaddyAdmin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/adapt":
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
a.mu.Lock()
|
||||
a.adaptRequests = append(a.adaptRequests, string(body))
|
||||
reject := a.rejectAdaptContaining != "" && strings.Contains(string(body), a.rejectAdaptContaining)
|
||||
a.mu.Unlock()
|
||||
if reject {
|
||||
http.Error(w, "invalid Caddyfile", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, _ = io.WriteString(w, `{"result":{}}`)
|
||||
case "/load":
|
||||
a.mu.Lock()
|
||||
a.loadCount++
|
||||
reject := a.rejectLoad
|
||||
a.mu.Unlock()
|
||||
if reject {
|
||||
http.Error(w, "load rejected", http.StatusBadRequest)
|
||||
}
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *testCaddyAdmin) snapshot() ([]string, int) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
return append([]string(nil), a.adaptRequests...), a.loadCount
|
||||
}
|
||||
|
||||
func (a *testCaddyAdmin) setRejectLoad(reject bool) {
|
||||
a.mu.Lock()
|
||||
a.rejectLoad = reject
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
func TestController_GenerateAndLoadCaddyfile(t *testing.T) {
|
||||
t.Run("keeps the saved file when there is no local Caddy container", func(t *testing.T) {
|
||||
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||
saved := "# previous full Caddyfile\n"
|
||||
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||
remote := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{ remote }",
|
||||
"other-machine", time.Now())
|
||||
|
||||
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{remote}))
|
||||
got, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, saved, string(got))
|
||||
})
|
||||
|
||||
t.Run("does not publish a bootstrap with an invalid global template", func(t *testing.T) {
|
||||
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{{upstreams",
|
||||
"test-machine-id", time.Now())
|
||||
|
||||
require.ErrorContains(t, controller.generateAndLoadCaddyfile(context.Background(),
|
||||
[]store.ContainerRecord{caddyCtr}), "render template")
|
||||
_, err := os.Stat(controller.caddyfilePath)
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
})
|
||||
|
||||
t.Run("bootstraps unhealthy Caddy with its globals and healthy routes", func(t *testing.T) {
|
||||
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||
"test-machine-id", time.Now())
|
||||
caddyCtr.Container.State.Health = &container.Health{Status: "unhealthy"}
|
||||
newerStopped := newContainerRecordWithCaddyConfig("caddy", "10.210.0.6", "{\n\tstorage replacement\n}",
|
||||
"test-machine-id", time.Now().Add(time.Minute))
|
||||
newerStopped.Container.State.Running = false
|
||||
app := newContainerRecordWithPorts("web", "10.210.0.3", []string{"app.example.com:8080/http"},
|
||||
"test-machine-id")
|
||||
app.Container.ServiceSpec.Caddy = &api.CaddySpec{Config: "custom.example.com { respond app }"}
|
||||
unhealthy := newContainerRecordWithPorts("unhealthy", "10.210.0.4",
|
||||
[]string{"unhealthy.example.com:8080/http"}, "test-machine-id")
|
||||
unhealthy.Container.State.Health = &container.Health{Status: "unhealthy"}
|
||||
hook := newContainerRecordWithPorts("hook", "10.210.0.5",
|
||||
[]string{"hook.example.com:8080/http"}, "test-machine-id")
|
||||
hook.Container.Config.Labels[api.LabelHook] = "pre-deploy"
|
||||
|
||||
err := controller.generateAndLoadCaddyfile(context.Background(),
|
||||
[]store.ContainerRecord{newerStopped, caddyCtr, app, unhealthy, hook})
|
||||
require.ErrorContains(t, err, "admin socket unavailable")
|
||||
saved, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(saved), bootstrapMarker+caddyCtr.Container.ID)
|
||||
assert.Contains(t, string(saved), "storage uncloud")
|
||||
assert.NotContains(t, string(saved), "storage replacement")
|
||||
assert.Contains(t, string(saved), "app.example.com")
|
||||
assert.NotContains(t, string(saved), "custom.example.com")
|
||||
assert.NotContains(t, string(saved), "unhealthy.example.com")
|
||||
assert.NotContains(t, string(saved), "hook.example.com")
|
||||
})
|
||||
|
||||
t.Run("replaces a legacy offline file with a bootstrap containing globals", func(t *testing.T) {
|
||||
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||
legacy := "# generated by older daemon\nhttp:// { respond ok }\n" + legacyBootstrapMarker + "\n"
|
||||
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(legacy), 0o640))
|
||||
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||
"test-machine-id", time.Now())
|
||||
|
||||
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr})
|
||||
require.ErrorContains(t, err, "admin socket unavailable")
|
||||
saved, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(saved), "storage uncloud")
|
||||
assert.Contains(t, string(saved), bootstrapMarker+caddyCtr.Container.ID)
|
||||
assert.NotContains(t, string(saved), legacyBootstrapMarker)
|
||||
})
|
||||
|
||||
t.Run("saves a bootstrap for a stopped Caddy container with no file", func(t *testing.T) {
|
||||
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||
"test-machine-id", time.Now())
|
||||
caddyCtr.Container.State.Running = false
|
||||
|
||||
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr}))
|
||||
saved, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(saved), bootstrapMarker+caddyCtr.Container.ID)
|
||||
assert.Contains(t, string(saved), "storage uncloud")
|
||||
})
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
running bool
|
||||
}{
|
||||
{name: "preserves a full file when the admin socket is unavailable", running: true},
|
||||
{name: "preserves a stopped container's full file", running: false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||
saved := "{\n\tstorage uncloud\n}\n\nold.example.com { respond old }\n"
|
||||
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage new\n}",
|
||||
"test-machine-id", time.Now())
|
||||
caddyCtr.Container.State.Running = tc.running
|
||||
app := newContainerRecordWithPorts("web", "10.210.0.3", []string{"new.example.com:8080/http"},
|
||||
"test-machine-id")
|
||||
|
||||
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr, app})
|
||||
if tc.running {
|
||||
require.ErrorContains(t, err, "admin socket unavailable")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
got, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, saved, string(got))
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("loads a full file and reloads after Caddy restarts", func(t *testing.T) {
|
||||
admin := &testCaddyAdmin{}
|
||||
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
|
||||
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||
"test-machine-id", time.Now())
|
||||
caddyCtr.Container.State.Health = &container.Health{Status: "unhealthy"}
|
||||
app := newContainerRecordWithCaddyConfig("web", "10.210.0.3", "app.example.com { respond app }",
|
||||
"test-machine-id", time.Now())
|
||||
records := []store.ContainerRecord{caddyCtr, app}
|
||||
|
||||
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||
saved, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(saved), "storage uncloud")
|
||||
assert.Contains(t, string(saved), "app.example.com")
|
||||
assert.NotContains(t, string(saved), bootstrapMarker)
|
||||
adapted, loads := admin.snapshot()
|
||||
require.NotEmpty(t, adapted)
|
||||
assert.Contains(t, adapted[0], "storage uncloud")
|
||||
assert.Equal(t, 1, loads)
|
||||
|
||||
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||
_, loads = admin.snapshot()
|
||||
assert.Equal(t, 1, loads, "unchanged inputs should not reload Caddy")
|
||||
|
||||
records[0].Container.State.StartedAt = time.Now().UTC().Format(time.RFC3339Nano)
|
||||
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||
_, loads = admin.snapshot()
|
||||
assert.Equal(t, 2, loads, "a restarted Caddy process needs the full configuration")
|
||||
})
|
||||
|
||||
t.Run("reloads when an application route changes", func(t *testing.T) {
|
||||
admin := &testCaddyAdmin{}
|
||||
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
|
||||
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||
"test-machine-id", time.Now())
|
||||
app := newContainerRecordWithPorts("web", "10.210.0.3", []string{"app.example.com:8080/http"},
|
||||
"test-machine-id")
|
||||
records := []store.ContainerRecord{caddyCtr, app}
|
||||
|
||||
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||
_, loads := admin.snapshot()
|
||||
assert.Equal(t, 1, loads)
|
||||
|
||||
records[1] = newContainerRecordWithPorts("web", "10.210.0.4",
|
||||
[]string{"app.example.com:8080/http"}, "test-machine-id")
|
||||
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||
saved, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(saved), "10.210.0.4:8080")
|
||||
assert.NotContains(t, string(saved), "10.210.0.3:8080")
|
||||
_, loads = admin.snapshot()
|
||||
assert.Equal(t, 2, loads)
|
||||
})
|
||||
|
||||
t.Run("rejects invalid globals without loading or replacing the saved file", func(t *testing.T) {
|
||||
admin := &testCaddyAdmin{rejectAdaptContaining: "storage rejected"}
|
||||
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
|
||||
saved := "{\n\tstorage uncloud\n}\n\nold.example.com { respond old }\n"
|
||||
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage rejected\n}",
|
||||
"test-machine-id", time.Now())
|
||||
|
||||
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr})
|
||||
require.ErrorContains(t, err, "validate user-defined global Caddy config")
|
||||
got, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, saved, string(got))
|
||||
_, loads := admin.snapshot()
|
||||
assert.Zero(t, loads)
|
||||
})
|
||||
|
||||
t.Run("skips invalid application custom config", func(t *testing.T) {
|
||||
admin := &testCaddyAdmin{rejectAdaptContaining: "invalid.example.com"}
|
||||
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
|
||||
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||
"test-machine-id", time.Now())
|
||||
invalid := newContainerRecordWithCaddyConfig("invalid", "10.210.0.3",
|
||||
"invalid.example.com { respond bad }", "test-machine-id", time.Now())
|
||||
valid := newContainerRecordWithCaddyConfig("valid", "10.210.0.4",
|
||||
"valid.example.com { respond good }", "test-machine-id", time.Now())
|
||||
|
||||
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(),
|
||||
[]store.ContainerRecord{caddyCtr, invalid, valid}))
|
||||
saved, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(saved), "storage uncloud")
|
||||
assert.Contains(t, string(saved), "valid.example.com")
|
||||
assert.NotContains(t, string(saved), "invalid.example.com")
|
||||
assert.Contains(t, string(saved), "Skipped invalid user-defined configs")
|
||||
_, loads := admin.snapshot()
|
||||
assert.Equal(t, 1, loads)
|
||||
})
|
||||
|
||||
t.Run("keeps the saved file when application validation cannot complete", func(t *testing.T) {
|
||||
var loads atomic.Int64
|
||||
socket := testAdminServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/adapt":
|
||||
http.Error(w, "adapter unavailable", http.StatusInternalServerError)
|
||||
case "/load":
|
||||
loads.Add(1)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
controller := testController(t, socket)
|
||||
saved := "old.example.com { respond old }\n"
|
||||
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "",
|
||||
"test-machine-id", time.Now())
|
||||
app := newContainerRecordWithCaddyConfig("web", "10.210.0.3", "new.example.com { respond new }",
|
||||
"test-machine-id", time.Now())
|
||||
|
||||
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr, app})
|
||||
require.ErrorContains(t, err, "validate Caddy config for service")
|
||||
got, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, saved, string(got))
|
||||
assert.Zero(t, loads.Load())
|
||||
})
|
||||
|
||||
t.Run("retries a rejected load without changing inputs", func(t *testing.T) {
|
||||
admin := &testCaddyAdmin{rejectLoad: true}
|
||||
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
|
||||
saved := "{\n\tstorage old\n}\n\nold.example.com { respond old }\n"
|
||||
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||
"test-machine-id", time.Now())
|
||||
records := []store.ContainerRecord{caddyCtr}
|
||||
|
||||
require.ErrorContains(t, controller.generateAndLoadCaddyfile(context.Background(), records), "load Caddyfile")
|
||||
got, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, saved, string(got))
|
||||
admin.setRejectLoad(false)
|
||||
|
||||
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||
got, err = os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(got), "storage uncloud")
|
||||
_, loads := admin.snapshot()
|
||||
assert.Equal(t, 2, loads)
|
||||
})
|
||||
|
||||
t.Run("retries saving after a successful load", func(t *testing.T) {
|
||||
admin := &testCaddyAdmin{}
|
||||
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
|
||||
group := controller.fileGroup
|
||||
controller.fileGroup = "uncloud-test-group-that-does-not-exist"
|
||||
saved := "{\n\tstorage old\n}\n\nold.example.com { respond old }\n"
|
||||
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||
"test-machine-id", time.Now())
|
||||
records := []store.ContainerRecord{caddyCtr}
|
||||
|
||||
require.ErrorContains(t, controller.generateAndLoadCaddyfile(context.Background(), records), "save loaded Caddyfile")
|
||||
got, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, saved, string(got))
|
||||
controller.fileGroup = group
|
||||
|
||||
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||
got, err = os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(got), "storage uncloud")
|
||||
_, loads := admin.snapshot()
|
||||
assert.Equal(t, 2, loads)
|
||||
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||
_, loads = admin.snapshot()
|
||||
assert.Equal(t, 2, loads, "a loaded and saved revision should not reload")
|
||||
})
|
||||
|
||||
t.Run("loads a saved bootstrap when the admin socket becomes available", func(t *testing.T) {
|
||||
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
|
||||
"test-machine-id", time.Now())
|
||||
records := []store.ContainerRecord{caddyCtr}
|
||||
require.Error(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||
bootstrap, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(bootstrap), bootstrapMarker)
|
||||
|
||||
admin := &testCaddyAdmin{}
|
||||
socket := testAdminServer(t, admin.ServeHTTP)
|
||||
controller.client = NewCaddyAdminClient(socket)
|
||||
controller.generator = NewCaddyfileGenerator("test-machine-id", "test-machine", controller.client, nil)
|
||||
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||
full, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, string(full), bootstrapMarker)
|
||||
_, loads := admin.snapshot()
|
||||
assert.Equal(t, 1, loads)
|
||||
|
||||
require.NoError(t, os.WriteFile(controller.caddyfilePath, bootstrap, 0o640))
|
||||
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
|
||||
_, loads = admin.snapshot()
|
||||
assert.Equal(t, 2, loads, "a saved bootstrap is not an applied full configuration")
|
||||
})
|
||||
}
|
||||
|
||||
func TestController_WriteCaddyfileIfChanged(t *testing.T) {
|
||||
t.Run("publishes a complete file with the configured group", func(t *testing.T) {
|
||||
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||
caddyfile := "# Generated now\n{\n\tstorage uncloud\n}\n"
|
||||
|
||||
require.NoError(t, controller.writeCaddyfileIfChanged(caddyfile))
|
||||
got, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, caddyfile, string(got))
|
||||
info, err := os.Stat(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, os.FileMode(0o640), info.Mode().Perm())
|
||||
})
|
||||
|
||||
t.Run("does not replace a file when only the timestamp changes", func(t *testing.T) {
|
||||
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||
saved := "# Generated yesterday\nsite.example.com { respond ok }\n"
|
||||
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||
before, err := os.Stat(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, controller.writeCaddyfileIfChanged(
|
||||
"# Generated today\nsite.example.com { respond ok }\n"))
|
||||
got, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, saved, string(got))
|
||||
after, err := os.Stat(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, os.SameFile(before, after), "an unchanged body should not replace the file")
|
||||
})
|
||||
|
||||
t.Run("preserves the previous file when publication fails", func(t *testing.T) {
|
||||
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
|
||||
controller.fileGroup = "uncloud-test-group-that-does-not-exist"
|
||||
saved := "# previous\nsite.example.com { respond old }\n"
|
||||
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
|
||||
|
||||
err := controller.writeCaddyfileIfChanged("# next\nsite.example.com { respond new }\n")
|
||||
require.ErrorContains(t, err, "change owner of temporary Caddyfile")
|
||||
got, err := os.ReadFile(controller.caddyfilePath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, saved, string(got))
|
||||
tmpFiles, err := filepath.Glob(filepath.Join(filepath.Dir(controller.caddyfilePath), ".Caddyfile-*"))
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, tmpFiles)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSelectLocalCaddyContainer(t *testing.T) {
|
||||
t.Run("prefers a running unhealthy container over a newer stopped one", func(t *testing.T) {
|
||||
older := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{ older }",
|
||||
"test-machine-id", time.Now().Add(-time.Hour))
|
||||
older.Container.State.Health = &container.Health{Status: "unhealthy"}
|
||||
newer := newContainerRecordWithCaddyConfig("caddy", "10.210.0.3", "{ newer }",
|
||||
"test-machine-id", time.Now())
|
||||
newer.Container.State.Running = false
|
||||
|
||||
selected := selectLocalCaddyContainer([]store.ContainerRecord{newer, older}, "test-machine-id")
|
||||
require.NotNil(t, selected)
|
||||
assert.Equal(t, older.Container.ID, selected.ID)
|
||||
})
|
||||
|
||||
t.Run("ignores remote and hook containers", func(t *testing.T) {
|
||||
local := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{ local }",
|
||||
"test-machine-id", time.Now().Add(-time.Hour))
|
||||
remote := newContainerRecordWithCaddyConfig("caddy", "10.210.0.3", "{ remote }",
|
||||
"other-machine", time.Now())
|
||||
hook := newContainerRecordWithCaddyConfig("caddy", "10.210.0.4", "{ hook }",
|
||||
"test-machine-id", time.Now())
|
||||
hook.Container.Config.Labels[api.LabelHook] = "pre-deploy"
|
||||
|
||||
selected := selectLocalCaddyContainer([]store.ContainerRecord{remote, hook, local}, "test-machine-id")
|
||||
require.NotNil(t, selected)
|
||||
assert.Equal(t, local.Container.ID, selected.ID)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContainerFingerprint_EqualCoversAllFields is a guard: when a field is added to containerFingerprint,
|
||||
// Equal must also compare it. A mutation of any single field should flip equality to false. If this test fails
|
||||
// after adding a field, update Equal to include it.
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
package caddyconfig
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
)
|
||||
|
||||
func GenerateJSONConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) {
|
||||
httpHostUpstreams, httpsHostUpstreams := httpUpstreamsFromPorts(containers)
|
||||
|
||||
var warnings []caddyconfig.Warning
|
||||
servers := make(map[string]*caddyhttp.Server)
|
||||
servers["http"] = &caddyhttp.Server{
|
||||
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPPort)},
|
||||
// All http requests to this server are logged to the default logger.
|
||||
Logs: &caddyhttp.ServerLogConfig{},
|
||||
Routes: append(
|
||||
hostUpstreamsToRoutes(httpHostUpstreams, &warnings),
|
||||
// Add a route to respond with a static verification response at the /.uncloud-verify path.
|
||||
verificationRoute(verifyResponse, &warnings),
|
||||
),
|
||||
}
|
||||
servers["https"] = &caddyhttp.Server{
|
||||
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPSPort)},
|
||||
// All https requests to this server are logged to the default logger.
|
||||
Logs: &caddyhttp.ServerLogConfig{},
|
||||
Routes: hostUpstreamsToRoutes(httpsHostUpstreams, &warnings),
|
||||
}
|
||||
|
||||
httpApp := caddyhttp.App{
|
||||
Servers: servers,
|
||||
}
|
||||
config := &caddy.Config{
|
||||
AppsRaw: caddy.ModuleMap{
|
||||
"http": caddyconfig.JSON(httpApp, &warnings),
|
||||
},
|
||||
}
|
||||
|
||||
var err error
|
||||
if len(warnings) > 0 {
|
||||
// warnings only contains errors from JSON marshaling, which are highly unlikely with correct code.
|
||||
for _, w := range warnings {
|
||||
err = errors.Join(err, errors.New(w.Message))
|
||||
}
|
||||
return nil, fmt.Errorf("marshal Caddy configuration: %w", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// hostUpstreamsToRoutes converts a map of hostnames to upstreams to a list of Caddy routes.
|
||||
func hostUpstreamsToRoutes(hostUpstreams map[string][]string, warnings *[]caddyconfig.Warning) []caddyhttp.Route {
|
||||
// Sort hostnames for deterministic output.
|
||||
hostnames := slices.Collect(maps.Keys(hostUpstreams))
|
||||
slices.Sort(hostnames)
|
||||
|
||||
routes := make([]caddyhttp.Route, 0, len(hostUpstreams))
|
||||
for _, hostname := range hostnames {
|
||||
upstreams := hostUpstreams[hostname]
|
||||
upstreamPool := make([]*reverseproxy.Upstream, len(upstreams))
|
||||
for i, upstream := range upstreams {
|
||||
upstreamPool[i] = &reverseproxy.Upstream{
|
||||
Dial: upstream,
|
||||
}
|
||||
}
|
||||
handler := &reverseproxy.Handler{
|
||||
HealthChecks: &reverseproxy.HealthChecks{
|
||||
// Enable passive health checks to automatically detect unhealthy upstreams.
|
||||
Passive: &reverseproxy.PassiveHealthChecks{
|
||||
FailDuration: caddy.Duration(30 * time.Second),
|
||||
},
|
||||
},
|
||||
LoadBalancing: &reverseproxy.LoadBalancing{
|
||||
// Retry failed requests to skip over temporarily unavailable upstreams.
|
||||
Retries: 3,
|
||||
},
|
||||
Upstreams: upstreamPool,
|
||||
}
|
||||
|
||||
routes = append(routes, caddyhttp.Route{
|
||||
MatcherSetsRaw: caddyhttp.RawMatcherSets{
|
||||
{
|
||||
"host": caddyconfig.JSON(caddyhttp.MatchHost{hostname}, warnings),
|
||||
},
|
||||
},
|
||||
HandlersRaw: []json.RawMessage{
|
||||
caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", warnings),
|
||||
},
|
||||
})
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
// verificationRoute returns a Caddy route that responds with the given static response at the /.uncloud-verify path.
|
||||
func verificationRoute(response string, warnings *[]caddyconfig.Warning) caddyhttp.Route {
|
||||
// Return the following route:
|
||||
// {
|
||||
// "match": [
|
||||
// {
|
||||
// "path": [
|
||||
// "/.uncloud-verify"
|
||||
// ]
|
||||
// }
|
||||
// ],
|
||||
// "handle": [
|
||||
// {
|
||||
// "handler": "static_response",
|
||||
// "body": "<response>",
|
||||
// "status_code": 200
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
staticResponse := caddyhttp.StaticResponse{
|
||||
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusOK)),
|
||||
Body: response,
|
||||
}
|
||||
|
||||
return caddyhttp.Route{
|
||||
MatcherSetsRaw: caddyhttp.RawMatcherSets{
|
||||
{
|
||||
"path": caddyconfig.JSON(caddyhttp.MatchPath{VerifyPath}, warnings),
|
||||
},
|
||||
},
|
||||
HandlersRaw: []json.RawMessage{
|
||||
caddyconfig.JSONModuleObject(staticResponse, "handler", "static_response", warnings),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
package caddyconfig
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/psviderski/uncloud/internal/machine/docker"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerateJSONConfig(t *testing.T) {
|
||||
configWithoutServices := `{
|
||||
"servers": {
|
||||
"http": {
|
||||
"listen": [":80"],
|
||||
"routes": [{
|
||||
"match": [{"path": ["/.uncloud-verify"]}],
|
||||
"handle": [{
|
||||
"body": "verification-response-body",
|
||||
"handler": "static_response",
|
||||
"status_code": 200
|
||||
}]
|
||||
}],
|
||||
"logs": {}
|
||||
},
|
||||
"https": {
|
||||
"listen": [":443"],
|
||||
"logs": {}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
containers []api.ServiceContainer
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty containers",
|
||||
containers: []api.ServiceContainer{},
|
||||
want: configWithoutServices,
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "HTTP container",
|
||||
containers: []api.ServiceContainer{
|
||||
newContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||
},
|
||||
want: `{
|
||||
"servers": {
|
||||
"http": {
|
||||
"listen": [":80"],
|
||||
"routes": [
|
||||
{
|
||||
"match": [{"host": ["app.example.com"]}],
|
||||
"handle": [{
|
||||
"handler": "reverse_proxy",
|
||||
"health_checks": {
|
||||
"passive": {
|
||||
"fail_duration": 30000000000
|
||||
}
|
||||
},
|
||||
"load_balancing": {
|
||||
"retries": 3
|
||||
},
|
||||
"upstreams": [{"dial": "10.210.0.2:8080"}]
|
||||
}]
|
||||
},
|
||||
{
|
||||
"match": [{"path": ["/.uncloud-verify"]}],
|
||||
"handle": [{
|
||||
"body": "verification-response-body",
|
||||
"handler": "static_response",
|
||||
"status_code": 200
|
||||
}]
|
||||
}
|
||||
],
|
||||
"logs": {}
|
||||
},
|
||||
"https": {
|
||||
"listen": [":443"],
|
||||
"logs": {}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "load balancing multiple containers",
|
||||
containers: []api.ServiceContainer{
|
||||
newContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||
newContainer("10.210.0.3", "app.example.com:8080/http"),
|
||||
},
|
||||
want: `{
|
||||
"servers": {
|
||||
"http": {
|
||||
"listen": [":80"],
|
||||
"routes": [
|
||||
{
|
||||
"match": [{"host": ["app.example.com"]}],
|
||||
"handle": [{
|
||||
"handler": "reverse_proxy",
|
||||
"health_checks": {
|
||||
"passive": {
|
||||
"fail_duration": 30000000000
|
||||
}
|
||||
},
|
||||
"load_balancing": {
|
||||
"retries": 3
|
||||
},
|
||||
"upstreams": [
|
||||
{"dial": "10.210.0.2:8080"},
|
||||
{"dial": "10.210.0.3:8080"}
|
||||
]
|
||||
}]
|
||||
},
|
||||
{
|
||||
"match": [{"path": ["/.uncloud-verify"]}],
|
||||
"handle": [{
|
||||
"body": "verification-response-body",
|
||||
"handler": "static_response",
|
||||
"status_code": 200
|
||||
}]
|
||||
}
|
||||
],
|
||||
"logs": {}
|
||||
},
|
||||
"https": {
|
||||
"listen": [":443"],
|
||||
"logs": {}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "HTTPS container",
|
||||
containers: []api.ServiceContainer{
|
||||
newContainer("10.210.0.2", "secure.example.com:8000/https"),
|
||||
},
|
||||
want: `{
|
||||
"servers": {
|
||||
"http": {
|
||||
"listen": [":80"],
|
||||
"routes": [
|
||||
{
|
||||
"match": [{"path": ["/.uncloud-verify"]}],
|
||||
"handle": [{
|
||||
"body": "verification-response-body",
|
||||
"handler": "static_response",
|
||||
"status_code": 200
|
||||
}]
|
||||
}
|
||||
],
|
||||
"logs": {}
|
||||
},
|
||||
"https": {
|
||||
"listen": [":443"],
|
||||
"routes": [
|
||||
{
|
||||
"match": [{"host": ["secure.example.com"]}],
|
||||
"handle": [{
|
||||
"handler": "reverse_proxy",
|
||||
"health_checks": {
|
||||
"passive": {
|
||||
"fail_duration": 30000000000
|
||||
}
|
||||
},
|
||||
"load_balancing": {
|
||||
"retries": 3
|
||||
},
|
||||
"upstreams": [{"dial": "10.210.0.2:8000"}]
|
||||
}]
|
||||
}
|
||||
],
|
||||
"logs": {}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "mixed HTTP and HTTPS",
|
||||
containers: []api.ServiceContainer{
|
||||
newContainer("10.210.0.2",
|
||||
"app.example.com:8080/http",
|
||||
"web.example.com:8000/http"),
|
||||
newContainer("10.210.0.3",
|
||||
"app.example.com:8080/http",
|
||||
"secure.example.com:8888/https"),
|
||||
newContainer("10.210.0.4",
|
||||
"web.example.com:8000/http",
|
||||
"secure.example.com:8888/https"),
|
||||
newContainer("10.210.0.5",
|
||||
"app.example.com:8080/http",
|
||||
"web.example.com:8000/http",
|
||||
"secure.example.com:8888/https"),
|
||||
},
|
||||
want: `{
|
||||
"servers": {
|
||||
"http": {
|
||||
"listen": [":80"],
|
||||
"routes": [
|
||||
{
|
||||
"match": [{"host": ["app.example.com"]}],
|
||||
"handle": [{
|
||||
"handler": "reverse_proxy",
|
||||
"health_checks": {
|
||||
"passive": {
|
||||
"fail_duration": 30000000000
|
||||
}
|
||||
},
|
||||
"load_balancing": {
|
||||
"retries": 3
|
||||
},
|
||||
"upstreams": [
|
||||
{"dial": "10.210.0.2:8080"},
|
||||
{"dial": "10.210.0.3:8080"},
|
||||
{"dial": "10.210.0.5:8080"}
|
||||
]
|
||||
}]
|
||||
},
|
||||
{
|
||||
"match": [{"host": ["web.example.com"]}],
|
||||
"handle": [{
|
||||
"handler": "reverse_proxy",
|
||||
"health_checks": {
|
||||
"passive": {
|
||||
"fail_duration": 30000000000
|
||||
}
|
||||
},
|
||||
"load_balancing": {
|
||||
"retries": 3
|
||||
},
|
||||
"upstreams": [
|
||||
{"dial": "10.210.0.2:8000"},
|
||||
{"dial": "10.210.0.4:8000"},
|
||||
{"dial": "10.210.0.5:8000"}
|
||||
]
|
||||
}]
|
||||
},
|
||||
{
|
||||
"match": [{"path": ["/.uncloud-verify"]}],
|
||||
"handle": [{
|
||||
"body": "verification-response-body",
|
||||
"handler": "static_response",
|
||||
"status_code": 200
|
||||
}]
|
||||
}
|
||||
],
|
||||
"logs": {}
|
||||
},
|
||||
"https": {
|
||||
"listen": [":443"],
|
||||
"routes": [
|
||||
{
|
||||
"match": [{"host": ["secure.example.com"]}],
|
||||
"handle": [{
|
||||
"handler": "reverse_proxy",
|
||||
"health_checks": {
|
||||
"passive": {
|
||||
"fail_duration": 30000000000
|
||||
}
|
||||
},
|
||||
"load_balancing": {
|
||||
"retries": 3
|
||||
},
|
||||
"upstreams": [
|
||||
{"dial": "10.210.0.3:8888"},
|
||||
{"dial": "10.210.0.4:8888"},
|
||||
{"dial": "10.210.0.5:8888"}
|
||||
]
|
||||
}]
|
||||
}
|
||||
],
|
||||
"logs": {}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "container without uncloud network ignored",
|
||||
containers: []api.ServiceContainer{
|
||||
newContainerWithoutNetwork("ignored.example.com:8080/http"),
|
||||
},
|
||||
want: configWithoutServices,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "container with invalid port ignored",
|
||||
containers: []api.ServiceContainer{
|
||||
newContainer("10.210.0.2", "invalid-port"),
|
||||
},
|
||||
want: configWithoutServices,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "containers with unsupported protocols and host mode ignored",
|
||||
containers: []api.ServiceContainer{
|
||||
newContainer("10.210.0.2", "5000/tcp"),
|
||||
newContainer("10.210.0.3", "5000/udp"),
|
||||
newContainer("10.210.0.4", "80:8080/tcp@host"),
|
||||
},
|
||||
want: configWithoutServices,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
config, err := GenerateJSONConfig(tt.containers, "verification-response-body")
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, config.AppsRaw, 1, "Expected one http app")
|
||||
require.Contains(t, config.AppsRaw, "http", "Expected http app")
|
||||
|
||||
assert.JSONEq(t, tt.want, string(config.AppsRaw["http"]), "Generated Caddy app config doesn't match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newContainer(ip string, ports ...string) api.ServiceContainer {
|
||||
portsLabel := strings.Join(ports, ",")
|
||||
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
State: &container.State{
|
||||
Running: true,
|
||||
},
|
||||
},
|
||||
NetworkSettings: &container.NetworkSettings{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
docker.NetworkName: {
|
||||
IPAddress: ip,
|
||||
},
|
||||
},
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
api.LabelServicePorts: portsLabel,
|
||||
},
|
||||
},
|
||||
}}}
|
||||
}
|
||||
|
||||
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
||||
portsLabel := strings.Join(ports, ",")
|
||||
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
State: &container.State{
|
||||
Running: true,
|
||||
},
|
||||
},
|
||||
NetworkSettings: &container.NetworkSettings{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"other-network": {
|
||||
IPAddress: "172.17.0.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
api.LabelServicePorts: portsLabel,
|
||||
},
|
||||
},
|
||||
}}}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package caddyconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
@@ -22,18 +23,26 @@ func NewServer(service *Service) *Server {
|
||||
return &Server{service: service}
|
||||
}
|
||||
|
||||
// GetConfig retrieves the current Caddy configuration from the machine.
|
||||
// GetConfig retrieves the saved Caddy configuration and the latest reconciliation error from the machine.
|
||||
func (s *Server) GetConfig(ctx context.Context, _ *emptypb.Empty) (*pb.GetCaddyConfigResponse, error) {
|
||||
caddyfile, modifiedAt, err := s.service.Caddyfile()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if lastErr := s.service.LastReconciliationError(); lastErr != nil {
|
||||
return nil, status.Errorf(codes.NotFound, "%v; last Caddy config load failed: %v", err, lastErr)
|
||||
}
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
recErr := ""
|
||||
if lastErr := s.service.LastReconciliationError(); lastErr != nil {
|
||||
recErr = lastErr.Error()
|
||||
}
|
||||
return &pb.GetCaddyConfigResponse{
|
||||
Caddyfile: caddyfile,
|
||||
ModifiedAt: timestamppb.New(modifiedAt),
|
||||
Caddyfile: caddyfile,
|
||||
ModifiedAt: timestamppb.New(modifiedAt),
|
||||
LastReconciliationError: recErr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package caddyconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
func TestServer_GetConfig(t *testing.T) {
|
||||
t.Run("returns saved Caddyfile and reconciliation error", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
caddyfile := "example.com { respond ok }\n"
|
||||
path := filepath.Join(dir, "Caddyfile")
|
||||
require.NoError(t, os.WriteFile(path, []byte(caddyfile), 0o640))
|
||||
info, err := os.Stat(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
service := NewService(dir)
|
||||
service.setReconciliationResult(errors.New("module not registered: caddy.storage.uncloud"))
|
||||
config, err := NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, caddyfile, config.GetCaddyfile())
|
||||
assert.True(t, info.ModTime().Equal(config.GetModifiedAt().AsTime()))
|
||||
assert.Equal(t, "module not registered: caddy.storage.uncloud", config.GetLastReconciliationError())
|
||||
|
||||
service.setReconciliationResult(nil)
|
||||
config, err = NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, config.GetLastReconciliationError())
|
||||
})
|
||||
|
||||
t.Run("returns NotFound with reconciliation error when Caddyfile is absent", func(t *testing.T) {
|
||||
service := NewService(t.TempDir())
|
||||
service.setReconciliationResult(errors.New("module not registered: caddy.storage.uncloud"))
|
||||
|
||||
config, err := NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
|
||||
|
||||
assert.Nil(t, config)
|
||||
assert.Equal(t, codes.NotFound, status.Code(err))
|
||||
assert.ErrorContains(t, err, "Caddyfile")
|
||||
assert.ErrorContains(t, err, "last Caddy config load failed: module not registered: caddy.storage.uncloud")
|
||||
})
|
||||
|
||||
t.Run("returns NotFound without reconciliation error when Caddyfile is absent", func(t *testing.T) {
|
||||
service := NewService(t.TempDir())
|
||||
|
||||
config, err := NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
|
||||
|
||||
assert.Nil(t, config)
|
||||
assert.Equal(t, codes.NotFound, status.Code(err))
|
||||
assert.NotContains(t, err.Error(), "last Caddy config load failed")
|
||||
})
|
||||
}
|
||||
@@ -4,12 +4,15 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Service provides methods to interact with the Caddy configuration on the machine.
|
||||
type Service struct {
|
||||
configDir string
|
||||
configDir string
|
||||
mu sync.RWMutex
|
||||
lastReconciliationError error
|
||||
}
|
||||
|
||||
// NewService creates a new Service instance with the specified Caddy configuration directory.
|
||||
@@ -17,7 +20,8 @@ func NewService(configDir string) *Service {
|
||||
return &Service{configDir: configDir}
|
||||
}
|
||||
|
||||
// Caddyfile retrieves the current Caddy configuration (Caddyfile) from the machine's config directory.
|
||||
// Caddyfile retrieves the saved Caddyfile from the machine's config directory. The saved file may differ from the
|
||||
// running configuration if Caddy accepted a load but the subsequent write failed.
|
||||
func (s *Service) Caddyfile() (string, time.Time, error) {
|
||||
path := filepath.Join(s.configDir, "Caddyfile")
|
||||
content, err := os.ReadFile(path)
|
||||
@@ -33,3 +37,17 @@ func (s *Service) Caddyfile() (string, time.Time, error) {
|
||||
|
||||
return string(content), fileInfo.ModTime(), nil
|
||||
}
|
||||
|
||||
// LastReconciliationError reports the most recent unsuccessful controller attempt, if any. A successful attempt
|
||||
// clears it. An empty value does not prove that Caddy has loaded the saved Caddyfile.
|
||||
func (s *Service) LastReconciliationError() error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.lastReconciliationError
|
||||
}
|
||||
|
||||
func (s *Service) setReconciliationResult(err error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.lastReconciliationError = err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package caddyconfig
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestService_LastReconciliationError(t *testing.T) {
|
||||
service := NewService(t.TempDir())
|
||||
assert.NoError(t, service.LastReconciliationError())
|
||||
|
||||
service.setReconciliationResult(errors.New("global Caddy config rejected"))
|
||||
assert.EqualError(t, service.LastReconciliationError(), "global Caddy config rejected")
|
||||
|
||||
service.setReconciliationResult(nil)
|
||||
assert.NoError(t, service.LastReconciliationError())
|
||||
}
|
||||
|
||||
func TestService_LastReconciliationErrorConcurrent(t *testing.T) {
|
||||
service := NewService(t.TempDir())
|
||||
var wg sync.WaitGroup
|
||||
for range 10 {
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for range 100 {
|
||||
service.setReconciliationResult(errors.New("retry"))
|
||||
service.setReconciliationResult(nil)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for range 100 {
|
||||
_ = service.LastReconciliationError()
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -538,6 +538,15 @@ func (s *Server) CreateServiceContainer(
|
||||
containerName = fmt.Sprintf("%s-%s", spec.Name, suffix)
|
||||
}
|
||||
|
||||
desiredSpec := spec
|
||||
renderedSpec, renderErr := desiredSpec.RenderRuntimeTemplates(api.RuntimeTemplateContext{
|
||||
Container: api.RuntimeTemplateContainerContext{Name: containerName},
|
||||
})
|
||||
if renderErr != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "render runtime templates: %v", renderErr)
|
||||
}
|
||||
spec = renderedSpec
|
||||
|
||||
envVars := maps.Clone(spec.Container.Env)
|
||||
if envVars == nil {
|
||||
envVars = make(api.EnvVars)
|
||||
@@ -745,7 +754,7 @@ func (s *Server) CreateServiceContainer(
|
||||
_ = s.client.ContainerRemove(ctx, resp.ID, container.RemoveOptions{RemoveVolumes: true})
|
||||
}
|
||||
|
||||
specBytes, err := json.Marshal(spec)
|
||||
specBytes, err := json.Marshal(desiredSpec)
|
||||
if err != nil {
|
||||
removeContainer()
|
||||
return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/psviderski/uncloud/api/pb"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func TestCreateServiceContainer_RejectsRuntimeTemplateError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
desiredSpec := api.ServiceSpec{
|
||||
Name: "web",
|
||||
Container: api.ContainerSpec{
|
||||
Image: "busybox:latest",
|
||||
VolumeMounts: []api.VolumeMount{
|
||||
{VolumeName: "data", ContainerPath: "/data"},
|
||||
},
|
||||
},
|
||||
Volumes: []api.VolumeSpec{
|
||||
{
|
||||
Name: "data",
|
||||
Type: api.VolumeTypeBind,
|
||||
BindOptions: &api.BindOptions{
|
||||
// Early validation takes the valid branch. Rendering with the real container name takes the invalid one.
|
||||
HostPath: `/host/{{if eq .Container.Name "web-a1b2"}}{{.Container.ID}}{{else}}valid{{end}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
specJSON, err := json.Marshal(desiredSpec)
|
||||
require.NoError(t, err)
|
||||
|
||||
server := &Server{}
|
||||
_, err = server.CreateServiceContainer(context.Background(), &pb.CreateServiceContainerRequest{
|
||||
ServiceId: strings.Repeat("a", 32),
|
||||
ServiceSpec: specJSON,
|
||||
ContainerName: "web-a1b2",
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, codes.InvalidArgument, status.Code(err))
|
||||
assert.ErrorContains(t, err, "render runtime template in bind volume 'data' host path")
|
||||
}
|
||||
@@ -198,6 +198,8 @@ type Machine struct {
|
||||
dockerServer *machinedocker.Server
|
||||
// machineAPIServer handles API requests directly on this machine.
|
||||
machineAPIServer *grpc.Server
|
||||
// caddyService provides methods to interact with the Caddy configuration on the machine.
|
||||
caddyService *caddyconfig.Service
|
||||
|
||||
// proxyDirector routes API requests to local or remote machines.
|
||||
proxyDirector *apiproxy.Director
|
||||
@@ -302,6 +304,7 @@ func NewMachine(config *Config) (*Machine, error) {
|
||||
dockerService: dockerService,
|
||||
clusterAPIServer: clusterAPIServer,
|
||||
proxyDirector: proxyDirector,
|
||||
caddyService: caddyconfig.NewService(config.CaddyConfigDir),
|
||||
}
|
||||
|
||||
// Machine IP will only be available after the machine is initialised as a cluster member so wrap it in a function.
|
||||
@@ -316,7 +319,7 @@ func NewMachine(config *Config) (*Machine, error) {
|
||||
NetworkReady: m.IsNetworkReady,
|
||||
WaitForNetworkReady: m.WaitForNetworkReady,
|
||||
})
|
||||
caddyServer := caddyconfig.NewServer(caddyconfig.NewService(config.CaddyConfigDir))
|
||||
caddyServer := caddyconfig.NewServer(m.caddyService)
|
||||
|
||||
caddyStore, err := corroStore.Keyspace(caddystorage.Namespace)
|
||||
if err != nil {
|
||||
@@ -535,7 +538,7 @@ func (m *Machine) Run(ctx context.Context) error {
|
||||
// It will also serve the current machine ID at /.uncloud-verify to verify Caddy reachability.
|
||||
caddyconfigCtrl, err := caddyconfig.NewController(
|
||||
m.state.ID,
|
||||
m.config.CaddyConfigDir,
|
||||
m.caddyService,
|
||||
DefaultCaddyAdminSockPath,
|
||||
m.store,
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"github.com/psviderski/uncloud/api/pb"
|
||||
"github.com/psviderski/uncloud/internal/machine"
|
||||
"github.com/psviderski/uncloud/internal/machine/cluster"
|
||||
"github.com/psviderski/uncloud/pkg/client"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
@@ -322,6 +324,78 @@ func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout t
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitClusterMeshReady waits until every machine can reach every other machine through the WireGuard mesh network.
|
||||
func (p *Provisioner) WaitClusterMeshReady(ctx context.Context, c Cluster, timeout time.Duration) error {
|
||||
if len(c.Machines) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
clients := make([]*client.Client, len(c.Machines))
|
||||
for i := range c.Machines {
|
||||
cli, err := c.Machines[i].Connect(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to machine '%s' over TCP '%s': %w",
|
||||
c.Machines[i].Name, c.Machines[i].APIAddress, err)
|
||||
}
|
||||
clients[i] = cli
|
||||
defer cli.Close()
|
||||
}
|
||||
|
||||
type machinePair struct {
|
||||
source int
|
||||
target int
|
||||
}
|
||||
pairs := make([]machinePair, 0, len(c.Machines)*(len(c.Machines)-1))
|
||||
for source := range c.Machines {
|
||||
for target := range c.Machines {
|
||||
if source != target {
|
||||
pairs = append(pairs, machinePair{source: source, target: target})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boff := backoff.WithContext(backoff.NewExponentialBackOff(
|
||||
backoff.WithInitialInterval(100*time.Millisecond),
|
||||
backoff.WithMaxInterval(time.Second),
|
||||
backoff.WithMaxElapsedTime(timeout),
|
||||
), ctx)
|
||||
|
||||
checkMeshReady := func() error {
|
||||
errs := make([]error, len(pairs))
|
||||
var wg sync.WaitGroup
|
||||
for i, pair := range pairs {
|
||||
wg.Go(func() {
|
||||
source := c.Machines[pair.source]
|
||||
target := c.Machines[pair.target]
|
||||
|
||||
probeCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
resp, err := clients[pair.source].MachineClient.InspectMachine(
|
||||
clients[pair.source].ProxySingleMachineContext(probeCtx, target.ID),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
errs[i] = fmt.Errorf("reach machine '%s' from '%s': %w", target.Name, source.Name, err)
|
||||
return
|
||||
}
|
||||
if len(resp.Machines) != 1 || resp.Machines[0].Machine == nil ||
|
||||
resp.Machines[0].Machine.Id != target.ID {
|
||||
errs[i] = fmt.Errorf("machine '%s' returned an unexpected response when reached from '%s'",
|
||||
target.Name, source.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
if err := backoff.Retry(checkMeshReady, boff); err != nil {
|
||||
return fmt.Errorf("wait for cluster WireGuard mesh to be ready: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provisioner) ListClusters(ctx context.Context) ([]Cluster, error) {
|
||||
nets, err := p.dockerCli.NetworkList(ctx, network.ListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// RuntimeTemplateContext contains the data available to runtime templates in a service spec.
|
||||
type RuntimeTemplateContext struct {
|
||||
Container RuntimeTemplateContainerContext
|
||||
}
|
||||
|
||||
// RuntimeTemplateContainerContext contains container data available to runtime templates in a service spec.
|
||||
type RuntimeTemplateContainerContext struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// RenderRuntimeTemplates returns a copy of the service spec with supported runtime template expressions rendered
|
||||
// using metadata from ctx. Runtime templates use Go template expressions such as {{.Container.Name}} and are supported
|
||||
// in bind volume host paths and volume mount container paths. The original service spec is not modified.
|
||||
func (s *ServiceSpec) RenderRuntimeTemplates(ctx RuntimeTemplateContext) (ServiceSpec, error) {
|
||||
if ctx.Container.Name == "" {
|
||||
return ServiceSpec{}, fmt.Errorf("container name must not be empty")
|
||||
}
|
||||
|
||||
spec := s.Clone()
|
||||
|
||||
for i := range spec.Volumes {
|
||||
volume := &spec.Volumes[i]
|
||||
if volume.Type != VolumeTypeBind {
|
||||
continue
|
||||
}
|
||||
if err := volume.Validate(); err != nil {
|
||||
return ServiceSpec{}, fmt.Errorf("invalid bind volume '%s': %w", volume.Name, err)
|
||||
}
|
||||
|
||||
hostPath, err := renderRuntimeTemplate(volume.BindOptions.HostPath, ctx)
|
||||
if err != nil {
|
||||
return ServiceSpec{}, fmt.Errorf("render runtime template in bind volume '%s' host path: %w",
|
||||
volume.Name, err)
|
||||
}
|
||||
volume.BindOptions.HostPath = hostPath
|
||||
|
||||
if err = volume.Validate(); err != nil {
|
||||
return ServiceSpec{}, fmt.Errorf("invalid rendered bind volume '%s': %w", volume.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range spec.Container.VolumeMounts {
|
||||
volumeMount := &spec.Container.VolumeMounts[i]
|
||||
if err := volumeMount.Validate(); err != nil {
|
||||
return ServiceSpec{}, fmt.Errorf("invalid volume mount '%s': %w", volumeMount.VolumeName, err)
|
||||
}
|
||||
|
||||
containerPath, err := renderRuntimeTemplate(volumeMount.ContainerPath, ctx)
|
||||
if err != nil {
|
||||
return ServiceSpec{}, fmt.Errorf(
|
||||
"render runtime template in volume '%s' container path: %w", volumeMount.VolumeName, err)
|
||||
}
|
||||
volumeMount.ContainerPath = containerPath
|
||||
|
||||
if err = volumeMount.Validate(); err != nil {
|
||||
return ServiceSpec{}, fmt.Errorf("invalid rendered volume mount '%s': %w", volumeMount.VolumeName, err)
|
||||
}
|
||||
}
|
||||
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func renderRuntimeTemplate(value string, ctx RuntimeTemplateContext) (string, error) {
|
||||
tmpl, err := template.New("runtime template").Option("missingkey=error").Parse(value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse runtime template: %w", err)
|
||||
}
|
||||
|
||||
var rendered bytes.Buffer
|
||||
if err = tmpl.Execute(&rendered, ctx); err != nil {
|
||||
return "", fmt.Errorf("execute runtime template: %w", err)
|
||||
}
|
||||
|
||||
return rendered.String(), nil
|
||||
}
|
||||
|
||||
func validateRuntimeTemplates(spec *ServiceSpec) error {
|
||||
_, err := spec.RenderRuntimeTemplates(RuntimeTemplateContext{
|
||||
Container: RuntimeTemplateContainerContext{Name: "validation-container-name"},
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServiceSpec_RenderRuntimeTemplates(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
spec := ServiceSpec{
|
||||
Container: ContainerSpec{
|
||||
Image: "busybox:latest",
|
||||
Env: EnvVars{
|
||||
"UNCHANGED": "{{.Container.Name}}",
|
||||
},
|
||||
VolumeMounts: []VolumeMount{
|
||||
{VolumeName: "config", ContainerPath: "/etc/app/{{.Container.Name}}", ReadOnly: true},
|
||||
{VolumeName: "data", ContainerPath: "/{{.Container.Name}}"},
|
||||
{VolumeName: "scratch", ContainerPath: "/tmp/{{.Container.Name}}"},
|
||||
},
|
||||
Volumes: []string{"/legacy/{{.Container.Name}}:/legacy"},
|
||||
},
|
||||
Volumes: []VolumeSpec{
|
||||
{
|
||||
Name: "config",
|
||||
Type: VolumeTypeBind,
|
||||
BindOptions: &BindOptions{
|
||||
HostPath: "/var/lib/uncloud/{{.Container.Name}}",
|
||||
CreateHostPath: true,
|
||||
Propagation: mount.PropagationRShared,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "data",
|
||||
Type: VolumeTypeVolume,
|
||||
VolumeOptions: &VolumeOptions{
|
||||
Name: "data-{{.Container.Name}}",
|
||||
},
|
||||
},
|
||||
{Name: "scratch", Type: VolumeTypeTmpfs},
|
||||
},
|
||||
}
|
||||
original := spec.Clone()
|
||||
|
||||
rendered, err := spec.RenderRuntimeTemplates(RuntimeTemplateContext{
|
||||
Container: RuntimeTemplateContainerContext{Name: "web-a1b2"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "/var/lib/uncloud/web-a1b2", rendered.Volumes[0].BindOptions.HostPath)
|
||||
assert.True(t, rendered.Volumes[0].BindOptions.CreateHostPath)
|
||||
assert.Equal(t, mount.PropagationRShared, rendered.Volumes[0].BindOptions.Propagation)
|
||||
assert.Equal(t, "data-{{.Container.Name}}", rendered.Volumes[1].VolumeOptions.Name)
|
||||
assert.Equal(t, "/etc/app/web-a1b2", rendered.Container.VolumeMounts[0].ContainerPath)
|
||||
assert.Equal(t, "/web-a1b2", rendered.Container.VolumeMounts[1].ContainerPath)
|
||||
assert.Equal(t, "/tmp/web-a1b2", rendered.Container.VolumeMounts[2].ContainerPath)
|
||||
assert.Equal(t, "{{.Container.Name}}", rendered.Container.Env["UNCHANGED"])
|
||||
assert.Equal(t, "/legacy/{{.Container.Name}}:/legacy", rendered.Container.Volumes[0])
|
||||
assert.Equal(t, original, spec)
|
||||
}
|
||||
|
||||
func TestServiceSpec_RenderRuntimeTemplates_GoTemplateLanguage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
runtimeTemplate string
|
||||
containerName string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "condition variables and pipeline",
|
||||
runtimeTemplate: `/data/{{if .Container.Name}}{{$name := .Container.Name}}{{$name | printf "%s"}}{{end}}`,
|
||||
containerName: "web-a1b2",
|
||||
want: "/data/web-a1b2",
|
||||
},
|
||||
{
|
||||
name: "literal delimiters",
|
||||
runtimeTemplate: `/data/{{"{{"}}.Container.Name{{"}}"}}`,
|
||||
containerName: "web-a1b2",
|
||||
want: "/data/{{.Container.Name}}",
|
||||
},
|
||||
{
|
||||
name: "one pass",
|
||||
runtimeTemplate: "/data/{{.Container.Name}}",
|
||||
containerName: "{{.Container.Name}}",
|
||||
want: "/data/{{.Container.Name}}",
|
||||
},
|
||||
{
|
||||
name: "static path",
|
||||
runtimeTemplate: "/data/static",
|
||||
containerName: "web-a1b2",
|
||||
want: "/data/static",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
spec := serviceSpecWithBindRuntimeTemplate(tt.runtimeTemplate)
|
||||
rendered, err := spec.RenderRuntimeTemplates(RuntimeTemplateContext{
|
||||
Container: RuntimeTemplateContainerContext{Name: tt.containerName},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, rendered.Volumes[0].BindOptions.HostPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceSpec_RenderRuntimeTemplates_Errors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
runtimeTemplate string
|
||||
containerName string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "empty container name",
|
||||
runtimeTemplate: "/data/{{.Container.Name}}",
|
||||
containerName: "",
|
||||
wantErr: "container name must not be empty",
|
||||
},
|
||||
{
|
||||
name: "malformed runtime template",
|
||||
runtimeTemplate: "/data/{{.Container.Name",
|
||||
containerName: "web-a1b2",
|
||||
wantErr: "parse runtime template",
|
||||
},
|
||||
{
|
||||
name: "unknown field",
|
||||
runtimeTemplate: "/data/{{.Container.ID}}",
|
||||
containerName: "web-a1b2",
|
||||
wantErr: "can't evaluate field ID",
|
||||
},
|
||||
{
|
||||
name: "unregistered function",
|
||||
runtimeTemplate: `/data/{{env "HOME"}}`,
|
||||
containerName: "web-a1b2",
|
||||
wantErr: `function "env" not defined`,
|
||||
},
|
||||
{
|
||||
name: "relative rendered path",
|
||||
runtimeTemplate: "{{.Container.Name}}",
|
||||
containerName: "web-a1b2",
|
||||
wantErr: "must be an absolute path",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
spec := serviceSpecWithBindRuntimeTemplate(tt.runtimeTemplate)
|
||||
_, err := spec.RenderRuntimeTemplates(RuntimeTemplateContext{
|
||||
Container: RuntimeTemplateContainerContext{Name: tt.containerName},
|
||||
})
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceSpec_Validate_RuntimeTemplates(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
runtimeTemplate string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
runtimeTemplate: "/data/{{.Container.Name}}",
|
||||
},
|
||||
{
|
||||
name: "malformed",
|
||||
runtimeTemplate: "/data/{{.Container.Name",
|
||||
wantErr: "validate runtime templates",
|
||||
},
|
||||
{
|
||||
name: "unknown field",
|
||||
runtimeTemplate: "/data/{{.Container.ID}}",
|
||||
wantErr: "can't evaluate field ID",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
spec := serviceSpecWithBindRuntimeTemplate(tt.runtimeTemplate)
|
||||
err := spec.Validate()
|
||||
if tt.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func serviceSpecWithBindRuntimeTemplate(hostPath string) ServiceSpec {
|
||||
return ServiceSpec{
|
||||
Name: "web",
|
||||
Container: ContainerSpec{
|
||||
Image: "busybox:latest",
|
||||
VolumeMounts: []VolumeMount{
|
||||
{VolumeName: "data", ContainerPath: "/data"},
|
||||
},
|
||||
},
|
||||
Volumes: []VolumeSpec{
|
||||
{
|
||||
Name: "data",
|
||||
Type: VolumeTypeBind,
|
||||
BindOptions: &BindOptions{HostPath: hostPath},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -214,6 +214,10 @@ func (s *ServiceSpec) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateRuntimeTemplates(s); err != nil {
|
||||
return fmt.Errorf("validate runtime templates: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+10
-6
@@ -3,10 +3,10 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"path"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
@@ -39,7 +39,8 @@ type VolumeSpec struct {
|
||||
|
||||
// BindOptions represents options for a bind volume.
|
||||
type BindOptions struct {
|
||||
// HostPath is the absolute path on the host filesystem.
|
||||
// HostPath is the absolute path on the host filesystem. It may contain runtime templates such as
|
||||
// {{.Container.Name}}, which the destination daemon renders before creating the container.
|
||||
HostPath string
|
||||
// CreateHostPath indicates whether the host path should be created if it doesn't exist.
|
||||
// If false, deployment will fail if the path doesn't exist.
|
||||
@@ -103,6 +104,9 @@ func (v *VolumeSpec) Validate() error {
|
||||
if v.BindOptions == nil {
|
||||
return fmt.Errorf("bind volume must have bind options")
|
||||
}
|
||||
if !path.IsAbs(v.BindOptions.HostPath) {
|
||||
return fmt.Errorf("invalid host path: %q must be an absolute path", v.BindOptions.HostPath)
|
||||
}
|
||||
case VolumeTypeVolume, VolumeTypeTmpfs:
|
||||
default:
|
||||
return fmt.Errorf("invalid volume type: '%s', must be one of '%s', '%s', '%s')",
|
||||
@@ -196,7 +200,8 @@ func (v *VolumeSpec) Clone() VolumeSpec {
|
||||
type VolumeMount struct {
|
||||
// VolumeName references a volume defined in ServiceSpec.Volumes by its Name field.
|
||||
VolumeName string
|
||||
// ContainerPath is the absolute path where the volume is mounted in the container.
|
||||
// ContainerPath is the absolute path where the volume is mounted in the container. It may contain runtime templates
|
||||
// such as {{.Container.Name}}, which the destination daemon renders before creating the container.
|
||||
ContainerPath string
|
||||
// ReadOnly indicates whether the volume should be mounted read-only.
|
||||
// If false (default), the volume is mounted read-write.
|
||||
@@ -207,9 +212,8 @@ func (m *VolumeMount) Validate() error {
|
||||
if m.VolumeName == "" {
|
||||
return fmt.Errorf("volume name must not be empty")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(m.ContainerPath, "/") {
|
||||
return fmt.Errorf("invalid container path: '%s', must be an absolute path in the container", m.ContainerPath)
|
||||
if !path.IsAbs(m.ContainerPath) {
|
||||
return fmt.Errorf("invalid container path: %q must be an absolute path", m.ContainerPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -53,6 +53,11 @@ func (cli *Client) NewCaddyDeployment(image, config string, placement api.Placem
|
||||
VolumeName: "run",
|
||||
ContainerPath: "/run/caddy",
|
||||
},
|
||||
{
|
||||
VolumeName: "uncloud-api",
|
||||
ContainerPath: "/run/uncloud/api",
|
||||
ReadOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Mode: api.ServiceModeGlobal,
|
||||
@@ -95,6 +100,16 @@ func (cli *Client) NewCaddyDeployment(image, config string, placement api.Placem
|
||||
CreateHostPath: true,
|
||||
},
|
||||
},
|
||||
// Bind the Uncloud API socket so caddy.storage.uncloud module can use it to store assets in the cluster.
|
||||
{
|
||||
Name: "uncloud-api",
|
||||
Type: api.VolumeTypeBind,
|
||||
BindOptions: &api.BindOptions{
|
||||
// Mount the parent directory that contains the socket so it lets the container see a replacement
|
||||
// socket after the Uncloud daemon restarts (in case it doesn't use a systemd-activated socket).
|
||||
HostPath: "/run/uncloud/api",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -160,6 +160,10 @@ func TestServiceSpecFromCompose(t *testing.T) {
|
||||
ContainerPath: "/host/etc/passwd",
|
||||
ReadOnly: true,
|
||||
},
|
||||
{
|
||||
VolumeName: "bind-cd9bef32904c6831a0cd0d49cf503078a46a0989acea375fdd3d3b43b2bfec4f",
|
||||
ContainerPath: "/unique/host/bind",
|
||||
},
|
||||
{
|
||||
VolumeName: "data1",
|
||||
ContainerPath: "/data1",
|
||||
@@ -240,6 +244,14 @@ func TestServiceSpecFromCompose(t *testing.T) {
|
||||
CreateHostPath: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "bind-cd9bef32904c6831a0cd0d49cf503078a46a0989acea375fdd3d3b43b2bfec4f",
|
||||
Type: "bind",
|
||||
BindOptions: &api.BindOptions{
|
||||
HostPath: "/runtime/template/{{.Container.Name}}",
|
||||
CreateHostPath: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "data-external",
|
||||
Type: api.VolumeTypeVolume,
|
||||
@@ -1293,3 +1305,41 @@ services:
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceSpecFromCompose_RuntimeTemplates(t *testing.T) {
|
||||
t.Setenv("UNCLOUD_TEST_TEMPLATE_ROOT", "uncloud-test")
|
||||
|
||||
project, err := LoadProjectFromContent(context.Background(), `
|
||||
services:
|
||||
short:
|
||||
image: busybox:latest
|
||||
volumes:
|
||||
- "/var/lib/${UNCLOUD_TEST_TEMPLATE_ROOT}/{{.Container.Name}}:/data/{{.Container.Name}}:ro"
|
||||
long:
|
||||
image: busybox:latest
|
||||
volumes:
|
||||
- type: bind
|
||||
source: "/var/lib/${UNCLOUD_TEST_TEMPLATE_ROOT}/{{.Container.Name}}"
|
||||
target: "/data/{{.Container.Name}}"
|
||||
read_only: true
|
||||
bind:
|
||||
create_host_path: true
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
shortSpec, err := ServiceSpecFromCompose(project, "short")
|
||||
require.NoError(t, err)
|
||||
longSpec, err := ServiceSpecFromCompose(project, "long")
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, spec := range []api.ServiceSpec{shortSpec, longSpec} {
|
||||
require.Len(t, spec.Volumes, 1)
|
||||
require.NotNil(t, spec.Volumes[0].BindOptions)
|
||||
assert.Equal(t, "/var/lib/uncloud-test/{{.Container.Name}}", spec.Volumes[0].BindOptions.HostPath)
|
||||
assert.True(t, spec.Volumes[0].BindOptions.CreateHostPath)
|
||||
require.Len(t, spec.Container.VolumeMounts, 1)
|
||||
assert.Equal(t, "/data/{{.Container.Name}}", spec.Container.VolumeMounts[0].ContainerPath)
|
||||
assert.True(t, spec.Container.VolumeMounts[0].ReadOnly)
|
||||
require.NoError(t, spec.Validate())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ services:
|
||||
user: nginx:nginx
|
||||
volumes:
|
||||
- /etc/passwd:/host/etc/passwd:ro
|
||||
- /runtime/template/{{.Container.Name}}:/unique/host/bind
|
||||
- data1:/data1
|
||||
- type: bind
|
||||
source: /path/on/host
|
||||
|
||||
+18
-2
@@ -38,9 +38,10 @@ func assertServiceMatchesSpec(t *testing.T, svc api.Service, spec api.ServiceSpe
|
||||
}
|
||||
|
||||
func assertContainerMatchesSpec(t *testing.T, ctr api.ServiceContainer, spec api.ServiceSpec) {
|
||||
spec = spec.SetDefaults()
|
||||
status := deploy.EvalContainerSpecChange(ctr.ServiceSpec, spec)
|
||||
desiredSpec := spec.SetDefaults()
|
||||
status := deploy.EvalContainerSpecChange(ctr.ServiceSpec, desiredSpec)
|
||||
assert.Equal(t, deploy.ContainerUpToDate, status)
|
||||
spec = renderRuntimeTemplatesForContainer(t, desiredSpec, ctr.Name)
|
||||
|
||||
// Verify labels.
|
||||
assert.True(t, api.ValidateServiceID(ctr.Config.Labels[api.LabelServiceID]))
|
||||
@@ -148,8 +149,10 @@ func assertHookContainersMatchSpec(t *testing.T, svc api.Service, spec api.Servi
|
||||
t.Helper()
|
||||
require.NotEmpty(t, svc.HookContainers, "Expected at least one hook container")
|
||||
|
||||
desiredSpec := spec
|
||||
for _, mc := range svc.HookContainers {
|
||||
ctr := mc.Container
|
||||
spec := renderRuntimeTemplatesForContainer(t, desiredSpec, ctr.Name)
|
||||
|
||||
// Verify labels.
|
||||
assert.True(t, api.ValidateServiceID(ctr.Config.Labels[api.LabelServiceID]))
|
||||
@@ -215,6 +218,19 @@ func assertHookContainersMatchSpec(t *testing.T, svc api.Service, spec api.Servi
|
||||
}
|
||||
}
|
||||
|
||||
func renderRuntimeTemplatesForContainer(
|
||||
t *testing.T, spec api.ServiceSpec, containerName string,
|
||||
) api.ServiceSpec {
|
||||
t.Helper()
|
||||
|
||||
renderedSpec, err := spec.RenderRuntimeTemplates(api.RuntimeTemplateContext{
|
||||
Container: api.RuntimeTemplateContainerContext{Name: containerName},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return renderedSpec
|
||||
}
|
||||
|
||||
func assertContainerMountsMatchSpec(t *testing.T, mounts []mount.Mount, spec api.ServiceSpec) {
|
||||
expectedMounts, err := machinedocker.ToDockerMounts(spec.Volumes, spec.Container.VolumeMounts)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -36,6 +36,10 @@ func createTestCluster(
|
||||
if envName != "" {
|
||||
c, err := p.InspectCluster(ctx, envName)
|
||||
if err == nil {
|
||||
if waitReady {
|
||||
require.NoError(t, p.WaitClusterReady(ctx, c, 90*time.Second))
|
||||
require.NoError(t, p.WaitClusterMeshReady(ctx, c, 90*time.Second))
|
||||
}
|
||||
return c, p
|
||||
}
|
||||
if !errors.Is(err, ucind.ErrNotFound) {
|
||||
@@ -60,6 +64,7 @@ func createTestCluster(
|
||||
|
||||
if waitReady {
|
||||
require.NoError(t, p.WaitClusterReady(ctx, c, 90*time.Second))
|
||||
require.NoError(t, p.WaitClusterMeshReady(ctx, c, 90*time.Second))
|
||||
}
|
||||
|
||||
return c, p
|
||||
@@ -111,6 +116,7 @@ func TestClusterLifecycle(t *testing.T) {
|
||||
}, 30*time.Second, 50*time.Millisecond, "cluster store not reconciled on machine #%d", i+1)
|
||||
}
|
||||
})
|
||||
require.NoError(t, p.WaitClusterMeshReady(ctx, c, 90*time.Second))
|
||||
|
||||
t.Run("inspect", func(t *testing.T) {
|
||||
cluster, err := p.InspectCluster(ctx, name)
|
||||
|
||||
@@ -282,6 +282,11 @@ func TestComposeDeployment(t *testing.T) {
|
||||
ContainerPath: "/host/etc/passwd",
|
||||
ReadOnly: true,
|
||||
},
|
||||
{
|
||||
VolumeName: "bind-b78f4a1e9255dca2997ee1006f32b0fc168bac785cf2440f00aa84c61e58f6fb",
|
||||
ContainerPath: "/host/{{.Container.Name}}",
|
||||
ReadOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Volumes: []api.VolumeSpec{
|
||||
@@ -297,6 +302,14 @@ func TestComposeDeployment(t *testing.T) {
|
||||
CreateHostPath: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "bind-b78f4a1e9255dca2997ee1006f32b0fc168bac785cf2440f00aa84c61e58f6fb",
|
||||
Type: api.VolumeTypeBind,
|
||||
BindOptions: &api.BindOptions{
|
||||
HostPath: "/tmp/uncloud-e2e/{{.Container.Name}}",
|
||||
CreateHostPath: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Replicas: 3,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ services:
|
||||
volumes:
|
||||
- test-compose-volumes-data1:/data1
|
||||
- /etc/passwd:/host/etc/passwd:ro
|
||||
- "/tmp/uncloud-e2e/{{.Container.Name}}:/host/{{.Container.Name}}:ro"
|
||||
deploy:
|
||||
replicas: 3
|
||||
test-compose-volumes-service2:
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/psviderski/uncloud/api/pb"
|
||||
"github.com/psviderski/uncloud/internal/ucind"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/psviderski/uncloud/pkg/client"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -456,6 +457,23 @@ func TestMachineOperations(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("remove machine clears container records from cluster store", func(t *testing.T) {
|
||||
// The Caddy controller needs a local Caddy container to supply the global config before it can generate
|
||||
// routes. Place it on the connected machine by ID because earlier tests rename that machine.
|
||||
caddyDeployment, err := cli.NewCaddyDeployment("", "", api.Placement{
|
||||
Machines: []string{c.Machines[0].ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
monitorPeriod := 5 * time.Second
|
||||
caddyDeployment.Spec.UpdateConfig.MonitorPeriod = &monitorPeriod
|
||||
_, err = caddyDeployment.Run(ctx)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err := cli.RemoveService(ctx, client.CaddyServiceName)
|
||||
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
|
||||
// Deploy a global service with an HTTP ingress port so the auto-generated Caddyfile lists
|
||||
// each container's IP as an upstream.
|
||||
serviceName := "test-machine-rm-cleanup"
|
||||
|
||||
+23
-10
@@ -27,6 +27,8 @@ import (
|
||||
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func newServiceID() string {
|
||||
@@ -42,6 +44,9 @@ func TestDeployment(t *testing.T) {
|
||||
|
||||
clusterName := "ucind-test.deployment"
|
||||
ctx := context.Background()
|
||||
// Caddy may restart once while the controller writes its bootstrap file. Use the normal deployment monitor period
|
||||
// instead of the zero-duration override used by most e2e tests.
|
||||
caddyMonitorPeriod := 5 * time.Second
|
||||
c, _ := createTestCluster(t, clusterName, ucind.CreateClusterOptions{Machines: 3}, true)
|
||||
|
||||
cli, cErr := c.Machines[0].Connect(ctx)
|
||||
@@ -313,6 +318,7 @@ func TestDeployment(t *testing.T) {
|
||||
|
||||
deployment, err := cli.NewCaddyDeployment("", "", api.Placement{})
|
||||
require.NoError(t, err)
|
||||
deployment.Spec.UpdateConfig.MonitorPeriod = &caddyMonitorPeriod
|
||||
|
||||
_, err = deployment.Run(ctx)
|
||||
require.NoError(t, err)
|
||||
@@ -345,6 +351,7 @@ func TestDeployment(t *testing.T) {
|
||||
Machines: []string{c.Machines[0].Name},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
deployment.Spec.UpdateConfig.MonitorPeriod = &caddyMonitorPeriod
|
||||
image := deployment.Spec.Container.Image
|
||||
|
||||
_, err = deployment.Run(ctx)
|
||||
@@ -361,6 +368,7 @@ func TestDeployment(t *testing.T) {
|
||||
// Deploy to all machines without a placement constraint.
|
||||
deployment, err = cli.NewCaddyDeployment(image, "", api.Placement{})
|
||||
require.NoError(t, err)
|
||||
deployment.Spec.UpdateConfig.MonitorPeriod = &caddyMonitorPeriod
|
||||
|
||||
_, err = deployment.Run(ctx)
|
||||
require.NoError(t, err)
|
||||
@@ -391,6 +399,13 @@ func TestDeployment(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// Without a Caddy container, the controller has no global config to pair with application configs. Keep any
|
||||
// previously saved Caddyfile unchanged rather than publishing a new one without Caddy's global settings.
|
||||
savedBefore, savedBeforeErr := cli.Caddy.GetConfig(ctx, nil)
|
||||
if savedBeforeErr != nil {
|
||||
require.Equal(t, codes.NotFound, status.Code(savedBeforeErr))
|
||||
}
|
||||
|
||||
// First deploy a service with custom caddy config before caddy is deployed.
|
||||
serviceCaddyfile := `test-custom-caddy-config.example.com {
|
||||
reverse_proxy {{upstreams}} {
|
||||
@@ -416,17 +431,13 @@ func TestDeployment(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assertServiceMatchesSpec(t, svc, spec)
|
||||
|
||||
// Check that the generated Caddyfile contains a comment that user-define configs were skipped.
|
||||
var config *pb.GetCaddyConfigResponse
|
||||
require.Eventually(t, func() bool {
|
||||
config, err = cli.Caddy.GetConfig(ctx, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
require.Never(t, func() bool {
|
||||
current, currentErr := cli.Caddy.GetConfig(ctx, nil)
|
||||
if savedBeforeErr != nil {
|
||||
return currentErr == nil
|
||||
}
|
||||
return strings.Contains(config.Caddyfile, "# NOTE: User-defined configs for services were skipped")
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
|
||||
assert.NotContains(t, config.Caddyfile, "test-custom-caddy-config.example.com {")
|
||||
return currentErr == nil && current.Caddyfile != savedBefore.Caddyfile
|
||||
}, 2*time.Second, 100*time.Millisecond)
|
||||
|
||||
// Now deploy caddy with custom config.
|
||||
caddyCaddyfile := `{
|
||||
@@ -438,6 +449,7 @@ myapp.example.com {
|
||||
}`
|
||||
caddyDeployment, err := cli.NewCaddyDeployment("", caddyCaddyfile, api.Placement{})
|
||||
require.NoError(t, err)
|
||||
caddyDeployment.Spec.UpdateConfig.MonitorPeriod = &caddyMonitorPeriod
|
||||
|
||||
_, err = caddyDeployment.Run(ctx)
|
||||
require.NoError(t, err)
|
||||
@@ -447,6 +459,7 @@ myapp.example.com {
|
||||
assertServiceMatchesSpec(t, caddySvc, caddyDeployment.Spec)
|
||||
|
||||
// Wait for the Caddyfile to be regenerated with both custom configs.
|
||||
var config *pb.GetCaddyConfigResponse
|
||||
require.Eventually(t, func() bool {
|
||||
config, err = cli.Caddy.GetConfig(ctx, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -83,6 +83,7 @@ services:
|
||||
image: caddybuilds/caddy-cloudflare:2.10.2
|
||||
command: caddy run -c /config/Caddyfile
|
||||
environment:
|
||||
# unix// is not a typo. Caddy uses network/address format, not a unix:// URL.
|
||||
CADDY_ADMIN: unix//run/caddy/admin.sock
|
||||
env_file:
|
||||
# Contains CLOUDFLARE_API_TOKEN=xxxxx
|
||||
@@ -91,6 +92,9 @@ services:
|
||||
- /var/lib/uncloud/caddy:/data
|
||||
- /var/lib/uncloud/caddy:/config
|
||||
- /run/uncloud/caddy:/run/caddy
|
||||
# Required by caddy.storage.uncloud module. Remove this mount if Uncloud cluster storage is not used for Caddy.
|
||||
# Mount the directory, not the socket file, so Caddy sees a replacement socket after the daemon restarts.
|
||||
- /run/uncloud/api:/run/uncloud/api:ro
|
||||
x-ports:
|
||||
- 80:80@host
|
||||
- 443:443@host
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# Runtime templates
|
||||
|
||||
> Added in [v0.21.0](https://github.com/psviderski/uncloud/releases/tag/v0.21.0)
|
||||
|
||||
Runtime templates let service configuration use metadata that is only known when Uncloud creates a container. They use
|
||||
[Go template](https://pkg.go.dev/text/template) syntax.
|
||||
|
||||
Uncloud renders runtime templates on the destination machine before it creates each container. This means that every
|
||||
replica can receive a value based on its own container metadata.
|
||||
|
||||
## Available metadata
|
||||
|
||||
Runtime templates currently expose the following fields:
|
||||
|
||||
| Field | Description | Example |
|
||||
|-------------------|--------------------------------|------------|
|
||||
| `.Container.Name` | Name assigned to the container | `app-c1zd` |
|
||||
|
||||
## Supported template locations
|
||||
|
||||
You can currently use runtime templates in these locations of a service definition:
|
||||
|
||||
| Location | Compose attribute |
|
||||
|---------------------------------|-----------------------------------------------------------------------------|
|
||||
| Host path of a bind mount | `volumes[].source` or `volumes` short-syntax `- /host/path:/container/path` |
|
||||
| Mount path inside the container | `volumes[].target` or `volumes` short-syntax `- /host/path:/container/path` |
|
||||
|
||||
:::tip Want other locations or metadata fields?
|
||||
|
||||
If you want to use runtime templates in other locations, such as environment variable, or use other metadata fields, add
|
||||
a 👍 reaction or comment with your use case on the issue [#431](https://github.com/psviderski/uncloud/issues/431).
|
||||
|
||||
:::
|
||||
|
||||
## Use runtime templates in a Compose file
|
||||
|
||||
The following service gets a separate host path for every replica:
|
||||
|
||||
```yaml title="compose.yaml"
|
||||
services:
|
||||
app:
|
||||
image: app:latest
|
||||
volumes:
|
||||
- "/var/lib/app/{{.Container.Name}}:/data"
|
||||
scale: 2
|
||||
```
|
||||
|
||||
If Uncloud names the containers `app-c1zd` and `app-f7kx`, their host paths are `/var/lib/app/app-c1zd` and
|
||||
`/var/lib/app/app-f7kx`.
|
||||
|
||||
You can combine runtime templates with
|
||||
[Compose environment interpolation](https://github.com/compose-spec/compose-spec/blob/main/12-interpolation.md):
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- "${DATA_ROOT:-/var/lib/app}/{{.Container.Name}}:/data"
|
||||
```
|
||||
|
||||
Uncloud expands the `DATA_ROOT` environment variable on the local machine first and renders `.Container.Name` later on
|
||||
the destination machine.
|
||||
|
||||
## Use runtime templates with `uc run`
|
||||
|
||||
Quote the volume argument so your shell passes the template through unchanged:
|
||||
|
||||
```shell
|
||||
uc run --replicas 2 \
|
||||
--volume "/var/lib/app/{{.Container.Name}}:/data" \
|
||||
app:latest
|
||||
```
|
||||
|
||||
:::warning Clean up templated host directories
|
||||
|
||||
Uncloud does not remove host directories created for bind mounts when it replaces a container or removes a service. If a
|
||||
runtime template gives each container a unique host directory, you are responsible for cleaning up directories that are
|
||||
no longer needed.
|
||||
|
||||
If you only need per-container ephemeral storage, consider a `tmpfs` mount. An anonymous volume declared by the Docker
|
||||
image can also provide Docker-managed disk storage for each container. Use a named volume or a stable bind mount path
|
||||
for data that must remain available across container replacements.
|
||||
|
||||
:::
|
||||
|
||||
## Runtime templates and image tag templates
|
||||
|
||||
Runtime templates are separate from [image tag templates](../../8-compose-file-reference/3-image-tag-template.md). Image
|
||||
tag templates run locally when Uncloud builds an image. Runtime templates run on a cluster machine when Uncloud deploys
|
||||
a container. Each template type exposes different metadata.
|
||||
|
||||
## See also
|
||||
|
||||
- [Compose support matrix](../../8-compose-file-reference/1-support-matrix.md)
|
||||
- [`uc run` reference](../../9-cli-reference/uc_run.md)
|
||||
@@ -64,7 +64,7 @@ If you rely on a specific Compose feature that is not supported by Uncloud, plea
|
||||
| `update_config` | ⚠️ Limited | `order` and `monitor` supported. See [rolling deployments](../4-guides/1-deployments/4-rolling-deployments.md) |
|
||||
| **Volumes** | | |
|
||||
| Named volumes | ✅ Supported | Docker volumes |
|
||||
| Bind mounts | ✅ Supported | Host path binding |
|
||||
| Bind mounts | ✅ Supported | Host path binding with optional [runtime templates](../3-concepts/6-services/3-runtime-templates.md) |
|
||||
| Tmpfs mounts | ✅ Supported | In-memory filesystems |
|
||||
| Volume labels | ✅ Supported | Custom labels |
|
||||
| External volumes | ✅ Supported | Must exist before deployment |
|
||||
|
||||
@@ -143,6 +143,7 @@ image: myapp:{{gitsha 7}}.${GITHUB_RUN_ID:-local} # GITHUB_RUN_ID not set →
|
||||
|
||||
## See also
|
||||
|
||||
- [Runtime templates](../3-concepts/6-services/3-runtime-templates.md): Use per-container metadata in service configuration
|
||||
- [Deploy an app](../4-guides/1-deployments/1-deploy-app.md): Deploy from source code or pre-built images
|
||||
- [Compose Build Specification](https://github.com/compose-spec/compose-spec/blob/main/build.md)
|
||||
- [Compose Specification: image](https://github.com/compose-spec/compose-spec/blob/main/spec.md#image)
|
||||
|
||||
@@ -47,6 +47,7 @@ uc run IMAGE [COMMAND...] [flags]
|
||||
-v, --volume strings Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where
|
||||
the volume is located. Can be specified multiple times.
|
||||
Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]
|
||||
Host and container paths support runtime templates such as {{.Container.Name}}.
|
||||
Examples:
|
||||
-v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container
|
||||
-v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container
|
||||
|
||||
@@ -47,6 +47,7 @@ uc service run IMAGE [COMMAND...] [flags]
|
||||
-v, --volume strings Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where
|
||||
the volume is located. Can be specified multiple times.
|
||||
Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]
|
||||
Host and container paths support runtime templates such as {{.Container.Name}}.
|
||||
Examples:
|
||||
-v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container
|
||||
-v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
<a href="https://uncloud.run/discord" target="_blank" rel="noopener"
|
||||
class="group block relative text-center md:px-5 after:hidden md:after:block after:absolute after:right-0 after:top-1/2 after:-translate-y-1/2 after:w-px after:h-8 after:border-l after:border-zinc-300 after:border-dashed last:after:hidden">
|
||||
<h4 class="font-inter-tight text-2xl md:text-3xl font-bold tabular-nums mb-2"><span
|
||||
x-data="counter(390)" x-text="counterValue">390</span>+</h4>
|
||||
x-data="counter(420)" x-text="counterValue">420</span>+</h4>
|
||||
<p class="text-zinc-500 transition-colors group-hover:text-zinc-800">Fans on
|
||||
Discord</p>
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user