diff --git a/commands/install-runner.go b/commands/install-runner.go index e073008e..bf5debbe 100644 --- a/commands/install-runner.go +++ b/commands/install-runner.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/docker/model-cli/pkg/types" "os" - "time" "github.com/docker/docker/api/types/container" "github.com/docker/model-cli/commands/completion" @@ -16,34 +15,6 @@ import ( "github.com/spf13/cobra" ) -const ( - // installWaitTries controls how many times the automatic installation will - // try to reach the model runner while waiting for it to be ready. - installWaitTries = 20 - // installWaitRetryInterval controls the interval at which automatic - // installation will try to reach the model runner while waiting for it to - // be ready. - installWaitRetryInterval = 500 * time.Millisecond -) - -// waitForStandaloneRunnerAfterInstall waits for a standalone model runner -// container to come online after installation. The CPU version can take about a -// second to start serving requests once the container has started, the CUDA -// version can take several seconds. -func waitForStandaloneRunnerAfterInstall(ctx context.Context) error { - for tries := installWaitTries; tries > 0; tries-- { - if status := desktopClient.Status(); status.Error == nil && status.Running { - return nil - } - select { - case <-time.After(installWaitRetryInterval): - case <-ctx.Done(): - return errors.New("cancelled waiting for standalone model runner to initialize") - } - } - return errors.New("standalone model runner took too long to initialize") -} - // standaloneRunner encodes the standalone runner configuration, if one exists. type standaloneRunner struct { // hostPort is the port that the runner is listening to on the host. @@ -132,15 +103,11 @@ func ensureStandaloneRunnerAvailable(ctx context.Context, printer standalone.Sta port = standalone.DefaultControllerPortCloud environment = "cloud" } - if err := standalone.CreateControllerContainer(ctx, dockerClient, port, environment, false, gpu, modelStorageVolume, printer, engineKind); err != nil { + err = standalone.CreateControllerContainer(ctx, port, environment, false, gpu, modelStorageVolume, printer, engineKind) + if err != nil { return nil, fmt.Errorf("unable to initialize standalone model runner container: %w", err) } - // Poll until we get a response from the model runner. - if err := waitForStandaloneRunnerAfterInstall(ctx); err != nil { - return nil, err - } - // Find the runner container. // // TODO: We should actually find this before calling @@ -239,12 +206,14 @@ func newInstallRunner() *cobra.Command { return fmt.Errorf("unable to initialize standalone model storage: %w", err) } // Create the model runner container. - if err := standalone.CreateControllerContainer(cmd.Context(), dockerClient, port, environment, doNotTrack, gpu, modelStorageVolume, cmd, engineKind); err != nil { + err = standalone.CreateControllerContainer(cmd.Context(), port, environment, doNotTrack, gpu, modelStorageVolume, cmd, engineKind) + if err != nil { return fmt.Errorf("unable to initialize standalone model runner container: %w", err) } - // Poll until we get a response from the model runner. - return waitForStandaloneRunnerAfterInstall(cmd.Context()) + cmd.Println("Model runner container created") + + return nil }, ValidArgsFunction: completion.NoComplete, } diff --git a/go.mod b/go.mod index 83368b74..2203b4db 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,10 @@ require ( github.com/docker/cli v28.3.0+incompatible github.com/docker/cli-docs-tool v0.10.0 github.com/docker/docker v28.2.2+incompatible - github.com/docker/go-connections v0.5.0 + github.com/docker/go-sdk/client v0.1.0-alpha006 + github.com/docker/go-sdk/config v0.1.0-alpha006 + github.com/docker/go-sdk/container v0.1.0-alpha006 + github.com/docker/go-sdk/context v0.1.0-alpha006 github.com/docker/go-units v0.5.0 github.com/docker/model-distribution v0.0.0-20250627163720-aff34abcf3e0 github.com/docker/model-runner v0.0.0-20250627142917-26a0a73fbbc0 @@ -30,6 +33,7 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/StackExchange/wmi v1.2.1 // indirect + github.com/caarlos0/env/v11 v11.3.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/containerd/containerd/v2 v2.1.3 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect @@ -43,6 +47,9 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-sdk/image v0.1.0-alpha006 // indirect + github.com/docker/go-sdk/network v0.1.0-alpha006 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fvbommel/sortorder v1.1.0 // indirect @@ -53,7 +60,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gpustack/gguf-parser-go v0.14.1 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/henvic/httpretty v0.1.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jaypipes/ghw v0.17.0 // indirect @@ -87,28 +94,26 @@ require ( go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/sdk v1.37.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect - go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.0 // indirect golang.org/x/crypto v0.39.0 // indirect golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/mod v0.25.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.26.0 // indirect - golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.34.0 // indirect gonum.org/v1/gonum v0.15.1 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/grpc v1.72.2 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gotest.tools/v3 v3.5.2 // indirect howett.net/plist v1.0.1 // indirect ) diff --git a/go.sum b/go.sum index 8b6587c3..23672816 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,12 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dR github.com/bugsnag/bugsnag-go v1.0.5/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= @@ -75,6 +79,18 @@ github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6 github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-sdk/client v0.1.0-alpha006 h1:APDYOldHUr+EI97ruJ14gDYU701ENSWJxdZY0A58xZw= +github.com/docker/go-sdk/client v0.1.0-alpha006/go.mod h1:CgtYlN6PmVjkpBjuBlE41dbpDJbwmp3j4eF2XUurBNM= +github.com/docker/go-sdk/config v0.1.0-alpha006 h1:lXBQ4yQLN6aR1trCMPkqdrrgPfH16FzuNHAVv/z31IM= +github.com/docker/go-sdk/config v0.1.0-alpha006/go.mod h1:eygQMlGzqLYetN/Qkc+lkHZJ9KJOMMCLy73OXn1rvzc= +github.com/docker/go-sdk/container v0.1.0-alpha006 h1:wNFSkwdb/NXkhyrFOd6fsm6jJieljHeJE0/8G3LO5Uw= +github.com/docker/go-sdk/container v0.1.0-alpha006/go.mod h1:faz2l1YrW50YD3zxmZmQXpHkXsh+/xuL40ZGeGN83XI= +github.com/docker/go-sdk/context v0.1.0-alpha006 h1:cFyzaxGedF9WQTg1zVheGopYWYISxAyQn2pTTM24S3k= +github.com/docker/go-sdk/context v0.1.0-alpha006/go.mod h1:7n4TkYW0aYCdTp6hiIO/rdIRhI23nZ6N4s5Mjy7UFj4= +github.com/docker/go-sdk/image v0.1.0-alpha006 h1:SNAlDWFQ0vzroUg9bh0Z90ec6JoyPbdVYeCuvbVUJ64= +github.com/docker/go-sdk/image v0.1.0-alpha006/go.mod h1:9Z21DcaPi39hc+p28MLbCJ6xqiJnbUBeoXKfMQuSpv8= +github.com/docker/go-sdk/network v0.1.0-alpha006 h1:f5CtYYzoRKb292CR+Qec0VVoRjmLMMGSG4grVJAkOlI= +github.com/docker/go-sdk/network v0.1.0-alpha006/go.mod h1:KwxdVskGxPygHJJodobohre1uL47TktunQSGEszgZPc= 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/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= @@ -124,8 +140,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gpustack/gguf-parser-go v0.14.1 h1:tmz2eTnSEFfE52V10FESqo9oAUquZ6JKQFntWC/wrEg= github.com/gpustack/gguf-parser-go v0.14.1/go.mod h1:GvHh1Kvvq5ojCOsJ5UpwiJJmIjFw3Qk5cW7R+CZ3IJo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU= github.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM= @@ -253,6 +269,8 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -276,12 +294,12 @@ go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 h1:ajl4QczuJVA2TU9W9AGw++86Xga/RKt//16z/yxPgdk= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0/go.mod h1:Vn3/rlOJ3ntf/Q3zAI0V5lDnTbHGaUsNUeF6nZmm7pA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= @@ -290,8 +308,8 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= -go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= @@ -344,8 +362,8 @@ 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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -358,10 +376,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= -google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 h1:35ZFtrCgaAjF7AFAK0+lRSf+4AyYnWRbH7og13p7rZ4= -google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:W9ynFDP/shebLB1Hl/ESTOap2jHd6pmLXPNZC7SVDbA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= diff --git a/pkg/standalone/containers.go b/pkg/standalone/containers.go index 9d481b67..900e8c50 100644 --- a/pkg/standalone/containers.go +++ b/pkg/standalone/containers.go @@ -1,13 +1,15 @@ package standalone import ( - "archive/tar" - "bytes" "context" "errors" "fmt" "io" "os" + "log/slog" + "net/http" + "path/filepath" + "regexp" "strconv" "strings" "time" @@ -18,7 +20,11 @@ import ( "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" - "github.com/docker/go-connections/nat" + clientsdk "github.com/docker/go-sdk/client" + "github.com/docker/go-sdk/config" + containersdk "github.com/docker/go-sdk/container" + "github.com/docker/go-sdk/container/wait" + contextsdk "github.com/docker/go-sdk/context" gpupkg "github.com/docker/model-cli/pkg/gpu" "github.com/docker/model-cli/pkg/types" ) @@ -26,105 +32,10 @@ import ( // controllerContainerName is the name to use for the controller container. const controllerContainerName = "docker-model-runner" -// copyDockerConfigToContainer copies the Docker config file from the host to the container -// and sets up proper ownership and permissions for the modelrunner user. -// It does nothing for Desktop and Cloud engine kinds. -func copyDockerConfigToContainer(ctx context.Context, dockerClient *client.Client, containerID string, engineKind types.ModelRunnerEngineKind) error { - // Do nothing for Desktop and Cloud engine kinds - if engineKind == types.ModelRunnerEngineKindDesktop || engineKind == types.ModelRunnerEngineKindCloud { - return nil - } - - dockerConfigPath := os.ExpandEnv("$HOME/.docker/config.json") - if s, err := os.Stat(dockerConfigPath); err != nil || s.Mode()&os.ModeType != 0 { - return nil - } - - configData, err := os.ReadFile(dockerConfigPath) - if err != nil { - return fmt.Errorf("failed to read Docker config file: %w", err) - } - - var buf bytes.Buffer - tw := tar.NewWriter(&buf) - header := &tar.Header{ - Name: ".docker/config.json", - Mode: 0600, - Size: int64(len(configData)), - } - if err := tw.WriteHeader(header); err != nil { - return fmt.Errorf("failed to write tar header: %w", err) - } - if _, err := tw.Write(configData); err != nil { - return fmt.Errorf("failed to write config data to tar: %w", err) - } - if err := tw.Close(); err != nil { - return fmt.Errorf("failed to close tar writer: %w", err) - } - - // Ensure the .docker directory exists - mkdirCmd := "mkdir -p /home/modelrunner/.docker && chown modelrunner:modelrunner /home/modelrunner/.docker" - if err := execInContainer(ctx, dockerClient, containerID, mkdirCmd); err != nil { - return err - } - - // Copy directly into the .docker directory - err = dockerClient.CopyToContainer(ctx, containerID, "/home/modelrunner", &buf, container.CopyToContainerOptions{ - CopyUIDGID: true, - }) - if err != nil { - return fmt.Errorf("failed to copy config file to container: %w", err) - } - - // Set correct ownership and permissions - chmodCmd := "chown modelrunner:modelrunner /home/modelrunner/.docker/config.json && chmod 600 /home/modelrunner/.docker/config.json" - if err := execInContainer(ctx, dockerClient, containerID, chmodCmd); err != nil { - return err - } - - return nil -} - -func execInContainer(ctx context.Context, dockerClient *client.Client, containerID, cmd string) error { - execConfig := container.ExecOptions{ - Cmd: []string{"sh", "-c", cmd}, - } - execResp, err := dockerClient.ContainerExecCreate(ctx, containerID, execConfig) - if err != nil { - return fmt.Errorf("failed to create exec for command '%s': %w", cmd, err) - } - if err := dockerClient.ContainerExecStart(ctx, execResp.ID, container.ExecStartOptions{}); err != nil { - return fmt.Errorf("failed to start exec for command '%s': %w", cmd, err) - } - - // Create a timeout context for the polling loop - timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - - // Poll until the command finishes or timeout occurs - for { - inspectResp, err := dockerClient.ContainerExecInspect(ctx, execResp.ID) - if err != nil { - return fmt.Errorf("failed to inspect exec for command '%s': %w", cmd, err) - } - - if !inspectResp.Running { - // Command has finished, now we can safely check the exit code - if inspectResp.ExitCode != 0 { - return fmt.Errorf("command '%s' failed with exit code %d", cmd, inspectResp.ExitCode) - } - return nil - } - - // Brief sleep to avoid busy polling, with timeout check - select { - case <-time.After(100 * time.Millisecond): - // Continue polling - case <-timeoutCtx.Done(): - return fmt.Errorf("command '%s' timed out after 10 seconds", cmd) - } - } -} +// concurrentInstallMatcher matches error message that indicate a concurrent +// standalone model runner installation is taking place. It extracts the ID of +// the conflicting container in a capture group. +var concurrentInstallMatcher = regexp.MustCompile(`is already in use by container "([a-z0-9]+)"`) // FindControllerContainer searches for a running controller container. It // returns the ID of the container (if found), the container name (if any), the @@ -173,9 +84,11 @@ func determineBridgeGatewayIP(ctx context.Context, dockerClient client.NetworkAP return "", nil } -// ensureContainerStarted ensures that a container has started. It may be called -// concurrently, taking advantage of the fact that ContainerStart is idempotent. -func ensureContainerStarted(ctx context.Context, dockerClient client.ContainerAPIClient, containerID string) error { +// waitForContainerToStart waits for a container to start. +func waitForContainerToStart(ctx context.Context, dockerClient *client.Client, containerID string) error { + // Unfortunately the Docker API's /containers/{id}/wait API (and the + // corresponding Client.ContainerWait method) don't allow waiting for + // container startup, so instead we'll take a polling approach. for i := 10; i > 0; i-- { err := dockerClient.ContainerStart(ctx, containerID, container.StartOptions{}) if err == nil { @@ -213,7 +126,9 @@ func ensureContainerStarted(ctx context.Context, dockerClient client.ContainerAP } // CreateControllerContainer creates and starts a controller container. -func CreateControllerContainer(ctx context.Context, dockerClient *client.Client, port uint16, environment string, doNotTrack bool, gpu gpupkg.GPUSupport, modelStorageVolume string, printer StatusPrinter, engineKind types.ModelRunnerEngineKind) error { +func CreateControllerContainer( + ctx context.Context, port uint16, environment string, doNotTrack bool, gpu gpupkg.GPUSupport, modelStorageVolume string, printer StatusPrinter, engineKind types.ModelRunnerEngineKind, +) error { // Determine the target image. var imageName string switch gpu { @@ -223,80 +138,127 @@ func CreateControllerContainer(ctx context.Context, dockerClient *client.Client, imageName = ControllerImage + ":" + controllerImageTagCPU() } - // Set up the container configuration. + crrContext, err := contextsdk.Current() + if err != nil { + return fmt.Errorf("failed to get current Docker context: %w", err) + } + + dockerClient, err := clientsdk.New( + ctx, + clientsdk.WithDockerContext(crrContext), + clientsdk.WithLogger(slog.New(slog.NewTextHandler(os.Stdout, nil))), + ) + if err != nil { + return fmt.Errorf("failed to create Docker client: %w", err) + } + + // TODO: check if the config.json file exists + dockerCfg, err := config.Dir() + if err != nil { + return fmt.Errorf("failed to get Docker config directory: %w", err) + } + portStr := strconv.Itoa(int(port)) - env := []string{ - "MODEL_RUNNER_PORT=" + portStr, - "MODEL_RUNNER_ENVIRONMENT=" + environment, + + env := map[string]string{ + "MODEL_RUNNER_PORT": portStr, + "MODEL_RUNNER_ENVIRONMENT": environment, } if doNotTrack { - env = append(env, "DO_NOT_TRACK=1") + env["DO_NOT_TRACK"] = "1" } - config := &container.Config{ - Image: imageName, - Env: env, - ExposedPorts: nat.PortSet{ - nat.Port(portStr + "/tcp"): struct{}{}, - }, - Labels: map[string]string{ + + customizeOptions := []containersdk.ContainerCustomizer{ + containersdk.WithDockerClient(dockerClient), + containersdk.WithImage(imageName), + containersdk.WithEnv(env), + containersdk.WithName(controllerContainerName), + // using a fixed port for now, although it could be convenient to have it + // be dynamic and use a random port. Then consumers of this container would + // need a way to get a reference to the container, and use the API to get the + // mapped port. + containersdk.WithExposedPorts(portStr + ":" + portStr + "/tcp"), + containersdk.WithLabels(map[string]string{ labelDesktopService: serviceModelRunner, labelRole: roleController, - }, - } - hostConfig := &container.HostConfig{ - Mounts: []mount.Mount{ - { - Type: mount.TypeVolume, - Source: modelStorageVolume, - Target: "/models", - }, - }, - RestartPolicy: container.RestartPolicy{ - Name: "always", - }, + }), + containersdk.WithWaitStrategy(wait.ForAll( + //wait.ForListeningPort(nat.Port(portStr+"/tcp")).WithTimeout(1*time.Minute), // wait for the container to be listening on the port + wait.ForHTTP("/models").WithTimeout(1 * time.Minute).WithStatus(http.StatusOK), // wait for the container to be ready to serve requests + )), + containersdk.WithHostConfigModifier(func(hc *container.HostConfig) { + hc.Mounts = []mount.Mount{ + { + Type: mount.TypeVolume, + Source: modelStorageVolume, + Target: "/models", + }, + } + hc.RestartPolicy = container.RestartPolicy{ + Name: "always", + } + + if gpu == gpupkg.GPUSupportCUDA { + hc.Runtime = "nvidia" + hc.DeviceRequests = []container.DeviceRequest{{Count: -1, Capabilities: [][]string{{"gpu"}}}} + } + }), } - portBindings := []nat.PortBinding{{HostIP: "127.0.0.1", HostPort: portStr}} - if os.Getenv("_MODEL_RUNNER_TREAT_DESKTOP_AS_MOBY") != "1" { - // Don't bind the bridge gateway IP if we're treating Docker Desktop as Moby. - if bridgeGatewayIP, err := determineBridgeGatewayIP(ctx, dockerClient); err == nil && bridgeGatewayIP != "" { - portBindings = append(portBindings, nat.PortBinding{HostIP: bridgeGatewayIP, HostPort: portStr}) + + // Set up the container configuration. + /* + portBindings := []nat.PortBinding{{HostIP: "127.0.0.1", HostPort: portStr}} + if os.Getenv("_MODEL_RUNNER_TREAT_DESKTOP_AS_MOBY") != "1" { + // Don't bind the bridge gateway IP if we're treating Docker Desktop as Moby. + if bridgeGatewayIP, err := determineBridgeGatewayIP(ctx, dockerClient); err == nil && bridgeGatewayIP != "" { + portBindings = append(portBindings, nat.PortBinding{HostIP: bridgeGatewayIP, HostPort: portStr}) + } } + hostConfig.PortBindings = nat.PortMap{ + nat.Port(portStr + "/tcp"): portBindings, + }*/ + + underlyingClient, err := dockerClient.Client() + if err != nil { + return fmt.Errorf("failed to get underlying Docker client: %w", err) } - hostConfig.PortBindings = nat.PortMap{ - nat.Port(portStr + "/tcp"): portBindings, + + // Run the container. If we detect that a concurrent installation is in + // progress, then we wait for whichever install process creates the + // container first and then wait for its container to be ready. + dmrContainer, err := containersdk.Run(ctx, customizeOptions...) + if err != nil { + if match := concurrentInstallMatcher.FindStringSubmatch(err.Error()); match != nil { + if err := waitForContainerToStart(ctx, underlyingClient, match[1]); err != nil { + return fmt.Errorf("failed waiting for concurrent installation: %w", err) + } + return nil + } + return fmt.Errorf("failed to create container %s: %w", controllerContainerName, err) } - if gpu == gpupkg.GPUSupportCUDA { - hostConfig.Runtime = "nvidia" - hostConfig.DeviceRequests = []container.DeviceRequest{{Count: -1, Capabilities: [][]string{{"gpu"}}}} + + printer.Printf("Model runner container %s is running\n", controllerContainerName) + + // Do not copy the config file for Desktop and Cloud engine kinds + if engineKind == types.ModelRunnerEngineKindDesktop || engineKind == types.ModelRunnerEngineKindCloud { + return nil } - // Create the container. If we detect that a concurrent installation is in - // progress (as indicated by a conflicting container name (which should have - // been detected just before installation)), then we'll allow the error to - // pass silently and simply work in conjunction with any concurrent - // installers to start the container. - resp, err := dockerClient.ContainerCreate(ctx, config, hostConfig, nil, nil, controllerContainerName) - if err != nil && !errdefs.IsConflict(err) { - return fmt.Errorf("failed to create container %s: %w", controllerContainerName, err) + _, _, err = dmrContainer.Exec(ctx, []string{"/bin/sh", "-c", "mkdir -p /home/modelrunner/.docker"}) + if err != nil { + return fmt.Errorf("mkdir config directory in container: %w", err) } - created := err == nil - // Start the container. - printer.Printf("Starting model runner container %s...\n", controllerContainerName) - if err := ensureContainerStarted(ctx, dockerClient, controllerContainerName); err != nil { - if created { - _ = dockerClient.ContainerRemove(ctx, resp.ID, container.RemoveOptions{Force: true}) - } - return fmt.Errorf("failed to start container %s: %w", controllerContainerName, err) + cfgFile, err := os.ReadFile(filepath.Join(dockerCfg, config.FileName)) + if err != nil { + return fmt.Errorf("read config file: %w", err) } - // Copy Docker config file if it exists and we're the container creator. - if created { - if err := copyDockerConfigToContainer(ctx, dockerClient, resp.ID, engineKind); err != nil { - // Log warning but continue - don't fail container creation - printer.Printf("Warning: failed to copy Docker config: %v\n", err) - } + err = dmrContainer.CopyToContainer(ctx, cfgFile, "/home/modelrunner/.docker/"+config.FileName, 0o600) + if err != nil { + return fmt.Errorf("copy directory to container: %w", err) } + return nil } diff --git a/vendor/github.com/caarlos0/env/v11/.editorconfig b/vendor/github.com/caarlos0/env/v11/.editorconfig new file mode 100644 index 00000000..0d3bd399 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/.editorconfig @@ -0,0 +1,28 @@ +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +max_line_length = 120 + +[{go.mod,go.sum,*.go}] +insert_final_newline = true +indent_size = tab +indent_style = tab +tab_width = 4 + +[Makefile] +max_line_length = off +insert_final_newline = true +indent_size = tab +indent_style = tab +tab_width = 4 + +[*.md] +max_line_length = off +trim_trailing_whitespace = false +indent_size = tab +indent_style = space +tab_width = 2 + +[.mailmap] +max_line_length = off diff --git a/vendor/github.com/caarlos0/env/v11/.gitignore b/vendor/github.com/caarlos0/env/v11/.gitignore new file mode 100644 index 00000000..9eea1c98 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/.gitignore @@ -0,0 +1,5 @@ +coverage.txt +bin +card.png +dist +codecov* diff --git a/vendor/github.com/caarlos0/env/v11/.golangci.yml b/vendor/github.com/caarlos0/env/v11/.golangci.yml new file mode 100644 index 00000000..8e51bdcc --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/.golangci.yml @@ -0,0 +1,32 @@ +linters-settings: + gocritic: + enabled-checks: + - emptyStringTest + - evalOrder + - paramTypeCombine + - preferStringWriter + - sprintfQuotedString + - stringConcatSimplify + - yodaStyleExpr + revive: + rules: + - name: line-length-limit + arguments: [120] + +issues: + exclude-rules: + - path: _test\.go + linters: + - revive + text: "line-length-limit:" + +linters: + enable: + - thelper + - gofumpt + - gocritic + - tparallel + - unconvert + - unparam + - wastedassign + - revive diff --git a/vendor/github.com/caarlos0/env/v11/.goreleaser.yml b/vendor/github.com/caarlos0/env/v11/.goreleaser.yml new file mode 100644 index 00000000..a5b7d4d2 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/.goreleaser.yml @@ -0,0 +1,5 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json +version: 2 +includes: + - from_url: + url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/lib.yml diff --git a/vendor/github.com/caarlos0/env/v11/.mailmap b/vendor/github.com/caarlos0/env/v11/.mailmap new file mode 100644 index 00000000..eeeee601 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/.mailmap @@ -0,0 +1,7 @@ +Carlos Alexandro Becker Carlos A Becker +Carlos Alexandro Becker Carlos A Becker +Carlos Alexandro Becker Carlos Alexandro Becker +Carlos Alexandro Becker Carlos Alexandro Becker +Carlos Alexandro Becker Carlos Becker +dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> +actions-user github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> diff --git a/vendor/github.com/caarlos0/env/v11/LICENSE.md b/vendor/github.com/caarlos0/env/v11/LICENSE.md new file mode 100644 index 00000000..28463401 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2024 Carlos Alexandro Becker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/caarlos0/env/v11/Makefile b/vendor/github.com/caarlos0/env/v11/Makefile new file mode 100644 index 00000000..da8595fb --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/Makefile @@ -0,0 +1,37 @@ +SOURCE_FILES?=./... +TEST_PATTERN?=. + +export GO111MODULE := on + +setup: + go mod tidy +.PHONY: setup + +build: + go build +.PHONY: build + +test: + go test -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m +.PHONY: test + +cover: test + go tool cover -html=coverage.txt +.PHONY: cover + +fmt: + gofumpt -w -l . +.PHONY: fmt + +lint: + golangci-lint run ./... +.PHONY: lint + +ci: build test +.PHONY: ci + +card: + wget -O card.png -c "https://og.caarlos0.dev/**env**: parse envs to structs.png?theme=light&md=1&fontSize=100px&images=https://github.com/caarlos0.png" +.PHONY: card + +.DEFAULT_GOAL := ci diff --git a/vendor/github.com/caarlos0/env/v11/README.md b/vendor/github.com/caarlos0/env/v11/README.md new file mode 100644 index 00000000..de6f8249 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/README.md @@ -0,0 +1,156 @@ +

+ GoReleaser Logo +

A simple, zero-dependencies library to parse environment variables into structs.

+

+ +###### Installation + +```bash +go get github.com/caarlos0/env/v11 +``` + +###### Getting started + +```go +type config struct { + Home string `env:"HOME"` +} + +// parse +var cfg config +err := env.Parse(&cfg) + +// parse with generics +cfg, err := env.ParseAs[config]() +``` + +You can see the full documentation and list of examples at [pkg.go.dev](https://pkg.go.dev/github.com/caarlos0/env/v11). + +--- + +## Used and supported by + +

+ + encore icon + +
+
+ Encore – the platform for building Go-based cloud backends. +
+

