Просмотр исходного кода

docker credentials helper cli command

Alexander Belanger 5 лет назад
Родитель
Сommit
0d2a60a0bc

+ 3 - 1
cli/cmd/api/registry.go

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"net/http"
 	"strings"
+	"time"
 
 	"github.com/porter-dev/porter/internal/registry"
 
@@ -167,7 +168,8 @@ func (c *Client) DeleteProjectRegistry(
 
 // GetECRTokenResponse blah
 type GetECRTokenResponse struct {
-	Token string `json:"token"`
+	Token     string     `json:"token"`
+	ExpiresAt *time.Time `json:"expires_at"`
 }
 
 // GetECRAuthorizationToken gets an ECR authorization token

+ 90 - 0
cli/cmd/docker.go

@@ -0,0 +1,90 @@
+package cmd
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"github.com/fatih/color"
+	"github.com/spf13/cobra"
+
+	"github.com/docker/cli/cli/config/configfile"
+)
+
+var dockerCmd = &cobra.Command{
+	Use:   "docker",
+	Short: "Commands to configure Docker for a project",
+}
+
+var configureCmd = &cobra.Command{
+	Use:   "configure",
+	Short: "Configures the host's Docker instance",
+	Run: func(cmd *cobra.Command, args []string) {
+		if err := dockerConfig(); err != nil {
+			color.New(color.FgRed).Println("Configuring Docker unsuccessful:", err.Error())
+			os.Exit(1)
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(dockerCmd)
+
+	dockerCmd.AddCommand(configureCmd)
+}
+
+func dockerConfig() error {
+	dockerConfigFile := filepath.Join(home, ".docker", "config.json")
+
+	// check that a compatible version of docker is installed
+
+	// determine if configfile exists
+
+	// if it does not exist, create it
+
+	// if it does exist, read it
+	configBytes, err := ioutil.ReadFile(dockerConfigFile)
+
+	if err != nil {
+		return err
+	}
+
+	config := &configfile.ConfigFile{
+		Filename: dockerConfigFile,
+	}
+
+	err = json.Unmarshal(configBytes, config)
+
+	if err != nil {
+		return err
+	}
+
+	config.CredentialHelpers["393629051022.dkr.ecr.us-east-2.amazonaws.com"] = "porter"
+
+	err = config.Save()
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+
+	// z := &github.ZIPReleaseGetter{
+	// 	AssetName:           "docker-credential-porter",
+	// 	AssetFolderDest:     "/usr/local/bin",
+	// 	ZipFolderDest:       filepath.Join(home, ".porter"),
+	// 	ZipName:             "docker-credential-porter_latest.zip",
+	// 	EntityID:            "porter-dev",
+	// 	RepoName:            "porter",
+	// 	IsPlatformDependent: true,
+	// }
+
+	// err = z.GetLatestRelease()
+
+	// if err != nil {
+	// 	return err
+	// }
+
+	// return nil
+}

+ 144 - 46
cli/cmd/github/release.go

@@ -15,85 +15,183 @@ import (
 	"github.com/google/go-github/github"
 )
 
-func getLatestReleaseDownloadURL() (string, string, error) {
-	client := github.NewClient(nil)
+// ZIPReleaseGetter retrieves a release from Github in ZIP format and downloads it
+// to a directory on host
+type ZIPReleaseGetter struct {
+	// The name of the asset, i.e. "porter", "portersvr", "static"
+	AssetName string
+
+	// The host folder destination of the asset
+	AssetFolderDest string
+
+	// The host folder destination for the .zip file
+	ZipFolderDest string
+
+	// The name of the .zip file to download to
+	ZipName string
+
+	// The name of the Github entity whose repo is queried: i.e. "porter-dev"
+	EntityID string
+
+	// The name of the Github repo to get releases from
+	RepoName string
+
+	// If the asset is platform dependent
+	IsPlatformDependent bool
+}
+
+// GetLatestRelease downloads the latest .zip release from a given Github repository
+func (z *ZIPReleaseGetter) GetLatestRelease() error {
+	releaseURL, err := z.getLatestReleaseDownloadURL()
+
+	if err != nil {
+		return err
+	}
+
+	return z.getReleaseFromURL(releaseURL)
+}
+
+// GetRelease downloads a specific .zip release from a given Github repository
+func (z *ZIPReleaseGetter) GetRelease(releaseTag string) error {
+	releaseURL, err := z.getReleaseDownloadURL(releaseTag)
+
+	fmt.Printf("getting release %s\n", releaseURL)
 
-	rel, _, err := client.Repositories.GetLatestRelease(context.Background(), "porter-dev", "porter")
+	if err != nil {
+		return err
+	}
+
+	return z.getReleaseFromURL(releaseURL)
+}
+
+func (z *ZIPReleaseGetter) getReleaseFromURL(releaseURL string) error {
+	fmt.Printf("getting release %s\n", releaseURL)
+
+	err := z.downloadToFile(releaseURL)
+
+	fmt.Printf("downloaded release %s to file %s\n", z.AssetName, filepath.Join(z.ZipFolderDest, z.ZipName))
 
 	if err != nil {
-		return "", "", err
+		return err
 	}
 
-	var re *regexp.Regexp
+	fmt.Printf("unzipping %s to %s\n", z.AssetName, z.AssetFolderDest)
 
-	switch os := runtime.GOOS; os {
-	case "darwin":
-		re = regexp.MustCompile(`portersvr_.*_Darwin_x86_64\.zip`)
-	case "linux":
-		re = regexp.MustCompile(`portersvr_.*_Linux_x86_64\.zip`)
-	default:
-		fmt.Printf("%s.\n", os)
+	err = z.unzipToDir()
+
+	return err
+}
+
+// retrieves the download url for the latest release of an asset
+func (z *ZIPReleaseGetter) getLatestReleaseDownloadURL() (string, error) {
+	client := github.NewClient(nil)
+
+	rel, _, err := client.Repositories.GetLatestRelease(context.Background(), z.EntityID, z.RepoName)
+
+	if err != nil {
+		return "", err
 	}
 
-	staticRE := regexp.MustCompile(`static_.*\.zip`)
+	re, err := z.getDownloadRegexp()
+
+	if err != nil {
+		return "", err
+	}
 
 	releaseURL := ""
-	staticReleaseURL := ""
 
 	// iterate through the assets
 	for _, asset := range rel.Assets {
 		if downloadURL := asset.GetBrowserDownloadURL(); re.MatchString(downloadURL) {
 			releaseURL = downloadURL
-		} else if staticRE.MatchString(downloadURL) {
-			staticReleaseURL = downloadURL
 		}
 	}
 
-	return releaseURL, staticReleaseURL, nil
+	return releaseURL, nil
 }
 
-// DownloadLatestServerRelease retrieves the latest Porter server release from Github, unzips
-// it, and adds the binary to the porter directory
-func DownloadLatestServerRelease(porterDir string) error {
-	releaseURL, staticReleaseURL, err := getLatestReleaseDownloadURL()
-	fmt.Println(releaseURL)
+func (z *ZIPReleaseGetter) getReleaseDownloadURL(releaseTag string) (string, error) {
+	client := github.NewClient(nil)
+
+	rel, _, err := client.Repositories.GetReleaseByTag(context.Background(), z.EntityID, z.RepoName, releaseTag)
 
 	if err != nil {
-		return err
+		return "", fmt.Errorf("release %s does not exist", releaseTag)
 	}
 
-	zipFile := filepath.Join(porterDir, "portersrv_latest.zip")
-
-	err = downloadToFile(releaseURL, zipFile)
+	re, err := z.getDownloadRegexp()
 
 	if err != nil {
-		return err
+		return "", err
 	}
 
-	err = unzipToDir(zipFile, porterDir)
+	releaseURL := ""
 
-	if err != nil {
-		return err
+	// iterate through the assets
+	for _, asset := range rel.Assets {
+		if downloadURL := asset.GetBrowserDownloadURL(); re.MatchString(downloadURL) {
+			releaseURL = downloadURL
+		}
 	}
 
-	staticZipFile := filepath.Join(porterDir, "static_latest.zip")
-
-	err = downloadToFile(staticReleaseURL, staticZipFile)
+	return releaseURL, nil
+}
 
-	if err != nil {
-		return err
+func (z *ZIPReleaseGetter) getDownloadRegexp() (*regexp.Regexp, error) {
+	if z.IsPlatformDependent {
+		switch os := runtime.GOOS; os {
+		case "darwin":
+			return regexp.MustCompile(fmt.Sprintf(`(?i)%s_.*_Darwin_x86_64\.zip`, z.AssetName)), nil
+		case "linux":
+			return regexp.MustCompile(fmt.Sprintf(`(?i)%s_.*_Linux_x86_64\.zip`, z.AssetName)), nil
+		default:
+			return nil, fmt.Errorf("%s is not a supported platform for Porter binaries", os)
+		}
 	}
 
-	staticDir := filepath.Join(porterDir, "static")
+	return regexp.MustCompile(fmt.Sprintf(`(?i)%s_.*\.zip`, z.AssetName)), nil
+}
 
-	err = unzipToDir(staticZipFile, staticDir)
+// // DownloadLatestServerRelease retrieves the latest Porter server release from Github, unzips
+// // it, and adds the binary to the porter directory
+// func DownloadLatestServerRelease(porterDir string) error {
+// 	releaseURL, staticReleaseURL, err := getLatestReleaseDownloadURL()
+// 	fmt.Println(releaseURL)
 
-	return err
-}
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	zipFile := filepath.Join(porterDir, "portersrv_latest.zip")
+
+// 	err = downloadToFile(releaseURL, zipFile)
+
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	err = unzipToDir(zipFile, porterDir)
+
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	staticZipFile := filepath.Join(porterDir, "static_latest.zip")
+
+// 	err = downloadToFile(staticReleaseURL, staticZipFile)
+
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	staticDir := filepath.Join(porterDir, "static")
+
+// 	err = unzipToDir(staticZipFile, staticDir)
 
-func downloadToFile(url string, filepath string) error {
-	fmt.Println("Downloading:", url)
+// 	return err
+// }
 
+func (z *ZIPReleaseGetter) downloadToFile(url string) error {
 	// Get the data
 	resp, err := http.Get(url)
 
@@ -104,7 +202,7 @@ func downloadToFile(url string, filepath string) error {
 	defer resp.Body.Close()
 
 	// Create the file
-	out, err := os.Create(filepath)
+	out, err := os.Create(filepath.Join(z.ZipFolderDest, z.ZipName))
 
 	if err != nil {
 		return err
@@ -118,8 +216,8 @@ func downloadToFile(url string, filepath string) error {
 	return err
 }
 
-func unzipToDir(zipfile string, dir string) error {
-	r, err := zip.OpenReader(zipfile)
+func (z *ZIPReleaseGetter) unzipToDir() error {
+	r, err := zip.OpenReader(filepath.Join(z.ZipFolderDest, z.ZipName))
 
 	if err != nil {
 		return err
@@ -129,10 +227,10 @@ func unzipToDir(zipfile string, dir string) error {
 
 	for _, f := range r.File {
 		// Store filename/path for returning and using later on
-		fpath := filepath.Join(dir, f.Name)
+		fpath := filepath.Join(z.AssetFolderDest, f.Name)
 
 		// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
-		if !strings.HasPrefix(fpath, filepath.Clean(dir)+string(os.PathSeparator)) {
+		if !strings.HasPrefix(fpath, filepath.Clean(z.AssetFolderDest)+string(os.PathSeparator)) {
 			return fmt.Errorf("%s: illegal file path", fpath)
 		}
 

+ 31 - 1
cli/cmd/server.go

@@ -175,7 +175,7 @@ func startLocal(
 	staticFilePath := filepath.Join(home, ".porter", "static")
 
 	if _, err := os.Stat(cmdPath); os.IsNotExist(err) {
-		err := github.DownloadLatestServerRelease(porterDir)
+		err := downloadLatestReleases(porterDir)
 
 		if err != nil {
 			color.New(color.FgRed).Println("Failed:", err.Error())
@@ -224,3 +224,33 @@ func stopDocker() error {
 
 	return nil
 }
+
+func downloadLatestReleases(porterDir string) error {
+	z := &github.ZIPReleaseGetter{
+		AssetName:           "portersvr",
+		AssetFolderDest:     porterDir,
+		ZipFolderDest:       porterDir,
+		ZipName:             "portersvr_latest.zip",
+		EntityID:            "porter-dev",
+		RepoName:            "porter",
+		IsPlatformDependent: true,
+	}
+
+	err := z.GetLatestRelease()
+
+	if err != nil {
+		return err
+	}
+
+	zStatic := &github.ZIPReleaseGetter{
+		AssetName:           "static",
+		AssetFolderDest:     filepath.Join(porterDir, "static"),
+		ZipFolderDest:       porterDir,
+		ZipName:             "static_latest.zip",
+		EntityID:            "porter-dev",
+		RepoName:            "porter",
+		IsPlatformDependent: false,
+	}
+
+	return zStatic.GetLatestRelease()
+}

+ 0 - 2
cmd/docker-credential-porter/helper/cache.go

@@ -11,7 +11,6 @@ import (
 	"time"
 
 	"github.com/aws/aws-sdk-go/aws/credentials"
-	"github.com/sirupsen/logrus"
 	"k8s.io/client-go/util/homedir"
 )
 
@@ -162,7 +161,6 @@ func (f *fileCredentialCache) save(registryCache *RegistryCache) error {
 func (f *fileCredentialCache) init() *RegistryCache {
 	registryCache, err := f.load()
 	if err != nil {
-		logrus.WithError(err).Info("Could not load existing cache")
 		f.Clear()
 		registryCache = newRegistryCache()
 	}

+ 51 - 29
cmd/docker-credential-porter/helper/helper.go

@@ -20,7 +20,11 @@ import (
 
 // PorterHelper implements credentials.Helper: it acts as a credentials
 // helper for Docker that allows authentication with different registries.
-type PorterHelper struct{}
+type PorterHelper struct {
+	Debug bool
+
+	credCache CredentialsCache
+}
 
 // Add appends credentials to the store.
 func (p *PorterHelper) Add(cr *credentials.Credentials) error {
@@ -39,18 +43,25 @@ var ecrPattern = regexp.MustCompile(`(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fi
 // Get retrieves credentials from the store.
 // It returns username and secret as strings.
 func (p *PorterHelper) Get(serverURL string) (user string, secret string, err error) {
-	cmd.Setup()
-	var home = homedir.HomeDir()
-	file, _ := os.OpenFile(filepath.Join(home, ".porter", "logs.txt"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
-	log.SetOutput(file)
-
 	// parse the server url for region
 	matches := ecrPattern.FindStringSubmatch(serverURL)
 
 	if len(matches) == 0 {
-		return "", "", fmt.Errorf("docker-credential-porter can only be used with Amazon Elastic Container Registry.")
+		err := fmt.Errorf("only ECR registry URLs are supported")
+
+		if p.Debug {
+			log.Printf("Error: %s\n", err.Error())
+		}
+
+		return "", "", err
 	} else if len(matches) < 3 {
-		return "", "", fmt.Errorf(serverURL + "is not a valid repository URI for Amazon Elastic Container Registry.")
+		err := fmt.Errorf("%s is not a valid ECR repository URI", serverURL)
+
+		if p.Debug {
+			log.Printf("Error: %s\n", err.Error())
+		}
+
+		return "", "", err
 	}
 
 	region := matches[3]
@@ -81,24 +92,12 @@ func (p *PorterHelper) Get(serverURL string) (user string, secret string, err er
 		credCache.Set(serverURL, &AuthEntry{
 			AuthorizationToken: token,
 			RequestedAt:        time.Now(),
-			ExpiresAt:          time.Now().Add(12 * time.Hour),
+			ExpiresAt:          *tokenResp.ExpiresAt,
 			ProxyEndpoint:      serverURL,
 		})
 	}
 
-	decodedToken, err := base64.StdEncoding.DecodeString(token)
-
-	if err != nil {
-		return "", "", fmt.Errorf("Invalid token: %v", err)
-	}
-
-	parts := strings.SplitN(string(decodedToken), ":", 2)
-
-	if len(parts) < 2 {
-		return "", "", fmt.Errorf("Invalid token: expected two parts, got %d", len(parts))
-	}
-
-	return parts[0], parts[1], nil
+	return p.getAuth(token)
 }
 
 // List returns the stored serverURLs and their associated usernames.
@@ -109,20 +108,43 @@ func (p *PorterHelper) List() (map[string]string, error) {
 	res := make(map[string]string)
 
 	for _, entry := range entries {
-		decodedToken, err := base64.StdEncoding.DecodeString(entry.AuthorizationToken)
+		user, _, err := p.getAuth(entry.AuthorizationToken)
 
 		if err != nil {
 			continue
 		}
 
-		parts := strings.SplitN(string(decodedToken), ":", 2)
+		res[entry.ProxyEndpoint] = user
+	}
 
-		if len(parts) < 2 {
-			continue
-		}
+	return res, nil
+}
+
+func (p *PorterHelper) getAuth(token string) (string, string, error) {
+	decodedToken, err := base64.StdEncoding.DecodeString(token)
 
-		res[entry.ProxyEndpoint] = parts[0]
+	if err != nil {
+		return "", "", fmt.Errorf("Invalid token: %v", err)
 	}
 
-	return res, nil
+	parts := strings.SplitN(string(decodedToken), ":", 2)
+
+	if len(parts) < 2 {
+		return "", "", fmt.Errorf("Invalid token: expected two parts, got %d", len(parts))
+	}
+
+	return parts[0], parts[1], nil
+}
+
+func (p *PorterHelper) init() {
+	cmd.Setup()
+
+	if p.Debug {
+		var home = homedir.HomeDir()
+		file, err := os.OpenFile(filepath.Join(home, ".porter", "logs.txt"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
+
+		if err == nil {
+			log.SetOutput(file)
+		}
+	}
 }

+ 20 - 1
cmd/docker-credential-porter/main.go

@@ -1,10 +1,29 @@
 package main
 
 import (
+	"flag"
+	"fmt"
+	"os"
+
 	"github.com/docker/docker-credential-helpers/credentials"
 	"github.com/porter-dev/porter/cmd/docker-credential-porter/helper"
 )
 
+// Version will be linked by an ldflag during build
+var Version string = "dev"
+
 func main() {
-	credentials.Serve(&helper.PorterHelper{})
+	var versionFlag bool
+	flag.BoolVar(&versionFlag, "version", false, "print version and exit")
+	flag.Parse()
+
+	// Exit safely when version is used
+	if versionFlag {
+		fmt.Println(Version)
+		os.Exit(0)
+	}
+
+	credentials.Serve(&helper.PorterHelper{
+		Debug: Version == "dev",
+	})
 }

+ 1 - 0
go.mod

@@ -12,6 +12,7 @@ require (
 	github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20201113001948-d77edb6d2e47
 	github.com/containerd/containerd v1.4.1 // indirect
 	github.com/coreos/rkt v1.30.0
+	github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492
 	github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce
 	github.com/docker/docker-credential-helpers v0.6.3
 	github.com/docker/go-connections v0.4.0

+ 7 - 2
server/api/registry_handler.go

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"net/http"
 	"strconv"
+	"time"
 
 	"github.com/porter-dev/porter/internal/registry"
 
@@ -99,7 +100,8 @@ func (app *App) HandleListProjectRegistries(w http.ResponseWriter, r *http.Reque
 
 // temp -- token response
 type ECRTokenResponse struct {
-	Token string `json:"token"`
+	Token     string     `json:"token"`
+	ExpiresAt *time.Time `json:"expires_at"`
 }
 
 // HandleGetProjectRegistryECRToken gets an ECR token for a registry
@@ -121,6 +123,7 @@ func (app *App) HandleGetProjectRegistryECRToken(w http.ResponseWriter, r *http.
 	// list registries and find one that matches the region
 	regs, err := app.Repo.Registry.ListRegistriesByProjectID(uint(projID))
 	var token string
+	var expiresAt *time.Time
 
 	for _, reg := range regs {
 		if reg.AWSIntegrationID != 0 {
@@ -150,12 +153,14 @@ func (app *App) HandleGetProjectRegistryECRToken(w http.ResponseWriter, r *http.
 				}
 
 				token = *output.AuthorizationData[0].AuthorizationToken
+				expiresAt = output.AuthorizationData[0].ExpiresAt
 			}
 		}
 	}
 
 	resp := &ECRTokenResponse{
-		Token: token,
+		Token:     token,
+		ExpiresAt: expiresAt,
 	}
 
 	w.WriteHeader(http.StatusOK)