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

Merge pull request #317 from porter-dev/beta.3.private-docker-registry

Beta.3.private docker registry
abelanger5 5 лет назад
Родитель
Сommit
2186f82c02

+ 76 - 0
cli/cmd/api/registry.go

@@ -59,6 +59,53 @@ func (c *Client) CreateECR(
 	return bodyResp, nil
 }
 
+// CreatePrivateRegistryRequest represents the accepted fields for creating
+// a private registry
+type CreatePrivateRegistryRequest struct {
+	Name               string `json:"name"`
+	URL                string `json:"url"`
+	BasicIntegrationID uint   `json:"basic_integration_id"`
+}
+
+// CreatePrivateRegistryResponse is the resulting registry after creation
+type CreatePrivateRegistryResponse models.RegistryExternal
+
+// CreatePrivateRegistry creates a private registry integration
+func (c *Client) CreatePrivateRegistry(
+	ctx context.Context,
+	projectID uint,
+	createPR *CreatePrivateRegistryRequest,
+) (*CreatePrivateRegistryResponse, error) {
+	data, err := json.Marshal(createPR)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := http.NewRequest(
+		"POST",
+		fmt.Sprintf("%s/projects/%d/registries", c.BaseURL, projectID),
+		strings.NewReader(string(data)),
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+	bodyResp := &CreatePrivateRegistryResponse{}
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return bodyResp, nil
+}
+
 // CreateGCRRequest represents the accepted fields for creating
 // a GCR registry
 type CreateGCRRequest struct {
@@ -290,6 +337,35 @@ func (c *Client) GetGCRAuthorizationToken(
 	return bodyResp, nil
 }
 
+// GetDockerhubAuthorizationToken gets a Docker Hub authorization token
+func (c *Client) GetDockerhubAuthorizationToken(
+	ctx context.Context,
+	projectID uint,
+) (*GetTokenResponse, error) {
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/projects/%d/registries/dockerhub/token", c.BaseURL, projectID),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	bodyResp := &GetTokenResponse{}
+	req = req.WithContext(ctx)
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return bodyResp, nil
+}
+
 type GetDOCRTokenRequest struct {
 	ServerURL string `json:"server_url"`
 }

+ 52 - 0
cli/cmd/connect.go

@@ -43,6 +43,30 @@ var connectECRCmd = &cobra.Command{
 	},
 }
 
+var connectDockerhubCmd = &cobra.Command{
+	Use:   "dockerhub",
+	Short: "Adds a Docker Hub registry integration to a project",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, runConnectDockerhub)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var connectRegistryCmd = &cobra.Command{
+	Use:   "registry",
+	Short: "Adds a custom image registry to a project",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, runConnectRegistry)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
 var connectActionsCmd = &cobra.Command{
 	Use:   "actions",
 	Short: "Adds Github Actions to a project",
@@ -127,6 +151,8 @@ func init() {
 
 	connectCmd.AddCommand(connectActionsCmd)
 	connectCmd.AddCommand(connectECRCmd)
+	connectCmd.AddCommand(connectRegistryCmd)
+	connectCmd.AddCommand(connectDockerhubCmd)
 	connectCmd.AddCommand(connectGCRCmd)
 	connectCmd.AddCommand(connectDOCRCmd)
 	connectCmd.AddCommand(connectHRCmd)
@@ -193,6 +219,32 @@ func runConnectDOCR(_ *api.AuthCheckResponse, client *api.Client, _ []string) er
 	return setRegistry(regID)
 }
 
+func runConnectDockerhub(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {
+	regID, err := connect.Dockerhub(
+		client,
+		getProjectID(),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	return setRegistry(regID)
+}
+
+func runConnectRegistry(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {
+	regID, err := connect.Registry(
+		client,
+		getProjectID(),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	return setRegistry(regID)
+}
+
 func runConnectHelmRepoBasic(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {
 	hrID, err := connect.Helm(
 		client,

+ 76 - 0
cli/cmd/connect/dockerhub.go

@@ -0,0 +1,76 @@
+package connect
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/fatih/color"
+	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/utils"
+)
+
+func Dockerhub(
+	client *api.Client,
+	projectID uint,
+) (uint, error) {
+	// if project ID is 0, ask the user to set the project ID or create a project
+	if projectID == 0 {
+		return 0, fmt.Errorf("no project set, please run porter project set [id]")
+	}
+
+	// query for dockerhub name
+
+	repoName, err := utils.PromptPlaintext(fmt.Sprintf(`Provide the Docker Hub image path, in the form of ${org_name}/${repo_name}. For example, porter1/porter.
+Image path: `))
+
+	if err != nil {
+		return 0, err
+	}
+
+	username, err := utils.PromptPlaintext(fmt.Sprintf(`Docker Hub username: `))
+
+	if err != nil {
+		return 0, err
+	}
+
+	password, err := utils.PromptPassword(`Provide the Docker Hub personal access token.
+Token:`)
+
+	if err != nil {
+		return 0, err
+	}
+
+	// create the basic auth integration
+	integration, err := client.CreateBasicAuthIntegration(
+		context.Background(),
+		projectID,
+		&api.CreateBasicAuthIntegrationRequest{
+			Username: username,
+			Password: password,
+		},
+	)
+
+	if err != nil {
+		return 0, err
+	}
+
+	color.New(color.FgGreen).Printf("created basic auth integration with id %d\n", integration.ID)
+
+	reg, err := client.CreatePrivateRegistry(
+		context.Background(),
+		projectID,
+		&api.CreatePrivateRegistryRequest{
+			URL:                fmt.Sprintf("index.docker.io/%s", repoName),
+			Name:               repoName,
+			BasicIntegrationID: integration.ID,
+		},
+	)
+
+	if err != nil {
+		return 0, err
+	}
+
+	color.New(color.FgGreen).Printf("created private registry with id %d and name %s\n", reg.ID, reg.Name)
+
+	return reg.ID, nil
+}

+ 76 - 0
cli/cmd/connect/registry.go

@@ -0,0 +1,76 @@
+package connect
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/fatih/color"
+	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/utils"
+)
+
+// Helm connects a Helm repository using HTTP basic authentication
+func Registry(
+	client *api.Client,
+	projectID uint,
+) (uint, error) {
+	// if project ID is 0, ask the user to set the project ID or create a project
+	if projectID == 0 {
+		return 0, fmt.Errorf("no project set, please run porter project set [id]")
+	}
+
+	// query for helm repo name
+	repoURL, err := utils.PromptPlaintext(fmt.Sprintf(`Provide the image registry URL (include the protocol). For example, https://my-custom-registry.getporter.dev.
+Image registry URL: `))
+
+	if err != nil {
+		return 0, err
+	}
+
+	username, err := utils.PromptPlaintext(fmt.Sprintf(`Provide the username/password for authentication (press enter if no authenicaiton is required).
+Username: `))
+
+	if err != nil {
+		return 0, err
+	}
+
+	password, err := utils.PromptPasswordWithConfirmation()
+
+	if err != nil {
+		return 0, err
+	}
+
+	// create the basic auth integration
+	integration, err := client.CreateBasicAuthIntegration(
+		context.Background(),
+		projectID,
+		&api.CreateBasicAuthIntegrationRequest{
+			Username: username,
+			Password: password,
+		},
+	)
+
+	if err != nil {
+		return 0, err
+	}
+
+	color.New(color.FgGreen).Printf("created basic auth integration with id %d\n", integration.ID)
+
+	reg, err := client.CreatePrivateRegistry(
+		context.Background(),
+		projectID,
+		&api.CreatePrivateRegistryRequest{
+			URL:                repoURL,
+			Name:               repoURL,
+			BasicIntegrationID: integration.ID,
+		},
+	)
+
+	if err != nil {
+		return 0, err
+	}
+
+	color.New(color.FgGreen).Printf("created private registry with id %d and name %s\n", reg.ID, reg.Name)
+
+	return reg.ID, nil
+}

+ 46 - 1
cli/cmd/docker.go

@@ -2,7 +2,9 @@ package cmd
 
 import (
 	"context"
+	"encoding/base64"
 	"encoding/json"
+	"fmt"
 	"io/ioutil"
 	"net/url"
 	"os"
@@ -16,6 +18,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/docker/cli/cli/config/configfile"
+	"github.com/docker/cli/cli/config/types"
 )
 
 var dockerCmd = &cobra.Command{
@@ -132,7 +135,49 @@ func dockerConfig(user *api.AuthCheckResponse, client *api.Client, args []string
 	}
 
 	for _, regURL := range regToAdd {
-		config.CredentialHelpers[regURL] = "porter"
+		// if this is a dockerhub registry, see if an auth config has already been generated
+		// for index.docker.io
+		if strings.Contains(regURL, "index.docker.io") {
+			isAuthenticated := false
+
+			for key, _ := range config.AuthConfigs {
+				if key == "https://index.docker.io/v1/" {
+					isAuthenticated = true
+				}
+			}
+
+			if !isAuthenticated {
+				// get a dockerhub token from the Porter API
+				tokenResp, err := client.GetDockerhubAuthorizationToken(context.Background(), getProjectID())
+
+				if err != nil {
+					return err
+				}
+
+				decodedToken, err := base64.StdEncoding.DecodeString(tokenResp.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))
+				}
+
+				config.AuthConfigs["https://index.docker.io/v1/"] = types.AuthConfig{
+					Auth:     tokenResp.Token,
+					Username: parts[0],
+					Password: parts[1],
+				}
+
+				// since we're using token-based auth, unset the credstore
+				config.CredentialsStore = ""
+			}
+		} else {
+			config.CredentialHelpers[regURL] = "porter"
+		}
 	}
 
 	return config.Save()

+ 1 - 1
cli/cmd/version.go

@@ -7,7 +7,7 @@ import (
 )
 
 // Version will be linked by an ldflag during build
-var Version string = "dev"
+var Version string = "v0.1.0-beta.3.4"
 
 var versionCmd = &cobra.Command{
 	Use:     "version",

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

@@ -10,7 +10,7 @@ import (
 )
 
 // Version will be linked by an ldflag during build
-var Version string = "dev"
+var Version string = "v0.1.0-beta.3.4"
 
 func main() {
 	var versionFlag bool

+ 14 - 12
internal/forms/registry.go

@@ -9,23 +9,25 @@ import (
 // CreateRegistry represents the accepted values for creating a
 // registry
 type CreateRegistry struct {
-	Name             string `json:"name" form:"required"`
-	ProjectID        uint   `json:"project_id" form:"required"`
-	URL              string `json:"url"`
-	GCPIntegrationID uint   `json:"gcp_integration_id"`
-	AWSIntegrationID uint   `json:"aws_integration_id"`
-	DOIntegrationID  uint   `json:"do_integration_id"`
+	Name               string `json:"name" form:"required"`
+	ProjectID          uint   `json:"project_id" form:"required"`
+	URL                string `json:"url"`
+	GCPIntegrationID   uint   `json:"gcp_integration_id"`
+	AWSIntegrationID   uint   `json:"aws_integration_id"`
+	DOIntegrationID    uint   `json:"do_integration_id"`
+	BasicIntegrationID uint   `json:"basic_integration_id"`
 }
 
 // ToRegistry converts the form to a gorm registry model
 func (cr *CreateRegistry) ToRegistry(repo repository.Repository) (*models.Registry, error) {
 	registry := &models.Registry{
-		Name:             cr.Name,
-		ProjectID:        cr.ProjectID,
-		URL:              cr.URL,
-		GCPIntegrationID: cr.GCPIntegrationID,
-		AWSIntegrationID: cr.AWSIntegrationID,
-		DOIntegrationID:  cr.DOIntegrationID,
+		Name:               cr.Name,
+		ProjectID:          cr.ProjectID,
+		URL:                cr.URL,
+		GCPIntegrationID:   cr.GCPIntegrationID,
+		AWSIntegrationID:   cr.AWSIntegrationID,
+		DOIntegrationID:    cr.DOIntegrationID,
+		BasicIntegrationID: cr.BasicIntegrationID,
 	}
 
 	if registry.URL == "" && registry.AWSIntegrationID != 0 {

+ 13 - 12
internal/models/integrations/integration.go

@@ -5,18 +5,19 @@ type IntegrationService string
 
 // The list of supported third-party services
 const (
-	GKE      IntegrationService = "gke"
-	DOKS     IntegrationService = "doks"
-	GCS      IntegrationService = "gcs"
-	S3       IntegrationService = "s3"
-	HelmRepo IntegrationService = "helm"
-	EKS      IntegrationService = "eks"
-	Kube     IntegrationService = "kube"
-	GCR      IntegrationService = "gcr"
-	ECR      IntegrationService = "ecr"
-	DOCR     IntegrationService = "docr"
-	Github   IntegrationService = "github"
-	Docker   IntegrationService = "docker"
+	GKE       IntegrationService = "gke"
+	DOKS      IntegrationService = "doks"
+	GCS       IntegrationService = "gcs"
+	S3        IntegrationService = "s3"
+	HelmRepo  IntegrationService = "helm"
+	EKS       IntegrationService = "eks"
+	Kube      IntegrationService = "kube"
+	GCR       IntegrationService = "gcr"
+	ECR       IntegrationService = "ecr"
+	DOCR      IntegrationService = "docr"
+	Github    IntegrationService = "github"
+	DockerHub IntegrationService = "dockerhub"
+	Docker    IntegrationService = "docker"
 )
 
 // PorterIntegration is a supported integration service, specifying an auth

+ 8 - 3
internal/models/registry.go

@@ -1,6 +1,8 @@
 package models
 
 import (
+	"strings"
+
 	"github.com/porter-dev/porter/internal/models/integrations"
 	"gorm.io/gorm"
 )
@@ -26,9 +28,10 @@ type Registry struct {
 	// All fields below this line are encrypted before storage
 	// ------------------------------------------------------------------
 
-	GCPIntegrationID uint
-	AWSIntegrationID uint
-	DOIntegrationID  uint
+	GCPIntegrationID   uint
+	AWSIntegrationID   uint
+	DOIntegrationID    uint
+	BasicIntegrationID uint
 
 	// A token cache that can be used by an auth mechanism (integration), if desired
 	TokenCache integrations.RegTokenCache
@@ -64,6 +67,8 @@ func (r *Registry) Externalize() *RegistryExternal {
 		serv = integrations.GCR
 	} else if r.DOIntegrationID != 0 {
 		serv = integrations.DOCR
+	} else if strings.Contains(r.URL, "index.docker.io") {
+		serv = integrations.DockerHub
 	}
 
 	return &RegistryExternal{

+ 256 - 0
internal/registry/registry.go

@@ -71,6 +71,10 @@ func (r *Registry) ListRepositories(
 		return r.listDOCRRepositories(repo, doAuth)
 	}
 
+	if r.BasicIntegrationID != 0 {
+		return r.listPrivateRegistryRepositories(repo)
+	}
+
 	return nil, fmt.Errorf("error listing repositories")
 }
 
@@ -252,6 +256,102 @@ func (r *Registry) listDOCRRepositories(
 	return res, nil
 }
 
+func (r *Registry) listPrivateRegistryRepositories(
+	repo repository.Repository,
+) ([]*Repository, error) {
+	// handle dockerhub different, as it doesn't implement the docker registry http api
+	if strings.Contains(r.URL, "docker.io") {
+		// in this case, we just return the single dockerhub repository that's linked
+		res := make([]*Repository, 0)
+
+		res = append(res, &Repository{
+			Name: strings.Split(r.URL, "docker.io/")[1],
+			URI:  r.URL,
+		})
+
+		return res, nil
+	}
+
+	basic, err := repo.BasicIntegration.ReadBasicIntegration(
+		r.BasicIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// Just use service account key to authenticate, since scopes may not be in place
+	// for oauth. This also prevents us from making more requests.
+	client := &http.Client{}
+
+	// get the host and scheme to make the request
+	parsedURL, err := url.Parse(r.URL)
+
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s://%s/v2/_catalog", parsedURL.Scheme, parsedURL.Host),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req.SetBasicAuth(string(basic.Username), string(basic.Password))
+
+	resp, err := client.Do(req)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// if the status code is 404, fallback to the Docker Hub implementation
+	if resp.StatusCode == 404 {
+		req, err := http.NewRequest(
+			"GET",
+			fmt.Sprintf("%s/", r.URL),
+			nil,
+		)
+
+		if err != nil {
+			return nil, err
+		}
+
+		fmt.Println("AUTH IS", string(basic.Username), string(basic.Password))
+
+		req.SetBasicAuth(string(basic.Username), string(basic.Password))
+
+		resp, err = client.Do(req)
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	gcrResp := gcrRepositoryResp{}
+
+	fmt.Println("STATUS IS", resp.Status)
+
+	if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
+		return nil, fmt.Errorf("Could not read private registry repositories: %v", err)
+	}
+
+	res := make([]*Repository, 0)
+
+	if err != nil {
+		return nil, err
+	}
+
+	for _, repo := range gcrResp.Repositories {
+		res = append(res, &Repository{
+			Name: repo,
+			URI:  parsedURL.Host + "/" + repo,
+		})
+	}
+
+	return res, nil
+}
+
 func (r *Registry) getTokenCache() (tok *ints.TokenCache, err error) {
 	return &ints.TokenCache{
 		Token:  r.TokenCache.Token,
@@ -296,6 +396,10 @@ func (r *Registry) ListImages(
 		return r.listDOCRImages(repoName, repo, doAuth)
 	}
 
+	if r.BasicIntegrationID != 0 {
+		return r.listPrivateRegistryImages(repoName, repo)
+	}
+
 	return nil, fmt.Errorf("error listing images")
 }
 
@@ -444,6 +548,118 @@ func (r *Registry) listDOCRImages(
 	return res, nil
 }
 
+func (r *Registry) listPrivateRegistryImages(repoName string, repo repository.Repository) ([]*Image, error) {
+	// handle dockerhub different, as it doesn't implement the docker registry http api
+	if strings.Contains(r.URL, "docker.io") {
+		return r.listDockerHubImages(repoName, repo)
+	}
+
+	basic, err := repo.BasicIntegration.ReadBasicIntegration(
+		r.BasicIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// Just use service account key to authenticate, since scopes may not be in place
+	// for oauth. This also prevents us from making more requests.
+	client := &http.Client{}
+
+	// get the host and scheme to make the request
+	parsedURL, err := url.Parse(r.URL)
+
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s://%s/v2/%s/tags/list", parsedURL.Scheme, parsedURL.Host, repoName),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req.SetBasicAuth(string(basic.Username), string(basic.Password))
+
+	resp, err := client.Do(req)
+
+	if err != nil {
+		return nil, err
+	}
+
+	gcrResp := gcrImageResp{}
+
+	if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
+		return nil, fmt.Errorf("Could not read private registry repositories: %v", err)
+	}
+
+	res := make([]*Image, 0)
+
+	for _, tag := range gcrResp.Tags {
+		res = append(res, &Image{
+			RepositoryName: repoName,
+			Tag:            tag,
+		})
+	}
+
+	return res, nil
+}
+
+type dockerHubImageResult struct {
+	Name string `json:"name"`
+}
+
+type dockerHubImageResp struct {
+	Results []dockerHubImageResult `json:"results"`
+}
+
+func (r *Registry) listDockerHubImages(repoName string, repo repository.Repository) ([]*Image, error) {
+	basic, err := repo.BasicIntegration.ReadBasicIntegration(
+		r.BasicIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	client := &http.Client{}
+
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/tags", strings.Split(r.URL, "docker.io/")[1]),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req.SetBasicAuth(string(basic.Username), string(basic.Password))
+
+	resp, err := client.Do(req)
+
+	if err != nil {
+		return nil, err
+	}
+
+	imageResp := dockerHubImageResp{}
+
+	if err := json.NewDecoder(resp.Body).Decode(&imageResp); err != nil {
+		return nil, fmt.Errorf("Could not read private registry repositories: %v", err)
+	}
+
+	res := make([]*Image, 0)
+
+	for _, result := range imageResp.Results {
+		res = append(res, &Image{
+			RepositoryName: repoName,
+			Tag:            result.Name,
+		})
+	}
+
+	return res, nil
+}
+
 // GetDockerConfigJSON returns a dockerconfigjson file contents with "auths"
 // populated.
 func (r *Registry) GetDockerConfigJSON(
@@ -466,6 +682,10 @@ func (r *Registry) GetDockerConfigJSON(
 		conf, err = r.getDOCRDockerConfigFile(repo, doAuth)
 	}
 
+	if r.BasicIntegrationID != 0 {
+		conf, err = r.getPrivateRegistryDockerConfigFile(repo)
+	}
+
 	if err != nil {
 		return nil, err
 	}
@@ -596,6 +816,42 @@ func (r *Registry) getDOCRDockerConfigFile(
 	}, nil
 }
 
+func (r *Registry) getPrivateRegistryDockerConfigFile(
+	repo repository.Repository,
+) (*configfile.ConfigFile, error) {
+	basic, err := repo.BasicIntegration.ReadBasicIntegration(
+		r.BasicIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	key := r.URL
+
+	if !strings.Contains(key, "http") {
+		key = "https://" + key
+	}
+
+	parsedURL, _ := url.Parse(key)
+
+	authConfigKey := parsedURL.Host
+
+	if strings.Contains(r.URL, "index.docker.io") {
+		authConfigKey = "https://index.docker.io/v1/"
+	}
+
+	return &configfile.ConfigFile{
+		AuthConfigs: map[string]types.AuthConfig{
+			authConfigKey: types.AuthConfig{
+				Username: string(basic.Username),
+				Password: string(basic.Password),
+				Auth:     generateAuthToken(string(basic.Username), string(basic.Password)),
+			},
+		},
+	}, nil
+}
+
 func generateAuthToken(username, password string) string {
 	return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
 }

+ 3 - 9
server/api/integration_handler.go

@@ -66,15 +66,13 @@ func (app *App) HandleListRepoIntegrations(w http.ResponseWriter, r *http.Reques
 
 // HandleCreateGCPIntegration creates a new GCP integration in the DB
 func (app *App) HandleCreateGCPIntegration(w http.ResponseWriter, r *http.Request) {
-	session, err := app.Store.Get(r, app.ServerConf.CookieName)
+	userID, err := app.getUserIDFromRequest(r)
 
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
 
-	userID, _ := session.Values["user_id"].(uint)
-
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 
 	if err != nil || projID == 0 {
@@ -129,15 +127,13 @@ func (app *App) HandleCreateGCPIntegration(w http.ResponseWriter, r *http.Reques
 
 // HandleCreateAWSIntegration creates a new AWS integration in the DB
 func (app *App) HandleCreateAWSIntegration(w http.ResponseWriter, r *http.Request) {
-	session, err := app.Store.Get(r, app.ServerConf.CookieName)
+	userID, err := app.getUserIDFromRequest(r)
 
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
 
-	userID, _ := session.Values["user_id"].(uint)
-
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 
 	if err != nil || projID == 0 {
@@ -192,15 +188,13 @@ func (app *App) HandleCreateAWSIntegration(w http.ResponseWriter, r *http.Reques
 
 // HandleCreateBasicAuthIntegration creates a new basic auth integration in the DB
 func (app *App) HandleCreateBasicAuthIntegration(w http.ResponseWriter, r *http.Request) {
-	session, err := app.Store.Get(r, app.ServerConf.CookieName)
+	userID, err := app.getUserIDFromRequest(r)
 
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
 
-	userID, _ := session.Values["user_id"].(uint)
-
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 
 	if err != nil || projID == 0 {

+ 45 - 0
server/api/registry_handler.go

@@ -1,6 +1,7 @@
 package api
 
 import (
+	"encoding/base64"
 	"encoding/json"
 	"net/http"
 	"strconv"
@@ -174,6 +175,50 @@ func (app *App) HandleGetProjectRegistryECRToken(w http.ResponseWriter, r *http.
 	}
 }
 
+// HandleGetProjectRegistryDockerhubToken gets a Dockerhub token for a registry
+func (app *App) HandleGetProjectRegistryDockerhubToken(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// 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.BasicIntegrationID != 0 && strings.Contains(reg.URL, "index.docker.io") {
+			basic, err := app.Repo.BasicIntegration.ReadBasicIntegration(reg.BasicIntegrationID)
+
+			if err != nil {
+				app.handleErrorDataRead(err, w)
+				return
+			}
+
+			token = base64.StdEncoding.EncodeToString([]byte(string(basic.Username) + ":" + string(basic.Password)))
+
+			// we'll just set an arbitrary 30-day expiry time (this is not enforced)
+			timeExpires := time.Now().Add(30 * 24 * 3600 * time.Second)
+			expiresAt = &timeExpires
+		}
+	}
+
+	resp := &RegTokenResponse{
+		Token:     token,
+		ExpiresAt: expiresAt,
+	}
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(resp); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
 type GCRTokenRequestBody struct {
 	ServerURL string `json:"server_url"`
 }

+ 19 - 0
server/api/user_handler.go

@@ -480,3 +480,22 @@ func (app *App) sendUser(w http.ResponseWriter, userID uint, email, redirect str
 	}
 	return nil
 }
+
+func (app *App) getUserIDFromRequest(r *http.Request) (uint, error) {
+	session, err := app.Store.Get(r, app.ServerConf.CookieName)
+
+	if err != nil {
+		return 0, err
+	}
+
+	// first, check for token
+	tok := app.getTokenFromRequest(r)
+
+	if tok != nil {
+		return tok.IBy, nil
+	}
+
+	userID, _ := session.Values["user_id"].(uint)
+
+	return userID, nil
+}

+ 10 - 0
server/router/router.go

@@ -753,6 +753,16 @@ func New(a *api.App) *chi.Mux {
 			),
 		)
 
+		r.Method(
+			"GET",
+			"/projects/{project_id}/registries/dockerhub/token",
+			auth.DoesUserHaveProjectAccess(
+				requestlog.NewHandler(a.HandleGetProjectRegistryDockerhubToken, l),
+				mw.URLParam,
+				mw.WriteAccess,
+			),
+		)
+
 		r.Method(
 			"GET",
 			"/projects/{project_id}/registries/docr/token",