+ +## Usage + +### Caveats + +> [!CAUTION] +> +> _Unexported fields_ will be **ignored** by `env`. +> This is by design and will not change. + +### Functions + +- `Parse`: parse the current environment into a type +- `ParseAs`: parse the current environment into a type using generics +- `ParseWithOptions`: parse the current environment into a type with custom options +- `ParseAsWithOptions`: parse the current environment into a type with custom options and using generics +- `Must`: can be used to wrap `Parse.*` calls to panic on error +- `GetFieldParams`: get the `env` parsed options for a type +- `GetFieldParamsWithOptions`: get the `env` parsed options for a type with custom options + +### Supported types + +Out of the box all built-in types are supported, plus a few others that are commonly used. + +Complete list: + +- `bool` +- `float32` +- `float64` +- `int16` +- `int32` +- `int64` +- `int8` +- `int` +- `string` +- `uint16` +- `uint32` +- `uint64` +- `uint8` +- `uint` +- `time.Duration` +- `time.Location` +- `encoding.TextUnmarshaler` +- `url.URL` + +Pointers, slices and slices of pointers, and maps of those types are also supported. + +You may also add custom parsers for your types. + +### Tags + +The following tags are provided: + +- `env`: sets the environment variable name and optionally takes the tag options described below +- `envDefault`: sets the default value for the field +- `envPrefix`: can be used in a field that is a complex type to set a prefix to all environment variables used in it +- `envSeparator`: sets the character to be used to separate items in slices and maps (default: `,`) +- `envKeyValSeparator`: sets the character to be used to separate keys and their values in maps (default: `:`) + +### `env` tag options + +Here are all the options available for the `env` tag: + +- `,expand`: expands environment variables, e.g. `FOO_${BAR}` +- `,file`: instructs that the content of the variable is a path to a file that should be read +- `,init`: initialize nil pointers +- `,notEmpty`: make the field errors if the environment variable is empty +- `,required`: make the field errors if the environment variable is not set +- `,unset`: unset the environment variable after use + +### Parse Options + +There are a few options available in the functions that end with `WithOptions`: + +- `Environment`: keys and values to be used instead of `os.Environ()` +- `TagName`: specifies another tag name to use rather than the default `env` +- `PrefixTagName`: specifies another prefix tag name to use rather than the default `envPrefix` +- `DefaultValueTagName`: specifies another default tag name to use rather than the default `envDefault` +- `RequiredIfNoDef`: set all `env` fields as required if they do not declare `envDefault` +- `OnSet`: allows to hook into the `env` parsing and do something when a value is set +- `Prefix`: prefix to be used in all environment variables +- `UseFieldNameByDefault`: defines whether or not `env` should use the field name by default if the `env` key is missing +- `FuncMap`: custom parse functions for custom types + +### Documentation and examples + +Examples are live in [pkg.go.dev](https://pkg.go.dev/github.com/caarlos0/env/v11), +and also in the [example test file](./example_test.go). + +## Current state + +`env` is considered feature-complete. + +I do not intent to add any new features unless they really make sense, and are +requested by many people. + +Eventual bug fixes will keep being merged. + +## Badges + +[![Release](https://img.shields.io/github/release/caarlos0/env.svg?style=for-the-badge)](https://github.com/goreleaser/goreleaser/releases/latest) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge)](/LICENSE.md) +[![Build status](https://img.shields.io/github/actions/workflow/status/caarlos0/env/build.yml?style=for-the-badge&branch=main)](https://github.com/caarlos0/env/actions?workflow=build) +[![Codecov branch](https://img.shields.io/codecov/c/github/caarlos0/env/main.svg?style=for-the-badge)](https://codecov.io/gh/caarlos0/env) +[![Go docs](https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge)](http://godoc.org/github.com/caarlos0/env/v11) +[![Powered By: GoReleaser](https://img.shields.io/badge/powered%20by-goreleaser-green.svg?style=for-the-badge)](https://github.com/goreleaser) +[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=for-the-badge)](https://conventionalcommits.org) + +## Related projects + +- [envdoc](https://github.com/g4s8/envdoc) - generate documentation for environment variables from `env` tags + +## Stargazers over time + +[![Stargazers over time](https://starchart.cc/caarlos0/env.svg)](https://starchart.cc/caarlos0/env) diff --git a/vendor/github.com/caarlos0/env/v11/env.go b/vendor/github.com/caarlos0/env/v11/env.go new file mode 100644 index 00000000..805ee2de --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/env.go @@ -0,0 +1,845 @@ +// Package env is a simple, zero-dependencies library to parse environment +// variables into structs. +// +// Example: +// +// type config struct { +// Home string `env:"HOME"` +// } +// // parse +// var cfg config +// err := env.Parse(&cfg) +// // or parse with generics +// cfg, err := env.ParseAs[config]() +// +// Check the examples and README for more detailed usage. +package env + +import ( + "encoding" + "fmt" + "net/url" + "os" + "reflect" + "strconv" + "strings" + "time" + "unicode" +) + +// nolint: gochecknoglobals +var ( + defaultBuiltInParsers = map[reflect.Kind]ParserFunc{ + reflect.Bool: func(v string) (interface{}, error) { + return strconv.ParseBool(v) + }, + reflect.String: func(v string) (interface{}, error) { + return v, nil + }, + reflect.Int: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 32) + return int(i), err + }, + reflect.Int16: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 16) + return int16(i), err + }, + reflect.Int32: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 32) + return int32(i), err + }, + reflect.Int64: func(v string) (interface{}, error) { + return strconv.ParseInt(v, 10, 64) + }, + reflect.Int8: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 8) + return int8(i), err + }, + reflect.Uint: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 32) + return uint(i), err + }, + reflect.Uint16: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 16) + return uint16(i), err + }, + reflect.Uint32: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 32) + return uint32(i), err + }, + reflect.Uint64: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 64) + return i, err + }, + reflect.Uint8: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 8) + return uint8(i), err + }, + reflect.Float64: func(v string) (interface{}, error) { + return strconv.ParseFloat(v, 64) + }, + reflect.Float32: func(v string) (interface{}, error) { + f, err := strconv.ParseFloat(v, 32) + return float32(f), err + }, + } +) + +func defaultTypeParsers() map[reflect.Type]ParserFunc { + return map[reflect.Type]ParserFunc{ + reflect.TypeOf(url.URL{}): parseURL, + reflect.TypeOf(time.Nanosecond): parseDuration, + reflect.TypeOf(time.Location{}): parseLocation, + } +} + +func parseURL(v string) (interface{}, error) { + u, err := url.Parse(v) + if err != nil { + return nil, newParseValueError("unable to parse URL", err) + } + return *u, nil +} + +func parseDuration(v string) (interface{}, error) { + d, err := time.ParseDuration(v) + if err != nil { + return nil, newParseValueError("unable to parse duration", err) + } + return d, err +} + +func parseLocation(v string) (interface{}, error) { + loc, err := time.LoadLocation(v) + if err != nil { + return nil, newParseValueError("unable to parse location", err) + } + return *loc, nil +} + +// ParserFunc defines the signature of a function that can be used within +// `Options`' `FuncMap`. +type ParserFunc func(v string) (interface{}, error) + +// OnSetFn is a hook that can be run when a value is set. +type OnSetFn func(tag string, value interface{}, isDefault bool) + +// processFieldFn is a function which takes all information about a field and processes it. +type processFieldFn func( + refField reflect.Value, + refTypeField reflect.StructField, + opts Options, + fieldParams FieldParams, +) error + +// Options for the parser. +type Options struct { + // Environment keys and values that will be accessible for the service. + Environment map[string]string + + // TagName specifies another tag name to use rather than the default 'env'. + TagName string + + // PrefixTagName specifies another prefix tag name to use rather than the default 'envPrefix'. + PrefixTagName string + + // DefaultValueTagName specifies another default tag name to use rather than the default 'envDefault'. + DefaultValueTagName string + + // RequiredIfNoDef automatically sets all fields as required if they do not + // declare 'envDefault'. + RequiredIfNoDef bool + + // OnSet allows to run a function when a value is set. + OnSet OnSetFn + + // Prefix define a prefix for every key. + Prefix string + + // UseFieldNameByDefault defines whether or not `env` should use the field + // name by default if the `env` key is missing. + // Note that the field name will be "converted" to conform with environment + // variable names conventions. + UseFieldNameByDefault bool + + // Custom parse functions for different types. + FuncMap map[reflect.Type]ParserFunc + + // Used internally. maps the env variable key to its resolved string value. + // (for env var expansion) + rawEnvVars map[string]string +} + +func (opts *Options) getRawEnv(s string) string { + val := opts.rawEnvVars[s] + if val == "" { + val = opts.Environment[s] + } + return os.Expand(val, opts.getRawEnv) +} + +func defaultOptions() Options { + return Options{ + TagName: "env", + PrefixTagName: "envPrefix", + DefaultValueTagName: "envDefault", + Environment: toMap(os.Environ()), + FuncMap: defaultTypeParsers(), + rawEnvVars: make(map[string]string), + } +} + +func mergeOptions[T any](target, source *T) { + targetPtr := reflect.ValueOf(target).Elem() + sourcePtr := reflect.ValueOf(source).Elem() + + targetType := targetPtr.Type() + for i := 0; i < targetPtr.NumField(); i++ { + fieldName := targetType.Field(i).Name + targetField := targetPtr.Field(i) + sourceField := sourcePtr.FieldByName(fieldName) + + if targetField.CanSet() && !isZero(sourceField) { + // FuncMaps are being merged, while Environments must be overwritten + if fieldName == "FuncMap" { + if !sourceField.IsZero() { + iter := sourceField.MapRange() + for iter.Next() { + targetField.SetMapIndex(iter.Key(), iter.Value()) + } + } + } else { + targetField.Set(sourceField) + } + } + } +} + +func isZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Func, reflect.Map, reflect.Slice: + return v.IsNil() + default: + zero := reflect.Zero(v.Type()) + return v.Interface() == zero.Interface() + } +} + +func customOptions(opts Options) Options { + defOpts := defaultOptions() + mergeOptions(&defOpts, &opts) + return defOpts +} + +func optionsWithSliceEnvPrefix(opts Options, index int) Options { + return Options{ + Environment: opts.Environment, + TagName: opts.TagName, + PrefixTagName: opts.PrefixTagName, + DefaultValueTagName: opts.DefaultValueTagName, + RequiredIfNoDef: opts.RequiredIfNoDef, + OnSet: opts.OnSet, + Prefix: fmt.Sprintf("%s%d_", opts.Prefix, index), + UseFieldNameByDefault: opts.UseFieldNameByDefault, + FuncMap: opts.FuncMap, + rawEnvVars: opts.rawEnvVars, + } +} + +func optionsWithEnvPrefix(field reflect.StructField, opts Options) Options { + return Options{ + Environment: opts.Environment, + TagName: opts.TagName, + PrefixTagName: opts.PrefixTagName, + DefaultValueTagName: opts.DefaultValueTagName, + RequiredIfNoDef: opts.RequiredIfNoDef, + OnSet: opts.OnSet, + Prefix: opts.Prefix + field.Tag.Get(opts.PrefixTagName), + UseFieldNameByDefault: opts.UseFieldNameByDefault, + FuncMap: opts.FuncMap, + rawEnvVars: opts.rawEnvVars, + } +} + +// Parse parses a struct containing `env` tags and loads its values from +// environment variables. +func Parse(v interface{}) error { + return parseInternal(v, setField, defaultOptions()) +} + +// ParseWithOptions parses a struct containing `env` tags and loads its values from +// environment variables. +func ParseWithOptions(v interface{}, opts Options) error { + return parseInternal(v, setField, customOptions(opts)) +} + +// ParseAs parses the given struct type containing `env` tags and loads its +// values from environment variables. +func ParseAs[T any]() (T, error) { + var t T + err := Parse(&t) + return t, err +} + +// ParseWithOptions parses the given struct type containing `env` tags and +// loads its values from environment variables. +func ParseAsWithOptions[T any](opts Options) (T, error) { + var t T + err := ParseWithOptions(&t, opts) + return t, err +} + +// Must panic is if err is not nil, and returns t otherwise. +func Must[T any](t T, err error) T { + if err != nil { + panic(err) + } + return t +} + +// GetFieldParams parses a struct containing `env` tags and returns information about +// tags it found. +func GetFieldParams(v interface{}) ([]FieldParams, error) { + return GetFieldParamsWithOptions(v, defaultOptions()) +} + +// GetFieldParamsWithOptions parses a struct containing `env` tags and returns information about +// tags it found. +func GetFieldParamsWithOptions(v interface{}, opts Options) ([]FieldParams, error) { + var result []FieldParams + err := parseInternal( + v, + func(_ reflect.Value, _ reflect.StructField, _ Options, fieldParams FieldParams) error { + if fieldParams.OwnKey != "" { + result = append(result, fieldParams) + } + return nil + }, + customOptions(opts), + ) + if err != nil { + return nil, err + } + + return result, nil +} + +func parseInternal(v interface{}, processField processFieldFn, opts Options) error { + ptrRef := reflect.ValueOf(v) + if ptrRef.Kind() != reflect.Ptr { + return newAggregateError(NotStructPtrError{}) + } + ref := ptrRef.Elem() + if ref.Kind() != reflect.Struct { + return newAggregateError(NotStructPtrError{}) + } + + return doParse(ref, processField, opts) +} + +func doParse(ref reflect.Value, processField processFieldFn, opts Options) error { + refType := ref.Type() + + var agrErr AggregateError + + for i := 0; i < refType.NumField(); i++ { + refField := ref.Field(i) + refTypeField := refType.Field(i) + + if err := doParseField(refField, refTypeField, processField, opts); err != nil { + if val, ok := err.(AggregateError); ok { + agrErr.Errors = append(agrErr.Errors, val.Errors...) + } else { + agrErr.Errors = append(agrErr.Errors, err) + } + } + } + + if len(agrErr.Errors) == 0 { + return nil + } + + return agrErr +} + +func doParseField( + refField reflect.Value, + refTypeField reflect.StructField, + processField processFieldFn, + opts Options, +) error { + if !refField.CanSet() { + return nil + } + if refField.Kind() == reflect.Ptr && refField.Elem().Kind() == reflect.Struct && !refField.IsNil() { + return parseInternal(refField.Interface(), processField, optionsWithEnvPrefix(refTypeField, opts)) + } + if refField.Kind() == reflect.Struct && refField.CanAddr() && refField.Type().Name() == "" { + return parseInternal(refField.Addr().Interface(), processField, optionsWithEnvPrefix(refTypeField, opts)) + } + + params, err := parseFieldParams(refTypeField, opts) + if err != nil { + return err + } + + if params.Ignored { + return nil + } + + if err := processField(refField, refTypeField, opts, params); err != nil { + return err + } + + if params.Init && isInvalidPtr(refField) { + refField.Set(reflect.New(refField.Type().Elem())) + refField = refField.Elem() + } + + if refField.Kind() == reflect.Struct { + return doParse(refField, processField, optionsWithEnvPrefix(refTypeField, opts)) + } + + if isSliceOfStructs(refTypeField) { + return doParseSlice(refField, processField, optionsWithEnvPrefix(refTypeField, opts)) + } + + return nil +} + +func isSliceOfStructs(refTypeField reflect.StructField) bool { + field := refTypeField.Type + + // *[]struct + if field.Kind() == reflect.Ptr { + field = field.Elem() + if field.Kind() == reflect.Slice && field.Elem().Kind() == reflect.Struct { + return true + } + } + + // []struct{} + if field.Kind() == reflect.Slice && field.Elem().Kind() == reflect.Struct { + return true + } + + return false +} + +func doParseSlice(ref reflect.Value, processField processFieldFn, opts Options) error { + if opts.Prefix != "" && !strings.HasSuffix(opts.Prefix, string(underscore)) { + opts.Prefix += string(underscore) + } + + var environments []string + for environment := range opts.Environment { + if strings.HasPrefix(environment, opts.Prefix) { + environments = append(environments, environment) + } + } + + if len(environments) > 0 { + counter := 0 + for finished := false; !finished; { + finished = true + prefix := fmt.Sprintf("%s%d%c", opts.Prefix, counter, underscore) + for _, variable := range environments { + if strings.HasPrefix(variable, prefix) { + counter++ + finished = false + break + } + } + } + + sliceType := ref.Type() + var initialized int + if reflect.Ptr == ref.Kind() { + sliceType = sliceType.Elem() + // Due to the rest of code the pre-initialized slice has no chance for this situation + initialized = 0 + } else { + initialized = ref.Len() + } + + var capacity int + if capacity = initialized; counter > initialized { + capacity = counter + } + result := reflect.MakeSlice(sliceType, capacity, capacity) + for i := 0; i < capacity; i++ { + item := result.Index(i) + if i < initialized { + item.Set(ref.Index(i)) + } + if err := doParse(item, processField, optionsWithSliceEnvPrefix(opts, i)); err != nil { + return err + } + } + + if result.Len() > 0 { + if reflect.Ptr == ref.Kind() { + resultPtr := reflect.New(sliceType) + resultPtr.Elem().Set(result) + result = resultPtr + } + ref.Set(result) + } + } + + return nil +} + +func setField(refField reflect.Value, refTypeField reflect.StructField, opts Options, fieldParams FieldParams) error { + value, err := get(fieldParams, opts) + if err != nil { + return err + } + + if value != "" { + return set(refField, refTypeField, value, opts.FuncMap) + } + + return nil +} + +const underscore rune = '_' + +func toEnvName(input string) string { + var output []rune + for i, c := range input { + if c == underscore { + continue + } + if len(output) > 0 && unicode.IsUpper(c) { + if len(input) > i+1 { + peek := rune(input[i+1]) + if unicode.IsLower(peek) || unicode.IsLower(rune(input[i-1])) { + output = append(output, underscore) + } + } + } + output = append(output, unicode.ToUpper(c)) + } + return string(output) +} + +// FieldParams contains information about parsed field tags. +type FieldParams struct { + OwnKey string + Key string + DefaultValue string + HasDefaultValue bool + Required bool + LoadFile bool + Unset bool + NotEmpty bool + Expand bool + Init bool + Ignored bool +} + +func parseFieldParams(field reflect.StructField, opts Options) (FieldParams, error) { + ownKey, tags := parseKeyForOption(field.Tag.Get(opts.TagName)) + if ownKey == "" && opts.UseFieldNameByDefault { + ownKey = toEnvName(field.Name) + } + + defaultValue, hasDefaultValue := field.Tag.Lookup(opts.DefaultValueTagName) + + result := FieldParams{ + OwnKey: ownKey, + Key: opts.Prefix + ownKey, + Required: opts.RequiredIfNoDef, + DefaultValue: defaultValue, + HasDefaultValue: hasDefaultValue, + Ignored: ownKey == "-", + } + + for _, tag := range tags { + switch tag { + case "": + continue + case "file": + result.LoadFile = true + case "required": + result.Required = true + case "unset": + result.Unset = true + case "notEmpty": + result.NotEmpty = true + case "expand": + result.Expand = true + case "init": + result.Init = true + case "-": + result.Ignored = true + default: + return FieldParams{}, newNoSupportedTagOptionError(tag) + } + } + + return result, nil +} + +func get(fieldParams FieldParams, opts Options) (val string, err error) { + var exists, isDefault bool + + val, exists, isDefault = getOr( + fieldParams.Key, + fieldParams.DefaultValue, + fieldParams.HasDefaultValue, + opts.Environment, + ) + + if fieldParams.Expand { + val = os.Expand(val, opts.getRawEnv) + } + + opts.rawEnvVars[fieldParams.OwnKey] = val + + if fieldParams.Unset { + defer os.Unsetenv(fieldParams.Key) + } + + if fieldParams.Required && !exists && fieldParams.OwnKey != "" { + return "", newVarIsNotSetError(fieldParams.Key) + } + + if fieldParams.NotEmpty && val == "" { + return "", newEmptyVarError(fieldParams.Key) + } + + if fieldParams.LoadFile && val != "" { + filename := val + val, err = getFromFile(filename) + if err != nil { + return "", newLoadFileContentError(filename, fieldParams.Key, err) + } + } + + if opts.OnSet != nil { + if fieldParams.OwnKey != "" { + opts.OnSet(fieldParams.Key, val, isDefault) + } + } + return val, err +} + +// split the env tag's key into the expected key and desired option, if any. +func parseKeyForOption(key string) (string, []string) { + opts := strings.Split(key, ",") + return opts[0], opts[1:] +} + +func getFromFile(filename string) (value string, err error) { + b, err := os.ReadFile(filename) + return string(b), err +} + +func getOr(key, defaultValue string, defExists bool, envs map[string]string) (val string, exists, isDefault bool) { + value, exists := envs[key] + switch { + case (!exists || key == "") && defExists: + return defaultValue, true, true + case exists && value == "" && defExists: + return defaultValue, true, true + case !exists: + return "", false, false + } + + return value, true, false +} + +func set(field reflect.Value, sf reflect.StructField, value string, funcMap map[reflect.Type]ParserFunc) error { + if tm := asTextUnmarshaler(field); tm != nil { + if err := tm.UnmarshalText([]byte(value)); err != nil { + return newParseError(sf, err) + } + return nil + } + + typee := sf.Type + fieldee := field + if typee.Kind() == reflect.Ptr { + typee = typee.Elem() + fieldee = field.Elem() + } + + parserFunc, ok := funcMap[typee] + if ok { + val, err := parserFunc(value) + if err != nil { + return newParseError(sf, err) + } + + fieldee.Set(reflect.ValueOf(val)) + return nil + } + + parserFunc, ok = defaultBuiltInParsers[typee.Kind()] + if ok { + val, err := parserFunc(value) + if err != nil { + return newParseError(sf, err) + } + + fieldee.Set(reflect.ValueOf(val).Convert(typee)) + return nil + } + + switch field.Kind() { + case reflect.Slice: + return handleSlice(field, value, sf, funcMap) + case reflect.Map: + return handleMap(field, value, sf, funcMap) + } + + return newNoParserError(sf) +} + +func handleSlice(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error { + separator := sf.Tag.Get("envSeparator") + if separator == "" { + separator = "," + } + parts := strings.Split(value, separator) + + typee := sf.Type.Elem() + if typee.Kind() == reflect.Ptr { + typee = typee.Elem() + } + + if _, ok := reflect.New(typee).Interface().(encoding.TextUnmarshaler); ok { + return parseTextUnmarshalers(field, parts, sf) + } + + parserFunc, ok := funcMap[typee] + if !ok { + parserFunc, ok = defaultBuiltInParsers[typee.Kind()] + if !ok { + return newNoParserError(sf) + } + } + + result := reflect.MakeSlice(sf.Type, 0, len(parts)) + for _, part := range parts { + r, err := parserFunc(part) + if err != nil { + return newParseError(sf, err) + } + v := reflect.ValueOf(r).Convert(typee) + if sf.Type.Elem().Kind() == reflect.Ptr { + v = reflect.New(typee) + v.Elem().Set(reflect.ValueOf(r).Convert(typee)) + } + result = reflect.Append(result, v) + } + field.Set(result) + return nil +} + +func handleMap(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error { + keyType := sf.Type.Key() + keyParserFunc, ok := funcMap[keyType] + if !ok { + keyParserFunc, ok = defaultBuiltInParsers[keyType.Kind()] + if !ok { + return newNoParserError(sf) + } + } + + elemType := sf.Type.Elem() + elemParserFunc, ok := funcMap[elemType] + if !ok { + elemParserFunc, ok = defaultBuiltInParsers[elemType.Kind()] + if !ok { + return newNoParserError(sf) + } + } + + separator := sf.Tag.Get("envSeparator") + if separator == "" { + separator = "," + } + + keyValSeparator := sf.Tag.Get("envKeyValSeparator") + if keyValSeparator == "" { + keyValSeparator = ":" + } + + result := reflect.MakeMap(sf.Type) + for _, part := range strings.Split(value, separator) { + pairs := strings.SplitN(part, keyValSeparator, 2) + if len(pairs) != 2 { + return newParseError(sf, fmt.Errorf(`%q should be in "key%svalue" format`, part, keyValSeparator)) + } + + key, err := keyParserFunc(pairs[0]) + if err != nil { + return newParseError(sf, err) + } + + elem, err := elemParserFunc(pairs[1]) + if err != nil { + return newParseError(sf, err) + } + + result.SetMapIndex(reflect.ValueOf(key).Convert(keyType), reflect.ValueOf(elem).Convert(elemType)) + } + + field.Set(result) + return nil +} + +func asTextUnmarshaler(field reflect.Value) encoding.TextUnmarshaler { + if field.Kind() == reflect.Ptr { + if field.IsNil() { + field.Set(reflect.New(field.Type().Elem())) + } + } else if field.CanAddr() { + field = field.Addr() + } + + tm, ok := field.Interface().(encoding.TextUnmarshaler) + if !ok { + return nil + } + return tm +} + +func parseTextUnmarshalers(field reflect.Value, data []string, sf reflect.StructField) error { + s := len(data) + elemType := field.Type().Elem() + slice := reflect.MakeSlice(reflect.SliceOf(elemType), s, s) + for i, v := range data { + sv := slice.Index(i) + kind := sv.Kind() + if kind == reflect.Ptr { + sv = reflect.New(elemType.Elem()) + } else { + sv = sv.Addr() + } + tm := sv.Interface().(encoding.TextUnmarshaler) + if err := tm.UnmarshalText([]byte(v)); err != nil { + return newParseError(sf, err) + } + if kind == reflect.Ptr { + slice.Index(i).Set(sv) + } + } + + field.Set(slice) + + return nil +} + +// ToMap Converts list of env vars as provided by os.Environ() to map you +// can use as Options.Environment field +func ToMap(env []string) map[string]string { + return toMap(env) +} + +func isInvalidPtr(v reflect.Value) bool { + return reflect.Ptr == v.Kind() && v.Elem().Kind() == reflect.Invalid +} diff --git a/vendor/github.com/caarlos0/env/v11/env_tomap.go b/vendor/github.com/caarlos0/env/v11/env_tomap.go new file mode 100644 index 00000000..aece2ae9 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/env_tomap.go @@ -0,0 +1,16 @@ +//go:build !windows + +package env + +import "strings" + +func toMap(env []string) map[string]string { + r := map[string]string{} + for _, e := range env { + p := strings.SplitN(e, "=", 2) + if len(p) == 2 { + r[p[0]] = p[1] + } + } + return r +} diff --git a/vendor/github.com/caarlos0/env/v11/env_tomap_windows.go b/vendor/github.com/caarlos0/env/v11/env_tomap_windows.go new file mode 100644 index 00000000..04ce66f5 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/env_tomap_windows.go @@ -0,0 +1,29 @@ +//go:build windows + +package env + +import "strings" + +func toMap(env []string) map[string]string { + r := map[string]string{} + for _, e := range env { + p := strings.SplitN(e, "=", 2) + + // On Windows, environment variables can start with '='. If so, Split at next character. + // See env_windows.go in the Go source: https://github.com/golang/go/blob/master/src/syscall/env_windows.go#L58 + prefixEqualSign := false + if len(e) > 0 && e[0] == '=' { + e = e[1:] + prefixEqualSign = true + } + p = strings.SplitN(e, "=", 2) + if prefixEqualSign { + p[0] = "=" + p[0] + } + + if len(p) == 2 { + r[p[0]] = p[1] + } + } + return r +} diff --git a/vendor/github.com/caarlos0/env/v11/error.go b/vendor/github.com/caarlos0/env/v11/error.go new file mode 100644 index 00000000..d615912c --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/error.go @@ -0,0 +1,173 @@ +package env + +import ( + "fmt" + "reflect" + "strings" +) + +// An aggregated error wrapper to combine gathered errors. +// This allows either to display all errors or convert them individually +// List of the available errors +// ParseError +// NotStructPtrError +// NoParserError +// NoSupportedTagOptionError +// VarIsNotSetError +// EmptyVarError +// LoadFileContentError +// ParseValueError +type AggregateError struct { + Errors []error +} + +func newAggregateError(initErr error) error { + return AggregateError{ + []error{ + initErr, + }, + } +} + +func (e AggregateError) Error() string { + var sb strings.Builder + + sb.WriteString("env:") + + for _, err := range e.Errors { + sb.WriteString(fmt.Sprintf(" %v;", err.Error())) + } + + return strings.TrimRight(sb.String(), ";") +} + +// Unwrap implements std errors.Join go1.20 compatibility +func (e AggregateError) Unwrap() []error { + return e.Errors +} + +// Is conforms with errors.Is. +func (e AggregateError) Is(err error) bool { + for _, ie := range e.Errors { + if reflect.TypeOf(ie) == reflect.TypeOf(err) { + return true + } + } + return false +} + +// The error occurs when it's impossible to convert the value for given type. +type ParseError struct { + Name string + Type reflect.Type + Err error +} + +func newParseError(sf reflect.StructField, err error) error { + return ParseError{sf.Name, sf.Type, err} +} + +func (e ParseError) Error() string { + return fmt.Sprintf("parse error on field %q of type %q: %v", e.Name, e.Type, e.Err) +} + +// The error occurs when pass something that is not a pointer to a struct to Parse +type NotStructPtrError struct{} + +func (e NotStructPtrError) Error() string { + return "expected a pointer to a Struct" +} + +// This error occurs when there is no parser provided for given type. +type NoParserError struct { + Name string + Type reflect.Type +} + +func newNoParserError(sf reflect.StructField) error { + return NoParserError{sf.Name, sf.Type} +} + +func (e NoParserError) Error() string { + return fmt.Sprintf("no parser found for field %q of type %q", e.Name, e.Type) +} + +// This error occurs when the given tag is not supported. +// Built-in supported tags: "", "file", "required", "unset", "notEmpty", +// "expand", "envDefault", and "envSeparator". +type NoSupportedTagOptionError struct { + Tag string +} + +func newNoSupportedTagOptionError(tag string) error { + return NoSupportedTagOptionError{tag} +} + +func (e NoSupportedTagOptionError) Error() string { + return fmt.Sprintf("tag option %q not supported", e.Tag) +} + +// This error occurs when the required variable is not set. +// +// Deprecated: use VarIsNotSetError. +type EnvVarIsNotSetError = VarIsNotSetError + +// This error occurs when the required variable is not set. +type VarIsNotSetError struct { + Key string +} + +func newVarIsNotSetError(key string) error { + return VarIsNotSetError{key} +} + +func (e VarIsNotSetError) Error() string { + return fmt.Sprintf(`required environment variable %q is not set`, e.Key) +} + +// This error occurs when the variable which must be not empty is existing but has an empty value +// +// Deprecated: use EmptyVarError. +type EmptyEnvVarError = EmptyVarError + +// This error occurs when the variable which must be not empty is existing but has an empty value +type EmptyVarError struct { + Key string +} + +func newEmptyVarError(key string) error { + return EmptyVarError{key} +} + +func (e EmptyVarError) Error() string { + return fmt.Sprintf("environment variable %q should not be empty", e.Key) +} + +// This error occurs when it's impossible to load the value from the file. +type LoadFileContentError struct { + Filename string + Key string + Err error +} + +func newLoadFileContentError(filename, key string, err error) error { + return LoadFileContentError{filename, key, err} +} + +func (e LoadFileContentError) Error() string { + return fmt.Sprintf("could not load content of file %q from variable %s: %v", e.Filename, e.Key, e.Err) +} + +// This error occurs when it's impossible to convert value using given parser. +type ParseValueError struct { + Msg string + Err error +} + +func newParseValueError(message string, err error) error { + return ParseValueError{message, err} +} + +func (e ParseValueError) Error() string { + return fmt.Sprintf("%s: %v", e.Msg, e.Err) +} diff --git a/vendor/github.com/docker/go-sdk/client/LICENSE b/vendor/github.com/docker/go-sdk/client/LICENSE new file mode 100644 index 00000000..a0e6f9bc --- /dev/null +++ b/vendor/github.com/docker/go-sdk/client/LICENSE @@ -0,0 +1,217 @@ +Copyright (c) 2025–present Docker, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +----------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/docker/go-sdk/client/Makefile b/vendor/github.com/docker/go-sdk/client/Makefile new file mode 100644 index 00000000..748cb213 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/client/Makefile @@ -0,0 +1 @@ +include ../commons-test.mk diff --git a/vendor/github.com/docker/go-sdk/client/README.md b/vendor/github.com/docker/go-sdk/client/README.md new file mode 100644 index 00000000..2b48aae1 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/client/README.md @@ -0,0 +1,39 @@ +# Docker Client + +This package provides a client for the Docker API. + +## Installation + +```bash +go get github.com/docker/go-sdk/client +``` + +## Usage + +The library provides a default client that is initialised with the current docker context. It uses a default logger that is configured to print to the standard output using the `slog` package. + +```go +cli := client.DefaultClient +``` + +It's also possible to create a new client, with optional configuration: + +```go +cli, err := client.New(context.Background()) +if err != nil { + log.Fatalf("failed to create docker client: %v", err) +} + +// Close the docker client when done +defer cli.Close() +``` + +## Customizing the client + +The client created with the `New` function can be customized using functional options. The following options are available: + +- `WithHealthCheck(healthCheck func(ctx context.Context) func(c *Client) error) ClientOption`: A healthcheck function that is called to check the health of the client. By default, the client uses `Ping` to check the health of the client. +- `WithDockerHost(dockerHost string) ClientOption`: The docker host to use. By default, the client uses the current docker host. +- `WithDockerContext(dockerContext string) ClientOption`: The docker context to use. By default, the client uses the current docker context. + +In the case that both the docker host and the docker context are provided, the docker context takes precedence. diff --git a/vendor/github.com/docker/go-sdk/client/client.container.go b/vendor/github.com/docker/go-sdk/client/client.container.go new file mode 100644 index 00000000..5b354a51 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/client/client.container.go @@ -0,0 +1,188 @@ +package client + +import ( + "context" + "fmt" + "io" + + "github.com/containerd/errdefs" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/network" +) + +// ContainerCreate creates a new container. +func (c *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, name string) (container.CreateResponse, error) { + dockerClient, err := c.Client() + if err != nil { + return container.CreateResponse{}, fmt.Errorf("docker client: %w", err) + } + + return dockerClient.ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, name) +} + +// ContainerExecStart starts a new exec instance. +func (c *Client) ContainerExecAttach(ctx context.Context, execID string, config container.ExecAttachOptions) (types.HijackedResponse, error) { + dockerClient, err := c.Client() + if err != nil { + return types.HijackedResponse{}, fmt.Errorf("docker client: %w", err) + } + + return dockerClient.ContainerExecAttach(ctx, execID, config) +} + +// ContainerExecCreate creates a new exec instance. +func (c *Client) ContainerExecCreate(ctx context.Context, containerID string, options container.ExecOptions) (container.ExecCreateResponse, error) { + dockerClient, err := c.Client() + if err != nil { + return container.ExecCreateResponse{}, fmt.Errorf("docker client: %w", err) + } + + return dockerClient.ContainerExecCreate(ctx, containerID, options) +} + +// ContainerExecInspect inspects a exec instance. +func (c *Client) ContainerExecInspect(ctx context.Context, execID string) (container.ExecInspect, error) { + dockerClient, err := c.Client() + if err != nil { + return container.ExecInspect{}, fmt.Errorf("docker client: %w", err) + } + + return dockerClient.ContainerExecInspect(ctx, execID) +} + +// ContainerInspect inspects a container. +func (c *Client) ContainerInspect(ctx context.Context, containerID string) (container.InspectResponse, error) { + dockerClient, err := c.Client() + if err != nil { + return container.InspectResponse{}, fmt.Errorf("docker client: %w", err) + } + + return dockerClient.ContainerInspect(ctx, containerID) +} + +// ContainerList lists all containers. +func (c *Client) ContainerList(ctx context.Context, options container.ListOptions) ([]container.Summary, error) { + dockerClient, err := c.Client() + if err != nil { + return nil, fmt.Errorf("docker client: %w", err) + } + + containers, err := dockerClient.ContainerList(ctx, options) + if err != nil { + return nil, fmt.Errorf("container list: %w", err) + } + + return containers, nil +} + +// ContainerLogs returns the logs of a container. +func (c *Client) ContainerLogs(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) { + dockerClient, err := c.Client() + if err != nil { + return nil, fmt.Errorf("docker client: %w", err) + } + + return dockerClient.ContainerLogs(ctx, containerID, options) +} + +// ContainerPause pauses a container. +func (c *Client) ContainerPause(ctx context.Context, containerID string) error { + dockerClient, err := c.Client() + if err != nil { + return fmt.Errorf("docker client: %w", err) + } + + if containerID == "" { + return errdefs.ErrInvalidArgument.WithMessage("containerID is empty") + } + + return dockerClient.ContainerPause(ctx, containerID) +} + +// ContainerUnpause unpauses a container. +func (c *Client) ContainerUnpause(ctx context.Context, containerID string) error { + dockerClient, err := c.Client() + if err != nil { + return fmt.Errorf("docker client: %w", err) + } + + if containerID == "" { + return errdefs.ErrInvalidArgument.WithMessage("containerID is empty") + } + + return dockerClient.ContainerUnpause(ctx, containerID) +} + +// ContainerRemove removes a container. +func (c *Client) ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error { + dockerClient, err := c.Client() + if err != nil { + return fmt.Errorf("docker client: %w", err) + } + + return dockerClient.ContainerRemove(ctx, containerID, options) +} + +// ContainerStart starts a container. +func (c *Client) ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error { + dockerClient, err := c.Client() + if err != nil { + return fmt.Errorf("docker client: %w", err) + } + + return dockerClient.ContainerStart(ctx, containerID, options) +} + +// ContainerStop stops a container. +func (c *Client) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error { + dockerClient, err := c.Client() + if err != nil { + return fmt.Errorf("docker client: %w", err) + } + + return dockerClient.ContainerStop(ctx, containerID, options) +} + +// CopyFromContainer copies a file from a container. +func (c *Client) CopyFromContainer(ctx context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) { + dockerClient, err := c.Client() + if err != nil { + return nil, container.PathStat{}, fmt.Errorf("docker client: %w", err) + } + + return dockerClient.CopyFromContainer(ctx, containerID, srcPath) +} + +// ContainerLogs returns the logs of a container. +func (c *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options container.CopyToContainerOptions) error { + dockerClient, err := c.Client() + if err != nil { + return fmt.Errorf("docker client: %w", err) + } + + return dockerClient.CopyToContainer(ctx, containerID, dstPath, content, options) +} + +// FindContainerByName finds a container by name. The name filter uses a regex to find the containers. +func (c *Client) FindContainerByName(ctx context.Context, name string) (*container.Summary, error) { + if name == "" { + return nil, errdefs.ErrInvalidArgument.WithMessage("name is empty") + } + + // Note that, 'name' filter will use regex to find the containers + filter := filters.NewArgs(filters.Arg("name", fmt.Sprintf("^%s$", name))) + containers, err := c.ContainerList(ctx, container.ListOptions{All: true, Filters: filter}) + if err != nil { + return nil, fmt.Errorf("container list: %w", err) + } + + if len(containers) > 0 { + return &containers[0], nil + } + + return nil, errdefs.ErrNotFound.WithMessage(fmt.Sprintf("container %s not found", name)) +} diff --git a/vendor/github.com/docker/go-sdk/client/client.go b/vendor/github.com/docker/go-sdk/client/client.go new file mode 100644 index 00000000..1372b6b4 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/client/client.go @@ -0,0 +1,225 @@ +package client + +import ( + "context" + "fmt" + "io" + "log/slog" + "maps" + "path/filepath" + "time" + + "github.com/docker/docker/api/types/system" + "github.com/docker/docker/client" + dockercontext "github.com/docker/go-sdk/context" +) + +const ( + // Headers used for docker client requests. + headerUserAgent = "User-Agent" + + // TLS certificate files. + tlsCACertFile = "ca.pem" + tlsCertFile = "cert.pem" + tlsKeyFile = "key.pem" +) + +var ( + defaultLogger = slog.New(slog.NewTextHandler(io.Discard, nil)) + + defaultUserAgent = "docker-go-sdk/" + Version() + + defaultOpts = []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()} + + defaultHealthCheck = func(ctx context.Context) func(c *Client) error { + return func(c *Client) error { + dockerClient, err := c.Client() + if err != nil { + return fmt.Errorf("docker client: %w", err) + } + var pingErr error + for i := range 3 { + if _, pingErr = dockerClient.Ping(ctx); pingErr == nil { + return nil + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(time.Millisecond * time.Duration(i+1) * 100): + } + } + return fmt.Errorf("docker daemon not ready: %w", pingErr) + } + } +) + +// New returns a new client for interacting with containers. +// The client is configured using the provided options, that must be compatible with +// docker's [client.Opt] type. +// +// The Docker host is automatically resolved reading it from the current docker context; +// in case you need to pass [client.Opt] options that override the docker host, you can +// do so by providing the [FromDockerOpt] options adapter. +// E.g. +// +// cli, err := client.New(context.Background(), client.FromDockerOpt(client.WithHost("tcp://foobar:2375"))) +// +// The client uses a logger that is initialized to [io.Discard]; you can change it by +// providing the [WithLogger] option. +// E.g. +// +// cli, err := client.New(context.Background(), client.WithLogger(slog.Default())) +// +// The client is safe for concurrent use by multiple goroutines. +func New(ctx context.Context, options ...ClientOption) (*Client, error) { + c := &Client{ + healthCheck: defaultHealthCheck, + } + for _, opt := range options { + if err := opt.Apply(c); err != nil { + return nil, fmt.Errorf("apply option: %w", err) + } + } + + if err := c.init(ctx); err != nil { + return nil, fmt.Errorf("load config: %w", err) + } + + if err := c.healthCheck(ctx)(c); err != nil { + return nil, fmt.Errorf("health check: %w", err) + } + + return c, nil +} + +// init initializes the client. +// This method is safe for concurrent use by multiple goroutines. +func (c *Client) init(ctx context.Context) error { + c.once.Do(func() { + err := c.initOnce(ctx) + if err != nil { + c.err = err + } + }) + return c.err +} + +// initOnce initializes the client once. +// This method is safe for concurrent use by multiple goroutines. +func (c *Client) initOnce(_ context.Context) error { + if c.dockerClient != nil || c.err != nil { + return c.err + } + + // Set the default values for the client: + // - log + // - dockerHost + // - currentContext + if c.err = c.defaultValues(); c.err != nil { + return fmt.Errorf("default values: %w", c.err) + } + + if c.cfg, c.err = newConfig(c.dockerHost); c.err != nil { + return c.err + } + + opts := make([]client.Opt, len(defaultOpts), len(defaultOpts)+len(c.dockerOpts)) + copy(opts, defaultOpts) + + // Add all collected Docker options + opts = append(opts, c.dockerOpts...) + + if c.cfg.TLSVerify { + // For further information see: + // https://docs.docker.com/engine/security/protect-access/#use-tls-https-to-protect-the-docker-daemon-socket + opts = append(opts, client.WithTLSClientConfig( + filepath.Join(c.cfg.CertPath, tlsCACertFile), + filepath.Join(c.cfg.CertPath, tlsCertFile), + filepath.Join(c.cfg.CertPath, tlsKeyFile), + )) + } + if c.cfg.Host != "" { + // apply the host from the config if it is set + opts = append(opts, client.WithHost(c.cfg.Host)) + } + + httpHeaders := make(map[string]string) + maps.Copy(httpHeaders, c.extraHeaders) + + // Append the SDK headers last. + httpHeaders[headerUserAgent] = defaultUserAgent + + opts = append(opts, client.WithHTTPHeaders(httpHeaders)) + + if c.dockerClient, c.err = client.NewClientWithOpts(opts...); c.err != nil { + c.err = fmt.Errorf("new client: %w", c.err) + return c.err + } + + // Because each encountered error is immediately returned, it's safe to set the error to nil. + c.err = nil + return nil +} + +// defaultValues sets the default values for the client. +// If no logger is provided, the default one is used. +// If no docker host is provided and no docker context is provided, the current docker host and context are used. +// If no docker host is provided but a docker context is provided, the docker host from the context is used. +// If a docker host is provided, it is used as is. +func (c *Client) defaultValues() error { + if c.log == nil { + c.log = defaultLogger + } + + if c.dockerHost == "" && c.dockerContext == "" { + currentDockerHost, err := dockercontext.CurrentDockerHost() + if err != nil { + return fmt.Errorf("current docker host: %w", err) + } + currentContext, err := dockercontext.Current() + if err != nil { + return fmt.Errorf("current context: %w", err) + } + + c.dockerHost = currentDockerHost + c.dockerContext = currentContext + + return nil + } + + if c.dockerContext != "" { + dockerHost, err := dockercontext.DockerHostFromContext(c.dockerContext) + if err != nil { + return fmt.Errorf("docker host from context: %w", err) + } + + c.dockerHost = dockerHost + } + + return nil +} + +// Close closes the client. +// This method is safe for concurrent use by multiple goroutines. +func (c *Client) Close() error { + c.mtx.Lock() + defer c.mtx.Unlock() + + if c.dockerClient == nil { + return nil + } + + // Store the error before clearing the client + err := c.dockerClient.Close() + + // Clear the client after closing to prevent use-after-close issues + c.dockerInfo = system.Info{} + c.dockerInfoSet = false + + return err +} + +// ClientVersion returns the API version used by this client. +func (c *Client) ClientVersion() string { + return c.dockerClient.ClientVersion() +} diff --git a/vendor/github.com/docker/go-sdk/client/client.image.go b/vendor/github.com/docker/go-sdk/client/client.image.go new file mode 100644 index 00000000..45e689b6 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/client/client.image.go @@ -0,0 +1,50 @@ +package client + +import ( + "context" + "fmt" + "io" + + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" +) + +// ImageInspect inspects an image. +func (c *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts ...client.ImageInspectOption) (image.InspectResponse, error) { + dockerClient, err := c.Client() + if err != nil { + return image.InspectResponse{}, fmt.Errorf("docker client: %w", err) + } + + return dockerClient.ImageInspect(ctx, imageID, inspectOpts...) +} + +// ImagePull pulls an image from a remote registry. +func (c *Client) ImagePull(ctx context.Context, image string, options image.PullOptions) (io.ReadCloser, error) { + dockerClient, err := c.Client() + if err != nil { + return nil, fmt.Errorf("docker client: %w", err) + } + + return dockerClient.ImagePull(ctx, image, options) +} + +// ImageRemove removes an image from the local repository. +func (c *Client) ImageRemove(ctx context.Context, image string, options image.RemoveOptions) ([]image.DeleteResponse, error) { + dockerClient, err := c.Client() + if err != nil { + return nil, fmt.Errorf("docker client: %w", err) + } + + return dockerClient.ImageRemove(ctx, image, options) +} + +// ImageSave saves an image to a file. +func (c *Client) ImageSave(ctx context.Context, images []string, saveOptions ...client.ImageSaveOption) (io.ReadCloser, error) { + dockerClient, err := c.Client() + if err != nil { + return nil, fmt.Errorf("docker client: %w", err) + } + + return dockerClient.ImageSave(ctx, images, saveOptions...) +} diff --git a/vendor/github.com/docker/go-sdk/client/client.network.go b/vendor/github.com/docker/go-sdk/client/client.network.go new file mode 100644 index 00000000..36527024 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/client/client.network.go @@ -0,0 +1,58 @@ +package client + +import ( + "context" + "fmt" + + "github.com/docker/docker/api/types/network" +) + +// NetworkConnect connects a container to a network +func (c *Client) NetworkConnect(ctx context.Context, networkID, containerID string, config *network.EndpointSettings) error { + dockerClient, err := c.Client() + if err != nil { + return fmt.Errorf("docker client: %w", err) + } + + return dockerClient.NetworkConnect(ctx, networkID, containerID, config) +} + +// NetworkCreate creates a new network +func (c *Client) NetworkCreate(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error) { + dockerClient, err := c.Client() + if err != nil { + return network.CreateResponse{}, fmt.Errorf("docker client: %w", err) + } + + return dockerClient.NetworkCreate(ctx, name, options) +} + +// NetworkInspect inspects a network +func (c *Client) NetworkInspect(ctx context.Context, name string, options network.InspectOptions) (network.Inspect, error) { + dockerClient, err := c.Client() + if err != nil { + return network.Inspect{}, fmt.Errorf("docker client: %w", err) + } + + return dockerClient.NetworkInspect(ctx, name, options) +} + +// NetworkRemove removes a network +func (c *Client) NetworkRemove(ctx context.Context, name string) error { + dockerClient, err := c.Client() + if err != nil { + return fmt.Errorf("docker client: %w", err) + } + + return dockerClient.NetworkRemove(ctx, name) +} + +// NetworkList lists networks +func (c *Client) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) { + dockerClient, err := c.Client() + if err != nil { + return nil, fmt.Errorf("docker client: %w", err) + } + + return dockerClient.NetworkList(ctx, options) +} diff --git a/vendor/github.com/docker/go-sdk/client/client.volume.go b/vendor/github.com/docker/go-sdk/client/client.volume.go new file mode 100644 index 00000000..dde0753a --- /dev/null +++ b/vendor/github.com/docker/go-sdk/client/client.volume.go @@ -0,0 +1,16 @@ +package client + +import ( + "context" + "fmt" +) + +// VolumeRemove removes a volume. +func (c *Client) VolumeRemove(ctx context.Context, volumeID string, force bool) error { + dockerClient, err := c.Client() + if err != nil { + return fmt.Errorf("docker client: %w", err) + } + + return dockerClient.VolumeRemove(ctx, volumeID, force) +} diff --git a/vendor/github.com/docker/go-sdk/client/config.go b/vendor/github.com/docker/go-sdk/client/config.go new file mode 100644 index 00000000..d95e0cbf --- /dev/null +++ b/vendor/github.com/docker/go-sdk/client/config.go @@ -0,0 +1,63 @@ +package client + +import ( + "errors" + "fmt" + "os" + + "github.com/caarlos0/env/v11" +) + +// config represents the configuration for the Docker client. +// User values are read from the specified environment variables. +type config struct { + // Host is the address of the Docker daemon. + // Default: "" + Host string `env:"DOCKER_HOST"` + + // TLSVerify is a flag to enable or disable TLS verification when connecting to a Docker daemon. + // Default: 0 + TLSVerify bool `env:"DOCKER_TLS_VERIFY"` + + // CertPath is the path to the directory containing the Docker certificates. + // This is used when connecting to a Docker daemon over TLS. + // Default: "" + CertPath string `env:"DOCKER_CERT_PATH"` +} + +// newConfig returns a new configuration loaded from the properties file +// located in the user's home directory and overridden by environment variables. +func newConfig(host string) (*config, error) { + cfg := &config{ + Host: host, + } + + if err := env.Parse(cfg); err != nil { + return nil, fmt.Errorf("parse env: %w", err) + } + + if err := cfg.validate(); err != nil { + return nil, fmt.Errorf("validate: %w", err) + } + + return cfg, nil +} + +// validate verifies the configuration is valid. +func (c *config) validate() error { + if c.TLSVerify && c.CertPath == "" { + return errors.New("cert path required when TLS is enabled") + } + + if c.TLSVerify { + if _, err := os.Stat(c.CertPath); os.IsNotExist(err) { + return fmt.Errorf("cert path does not exist: %s", c.CertPath) + } + } + + if c.Host == "" { + return errors.New("host is required") + } + + return nil +} diff --git a/vendor/github.com/docker/go-sdk/client/daemon.go b/vendor/github.com/docker/go-sdk/client/daemon.go new file mode 100644 index 00000000..703a4795 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/client/daemon.go @@ -0,0 +1,87 @@ +package client + +import ( + "context" + "errors" + "fmt" + "net/url" + "os" + + "github.com/docker/docker/api/types/network" +) + +// dockerEnvFile is the file that is created when running inside a container. +// It's a variable to allow testing. +var dockerEnvFile = "/.dockerenv" + +// DaemonHost gets the host or ip of the Docker daemon where ports are exposed on +// Warning: this is based on your Docker host setting. Will fail if using an SSH tunnel +func (c *Client) DaemonHost(ctx context.Context) (string, error) { + c.mtx.Lock() + defer c.mtx.Unlock() + + return c.daemonHostLocked(ctx) +} + +func (c *Client) daemonHostLocked(ctx context.Context) (string, error) { + dockerClient, err := c.Client() + if err != nil { + return "", fmt.Errorf("docker client: %w", err) + } + + // infer from Docker host + daemonURL, err := url.Parse(dockerClient.DaemonHost()) + if err != nil { + return "", err + } + + var host string + + switch daemonURL.Scheme { + case "http", "https", "tcp": + host = daemonURL.Hostname() + case "unix", "npipe": + if inAContainer(dockerEnvFile) { + ip, err := c.getGatewayIP(ctx, "bridge") + if err != nil { + ip = "localhost" + } + host = ip + } else { + host = "localhost" + } + default: + return "", errors.New("could not determine host through env or docker host") + } + + return host, nil +} + +func (c *Client) getGatewayIP(ctx context.Context, defaultNetwork string) (string, error) { + nw, err := c.NetworkInspect(ctx, defaultNetwork, network.InspectOptions{}) + if err != nil { + return "", err + } + + var ip string + for _, cfg := range nw.IPAM.Config { + if cfg.Gateway != "" { + ip = cfg.Gateway + break + } + } + if ip == "" { + return "", errors.New("failed to get gateway IP from network settings") + } + + return ip, nil +} + +// InAContainer returns true if the code is running inside a container +// See https://github.com/docker/docker/blob/a9fa38b1edf30b23cae3eade0be48b3d4b1de14b/daemon/initlayer/setup_unix.go#L25 +func inAContainer(path string) bool { + if _, err := os.Stat(path); err == nil { + return true + } + return false +} diff --git a/vendor/github.com/docker/go-sdk/client/errors.go b/vendor/github.com/docker/go-sdk/client/errors.go new file mode 100644 index 00000000..c4b3342d --- /dev/null +++ b/vendor/github.com/docker/go-sdk/client/errors.go @@ -0,0 +1,24 @@ +package client + +import ( + "github.com/containerd/errdefs" +) + +var permanentClientErrors = []func(error) bool{ + errdefs.IsNotFound, + errdefs.IsInvalidArgument, + errdefs.IsUnauthorized, + errdefs.IsPermissionDenied, + errdefs.IsNotImplemented, + errdefs.IsInternal, +} + +// IsPermanentClientError returns true if the error is a permanent client error. +func IsPermanentClientError(err error) bool { + for _, isErrFn := range permanentClientErrors { + if isErrFn(err) { + return true + } + } + return false +} diff --git a/vendor/github.com/docker/go-sdk/client/labels.go b/vendor/github.com/docker/go-sdk/client/labels.go new file mode 100644 index 00000000..e6ddd9d2 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/client/labels.go @@ -0,0 +1,27 @@ +package client + +import "maps" + +const ( + // LabelBase is the base label for all Docker labels. + LabelBase = "com.docker.sdk" + + // LabelLang specifies the language which created the container. + LabelLang = LabelBase + ".lang" + + // LabelVersion specifies the version of go-sdk which created the container. + LabelVersion = LabelBase + ".version" +) + +// SDKLabels returns a map of labels that can be used to identify resources +// created by this library. +var SDKLabels = map[string]string{ + LabelBase: "true", + LabelLang: "go", + LabelVersion: Version(), +} + +// AddSDKLabels adds the SDK labels to target. +func AddSDKLabels(target map[string]string) { + maps.Copy(target, SDKLabels) +} diff --git a/vendor/github.com/docker/go-sdk/client/options.go b/vendor/github.com/docker/go-sdk/client/options.go new file mode 100644 index 00000000..8963f1d9 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/client/options.go @@ -0,0 +1,94 @@ +package client + +import ( + "context" + "errors" + "log/slog" + + "github.com/docker/docker/client" +) + +// ClientOption is a type that represents an option for configuring a client. +// It is compatible with docker's Opt type. +type ClientOption interface { + // Apply applies the option to the client. + // This method is used to make ClientOption compatible with docker's Opt type. + Apply(*Client) error +} + +// dockerOptAdapter adapts a docker Opt to our ClientOption interface +type dockerOptAdapter struct { + opt client.Opt +} + +// Apply implements the ClientOption interface, adding the docker Opt to the client. +func (a *dockerOptAdapter) Apply(c *Client) error { + c.dockerOpts = append(c.dockerOpts, a.opt) + return nil +} + +// FromDockerOpt converts a docker Opt to our ClientOption +func FromDockerOpt(opt client.Opt) ClientOption { + return &dockerOptAdapter{opt: opt} +} + +// funcOpt is a function that implements ClientOption +type funcOpt func(*Client) error + +// Apply implements the ClientOption interface. +func (f funcOpt) Apply(c *Client) error { + return f(c) +} + +// NewClientOption creates a new ClientOption from a function +func NewClientOption(f func(*Client) error) ClientOption { + return funcOpt(f) +} + +// WithDockerHost returns a client option that sets the docker host for the client. +func WithDockerHost(dockerHost string) ClientOption { + return NewClientOption(func(c *Client) error { + c.dockerHost = dockerHost + return nil + }) +} + +// WithDockerContext returns a client option that sets the docker context for the client. +// If set, the client will use the docker context to determine the docker host. +// If used in combination with [WithDockerHost], the host in the context will take precedence. +func WithDockerContext(dockerContext string) ClientOption { + return NewClientOption(func(c *Client) error { + c.dockerContext = dockerContext + return nil + }) +} + +// WithExtraHeaders returns a client option that sets the extra headers for the client. +func WithExtraHeaders(headers map[string]string) ClientOption { + return NewClientOption(func(c *Client) error { + c.extraHeaders = headers + return nil + }) +} + +// WithHealthCheck returns a client option that sets the health check for the client. +// If not set, the default health check will be used, which retries the ping to the +// docker daemon until it is ready, three times, or the context is done. +func WithHealthCheck(healthCheck func(ctx context.Context) func(c *Client) error) ClientOption { + return NewClientOption(func(c *Client) error { + if healthCheck == nil { + return errors.New("health check is nil") + } + + c.healthCheck = healthCheck + return nil + }) +} + +// WithLogger returns a client option that sets the logger for the client. +func WithLogger(log *slog.Logger) ClientOption { + return NewClientOption(func(c *Client) error { + c.log = log + return nil + }) +} diff --git a/vendor/github.com/docker/go-sdk/client/types.go b/vendor/github.com/docker/go-sdk/client/types.go new file mode 100644 index 00000000..cbee752e --- /dev/null +++ b/vendor/github.com/docker/go-sdk/client/types.go @@ -0,0 +1,128 @@ +package client + +import ( + "context" + "fmt" + "log/slog" + "sync" + + "github.com/docker/docker/api/types/system" + "github.com/docker/docker/client" +) + +// packagePath is the package path for the docker-go-sdk package. +const packagePath = "github.com/docker/go-sdk" + +// DefaultClient is the default client for interacting with containers. +var DefaultClient = &Client{ + log: defaultLogger, + healthCheck: defaultHealthCheck, +} + +// Client is a type that represents a client for interacting with containers. +type Client struct { + // log is the logger for the client. + log *slog.Logger + + // mtx is a mutex for synchronizing access to the fields below. + mtx sync.RWMutex + + // once is used to initialize the client once. + once sync.Once + + // client is the underlying docker client, embedded to avoid + // having to re-implement all the methods. + dockerClient *client.Client + + // cfg is the configuration for the client, obtained from the environment variables. + cfg *config + + // err is used to store errors that occur during the client's initialization. + err error + + // dockerOpts are options to be passed to the docker client. + dockerOpts []client.Opt + + // dockerContext is the current context of the docker daemon. + dockerContext string + + // dockerHost is the host of the docker daemon. + dockerHost string + + // extraHeaders are additional headers to be sent to the docker client. + extraHeaders map[string]string + + // cached docker info + dockerInfo system.Info + dockerInfoSet bool + + // healthCheck is a function that returns the health of the docker daemon. + // If not set, the default health check will be used. + healthCheck func(ctx context.Context) func(c *Client) error +} + +// Client returns the underlying docker client. +// It verifies that the client is initialized. +// It is safe to call this method concurrently. +func (c *Client) Client() (*client.Client, error) { + ctx := context.Background() + + if err := c.init(ctx); err != nil { + return nil, fmt.Errorf("init client: %w", err) + } + + return c.dockerClient, nil +} + +// Logger returns the logger for the client. +func (c *Client) Logger() *slog.Logger { + return c.log +} + +// Info returns information about the docker server. The result of Info is cached +// and reused every time Info is called. +// It will also print out the docker server info, and the resolved Docker paths, to the default logger. +func (c *Client) Info(ctx context.Context) (system.Info, error) { + c.mtx.Lock() + if c.dockerInfoSet { + defer c.mtx.Unlock() + return c.dockerInfo, nil + } + c.mtx.Unlock() + + var info system.Info + + cli, err := c.Client() + if err != nil { + return info, fmt.Errorf("docker client: %w", err) + } + + info, err = cli.Info(ctx) + if err != nil { + return info, fmt.Errorf("docker info: %w", err) + } + c.dockerInfo = info + c.dockerInfoSet = true + + infoLabels := "" + if len(c.dockerInfo.Labels) > 0 { + infoLabels = ` + Labels:` + for _, lb := range c.dockerInfo.Labels { + infoLabels += "\n " + lb + } + } + + c.log.Info("Connected to docker", + "package", packagePath, + "server_version", c.dockerInfo.ServerVersion, + "client_version", cli.ClientVersion(), + "operating_system", c.dockerInfo.OperatingSystem, + "mem_total", c.dockerInfo.MemTotal/1024/1024, + "labels", infoLabels, + "docker_context", c.dockerContext, + "docker_host", c.dockerHost, + ) + + return c.dockerInfo, nil +} diff --git a/vendor/github.com/docker/go-sdk/client/version.go b/vendor/github.com/docker/go-sdk/client/version.go new file mode 100644 index 00000000..b2d8ba20 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/client/version.go @@ -0,0 +1,10 @@ +package client + +const ( + version = "0.1.0-alpha006" +) + +// Version returns the version of the client package. +func Version() string { + return version +} diff --git a/vendor/github.com/docker/go-sdk/config/LICENSE b/vendor/github.com/docker/go-sdk/config/LICENSE new file mode 100644 index 00000000..a0e6f9bc --- /dev/null +++ b/vendor/github.com/docker/go-sdk/config/LICENSE @@ -0,0 +1,217 @@ +Copyright (c) 2025–present Docker, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +----------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/docker/go-sdk/config/Makefile b/vendor/github.com/docker/go-sdk/config/Makefile new file mode 100644 index 00000000..748cb213 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/config/Makefile @@ -0,0 +1 @@ +include ../commons-test.mk diff --git a/vendor/github.com/docker/go-sdk/config/README.md b/vendor/github.com/docker/go-sdk/config/README.md new file mode 100644 index 00000000..a0d41f44 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/config/README.md @@ -0,0 +1,82 @@ +# Docker Config + +This package provides a simple API to load docker CLI configs, auths, etc. with minimal deps. + +This library is a fork of [github.com/cpuguy83/dockercfg](https://github.com/cpuguy83/dockercfg). Read the [NOTICE](../NOTICE) file for more details. + +## Installation + +```bash +go get github.com/docker/go-sdk/config +``` + +## Usage + +### Docker Config + +#### Directory + +It will return the current Docker config directory. + +```go +dir, err := config.Dir() +if err != nil { + log.Fatalf("failed to get current docker config directory: %v", err) +} + +fmt.Printf("current docker config directory: %s", dir) +``` + +#### Filepath + +It will return the path to the Docker config file. + +```go +filepath, err := config.Filepath() +if err != nil { + log.Fatalf("failed to get current docker config file path: %v", err) +} + +fmt.Printf("current docker config file path: %s", filepath) +``` + +#### Load + +It will return the Docker config. + +```go +cfg, err := config.Load() +if err != nil { + log.Fatalf("failed to load docker config: %v", err) +} + +fmt.Printf("docker config: %+v", cfg) +``` + +### Auth + +#### Registry Credentials + +It will return the registry credentials for the given Docker image. + +```go +authConfig, err := config.RegistryCredentials("nginx:latest") +if err != nil { + log.Fatalf("failed to get registry credentials: %v", err) +} + +fmt.Printf("registry credentials: %+v", authConfig) +``` + +#### Registry Credentials For Hostname + +It will return the registry credentials for the given Docker registry. + +```go +authConfig, err := config.RegistryCredentialsForHostname("https://index.docker.io/v1/") +if err != nil { + log.Fatalf("failed to get registry credentials: %v", err) +} + +fmt.Printf("registry credentials: %+v", authConfig) +``` diff --git a/vendor/github.com/docker/go-sdk/config/auth.go b/vendor/github.com/docker/go-sdk/config/auth.go new file mode 100644 index 00000000..4dd0d94b --- /dev/null +++ b/vendor/github.com/docker/go-sdk/config/auth.go @@ -0,0 +1,135 @@ +package config + +import ( + "encoding/base64" + "errors" + "fmt" + "io/fs" + "strings" + + "github.com/docker/go-sdk/config/auth" +) + +// This is used by the docker CLI in cases where an oauth identity token is used. +// In that case the username is stored literally as `` +// When fetching the credentials we check for this value to determine if. +const tokenUsername = "" + +// RegistryCredentials gets registry credentials for the passed in image reference. +// +// This will use [Load] to read registry auth details from the config. +// If the config doesn't exist, it will attempt to load registry credentials using the default credential helper for the platform. +func RegistryCredentials(imageRef string) (AuthConfig, error) { + var ref auth.ImageReference + var creds AuthConfig + + ref, err := auth.ParseImageRef(imageRef) + if err != nil { + return creds, fmt.Errorf("parse image ref: %w", err) + } + + creds, err = RegistryCredentialsForHostname(ref.Registry) + if err != nil { + return creds, fmt.Errorf("get credentials for hostname: %w", err) + } + + return creds, nil +} + +// RegistryCredentialsForHostname gets registry credentials for the passed in registry host. +// +// This will use [Load] to read registry auth details from the config. +// If the config doesn't exist, it will attempt to load registry credentials using the default credential helper for the platform. +func RegistryCredentialsForHostname(hostname string) (AuthConfig, error) { + var creds AuthConfig + cfg, err := Load() + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return creds, fmt.Errorf("load default config: %w", err) + } + + return credentialsFromHelper("", hostname) + } + + return cfg.RegistryCredentialsForHostname(hostname) +} + +// RegistryCredentialsForHostname gets credentials, if any, for the provided hostname. +// +// Hostnames should already be resolved using [ResolveRegistryHost]. +// +// If the returned username string is empty, the password is an identity token. +func (c *Config) RegistryCredentialsForHostname(hostname string) (AuthConfig, error) { + var zero AuthConfig + h, ok := c.CredentialHelpers[hostname] + if ok { + return credentialsFromHelper(h, hostname) + } + + if c.CredentialsStore != "" { + creds, err := credentialsFromHelper(c.CredentialsStore, hostname) + if err != nil { + return zero, fmt.Errorf("get credentials from store: %w", err) + } + + if creds.Username != "" || creds.Password != "" { + return creds, nil + } + } + + authConfig, ok := c.AuthConfigs[hostname] + if !ok { + return credentialsFromHelper("", hostname) + } + + creds := AuthConfig{} + + if authConfig.IdentityToken != "" { + creds.Username = "" + creds.Password = authConfig.IdentityToken + return creds, nil + } + + if authConfig.Username != "" && authConfig.Password != "" { + creds.Username = authConfig.Username + creds.Password = authConfig.Password + return creds, nil + } + + user, pass, err := decodeBase64Auth(authConfig) + if err != nil { + return zero, fmt.Errorf("decode base64 auth: %w", err) + } + + creds.Username = user + creds.Password = pass + + return creds, nil +} + +// decodeBase64Auth decodes the legacy file-based auth storage from the docker CLI. +// It takes the "Auth" filed from AuthConfig and decodes that into a username and password. +// +// If "Auth" is empty, an empty user/pass will be returned, but not an error. +func decodeBase64Auth(auth AuthConfig) (string, string, error) { + if auth.Auth == "" { + return "", "", nil + } + + decLen := base64.StdEncoding.DecodedLen(len(auth.Auth)) + decoded := make([]byte, decLen) + n, err := base64.StdEncoding.Decode(decoded, []byte(auth.Auth)) + if err != nil { + return "", "", fmt.Errorf("decode auth: %w", err) + } + + decoded = decoded[:n] + + const sep = ":" + user, pass, found := strings.Cut(string(decoded), sep) + if !found { + return "", "", fmt.Errorf("invalid auth: missing %q separator", sep) + } + + return user, pass, nil +} diff --git a/vendor/github.com/docker/go-sdk/config/auth/registry.go b/vendor/github.com/docker/go-sdk/config/auth/registry.go new file mode 100644 index 00000000..1071eb13 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/config/auth/registry.go @@ -0,0 +1,119 @@ +package auth + +import ( + "fmt" + "regexp" +) + +const ( + IndexDockerIO = "https://index.docker.io/v1/" + + // Protocol part (optional) + protocolGroup = `(?:https?://)?` + + // Hostname part (domain, IP, or localhost) + hostnameGroup = `(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}|(?:\d{1,3}\.){3}\d{1,3}|localhost)` + + // Port part (optional) + portGroup = `(?::\d+)?` + + // Registry part (must be a valid hostname/IP with optional protocol and port) + registryGroup = `(?:(?P` + protocolGroup + hostnameGroup + portGroup + `)/)?` + + // Repository part (can be single or multi-level) + repositoryGroup = `(?P(?:[^/:@]+/)*[^/:@]+)` + + // Tag part + tagGroup = `(?::(?P[^@]+))?` + + // Digest part + digestGroup = `(?:@(?Psha256:[a-f0-9]{64}|sha512:[a-f0-9]{128}))?` + + // 1. registry/repository[tag][digest] + // 2. repository[tag][digest] (when no registry) + regexImageRef = `^` + registryGroup + repositoryGroup + tagGroup + digestGroup + `$` +) + +// ImageReference represents a parsed Docker image reference +type ImageReference struct { + // Registry is the registry hostname (e.g., "docker.io", "myregistry.com:5000") + Registry string + // Repository is the image repository (e.g., "library/nginx", "user/image") + Repository string + // Tag is the image tag (e.g., "latest", "v1.0.0") + Tag string + // Digest is the image digest if present (e.g., "sha256:...") + Digest string +} + +// ParseImageRef extracts the registry from the image name, using a regular expression to extract the registry from the image name. +// - image:tag +// - image:tag@digest +// - image +// - image@digest +// - repository/image:tag +// - repository/image:tag@digest +// - repository/image +// - repository/image@digest +// - registry/image:tag +// - registry/image:tag@digest +// - registry/image +// - registry/image@digest +// - registry/repository/image:tag +// - registry/repository/image:tag@digest +// - registry/repository/image +// - registry/repository/image@digest +// - registry:port/repository/image:tag +// - registry:port/repository/image:tag@digest +// - registry:port/repository/image +// - registry:port/repository/image@digest +// - registry:port/image:tag +// - registry:port/image:tag@digest +// - registry:port/image +// - registry:port/image@digest +// Once extracted the registry, it is validated to return the Docker Index URL +// if the registry is a valid Docker Index URL, otherwise it returns the registry as is. +func ParseImageRef(imageRef string) (ImageReference, error) { + var ref ImageReference + + r := regexp.MustCompile(regexImageRef) + + matches := r.FindStringSubmatch(imageRef) + if len(matches) == 0 { + return ref, fmt.Errorf("invalid image reference: %s", imageRef) + } + + // Get named groups + names := r.SubexpNames() + result := make(map[string]string) + for i, name := range names { + if i != 0 && name != "" { // Skip the first empty name + result[name] = matches[i] + } + } + + if result["registry"] == "" { + result["registry"] = IndexDockerIO + } + + ref = ImageReference{ + Registry: resolveRegistryHost(result["registry"]), + Repository: result["repository"], + Tag: result["tag"], + Digest: result["digest"], + } + + return ref, nil +} + +// resolveRegistryHost can be used to transform a docker registry host name into what is used for the docker config/cred helpers +// +// This is useful for using with containerd authorizers. +// Naturally this only transforms docker hub URLs. +func resolveRegistryHost(host string) string { + switch host { + case "index.docker.io", "docker.io", IndexDockerIO, "registry-1.docker.io": + return IndexDockerIO + } + return host +} diff --git a/vendor/github.com/docker/go-sdk/config/credentials_helpers.go b/vendor/github.com/docker/go-sdk/config/credentials_helpers.go new file mode 100644 index 00000000..4028b41c --- /dev/null +++ b/vendor/github.com/docker/go-sdk/config/credentials_helpers.go @@ -0,0 +1,125 @@ +package config + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "os/exec" + "runtime" + "strings" +) + +// Errors from credential helpers. +var ( + ErrCredentialsNotFound = errors.New("credentials not found in native keychain") + ErrCredentialsMissingServerURL = errors.New("no credentials server URL") +) + +//nolint:gochecknoglobals // These are used to mock exec in tests. +var ( + // execLookPath is a variable that can be used to mock exec.LookPath in tests. + execLookPath = exec.LookPath + // execCommand is a variable that can be used to mock exec.Command in tests. + execCommand = exec.Command +) + +// credentialsFromHelper attempts to lookup credentials from the passed in docker credential helper. +// +// The credential helper should just be the suffix name (no "docker-credential-"). +// If the passed in helper program is empty this will look up the default helper for the platform. +// +// If the credentials are not found, no error is returned, only empty credentials. +// +// Hostnames should already be resolved using [ResolveRegistryHost] +// +// If the username string is empty, the password string is an identity token. +func credentialsFromHelper(helper, hostname string) (AuthConfig, error) { + var creds AuthConfig + credHelperName := helper + if helper == "" { + helper, helperErr := getCredentialHelper() + if helperErr != nil { + return creds, fmt.Errorf("get credential helper: %w", helperErr) + } + + if helper == "" { + return creds, nil + } + + credHelperName = helper + } + + helper = "docker-credential-" + credHelperName + p, err := execLookPath(helper) + if err != nil { + if !errors.Is(err, exec.ErrNotFound) { + return creds, fmt.Errorf("look up %q: %w", helper, err) + } + + return creds, nil + } + + var outBuf, errBuf bytes.Buffer + cmd := execCommand(p, "get") + cmd.Stdin = strings.NewReader(hostname) + cmd.Stdout = &outBuf + cmd.Stderr = &errBuf + + if err = cmd.Run(); err != nil { + out := strings.TrimSpace(outBuf.String()) + switch out { + case ErrCredentialsNotFound.Error(): + return creds, nil + case ErrCredentialsMissingServerURL.Error(): + return creds, ErrCredentialsMissingServerURL + default: + return creds, fmt.Errorf("execute %q stdout: %q stderr: %q: %w", + helper, out, strings.TrimSpace(errBuf.String()), err, + ) + } + } + + // ServerURL is not always present in the output, + // only some credential helpers include it (e.g. Google Cloud). + var bytesCreds struct { + Username string `json:"Username"` + Secret string `json:"Secret"` + ServerURL string `json:"ServerURL,omitempty"` + } + + if err = json.Unmarshal(outBuf.Bytes(), &bytesCreds); err != nil { + return creds, fmt.Errorf("unmarshal credentials from: %q: %w", helper, err) + } + + // When tokenUsername is used, the output is an identity token and the username is garbage. + if bytesCreds.Username == tokenUsername { + bytesCreds.Username = "" + } + + creds.Username = bytesCreds.Username + creds.Password = bytesCreds.Secret + creds.ServerAddress = bytesCreds.ServerURL + + return creds, nil +} + +// getCredentialHelper gets the default credential helper name for the current platform. +func getCredentialHelper() (string, error) { + switch runtime.GOOS { + case "linux": + if _, err := execLookPath("pass"); err != nil { + if errors.Is(err, exec.ErrNotFound) { + return "secretservice", nil + } + return "", fmt.Errorf(`look up "pass": %w`, err) + } + return "pass", nil + case "darwin": + return "osxkeychain", nil + case "windows": + return "wincred", nil + default: + return "", nil + } +} diff --git a/vendor/github.com/docker/go-sdk/config/load.go b/vendor/github.com/docker/go-sdk/config/load.go new file mode 100644 index 00000000..ad5bb50f --- /dev/null +++ b/vendor/github.com/docker/go-sdk/config/load.go @@ -0,0 +1,133 @@ +package config + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "os/user" + "path/filepath" + "runtime" +) + +// getHomeDir returns the home directory of the current user with the help of +// environment variables depending on the target operating system. +// Returned path should be used with "path/filepath" to form new paths. +// +// On non-Windows platforms, it falls back to nss lookups, if the home +// directory cannot be obtained from environment-variables. +// +// If linking statically with cgo enabled against glibc, ensure the +// osusergo build tag is used. +// +// If needing to do nss lookups, do not disable cgo or set osusergo. +// +// getHomeDir is a copy of [pkg/homedir.Get] to prevent adding docker/docker +// as dependency for consumers that only need to read the config-file. +// +// [pkg/homedir.Get]: https://pkg.go.dev/github.com/docker/docker@v26.1.4+incompatible/pkg/homedir#Get +func getHomeDir() (string, error) { + home, _ := os.UserHomeDir() + if home == "" && runtime.GOOS != "windows" { + if u, err := user.Current(); err == nil { + return u.HomeDir, nil + } + } + + if home == "" { + return "", errors.New("user home directory not determined") + } + + return home, nil +} + +// Dir returns the directory the configuration file is stored in, +// checking if the directory exists. +func Dir() (string, error) { + dir := os.Getenv(EnvOverrideDir) + if dir != "" { + if err := fileExists(dir); err != nil { + return "", fmt.Errorf("config dir: %w", err) + } + return dir, nil + } + + home, err := getHomeDir() + if err != nil { + return "", fmt.Errorf("user home dir: %w", err) + } + + configDir := filepath.Join(home, configFileDir) + if err := fileExists(configDir); err != nil { + return "", fmt.Errorf("config dir: %w", err) + } + + return configDir, nil +} + +func fileExists(path string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + return fmt.Errorf("file does not exist: %w", err) + } + + return nil +} + +// Filepath returns the path to the docker cli config file, +// checking if the file exists. +func Filepath() (string, error) { + dir, err := Dir() + if err != nil { + return "", fmt.Errorf("config dir: %w", err) + } + + configFilePath := filepath.Join(dir, FileName) + if err := fileExists(configFilePath); err != nil { + return "", fmt.Errorf("config file: %w", err) + } + + return configFilePath, nil +} + +// Load returns the docker config file. It will internally check, in this particular order: +// 1. the DOCKER_AUTH_CONFIG environment variable, unmarshalling it into a Config +// 2. the DOCKER_CONFIG environment variable, as the path to the config file +// 3. else it will load the default config file, which is ~/.docker/config.json +func Load() (Config, error) { + if env := os.Getenv("DOCKER_AUTH_CONFIG"); env != "" { + var cfg Config + if err := json.Unmarshal([]byte(env), &cfg); err != nil { + return Config{}, fmt.Errorf("unmarshal DOCKER_AUTH_CONFIG: %w", err) + } + return cfg, nil + } + + var cfg Config + p, err := Filepath() + if err != nil { + return cfg, fmt.Errorf("config path: %w", err) + } + + cfg, err = loadFromFilepath(p) + if err != nil { + return cfg, fmt.Errorf("load config: %w", err) + } + + return cfg, nil +} + +// loadFromFilepath loads config from the specified path into cfg. +func loadFromFilepath(configPath string) (Config, error) { + var cfg Config + f, err := os.Open(configPath) + if err != nil { + return cfg, fmt.Errorf("open config: %w", err) + } + defer f.Close() + + if err = json.NewDecoder(f).Decode(&cfg); err != nil { + return cfg, fmt.Errorf("decode config: %w", err) + } + + return cfg, nil +} diff --git a/vendor/github.com/docker/go-sdk/config/types.go b/vendor/github.com/docker/go-sdk/config/types.go new file mode 100644 index 00000000..10ec40d7 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/config/types.go @@ -0,0 +1,80 @@ +package config + +const ( + // EnvOverrideDir is the name of the environment variable that can be + // used to override the location of the client configuration files (~/.docker). + // + // It takes priority over the default. + EnvOverrideDir = "DOCKER_CONFIG" + + // configFileDir is the name of the directory containing the client configuration files + configFileDir = ".docker" + + // configFileName is the name of the client configuration file inside the + // config-directory. + FileName = "config.json" +) + +// Config represents the on disk format of the docker CLI's config file. +type Config struct { + AuthConfigs map[string]AuthConfig `json:"auths"` + HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` + PsFormat string `json:"psFormat,omitempty"` + ImagesFormat string `json:"imagesFormat,omitempty"` + NetworksFormat string `json:"networksFormat,omitempty"` + PluginsFormat string `json:"pluginsFormat,omitempty"` + VolumesFormat string `json:"volumesFormat,omitempty"` + StatsFormat string `json:"statsFormat,omitempty"` + DetachKeys string `json:"detachKeys,omitempty"` + CredentialsStore string `json:"credsStore,omitempty"` + CredentialHelpers map[string]string `json:"credHelpers,omitempty"` + Filename string `json:"-"` // Note: for internal use only. + ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"` + ServicesFormat string `json:"servicesFormat,omitempty"` + TasksFormat string `json:"tasksFormat,omitempty"` + SecretFormat string `json:"secretFormat,omitempty"` + ConfigFormat string `json:"configFormat,omitempty"` + NodesFormat string `json:"nodesFormat,omitempty"` + PruneFilters []string `json:"pruneFilters,omitempty"` + Proxies map[string]ProxyConfig `json:"proxies,omitempty"` + Experimental string `json:"experimental,omitempty"` + StackOrchestrator string `json:"stackOrchestrator,omitempty"` + Kubernetes *KubernetesConfig `json:"kubernetes,omitempty"` + CurrentContext string `json:"currentContext,omitempty"` + CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"` + Aliases map[string]string `json:"aliases,omitempty"` +} + +// ProxyConfig contains proxy configuration settings. +type ProxyConfig struct { + HTTPProxy string `json:"httpProxy,omitempty"` + HTTPSProxy string `json:"httpsProxy,omitempty"` + NoProxy string `json:"noProxy,omitempty"` + FTPProxy string `json:"ftpProxy,omitempty"` +} + +// AuthConfig contains authorization information for connecting to a Registry. +type AuthConfig struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Auth string `json:"auth,omitempty"` + + // Email is an optional value associated with the username. + // This field is deprecated and will be removed in a later + // version of docker. + Email string `json:"email,omitempty"` + + ServerAddress string `json:"serveraddress,omitempty"` + + // IdentityToken is used to authenticate the user and get + // an access token for the registry. + IdentityToken string `json:"identitytoken,omitempty"` + + // RegistryToken is a bearer token to be sent to a registry. + RegistryToken string `json:"registrytoken,omitempty"` +} + +// KubernetesConfig contains Kubernetes orchestrator settings. +type KubernetesConfig struct { + AllNamespaces string `json:"allNamespaces,omitempty"` +} diff --git a/vendor/github.com/docker/go-sdk/config/version.go b/vendor/github.com/docker/go-sdk/config/version.go new file mode 100644 index 00000000..c29893d0 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/config/version.go @@ -0,0 +1,10 @@ +package config + +const ( + version = "0.1.0-alpha006" +) + +// Version returns the version of the config package. +func Version() string { + return version +} diff --git a/vendor/github.com/docker/go-sdk/container/.mockery.yaml b/vendor/github.com/docker/go-sdk/container/.mockery.yaml new file mode 100644 index 00000000..e0e47543 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/.mockery.yaml @@ -0,0 +1,11 @@ +quiet: True +disable-version-string: True +with-expecter: True +mockname: "mock{{.InterfaceName}}" +filename: "{{ .InterfaceName | lower }}_mock_test.go" +outpkg: "{{.PackageName}}_test" +dir: "{{.InterfaceDir}}" +packages: + github.com/docker/go-sdk/container/wait: + interfaces: + StrategyTarget: diff --git a/vendor/github.com/docker/go-sdk/container/LICENSE b/vendor/github.com/docker/go-sdk/container/LICENSE new file mode 100644 index 00000000..a0e6f9bc --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/LICENSE @@ -0,0 +1,217 @@ +Copyright (c) 2025–present Docker, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +----------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/docker/go-sdk/container/Makefile b/vendor/github.com/docker/go-sdk/container/Makefile new file mode 100644 index 00000000..748cb213 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/Makefile @@ -0,0 +1 @@ +include ../commons-test.mk diff --git a/vendor/github.com/docker/go-sdk/container/README.md b/vendor/github.com/docker/go-sdk/container/README.md new file mode 100644 index 00000000..462415e1 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/README.md @@ -0,0 +1,162 @@ +# Docker Containers + +This package provides a simple API to create and manage Docker containers. + +This library is a fork of [github.com/testcontainers/testcontainers-go](https://github.com/testcontainers/testcontainers-go). Read the [NOTICE](../NOTICE) file for more details. + +## Installation + +```bash +go get github.com/docker/go-sdk/container +``` + +## Usage + +The `Run` function is the main function to create and manage containers. It can be used to create a new container, start it, wait for it to be ready, and stop it. It receives a Go context, and a variadic list of options to customize the container definition: + +```go +err = container.Run(ctx, container.WithImage("nginx:alpine")) +if err != nil { + log.Fatalf("failed to run container: %v", err) +} +``` + +## Container Definition + +The container definition is a struct that contains the configuration for the container. It represents the container's configuration before it's started, and it's used to create the container in the desired state. You can customize the container definition using functional options when calling the `Run` function. More on this below. + +## Container Lifecycle Hooks + +The container lifecycle hooks are a set of functions that are called at different stages of the container's lifecycle. + +- PreCreate +- PostCreate +- PreStart +- PostStart +- PostReady +- PreStop +- PostStop +- PreTerminate +- PostTerminate + +They allow you to customize the container's behavior at different stages of its lifecycle, running custom code before or after the container is created, started, ready, stopped or terminated. + +## Copy Files + +It's possible to copy files to the container, and this can happen in different stages of the container's lifecycle: + +- After the container is created but before it's started: using the `WithFiles` option, you can add files to the container. +- After the container is started: using the container's `CopyToContainer` method, you can copy files to the container. + +If you need to copy a directory, you can use the `CopyDirToContainer` method, which uses the parent directory of the container path as the target directory. + +It's also possible to copy files from the container to the host, using the container's `CopyFromContainer` method. + +## Defining the readiness state for the container + +In order to wait for the container to be ready, you can use the `WithWaitStrategy` options, that can be used to define a custom wait strategy for the container. The library provides some predefined wait strategies in the `wait` package: + +- ForExec: waits for a command to exit +- ForExit: waits for a container to exit +- ForFile: waits for a file to exist +- ForHealth: waits for a container to be healthy +- ForListeningPort: waits for a port to be listening +- ForHTTP: waits for a container to respond to an HTTP request +- ForLog: waits for a container to log a message +- ForSQL: waits for a SQL connection to be established +- ForAll: waits for a combination of strategies + +You can also define your own wait strategy by implementing the `wait.Strategy` interface. + +Using wait strategies, you don't need to poll the container state, as the wait strategy will block the execution until the condition is met. This is useful to avoid adding `time.Sleep` to your code, making it more reliable, even on slower systems. + +## Customizing the Run function + +The Run function can be customized using functional options. The following options are available: + +### Available Options + +The following options are available to customize the container definition: + +- `WithAdditionalLifecycleHooks(hooks ...LifecycleHooks) CustomizeDefinitionOption` +- `WithAdditionalWaitStrategy(strategies ...wait.Strategy) CustomizeDefinitionOption` +- `WithAdditionalWaitStrategyAndDeadline(deadline time.Duration, strategies ...wait.Strategy) CustomizeDefinitionOption` +- `WithAfterReadyCommand(execs ...Executable) CustomizeDefinitionOption` +- `WithAlwaysPull() CustomizeDefinitionOption` +- `WithBridgeNetwork() CustomizeDefinitionOption` +- `WithCmd(cmd ...string) CustomizeDefinitionOption` +- `WithCmdArgs(cmdArgs ...string) CustomizeDefinitionOption` +- `WithConfigModifier(modifier func(config *container.Config)) CustomizeDefinitionOption` +- `WithDockerClient(dockerClient *client.Client) CustomizeDefinitionOption` +- `WithEndpointSettingsModifier(modifier func(settings map[string]*apinetwork.EndpointSettings)) CustomizeDefinitionOption` +- `WithEntrypoint(entrypoint ...string) CustomizeDefinitionOption` +- `WithEntrypointArgs(entrypointArgs ...string) CustomizeDefinitionOption` +- `WithEnv(envs map[string]string) CustomizeDefinitionOption` +- `WithExposedPorts(ports ...string) CustomizeDefinitionOption` +- `WithFiles(files ...File) CustomizeDefinitionOption` +- `WithHostConfigModifier(modifier func(hostConfig *container.HostConfig)) CustomizeDefinitionOption` +- `WithImage(image string) CustomizeDefinitionOption` +- `WithImagePlatform(platform string) CustomizeDefinitionOption` +- `WithImageSubstitutors(fn ...ImageSubstitutor) CustomizeDefinitionOption` +- `WithLabels(labels map[string]string) CustomizeDefinitionOption` +- `WithLifecycleHooks(hooks ...LifecycleHooks) CustomizeDefinitionOption` +- `WithName(containerName string) CustomizeDefinitionOption` +- `WithNetwork(aliases []string, nw *network.Network) CustomizeDefinitionOption` +- `WithNetworkName(aliases []string, networkName string) CustomizeDefinitionOption` +- `WithNewNetwork(ctx context.Context, aliases []string, opts ...network.Option) CustomizeDefinitionOption` +- `WithNoStart() CustomizeDefinitionOption` +- `WithStartupCommand(execs ...Executable) CustomizeDefinitionOption` +- `WithWaitStrategy(strategies ...wait.Strategy) CustomizeDefinitionOption` +- `WithWaitStrategyAndDeadline(deadline time.Duration, strategies ...wait.Strategy) CustomizeDefinitionOption` + +Please consider that the options using the `WithAdditional` prefix are cumulative, so you can add multiple options to customize the container definition. On the same hand, the options modifying a map are also cumulative, so you can add multiple options to modify the same map. + +For slices, the options are not cumulative, so the last option will override the previous ones. The library offers some helper functions to add elements to the slices, like `WithCmdArgs` or `WithEntrypointArgs`, making them cumulative. + +## The Container type + +The `Container` type is a struct that represents the created container. It provides methods to interact with the container, such as starting, stopping, executing commands, and accessing logs. + +### Available Methods + +The following methods are available on the `Container` type: + +#### Lifecycle Methods + +- `Start(ctx context.Context) error` - Starts the container +- `Stop(ctx context.Context, opts ...StopOption) error` - Stops the container +- `Terminate(ctx context.Context, opts ...TerminateOption) error` - Terminates and removes the container + +#### Information Methods + +- `ID() string` - Returns the container ID +- `ShortID() string` - Returns the short container ID (first 12 characters) +- `Image() string` - Returns the image used by the container +- `Host(ctx context.Context) (string, error)` - Gets the host of the docker daemon +- `Inspect(ctx context.Context) (*container.InspectResponse, error)` - Inspects the container +- `State(ctx context.Context) (*container.State, error)` - Gets the container state + +#### Network Methods + +- `ContainerIP(ctx context.Context) (string, error)` - Gets the container's IP address +- `ContainerIPs(ctx context.Context) ([]string, error)` - Gets all container IP addresses +- `NetworkAliases(ctx context.Context) (map[string][]string, error)` - Gets network aliases +- `Networks(ctx context.Context) ([]string, error)` - Gets network names + +#### Port Methods + +- `MappedPort(ctx context.Context, port nat.Port) (nat.Port, error)` - Gets the mapped port for a container's exposed port + +#### Execution Methods + +- `Exec(ctx context.Context, cmd []string, options ...exec.ProcessOption) (int, io.Reader, error)` - Executes a command in the container + +#### File Operations + +- `CopyFromContainer(ctx context.Context, containerFilePath string) (io.ReadCloser, error)` - Copies a file from the container +- `CopyToContainer(ctx context.Context, fileContent []byte, containerFilePath string, fileMode int64) error` - Copies a file to the container + +#### Logging Methods + +- `Logger() *slog.Logger` - Returns the container's logger, which is a `slog.Logger` instance, set at the Docker client level +- `Logs(ctx context.Context) (io.ReadCloser, error)` - Gets container logs diff --git a/vendor/github.com/docker/go-sdk/container/container.exec.go b/vendor/github.com/docker/go-sdk/container/container.exec.go new file mode 100644 index 00000000..0e188620 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/container.exec.go @@ -0,0 +1,63 @@ +package container + +import ( + "context" + "fmt" + "io" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/go-sdk/container/exec" +) + +// Exec executes a command in the current container. +// It returns the exit status of the executed command, an [io.Reader] containing the combined +// stdout and stderr, and any encountered error. Note that reading directly from the [io.Reader] +// may result in unexpected bytes due to custom stream multiplexing headers. +// Use [cexec.Multiplexed] option to read the combined output without the multiplexing headers. +// Alternatively, to separate the stdout and stderr from [io.Reader] and interpret these headers properly, +// [github.com/docker/docker/pkg/stdcopy.StdCopy] from the Docker API should be used. +func (c *Container) Exec(ctx context.Context, cmd []string, options ...exec.ProcessOption) (int, io.Reader, error) { + processOptions := exec.NewProcessOptions(cmd) + + // processing all the options in a first loop because for the multiplexed option + // we first need to have a containerExecCreateResponse + for _, o := range options { + o.Apply(processOptions) + } + + response, err := c.dockerClient.ContainerExecCreate(ctx, c.ID(), processOptions.ExecConfig) + if err != nil { + return 0, nil, fmt.Errorf("container exec create: %w", err) + } + + hijack, err := c.dockerClient.ContainerExecAttach(ctx, response.ID, container.ExecAttachOptions{}) + if err != nil { + return 0, nil, fmt.Errorf("container exec attach: %w", err) + } + + processOptions.Reader = hijack.Reader + + // second loop to process the multiplexed option, as now we have a reader + // from the created exec response. + for _, o := range options { + o.Apply(processOptions) + } + + var exitCode int + for { + execResp, err := c.dockerClient.ContainerExecInspect(ctx, response.ID) + if err != nil { + return 0, nil, fmt.Errorf("container exec inspect: %w", err) + } + + if !execResp.Running { + exitCode = execResp.ExitCode + break + } + + time.Sleep(100 * time.Millisecond) + } + + return exitCode, processOptions.Reader, nil +} diff --git a/vendor/github.com/docker/go-sdk/container/container.go b/vendor/github.com/docker/go-sdk/container/container.go new file mode 100644 index 00000000..a942cd2f --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/container.go @@ -0,0 +1,68 @@ +package container + +import ( + "context" + "log/slog" + + "github.com/docker/go-sdk/client" + "github.com/docker/go-sdk/container/wait" +) + +// Container represents a container +type Container struct { + dockerClient *client.Client + + // containerID the Container ID + containerID string + + // shortID the short Container ID, using the first 12 characters of the ID + shortID string + + // WaitingFor the waiting strategy to use for the container. + waitingFor wait.Strategy + + // image the image to use for the container. + image string + + // exposedPorts the ports exposed by the container. + exposedPorts []string + + // logger the logger to use for the container. + logger *slog.Logger + + // lifecycleHooks the lifecycle hooks to use for the container. + lifecycleHooks []LifecycleHooks + + // isRunning the flag to check if the container is running. + isRunning bool +} + +// DockerClient returns the docker client used by the container. +func (c *Container) DockerClient() *client.Client { + return c.dockerClient +} + +// ID returns the container ID +func (c *Container) ID() string { + return c.containerID +} + +// Image returns the image used by the container. +func (c *Container) Image() string { + return c.image +} + +// ShortID returns the short container ID, using the first 12 characters of the ID +func (c *Container) ShortID() string { + return c.shortID +} + +// Host gets host (ip or name) of the docker daemon where the container port is exposed +// Warning: this is based on your Docker host setting. Will fail if using an SSH tunnel +func (c *Container) Host(ctx context.Context) (string, error) { + host, err := c.dockerClient.DaemonHost(ctx) + if err != nil { + return "", err + } + return host, nil +} diff --git a/vendor/github.com/docker/go-sdk/container/container.logs.go b/vendor/github.com/docker/go-sdk/container/container.logs.go new file mode 100644 index 00000000..5648256e --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/container.logs.go @@ -0,0 +1,90 @@ +package container + +import ( + "bufio" + "context" + "io" + "log/slog" + + "github.com/docker/docker/api/types/container" +) + +// Logger returns the logger for the container. +func (c *Container) Logger() *slog.Logger { + return c.logger +} + +// Logs will fetch both STDOUT and STDERR from the current container. Returns a +// ReadCloser and leaves it up to the caller to extract what it wants. +func (c *Container) Logs(ctx context.Context) (io.ReadCloser, error) { + const streamHeaderSize = 8 + + options := container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + } + + rc, err := c.dockerClient.ContainerLogs(ctx, c.ID(), options) + if err != nil { + return nil, err + } + + pr, pw := io.Pipe() + r := bufio.NewReader(rc) + + go func() { + lineStarted := true + for err == nil { + line, isPrefix, err := r.ReadLine() + + if lineStarted && len(line) >= streamHeaderSize { + line = line[streamHeaderSize:] // trim stream header + lineStarted = false + } + if !isPrefix { + lineStarted = true + } + + _, errW := pw.Write(line) + if errW != nil { + return + } + + if !isPrefix { + _, errW := pw.Write([]byte("\n")) + if errW != nil { + return + } + } + + if err != nil { + _ = pw.CloseWithError(err) + return + } + } + }() + + return pr, nil +} + +// printLogs is a helper function that will print the logs of a Docker container +// We are going to use this helper function to inform the user of the logs when an error occurs +func (c *Container) printLogs(ctx context.Context, cause error) { + reader, err := c.Logs(ctx) + if err != nil { + c.logger.Error("failed accessing container logs", "error", err) + return + } + + b, err := io.ReadAll(reader) + if err != nil { + if len(b) > 0 { + c.logger.Error("failed reading container logs", "error", err, "cause", cause, "logs", b) + } else { + c.logger.Error("failed reading container logs", "error", err, "cause", cause) + } + return + } + + c.logger.Info("container logs", "cause", cause, "logs", b) +} diff --git a/vendor/github.com/docker/go-sdk/container/container.network.go b/vendor/github.com/docker/go-sdk/container/container.network.go new file mode 100644 index 00000000..d3c5aa55 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/container.network.go @@ -0,0 +1,78 @@ +package container + +import "context" + +// ContainerIP gets the IP address of the primary network within the container. +// If there are multiple networks, it returns an empty string. +func (c *Container) ContainerIP(ctx context.Context) (string, error) { + inspect, err := c.Inspect(ctx) + if err != nil { + return "", err + } + + ip := inspect.NetworkSettings.IPAddress + if ip == "" { + // use IP from "Networks" if only single network defined + networks := inspect.NetworkSettings.Networks + if len(networks) == 1 { + for _, v := range networks { + ip = v.IPAddress + } + } + } + + return ip, nil +} + +// ContainerIPs gets the IP addresses of all the networks within the container. +func (c *Container) ContainerIPs(ctx context.Context) ([]string, error) { + ips := make([]string, 0) + + inspect, err := c.Inspect(ctx) + if err != nil { + return nil, err + } + + networks := inspect.NetworkSettings.Networks + for _, nw := range networks { + ips = append(ips, nw.IPAddress) + } + + return ips, nil +} + +// NetworkAliases gets the aliases of the container for the networks it is attached to. +func (c *Container) NetworkAliases(ctx context.Context) (map[string][]string, error) { + inspect, err := c.Inspect(ctx) + if err != nil { + return map[string][]string{}, err + } + + networks := inspect.NetworkSettings.Networks + + a := map[string][]string{} + + for k := range networks { + a[k] = networks[k].Aliases + } + + return a, nil +} + +// Networks gets the names of the networks the container is attached to. +func (c *Container) Networks(ctx context.Context) ([]string, error) { + inspect, err := c.Inspect(ctx) + if err != nil { + return []string{}, err + } + + networks := inspect.NetworkSettings.Networks + + n := []string{} + + for k := range networks { + n = append(n, k) + } + + return n, nil +} diff --git a/vendor/github.com/docker/go-sdk/container/container.run.go b/vendor/github.com/docker/go-sdk/container/container.run.go new file mode 100644 index 00000000..79da9ae9 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/container.run.go @@ -0,0 +1,188 @@ +package container + +import ( + "context" + "fmt" + + "github.com/containerd/errdefs" + "github.com/containerd/platforms" + + "github.com/docker/docker/api/types/container" + apiimage "github.com/docker/docker/api/types/image" + apinetwork "github.com/docker/docker/api/types/network" + "github.com/docker/go-sdk/client" + "github.com/docker/go-sdk/image" +) + +// Run is a convenience function that creates a new container and starts it. +// By default, the container is started after creation, unless requested otherwise +// using the [WithNoStart] option. +func Run(ctx context.Context, opts ...ContainerCustomizer) (*Container, error) { + def := Definition{ + env: make(map[string]string), + started: true, + } + + for _, opt := range opts { + if err := opt.Customize(&def); err != nil { + return nil, fmt.Errorf("customize: %w", err) + } + } + + if err := def.validate(); err != nil { + return nil, fmt.Errorf("validate: %w", err) + } + + if def.dockerClient == nil { + // use the default docker client + def.dockerClient = client.DefaultClient + } + + env := []string{} + for envKey, envVar := range def.env { + env = append(env, envKey+"="+envVar) + } + + if def.labels == nil { + def.labels = make(map[string]string) + } + + defaultHooks := []LifecycleHooks{ + DefaultLoggingHook, + } + + for _, is := range def.imageSubstitutors { + modifiedTag, err := is.Substitute(def.image) + if err != nil { + return nil, fmt.Errorf("failed to substitute image %s with %s: %w", def.image, is.Description(), err) + } + + if modifiedTag != def.image { + def.dockerClient.Logger().Info("Replacing image", "description", is.Description(), "from", def.image, "to", modifiedTag) + def.image = modifiedTag + } + } + + var platform *platforms.Platform + + if def.imagePlatform != "" { + p, err := platforms.Parse(def.imagePlatform) + if err != nil { + return nil, fmt.Errorf("invalid platform %s: %w", def.imagePlatform, err) + } + platform = &p + } + + var shouldPullImage bool + + if def.alwaysPullImage { + shouldPullImage = true // If requested always attempt to pull image + } else { + img, err := def.dockerClient.ImageInspect(ctx, def.image) + if err != nil { + if !errdefs.IsNotFound(err) { + return nil, err + } + shouldPullImage = true + } + if platform != nil && (img.Architecture != platform.Architecture || img.Os != platform.OS) { + shouldPullImage = true + } + } + + if shouldPullImage { + pullOpt := apiimage.PullOptions{ + Platform: def.imagePlatform, // may be empty + } + if err := image.Pull(ctx, def.image, image.WithPullClient(def.dockerClient), image.WithPullOptions(pullOpt)); err != nil { + return nil, err + } + } + + // Add the labels that identify this as a container created by the SDK. + client.AddSDKLabels(def.labels) + + dockerInput := &container.Config{ + Entrypoint: def.entrypoint, + Image: def.image, + Env: env, + Labels: def.labels, + Cmd: def.cmd, + } + + hostConfig := &container.HostConfig{} + + networkingConfig := &apinetwork.NetworkingConfig{} + + // default hooks include logger hook and pre-create hook + defaultHooks = append(defaultHooks, + defaultPreCreateHook(def.dockerClient, dockerInput, hostConfig, networkingConfig), + defaultCopyFileToContainerHook(def.files), + defaultReadinessHook(), + ) + + // Combine with the original LifecycleHooks to avoid duplicate logging hooks. + origLifecycleHooks := def.lifecycleHooks + def.lifecycleHooks = []LifecycleHooks{ + combineContainerHooks(defaultHooks, origLifecycleHooks), + } + + err := def.creatingHook(ctx) + if err != nil { + return nil, err + } + + resp, err := def.dockerClient.ContainerCreate(ctx, dockerInput, hostConfig, networkingConfig, platform, def.name) + if err != nil { + return nil, fmt.Errorf("container create: %w", err) + } + + // This should match the fields set in ContainerFromDockerResponse. + ctr := &Container{ + dockerClient: def.dockerClient, + containerID: resp.ID, + shortID: resp.ID[:12], + waitingFor: def.waitingFor, + image: def.image, + exposedPorts: def.exposedPorts, + logger: def.dockerClient.Logger(), + lifecycleHooks: def.lifecycleHooks, + } + + // Note: `ctr.dockerClient` is the same instance as `def.dockerClient`. + // The switch is intentional to emphasize that operations are now being performed + // on the container object (`ctr`) rather than the definition object (`def`). + + // If there is more than one network specified in the request attach newly created container to them one by one + if len(def.networks) > 1 { + for _, n := range def.networks[1:] { + nwInspect, err := ctr.dockerClient.NetworkInspect(ctx, n, apinetwork.InspectOptions{ + Verbose: true, + }) + if err != nil { + return ctr, fmt.Errorf("network inspect: %w", err) + } + + endpointSetting := apinetwork.EndpointSettings{ + Aliases: def.networkAliases[n], + } + err = ctr.dockerClient.NetworkConnect(ctx, nwInspect.ID, resp.ID, &endpointSetting) + if err != nil { + return ctr, fmt.Errorf("network connect: %w", err) + } + } + } + + if err = ctr.createdHook(ctx); err != nil { + // Return the container to allow caller to clean up. + return ctr, fmt.Errorf("created hook: %w", err) + } + + if def.started { + if err := ctr.Start(ctx); err != nil { + return ctr, fmt.Errorf("start container: %w", err) + } + } + + return ctr, nil +} diff --git a/vendor/github.com/docker/go-sdk/container/container.start.go b/vendor/github.com/docker/go-sdk/container/container.start.go new file mode 100644 index 00000000..d9040b7b --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/container.start.go @@ -0,0 +1,35 @@ +package container + +import ( + "context" + "fmt" + + "github.com/docker/docker/api/types/container" +) + +// Start will start an already created container +func (c *Container) Start(ctx context.Context) error { + err := c.startingHook(ctx) + if err != nil { + return fmt.Errorf("starting hook: %w", err) + } + + if err := c.dockerClient.ContainerStart(ctx, c.ID(), container.StartOptions{}); err != nil { + return fmt.Errorf("container start: %w", err) + } + defer c.dockerClient.Close() + + err = c.startedHook(ctx) + if err != nil { + return fmt.Errorf("started hook: %w", err) + } + + c.isRunning = true + + err = c.readiedHook(ctx) + if err != nil { + return fmt.Errorf("readied hook: %w", err) + } + + return nil +} diff --git a/vendor/github.com/docker/go-sdk/container/container.stop.go b/vendor/github.com/docker/go-sdk/container/container.stop.go new file mode 100644 index 00000000..45b74859 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/container.stop.go @@ -0,0 +1,90 @@ +package container + +import ( + "context" + "fmt" + "time" + + "github.com/docker/docker/api/types/container" +) + +// StopOptions is a type that holds the options for stopping a container. +type StopOptions struct { + ctx context.Context + stopTimeout time.Duration +} + +// StopOption is a type that represents an option for stopping a container. +type StopOption func(*StopOptions) + +// Context returns the context to use during a Stop or Terminate. +func (o *StopOptions) Context() context.Context { + return o.ctx +} + +// StopTimeout returns the stop timeout to use during a Stop or Terminate. +func (o *StopOptions) StopTimeout() time.Duration { + return o.stopTimeout +} + +// StopTimeout returns a StopOption that sets the timeout. +// Default: See [Container.Stop]. +func StopTimeout(timeout time.Duration) StopOption { + return func(c *StopOptions) { + c.stopTimeout = timeout + } +} + +// NewStopOptions returns a fully initialised StopOptions. +// Defaults: StopTimeout: 10 seconds. +func NewStopOptions(ctx context.Context, opts ...StopOption) *StopOptions { + options := &StopOptions{ + stopTimeout: time.Second * 10, + ctx: ctx, + } + for _, opt := range opts { + opt(options) + } + return options +} + +// Stop stops the container. +// +// In case the container fails to stop gracefully within a time frame specified +// by the timeout argument, it is forcefully terminated (killed). +// +// If no timeout is passed, the default StopTimeout value is used, 10 seconds, +// otherwise the engine default. A negative timeout value can be specified, +// meaning no timeout, i.e. no forceful termination is performed. +// +// All hooks are called in the following order: +// - [LifecycleHooks.PreStops] +// - [LifecycleHooks.PostStops] +// +// If the container is already stopped, the method is a no-op. +func (c *Container) Stop(ctx context.Context, opts ...StopOption) error { + stopOptions := NewStopOptions(ctx, opts...) + + err := c.stoppingHook(stopOptions.Context()) + if err != nil { + return fmt.Errorf("stopping hook: %w", err) + } + + var options container.StopOptions + + timeoutSeconds := int(stopOptions.StopTimeout().Seconds()) + options.Timeout = &timeoutSeconds + + if err := c.dockerClient.ContainerStop(stopOptions.Context(), c.ID(), options); err != nil { + return fmt.Errorf("container stop: %w", err) + } + + c.isRunning = false + + err = c.stoppedHook(stopOptions.Context()) + if err != nil { + return fmt.Errorf("stopped hook: %w", err) + } + + return nil +} diff --git a/vendor/github.com/docker/go-sdk/container/container.terminate.go b/vendor/github.com/docker/go-sdk/container/container.terminate.go new file mode 100644 index 00000000..7e55d583 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/container.terminate.go @@ -0,0 +1,140 @@ +package container + +import ( + "context" + "errors" + "fmt" + "reflect" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/go-sdk/client" +) + +// TerminableContainer is a container that can be terminated. +type TerminableContainer interface { + Terminate(ctx context.Context, opts ...TerminateOption) error +} + +// TerminateOptions is a type that holds the options for terminating a container. +type TerminateOptions struct { + *StopOptions + volumes []string +} + +// TerminateOption is a type that represents an option for terminating a container. +type TerminateOption func(*TerminateOptions) + +// NewTerminateOptions returns a fully initialised TerminateOptions. +// Defaults: StopTimeout: 10 seconds. +func NewTerminateOptions(ctx context.Context, opts ...TerminateOption) *TerminateOptions { + options := &TerminateOptions{ + StopOptions: NewStopOptions(ctx), + } + for _, opt := range opts { + opt(options) + } + return options +} + +// Cleanup performs any clean up needed +func (o *TerminateOptions) Cleanup(cli *client.Client) error { + if len(o.volumes) == 0 { + return nil + } + + // Best effort to remove all volumes. + var errs []error + for _, volume := range o.volumes { + if errRemove := cli.VolumeRemove(o.ctx, volume, true); errRemove != nil { + errs = append(errs, fmt.Errorf("volume remove %q: %w", volume, errRemove)) + } + } + return errors.Join(errs...) +} + +// TerminateTimeout returns a TerminateOption that sets the timeout. +// Default: See [Container.Stop]. +func TerminateTimeout(timeout time.Duration) TerminateOption { + return func(c *TerminateOptions) { + c.stopTimeout = timeout + } +} + +// RemoveVolumes returns a TerminateOption that sets additional volumes to remove. +// This is useful when the container creates named volumes that should be removed +// which are not removed by default. +// Default: nil. +func RemoveVolumes(volumes ...string) TerminateOption { + return func(c *TerminateOptions) { + c.volumes = volumes + } +} + +// Terminate calls [TerminableContainer.Terminate] on the container if it is not nil. +// +// This should be called as a defer directly after [Create](...) +// to ensure the container is terminated when the function ends. +func Terminate(ctr TerminableContainer, options ...TerminateOption) error { + if isNil(ctr) { + return nil + } + + err := ctr.Terminate(context.Background(), options...) + if !isCleanupSafe(err) { + return fmt.Errorf("terminate: %w", err) + } + + return nil +} + +// isNil returns true if val is nil or a nil instance false otherwise. +func isNil(val any) bool { + if val == nil { + return true + } + + valueOf := reflect.ValueOf(val) + switch valueOf.Kind() { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice: + return valueOf.IsNil() + default: + return false + } +} + +// Terminate calls stops and then removes the container including its volumes. +// If its image was built it and all child images are also removed unless +// the [FromDockerfile.KeepImage] on the [ContainerRequest] was set to true. +// +// The following hooks are called in order: +// - [LifecycleHooks.PreTerminates] +// - [LifecycleHooks.PostTerminates] +// +// Default: timeout is 10 seconds. +func (c *Container) Terminate(ctx context.Context, opts ...TerminateOption) error { + options := NewTerminateOptions(ctx, opts...) + err := c.Stop(options.Context(), StopTimeout(options.StopTimeout())) + if err != nil && !isCleanupSafe(err) { + return fmt.Errorf("stop: %w", err) + } + + // TODO: Handle errors from ContainerRemove more correctly, e.g. should we + // run the terminated hook? + errs := []error{ + c.terminatingHook(ctx), + c.dockerClient.ContainerRemove(ctx, c.ID(), container.RemoveOptions{ + RemoveVolumes: true, + Force: true, + }), + c.terminatedHook(ctx), + } + + c.isRunning = false + + if err = options.Cleanup(c.dockerClient); err != nil { + errs = append(errs, err) + } + + return errors.Join(errs...) +} diff --git a/vendor/github.com/docker/go-sdk/container/definition.go b/vendor/github.com/docker/go-sdk/container/definition.go new file mode 100644 index 00000000..d9613e68 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/definition.go @@ -0,0 +1,141 @@ +package container + +import ( + "errors" + "fmt" + "strings" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + "github.com/docker/go-sdk/client" + "github.com/docker/go-sdk/container/wait" +) + +var ( + // ErrDuplicateMountTarget is returned when a duplicate mount target is detected. + ErrDuplicateMountTarget = errors.New("duplicate mount target detected") + + // ErrInvalidBindMount is returned when an invalid bind mount is detected. + ErrInvalidBindMount = errors.New("invalid bind mount") +) + +// Definition is the definition of a container. +type Definition struct { + // dockerClient the docker client to use for the container. + dockerClient *client.Client + + // configModifier the modifier for the config before container creation + configModifier func(*container.Config) + + // cmd the command to use for the container. + cmd []string + + // endpointSettingsModifier the modifier for the network settings before container creation + endpointSettingsModifier func(map[string]*network.EndpointSettings) + + // entrypoint the entrypoint to use for the container. + entrypoint []string + + // env the environment variables to use for the container. + env map[string]string + + // files the files to be copied when container starts + files []File + + // hostConfigModifier the modifier for the host config before container creation + hostConfigModifier func(*container.HostConfig) + + // imageSubstitutors the image substitutors to use for the container. + imageSubstitutors []ImageSubstitutor + + // labels the labels to use for the container. + labels map[string]string + + // lifecycleHooks the hooks to be executed during container lifecycle + lifecycleHooks []LifecycleHooks + + // networkAliases the network aliases to use for the container. + networkAliases map[string][]string + + // networks the networks to use for the container. + networks []string + + // waitingFor the waiting strategy to use for the container. + waitingFor wait.Strategy + + // exposedPorts the ports exposed by the container. + exposedPorts []string + + // image the image to use for the container. + image string + + // imagePlatform the platform of the image + imagePlatform string + + // name the name of the container. + name string + + // alwaysPullImage whether to always pull the image + alwaysPullImage bool + + // started whether to auto-start the container. + started bool +} + +// validate validates the definition. +func (d *Definition) validate() error { + if d.image == "" { + return errors.New("image is required") + } + + if err := d.validateMounts(); err != nil { + return fmt.Errorf("validate mounts: %w", err) + } + + return nil +} + +// DockerClient returns the docker client used by the definition. +func (d *Definition) DockerClient() *client.Client { + return d.dockerClient +} + +// Image returns the image used by the definition. +func (d *Definition) Image() string { + return d.image +} + +// Name returns the name of the container. +func (d *Definition) Name() string { + return d.name +} + +// validateMounts ensures that the mounts do not have duplicate targets. +// It will check the HostConfigModifier.Binds field. +func (d *Definition) validateMounts() error { + targets := make(map[string]bool, 0) + + if d.hostConfigModifier == nil { + return nil + } + + hostConfig := container.HostConfig{} + + d.hostConfigModifier(&hostConfig) + + if len(hostConfig.Binds) > 0 { + for _, bind := range hostConfig.Binds { + parts := strings.Split(bind, ":") + if len(parts) != 2 && len(parts) != 3 { + return fmt.Errorf("%w: %s", ErrInvalidBindMount, bind) + } + targetPath := parts[1] + if targets[targetPath] { + return fmt.Errorf("%w: %s", ErrDuplicateMountTarget, targetPath) + } + targets[targetPath] = true + } + } + + return nil +} diff --git a/vendor/github.com/docker/go-sdk/container/exec/processor.go b/vendor/github.com/docker/go-sdk/container/exec/processor.go new file mode 100644 index 00000000..48e775c8 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/exec/processor.go @@ -0,0 +1,140 @@ +package exec + +import ( + "bytes" + "fmt" + "io" + "sync" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/pkg/stdcopy" +) + +// ProcessOptions defines options applicable to the reader processor +type ProcessOptions struct { + ExecConfig container.ExecOptions + Reader io.Reader +} + +// NewProcessOptions returns a new ProcessOptions instance +// with the given command and default options: +// - detach: false +// - attach stdout: true +// - attach stderr: true +func NewProcessOptions(cmd []string) *ProcessOptions { + return &ProcessOptions{ + ExecConfig: container.ExecOptions{ + Cmd: cmd, + AttachStdout: true, + AttachStderr: true, + }, + } +} + +// ProcessOption defines a common interface to modify the reader processor +// These options can be passed to the Exec function in a variadic way to customize the returned Reader instance +type ProcessOption interface { + Apply(opts *ProcessOptions) +} + +// ProcessOptionFunc is a function that implements the ProcessOption interface +type ProcessOptionFunc func(opts *ProcessOptions) + +// Apply applies the ProcessOption to the ProcessOptions +func (fn ProcessOptionFunc) Apply(opts *ProcessOptions) { + fn(opts) +} + +// WithUser sets the user for the exec command +func WithUser(user string) ProcessOption { + return ProcessOptionFunc(func(opts *ProcessOptions) { + opts.ExecConfig.User = user + }) +} + +// WithWorkingDir sets the working directory for the exec command +func WithWorkingDir(workingDir string) ProcessOption { + return ProcessOptionFunc(func(opts *ProcessOptions) { + opts.ExecConfig.WorkingDir = workingDir + }) +} + +// WithEnv sets the environment variables for the exec command +func WithEnv(env []string) ProcessOption { + return ProcessOptionFunc(func(opts *ProcessOptions) { + opts.ExecConfig.Env = env + }) +} + +// WithTTY sets the TTY for the exec command +func WithTTY(tty bool) ProcessOption { + return ProcessOptionFunc(func(opts *ProcessOptions) { + opts.ExecConfig.Tty = tty + }) +} + +// safeBuffer is a goroutine safe buffer. +type safeBuffer struct { + mtx sync.Mutex + buf bytes.Buffer + err error +} + +// Error sets an error for the next read. +func (sb *safeBuffer) Error(err error) { + sb.mtx.Lock() + defer sb.mtx.Unlock() + + sb.err = err +} + +// Write writes p to the buffer. +// It is safe for concurrent use by multiple goroutines. +func (sb *safeBuffer) Write(p []byte) (n int, err error) { + sb.mtx.Lock() + defer sb.mtx.Unlock() + + return sb.buf.Write(p) +} + +// Read reads up to len(p) bytes into p from the buffer. +// It is safe for concurrent use by multiple goroutines. +func (sb *safeBuffer) Read(p []byte) (n int, err error) { + sb.mtx.Lock() + defer sb.mtx.Unlock() + + if sb.err != nil { + return 0, sb.err + } + + return sb.buf.Read(p) +} + +// Multiplexed returns a [ProcessOption] that configures the command execution +// to combine stdout and stderr into a single stream without Docker's multiplexing headers. +func Multiplexed() ProcessOption { + return ProcessOptionFunc(func(opts *ProcessOptions) { + // returning fast to bypass those options with a nil reader, + // which could be the case when other options are used + // to configure the exec creation. + if opts.Reader == nil { + return + } + + done := make(chan struct{}) + + var outBuff safeBuffer + var errBuff safeBuffer + go func() { + defer close(done) + if _, err := stdcopy.StdCopy(&outBuff, &errBuff, opts.Reader); err != nil { + outBuff.Error(fmt.Errorf("copying output: %w", err)) + return + } + }() + + <-done + + opts.Reader = io.MultiReader(&outBuff, &errBuff) + }) +} diff --git a/vendor/github.com/docker/go-sdk/container/exec/types.go b/vendor/github.com/docker/go-sdk/container/exec/types.go new file mode 100644 index 00000000..7aa17efd --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/exec/types.go @@ -0,0 +1,31 @@ +package exec + +// ExecOptions is a struct that provides a default implementation for the Options method +// of the Executable interface. +type ExecOptions struct { + opts []ProcessOption +} + +func (ce ExecOptions) Options() []ProcessOption { + return ce.opts +} + +// RawCommand is a type that implements Executable and represents a command to be sent to a container +type RawCommand struct { + ExecOptions + cmds []string +} + +func NewRawCommand(cmds []string, opts ...ProcessOption) RawCommand { + return RawCommand{ + cmds: cmds, + ExecOptions: ExecOptions{ + opts: opts, + }, + } +} + +// AsCommand returns the command as a slice of strings +func (r RawCommand) AsCommand() []string { + return r.cmds +} diff --git a/vendor/github.com/docker/go-sdk/container/files.go b/vendor/github.com/docker/go-sdk/container/files.go new file mode 100644 index 00000000..d3b59946 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/files.go @@ -0,0 +1,268 @@ +package container + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "errors" + "fmt" + "io" + "log/slog" + "os" + "path/filepath" + "strings" + + "github.com/docker/docker/api/types/container" +) + +// File represents a file that will be copied when container starts +type File struct { + // Reader the reader to read the file from. + // It takes precedence over [HostPath]. + Reader io.Reader + + // HostPath the path to the file on the host. + // If [Reader] is not specified, the file will be read from the host path. + HostPath string + + // ContainerPath the path to the file in the container. + // Use the slash character that matches the path separator of the operating system + // for the container. + ContainerPath string + + // Mode the mode of the file + Mode int64 +} + +// validate validates the [File] +func (f *File) validate() error { + if f.Reader == nil && f.HostPath == "" { + return errors.New("reader or host path must be specified") + } + + if f.ContainerPath == "" { + return errors.New("container path must be specified") + } + + return nil +} + +// FileFromContainer implements io.ReadCloser and tar.Reader for a single file in a container. +type FileFromContainer struct { + underlying *io.ReadCloser + tarreader *tar.Reader +} + +// Read reads the file from the container. +func (fc *FileFromContainer) Read(b []byte) (int, error) { + return (*fc.tarreader).Read(b) +} + +// Close closes the file from the container. +func (fc *FileFromContainer) Close() error { + return (*fc.underlying).Close() +} + +// CopyFromContainer copies a file from the container to the local filesystem. +func (c *Container) CopyFromContainer(ctx context.Context, containerFilePath string) (io.ReadCloser, error) { + r, _, err := c.dockerClient.CopyFromContainer(ctx, c.ID(), containerFilePath) + if err != nil { + return nil, err + } + + tarReader := tar.NewReader(r) + + // if we got here we have exactly one file in the TAR-stream + // so we advance the index by one so the next call to Read will start reading it + _, err = tarReader.Next() + if err != nil { + return nil, err + } + + ret := &FileFromContainer{ + underlying: &r, + tarreader: tarReader, + } + + return ret, nil +} + +// CopyDirToContainer copies a directory to a container, using the parent directory +// of the container path as the target directory. +func (c *Container) CopyDirToContainer(ctx context.Context, hostDirPath string, containerFilePath string, fileMode int64) error { + dir, err := isDir(hostDirPath) + if err != nil { + return fmt.Errorf("is dir: %w", err) + } + + if !dir { + // it's not a dir: let the consumer handle the error + return fmt.Errorf("path %s is not a directory", hostDirPath) + } + + buffer, err := tarDir(c.logger, hostDirPath, fileMode) + if err != nil { + return fmt.Errorf("tar dir: %w", err) + } + + // create the directory under its parent + parent := filepath.Dir(containerFilePath) + + err = c.dockerClient.CopyToContainer(ctx, c.ID(), parent, buffer, container.CopyToContainerOptions{}) + if err != nil { + return fmt.Errorf("copy to container: %w", err) + } + + return nil +} + +// CopyToContainer copies fileContent data to a file in container +func (c *Container) CopyToContainer(ctx context.Context, fileContent []byte, containerFilePath string, fileMode int64) error { + contentFn := func(tw io.Writer) error { + _, err := tw.Write(fileContent) + return err + } + + buffer, err := tarFile(containerFilePath, contentFn, int64(len(fileContent)), fileMode) + if err != nil { + return fmt.Errorf("tar file: %w", err) + } + + err = c.dockerClient.CopyToContainer(ctx, c.ID(), "/", buffer, container.CopyToContainerOptions{}) + if err != nil { + return fmt.Errorf("copy to container: %w", err) + } + + return nil +} + +// isDir checks if a path is a directory +func isDir(path string) (bool, error) { + file, err := os.Open(path) + if err != nil { + return false, err + } + defer file.Close() + + fileInfo, err := file.Stat() + if err != nil { + return false, err + } + + if fileInfo.IsDir() { + return true, nil + } + + return false, nil +} + +// tarDir compress a directory using tar + gzip algorithms +func tarDir(logger *slog.Logger, src string, fileMode int64) (*bytes.Buffer, error) { + // always pass src as absolute path + abs, err := filepath.Abs(src) + if err != nil { + return &bytes.Buffer{}, fmt.Errorf("error getting absolute path: %w", err) + } + src = abs + + buffer := &bytes.Buffer{} + + logger.Info("creating TAR file", "src", src) + + // tar > gzip > buffer + zr := gzip.NewWriter(buffer) + tw := tar.NewWriter(zr) + + _, baseDir := filepath.Split(src) + // keep the path relative to the parent directory + index := strings.LastIndex(src, baseDir) + + // walk through every file in the folder + err = filepath.Walk(src, func(file string, fi os.FileInfo, errFn error) error { + if errFn != nil { + return fmt.Errorf("walk the file system: %w", errFn) + } + + // if a symlink, skip file + if fi.Mode().Type() == os.ModeSymlink { + logger.Warn("skipping symlink", "file", file) + return nil + } + + // generate tar header + header, err := tar.FileInfoHeader(fi, file) + if err != nil { + return fmt.Errorf("file info header: %w", err) + } + + // see https://pkg.go.dev/archive/tar#FileInfoHeader: + // Since fs.FileInfo's Name method only returns the base name of the file it describes, + // it may be necessary to modify Header.Name to provide the full path name of the file. + header.Name = filepath.ToSlash(file[index:]) + header.Mode = fileMode + + // write header + if err := tw.WriteHeader(header); err != nil { + return fmt.Errorf("write header: %w", err) + } + + // if not a dir, write file content + if !fi.IsDir() { + data, err := os.Open(file) + if err != nil { + return fmt.Errorf("open file: %w", err) + } + defer data.Close() + if _, err := io.Copy(tw, data); err != nil { + return fmt.Errorf("copy: %w", err) + } + } + return nil + }) + if err != nil { + return buffer, err + } + + // produce tar + if err := tw.Close(); err != nil { + return buffer, fmt.Errorf("close tar file: %w", err) + } + // produce gzip + if err := zr.Close(); err != nil { + return buffer, fmt.Errorf("close gzip file: %w", err) + } + + return buffer, nil +} + +// tarFile compress a single file using tar + gzip algorithms +func tarFile(basePath string, fileContent func(tw io.Writer) error, fileContentSize int64, fileMode int64) (*bytes.Buffer, error) { + buffer := &bytes.Buffer{} + + zr := gzip.NewWriter(buffer) + tw := tar.NewWriter(zr) + + hdr := &tar.Header{ + Name: basePath, + Mode: fileMode, + Size: fileContentSize, + } + if err := tw.WriteHeader(hdr); err != nil { + return buffer, fmt.Errorf("write header: %w", err) + } + if err := fileContent(tw); err != nil { + return buffer, fmt.Errorf("write file content: %w", err) + } + + // produce tar + if err := tw.Close(); err != nil { + return buffer, fmt.Errorf("close tar file: %w", err) + } + // produce gzip + if err := zr.Close(); err != nil { + return buffer, fmt.Errorf("close gzip file: %w", err) + } + + return buffer, nil +} diff --git a/vendor/github.com/docker/go-sdk/container/generate.go b/vendor/github.com/docker/go-sdk/container/generate.go new file mode 100644 index 00000000..82214060 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/generate.go @@ -0,0 +1,3 @@ +package container + +//go:generate mockery diff --git a/vendor/github.com/docker/go-sdk/container/image_substitutors.go b/vendor/github.com/docker/go-sdk/container/image_substitutors.go new file mode 100644 index 00000000..779d812d --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/image_substitutors.go @@ -0,0 +1,115 @@ +package container + +import ( + "fmt" + "net/url" + + "github.com/docker/go-sdk/config/auth" +) + +// ImageSubstitutor represents a way to substitute container image names +type ImageSubstitutor interface { + // Description returns the name of the type and a short description of how it modifies the image. + // Useful to be printed in logs + Description() string + Substitute(image string) (string, error) +} + +// CustomHubSubstitutor represents a way to substitute the hub of an image with a custom one, +// using provided value with respect to the HubImageNamePrefix configuration value. +type CustomHubSubstitutor struct { + hub string +} + +// NewCustomHubSubstitutor creates a new CustomHubSubstitutor +func NewCustomHubSubstitutor(hub string) CustomHubSubstitutor { + return CustomHubSubstitutor{ + hub: hub, + } +} + +// Description returns the name of the type and a short description of how it modifies the image. +func (c CustomHubSubstitutor) Description() string { + return fmt.Sprintf("CustomHubSubstitutor (replaces hub with %s)", c.hub) +} + +// Substitute replaces the hub of the image with the provided one, with certain conditions: +// - if the hub is empty, the image is returned as is. +// - if the image already contains a registry, the image is returned as is. +func (c CustomHubSubstitutor) Substitute(image string) (string, error) { + ref, err := auth.ParseImageRef(image) + if err != nil { + return "", err + } + + registry := ref.Registry + + exclusions := []func() bool{ + func() bool { return c.hub == "" }, + func() bool { return registry != auth.IndexDockerIO }, + } + + for _, exclusion := range exclusions { + if exclusion() { + return image, nil + } + } + + result, err := url.JoinPath(c.hub, image) + if err != nil { + return "", err + } + + return result, nil +} + +// prependHubRegistry represents a way to prepend a custom Hub registry to the image name, +// using the HubImageNamePrefix configuration value +type prependHubRegistry struct { + prefix string +} + +// newPrependHubRegistry creates a new prependHubRegistry +func newPrependHubRegistry(hubPrefix string) prependHubRegistry { + return prependHubRegistry{ + prefix: hubPrefix, + } +} + +// Description returns the name of the type and a short description of how it modifies the image. +func (p prependHubRegistry) Description() string { + return fmt.Sprintf("HubImageSubstitutor (prepends %s)", p.prefix) +} + +// Substitute prepends the Hub prefix to the image name, with certain conditions: +// - if the prefix is empty, the image is returned as is. +// - if the image is a non-hub image (e.g. where another registry is set), the image is returned as is. +// - if the image is a Docker Hub image where the hub registry is explicitly part of the name +// (i.e. anything with a registry.hub.docker.com host part), the image is returned as is. +func (p prependHubRegistry) Substitute(image string) (string, error) { + ref, err := auth.ParseImageRef(image) + if err != nil { + return "", err + } + + registry := ref.Registry + + // add the exclusions in the right order + exclusions := []func() bool{ + func() bool { return p.prefix == "" }, // no prefix set at the configuration level + func() bool { return registry != auth.IndexDockerIO }, // explicitly including Docker's URLs + } + + for _, exclusion := range exclusions { + if exclusion() { + return image, nil + } + } + + result, err := url.JoinPath(p.prefix, image) + if err != nil { + return "", err + } + + return result, nil +} diff --git a/vendor/github.com/docker/go-sdk/container/inspect.go b/vendor/github.com/docker/go-sdk/container/inspect.go new file mode 100644 index 00000000..0a209fea --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/inspect.go @@ -0,0 +1,27 @@ +package container + +import ( + "context" + + "github.com/docker/docker/api/types/container" +) + +// Inspect returns the container's raw info +func (c *Container) Inspect(ctx context.Context) (*container.InspectResponse, error) { + inspect, err := c.dockerClient.ContainerInspect(ctx, c.ID()) + if err != nil { + return nil, err + } + + return &inspect, nil +} + +// State returns container's running state. +func (c *Container) State(ctx context.Context) (*container.State, error) { + inspect, err := c.Inspect(ctx) + if err != nil { + return nil, err + } + + return inspect.State, nil +} diff --git a/vendor/github.com/docker/go-sdk/container/lifecycle.create.go b/vendor/github.com/docker/go-sdk/container/lifecycle.create.go new file mode 100644 index 00000000..febe3af4 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/lifecycle.create.go @@ -0,0 +1,198 @@ +package container + +import ( + "context" + "fmt" + "io" + "os" + "strings" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + "github.com/docker/go-connections/nat" + "github.com/docker/go-sdk/client" +) + +// defaultPreCreateHook is a hook that will apply the default configuration to the container +var defaultPreCreateHook = func(dockerClient *client.Client, dockerInput *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig) LifecycleHooks { + return LifecycleHooks{ + PreCreates: []DefinitionHook{ + func(ctx context.Context, def *Definition) error { + return preCreateContainerHook(ctx, dockerClient, def, dockerInput, hostConfig, networkingConfig) + }, + }, + } +} + +// defaultCopyFileToContainerHook is a hook that will copy files to the container after it's created +// but before it's started +var defaultCopyFileToContainerHook = func(files []File) LifecycleHooks { + return LifecycleHooks{ + PostCreates: []ContainerHook{ + // copy files to container after it's created + func(ctx context.Context, c *Container) error { + for _, f := range files { + if err := f.validate(); err != nil { + return fmt.Errorf("invalid file: %w", err) + } + + var bs []byte + var err error + if f.Reader != nil { + // Bytes takes precedence over HostFilePath + bs, err = io.ReadAll(f.Reader) + if err != nil { + return fmt.Errorf("read all: %w", err) + } + } else { + // no reader, read from host path, checking if it's a directory first + ok, err := isDir(f.HostPath) + if err != nil { + return err + } + + if ok { + err := c.CopyDirToContainer(ctx, f.HostPath, f.ContainerPath, f.Mode) + if err != nil { + return fmt.Errorf("copy dir to container: %w", err) + } + continue + } + + bs, err = os.ReadFile(f.HostPath) + if err != nil { + return fmt.Errorf("read file: %w", err) + } + } + + err = c.CopyToContainer(ctx, bs, f.ContainerPath, f.Mode) + if err != nil { + return fmt.Errorf("copy to container at %s: %w", f.ContainerPath, err) + } + } + + return nil + }, + }, + } +} + +// defaultReadinessHook is a hook that will wait for the container to be ready +var defaultReadinessHook = func() LifecycleHooks { + return LifecycleHooks{ + PostStarts: []ContainerHook{ + // wait for the container to be ready + func(ctx context.Context, c *Container) error { + // if a Wait Strategy has been specified, wait before returning + if c.waitingFor != nil { + c.logger.Info("Waiting for container to be ready", "containerID", c.ShortID(), "image", c.Image()) + if err := c.waitingFor.WaitUntilReady(ctx, c); err != nil { + return fmt.Errorf("wait until ready: %w", err) + } + } + + c.isRunning = true + + return nil + }, + }, + } +} + +// creatingHook is a hook that will be called before a container is created. +func (def *Definition) creatingHook(ctx context.Context) error { + return def.applyLifecycleHooks(func(lifecycleHooks LifecycleHooks) error { + return applyDefinitionHooks(ctx, lifecycleHooks.PreCreates, def) + }) +} + +// createdHook is a hook that will be called after a container is created. +func (c *Container) createdHook(ctx context.Context) error { + return c.applyLifecycleHooks(ctx, false, func(lifecycleHooks LifecycleHooks) error { + return applyContainerHooks(ctx, lifecycleHooks.PostCreates, c) + }) +} + +func mergePortBindings(configPortMap, exposedPortMap nat.PortMap, exposedPorts []string) nat.PortMap { + if exposedPortMap == nil { + exposedPortMap = make(map[nat.Port][]nat.PortBinding) + } + + mappedPorts := make(map[string]struct{}, len(exposedPorts)) + for _, p := range exposedPorts { + p = strings.Split(p, "/")[0] + mappedPorts[p] = struct{}{} + } + + for k, v := range configPortMap { + if _, ok := mappedPorts[k.Port()]; ok { + exposedPortMap[k] = v + } + } + return exposedPortMap +} + +func preCreateContainerHook(ctx context.Context, dockerClient *client.Client, def *Definition, dockerInput *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig) error { + endpointSettings := map[string]*network.EndpointSettings{} + + // Docker allows only one network to be specified during container creation + // If there is more than one network specified in the request container should be attached to them + // once it is created. We will take a first network if any specified in the request and use it to create container + if len(def.networks) > 0 { + attachContainerTo := def.networks[0] + + nwInspect, err := dockerClient.NetworkInspect(ctx, def.networks[0], network.InspectOptions{ + Verbose: true, + }) + if err != nil { + return fmt.Errorf("network inspect: %w", err) + } + + aliases := []string{} + if _, ok := def.networkAliases[attachContainerTo]; ok { + aliases = def.networkAliases[attachContainerTo] + } + endpointSetting := network.EndpointSettings{ + Aliases: aliases, + NetworkID: nwInspect.ID, + } + endpointSettings[attachContainerTo] = &endpointSetting + + } + + if def.configModifier != nil { + def.configModifier(dockerInput) + } + + if def.hostConfigModifier != nil { + def.hostConfigModifier(hostConfig) + } + + if def.endpointSettingsModifier != nil { + def.endpointSettingsModifier(endpointSettings) + } + + networkingConfig.EndpointsConfig = endpointSettings + + exposedPorts := def.exposedPorts + // this check must be done after the pre-creation Modifiers are called, so the network mode is already set + if len(exposedPorts) == 0 && !hostConfig.NetworkMode.IsContainer() { + hostConfig.PublishAllPorts = true + } + + exposedPortSet, exposedPortMap, err := nat.ParsePortSpecs(exposedPorts) + if err != nil { + return err + } + + dockerInput.ExposedPorts = exposedPortSet + + // only exposing those ports automatically if the container request exposes zero ports and the container does not run in a container network + if len(exposedPorts) == 0 && !hostConfig.NetworkMode.IsContainer() { + hostConfig.PortBindings = exposedPortMap + } else { + hostConfig.PortBindings = mergePortBindings(hostConfig.PortBindings, exposedPortMap, def.exposedPorts) + } + + return nil +} diff --git a/vendor/github.com/docker/go-sdk/container/lifecycle.go b/vendor/github.com/docker/go-sdk/container/lifecycle.go new file mode 100644 index 00000000..f51caa0d --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/lifecycle.go @@ -0,0 +1,220 @@ +package container + +import ( + "context" + "errors" + "reflect" + "strings" + "time" +) + +type LifecycleHooks struct { + PreCreates []DefinitionHook + PostCreates []ContainerHook + PreStarts []ContainerHook + PostStarts []ContainerHook + PostReadies []ContainerHook + PreStops []ContainerHook + PostStops []ContainerHook + PreTerminates []ContainerHook + PostTerminates []ContainerHook +} + +// DefinitionHook is a hook that will be called before a container is started. +// It can be used to modify the container definition on container creation, +// using the different lifecycle hooks that are available: +// - Building +// - Creating +// For that, it will receive a Definition, modify it and return an error if needed. +type DefinitionHook func(ctx context.Context, def *Definition) error + +// ContainerHook is a hook that is called after a container is created +// It can be used to modify the state of the container after it is created, +// using the different lifecycle hooks that are available: +// - Created +// - Starting +// - Started +// - Readied +// - Stopping +// - Stopped +// - Terminating +// - Terminated +// It receives a [Container], modify it and return an error if needed. +type ContainerHook func(ctx context.Context, ctr *Container) error + +// DefaultLoggingHook is a hook that will log the container lifecycle events +var DefaultLoggingHook = LifecycleHooks{ + PreCreates: []DefinitionHook{ + func(_ context.Context, def *Definition) error { + def.dockerClient.Logger().Info("Creating container", "image", def.image) + return nil + }, + }, + PostCreates: []ContainerHook{ + func(_ context.Context, c *Container) error { + c.logger.Info("Container created", "containerID", c.shortID) + return nil + }, + }, + PreStarts: []ContainerHook{ + func(_ context.Context, c *Container) error { + c.logger.Info("Starting container", "containerID", c.shortID) + return nil + }, + }, + PostStarts: []ContainerHook{ + func(_ context.Context, c *Container) error { + c.logger.Info("Container started", "containerID", c.shortID) + return nil + }, + }, + PostReadies: []ContainerHook{ + func(_ context.Context, c *Container) error { + c.logger.Info("Container is ready", "containerID", c.shortID) + return nil + }, + }, + PreStops: []ContainerHook{ + func(_ context.Context, c *Container) error { + c.logger.Info("Stopping container", "containerID", c.shortID) + return nil + }, + }, + PostStops: []ContainerHook{ + func(_ context.Context, c *Container) error { + c.logger.Info("Container stopped", "containerID", c.shortID) + return nil + }, + }, + PreTerminates: []ContainerHook{ + func(_ context.Context, c *Container) error { + c.logger.Info("Terminating container", "containerID", c.shortID) + return nil + }, + }, + PostTerminates: []ContainerHook{ + func(_ context.Context, c *Container) error { + c.logger.Info("Container terminated", "containerID", c.shortID) + return nil + }, + }, +} + +// combineContainerHooks returns a [LifecycleHook] as the result +// of combining the default hooks with the user-defined hooks. +// +// The order of hooks is the following: +// - Pre-hooks run the default hooks first then the user-defined hooks +// - Post-hooks run the user-defined hooks first then the default hooks +// The order of execution will be: +// - default pre-hooks +// - user-defined pre-hooks +// - user-defined post-hooks +// - default post-hooks +func combineContainerHooks(defaultHooks, userDefinedHooks []LifecycleHooks) LifecycleHooks { + // We use reflection here to ensure that any new hooks are handled. + var hooks LifecycleHooks + hooksVal := reflect.ValueOf(&hooks).Elem() + hooksType := reflect.TypeOf(hooks) + for _, defaultHook := range defaultHooks { + defaultVal := reflect.ValueOf(defaultHook) + for i := range hooksType.NumField() { + if strings.HasPrefix(hooksType.Field(i).Name, "Pre") { + field := hooksVal.Field(i) + field.Set(reflect.AppendSlice(field, defaultVal.Field(i))) + } + } + } + + // Append the user-defined hooks after the default pre-hooks + // and because the post hooks are still empty, the user-defined + // post-hooks will be the first ones to be executed. + for _, userDefinedHook := range userDefinedHooks { + userVal := reflect.ValueOf(userDefinedHook) + for i := range hooksType.NumField() { + field := hooksVal.Field(i) + field.Set(reflect.AppendSlice(field, userVal.Field(i))) + } + } + + // Finally, append the default post-hooks. + for _, defaultHook := range defaultHooks { + defaultVal := reflect.ValueOf(defaultHook) + for i := range hooksType.NumField() { + if strings.HasPrefix(hooksType.Field(i).Name, "Post") { + field := hooksVal.Field(i) + field.Set(reflect.AppendSlice(field, defaultVal.Field(i))) + } + } + } + + return hooks +} + +func applyContainerHooks(ctx context.Context, hooks []ContainerHook, ctr *Container) error { + var errs []error + for _, hook := range hooks { + if err := hook(ctx, ctr); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + +func applyDefinitionHooks(ctx context.Context, hooks []DefinitionHook, def *Definition) error { + var errs []error + for _, hook := range hooks { + if err := hook(ctx, def); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + +// applyLifecycleHooks calls hook on all LifecycleHooks. +func (def *Definition) applyLifecycleHooks(hook func(lifecycleHooks LifecycleHooks) error) error { + if def.lifecycleHooks == nil { + return nil + } + + var errs []error + for _, lifecycleHooks := range def.lifecycleHooks { + if err := hook(lifecycleHooks); err != nil { + errs = append(errs, err) + } + } + + return errors.Join(errs...) +} + +// applyLifecycleHooks applies all lifecycle hooks reporting the container logs on error if logError is true. +func (c *Container) applyLifecycleHooks(ctx context.Context, logError bool, hook func(lifecycleHooks LifecycleHooks) error) error { + if c.lifecycleHooks == nil { + return nil + } + + var errs []error + for _, lifecycleHooks := range c.lifecycleHooks { + if err := hook(lifecycleHooks); err != nil { + errs = append(errs, err) + } + } + + if err := errors.Join(errs...); err != nil { + if logError { + select { + case <-ctx.Done(): + // Context has timed out so need a new context to get logs. + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + c.printLogs(ctx, err) + default: + c.printLogs(ctx, err) + } + } + + return err + } + + return nil +} diff --git a/vendor/github.com/docker/go-sdk/container/lifecycle.start.go b/vendor/github.com/docker/go-sdk/container/lifecycle.start.go new file mode 100644 index 00000000..e172f409 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/lifecycle.start.go @@ -0,0 +1,24 @@ +package container + +import "context" + +// startingHook is a hook that will be called before a container is started. +func (c *Container) startingHook(ctx context.Context) error { + return c.applyLifecycleHooks(ctx, true, func(lifecycleHooks LifecycleHooks) error { + return applyContainerHooks(ctx, lifecycleHooks.PreStarts, c) + }) +} + +// startedHook is a hook that will be called after a container is started. +func (c *Container) startedHook(ctx context.Context) error { + return c.applyLifecycleHooks(ctx, true, func(lifecycleHooks LifecycleHooks) error { + return applyContainerHooks(ctx, lifecycleHooks.PostStarts, c) + }) +} + +// readiedHook is a hook that will be called after a container is ready. +func (c *Container) readiedHook(ctx context.Context) error { + return c.applyLifecycleHooks(ctx, true, func(lifecycleHooks LifecycleHooks) error { + return applyContainerHooks(ctx, lifecycleHooks.PostReadies, c) + }) +} diff --git a/vendor/github.com/docker/go-sdk/container/lifecycle.stop.go b/vendor/github.com/docker/go-sdk/container/lifecycle.stop.go new file mode 100644 index 00000000..d910f8e8 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/lifecycle.stop.go @@ -0,0 +1,17 @@ +package container + +import "context" + +// stoppingHook is a hook that will be called before a container is stopped. +func (c *Container) stoppingHook(ctx context.Context) error { + return c.applyLifecycleHooks(ctx, false, func(lifecycleHooks LifecycleHooks) error { + return applyContainerHooks(ctx, lifecycleHooks.PreStops, c) + }) +} + +// stoppedHook is a hook that will be called after a container is stopped. +func (c *Container) stoppedHook(ctx context.Context) error { + return c.applyLifecycleHooks(ctx, false, func(lifecycleHooks LifecycleHooks) error { + return applyContainerHooks(ctx, lifecycleHooks.PostStops, c) + }) +} diff --git a/vendor/github.com/docker/go-sdk/container/lifecycle.terminate.go b/vendor/github.com/docker/go-sdk/container/lifecycle.terminate.go new file mode 100644 index 00000000..ca35d6f6 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/lifecycle.terminate.go @@ -0,0 +1,17 @@ +package container + +import "context" + +// terminatingHook is a hook that will be called before a container is terminated. +func (c *Container) terminatingHook(ctx context.Context) error { + return c.applyLifecycleHooks(ctx, false, func(lifecycleHooks LifecycleHooks) error { + return applyContainerHooks(ctx, lifecycleHooks.PreTerminates, c) + }) +} + +// stoppedHook is a hook that will be called after a container is stopped. +func (c *Container) terminatedHook(ctx context.Context) error { + return c.applyLifecycleHooks(ctx, false, func(lifecycleHooks LifecycleHooks) error { + return applyContainerHooks(ctx, lifecycleHooks.PostTerminates, c) + }) +} diff --git a/vendor/github.com/docker/go-sdk/container/options.go b/vendor/github.com/docker/go-sdk/container/options.go new file mode 100644 index 00000000..1349bb71 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/options.go @@ -0,0 +1,369 @@ +package container + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/docker/docker/api/types/container" + apinetwork "github.com/docker/docker/api/types/network" + "github.com/docker/go-sdk/client" + "github.com/docker/go-sdk/container/exec" + "github.com/docker/go-sdk/container/wait" + "github.com/docker/go-sdk/network" +) + +var ErrReuseEmptyName = errors.New("with reuse option a container name mustn't be empty") + +// ContainerCustomizer is an interface that can be used to configure the container +// definition. The passed definition is merged with the default one. +type ContainerCustomizer interface { + Customize(def *Definition) error +} + +// CustomizeDefinitionOption is a type that can be used to configure the container definition. +// The passed definition is merged with the default one. +type CustomizeDefinitionOption func(def *Definition) error + +// Customize implements the ContainerCustomizer interface. +func (opt CustomizeDefinitionOption) Customize(def *Definition) error { + return opt(def) +} + +// WithDockerClient sets the docker client for a container +func WithDockerClient(dockerClient *client.Client) CustomizeDefinitionOption { + return func(def *Definition) error { + def.dockerClient = dockerClient + + return nil + } +} + +// WithConfigModifier allows to override the default container config +func WithConfigModifier(modifier func(config *container.Config)) CustomizeDefinitionOption { + return func(def *Definition) error { + def.configModifier = modifier + + return nil + } +} + +// WithEndpointSettingsModifier allows to override the default endpoint settings +func WithEndpointSettingsModifier(modifier func(settings map[string]*apinetwork.EndpointSettings)) CustomizeDefinitionOption { + return func(def *Definition) error { + def.endpointSettingsModifier = modifier + + return nil + } +} + +// WithEnv sets the environment variables for a container. +// If the environment variable already exists, it will be overridden. +func WithEnv(envs map[string]string) CustomizeDefinitionOption { + return func(def *Definition) error { + if def.env == nil { + def.env = map[string]string{} + } + + for key, val := range envs { + def.env[key] = val + } + + return nil + } +} + +// WithHostConfigModifier allows to override the default host config +func WithHostConfigModifier(modifier func(hostConfig *container.HostConfig)) CustomizeDefinitionOption { + return func(def *Definition) error { + def.hostConfigModifier = modifier + + return nil + } +} + +// WithName will set the name of the container. +func WithName(containerName string) CustomizeDefinitionOption { + return func(def *Definition) error { + if containerName == "" { + return ErrReuseEmptyName + } + def.name = containerName + return nil + } +} + +// WithNoStart will prevent the container from being started after creation. +func WithNoStart() CustomizeDefinitionOption { + return func(def *Definition) error { + def.started = false + return nil + } +} + +// WithImage sets the image for a container +func WithImage(image string) CustomizeDefinitionOption { + return func(def *Definition) error { + def.image = image + + return nil + } +} + +// WithImageSubstitutors sets the image substitutors for a container +func WithImageSubstitutors(fn ...ImageSubstitutor) CustomizeDefinitionOption { + return func(def *Definition) error { + def.imageSubstitutors = fn + + return nil + } +} + +// WithNetwork reuses an already existing network, attaching the container to it. +// Finally it sets the network alias on that network to the given alias. +func WithNetwork(aliases []string, nw *network.Network) CustomizeDefinitionOption { + return WithNetworkName(aliases, nw.Name()) +} + +// WithNetworkName attachs a container to an already existing network, by its name. +// If the network is not "bridge", it sets the network alias on that network +// to the given alias, else, it returns an error. This is because network-scoped alias +// is supported only for containers in user defined networks. +func WithNetworkName(aliases []string, networkName string) CustomizeDefinitionOption { + return func(def *Definition) error { + if networkName == "bridge" { + return errors.New("network-scoped aliases are supported only for containers in user defined networks") + } + + // attaching to the network because it was created with success or it already existed. + def.networks = append(def.networks, networkName) + + if def.networkAliases == nil { + def.networkAliases = make(map[string][]string) + } + def.networkAliases[networkName] = aliases + + return nil + } +} + +// WithBridgeNetwork attachs a container to the "bridge" network. +// There is no need to set the network alias, as it is not supported for the "bridge" network. +func WithBridgeNetwork() CustomizeDefinitionOption { + return func(def *Definition) error { + def.networks = append(def.networks, "bridge") + return nil + } +} + +// WithNewNetwork creates a new network with random name and customizers, and attaches the container to it. +// Finally it sets the network alias on that network to the given alias. +func WithNewNetwork(ctx context.Context, aliases []string, opts ...network.Option) CustomizeDefinitionOption { + return func(def *Definition) error { + newNetwork, err := network.New(ctx, opts...) + if err != nil { + return fmt.Errorf("new network: %w", err) + } + + networkName := newNetwork.Name() + + // attaching to the network because it was created with success or it already existed. + def.networks = append(def.networks, networkName) + + if def.networkAliases == nil { + def.networkAliases = make(map[string][]string) + } + def.networkAliases[networkName] = aliases + + return nil + } +} + +// Executable represents an executable command to be sent to a container, including options, +// as part of the different lifecycle hooks. +type Executable interface { + AsCommand() []string + // Options can container two different types of options: + // - Docker's ExecConfigs (WithUser, WithWorkingDir, WithEnv, etc.) + // - SDK's ProcessOptions (i.e. Multiplexed response) + Options() []exec.ProcessOption +} + +// WithStartupCommand will execute the command representation of each Executable into the container. +// It will leverage the container lifecycle hooks to call the command right after the container +// is started. +func WithStartupCommand(execs ...Executable) CustomizeDefinitionOption { + return func(def *Definition) error { + startupCommandsHook := LifecycleHooks{ + PostStarts: []ContainerHook{}, + } + + for _, exec := range execs { + execFn := func(ctx context.Context, c *Container) error { + _, _, err := c.Exec(ctx, exec.AsCommand(), exec.Options()...) + return err + } + + startupCommandsHook.PostStarts = append(startupCommandsHook.PostStarts, execFn) + } + + def.lifecycleHooks = append(def.lifecycleHooks, startupCommandsHook) + + return nil + } +} + +// WithAfterReadyCommand will execute the command representation of each Executable into the container. +// It will leverage the container lifecycle hooks to call the command right after the container +// is ready. +func WithAfterReadyCommand(execs ...Executable) CustomizeDefinitionOption { + return func(def *Definition) error { + postReadiesHook := []ContainerHook{} + + for _, exec := range execs { + execFn := func(ctx context.Context, c *Container) error { + _, _, err := c.Exec(ctx, exec.AsCommand(), exec.Options()...) + return err + } + + postReadiesHook = append(postReadiesHook, execFn) + } + + def.lifecycleHooks = append(def.lifecycleHooks, LifecycleHooks{ + PostReadies: postReadiesHook, + }) + + return nil + } +} + +// WithWaitStrategy replaces the wait strategy for a container, using 60 seconds as deadline +func WithWaitStrategy(strategies ...wait.Strategy) CustomizeDefinitionOption { + return WithWaitStrategyAndDeadline(60*time.Second, strategies...) +} + +// WithAdditionalWaitStrategy appends the wait strategy for a container, using 60 seconds as deadline +func WithAdditionalWaitStrategy(strategies ...wait.Strategy) CustomizeDefinitionOption { + return WithAdditionalWaitStrategyAndDeadline(60*time.Second, strategies...) +} + +// WithWaitStrategyAndDeadline replaces the wait strategy for a container, including deadline +func WithWaitStrategyAndDeadline(deadline time.Duration, strategies ...wait.Strategy) CustomizeDefinitionOption { + return func(def *Definition) error { + def.waitingFor = wait.ForAll(strategies...).WithDeadline(deadline) + + return nil + } +} + +// WithAdditionalWaitStrategyAndDeadline appends the wait strategy for a container, including deadline +func WithAdditionalWaitStrategyAndDeadline(deadline time.Duration, strategies ...wait.Strategy) CustomizeDefinitionOption { + return func(def *Definition) error { + if def.waitingFor == nil { + def.waitingFor = wait.ForAll(strategies...).WithDeadline(deadline) + return nil + } + + wss := make([]wait.Strategy, 0, len(strategies)+1) + wss = append(wss, def.waitingFor) + wss = append(wss, strategies...) + + def.waitingFor = wait.ForAll(wss...).WithDeadline(deadline) + + return nil + } +} + +// WithAlwaysPull will pull the image before starting the container +func WithAlwaysPull() CustomizeDefinitionOption { + return func(def *Definition) error { + def.alwaysPullImage = true + return nil + } +} + +// WithImagePlatform sets the platform for a container +func WithImagePlatform(platform string) CustomizeDefinitionOption { + return func(def *Definition) error { + def.imagePlatform = platform + return nil + } +} + +// WithEntrypoint completely replaces the entrypoint of a container +func WithEntrypoint(entrypoint ...string) CustomizeDefinitionOption { + return func(def *Definition) error { + def.entrypoint = entrypoint + return nil + } +} + +// WithEntrypointArgs appends the entrypoint arguments to the entrypoint of a container +func WithEntrypointArgs(entrypointArgs ...string) CustomizeDefinitionOption { + return func(def *Definition) error { + def.entrypoint = append(def.entrypoint, entrypointArgs...) + return nil + } +} + +// WithExposedPorts appends the ports to the exposed ports for a container +func WithExposedPorts(ports ...string) CustomizeDefinitionOption { + return func(def *Definition) error { + def.exposedPorts = append(def.exposedPorts, ports...) + return nil + } +} + +// WithCmd completely replaces the command for a container +func WithCmd(cmd ...string) CustomizeDefinitionOption { + return func(def *Definition) error { + def.cmd = cmd + return nil + } +} + +// WithCmdArgs appends the command arguments to the command for a container +func WithCmdArgs(cmdArgs ...string) CustomizeDefinitionOption { + return func(def *Definition) error { + def.cmd = append(def.cmd, cmdArgs...) + return nil + } +} + +// WithLabels appends the labels to the labels for a container +func WithLabels(labels map[string]string) CustomizeDefinitionOption { + return func(def *Definition) error { + if def.labels == nil { + def.labels = make(map[string]string) + } + for k, v := range labels { + def.labels[k] = v + } + return nil + } +} + +// WithLifecycleHooks completely replaces the lifecycle hooks for a container +func WithLifecycleHooks(hooks ...LifecycleHooks) CustomizeDefinitionOption { + return func(def *Definition) error { + def.lifecycleHooks = hooks + return nil + } +} + +// WithAdditionalLifecycleHooks appends lifecycle hooks to the existing ones for a container +func WithAdditionalLifecycleHooks(hooks ...LifecycleHooks) CustomizeDefinitionOption { + return func(def *Definition) error { + def.lifecycleHooks = append(def.lifecycleHooks, hooks...) + return nil + } +} + +// WithFiles appends the files to the files for a container +func WithFiles(files ...File) CustomizeDefinitionOption { + return func(def *Definition) error { + def.files = append(def.files, files...) + return nil + } +} diff --git a/vendor/github.com/docker/go-sdk/container/ports.go b/vendor/github.com/docker/go-sdk/container/ports.go new file mode 100644 index 00000000..b16bdfe3 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/ports.go @@ -0,0 +1,84 @@ +package container + +import ( + "context" + "fmt" + "net" + + "github.com/containerd/errdefs" + + "github.com/docker/go-connections/nat" +) + +// Endpoint gets proto://host:port string for the lowest numbered exposed port +// Will return just host:port if proto is empty +func (c *Container) Endpoint(ctx context.Context, proto string) (string, error) { + inspect, err := c.Inspect(ctx) + if err != nil { + return "", err + } + + if len(inspect.NetworkSettings.Ports) == 0 { + return "", errdefs.ErrNotFound.WithMessage("no ports exposed") + } + + // Get lowest numbered bound port. + var lowestPort nat.Port + for port := range inspect.NetworkSettings.Ports { + if lowestPort == "" || port.Int() < lowestPort.Int() { + lowestPort = port + } + } + + return c.PortEndpoint(ctx, lowestPort, proto) +} + +// PortEndpoint gets proto://host:port string for the given exposed port +// It returns proto://host:port or proto://[IPv6host]:port string for the given exposed port. +// It returns just host:port or [IPv6host]:port if proto is blank. +func (c *Container) PortEndpoint(ctx context.Context, port nat.Port, proto string) (string, error) { + host, err := c.Host(ctx) + if err != nil { + return "", err + } + + outerPort, err := c.MappedPort(ctx, port) + if err != nil { + return "", err + } + + hostPort := net.JoinHostPort(host, outerPort.Port()) + if proto == "" { + return hostPort, nil + } + + return proto + "://" + hostPort, nil +} + +// MappedPort gets externally mapped port for a container port +func (c *Container) MappedPort(ctx context.Context, port nat.Port) (nat.Port, error) { + inspect, err := c.Inspect(ctx) + if err != nil { + return "", fmt.Errorf("inspect: %w", err) + } + if inspect.HostConfig.NetworkMode == "host" { + return port, nil + } + + ports := inspect.NetworkSettings.Ports + + for k, p := range ports { + if k.Port() != port.Port() { + continue + } + if port.Proto() != "" && k.Proto() != port.Proto() { + continue + } + if len(p) == 0 { + continue + } + return nat.NewPort(k.Proto(), p[0].HostPort) + } + + return "", errdefs.ErrNotFound.WithMessage(fmt.Sprintf("port %q not found", port)) +} diff --git a/vendor/github.com/docker/go-sdk/container/testing.go b/vendor/github.com/docker/go-sdk/container/testing.go new file mode 100644 index 00000000..e8bad09d --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/testing.go @@ -0,0 +1,90 @@ +package container + +import ( + "regexp" + "testing" + + "github.com/containerd/errdefs" + "github.com/stretchr/testify/require" +) + +// errAlreadyInProgress is a regular expression that matches the error for a container +// removal that is already in progress. +var errAlreadyInProgress = regexp.MustCompile(`removal of container .* is already in progress`) + +// causer is an interface that allows to get the cause of an error. +type causer interface { + Cause() error +} + +// wrapErr is an interface that allows to unwrap an error. +type wrapErr interface { + Unwrap() error +} + +// unwrapErrs is an interface that allows to unwrap multiple errors. +type unwrapErrs interface { + Unwrap() []error +} + +// Cleanup is a helper function that schedules a [TerminableContainer] +// to be terminated when the test ends. +// +// This should be called directly after (before any error check) +// [Create](...) in a test to ensure the +// container is pruned when the function ends. +// If the container is nil, it's a no-op. +func Cleanup(tb testing.TB, ctr TerminableContainer, options ...TerminateOption) { + tb.Helper() + + tb.Cleanup(func() { + noErrorOrIgnored(tb, Terminate(ctr, options...)) + }) +} + +// isCleanupSafe checks if an error is cleanup safe. +func isCleanupSafe(err error) bool { + if err == nil { + return true + } + + // First try with containerd's errdefs + switch { + case errdefs.IsNotFound(err): + return true + case errdefs.IsConflict(err): + // Terminating a container that is already terminating. + if errAlreadyInProgress.MatchString(err.Error()) { + return true + } + return false + } + + switch x := err.(type) { //nolint:errorlint // We need to check for interfaces. + case causer: + return isCleanupSafe(x.Cause()) + case wrapErr: + return isCleanupSafe(x.Unwrap()) + case unwrapErrs: + for _, e := range x.Unwrap() { + if !isCleanupSafe(e) { + return false + } + } + return true + default: + return false + } +} + +// noErrorOrIgnored is a helper function that checks if the error is nil or an error +// we can ignore. +func noErrorOrIgnored(tb testing.TB, err error) { + tb.Helper() + + if isCleanupSafe(err) { + return + } + + require.NoError(tb, err) +} diff --git a/vendor/github.com/docker/go-sdk/container/version.go b/vendor/github.com/docker/go-sdk/container/version.go new file mode 100644 index 00000000..8bf17beb --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/version.go @@ -0,0 +1,10 @@ +package container + +const ( + version = "0.1.0-alpha006" +) + +// Version returns the version of the container package. +func Version() string { + return version +} diff --git a/vendor/github.com/docker/go-sdk/container/wait/all.go b/vendor/github.com/docker/go-sdk/container/wait/all.go new file mode 100644 index 00000000..98a1dea1 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/wait/all.go @@ -0,0 +1,83 @@ +package wait + +import ( + "context" + "errors" + "reflect" + "time" +) + +// Implement interface +var ( + _ Strategy = (*MultiStrategy)(nil) + _ StrategyTimeout = (*MultiStrategy)(nil) +) + +type MultiStrategy struct { + // all Strategies should have a startupTimeout to avoid waiting infinitely + timeout *time.Duration + deadline *time.Duration + + // additional properties + Strategies []Strategy +} + +// WithStartupTimeoutDefault sets the default timeout for all inner wait strategies +func (ms *MultiStrategy) WithStartupTimeoutDefault(timeout time.Duration) *MultiStrategy { + ms.timeout = &timeout + return ms +} + +// WithDeadline sets a time.Duration which limits all wait strategies +func (ms *MultiStrategy) WithDeadline(deadline time.Duration) *MultiStrategy { + ms.deadline = &deadline + return ms +} + +func ForAll(strategies ...Strategy) *MultiStrategy { + return &MultiStrategy{ + Strategies: strategies, + } +} + +func (ms *MultiStrategy) Timeout() *time.Duration { + return ms.timeout +} + +func (ms *MultiStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + var cancel context.CancelFunc + if ms.deadline != nil { + ctx, cancel = context.WithTimeout(ctx, *ms.deadline) + defer cancel() + } + + if len(ms.Strategies) == 0 { + return errors.New("no wait strategy supplied") + } + + for _, strategy := range ms.Strategies { + if strategy == nil || reflect.ValueOf(strategy).IsNil() { + // A module could be appending strategies after part of the container initialization, + // and use wait.ForAll on a not initialized strategy. + // In this case, we just skip the nil strategy. + continue + } + + strategyCtx := ctx + + // Set default Timeout when strategy implements StrategyTimeout + if st, ok := strategy.(StrategyTimeout); ok { + if ms.Timeout() != nil && st.Timeout() == nil { + strategyCtx, cancel = context.WithTimeout(ctx, *ms.Timeout()) + defer cancel() + } + } + + err := strategy.WaitUntilReady(strategyCtx, target) + if err != nil { + return err + } + } + + return nil +} diff --git a/vendor/github.com/docker/go-sdk/container/wait/errors.go b/vendor/github.com/docker/go-sdk/container/wait/errors.go new file mode 100644 index 00000000..3e3919a6 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/wait/errors.go @@ -0,0 +1,13 @@ +//go:build !windows +// +build !windows + +package wait + +import ( + "errors" + "syscall" +) + +func isConnRefusedErr(err error) bool { + return errors.Is(err, syscall.ECONNREFUSED) +} diff --git a/vendor/github.com/docker/go-sdk/container/wait/errors_windows.go b/vendor/github.com/docker/go-sdk/container/wait/errors_windows.go new file mode 100644 index 00000000..3ae346d8 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/wait/errors_windows.go @@ -0,0 +1,9 @@ +package wait + +import ( + "golang.org/x/sys/windows" +) + +func isConnRefusedErr(err error) bool { + return err == windows.WSAECONNREFUSED +} diff --git a/vendor/github.com/docker/go-sdk/container/wait/exec.go b/vendor/github.com/docker/go-sdk/container/wait/exec.go new file mode 100644 index 00000000..94c36157 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/wait/exec.go @@ -0,0 +1,107 @@ +package wait + +import ( + "context" + "io" + "time" + + "github.com/docker/go-sdk/container/exec" +) + +// Implement interface +var ( + _ Strategy = (*ExecStrategy)(nil) + _ StrategyTimeout = (*ExecStrategy)(nil) +) + +type ExecStrategy struct { + // all Strategies should have a startupTimeout to avoid waiting infinitely + timeout *time.Duration + cmd []string + + // additional properties + ExitCodeMatcher func(exitCode int) bool + ResponseMatcher func(body io.Reader) bool + PollInterval time.Duration +} + +// NewExecStrategy constructs an Exec strategy ... +func NewExecStrategy(cmd []string) *ExecStrategy { + return &ExecStrategy{ + cmd: cmd, + ExitCodeMatcher: defaultExitCodeMatcher, + ResponseMatcher: func(_ io.Reader) bool { return true }, + PollInterval: defaultPollInterval(), + } +} + +func defaultExitCodeMatcher(exitCode int) bool { + return exitCode == 0 +} + +// WithTimeout can be used to change the default startup timeout +func (ws *ExecStrategy) WithTimeout(startupTimeout time.Duration) *ExecStrategy { + ws.timeout = &startupTimeout + return ws +} + +func (ws *ExecStrategy) WithExitCode(exitCode int) *ExecStrategy { + return ws.WithExitCodeMatcher(func(actualCode int) bool { + return actualCode == exitCode + }) +} + +func (ws *ExecStrategy) WithExitCodeMatcher(exitCodeMatcher func(exitCode int) bool) *ExecStrategy { + ws.ExitCodeMatcher = exitCodeMatcher + return ws +} + +func (ws *ExecStrategy) WithResponseMatcher(matcher func(body io.Reader) bool) *ExecStrategy { + ws.ResponseMatcher = matcher + return ws +} + +// WithPollInterval can be used to override the default polling interval of 100 milliseconds +func (ws *ExecStrategy) WithPollInterval(pollInterval time.Duration) *ExecStrategy { + ws.PollInterval = pollInterval + return ws +} + +// ForExec is a convenience method to assign ExecStrategy +func ForExec(cmd []string) *ExecStrategy { + return NewExecStrategy(cmd) +} + +func (ws *ExecStrategy) Timeout() *time.Duration { + return ws.timeout +} + +func (ws *ExecStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + timeout := defaultTimeout() + if ws.timeout != nil { + timeout = *ws.timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(ws.PollInterval): + exitCode, resp, err := target.Exec(ctx, ws.cmd, exec.Multiplexed()) + if err != nil { + return err + } + if !ws.ExitCodeMatcher(exitCode) { + continue + } + if ws.ResponseMatcher != nil && !ws.ResponseMatcher(resp) { + continue + } + + return nil + } + } +} diff --git a/vendor/github.com/docker/go-sdk/container/wait/exit.go b/vendor/github.com/docker/go-sdk/container/wait/exit.go new file mode 100644 index 00000000..fcbf67e3 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/wait/exit.go @@ -0,0 +1,89 @@ +package wait + +import ( + "context" + "strings" + "time" +) + +// Implement interface +var ( + _ Strategy = (*ExitStrategy)(nil) + _ StrategyTimeout = (*ExitStrategy)(nil) +) + +// ExitStrategy will wait until container exit +type ExitStrategy struct { + // all Strategies should have a timeout to avoid waiting infinitely + timeout *time.Duration + + // additional properties + PollInterval time.Duration +} + +// NewExitStrategy constructs with polling interval of 100 milliseconds without timeout by default +func NewExitStrategy() *ExitStrategy { + return &ExitStrategy{ + PollInterval: defaultPollInterval(), + } +} + +// fluent builders for each property +// since go has neither covariance nor generics, the return type must be the type of the concrete implementation +// this is true for all properties, even the "shared" ones + +// WithTimeout can be used to change the default exit timeout +func (ws *ExitStrategy) WithTimeout(exitTimeout time.Duration) *ExitStrategy { + ws.timeout = &exitTimeout + return ws +} + +// WithPollInterval can be used to override the default polling interval of 100 milliseconds +func (ws *ExitStrategy) WithPollInterval(pollInterval time.Duration) *ExitStrategy { + ws.PollInterval = pollInterval + return ws +} + +// ForExit is the default construction for the fluid interface. +// +// For Example: +// +// wait. +// ForExit(). +// WithPollInterval(1 * time.Second) +func ForExit() *ExitStrategy { + return NewExitStrategy() +} + +func (ws *ExitStrategy) Timeout() *time.Duration { + return ws.timeout +} + +// WaitUntilReady implements Strategy.WaitUntilReady +func (ws *ExitStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + if ws.timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *ws.timeout) + defer cancel() + } + + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + state, err := target.State(ctx) + if err != nil { + if !strings.Contains(err.Error(), "No such container") { + return err + } + return nil + } + if state.Running { + time.Sleep(ws.PollInterval) + continue + } + return nil + } + } +} diff --git a/vendor/github.com/docker/go-sdk/container/wait/file.go b/vendor/github.com/docker/go-sdk/container/wait/file.go new file mode 100644 index 00000000..0ee46d54 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/wait/file.go @@ -0,0 +1,112 @@ +package wait + +import ( + "context" + "fmt" + "io" + "time" + + "github.com/containerd/errdefs" +) + +var ( + _ Strategy = (*FileStrategy)(nil) + _ StrategyTimeout = (*FileStrategy)(nil) +) + +// FileStrategy waits for a file to exist in the container. +type FileStrategy struct { + timeout *time.Duration + file string + pollInterval time.Duration + matcher func(io.Reader) error +} + +// NewFileStrategy constructs an FileStrategy strategy. +func NewFileStrategy(file string) *FileStrategy { + return &FileStrategy{ + file: file, + pollInterval: defaultPollInterval(), + } +} + +// WithTimeout can be used to change the default startup timeout +func (ws *FileStrategy) WithTimeout(startupTimeout time.Duration) *FileStrategy { + ws.timeout = &startupTimeout + return ws +} + +// WithPollInterval can be used to override the default polling interval of 100 milliseconds +func (ws *FileStrategy) WithPollInterval(pollInterval time.Duration) *FileStrategy { + ws.pollInterval = pollInterval + return ws +} + +// WithMatcher can be used to consume the file content. +// The matcher can return an errdefs.ErrNotFound to indicate that the file is not ready. +// Any other error will be considered a failure. +// Default: nil, will only wait for the file to exist. +func (ws *FileStrategy) WithMatcher(matcher func(io.Reader) error) *FileStrategy { + ws.matcher = matcher + return ws +} + +// ForFile is a convenience method to assign FileStrategy +func ForFile(file string) *FileStrategy { + return NewFileStrategy(file) +} + +// Timeout returns the timeout for the strategy +func (ws *FileStrategy) Timeout() *time.Duration { + return ws.timeout +} + +// WaitUntilReady waits until the file exists in the container and copies it to the target. +func (ws *FileStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + timeout := defaultTimeout() + if ws.timeout != nil { + timeout = *ws.timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + timer := time.NewTicker(ws.pollInterval) + defer timer.Stop() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-timer.C: + if err := ws.matchFile(ctx, target); err != nil { + if errdefs.IsNotFound(err) { + // Not found, continue polling. + continue + } + + return fmt.Errorf("copy from container: %w", err) + } + return nil + } + } +} + +// matchFile tries to copy the file from the container and match it. +func (ws *FileStrategy) matchFile(ctx context.Context, target StrategyTarget) error { + rc, err := target.CopyFromContainer(ctx, ws.file) + if err != nil { + return fmt.Errorf("copy from container: %w", err) + } + defer rc.Close() + + if ws.matcher == nil { + // No matcher, just check if the file exists. + return nil + } + + if err = ws.matcher(rc); err != nil { + return fmt.Errorf("matcher: %w", err) + } + + return nil +} diff --git a/vendor/github.com/docker/go-sdk/container/wait/health.go b/vendor/github.com/docker/go-sdk/container/wait/health.go new file mode 100644 index 00000000..48cf696c --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/wait/health.go @@ -0,0 +1,92 @@ +package wait + +import ( + "context" + "time" + + "github.com/docker/docker/api/types" +) + +// Implement interface +var ( + _ Strategy = (*HealthStrategy)(nil) + _ StrategyTimeout = (*HealthStrategy)(nil) +) + +// HealthStrategy will wait until the container becomes healthy +type HealthStrategy struct { + // all Strategies should have a startupTimeout to avoid waiting infinitely + timeout *time.Duration + + // additional properties + PollInterval time.Duration +} + +// NewHealthStrategy constructs with polling interval of 100 milliseconds and startup timeout of 60 seconds by default +func NewHealthStrategy() *HealthStrategy { + return &HealthStrategy{ + PollInterval: defaultPollInterval(), + } +} + +// fluent builders for each property +// since go has neither covariance nor generics, the return type must be the type of the concrete implementation +// this is true for all properties, even the "shared" ones like startupTimeout + +// WithTimeout can be used to change the default startup timeout +func (ws *HealthStrategy) WithTimeout(startupTimeout time.Duration) *HealthStrategy { + ws.timeout = &startupTimeout + return ws +} + +// WithPollInterval can be used to override the default polling interval of 100 milliseconds +func (ws *HealthStrategy) WithPollInterval(pollInterval time.Duration) *HealthStrategy { + ws.PollInterval = pollInterval + return ws +} + +// ForHealthCheck is the default construction for the fluid interface. +// +// For Example: +// +// wait. +// ForHealthCheck(). +// WithPollInterval(1 * time.Second) +func ForHealthCheck() *HealthStrategy { + return NewHealthStrategy() +} + +func (ws *HealthStrategy) Timeout() *time.Duration { + return ws.timeout +} + +// WaitUntilReady implements Strategy.WaitUntilReady +func (ws *HealthStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + timeout := defaultTimeout() + if ws.timeout != nil { + timeout = *ws.timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + state, err := target.State(ctx) + if err != nil { + return err + } + if err := checkState(state); err != nil { + return err + } + if state.Health == nil || state.Health.Status != types.Healthy { + time.Sleep(ws.PollInterval) + continue + } + return nil + } + } +} diff --git a/vendor/github.com/docker/go-sdk/container/wait/host_port.go b/vendor/github.com/docker/go-sdk/container/wait/host_port.go new file mode 100644 index 00000000..5a88bf04 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/wait/host_port.go @@ -0,0 +1,292 @@ +package wait + +import ( + "context" + "errors" + "fmt" + "net" + "os" + "strconv" + "time" + + "github.com/docker/go-connections/nat" +) + +const ( + exitEaccess = 126 // container cmd can't be invoked (permission denied) + exitCmdNotFound = 127 // container cmd not found/does not exist or invalid bind-mount +) + +// Implement interface +var ( + _ Strategy = (*HostPortStrategy)(nil) + _ StrategyTimeout = (*HostPortStrategy)(nil) +) + +var ( + errShellNotExecutable = errors.New("/bin/sh command not executable") + errShellNotFound = errors.New("/bin/sh command not found") +) + +type HostPortStrategy struct { + // Port is a string containing port number and protocol in the format "80/tcp" + // which + Port nat.Port + // all WaitStrategies should have a startupTimeout to avoid waiting infinitely + timeout *time.Duration + PollInterval time.Duration + + // skipInternalCheck is a flag to skip the internal check, which is useful when + // a shell is not available in the container or when the container doesn't bind + // the port internally until additional conditions are met. + skipInternalCheck bool + + // skipExternalCheck is a flag to skip the external check, which, if used with + // skipInternalCheck, makes strategy waiting only for port mapping completion + // without accessing port. + skipExternalCheck bool +} + +// NewHostPortStrategy constructs a default host port strategy that waits for the given +// port to be exposed. The default startup timeout is 60 seconds. +func NewHostPortStrategy(port nat.Port) *HostPortStrategy { + return &HostPortStrategy{ + Port: port, + PollInterval: defaultPollInterval(), + } +} + +// fluent builders for each property +// since go has neither covariance nor generics, the return type must be the type of the concrete implementation +// this is true for all properties, even the "shared" ones like startupTimeout + +// ForListeningPort returns a host port strategy that waits for the given port +// to be exposed and bound internally the container. +// Alias for `NewHostPortStrategy(port)`. +func ForListeningPort(port nat.Port) *HostPortStrategy { + return NewHostPortStrategy(port) +} + +// ForExposedPort returns a host port strategy that waits for the first port +// to be exposed and bound internally the container. +func ForExposedPort() *HostPortStrategy { + return NewHostPortStrategy("") +} + +// ForMappedPort returns a host port strategy that waits for the given port +// to be mapped without accessing the port itself. +func ForMappedPort(port nat.Port) *HostPortStrategy { + return NewHostPortStrategy(port).SkipInternalCheck().SkipExternalCheck() +} + +// SkipInternalCheck changes the host port strategy to skip the internal check, +// which is useful when a shell is not available in the container or when the +// container doesn't bind the port internally until additional conditions are met. +func (hp *HostPortStrategy) SkipInternalCheck() *HostPortStrategy { + hp.skipInternalCheck = true + + return hp +} + +// SkipExternalCheck changes the host port strategy to skip the external check, +// which, if used with SkipInternalCheck, makes strategy waiting only for port +// mapping completion without accessing port. +func (hp *HostPortStrategy) SkipExternalCheck() *HostPortStrategy { + hp.skipExternalCheck = true + + return hp +} + +// WithTimeout can be used to change the default startup timeout +func (hp *HostPortStrategy) WithTimeout(startupTimeout time.Duration) *HostPortStrategy { + hp.timeout = &startupTimeout + return hp +} + +// WithPollInterval can be used to override the default polling interval of 100 milliseconds +func (hp *HostPortStrategy) WithPollInterval(pollInterval time.Duration) *HostPortStrategy { + hp.PollInterval = pollInterval + return hp +} + +func (hp *HostPortStrategy) Timeout() *time.Duration { + return hp.timeout +} + +// detectInternalPort returns the lowest internal port that is currently bound. +// If no internal port is found, it returns the zero nat.Port value which +// can be checked against an empty string. +func (hp *HostPortStrategy) detectInternalPort(ctx context.Context, target StrategyTarget) (nat.Port, error) { + var internalPort nat.Port + inspect, err := target.Inspect(ctx) + if err != nil { + return internalPort, fmt.Errorf("inspect: %w", err) + } + + for port := range inspect.NetworkSettings.Ports { + if internalPort == "" || port.Int() < internalPort.Int() { + internalPort = port + } + } + + return internalPort, nil +} + +// WaitUntilReady implements Strategy.WaitUntilReady +func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + timeout := defaultTimeout() + if hp.timeout != nil { + timeout = *hp.timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + waitInterval := hp.PollInterval + + internalPort := hp.Port + i := 0 + if internalPort == "" { + var err error + // Port is not specified, so we need to detect it. + internalPort, err = hp.detectInternalPort(ctx, target) + if err != nil { + return fmt.Errorf("detect internal port: %w", err) + } + + for internalPort == "" { + select { + case <-ctx.Done(): + return fmt.Errorf("detect internal port: retries: %d, last err: %w, ctx err: %w", i, err, ctx.Err()) + case <-time.After(waitInterval): + if err := checkTarget(ctx, target); err != nil { + return fmt.Errorf("detect internal port: check target: retries: %d, last err: %w", i, err) + } + + internalPort, err = hp.detectInternalPort(ctx, target) + if err != nil { + return fmt.Errorf("detect internal port: %w", err) + } + } + } + } + + port, err := target.MappedPort(ctx, internalPort) + i = 0 + + for port == "" { + i++ + + select { + case <-ctx.Done(): + return fmt.Errorf("mapped port: retries: %d, port: %q, last err: %w, ctx err: %w", i, port, err, ctx.Err()) + case <-time.After(waitInterval): + if err := checkTarget(ctx, target); err != nil { + return fmt.Errorf("mapped port: check target: retries: %d, port: %q, last err: %w", i, port, err) + } + port, err = target.MappedPort(ctx, internalPort) + if err != nil { + target.Logger().Error("mapped port", "retries:", i, "port", port, "error", err) + } + } + } + + if !hp.skipExternalCheck { + ipAddress, err := target.Host(ctx) + if err != nil { + return fmt.Errorf("host: %w", err) + } + + if err := externalCheck(ctx, ipAddress, port, target, waitInterval); err != nil { + return fmt.Errorf("external check: %w", err) + } + } + + if hp.skipInternalCheck { + return nil + } + + if err = internalCheck(ctx, internalPort, target); err != nil { + switch { + case errors.Is(err, errShellNotExecutable): + target.Logger().Warn("Shell not executable in container, only external port validated", "error", err) + return nil + case errors.Is(err, errShellNotFound): + target.Logger().Warn("Shell not found in container", "error", err) + return nil + default: + return fmt.Errorf("internal check: %w", err) + } + } + + return nil +} + +func externalCheck(ctx context.Context, ipAddress string, port nat.Port, target StrategyTarget, waitInterval time.Duration) error { + proto := port.Proto() + portNumber := port.Int() + portString := strconv.Itoa(portNumber) + + dialer := net.Dialer{} + address := net.JoinHostPort(ipAddress, portString) + for i := 0; ; i++ { + if err := checkTarget(ctx, target); err != nil { + return fmt.Errorf("check target: retries: %d address: %s: %w", i, address, err) + } + conn, err := dialer.DialContext(ctx, proto, address) + if err != nil { + var v *net.OpError + if errors.As(err, &v) { + var v2 *os.SyscallError + if errors.As(v.Err, &v2) { + if isConnRefusedErr(v2.Err) { + time.Sleep(waitInterval) + continue + } + } + } + return fmt.Errorf("dial: %w", err) + } + + conn.Close() + return nil + } +} + +func internalCheck(ctx context.Context, internalPort nat.Port, target StrategyTarget) error { + command := buildInternalCheckCommand(internalPort.Int()) + for { + if ctx.Err() != nil { + return ctx.Err() + } + if err := checkTarget(ctx, target); err != nil { + return err + } + exitCode, _, err := target.Exec(ctx, []string{"/bin/sh", "-c", command}) + if err != nil { + return fmt.Errorf("%w, host port waiting failed", err) + } + + // Docker has an issue which override exit code 127 to 126 due to: + // https://github.com/moby/moby/issues/45795 + // Handle both to ensure compatibility with Docker and Podman for now. + switch exitCode { + case 0: + return nil + case exitEaccess: + return errShellNotExecutable + case exitCmdNotFound: + return errShellNotFound + } + } +} + +func buildInternalCheckCommand(internalPort int) string { + command := `( + cat /proc/net/tcp* | awk '{print $2}' | grep -i :%04x || + nc -vz -w 1 localhost %d || + /bin/sh -c ' 0 { + ws.TLSConfig = tlsconf[0] + } + return ws +} + +func (ws *HTTPStrategy) WithAllowInsecure(allowInsecure bool) *HTTPStrategy { + ws.AllowInsecure = allowInsecure + return ws +} + +func (ws *HTTPStrategy) WithMethod(method string) *HTTPStrategy { + ws.Method = method + return ws +} + +func (ws *HTTPStrategy) WithBody(reqdata io.Reader) *HTTPStrategy { + ws.Body = reqdata + return ws +} + +func (ws *HTTPStrategy) WithHeaders(headers map[string]string) *HTTPStrategy { + ws.Headers = headers + return ws +} + +func (ws *HTTPStrategy) WithResponseHeadersMatcher(matcher func(http.Header) bool) *HTTPStrategy { + ws.ResponseHeadersMatcher = matcher + return ws +} + +func (ws *HTTPStrategy) WithBasicAuth(username, password string) *HTTPStrategy { + ws.UserInfo = url.UserPassword(username, password) + return ws +} + +// WithPollInterval can be used to override the default polling interval of 100 milliseconds +func (ws *HTTPStrategy) WithPollInterval(pollInterval time.Duration) *HTTPStrategy { + ws.PollInterval = pollInterval + return ws +} + +// WithForcedIPv4LocalHost forces usage of localhost to be ipv4 127.0.0.1 +// to avoid ipv6 docker bugs https://github.com/moby/moby/issues/42442 https://github.com/moby/moby/issues/42375 +func (ws *HTTPStrategy) WithForcedIPv4LocalHost() *HTTPStrategy { + ws.ForceIPv4LocalHost = true + return ws +} + +// ForHTTP is a convenience method similar to Wait.java +// https://github.com/testcontainers/testcontainers-java/blob/1d85a3834bd937f80aad3a4cec249c027f31aeb4/core/src/main/java/org/testcontainers/containers/wait/strategy/Wait.java +func ForHTTP(path string) *HTTPStrategy { + return NewHTTPStrategy(path) +} + +func (ws *HTTPStrategy) Timeout() *time.Duration { + return ws.timeout +} + +// WaitUntilReady implements Strategy.WaitUntilReady +func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + timeout := defaultTimeout() + if ws.timeout != nil { + timeout = *ws.timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + ipAddress, err := target.Host(ctx) + if err != nil { + return err + } + // to avoid ipv6 docker bugs https://github.com/moby/moby/issues/42442 https://github.com/moby/moby/issues/42375 + if ws.ForceIPv4LocalHost { + ipAddress = strings.Replace(ipAddress, "localhost", "127.0.0.1", 1) + } + + var mappedPort nat.Port + if ws.Port == "" { + // We wait one polling interval before we grab the ports + // otherwise they might not be bound yet on startup. + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(ws.PollInterval): + // Port should now be bound so just continue. + } + + if err := checkTarget(ctx, target); err != nil { + return err + } + + inspect, err := target.Inspect(ctx) + if err != nil { + return err + } + + // Find the lowest numbered exposed tcp port. + var lowestPort nat.Port + var hostPort string + for port, bindings := range inspect.NetworkSettings.Ports { + if len(bindings) == 0 || port.Proto() != "tcp" { + continue + } + + if lowestPort == "" || port.Int() < lowestPort.Int() { + lowestPort = port + hostPort = bindings[0].HostPort + } + } + + if lowestPort == "" { + return errors.New("no exposed tcp ports or mapped ports - cannot wait for status") + } + + mappedPort, _ = nat.NewPort(lowestPort.Proto(), hostPort) + } else { + mappedPort, err = target.MappedPort(ctx, ws.Port) + + for mappedPort == "" { + select { + case <-ctx.Done(): + return fmt.Errorf("%w: %w", ctx.Err(), err) + case <-time.After(ws.PollInterval): + if err := checkTarget(ctx, target); err != nil { + return err + } + + mappedPort, err = target.MappedPort(ctx, ws.Port) + } + } + + if mappedPort.Proto() != "tcp" { + return errors.New("cannot use HTTP client on non-TCP ports") + } + } + + switch ws.Method { + case http.MethodGet, http.MethodHead, http.MethodPost, + http.MethodPut, http.MethodPatch, http.MethodDelete, + http.MethodConnect, http.MethodOptions, http.MethodTrace: + default: + if ws.Method != "" { + return fmt.Errorf("invalid http method %q", ws.Method) + } + ws.Method = http.MethodGet + } + + tripper := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + TLSClientConfig: ws.TLSConfig, + } + + var proto string + if ws.UseTLS { + proto = "https" + if ws.AllowInsecure { + if ws.TLSConfig == nil { + tripper.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } else { + ws.TLSConfig.InsecureSkipVerify = true + } + } + } else { + proto = "http" + } + + client := http.Client{Transport: tripper, Timeout: time.Second} + address := net.JoinHostPort(ipAddress, strconv.Itoa(mappedPort.Int())) + + endpoint, err := url.Parse(ws.Path) + if err != nil { + return err + } + endpoint.Scheme = proto + endpoint.Host = address + + if ws.UserInfo != nil { + endpoint.User = ws.UserInfo + } + + // cache the body into a byte-slice so that it can be iterated over multiple times + var body []byte + if ws.Body != nil { + body, err = io.ReadAll(ws.Body) + if err != nil { + return err + } + } + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(ws.PollInterval): + if err := checkTarget(ctx, target); err != nil { + return err + } + req, err := http.NewRequestWithContext(ctx, ws.Method, endpoint.String(), bytes.NewReader(body)) + if err != nil { + return err + } + + for k, v := range ws.Headers { + req.Header.Set(k, v) + } + + resp, err := client.Do(req) + if err != nil { + continue + } + if ws.StatusCodeMatcher != nil && !ws.StatusCodeMatcher(resp.StatusCode) { + _ = resp.Body.Close() + continue + } + if ws.ResponseMatcher != nil && !ws.ResponseMatcher(resp.Body) { + _ = resp.Body.Close() + continue + } + if ws.ResponseHeadersMatcher != nil && !ws.ResponseHeadersMatcher(resp.Header) { + _ = resp.Body.Close() + continue + } + if err := resp.Body.Close(); err != nil { + continue + } + return nil + } + } +} diff --git a/vendor/github.com/docker/go-sdk/container/wait/log.go b/vendor/github.com/docker/go-sdk/container/wait/log.go new file mode 100644 index 00000000..ba85c97a --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/wait/log.go @@ -0,0 +1,214 @@ +package wait + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "regexp" + "time" +) + +// Implement interface +var ( + _ Strategy = (*LogStrategy)(nil) + _ StrategyTimeout = (*LogStrategy)(nil) +) + +// PermanentError is a special error that will stop the wait and return an error. +type PermanentError struct { + err error +} + +// Error implements the error interface. +func (e *PermanentError) Error() string { + return e.err.Error() +} + +// NewPermanentError creates a new PermanentError. +func NewPermanentError(err error) *PermanentError { + return &PermanentError{err: err} +} + +// LogStrategy will wait until a given log entry shows up in the docker logs +type LogStrategy struct { + // all Strategies should have a startupTimeout to avoid waiting infinitely + timeout *time.Duration + + // additional properties + Log string + IsRegexp bool + Occurrence int + PollInterval time.Duration + + // check is the function that will be called to check if the log entry is present. + check func([]byte) error + + // submatchCallback is a callback that will be called with the sub matches of the regexp. + submatchCallback func(pattern string, matches [][][]byte) error + + // re is the optional compiled regexp. + re *regexp.Regexp + + // log byte slice version of [LogStrategy.Log] used for count checks. + log []byte +} + +// NewLogStrategy constructs with polling interval of 100 milliseconds and startup timeout of 60 seconds by default +func NewLogStrategy(log string) *LogStrategy { + return &LogStrategy{ + Log: log, + IsRegexp: false, + Occurrence: 1, + PollInterval: defaultPollInterval(), + } +} + +// fluent builders for each property +// since go has neither covariance nor generics, the return type must be the type of the concrete implementation +// this is true for all properties, even the "shared" ones like startupTimeout + +// AsRegexp can be used to change the default behavior of the log strategy to use regexp instead of plain text +func (ws *LogStrategy) AsRegexp() *LogStrategy { + ws.IsRegexp = true + return ws +} + +// Submatch configures a function that will be called with the result of +// [regexp.Regexp.FindAllSubmatch], allowing the caller to process the results. +// If the callback returns nil, the strategy will be considered successful. +// Returning a [PermanentError] will stop the wait and return an error, otherwise +// it will retry until the timeout is reached. +// [LogStrategy.Occurrence] is ignored if this option is set. +func (ws *LogStrategy) Submatch(callback func(pattern string, matches [][][]byte) error) *LogStrategy { + ws.submatchCallback = callback + + return ws +} + +// WithTimeout can be used to change the default startup timeout +func (ws *LogStrategy) WithTimeout(timeout time.Duration) *LogStrategy { + ws.timeout = &timeout + return ws +} + +// WithPollInterval can be used to override the default polling interval of 100 milliseconds +func (ws *LogStrategy) WithPollInterval(pollInterval time.Duration) *LogStrategy { + ws.PollInterval = pollInterval + return ws +} + +func (ws *LogStrategy) WithOccurrence(o int) *LogStrategy { + // the number of occurrence needs to be positive + if o <= 0 { + o = 1 + } + ws.Occurrence = o + return ws +} + +// ForLog is the default construction for the fluid interface. +// +// For Example: +// +// wait. +// ForLog("some text"). +// WithPollInterval(1 * time.Second) +func ForLog(log string) *LogStrategy { + return NewLogStrategy(log) +} + +func (ws *LogStrategy) Timeout() *time.Duration { + return ws.timeout +} + +// WaitUntilReady implements Strategy.WaitUntilReady +func (ws *LogStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + timeout := defaultTimeout() + if ws.timeout != nil { + timeout = *ws.timeout + } + + switch { + case ws.submatchCallback != nil: + ws.re = regexp.MustCompile(ws.Log) + ws.check = ws.checkSubmatch + case ws.IsRegexp: + ws.re = regexp.MustCompile(ws.Log) + ws.check = ws.checkRegexp + default: + ws.log = []byte(ws.Log) + ws.check = ws.checkCount + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + var lastLen int + var lastError error + for { + select { + case <-ctx.Done(): + return errors.Join(lastError, ctx.Err()) + default: + checkErr := checkTarget(ctx, target) + + reader, err := target.Logs(ctx) + if err != nil { + // TODO: fix as this will wait for timeout if the logs are not available. + time.Sleep(ws.PollInterval) + continue + } + + b, err := io.ReadAll(reader) + if err != nil { + // TODO: fix as this will wait for timeout if the logs are not readable. + time.Sleep(ws.PollInterval) + continue + } + + if lastLen == len(b) && checkErr != nil { + // Log length hasn't changed so we're not making progress. + return checkErr + } + + if err := ws.check(b); err != nil { + var errPermanent *PermanentError + if errors.As(err, &errPermanent) { + return err + } + + lastError = err + lastLen = len(b) + time.Sleep(ws.PollInterval) + continue + } + + return nil + } + } +} + +// checkCount checks if the log entry is present in the logs using a string count. +func (ws *LogStrategy) checkCount(b []byte) error { + if count := bytes.Count(b, ws.log); count < ws.Occurrence { + return fmt.Errorf("%q matched %d times, expected %d", ws.Log, count, ws.Occurrence) + } + + return nil +} + +// checkRegexp checks if the log entry is present in the logs using a regexp count. +func (ws *LogStrategy) checkRegexp(b []byte) error { + if matches := ws.re.FindAll(b, -1); len(matches) < ws.Occurrence { + return fmt.Errorf("`%s` matched %d times, expected %d", ws.Log, len(matches), ws.Occurrence) + } + + return nil +} + +// checkSubmatch checks if the log entry is present in the logs using a regexp sub match callback. +func (ws *LogStrategy) checkSubmatch(b []byte) error { + return ws.submatchCallback(ws.Log, ws.re.FindAllSubmatch(b, -1)) +} diff --git a/vendor/github.com/docker/go-sdk/container/wait/nop.go b/vendor/github.com/docker/go-sdk/container/wait/nop.go new file mode 100644 index 00000000..a0c529b9 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/wait/nop.go @@ -0,0 +1,81 @@ +package wait + +import ( + "context" + "io" + "log/slog" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" + "github.com/docker/go-sdk/container/exec" +) + +var ( + _ Strategy = (*NopStrategy)(nil) + _ StrategyTimeout = (*NopStrategy)(nil) + noopLogger = slog.New(slog.NewTextHandler(io.Discard, nil)) +) + +type NopStrategy struct { + timeout *time.Duration + waitUntilReady func(context.Context, StrategyTarget) error +} + +func ForNop( + waitUntilReady func(context.Context, StrategyTarget) error, +) *NopStrategy { + return &NopStrategy{ + waitUntilReady: waitUntilReady, + } +} + +func (ws *NopStrategy) Timeout() *time.Duration { + return ws.timeout +} + +func (ws *NopStrategy) WithTimeout(timeout time.Duration) *NopStrategy { + ws.timeout = &timeout + return ws +} + +func (ws *NopStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + return ws.waitUntilReady(ctx, target) +} + +type NopStrategyTarget struct { + ReaderCloser io.ReadCloser + ContainerState container.State +} + +func (st *NopStrategyTarget) Host(_ context.Context) (string, error) { + return "", nil +} + +func (st *NopStrategyTarget) Inspect(_ context.Context) (*container.InspectResponse, error) { + return nil, nil +} + +func (st *NopStrategyTarget) MappedPort(_ context.Context, n nat.Port) (nat.Port, error) { + return n, nil +} + +func (st *NopStrategyTarget) Logs(_ context.Context) (io.ReadCloser, error) { + return st.ReaderCloser, nil +} + +func (st *NopStrategyTarget) Exec(_ context.Context, _ []string, _ ...exec.ProcessOption) (int, io.Reader, error) { + return 0, nil, nil +} + +func (st *NopStrategyTarget) State(_ context.Context) (*container.State, error) { + return &st.ContainerState, nil +} + +func (st *NopStrategyTarget) CopyFromContainer(_ context.Context, _ string) (io.ReadCloser, error) { + return st.ReaderCloser, nil +} + +func (st *NopStrategyTarget) Logger() *slog.Logger { + return noopLogger +} diff --git a/vendor/github.com/docker/go-sdk/container/wait/sql.go b/vendor/github.com/docker/go-sdk/container/wait/sql.go new file mode 100644 index 00000000..917e4e99 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/wait/sql.go @@ -0,0 +1,118 @@ +package wait + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/docker/go-connections/nat" +) + +var ( + _ Strategy = (*waitForSQL)(nil) + _ StrategyTimeout = (*waitForSQL)(nil) +) + +const defaultForSQLQuery = "SELECT 1" + +// ForSQL constructs a new waitForSql strategy for the given driver +func ForSQL(port nat.Port, driver string, url func(host string, port nat.Port) string) *waitForSQL { + return &waitForSQL{ + Port: port, + URL: url, + Driver: driver, + startupTimeout: defaultTimeout(), + PollInterval: defaultPollInterval(), + query: defaultForSQLQuery, + } +} + +type waitForSQL struct { + timeout *time.Duration + + URL func(host string, port nat.Port) string + Driver string + Port nat.Port + startupTimeout time.Duration + PollInterval time.Duration + query string +} + +// WithTimeout can be used to change the default startup timeout +func (w *waitForSQL) WithTimeout(timeout time.Duration) *waitForSQL { + w.timeout = &timeout + return w +} + +// WithPollInterval can be used to override the default polling interval of 100 milliseconds +func (w *waitForSQL) WithPollInterval(pollInterval time.Duration) *waitForSQL { + w.PollInterval = pollInterval + return w +} + +// WithQuery can be used to override the default query used in the strategy. +func (w *waitForSQL) WithQuery(query string) *waitForSQL { + w.query = query + return w +} + +func (w *waitForSQL) Timeout() *time.Duration { + return w.timeout +} + +// WaitUntilReady repeatedly tries to run "SELECT 1" or user defined query on the given port using sql and driver. +// +// If it doesn't succeed until the timeout value which defaults to 60 seconds, it will return an error. +func (w *waitForSQL) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + timeout := defaultTimeout() + if w.timeout != nil { + timeout = *w.timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + host, err := target.Host(ctx) + if err != nil { + return err + } + + ticker := time.NewTicker(w.PollInterval) + defer ticker.Stop() + + var port nat.Port + port, err = target.MappedPort(ctx, w.Port) + + for port == "" { + select { + case <-ctx.Done(): + return fmt.Errorf("%w: %w", ctx.Err(), err) + case <-ticker.C: + if err := checkTarget(ctx, target); err != nil { + return err + } + port, err = target.MappedPort(ctx, w.Port) + } + } + + db, err := sql.Open(w.Driver, w.URL(host, port)) + if err != nil { + return fmt.Errorf("sql.Open: %w", err) + } + defer db.Close() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + if err := checkTarget(ctx, target); err != nil { + return err + } + if _, err := db.ExecContext(ctx, w.query); err != nil { + continue + } + return nil + } + } +} diff --git a/vendor/github.com/docker/go-sdk/container/wait/tls.go b/vendor/github.com/docker/go-sdk/container/wait/tls.go new file mode 100644 index 00000000..7625676f --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/wait/tls.go @@ -0,0 +1,167 @@ +package wait + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "time" +) + +// Validate we implement interface. +var _ Strategy = (*TLSStrategy)(nil) + +// TLSStrategy is a strategy for handling TLS. +type TLSStrategy struct { + // General Settings. + timeout *time.Duration + pollInterval time.Duration + + // Custom Settings. + certFiles *x509KeyPair + rootFiles []string + + // State. + tlsConfig *tls.Config +} + +// x509KeyPair is a pair of certificate and key files. +type x509KeyPair struct { + certPEMFile string + keyPEMFile string +} + +// ForTLSCert returns a CertStrategy that will add a Certificate to the [tls.Config] +// constructed from PEM formatted certificate key file pair in the container. +func ForTLSCert(certPEMFile, keyPEMFile string) *TLSStrategy { + return &TLSStrategy{ + certFiles: &x509KeyPair{ + certPEMFile: certPEMFile, + keyPEMFile: keyPEMFile, + }, + tlsConfig: &tls.Config{}, + pollInterval: defaultPollInterval(), + } +} + +// ForTLSRootCAs returns a CertStrategy that sets the root CAs for the [tls.Config] +// using the given PEM formatted files from the container. +func ForTLSRootCAs(pemFiles ...string) *TLSStrategy { + return &TLSStrategy{ + rootFiles: pemFiles, + tlsConfig: &tls.Config{}, + pollInterval: defaultPollInterval(), + } +} + +// WithRootCAs sets the root CAs for the [tls.Config] using the given files from +// the container. +func (ws *TLSStrategy) WithRootCAs(files ...string) *TLSStrategy { + ws.rootFiles = files + return ws +} + +// WithCert sets the [tls.Config] Certificates using the given files from the container. +func (ws *TLSStrategy) WithCert(certPEMFile, keyPEMFile string) *TLSStrategy { + ws.certFiles = &x509KeyPair{ + certPEMFile: certPEMFile, + keyPEMFile: keyPEMFile, + } + return ws +} + +// WithServerName sets the server for the [tls.Config]. +func (ws *TLSStrategy) WithServerName(serverName string) *TLSStrategy { + ws.tlsConfig.ServerName = serverName + return ws +} + +// WithTimeout can be used to change the default startup timeout. +func (ws *TLSStrategy) WithTimeout(startupTimeout time.Duration) *TLSStrategy { + ws.timeout = &startupTimeout + return ws +} + +// WithPollInterval can be used to override the default polling interval of 100 milliseconds. +func (ws *TLSStrategy) WithPollInterval(pollInterval time.Duration) *TLSStrategy { + ws.pollInterval = pollInterval + return ws +} + +// TLSConfig returns the TLS config once the strategy is ready. +// If the strategy is nil, it returns nil. +func (ws *TLSStrategy) TLSConfig() *tls.Config { + if ws == nil { + return nil + } + + return ws.tlsConfig +} + +// WaitUntilReady implements the [Strategy] interface. +// It waits for the CA, client cert and key files to be available in the container and +// uses them to setup the TLS config. +func (ws *TLSStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + size := len(ws.rootFiles) + if ws.certFiles != nil { + size += 2 + } + strategies := make([]Strategy, 0, size) + for _, file := range ws.rootFiles { + strategies = append(strategies, + ForFile(file).WithMatcher(func(r io.Reader) error { + buf, err := io.ReadAll(r) + if err != nil { + return fmt.Errorf("read CA cert file %q: %w", file, err) + } + + if ws.tlsConfig.RootCAs == nil { + ws.tlsConfig.RootCAs = x509.NewCertPool() + } + + if !ws.tlsConfig.RootCAs.AppendCertsFromPEM(buf) { + return fmt.Errorf("invalid CA cert file %q", file) + } + + return nil + }).WithPollInterval(ws.pollInterval), + ) + } + + if ws.certFiles != nil { + var certPEMBlock []byte + strategies = append(strategies, + ForFile(ws.certFiles.certPEMFile).WithMatcher(func(r io.Reader) error { + var err error + if certPEMBlock, err = io.ReadAll(r); err != nil { + return fmt.Errorf("read certificate cert %q: %w", ws.certFiles.certPEMFile, err) + } + + return nil + }).WithPollInterval(ws.pollInterval), + ForFile(ws.certFiles.keyPEMFile).WithMatcher(func(r io.Reader) error { + keyPEMBlock, err := io.ReadAll(r) + if err != nil { + return fmt.Errorf("read certificate key %q: %w", ws.certFiles.keyPEMFile, err) + } + + cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) + if err != nil { + return fmt.Errorf("x509 key pair %q %q: %w", ws.certFiles.certPEMFile, ws.certFiles.keyPEMFile, err) + } + + ws.tlsConfig.Certificates = []tls.Certificate{cert} + + return nil + }).WithPollInterval(ws.pollInterval), + ) + } + + strategy := ForAll(strategies...) + if ws.timeout != nil { + strategy.WithDeadline(*ws.timeout) + } + + return strategy.WaitUntilReady(ctx, target) +} diff --git a/vendor/github.com/docker/go-sdk/container/wait/wait.go b/vendor/github.com/docker/go-sdk/container/wait/wait.go new file mode 100644 index 00000000..8db6d14a --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/wait/wait.go @@ -0,0 +1,65 @@ +package wait + +import ( + "context" + "errors" + "fmt" + "io" + "log/slog" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" + "github.com/docker/go-sdk/container/exec" +) + +// Strategy defines the basic interface for a Wait Strategy +type Strategy interface { + WaitUntilReady(context.Context, StrategyTarget) error +} + +// StrategyTimeout allows MultiStrategy to configure a Strategy's Timeout +type StrategyTimeout interface { + Timeout() *time.Duration +} + +type StrategyTarget interface { + Host(context.Context) (string, error) + Inspect(context.Context) (*container.InspectResponse, error) + MappedPort(context.Context, nat.Port) (nat.Port, error) + Logs(context.Context) (io.ReadCloser, error) + Exec(context.Context, []string, ...exec.ProcessOption) (int, io.Reader, error) + State(context.Context) (*container.State, error) + CopyFromContainer(ctx context.Context, filePath string) (io.ReadCloser, error) + Logger() *slog.Logger +} + +func checkTarget(ctx context.Context, target StrategyTarget) error { + state, err := target.State(ctx) + if err != nil { + return fmt.Errorf("get state: %w", err) + } + + return checkState(state) +} + +func checkState(state *container.State) error { + switch { + case state.Running: + return nil + case state.OOMKilled: + return errors.New("container crashed with out-of-memory (OOMKilled)") + case state.Status == "exited": + return fmt.Errorf("container exited with code %d", state.ExitCode) + default: + return fmt.Errorf("unexpected container status %q", state.Status) + } +} + +func defaultTimeout() time.Duration { + return 60 * time.Second +} + +func defaultPollInterval() time.Duration { + return 100 * time.Millisecond +} diff --git a/vendor/github.com/docker/go-sdk/container/wait/walk.go b/vendor/github.com/docker/go-sdk/container/wait/walk.go new file mode 100644 index 00000000..7d65e2b8 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/container/wait/walk.go @@ -0,0 +1,74 @@ +package wait + +import ( + "errors" +) + +var ( + // ErrVisitStop is used as a return value from [VisitFunc] to stop the walk. + // It is not returned as an error by any function. + ErrVisitStop = errors.New("stop the walk") + + // ErrVisitRemove is used as a return value from [VisitFunc] to have the current node removed. + // It is not returned as an error by any function. + ErrVisitRemove = errors.New("remove this strategy") +) + +// VisitFunc is a function that visits a strategy node. +// If it returns [ErrVisitStop], the walk stops. +// If it returns [ErrVisitRemove], the current node is removed. +type VisitFunc func(root Strategy) error + +// Walk walks the strategies tree and calls the visit function for each node. +func Walk(root *Strategy, visit VisitFunc) error { + if root == nil { + return errors.New("root strategy is nil") + } + + if err := walk(root, visit); err != nil { + if errors.Is(err, ErrVisitRemove) || errors.Is(err, ErrVisitStop) { + return nil + } + return err + } + + return nil +} + +// walk walks the strategies tree and calls the visit function for each node. +// It returns an error if the visit function returns an error. +func walk(root *Strategy, visit VisitFunc) error { + if *root == nil { + // No strategy. + return nil + } + + // Allow the visit function to customize the behaviour of the walk before visiting the children. + if err := visit(*root); err != nil { + if errors.Is(err, ErrVisitRemove) { + *root = nil + } + + return err + } + + if s, ok := (*root).(*MultiStrategy); ok { + var i int + for range s.Strategies { + if err := walk(&s.Strategies[i], visit); err != nil { + if errors.Is(err, ErrVisitRemove) { + s.Strategies = append(s.Strategies[:i], s.Strategies[i+1:]...) + if errors.Is(err, ErrVisitStop) { + return ErrVisitStop + } + continue + } + + return err + } + i++ + } + } + + return nil +} diff --git a/vendor/github.com/docker/go-sdk/context/LICENSE b/vendor/github.com/docker/go-sdk/context/LICENSE new file mode 100644 index 00000000..a0e6f9bc --- /dev/null +++ b/vendor/github.com/docker/go-sdk/context/LICENSE @@ -0,0 +1,217 @@ +Copyright (c) 2025–present Docker, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +----------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/docker/go-sdk/context/Makefile b/vendor/github.com/docker/go-sdk/context/Makefile new file mode 100644 index 00000000..748cb213 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/context/Makefile @@ -0,0 +1 @@ +include ../commons-test.mk diff --git a/vendor/github.com/docker/go-sdk/context/README.md b/vendor/github.com/docker/go-sdk/context/README.md new file mode 100644 index 00000000..c8f92524 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/context/README.md @@ -0,0 +1,50 @@ +# Docker Contexts + +This package provides a simple API to discover Docker contexts. + +## Installation + +```bash +go get github.com/docker/go-sdk/context +``` + +## Usage + +### Current Context + +It will return the current Docker context name. + +```go +current, err := context.Current() +if err != nil { + log.Fatalf("failed to get current docker context: %v", err) +} + +fmt.Printf("current docker context: %s", current) +``` + +### Current Docker Host + +It will return the Docker host that the current context is configured to use. + +```go +dockerHost, err := context.CurrentDockerHost() +if err != nil { + log.Fatalf("failed to get current docker host: %v", err) +} +fmt.Printf("current docker host: %s", dockerHost) +``` + +### Docker Host From Context + +It will return the Docker host that the given context is configured to use. + +```go +dockerHost, err := context.DockerHostFromContext("desktop-linux") +if err != nil { + log.Printf("error getting docker host from context: %s", err) + return +} + +fmt.Printf("docker host from context: %s", dockerHost) +``` diff --git a/vendor/github.com/docker/go-sdk/context/context.go b/vendor/github.com/docker/go-sdk/context/context.go new file mode 100644 index 00000000..b0209cab --- /dev/null +++ b/vendor/github.com/docker/go-sdk/context/context.go @@ -0,0 +1,140 @@ +package context + +// The code in this file has been extracted from https://github.com/docker/cli, +// more especifically from https://github.com/docker/cli/blob/master/cli/context/store/metadatastore.go +// with the goal of not consuming the CLI package and all its dependencies. + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/docker/go-sdk/config" + "github.com/docker/go-sdk/context/internal" +) + +const ( + // DefaultContextName is the name reserved for the default context (config & env based) + DefaultContextName = "default" + + // EnvOverrideContext is the name of the environment variable that can be + // used to override the context to use. If set, it overrides the context + // that's set in the CLI's configuration file, but takes no effect if the + // "DOCKER_HOST" env-var is set (which takes precedence. + EnvOverrideContext = "DOCKER_CONTEXT" + + // EnvOverrideHost is the name of the environment variable that can be used + // to override the default host to connect to (DefaultDockerHost). + // + // This env-var is read by FromEnv and WithHostFromEnv and when set to a + // non-empty value, takes precedence over the default host (which is platform + // specific), or any host already set. + EnvOverrideHost = "DOCKER_HOST" + + // contextsDir is the name of the directory containing the contexts + contextsDir = "contexts" + + // metadataDir is the name of the directory containing the metadata + metadataDir = "meta" +) + +var ( + // DefaultDockerHost is the default host to connect to the Docker socket. + // The actual value is platform-specific and defined in host_linux.go and host_windows.go. + DefaultDockerHost = "" + + // ErrDockerHostNotSet is the error returned when the Docker host is not set in the Docker context + ErrDockerHostNotSet = internal.ErrDockerHostNotSet + + // ErrDockerContextNotFound is the error returned when the Docker context is not found. + ErrDockerContextNotFound = internal.ErrDockerContextNotFound +) + +// getContextFromEnv returns the context name from the environment variables. +func getContextFromEnv() string { + if os.Getenv(EnvOverrideHost) != "" { + return DefaultContextName + } + + if ctxName := os.Getenv(EnvOverrideContext); ctxName != "" { + return ctxName + } + + return "" +} + +// Current returns the current context name, based on +// environment variables and the cli configuration file. It does not +// validate if the given context exists or if it's valid. +// +// If the current context is not found, it returns the default context name. +func Current() (string, error) { + // Check env vars first (clearer precedence) + if ctx := getContextFromEnv(); ctx != "" { + return ctx, nil + } + + // Then check config + cfg, err := config.Load() + if err != nil { + if os.IsNotExist(err) { + return DefaultContextName, nil + } + return "", fmt.Errorf("load docker config: %w", err) + } + + if cfg.CurrentContext != "" { + return cfg.CurrentContext, nil + } + + return DefaultContextName, nil +} + +// CurrentDockerHost returns the Docker host from the current Docker context. +// For that, it traverses the directory structure of the Docker configuration directory, +// looking for the current context and its Docker endpoint. +// +// If the current context is the default context, it returns the value of the +// DOCKER_HOST environment variable. +func CurrentDockerHost() (string, error) { + current, err := Current() + if err != nil { + return "", fmt.Errorf("current context: %w", err) + } + + if current == DefaultContextName { + dockerHost := os.Getenv(EnvOverrideHost) + if dockerHost != "" { + return dockerHost, nil + } + + return DefaultDockerHost, nil + } + + metaRoot, err := metaRoot() + if err != nil { + return "", fmt.Errorf("meta root: %w", err) + } + + return internal.ExtractDockerHost(current, metaRoot) +} + +// DockerHostFromContext returns the Docker host from the given context. +func DockerHostFromContext(ctx string) (string, error) { + metaRoot, err := metaRoot() + if err != nil { + return "", fmt.Errorf("meta root: %w", err) + } + + return internal.ExtractDockerHost(ctx, metaRoot) +} + +// metaRoot returns the root directory of the Docker context metadata. +func metaRoot() (string, error) { + dir, err := config.Dir() + if err != nil { + return "", fmt.Errorf("docker config dir: %w", err) + } + + return filepath.Join(dir, contextsDir, metadataDir), nil +} diff --git a/vendor/github.com/docker/go-sdk/context/host_unix.go b/vendor/github.com/docker/go-sdk/context/host_unix.go new file mode 100644 index 00000000..51337e70 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/context/host_unix.go @@ -0,0 +1,9 @@ +//go:build !windows +// +build !windows + +package context + +func init() { + // DefaultDockerHost is the default host to connect to the Docker socket on Linux + DefaultDockerHost = "unix:///var/run/docker.sock" +} diff --git a/vendor/github.com/docker/go-sdk/context/host_windows.go b/vendor/github.com/docker/go-sdk/context/host_windows.go new file mode 100644 index 00000000..305381ac --- /dev/null +++ b/vendor/github.com/docker/go-sdk/context/host_windows.go @@ -0,0 +1,9 @@ +//go:build windows +// +build windows + +package context + +func init() { + // DefaultDockerHost is the default host to connect to the Docker socket on Windows + DefaultDockerHost = "npipe:////./pipe/docker_engine" +} diff --git a/vendor/github.com/docker/go-sdk/context/internal/context.go b/vendor/github.com/docker/go-sdk/context/internal/context.go new file mode 100644 index 00000000..7866af72 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/context/internal/context.go @@ -0,0 +1,114 @@ +package internal + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" +) + +const metaFile = "meta.json" + +// dockerContext represents the metadata stored for a context +type dockerContext struct { + Description string + Fields map[string]any // renamed from AdditionalFields for brevity +} + +// endpoint represents a Docker endpoint configuration +type endpoint struct { + Host string `json:",omitempty"` + SkipTLSVerify bool +} + +// metadata represents a complete context configuration +type metadata struct { + Name string `json:",omitempty"` + Context *dockerContext `json:"metadata,omitempty"` + Endpoints map[string]*endpoint `json:"endpoints,omitempty"` +} + +// store manages Docker context metadata files +type store struct { + root string +} + +// ExtractDockerHost extracts the Docker host from the given Docker context +func ExtractDockerHost(contextName string, metaRoot string) (string, error) { + s := &store{root: metaRoot} + + contexts, err := s.list() + if err != nil { + return "", fmt.Errorf("list contexts: %w", err) + } + + for _, ctx := range contexts { + if ctx.Name == contextName { + ep, ok := ctx.Endpoints["docker"] + if !ok || ep == nil || ep.Host == "" { // Check all conditions that should trigger the error + return "", ErrDockerHostNotSet + } + return ep.Host, nil + } + } + return "", ErrDockerContextNotFound +} + +func (s *store) list() ([]*metadata, error) { + dirs, err := s.findMetadataDirs(s.root) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } + return nil, fmt.Errorf("find contexts: %w", err) + } + + var contexts []*metadata + for _, dir := range dirs { + ctx, err := s.load(dir) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + continue + } + return nil, fmt.Errorf("load context %s: %w", dir, err) + } + contexts = append(contexts, ctx) + } + return contexts, nil +} + +func (s *store) load(dir string) (*metadata, error) { + data, err := os.ReadFile(filepath.Join(dir, metaFile)) + if err != nil { + return nil, err + } + + var meta metadata + if err := json.Unmarshal(data, &meta); err != nil { + return nil, fmt.Errorf("parse metadata: %w", err) + } + return &meta, nil +} + +func (s *store) findMetadataDirs(root string) ([]string, error) { + var dirs []string + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + if hasMetaFile(path) { + dirs = append(dirs, path) + return filepath.SkipDir // don't recurse into context dirs + } + } + return nil + }) + return dirs, err +} + +func hasMetaFile(dir string) bool { + info, err := os.Stat(filepath.Join(dir, metaFile)) + return err == nil && !info.IsDir() +} diff --git a/vendor/github.com/docker/go-sdk/context/internal/errors.go b/vendor/github.com/docker/go-sdk/context/internal/errors.go new file mode 100644 index 00000000..26e053f4 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/context/internal/errors.go @@ -0,0 +1,11 @@ +package internal + +import "errors" + +var ( + // ErrDockerHostNotSet is returned when the Docker host is not set in the Docker context. + ErrDockerHostNotSet = errors.New("docker host not set in Docker context") + + // ErrDockerContextNotFound is returned when the Docker context is not found. + ErrDockerContextNotFound = errors.New("docker context not found") +) diff --git a/vendor/github.com/docker/go-sdk/context/testing.go b/vendor/github.com/docker/go-sdk/context/testing.go new file mode 100644 index 00000000..f8b7ea6c --- /dev/null +++ b/vendor/github.com/docker/go-sdk/context/testing.go @@ -0,0 +1,88 @@ +package context + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/docker/go-sdk/config" +) + +// SetupTestDockerContexts creates a temporary directory structure for testing the Docker context functions. +// It creates the following structure, where $i is the index of the context, starting from 1: +// - $HOME/.docker +// - config.json +// - contexts +// - meta +// - context$i +// - meta.json +// +// The config.json file contains the current context, and the meta.json files contain the metadata for each context. +// It generates the specified number of contexts, setting the current context to the one specified by currentContextIndex. +// The docker host for each context is "tcp://127.0.0.1:$i". +// Finally it always adds a context with an empty host, to validate the behavior when the host is not set. +// This empty context can be used setting the currentContextIndex to a number greater than contextsCount. +func SetupTestDockerContexts(tb testing.TB, currentContextIndex int, contextsCount int) { + tb.Helper() + + tmpDir := tb.TempDir() + tb.Setenv("HOME", tmpDir) + tb.Setenv("USERPROFILE", tmpDir) // Windows support + + tempMkdirAll(tb, filepath.Join(tmpDir, ".docker")) + + configDir, err := config.Dir() + require.NoError(tb, err) + + configJSON := filepath.Join(configDir, config.FileName) + + const baseContext = "context" + + // default config.json with no current context + configBytes := `{"currentContext": ""}` + + if currentContextIndex <= contextsCount { + configBytes = fmt.Sprintf(`{ + "currentContext": "%s%d" +}`, baseContext, currentContextIndex) + } + + err = os.WriteFile(configJSON, []byte(configBytes), 0o644) + require.NoError(tb, err) + + metaDir, err := metaRoot() + require.NoError(tb, err) + + tempMkdirAll(tb, metaDir) + + // first index is 1 + for i := 1; i <= contextsCount; i++ { + createDockerContext(tb, metaDir, baseContext, i, fmt.Sprintf("tcp://127.0.0.1:%d", i)) + } + + // add a context with no host + createDockerContext(tb, metaDir, baseContext, contextsCount+1, "") +} + +// createDockerContext creates a Docker context with the specified name and host +func createDockerContext(tb testing.TB, metaDir, baseContext string, index int, host string) { + tb.Helper() + + contextDir := filepath.Join(metaDir, fmt.Sprintf("context%d", index)) + tempMkdirAll(tb, contextDir) + + context := fmt.Sprintf(`{"Name":"%s%d","Metadata":{"Description":"Docker Go SDK %d"},"Endpoints":{"docker":{"Host":"%s","SkipTLSVerify":false}}}`, + baseContext, index, index, host) + err := os.WriteFile(filepath.Join(contextDir, "meta.json"), []byte(context), 0o644) + require.NoError(tb, err) +} + +func tempMkdirAll(tb testing.TB, dir string) { + tb.Helper() + + err := os.MkdirAll(dir, 0o755) + require.NoError(tb, err) +} diff --git a/vendor/github.com/docker/go-sdk/context/version.go b/vendor/github.com/docker/go-sdk/context/version.go new file mode 100644 index 00000000..ebb47a2b --- /dev/null +++ b/vendor/github.com/docker/go-sdk/context/version.go @@ -0,0 +1,10 @@ +package context + +const ( + version = "0.1.0-alpha006" +) + +// Version returns the version of the context package. +func Version() string { + return version +} diff --git a/vendor/github.com/docker/go-sdk/image/LICENSE b/vendor/github.com/docker/go-sdk/image/LICENSE new file mode 100644 index 00000000..a0e6f9bc --- /dev/null +++ b/vendor/github.com/docker/go-sdk/image/LICENSE @@ -0,0 +1,217 @@ +Copyright (c) 2025–present Docker, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +----------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/docker/go-sdk/image/Makefile b/vendor/github.com/docker/go-sdk/image/Makefile new file mode 100644 index 00000000..748cb213 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/image/Makefile @@ -0,0 +1 @@ +include ../commons-test.mk diff --git a/vendor/github.com/docker/go-sdk/image/README.md b/vendor/github.com/docker/go-sdk/image/README.md new file mode 100644 index 00000000..30e092ae --- /dev/null +++ b/vendor/github.com/docker/go-sdk/image/README.md @@ -0,0 +1,56 @@ +# Docker Images + +This package provides a simple API to create and manage Docker images. + +## Installation + +```bash +go get github.com/docker/go-sdk/image +``` + +## Usage + +```go +err = image.Pull(ctx, "nginx:alpine") +if err != nil { + log.Fatalf("failed to pull image: %v", err) +} +``` + +## Customizing the Pull operation + +The Pull operation can be customized using functional options. The following options are available: + +- `WithPullClient(client *client.Client) image.PullOption`: The client to use to pull the image. If not provided, the default client will be used. +- `WithPullOptions(options apiimage.PullOptions) image.PullOption`: The options to use to pull the image. The type of the options is "github.com/docker/docker/api/types/image". + +First, you need to import the following packages: +```go +import ( + "context" + + apiimage "github.com/docker/docker/api/types/image" + "github.com/docker/go-sdk/client" + "github.com/docker/go-sdk/image" +) +``` + +And in your code: + +```go +ctx := context.Background() +dockerClient, err := client.New(ctx) +if err != nil { + log.Fatalf("failed to create docker client: %v", err) +} +defer dockerClient.Close() + +err = image.Pull(ctx, + "nginx:alpine", + image.WithPullClient(dockerClient), + image.WithPullOptions(apiimage.PullOptions{}), +) +if err != nil { + log.Fatalf("failed to pull image: %v", err) +} +``` diff --git a/vendor/github.com/docker/go-sdk/image/client.go b/vendor/github.com/docker/go-sdk/image/client.go new file mode 100644 index 00000000..643e3bb5 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/image/client.go @@ -0,0 +1,14 @@ +package image + +import ( + "log/slog" +) + +// ImageClient is a client to perform operations on images. +type ImageClient interface { + // Close closes the client. + Close() error + + // Logger returns the logger. + Logger() *slog.Logger +} diff --git a/vendor/github.com/docker/go-sdk/image/dockerfiles.go b/vendor/github.com/docker/go-sdk/image/dockerfiles.go new file mode 100644 index 00000000..d17eeed4 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/image/dockerfiles.go @@ -0,0 +1,57 @@ +package image + +import ( + "bufio" + "io" + "os" + "strings" +) + +// ImagesFromDockerfile extracts images from the Dockerfile sourced from dockerfile. +func ImagesFromDockerfile(dockerfile string, buildArgs map[string]*string) ([]string, error) { + file, err := os.Open(dockerfile) + if err != nil { + return nil, err + } + defer file.Close() + + return ImagesFromReader(file, buildArgs) +} + +// ImagesFromReader extracts images from the Dockerfile sourced from r. +func ImagesFromReader(r io.Reader, buildArgs map[string]*string) ([]string, error) { + var images []string + var lines []string + scanner := bufio.NewScanner(r) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + if scanner.Err() != nil { + return nil, scanner.Err() + } + + // extract images from dockerfile + for _, line := range lines { + line = strings.TrimSpace(line) + if !strings.HasPrefix(strings.ToUpper(line), "FROM") { + continue + } + + // remove FROM + line = strings.TrimPrefix(line, "FROM") + parts := strings.Split(strings.TrimSpace(line), " ") + if len(parts) == 0 { + continue + } + + // interpolate build args + for k, v := range buildArgs { + if v != nil { + parts[0] = strings.ReplaceAll(parts[0], "${"+k+"}", *v) + } + } + images = append(images, parts[0]) + } + + return images, nil +} diff --git a/vendor/github.com/docker/go-sdk/image/options.go b/vendor/github.com/docker/go-sdk/image/options.go new file mode 100644 index 00000000..accedd5a --- /dev/null +++ b/vendor/github.com/docker/go-sdk/image/options.go @@ -0,0 +1,79 @@ +package image + +import ( + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/docker/docker/api/types/image" +) + +// PullOption is a function that configures the pull options. +type PullOption func(*pullOptions) error + +type pullOptions struct { + pullClient ImagePullClient + pullOptions image.PullOptions +} + +// WithPullClient sets the pull client used to pull the image. +func WithPullClient(pullClient ImagePullClient) PullOption { + return func(opts *pullOptions) error { + opts.pullClient = pullClient + return nil + } +} + +// WithPullOptions sets the pull options used to pull the image. +func WithPullOptions(imagePullOptions image.PullOptions) PullOption { + return func(opts *pullOptions) error { + opts.pullOptions = imagePullOptions + return nil + } +} + +// RemoveOption is a function that configures the remove options. +type RemoveOption func(*removeOptions) error + +type removeOptions struct { + removeClient ImageRemoveClient + removeOptions image.RemoveOptions +} + +// WithRemoveClient sets the remove client used to remove the image. +func WithRemoveClient(removeClient ImageRemoveClient) RemoveOption { + return func(opts *removeOptions) error { + opts.removeClient = removeClient + return nil + } +} + +// WithRemoveOptions sets the remove options used to remove the image. +func WithRemoveOptions(options image.RemoveOptions) RemoveOption { + return func(opts *removeOptions) error { + opts.removeOptions = options + return nil + } +} + +// SaveOption is a function that configures the save options. +type SaveOption func(*saveOptions) error + +type saveOptions struct { + saveClient ImageSaveClient + platforms []ocispec.Platform +} + +// WithSaveClient sets the save client used to save the image. +func WithSaveClient(saveClient ImageSaveClient) SaveOption { + return func(opts *saveOptions) error { + opts.saveClient = saveClient + return nil + } +} + +// WithPlatforms sets the platforms to save the image from. +func WithPlatforms(platforms ...ocispec.Platform) SaveOption { + return func(opts *saveOptions) error { + opts.platforms = platforms + return nil + } +} diff --git a/vendor/github.com/docker/go-sdk/image/pull.go b/vendor/github.com/docker/go-sdk/image/pull.go new file mode 100644 index 00000000..25f47d57 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/image/pull.go @@ -0,0 +1,92 @@ +package image + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "time" + + "github.com/cenkalti/backoff/v4" + + "github.com/docker/docker/api/types/image" + "github.com/docker/go-sdk/client" + "github.com/docker/go-sdk/config" +) + +// ImagePullClient is a client that can pull images. +type ImagePullClient interface { + ImageClient + + // ImagePull pulls an image from a remote registry. + ImagePull(ctx context.Context, image string, options image.PullOptions) (io.ReadCloser, error) +} + +// Pull pulls an image from a remote registry, retrying on non-permanent errors. +// See [client.IsPermanentClientError] for the list of non-permanent errors. +// It first extracts the registry credentials from the image name, and sets them in the pull options. +// It needs to be called with a valid image name, and optional pull options, see [PullOption]. +func Pull(ctx context.Context, imageName string, opts ...PullOption) error { + pullOpts := &pullOptions{} + for _, opt := range opts { + if err := opt(pullOpts); err != nil { + return fmt.Errorf("apply pull option: %w", err) + } + } + + if pullOpts.pullClient == nil { + pullOpts.pullClient = client.DefaultClient + // In case there is no pull client set, we use the default docker client + // to pull the image. We need to close it when done. + defer pullOpts.pullClient.Close() + } + + if imageName == "" { + return errors.New("image name is not set") + } + + creds, err := config.RegistryCredentials(imageName) + if err != nil { + pullOpts.pullClient.Logger().Warn("failed to get image auth, setting empty credentials for the image", "image", imageName, "error", err) + } else { + authConfig := config.AuthConfig{ + Username: creds.Username, + Password: creds.Password, + } + encodedJSON, err := json.Marshal(authConfig) + if err != nil { + pullOpts.pullClient.Logger().Warn("failed to marshal image auth, setting empty credentials for the image", "image", imageName, "error", err) + } else { + pullOpts.pullOptions.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON) + } + } + + var pull io.ReadCloser + err = backoff.RetryNotify( + func() error { + pull, err = pullOpts.pullClient.ImagePull(ctx, imageName, pullOpts.pullOptions) + if err != nil { + if client.IsPermanentClientError(err) { + return backoff.Permanent(err) + } + return err + } + + return nil + }, + backoff.WithContext(backoff.NewExponentialBackOff(), ctx), + func(err error, _ time.Duration) { + pullOpts.pullClient.Logger().Warn("failed to pull image, will retry", "error", err) + }, + ) + if err != nil { + return err + } + defer pull.Close() + + // download of docker image finishes at EOF of the pull request + _, err = io.ReadAll(pull) + return err +} diff --git a/vendor/github.com/docker/go-sdk/image/remove.go b/vendor/github.com/docker/go-sdk/image/remove.go new file mode 100644 index 00000000..cf9661c9 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/image/remove.go @@ -0,0 +1,43 @@ +package image + +import ( + "context" + "errors" + "fmt" + + "github.com/docker/docker/api/types/image" + "github.com/docker/go-sdk/client" +) + +// ImageRemoveClient is a client that can remove images. +type ImageRemoveClient interface { + ImageClient + + // ImageRemove removes an image from the local repository. + ImageRemove(context.Context, string, image.RemoveOptions) ([]image.DeleteResponse, error) +} + +// Remove removes an image from the local repository. +func Remove(ctx context.Context, image string, opts ...RemoveOption) ([]image.DeleteResponse, error) { + removeOpts := &removeOptions{} + for _, opt := range opts { + if err := opt(removeOpts); err != nil { + return nil, fmt.Errorf("apply remove option: %w", err) + } + } + + if image == "" { + return nil, errors.New("image is required") + } + + if removeOpts.removeClient == nil { + removeOpts.removeClient = client.DefaultClient + } + + resp, err := removeOpts.removeClient.ImageRemove(ctx, image, removeOpts.removeOptions) + if err != nil { + return nil, fmt.Errorf("remove image: %w", err) + } + + return resp, nil +} diff --git a/vendor/github.com/docker/go-sdk/image/save.go b/vendor/github.com/docker/go-sdk/image/save.go new file mode 100644 index 00000000..2b57132f --- /dev/null +++ b/vendor/github.com/docker/go-sdk/image/save.go @@ -0,0 +1,71 @@ +package image + +import ( + "context" + "errors" + "fmt" + "io" + "os" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + dockerclient "github.com/docker/docker/client" + "github.com/docker/go-sdk/client" +) + +// ImageSaveClient is a client that can save images. +type ImageSaveClient interface { + ImageClient + + // ImageSave saves an image to a file. + ImageSave(ctx context.Context, images []string, saveOptions ...dockerclient.ImageSaveOption) (io.ReadCloser, error) +} + +// Save saves an image to a file. +func Save(ctx context.Context, output string, img string, opts ...SaveOption) error { + saveOpts := &saveOptions{ + platforms: []ocispec.Platform{}, + } + for _, opt := range opts { + if err := opt(saveOpts); err != nil { + return fmt.Errorf("apply save option: %w", err) + } + } + + if output == "" { + return errors.New("output is not set") + } + if img == "" { + return errors.New("image cannot be empty") + } + + if saveOpts.saveClient == nil { + saveOpts.saveClient = client.DefaultClient + } + + outputFile, err := os.Create(output) + if err != nil { + return fmt.Errorf("open output file %w", err) + } + defer func() { + _ = outputFile.Close() + }() + + imgSaveOpts := dockerclient.ImageSaveWithPlatforms(saveOpts.platforms...) + + imageReader, err := saveOpts.saveClient.ImageSave(ctx, []string{img}, imgSaveOpts) + if err != nil { + return fmt.Errorf("save images %w", err) + } + defer func() { + _ = imageReader.Close() + }() + + // Attempt optimized readFrom, implemented in linux + _, err = outputFile.ReadFrom(imageReader) + if err != nil { + return fmt.Errorf("write images to output %w", err) + } + + return nil +} diff --git a/vendor/github.com/docker/go-sdk/image/version.go b/vendor/github.com/docker/go-sdk/image/version.go new file mode 100644 index 00000000..d7c1907f --- /dev/null +++ b/vendor/github.com/docker/go-sdk/image/version.go @@ -0,0 +1,10 @@ +package image + +const ( + version = "0.1.0-alpha006" +) + +// Version returns the version of the image package. +func Version() string { + return version +} diff --git a/vendor/github.com/docker/go-sdk/network/LICENSE b/vendor/github.com/docker/go-sdk/network/LICENSE new file mode 100644 index 00000000..a0e6f9bc --- /dev/null +++ b/vendor/github.com/docker/go-sdk/network/LICENSE @@ -0,0 +1,217 @@ +Copyright (c) 2025–present Docker, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +----------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/docker/go-sdk/network/Makefile b/vendor/github.com/docker/go-sdk/network/Makefile new file mode 100644 index 00000000..748cb213 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/network/Makefile @@ -0,0 +1 @@ +include ../commons-test.mk diff --git a/vendor/github.com/docker/go-sdk/network/README.md b/vendor/github.com/docker/go-sdk/network/README.md new file mode 100644 index 00000000..9860755f --- /dev/null +++ b/vendor/github.com/docker/go-sdk/network/README.md @@ -0,0 +1,63 @@ +# Docker Networks + +This package provides a simple API to create and manage Docker networks. + +## Installation + +```bash +go get github.com/docker/go-sdk/network +``` + +## Usage + +```go +nw, err := network.New(ctx) +if err != nil { + log.Fatalf("failed to create network: %v", err) +} + +resp, err := nw.Inspect(ctx) +if err != nil { + log.Fatalf("failed to inspect network: %v", err) +} + +fmt.Printf("network: %+v", resp) + +inspect, err := network.GetByID(ctx, nw.ID()) +if err != nil { + log.Fatalf("failed to get network by id: %v", err) +} + +inspect, err = network.GetByName(ctx, nw.Name()) +if err != nil { + log.Fatalf("failed to get network by name: %v", err) +} + +_, err = network.List(ctx) +if err != nil { + log.Fatalf("failed to list networks: %v", err) +} + +_, err = network.List(ctx, network.WithFilters(filters.NewArgs(filters.Arg("driver", "bridge")))) +if err != nil { + log.Fatalf("failed to list networks with filters: %v", err) +} + +err = nw.Terminate(ctx) +if err != nil { + log.Fatalf("failed to terminate network: %v", err) +} +``` + +## Customizing the network + +The network created with the `New` function can be customized using functional options. The following options are available: + +- `WithClient(client *client.Client) network.Option`: The client to use to create the network. If not provided, the default client will be used. +- `WithName(name string) network.Option`: The name of the network. +- `WithDriver(driver string) network.Option`: The driver of the network. +- `WithInternal() network.Option`: Whether the network is internal. +- `WithEnableIPv6() network.Option`: Whether the network is IPv6 enabled. +- `WithAttachable() network.Option`: Whether the network is attachable. +- `WithLabels(labels map[string]string) network.Option`: The labels of the network. +- `WithIPAM(ipam *network.IPAM) network.Option`: The IPAM configuration of the network. diff --git a/vendor/github.com/docker/go-sdk/network/network.go b/vendor/github.com/docker/go-sdk/network/network.go new file mode 100644 index 00000000..718bfb57 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/network/network.go @@ -0,0 +1,59 @@ +package network + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/docker/docker/api/types/network" + "github.com/docker/go-sdk/client" +) + +// New creates a new network. +func New(ctx context.Context, opts ...Option) (*Network, error) { + networkOptions := &options{ + labels: make(map[string]string), + } + + for _, opt := range opts { + if err := opt(networkOptions); err != nil { + return nil, fmt.Errorf("apply option: %w", err) + } + } + + if networkOptions.name == "" { + networkOptions.name = uuid.New().String() + } + + if networkOptions.client == nil { + networkOptions.client = client.DefaultClient + } + + client.AddSDKLabels(networkOptions.labels) + + nc := network.CreateOptions{ + Driver: networkOptions.driver, + Internal: networkOptions.internal, + EnableIPv6: &networkOptions.enableIPv6, + Attachable: networkOptions.attachable, + Labels: networkOptions.labels, + IPAM: networkOptions.ipam, + } + + resp, err := networkOptions.client.NetworkCreate(ctx, networkOptions.name, nc) + if err != nil { + return nil, fmt.Errorf("create network: %w", err) + } + + if resp.Warning != "" { + networkOptions.client.Logger().Warn("warning creating network", "message", resp.Warning) + } + + return &Network{ + response: resp, + name: networkOptions.name, + opts: networkOptions, + dockerClient: networkOptions.client, + }, nil +} diff --git a/vendor/github.com/docker/go-sdk/network/network.inspect.go b/vendor/github.com/docker/go-sdk/network/network.inspect.go new file mode 100644 index 00000000..4a41bbfa --- /dev/null +++ b/vendor/github.com/docker/go-sdk/network/network.inspect.go @@ -0,0 +1,71 @@ +package network + +import ( + "context" + "errors" + + "github.com/docker/docker/api/types/network" +) + +type inspectOptions struct { + cache bool + options network.InspectOptions +} + +// InspectOptions is a function that modifies the inspect options. +type InspectOptions func(opts *inspectOptions) error + +// WithNoCache returns an InspectOptions that disables caching the result of the inspection. +// If passed, the Docker daemon will be queried for the latest information, so it can be +// used for refreshing the cached result of a previous inspection. +func WithNoCache() InspectOptions { + return func(o *inspectOptions) error { + o.cache = false + return nil + } +} + +// WithInspectOptions returns an InspectOptions that sets the inspect options. +func WithInspectOptions(opts network.InspectOptions) InspectOptions { + return func(o *inspectOptions) error { + o.options = opts + return nil + } +} + +// Inspect inspects the network, caching the results. +func (n *Network) Inspect(ctx context.Context, opts ...InspectOptions) (network.Inspect, error) { + var zero network.Inspect + if n.dockerClient == nil { + return zero, errors.New("docker client is not initialized") + } + + inspectOptions := &inspectOptions{ + cache: true, // cache the result by default + } + for _, opt := range opts { + if err := opt(inspectOptions); err != nil { + return zero, err + } + } + + if inspectOptions.cache { + // if the result was already cached, return it + if n.inspect.ID != "" { + return n.inspect, nil + } + + // else, log a warning and inspect the network + n.dockerClient.Logger().Warn("network not inspected yet, inspecting now", "network", n.ID(), "cache", inspectOptions.cache) + } + + inspect, err := n.dockerClient.NetworkInspect(ctx, n.ID(), inspectOptions.options) + if err != nil { + return zero, err + } + + // cache the result for subsequent calls + n.inspect = inspect + + return inspect, nil +} diff --git a/vendor/github.com/docker/go-sdk/network/network.list.go b/vendor/github.com/docker/go-sdk/network/network.list.go new file mode 100644 index 00000000..ca53d948 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/network/network.list.go @@ -0,0 +1,106 @@ +package network + +import ( + "context" + "errors" + "fmt" + + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/network" + "github.com/docker/go-sdk/client" +) + +const ( + // filterByID uses to filter network by identifier. + filterByID = "id" + + // filterByName uses to filter network by name. + filterByName = "name" +) + +type listOptions struct { + dockerClient *client.Client + filters filters.Args +} + +type ListOptions func(opts *listOptions) error + +// WithDockerClient sets the docker client to be used to list the networks. +func WithDockerClient(client *client.Client) ListOptions { + return func(opts *listOptions) error { + opts.dockerClient = client + return nil + } +} + +// WithFilters sets the filters to be used to filter the networks. +func WithFilters(filters filters.Args) ListOptions { + return func(opts *listOptions) error { + opts.filters = filters + return nil + } +} + +// GetByID returns a network by its ID. +func GetByID(ctx context.Context, id string, opts ...ListOptions) (network.Inspect, error) { + opts = append(opts, WithFilters(filters.NewArgs(filters.Arg(filterByID, id)))) + + nws, err := list(ctx, opts...) + if err != nil { + return network.Inspect{}, err + } + + return nws[0], nil +} + +// GetByName returns a network by its name. +func GetByName(ctx context.Context, name string, opts ...ListOptions) (network.Inspect, error) { + opts = append(opts, WithFilters(filters.NewArgs(filters.Arg(filterByName, name)))) + + nws, err := list(ctx, opts...) + if err != nil { + return network.Inspect{}, err + } + + return nws[0], nil +} + +// List returns a list of networks. +func List(ctx context.Context, opts ...ListOptions) ([]network.Inspect, error) { + return list(ctx, opts...) +} + +func list(ctx context.Context, opts ...ListOptions) ([]network.Inspect, error) { + var nws []network.Inspect // initialize to the zero value + + initialOpts := &listOptions{ + filters: filters.NewArgs(), + } + for _, opt := range opts { + if err := opt(initialOpts); err != nil { + return nws, err + } + } + + nwOpts := network.ListOptions{} + if initialOpts.filters.Len() > 0 { + nwOpts.Filters = initialOpts.filters + } + + if initialOpts.dockerClient == nil { + initialOpts.dockerClient = client.DefaultClient + } + + list, err := initialOpts.dockerClient.NetworkList(ctx, nwOpts) + if err != nil { + return nws, fmt.Errorf("failed to list networks: %w", err) + } + + if len(list) == 0 { + return nws, errors.New("no networks found") + } + + nws = append(nws, list...) + + return nws, nil +} diff --git a/vendor/github.com/docker/go-sdk/network/network.terminate.go b/vendor/github.com/docker/go-sdk/network/network.terminate.go new file mode 100644 index 00000000..c7ee7617 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/network/network.terminate.go @@ -0,0 +1,41 @@ +package network + +import ( + "context" + "errors" + "fmt" + "reflect" +) + +// TerminableNetwork is a network that can be terminated. +type TerminableNetwork interface { + Terminate(ctx context.Context) error +} + +// Terminate is used to remove the network. It is usually triggered by as defer function. +func (n *Network) Terminate(ctx context.Context) error { + if n.dockerClient == nil { + return errors.New("docker client is not initialized") + } + + if err := n.dockerClient.NetworkRemove(ctx, n.ID()); err != nil { + return fmt.Errorf("terminate network: %w", err) + } + + return nil +} + +// isNil returns true if val is nil or a nil instance false otherwise. +func isNil(val any) bool { + if val == nil { + return true + } + + valueOf := reflect.ValueOf(val) + switch valueOf.Kind() { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice: + return valueOf.IsNil() + default: + return false + } +} diff --git a/vendor/github.com/docker/go-sdk/network/options.go b/vendor/github.com/docker/go-sdk/network/options.go new file mode 100644 index 00000000..89c51702 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/network/options.go @@ -0,0 +1,90 @@ +package network + +import ( + "errors" + + "github.com/docker/docker/api/types/network" + "github.com/docker/go-sdk/client" +) + +type options struct { + client *client.Client + ipam *network.IPAM + labels map[string]string + driver string + name string + attachable bool + enableIPv6 bool + internal bool +} + +// Option is a function that modifies the options to create a network. +type Option func(*options) error + +// WithClient sets the docker client. +func WithClient(client *client.Client) Option { + return func(o *options) error { + o.client = client + return nil + } +} + +// WithName sets the name of the network. +func WithName(name string) Option { + return func(o *options) error { + if name == "" { + return errors.New("name is required") + } + + o.name = name + return nil + } +} + +// WithDriver sets the driver of the network. +func WithDriver(driver string) Option { + return func(o *options) error { + o.driver = driver + return nil + } +} + +// WithInternal makes the network internal. +func WithInternal() Option { + return func(o *options) error { + o.internal = true + return nil + } +} + +// WithEnableIPv6 enables IPv6 on the network. +func WithEnableIPv6() Option { + return func(o *options) error { + o.enableIPv6 = true + return nil + } +} + +// WithAttachable makes the network attachable. +func WithAttachable() Option { + return func(o *options) error { + o.attachable = true + return nil + } +} + +// WithLabels sets the labels of the network. +func WithLabels(labels map[string]string) Option { + return func(o *options) error { + o.labels = labels + return nil + } +} + +// WithIPAM sets the IPAM of the network. +func WithIPAM(ipam *network.IPAM) Option { + return func(o *options) error { + o.ipam = ipam + return nil + } +} diff --git a/vendor/github.com/docker/go-sdk/network/testing.go b/vendor/github.com/docker/go-sdk/network/testing.go new file mode 100644 index 00000000..be0eda83 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/network/testing.go @@ -0,0 +1,121 @@ +package network + +import ( + "context" + "regexp" + "testing" + + "github.com/containerd/errdefs" + "github.com/stretchr/testify/require" + + "github.com/docker/docker/api/types/network" + "github.com/docker/go-sdk/client" +) + +// errAlreadyInProgress is a regular expression that matches the error for a network +// removal that is already in progress. +var errAlreadyInProgress = regexp.MustCompile(`removal of network .* is already in progress`) + +// causer is an interface that allows to get the cause of an error. +type causer interface { + Cause() error +} + +// wrapErr is an interface that allows to unwrap an error. +type wrapErr interface { + Unwrap() error +} + +// unwrapErrs is an interface that allows to unwrap multiple errors. +type unwrapErrs interface { + Unwrap() []error +} + +// Cleanup is a helper function that schedules the network to be +// removed when the test ends. +// This should be the first call after [New] in a test before +// any error check. If network is nil, it's a no-op. +func Cleanup(tb testing.TB, nw TerminableNetwork) { + tb.Helper() + + tb.Cleanup(func() { + if !isNil(nw) { + noErrorOrIgnored(tb, nw.Terminate(context.Background())) + } + }) +} + +// CleanupByID is a helper function that schedules the network to be +// removed, identified by its ID, when the test ends. +// This should be the first call after NewNetwork(...) in a test before +// any error check. If network is nil, it's a no-op. +// It uses a new docker client to terminate the network, which is automatically +// closed when the test ends. +func CleanupByID(tb testing.TB, id string) { + tb.Helper() + + dockerClient, err := client.New(context.Background()) + if err != nil { + noErrorOrIgnored(tb, err) + } + + // synthetic network using a new docker client. + nw := &Network{ + response: network.CreateResponse{ + ID: id, + }, + dockerClient: dockerClient, + } + tb.Cleanup(func() { + noErrorOrIgnored(tb, dockerClient.Close()) + }) + + Cleanup(tb, nw) +} + +// isCleanupSafe checks if an error is cleanup safe. +func isCleanupSafe(err error) bool { + if err == nil { + return true + } + + // First try with containerd's errdefs + switch { + case errdefs.IsNotFound(err): + return true + case errdefs.IsConflict(err): + // Terminating a container that is already terminating. + if errAlreadyInProgress.MatchString(err.Error()) { + return true + } + return false + } + + switch x := err.(type) { //nolint:errorlint // We need to check for interfaces. + case causer: + return isCleanupSafe(x.Cause()) + case wrapErr: + return isCleanupSafe(x.Unwrap()) + case unwrapErrs: + for _, e := range x.Unwrap() { + if !isCleanupSafe(e) { + return false + } + } + return true + default: + return false + } +} + +// noErrorOrIgnored is a helper function that checks if the error is nil or an error +// we can ignore. +func noErrorOrIgnored(tb testing.TB, err error) { + tb.Helper() + + if isCleanupSafe(err) { + return + } + + require.NoError(tb, err) +} diff --git a/vendor/github.com/docker/go-sdk/network/types.go b/vendor/github.com/docker/go-sdk/network/types.go new file mode 100644 index 00000000..a1e9d771 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/network/types.go @@ -0,0 +1,30 @@ +package network + +import ( + "github.com/docker/docker/api/types/network" + "github.com/docker/go-sdk/client" +) + +// Network represents a Docker network. +type Network struct { + response network.CreateResponse + inspect network.Inspect + dockerClient *client.Client + opts *options + name string +} + +// ID returns the ID of the network. +func (n *Network) ID() string { + return n.response.ID +} + +// Driver returns the driver of the network. +func (n *Network) Driver() string { + return n.opts.driver +} + +// Name returns the name of the network. +func (n *Network) Name() string { + return n.name +} diff --git a/vendor/github.com/docker/go-sdk/network/version.go b/vendor/github.com/docker/go-sdk/network/version.go new file mode 100644 index 00000000..f4b32949 --- /dev/null +++ b/vendor/github.com/docker/go-sdk/network/version.go @@ -0,0 +1,10 @@ +package network + +const ( + version = "0.1.0-alpha006" +) + +// Version returns the version of the network package. +func Version() string { + return version +} diff --git a/vendor/github.com/grpc-ecosystem/grpc-gateway/v2/runtime/errors.go b/vendor/github.com/grpc-ecosystem/grpc-gateway/v2/runtime/errors.go index 41cd4f50..bbe7decf 100644 --- a/vendor/github.com/grpc-ecosystem/grpc-gateway/v2/runtime/errors.go +++ b/vendor/github.com/grpc-ecosystem/grpc-gateway/v2/runtime/errors.go @@ -148,22 +148,20 @@ func DefaultHTTPErrorHandler(ctx context.Context, mux *ServeMux, marshaler Marsh } md, ok := ServerMetadataFromContext(ctx) - if !ok { - grpclog.Error("Failed to extract ServerMetadata from context") - } - - handleForwardResponseServerMetadata(w, mux, md) - - // RFC 7230 https://tools.ietf.org/html/rfc7230#section-4.1.2 - // Unless the request includes a TE header field indicating "trailers" - // is acceptable, as described in Section 4.3, a server SHOULD NOT - // generate trailer fields that it believes are necessary for the user - // agent to receive. - doForwardTrailers := requestAcceptsTrailers(r) - - if doForwardTrailers { - handleForwardResponseTrailerHeader(w, mux, md) - w.Header().Set("Transfer-Encoding", "chunked") + if ok { + handleForwardResponseServerMetadata(w, mux, md) + + // RFC 7230 https://tools.ietf.org/html/rfc7230#section-4.1.2 + // Unless the request includes a TE header field indicating "trailers" + // is acceptable, as described in Section 4.3, a server SHOULD NOT + // generate trailer fields that it believes are necessary for the user + // agent to receive. + doForwardTrailers := requestAcceptsTrailers(r) + + if doForwardTrailers { + handleForwardResponseTrailerHeader(w, mux, md) + w.Header().Set("Transfer-Encoding", "chunked") + } } st := HTTPStatusFromCode(s.Code()) @@ -176,7 +174,7 @@ func DefaultHTTPErrorHandler(ctx context.Context, mux *ServeMux, marshaler Marsh grpclog.Errorf("Failed to write response: %v", err) } - if doForwardTrailers { + if ok && requestAcceptsTrailers(r) { handleForwardResponseTrailer(w, mux, md) } } diff --git a/vendor/github.com/grpc-ecosystem/grpc-gateway/v2/runtime/handler.go b/vendor/github.com/grpc-ecosystem/grpc-gateway/v2/runtime/handler.go index f0727cf7..2f0b9e9e 100644 --- a/vendor/github.com/grpc-ecosystem/grpc-gateway/v2/runtime/handler.go +++ b/vendor/github.com/grpc-ecosystem/grpc-gateway/v2/runtime/handler.go @@ -153,12 +153,10 @@ type responseBody interface { // ForwardResponseMessage forwards the message "resp" from gRPC server to REST client. func ForwardResponseMessage(ctx context.Context, mux *ServeMux, marshaler Marshaler, w http.ResponseWriter, req *http.Request, resp proto.Message, opts ...func(context.Context, http.ResponseWriter, proto.Message) error) { md, ok := ServerMetadataFromContext(ctx) - if !ok { - grpclog.Error("Failed to extract ServerMetadata from context") + if ok { + handleForwardResponseServerMetadata(w, mux, md) } - handleForwardResponseServerMetadata(w, mux, md) - // RFC 7230 https://tools.ietf.org/html/rfc7230#section-4.1.2 // Unless the request includes a TE header field indicating "trailers" // is acceptable, as described in Section 4.3, a server SHOULD NOT @@ -166,7 +164,7 @@ func ForwardResponseMessage(ctx context.Context, mux *ServeMux, marshaler Marsha // agent to receive. doForwardTrailers := requestAcceptsTrailers(req) - if doForwardTrailers { + if ok && doForwardTrailers { handleForwardResponseTrailerHeader(w, mux, md) w.Header().Set("Transfer-Encoding", "chunked") } @@ -204,7 +202,7 @@ func ForwardResponseMessage(ctx context.Context, mux *ServeMux, marshaler Marsha grpclog.Errorf("Failed to write response: %v", err) } - if doForwardTrailers { + if ok && doForwardTrailers { handleForwardResponseTrailer(w, mux, md) } } diff --git a/vendor/go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/tracetransform/attribute.go b/vendor/go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/tracetransform/attribute.go index 4571a5ca..ca4544f0 100644 --- a/vendor/go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/tracetransform/attribute.go +++ b/vendor/go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/tracetransform/attribute.go @@ -1,6 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +// Package tracetransform provides conversion functionality for the otlptrace +// exporters. package tracetransform // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/tracetransform" import ( diff --git a/vendor/go.opentelemetry.io/otel/exporters/otlp/otlptrace/version.go b/vendor/go.opentelemetry.io/otel/exporters/otlp/otlptrace/version.go index f5cad46b..5f78bfdf 100644 --- a/vendor/go.opentelemetry.io/otel/exporters/otlp/otlptrace/version.go +++ b/vendor/go.opentelemetry.io/otel/exporters/otlp/otlptrace/version.go @@ -5,5 +5,5 @@ package otlptrace // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace" // Version is the current release version of the OpenTelemetry OTLP trace exporter in use. func Version() string { - return "1.35.0" + return "1.36.0" } diff --git a/vendor/go.opentelemetry.io/proto/otlp/collector/metrics/v1/metrics_service_grpc.pb.go b/vendor/go.opentelemetry.io/proto/otlp/collector/metrics/v1/metrics_service_grpc.pb.go index 31d25fc1..fc668643 100644 --- a/vendor/go.opentelemetry.io/proto/otlp/collector/metrics/v1/metrics_service_grpc.pb.go +++ b/vendor/go.opentelemetry.io/proto/otlp/collector/metrics/v1/metrics_service_grpc.pb.go @@ -22,8 +22,6 @@ const _ = grpc.SupportPackageIsVersion7 // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type MetricsServiceClient interface { - // For performance reasons, it is recommended to keep this RPC - // alive for the entire life of the application. Export(ctx context.Context, in *ExportMetricsServiceRequest, opts ...grpc.CallOption) (*ExportMetricsServiceResponse, error) } @@ -48,8 +46,6 @@ func (c *metricsServiceClient) Export(ctx context.Context, in *ExportMetricsServ // All implementations must embed UnimplementedMetricsServiceServer // for forward compatibility type MetricsServiceServer interface { - // For performance reasons, it is recommended to keep this RPC - // alive for the entire life of the application. Export(context.Context, *ExportMetricsServiceRequest) (*ExportMetricsServiceResponse, error) mustEmbedUnimplementedMetricsServiceServer() } diff --git a/vendor/go.opentelemetry.io/proto/otlp/collector/trace/v1/trace_service_grpc.pb.go b/vendor/go.opentelemetry.io/proto/otlp/collector/trace/v1/trace_service_grpc.pb.go index dd1b73f1..892864ea 100644 --- a/vendor/go.opentelemetry.io/proto/otlp/collector/trace/v1/trace_service_grpc.pb.go +++ b/vendor/go.opentelemetry.io/proto/otlp/collector/trace/v1/trace_service_grpc.pb.go @@ -22,8 +22,6 @@ const _ = grpc.SupportPackageIsVersion7 // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type TraceServiceClient interface { - // For performance reasons, it is recommended to keep this RPC - // alive for the entire life of the application. Export(ctx context.Context, in *ExportTraceServiceRequest, opts ...grpc.CallOption) (*ExportTraceServiceResponse, error) } @@ -48,8 +46,6 @@ func (c *traceServiceClient) Export(ctx context.Context, in *ExportTraceServiceR // All implementations must embed UnimplementedTraceServiceServer // for forward compatibility type TraceServiceServer interface { - // For performance reasons, it is recommended to keep this RPC - // alive for the entire life of the application. Export(context.Context, *ExportTraceServiceRequest) (*ExportTraceServiceResponse, error) mustEmbedUnimplementedTraceServiceServer() } diff --git a/vendor/go.opentelemetry.io/proto/otlp/common/v1/common.pb.go b/vendor/go.opentelemetry.io/proto/otlp/common/v1/common.pb.go index 852209b0..a7c5d19b 100644 --- a/vendor/go.opentelemetry.io/proto/otlp/common/v1/common.pb.go +++ b/vendor/go.opentelemetry.io/proto/otlp/common/v1/common.pb.go @@ -430,6 +430,101 @@ func (x *InstrumentationScope) GetDroppedAttributesCount() uint32 { return 0 } +// A reference to an Entity. +// Entity represents an object of interest associated with produced telemetry: e.g spans, metrics, profiles, or logs. +// +// Status: [Development] +type EntityRef struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The Schema URL, if known. This is the identifier of the Schema that the entity data + // is recorded in. To learn more about Schema URL see + // https://opentelemetry.io/docs/specs/otel/schemas/#schema-url + // + // This schema_url applies to the data in this message and to the Resource attributes + // referenced by id_keys and description_keys. + // TODO: discuss if we are happy with this somewhat complicated definition of what + // the schema_url applies to. + // + // This field obsoletes the schema_url field in ResourceMetrics/ResourceSpans/ResourceLogs. + SchemaUrl string `protobuf:"bytes,1,opt,name=schema_url,json=schemaUrl,proto3" json:"schema_url,omitempty"` + // Defines the type of the entity. MUST not change during the lifetime of the entity. + // For example: "service" or "host". This field is required and MUST not be empty + // for valid entities. + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + // Attribute Keys that identify the entity. + // MUST not change during the lifetime of the entity. The Id must contain at least one attribute. + // These keys MUST exist in the containing {message}.attributes. + IdKeys []string `protobuf:"bytes,3,rep,name=id_keys,json=idKeys,proto3" json:"id_keys,omitempty"` + // Descriptive (non-identifying) attribute keys of the entity. + // MAY change over the lifetime of the entity. MAY be empty. + // These attribute keys are not part of entity's identity. + // These keys MUST exist in the containing {message}.attributes. + DescriptionKeys []string `protobuf:"bytes,4,rep,name=description_keys,json=descriptionKeys,proto3" json:"description_keys,omitempty"` +} + +func (x *EntityRef) Reset() { + *x = EntityRef{} + if protoimpl.UnsafeEnabled { + mi := &file_opentelemetry_proto_common_v1_common_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EntityRef) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EntityRef) ProtoMessage() {} + +func (x *EntityRef) ProtoReflect() protoreflect.Message { + mi := &file_opentelemetry_proto_common_v1_common_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EntityRef.ProtoReflect.Descriptor instead. +func (*EntityRef) Descriptor() ([]byte, []int) { + return file_opentelemetry_proto_common_v1_common_proto_rawDescGZIP(), []int{5} +} + +func (x *EntityRef) GetSchemaUrl() string { + if x != nil { + return x.SchemaUrl + } + return "" +} + +func (x *EntityRef) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *EntityRef) GetIdKeys() []string { + if x != nil { + return x.IdKeys + } + return nil +} + +func (x *EntityRef) GetDescriptionKeys() []string { + if x != nil { + return x.DescriptionKeys + } + return nil +} + var File_opentelemetry_proto_common_v1_common_proto protoreflect.FileDescriptor var file_opentelemetry_proto_common_v1_common_proto_rawDesc = []byte{ @@ -488,15 +583,23 @@ var file_opentelemetry_proto_common_v1_common_proto_rawDesc = []byte{ 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x16, 0x64, 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x7b, 0x0a, 0x20, 0x69, 0x6f, 0x2e, 0x6f, 0x70, 0x65, 0x6e, - 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x42, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x28, 0x67, 0x6f, 0x2e, 0x6f, 0x70, 0x65, - 0x6e, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x69, 0x6f, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2f, 0x6f, 0x74, 0x6c, 0x70, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, - 0x76, 0x31, 0xaa, 0x02, 0x1d, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, - 0x72, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x82, 0x01, 0x0a, 0x09, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x52, 0x65, 0x66, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x75, 0x72, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x55, + 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x64, 0x5f, 0x6b, 0x65, 0x79, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x69, 0x64, 0x4b, 0x65, 0x79, 0x73, 0x12, + 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, + 0x65, 0x79, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x73, 0x42, 0x7b, 0x0a, 0x20, 0x69, 0x6f, + 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x42, 0x0b, + 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x28, 0x67, + 0x6f, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, + 0x69, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6f, 0x74, 0x6c, 0x70, 0x2f, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x76, 0x31, 0xaa, 0x02, 0x1d, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x65, + 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -511,13 +614,14 @@ func file_opentelemetry_proto_common_v1_common_proto_rawDescGZIP() []byte { return file_opentelemetry_proto_common_v1_common_proto_rawDescData } -var file_opentelemetry_proto_common_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_opentelemetry_proto_common_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_opentelemetry_proto_common_v1_common_proto_goTypes = []interface{}{ (*AnyValue)(nil), // 0: opentelemetry.proto.common.v1.AnyValue (*ArrayValue)(nil), // 1: opentelemetry.proto.common.v1.ArrayValue (*KeyValueList)(nil), // 2: opentelemetry.proto.common.v1.KeyValueList (*KeyValue)(nil), // 3: opentelemetry.proto.common.v1.KeyValue (*InstrumentationScope)(nil), // 4: opentelemetry.proto.common.v1.InstrumentationScope + (*EntityRef)(nil), // 5: opentelemetry.proto.common.v1.EntityRef } var file_opentelemetry_proto_common_v1_common_proto_depIdxs = []int32{ 1, // 0: opentelemetry.proto.common.v1.AnyValue.array_value:type_name -> opentelemetry.proto.common.v1.ArrayValue @@ -599,6 +703,18 @@ func file_opentelemetry_proto_common_v1_common_proto_init() { return nil } } + file_opentelemetry_proto_common_v1_common_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EntityRef); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_opentelemetry_proto_common_v1_common_proto_msgTypes[0].OneofWrappers = []interface{}{ (*AnyValue_StringValue)(nil), @@ -615,7 +731,7 @@ func file_opentelemetry_proto_common_v1_common_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_opentelemetry_proto_common_v1_common_proto_rawDesc, NumEnums: 0, - NumMessages: 5, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, diff --git a/vendor/go.opentelemetry.io/proto/otlp/metrics/v1/metrics.pb.go b/vendor/go.opentelemetry.io/proto/otlp/metrics/v1/metrics.pb.go index 8799d6ba..ec187b13 100644 --- a/vendor/go.opentelemetry.io/proto/otlp/metrics/v1/metrics.pb.go +++ b/vendor/go.opentelemetry.io/proto/otlp/metrics/v1/metrics.pb.go @@ -526,7 +526,7 @@ type Metric struct { // description of the metric, which can be used in documentation. Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` // unit in which the metric value is reported. Follows the format - // described by http://unitsofmeasure.org/ucum.html. + // described by https://unitsofmeasure.org/ucum.html. Unit string `protobuf:"bytes,3,opt,name=unit,proto3" json:"unit,omitempty"` // Data determines the aggregation type (if any) of the metric, what is the // reported value type for the data points, as well as the relatationship to @@ -929,7 +929,7 @@ func (x *ExponentialHistogram) GetAggregationTemporality() AggregationTemporalit // Summary metric data are used to convey quantile summaries, // a Prometheus (see: https://prometheus.io/docs/concepts/metric_types/#summary) -// and OpenMetrics (see: https://github.com/OpenObservability/OpenMetrics/blob/4dbf6075567ab43296eed941037c12951faafb92/protos/prometheus.proto#L45) +// and OpenMetrics (see: https://github.com/prometheus/OpenMetrics/blob/4dbf6075567ab43296eed941037c12951faafb92/protos/prometheus.proto#L45) // data type. These data points cannot always be merged in a meaningful way. // While they can be useful in some applications, histogram data points are // recommended for new applications. @@ -1175,7 +1175,9 @@ type HistogramDataPoint struct { // The sum of the bucket_counts must equal the value in the count field. // // The number of elements in bucket_counts array must be by one greater than - // the number of elements in explicit_bounds array. + // the number of elements in explicit_bounds array. The exception to this rule + // is when the length of bucket_counts is 0, then the length of explicit_bounds + // must also be 0. BucketCounts []uint64 `protobuf:"fixed64,6,rep,packed,name=bucket_counts,json=bucketCounts,proto3" json:"bucket_counts,omitempty"` // explicit_bounds specifies buckets with explicitly defined bounds for values. // @@ -1190,6 +1192,9 @@ type HistogramDataPoint struct { // Histogram buckets are inclusive of their upper boundary, except the last // bucket where the boundary is at infinity. This format is intentionally // compatible with the OpenMetrics histogram definition. + // + // If bucket_counts length is 0 then explicit_bounds length must also be 0, + // otherwise the data point is invalid. ExplicitBounds []float64 `protobuf:"fixed64,7,rep,packed,name=explicit_bounds,json=explicitBounds,proto3" json:"explicit_bounds,omitempty"` // (Optional) List of exemplars collected from // measurements that were used to form the data point diff --git a/vendor/go.opentelemetry.io/proto/otlp/resource/v1/resource.pb.go b/vendor/go.opentelemetry.io/proto/otlp/resource/v1/resource.pb.go index b7545b03..eb7745d6 100644 --- a/vendor/go.opentelemetry.io/proto/otlp/resource/v1/resource.pb.go +++ b/vendor/go.opentelemetry.io/proto/otlp/resource/v1/resource.pb.go @@ -48,6 +48,12 @@ type Resource struct { // dropped_attributes_count is the number of dropped attributes. If the value is 0, then // no attributes were dropped. DroppedAttributesCount uint32 `protobuf:"varint,2,opt,name=dropped_attributes_count,json=droppedAttributesCount,proto3" json:"dropped_attributes_count,omitempty"` + // Set of entities that participate in this Resource. + // + // Note: keys in the references MUST exist in attributes of this message. + // + // Status: [Development] + EntityRefs []*v1.EntityRef `protobuf:"bytes,3,rep,name=entity_refs,json=entityRefs,proto3" json:"entity_refs,omitempty"` } func (x *Resource) Reset() { @@ -96,6 +102,13 @@ func (x *Resource) GetDroppedAttributesCount() uint32 { return 0 } +func (x *Resource) GetEntityRefs() []*v1.EntityRef { + if x != nil { + return x.EntityRefs + } + return nil +} + var File_opentelemetry_proto_resource_v1_resource_proto protoreflect.FileDescriptor var file_opentelemetry_proto_resource_v1_resource_proto_rawDesc = []byte{ @@ -106,7 +119,7 @@ var file_opentelemetry_proto_resource_v1_resource_proto_rawDesc = []byte{ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2a, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x76, 0x31, - 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x8d, 0x01, + 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd8, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x47, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x70, @@ -115,16 +128,21 @@ var file_opentelemetry_proto_resource_v1_resource_proto_rawDesc = []byte{ 0x74, 0x65, 0x73, 0x12, 0x38, 0x0a, 0x18, 0x64, 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x16, 0x64, 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x83, 0x01, - 0x0a, 0x22, 0x69, 0x6f, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, - 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x76, 0x31, 0x42, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2a, 0x67, 0x6f, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x65, - 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x69, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2f, 0x6f, 0x74, 0x6c, 0x70, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2f, 0x76, - 0x31, 0xaa, 0x02, 0x1f, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, - 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x49, 0x0a, + 0x0b, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x72, 0x65, 0x66, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, + 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x66, 0x52, 0x0a, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x66, 0x73, 0x42, 0x83, 0x01, 0x0a, 0x22, 0x69, 0x6f, 0x2e, + 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x42, + 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, + 0x5a, 0x2a, 0x67, 0x6f, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, + 0x72, 0x79, 0x2e, 0x69, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6f, 0x74, 0x6c, 0x70, + 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2f, 0x76, 0x31, 0xaa, 0x02, 0x1f, 0x4f, + 0x70, 0x65, 0x6e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x56, 0x31, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -141,16 +159,18 @@ func file_opentelemetry_proto_resource_v1_resource_proto_rawDescGZIP() []byte { var file_opentelemetry_proto_resource_v1_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_opentelemetry_proto_resource_v1_resource_proto_goTypes = []interface{}{ - (*Resource)(nil), // 0: opentelemetry.proto.resource.v1.Resource - (*v1.KeyValue)(nil), // 1: opentelemetry.proto.common.v1.KeyValue + (*Resource)(nil), // 0: opentelemetry.proto.resource.v1.Resource + (*v1.KeyValue)(nil), // 1: opentelemetry.proto.common.v1.KeyValue + (*v1.EntityRef)(nil), // 2: opentelemetry.proto.common.v1.EntityRef } var file_opentelemetry_proto_resource_v1_resource_proto_depIdxs = []int32{ 1, // 0: opentelemetry.proto.resource.v1.Resource.attributes:type_name -> opentelemetry.proto.common.v1.KeyValue - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 2, // 1: opentelemetry.proto.resource.v1.Resource.entity_refs:type_name -> opentelemetry.proto.common.v1.EntityRef + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_opentelemetry_proto_resource_v1_resource_proto_init() } diff --git a/vendor/google.golang.org/genproto/googleapis/api/httpbody/httpbody.pb.go b/vendor/google.golang.org/genproto/googleapis/api/httpbody/httpbody.pb.go index f388426b..d083dde3 100644 --- a/vendor/google.golang.org/genproto/googleapis/api/httpbody/httpbody.pb.go +++ b/vendor/google.golang.org/genproto/googleapis/api/httpbody/httpbody.pb.go @@ -1,4 +1,4 @@ -// Copyright 2024 Google LLC +// Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/vendor/modules.txt b/vendor/modules.txt index a7df29ea..aff21c7e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -12,6 +12,9 @@ github.com/Microsoft/go-winio/pkg/guid # github.com/StackExchange/wmi v1.2.1 ## explicit; go 1.13 github.com/StackExchange/wmi +# github.com/caarlos0/env/v11 v11.3.1 +## explicit; go 1.18 +github.com/caarlos0/env/v11 # github.com/cenkalti/backoff/v4 v4.3.0 ## explicit; go 1.18 github.com/cenkalti/backoff/v4 @@ -141,6 +144,28 @@ github.com/docker/docker-credential-helpers/credentials github.com/docker/go-connections/nat github.com/docker/go-connections/sockets github.com/docker/go-connections/tlsconfig +# github.com/docker/go-sdk/client v0.1.0-alpha006 +## explicit; go 1.23.6 +github.com/docker/go-sdk/client +# github.com/docker/go-sdk/config v0.1.0-alpha006 +## explicit; go 1.23.6 +github.com/docker/go-sdk/config +github.com/docker/go-sdk/config/auth +# github.com/docker/go-sdk/container v0.1.0-alpha006 +## explicit; go 1.23.6 +github.com/docker/go-sdk/container +github.com/docker/go-sdk/container/exec +github.com/docker/go-sdk/container/wait +# github.com/docker/go-sdk/context v0.1.0-alpha006 +## explicit; go 1.23.6 +github.com/docker/go-sdk/context +github.com/docker/go-sdk/context/internal +# github.com/docker/go-sdk/image v0.1.0-alpha006 +## explicit; go 1.23.6 +github.com/docker/go-sdk/image +# github.com/docker/go-sdk/network v0.1.0-alpha006 +## explicit; go 1.23.6 +github.com/docker/go-sdk/network # github.com/docker/go-units v0.5.0 ## explicit github.com/docker/go-units @@ -236,8 +261,8 @@ github.com/gpustack/gguf-parser-go/util/osx github.com/gpustack/gguf-parser-go/util/ptr github.com/gpustack/gguf-parser-go/util/slicex github.com/gpustack/gguf-parser-go/util/stringx -# github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 -## explicit; go 1.22 +# github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 +## explicit; go 1.23.0 github.com/grpc-ecosystem/grpc-gateway/v2/internal/httprule github.com/grpc-ecosystem/grpc-gateway/v2/runtime github.com/grpc-ecosystem/grpc-gateway/v2/utilities @@ -424,8 +449,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/envco go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/retry go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/transform -# go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 -## explicit; go 1.22.0 +# go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 +## explicit; go 1.23.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/tracetransform # go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 @@ -462,8 +487,8 @@ go.opentelemetry.io/otel/trace go.opentelemetry.io/otel/trace/embedded go.opentelemetry.io/otel/trace/internal/telemetry go.opentelemetry.io/otel/trace/noop -# go.opentelemetry.io/proto/otlp v1.5.0 -## explicit; go 1.22.0 +# go.opentelemetry.io/proto/otlp v1.7.0 +## explicit; go 1.23.0 go.opentelemetry.io/proto/otlp/collector/metrics/v1 go.opentelemetry.io/proto/otlp/collector/trace/v1 go.opentelemetry.io/proto/otlp/common/v1 @@ -515,8 +540,6 @@ golang.org/x/text/secure/bidirule golang.org/x/text/transform golang.org/x/text/unicode/bidi golang.org/x/text/unicode/norm -# golang.org/x/time v0.9.0 -## explicit; go 1.18 # golang.org/x/tools v0.34.0 ## explicit; go 1.23.0 golang.org/x/tools/cmd/stringer @@ -555,10 +578,10 @@ gonum.org/v1/gonum/lapack gonum.org/v1/gonum/lapack/gonum gonum.org/v1/gonum/lapack/lapack64 gonum.org/v1/gonum/mat -# google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 +# google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a ## explicit; go 1.23.0 google.golang.org/genproto/googleapis/api/httpbody -# google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e +# google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a ## explicit; go 1.23.0 google.golang.org/genproto/googleapis/rpc/errdetails google.golang.org/genproto/googleapis/rpc/status @@ -671,8 +694,6 @@ gopkg.in/tomb.v1 # gopkg.in/yaml.v3 v3.0.1 ## explicit gopkg.in/yaml.v3 -# gotest.tools/v3 v3.5.2 -## explicit; go 1.17 # howett.net/plist v1.0.1 ## explicit; go 1.12 howett.net/plist