Skip to content

Add support for local Docker images #1114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 9, 2020
8 changes: 2 additions & 6 deletions pkg/lib/configreader/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"time"

"github.com/cortexlabs/cortex/pkg/lib/aws"
"github.com/cortexlabs/cortex/pkg/lib/docker"
"github.com/cortexlabs/cortex/pkg/lib/files"
"github.com/cortexlabs/cortex/pkg/lib/urls"
)
Expand Down Expand Up @@ -155,12 +156,7 @@ func ValidateImageVersion(image, cortexVersion string) (string, error) {
return image, nil
}

var tag string

if colonIndex := strings.LastIndex(image, ":"); colonIndex != -1 {
tag = image[colonIndex+1:]
}

tag := docker.ExtractImageTag(image)
// in docker, missing tag implies "latest"
if tag == "" {
tag = "latest"
Expand Down
40 changes: 29 additions & 11 deletions pkg/lib/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/cortexlabs/cortex/pkg/lib/exit"
"github.com/cortexlabs/cortex/pkg/lib/parallel"
"github.com/cortexlabs/cortex/pkg/lib/print"
"github.com/cortexlabs/cortex/pkg/lib/slices"
dockertypes "github.com/docker/docker/api/types"
dockerclient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/jsonmessage"
Expand Down Expand Up @@ -142,17 +143,8 @@ func PullImage(image string, encodedAuthConfig string, pullVerbosity PullVerbosi
return false, err
}

existingImages, err := dockerClient.ImageList(context.Background(), dockertypes.ImageListOptions{})
if err != nil {
return false, WrapDockerError(err)
}

for _, existingImage := range existingImages {
for _, tag := range existingImage.RepoTags {
if tag == image {
return false, nil
}
}
if err := CheckLocalImageAccessible(dockerClient, image); err == nil {
return false, nil
}

pullOutput, err := dockerClient.ImagePull(context.Background(), image, dockertypes.ImagePullOptions{
Expand Down Expand Up @@ -245,3 +237,29 @@ func CheckImageAccessible(dockerClient *Client, dockerImage, authConfig string)
}
return nil
}

func CheckLocalImageAccessible(dockerClient *Client, dockerImage string) error {
images, err := dockerClient.ImageList(context.Background(), dockertypes.ImageListOptions{})
if err != nil {
return WrapDockerError(err)
}

// in docker, missing tag implies "latest"
if ExtractImageTag(dockerImage) == "" {
dockerImage = fmt.Sprintf("%s:latest", dockerImage)
}

for _, image := range images {
if slices.HasString(image.RepoTags, dockerImage) {
return nil
}
}
return ErrorImageInaccessible(dockerImage, nil)
}

func ExtractImageTag(dockerImage string) string {
if colonIndex := strings.LastIndex(dockerImage, ":"); colonIndex != -1 {
return dockerImage[colonIndex+1:]
}
return ""
}
8 changes: 6 additions & 2 deletions pkg/lib/docker/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,14 @@ func ErrorDockerPermissions(err error) error {
}

func ErrorImageInaccessible(image string, cause error) error {
dockerErrMsg := errors.Message(cause)
message := fmt.Sprintf("%s is not accessible", image)
if cause != nil {
message += "\n" + errors.Message(cause) // add \n because docker client errors are
}

return errors.WithStack(&errors.Error{
Kind: ErrImageInaccessible,
Message: fmt.Sprintf("%s is not accessible\n%s", image, dockerErrMsg), // add \n because docker client errors are verbose
Message: message,
Cause: cause,
})
}
24 changes: 16 additions & 8 deletions pkg/types/spec/validations.go
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ func validatePredictor(predictor *userconfig.Predictor, projectFiles ProjectFile
if err := validateTensorFlowPredictor(predictor, providerType, projectFiles, awsClient); err != nil {
return err
}
if err := validateDockerImagePath(predictor.TensorFlowServingImage, awsClient); err != nil {
if err := validateDockerImagePath(predictor.TensorFlowServingImage, providerType, awsClient); err != nil {
return errors.Wrap(err, userconfig.TensorFlowServingImageKey)
}
case userconfig.ONNXPredictorType:
Expand All @@ -480,7 +480,7 @@ func validatePredictor(predictor *userconfig.Predictor, projectFiles ProjectFile
}
}

if err := validateDockerImagePath(predictor.Image, awsClient); err != nil {
if err := validateDockerImagePath(predictor.Image, providerType, awsClient); err != nil {
return errors.Wrap(err, userconfig.ImageKey)
}

Expand Down Expand Up @@ -828,14 +828,26 @@ func FindDuplicateNames(apis []userconfig.API) []userconfig.API {
return nil
}

func validateDockerImagePath(image string, awsClient *aws.Client) error {
func validateDockerImagePath(image string, providerType types.ProviderType, awsClient *aws.Client) error {
if consts.DefaultImagePathsSet.Has(image) {
return nil
}
if _, err := cr.ValidateImageVersion(image, consts.CortexVersion); err != nil {
return err
}

dockerClient, err := docker.GetDockerClient()
if err != nil {
return err
}

if providerType == types.LocalProviderType {
// short circuit if the image is already available locally
if err := docker.CheckLocalImageAccessible(dockerClient, image); err == nil {
return nil
}
}

dockerAuth := docker.NoAuth
if regex.IsValidECRURL(image) {
if awsClient.IsAnonymous {
Expand Down Expand Up @@ -871,13 +883,9 @@ func validateDockerImagePath(image string, awsClient *aws.Client) error {
}
}

dockerClient, err := docker.GetDockerClient()
if err != nil {
return err
}

if err := docker.CheckImageAccessible(dockerClient, image, dockerAuth); err != nil {
return err
}

return nil
}