diff --git a/deploy.rb b/deploy.rb index b4a147c773..38a97f9d2e 100755 --- a/deploy.rb +++ b/deploy.rb @@ -10,6 +10,8 @@ DEPLOY_NOW = !get_env("DEPLOY_NOW").nil? DEPLOY_CUSTOMIZE = !get_env("NO_DEPLOY_CUSTOMIZE") DEPLOY_ONLY = !get_env("DEPLOY_ONLY").nil? +CREATE_AND_PUSH_BRANCH = !get_env("DEPLOY_CREATE_AND_PUSH_BRANCH").nil? +FLYIO_BRANCH_NAME = "flyio-new-files" DEPLOY_APP_NAME = get_env("DEPLOY_APP_NAME") if !DEPLOY_CUSTOMIZE && !DEPLOY_APP_NAME @@ -29,6 +31,8 @@ GIT_REPO = get_env("GIT_REPO") +CAN_CREATE_AND_PUSH_BRANCH = CREATE_AND_PUSH_BRANCH && GIT_REPO + GIT_REPO_URL = if GIT_REPO repo_url = begin URI(GIT_REPO) @@ -247,6 +251,10 @@ steps.push({id: Step::DEPLOY, description: "Deploy application"}) if DEPLOY_NOW + if CAN_CREATE_AND_PUSH_BRANCH + steps.push({id: Step::CREATE_AND_PUSH_BRANCH, description: "Create Fly.io git branch with new files"}) + end + artifact Artifact::META, { steps: steps } # Join the parallel task thread @@ -383,4 +391,15 @@ end end +if CAN_CREATE_AND_PUSH_BRANCH + in_step Step::CREATE_AND_PUSH_BRANCH do + exec_capture("git checkout -b #{FLYIO_BRANCH_NAME}") + exec_capture("git config user.name \"Fly.io\"") + exec_capture("git config user.email \"noreply@fly.io\"") + exec_capture("git add .") + exec_capture("git commit -m \"New files from Fly.io Launch\"") + exec_capture("git push -f origin #{FLYIO_BRANCH_NAME}") + end +end + event :end, { ts: ts() } \ No newline at end of file diff --git a/deploy/common.rb b/deploy/common.rb index eea2436024..5ed9a03f41 100644 --- a/deploy/common.rb +++ b/deploy/common.rb @@ -20,6 +20,7 @@ module Step UPSTASH_REDIS = :upstash_redis TIGRIS_OBJECT_STORAGE = :tigris_object_storage SENTRY = :sentry + CREATE_AND_PUSH_BRANCH = :create_and_push_branch DEPLOY = :deploy def self.current diff --git a/go.mod b/go.mod index ea94f85f61..b2883bfb66 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/coder/websocket v1.8.12 github.com/containerd/continuity v0.4.3 github.com/depot/depot-go v0.3.0 - github.com/docker/docker v27.2.1+incompatible + github.com/docker/docker v27.3.1+incompatible github.com/docker/go-connections v0.5.0 github.com/docker/go-units v0.5.0 github.com/dustin/go-humanize v1.0.1 @@ -94,7 +94,7 @@ require ( golang.org/x/text v0.18.0 golang.org/x/time v0.6.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 - google.golang.org/grpc v1.66.2 + google.golang.org/grpc v1.67.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -154,7 +154,6 @@ require ( github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect github.com/containerd/ttrpc v1.2.3 // indirect github.com/containerd/typeurl/v2 v2.1.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dimchansky/utfbom v1.1.1 // indirect @@ -228,7 +227,6 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 // indirect github.com/rivo/uniseg v0.4.3 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect @@ -237,7 +235,6 @@ require ( github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect - github.com/sosodev/duration v1.3.1 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect @@ -245,7 +242,6 @@ require ( github.com/tonistiigi/fsutil v0.0.0-20240424095704-91a3fc46842c // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531 // indirect - github.com/urfave/cli/v2 v2.27.4 // indirect github.com/vbatts/tar-split v0.11.5 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect @@ -253,7 +249,6 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 // indirect go.opentelemetry.io/otel/metric v1.30.0 // indirect diff --git a/go.sum b/go.sum index 64d9e09617..b7ceee16d0 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,6 @@ connectrpc.com/connect v1.16.1 h1:rOdrK/RTI/7TVnn3JsVxt3n028MlTRwmK5Q4heSpjis= connectrpc.com/connect v1.16.1/go.mod h1:XpZAduBQUySsb4/KO5JffORVkDI4B6/EYPi7N8xpNZw= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/99designs/gqlgen v0.17.53 h1:FJOJaF96d7Y5EBpoaLG96fz1NR6B8bFdCZI1yZwYArM= -github.com/99designs/gqlgen v0.17.53/go.mod h1:77/+pVe6zlTsz++oUg2m8VLgzdUPHxjoAG3BxI5y8Rc= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA= @@ -206,7 +204,6 @@ github.com/containerd/ttrpc v1.2.3 h1:4jlhbXIGvijRtNC8F/5CpuJZ7yKOBFGFOOXg1bkISz github.com/containerd/ttrpc v1.2.3/go.mod h1:ieWsXucbb8Mj9PH0rXCw1i8IunRbbAiDkpXkbfflWBM= github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -232,8 +229,8 @@ github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwen github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI= -github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8= github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= @@ -567,8 +564,6 @@ github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= @@ -599,8 +594,6 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:s github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= -github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= -github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY= @@ -655,9 +648,6 @@ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk= github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531 h1:Y/M5lygoNPKwVNLMPXgVfsRT40CSFKXCxuU8LoHySjs= github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= -github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= -github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= -github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8= @@ -675,8 +665,6 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -865,8 +853,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= -google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= diff --git a/internal/build/imgsrc/docker.go b/internal/build/imgsrc/docker.go index 63178a0cd8..e9e4f620c8 100644 --- a/internal/build/imgsrc/docker.go +++ b/internal/build/imgsrc/docker.go @@ -636,17 +636,20 @@ func clearDeploymentTags(ctx context.Context, docker *dockerclient.Client, tag s } func registryAuth(token string) registry.AuthConfig { + targetRegistry := viper.GetString(flyctl.ConfigRegistryHost) return registry.AuthConfig{ Username: "x", Password: token, - ServerAddress: "registry.fly.io", + ServerAddress: targetRegistry, } } func authConfigs(token string) map[string]registry.AuthConfig { + targetRegistry := viper.GetString(flyctl.ConfigRegistryHost) + authConfigs := map[string]registry.AuthConfig{} - authConfigs["registry.fly.io"] = registryAuth(token) + authConfigs[targetRegistry] = registryAuth(token) dockerhubUsername := os.Getenv("DOCKER_HUB_USERNAME") dockerhubPassword := os.Getenv("DOCKER_HUB_PASSWORD") diff --git a/internal/build/imgsrc/resolver.go b/internal/build/imgsrc/resolver.go index 222764d49a..7fe5b66093 100644 --- a/internal/build/imgsrc/resolver.go +++ b/internal/build/imgsrc/resolver.go @@ -245,7 +245,7 @@ func (r *Resolver) BuildImage(ctx context.Context, streams *iostreams.IOStreams, if r.dockerFactory.mode.UseNixpacks() { strategies = append(strategies, &nixpacksBuilder{}) - } else if r.dockerFactory.mode.UseDepot() && len(opts.Buildpacks) == 0 { + } else if r.dockerFactory.mode.UseDepot() && len(opts.Buildpacks) == 0 && opts.Builder == "" && opts.BuiltIn == "" { strategies = append(strategies, &DepotBuilder{Scope: builderScope}) } else { strategies = []imageBuilder{ diff --git a/internal/command/machine/list.go b/internal/command/machine/list.go index 50ca8beae9..30eac61a77 100644 --- a/internal/command/machine/list.go +++ b/internal/command/machine/list.go @@ -65,7 +65,7 @@ func runMachineList(ctx context.Context) (err error) { machines, err := flapsClient.List(ctx, "") if err != nil { - return fmt.Errorf("machines could not be retrieved") + return err } if cfg.JSONOutput { diff --git a/internal/command/volumes/fork.go b/internal/command/volumes/fork.go index 237bbb8abf..9f6812a82e 100644 --- a/internal/command/volumes/fork.go +++ b/internal/command/volumes/fork.go @@ -105,11 +105,6 @@ func runFork(ctx context.Context) error { machinesOnly = fly.Pointer(flag.GetBool(ctx, "machines-only")) } - var requireUniqueZone *bool - if flag.IsSpecified(ctx, "require-unique-zone") { - requireUniqueZone = fly.Pointer(flag.GetBool(ctx, "require-unique-zone")) - } - region := flag.GetString(ctx, "region") var attachedMachineImage string @@ -131,7 +126,7 @@ func runFork(ctx context.Context) error { input := fly.CreateVolumeRequest{ Name: name, MachinesOnly: machinesOnly, - RequireUniqueZone: requireUniqueZone, + RequireUniqueZone: fly.Pointer(flag.GetBool(ctx, "require-unique-zone")), SourceVolumeID: &vol.ID, ComputeRequirements: computeRequirements, ComputeImage: attachedMachineImage, diff --git a/internal/oci/image.go b/internal/oci/image.go new file mode 100644 index 0000000000..285a951e62 --- /dev/null +++ b/internal/oci/image.go @@ -0,0 +1,298 @@ +package oci + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "regexp" + "strings" +) + +type DHTokenResponse struct { + Token string `json:"token"` +} + +type ErrorDetail struct { + Type string `json:"Type"` + Class string `json:"Class"` + Name string `json:"Name"` + Action string `json:"Action"` +} + +type ErrorItem struct { + Code string `json:"code"` + Message string `json:"message"` + Detail []ErrorDetail `json:"detail"` +} + +type Platform struct { + Architecture string `json:"architecture"` + Os string `json:"os"` +} + +type Manifest struct { + Digest string `json:"digest"` + MediaType string `json:"mediaType"` + Size int `json:"size"` + Platform Platform `json:"platform"` +} + +type Manifests struct { + Errors *[]ErrorItem `json:"errors"` + MediaType string `json:"mediaType"` + Config Manifest `json:"config"` + Manifests []Manifest `json:"manifests"` +} + +func (m *Manifests) Error() error { + raw, err := json.Marshal(m.Errors) + if err != nil { + return nil + } + return errors.New(string(raw)) +} + +type ImageConfig struct { + Config Config `json:"config"` +} + +type Auth struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type Config struct { + Hostname string `json:"Hostname"` + Domainname string `json:"Domainname"` + User string `json:"User"` + AttachStdin bool `json:"AttachStdin"` + AttachStdout bool `json:"AttachStdout"` + AttachStderr bool `json:"AttachStderr"` + Tty bool `json:"Tty"` + OpenStdin bool `json:"OpenStdin"` + StdinOnce bool `json:"StdinOnce"` + Env []string `json:"Env"` + Cmd []string `json:"Cmd"` + Image string `json:"Image"` + Volumes map[string]struct{} `json:"Volumes"` + ExposedPorts map[string]interface{} `json:"ExposedPorts"` + WorkingDir string `json:"WorkingDir"` + Entrypoint []string `json:"Entrypoint"` + OnBuild []string `json:"OnBuild"` + Labels map[string]string `json:"Labels"` +} + +func GetImageConfig(image string, auth *Auth) (*Config, error) { + + headers := map[string]string{ + "Accept": strings.Join([]string{ + "application/vnd.docker.distribution.manifest.v1+json", + "application/vnd.oci.image.manifest.v1+json", + "application/vnd.docker.distribution.manifest.v2+json", + }, ","), + } + + registry, image := normalizeRegistryAndImage(image) + tag, image := getTagAndImage(image) + + header, statusCode, err := getRegistryHeaders(registry) + if err != nil { + return nil, fmt.Errorf("failed to get registry headers: %w", err) + } + + if statusCode == http.StatusUnauthorized && registry == "https://registry-1.docker.io" { + token, err := getDockerHubToken(header, image, auth) + if err != nil { + return nil, fmt.Errorf("failed to get dockerhub token: %w", err) + } + headers["Authorization"] = "Bearer " + token + } + + manifestList, err := fetchManifestList(registry, image, tag, headers) + if err != nil { + return nil, fmt.Errorf("failed to fetch manifest list: %w", err) + } + + if manifestList.Errors != nil { + return nil, fmt.Errorf("failed to fetch manifest list (api_error): %w", manifestList.Error()) + } + + var configDigest, sha256 string + var manifest *Manifests + + if strings.Contains(manifestList.MediaType, "manifest.v2") { + configDigest = manifestList.Config.Digest + } else { + pickedManifest := getPrioritizedManifest(manifestList.Manifests) + if pickedManifest != nil { + sha256 = pickedManifest.Digest + } else { + panic("no manifests exists") + } + + manifest, err = fetchManifestList(registry, image, sha256, headers) + if err != nil { + return nil, fmt.Errorf("failed to fetch manifest: %w", err) + } + + configDigest = manifest.Config.Digest + } + + config, err := fetchConfigBlob(registry, image, configDigest, headers) + if err != nil { + return nil, fmt.Errorf("failed to fetch config blob: %w", err) + } + + return &config.Config, nil + +} + +func fetchConfigBlob(registry, image, digest string, headers map[string]string) (*ImageConfig, error) { + var config ImageConfig + err := makeHttpRequest[ImageConfig](http.MethodGet, fmt.Sprintf("%s/v2/%s/blobs/%s", registry, image, digest), headers, &config) + if err != nil { + return nil, err + } + return &config, nil +} + +func getPrioritizedManifest(manifests []Manifest) *Manifest { + var pickedManifest *Manifest + + for _, manifest := range manifests { + if manifest.Platform.Os == "linux" { + pickedManifest = &manifest + } + } + + for _, manifest := range manifests { + if manifest.Platform.Os == "linux" && manifest.Platform.Architecture == "amd64" { + pickedManifest = &manifest + } + } + + if pickedManifest != nil { + return pickedManifest + } + + if len(manifests) > 0 { + return &manifests[0] + } + + return nil +} + +func makeHttpRequest[T any](method, address string, headers map[string]string, response *T) error { + client := &http.Client{} + req, err := http.NewRequest(method, address, nil) + if err != nil { + return err + } + for key, value := range headers { + req.Header.Add(key, value) + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer func() { + _ = resp.Body.Close() + }() + + bd, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + err = json.Unmarshal(bd, response) + if err != nil { + return err + } + + return nil +} + +func fetchManifestList(registry, image, tag string, headers map[string]string) (*Manifests, error) { + var manifests Manifests + err := makeHttpRequest[Manifests](http.MethodGet, fmt.Sprintf("%s/v2/%s/manifests/%s", registry, image, tag), headers, &manifests) + if err != nil { + return nil, err + } + + return &manifests, nil +} + +func getDockerHubToken(wwwAuthenticateHeader, image string, auth *Auth) (string, error) { + wwwAuthenticateHeader = strings.ReplaceAll(wwwAuthenticateHeader, "Bearer ", "") + parts := strings.Split(wwwAuthenticateHeader, ",") + authParams := make(map[string]string) + for _, part := range parts { + kv := strings.SplitN(strings.TrimSpace(part), "=", 2) + if len(kv) == 2 { + authParams[kv[0]] = strings.Trim(kv[1], "\"") + } + } + + realm := authParams["realm"] + if auth != nil { + realm = fmt.Sprintf("https://%s:%s@auth.docker.io/token", auth.Username, auth.Password) + } + + var td DHTokenResponse + err := makeHttpRequest[DHTokenResponse](http.MethodGet, fmt.Sprintf("%s?service=%s&scope=%s", realm, authParams["service"], fmt.Sprintf("repository:%s:pull", image)), map[string]string{}, &td) + if err != nil { + return "", err + } + + return td.Token, nil +} + +func getRegistryHeaders(registry string) (string, int, error) { + resp, err := http.Get(registry + "/v2/") + if err != nil { + return "", 0, err + } + defer func() { + _ = resp.Body.Close() + }() + + headers := resp.Header.Get("www-authenticate") + return headers, resp.StatusCode, nil +} + +func getTagAndImage(image string) (string, string) { + tag := "latest" + if strings.Contains(image, ":") { + parts := strings.Split(image, ":") + image = parts[0] + if parts[1] != "latest" { + tag = "sha256:" + parts[1] + } + } + + return tag, image +} + +func normalizeRegistryAndImage(image string) (string, string) { + registry := strings.Split(image, "/")[0] + if registry == image || (!strings.Contains(registry, ".") && registry != "localhost") { + registry = "docker.io" + } else { + image = strings.Join(strings.Split(image, "/")[1:], "/") + } + + if registry == "docker.io" && !strings.Contains(image, "/") { + image = "library/" + image + } + + if registry == "docker.io" { + registry = "https://registry-1.docker.io" + } else if matched, _ := regexp.MatchString(`^localhost(:[0-9]+)?$`, registry); !matched { + registry = "https://" + registry + } + + return registry, image +} diff --git a/scanner/scanner.go b/scanner/scanner.go index 23a686c786..d2ef81ed38 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -3,6 +3,7 @@ package scanner import ( "embed" "io/fs" + "os" "path/filepath" "strings" "text/template" @@ -141,7 +142,8 @@ func Scan(sourceDir string, config *ScannerConfig) (*SourceInfo, error) { if err != nil { return nil, err } - if si != nil { + optOutGithubActions := os.Getenv("OPT_OUT_GITHUB_ACTIONS") + if si != nil && optOutGithubActions == "" { github_actions(sourceDir, &si.GitHubActions) return si, nil } diff --git a/test/preflight/fly_deploy_test.go b/test/preflight/fly_deploy_test.go index a83a9b59f2..bc1ca28204 100644 --- a/test/preflight/fly_deploy_test.go +++ b/test/preflight/fly_deploy_test.go @@ -170,7 +170,8 @@ func TestDeployNodeApp(t *testing.T) { t.Run("With Depot", WithParallel(testDeployNodeAppWithDepotRemoteBuilder)) } -func testDeployNodeAppWithRemoteBuilder(t *testing.T) { +func testDeployNodeAppWithRemoteBuilder(tt *testing.T) { + t := testLogger{tt} f := testlib.NewTestEnvFromEnv(t) err := testlib.CopyFixtureIntoWorkDir(f.WorkDir(), "deploy-node") require.NoError(t, err) @@ -189,8 +190,10 @@ func testDeployNodeAppWithRemoteBuilder(t *testing.T) { }) require.NoError(t, err) + t.Logf("deploy %s", appName) f.Fly("deploy --remote-only --ha=false") + t.Logf("deploy %s again", appName) f.Fly("deploy --remote-only --strategy immediate --ha=false") body, err := testlib.RunHealthCheck(fmt.Sprintf("https://%s.fly.dev", appName)) @@ -199,7 +202,8 @@ func testDeployNodeAppWithRemoteBuilder(t *testing.T) { require.Contains(t, string(body), fmt.Sprintf("Hello, World! %s", f.ID())) } -func testDeployNodeAppWithRemoteBuilderWithoutWireguard(t *testing.T) { +func testDeployNodeAppWithRemoteBuilderWithoutWireguard(tt *testing.T) { + t := testLogger{tt} f := testlib.NewTestEnvFromEnv(t) // Since this uses a fixture with a size, no need to run it on alternate @@ -225,6 +229,7 @@ func testDeployNodeAppWithRemoteBuilderWithoutWireguard(t *testing.T) { }) require.NoError(t, err) + t.Logf("deploy %s without WireGuard", appName) f.Fly("deploy --remote-only --ha=false --wg=false") body, err := testlib.RunHealthCheck(fmt.Sprintf("https://%s.fly.dev", appName)) @@ -233,7 +238,8 @@ func testDeployNodeAppWithRemoteBuilderWithoutWireguard(t *testing.T) { require.Contains(t, string(body), fmt.Sprintf("Hello, World! %s", f.ID())) } -func testDeployNodeAppWithDepotRemoteBuilder(t *testing.T) { +func testDeployNodeAppWithDepotRemoteBuilder(tt *testing.T) { + t := testLogger{tt} f := testlib.NewTestEnvFromEnv(t) err := testlib.CopyFixtureIntoWorkDir(f.WorkDir(), "deploy-node") require.NoError(t, err) @@ -252,8 +258,10 @@ func testDeployNodeAppWithDepotRemoteBuilder(t *testing.T) { }) require.NoError(t, err) + t.Logf("deploy %s with Depot", appName) f.Fly("deploy --depot --ha=false") + t.Logf("deploy %s again with Depot", appName) f.Fly("deploy --depot --strategy immediate --ha=false") body, err := testlib.RunHealthCheck(fmt.Sprintf("https://%s.fly.dev", appName)) diff --git a/test/preflight/fly_volume_test.go b/test/preflight/fly_volume_test.go index 65e8082c5d..4e3ef8b177 100644 --- a/test/preflight/fly_volume_test.go +++ b/test/preflight/fly_volume_test.go @@ -35,6 +35,7 @@ func TestVolume(t *testing.T) { t.Run("Extend", WithParallel(testVolumeExtend)) t.Run("List", WithParallel(testVolumeLs)) t.Run("CreateFromDestroyedVolSnapshot", WithParallel(testVolumeCreateFromDestroyedVolSnapshot)) + t.Run("Fork", WithParallel(testVolumeFork)) } func testVolumeExtend(t *testing.T) { @@ -122,6 +123,21 @@ func testVolumeLs(t *testing.T) { }, 5*time.Minute, 10*time.Second) } +func testVolumeFork(t *testing.T) { + f := testlib.NewTestEnvFromEnv(t) + appName := f.CreateRandomAppMachines() + + var original *fly.Volume + j := f.Fly("vol create --json --app %s --region %s --yes foobar", appName, f.PrimaryRegion()) + j.StdOutJSON(&original) + + var fork *fly.Volume + j = f.Fly("vol fork --json --app %s --region %s %s", appName, f.PrimaryRegion(), original.ID) + j.StdOutJSON(&fork) + + assert.NotEqual(t, original.Zone, fork.Zone, "forked volume should be in a different zone") +} + func testVolumeCreateFromDestroyedVolSnapshot(tt *testing.T) { t := testLogger{tt}