Skip to content

Commit

Permalink
feat: add saucectl docker push cmd
Browse files Browse the repository at this point in the history
  • Loading branch information
tianfeng92 committed Nov 29, 2023
1 parent 09a2a7f commit b4f74a2
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 1 deletion.
2 changes: 2 additions & 0 deletions cmd/saucectl/saucectl.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/saucelabs/saucectl/internal/cmd/artifacts"
"github.com/saucelabs/saucectl/internal/cmd/completion"
"github.com/saucelabs/saucectl/internal/cmd/configure"
"github.com/saucelabs/saucectl/internal/cmd/docker"
"github.com/saucelabs/saucectl/internal/cmd/imagerunner"
"github.com/saucelabs/saucectl/internal/cmd/ini"
"github.com/saucelabs/saucectl/internal/cmd/jobs"
Expand Down Expand Up @@ -70,6 +71,7 @@ func main() {
jobs.Command(cmd.PersistentPreRun),
imagerunner.Command(cmd.PersistentPreRun),
apit.Command(cmd.PersistentPreRun),
docker.Command(cmd.PersistentPreRun),
)

if err := cmd.Execute(); err != nil {
Expand Down
12 changes: 11 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,27 @@ require (
github.com/spf13/viper v1.14.0
github.com/stretchr/testify v1.8.1
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/mod v0.6.0
golang.org/x/mod v0.8.0
gopkg.in/segmentio/analytics-go.v3 v3.1.0
gopkg.in/yaml.v2 v2.4.0
gotest.tools/v3 v3.0.3
)

require (
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
golang.org/x/tools v0.6.0 // indirect
)

require (
Expand All @@ -48,6 +57,7 @@ require (
require (
github.com/Masterminds/semver/v3 v3.1.1
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/docker v24.0.7+incompatible
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.0.0 // indirect
Expand Down
21 changes: 21 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
Expand Down Expand Up @@ -79,6 +81,16 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
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 v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
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/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
Expand Down Expand Up @@ -113,6 +125,7 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
Expand Down Expand Up @@ -285,6 +298,10 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
Expand Down Expand Up @@ -441,6 +458,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down Expand Up @@ -639,6 +658,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
52 changes: 52 additions & 0 deletions internal/cmd/docker/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package docker

import (
"errors"
"time"

"github.com/saucelabs/saucectl/internal/credentials"
"github.com/saucelabs/saucectl/internal/http"
"github.com/saucelabs/saucectl/internal/region"
"github.com/spf13/cobra"
)

var (
registryClient http.DockerRegistry
dockerPushTimeout = 1 * time.Minute
)

func Command(preRun func(cmd *cobra.Command, args []string)) *cobra.Command {
var regio string

cmd := &cobra.Command{
Use: "docker",
Short: "Interact with docker registry",
SilenceUsage: true,
TraverseChildren: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if preRun != nil {
preRun(cmd, args)
}

reg := region.FromString(regio)
if reg == region.None {
return errors.New("empty region. Options: us-west-1, eu-central-1")
}

creds := credentials.Get()
url := reg.APIBaseURL()
registryClient = http.NewDockerRegistry(url, creds.Username, creds.AccessKey, dockerPushTimeout)

return nil
},
}

flags := cmd.PersistentFlags()
flags.StringVarP(&regio, "region", "r", "us-west-1", "The Sauce Labs region. Options: us-west-1, eu-central-1.")

cmd.AddCommand(
PushCommand(),
)

return cmd
}
94 changes: 94 additions & 0 deletions internal/cmd/docker/push.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package docker

import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"os"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client"
cmds "github.com/saucelabs/saucectl/internal/cmd"
"github.com/saucelabs/saucectl/internal/segment"
"github.com/saucelabs/saucectl/internal/usage"
"github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)

func PushCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "push",
Short: "push docker image to Sauce Labs container registry",
SilenceUsage: true,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 || args[0] == "" {
return errors.New("no docker image specified")
}

return nil
},
PreRun: func(cmd *cobra.Command, args []string) {
tracker := segment.DefaultTracker

go func() {
tracker.Collect(
cases.Title(language.English).String(cmds.FullName(cmd)),
usage.Properties{}.SetFlags(cmd.Flags()),
)
_ = tracker.Close()
}()
},
RunE: func(cmd *cobra.Command, args []string) error {
auth, err := registryClient.Login(context.Background())
fmt.Println("auth: ", auth)
if err != nil {
return fmt.Errorf("failed to fetch auth token: %v", err)
}
return pushDockerImage(args[0], auth.Username, auth.Password)
},
}

return cmd
}

func pushDockerImage(imageName, username, password string) error {
ctx, cancel := context.WithTimeout(context.Background(), dockerPushTimeout)
defer cancel()

cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return fmt.Errorf("failed to create Docker client: %v", err)
}

authConfig := registry.AuthConfig{
Username: username,
Password: password,
}

authBytes, err := json.Marshal(authConfig)
if err != nil {
return fmt.Errorf("failed to marshal docker auth: %v", err)
}
authBase64 := base64.URLEncoding.EncodeToString(authBytes)

// Push the image to the registry
pushOptions := types.ImagePushOptions{RegistryAuth: authBase64}
out, err := cli.ImagePush(ctx, imageName, pushOptions)
if err != nil {
return fmt.Errorf("failed to push image: %v", err)
}
defer out.Close()

// Print the push output
_, err = io.Copy(os.Stdout, out)
if err != nil {
return fmt.Errorf("failed to copy push output: %v", err)
}

return nil
}
63 changes: 63 additions & 0 deletions internal/http/docker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package http

import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/hashicorp/go-retryablehttp"
)

type DockerRegistry struct {
HTTPClient *retryablehttp.Client
URL string
Username string
AccessKey string
}

type AuthToken struct {
Username string `json:"username"`
Password string `json:"password"`
}

func NewDockerRegistry(url, username, accessKey string, timeout time.Duration) DockerRegistry {
return DockerRegistry{
HTTPClient: NewRetryableClient(timeout),
URL: url,
Username: username,
AccessKey: accessKey,
}
}

func (c *DockerRegistry) Login(ctx context.Context) (AuthToken, error) {
url := fmt.Sprintf("%s/v1alpha1/hosted/container-registry/authorization-token", c.URL)

var authToken AuthToken
req, err := NewRequestWithContext(ctx, http.MethodPost, url, nil)
if err != nil {
return authToken, err
}
req.SetBasicAuth(c.Username, c.AccessKey)

r, err := retryablehttp.FromRequest(req)
if err != nil {
return authToken, err
}

resp, err := c.HTTPClient.Do(r)
if err != nil {
return authToken, err
}
defer resp.Body.Close()

if resp.StatusCode != 200 {
return authToken, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}

if err := json.NewDecoder(resp.Body).Decode(&authToken); err != nil {
return authToken, err
}
return authToken, nil
}

0 comments on commit b4f74a2

Please sign in to comment.