// Copyright © 2020-2021 Ettore Di Giacinto // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, see . package client import ( "encoding/json" "fmt" "os" "path" "path/filepath" "github.com/docker/docker/api/types" "github.com/docker/go-units" "github.com/pkg/errors" luetTypes "github.com/mudler/luet/pkg/api/core/types" "github.com/mudler/luet/pkg/api/core/types/artifact" "github.com/mudler/luet/pkg/helpers/docker" fileHelper "github.com/mudler/luet/pkg/helpers/file" ) const ( errImageDownloadMsg = "failed downloading image %s: %s" ) type DockerClient struct { RepoData RepoData auth *types.AuthConfig Cache *artifact.ArtifactCache context *luetTypes.Context } func NewDockerClient(r RepoData, ctx *luetTypes.Context) *DockerClient { auth := &types.AuthConfig{} dat, _ := json.Marshal(r.Authentication) json.Unmarshal(dat, auth) return &DockerClient{RepoData: r, auth: auth, Cache: artifact.NewCache(ctx.Config.GetSystem().GetSystemPkgsCacheDirPath()), context: ctx, } } func (c *DockerClient) DownloadArtifact(a *artifact.PackageArtifact) (*artifact.PackageArtifact, error) { //var u *url.URL = nil var err error c.context.Spinner() defer c.context.SpinnerStop() resultingArtifact := a.ShallowCopy() artifactName := path.Base(a.Path) downloaded := false // TODO: // Files are in URI/packagename:version (GetPackageImageName() method) // use downloadAndExtract .. and egenrate an archive to consume. Checksum should be already checked while downloading the image // with the above functions, because Docker images already contain such metadata // - Check how verification is done when calling DownloadArtifact outside, similarly we need to check DownloadFile, and how verification // is done in such cases (see repository.go) // Check if file is already in cache fileName, err := c.Cache.Get(a) // Check if file is already in cache if err == nil { resultingArtifact = a resultingArtifact.Path = fileName resultingArtifact.Checksums = artifact.Checksums{} c.context.Debug("Use artifact", artifactName, "from cache.") } else { temp, err := c.context.Config.GetSystem().TempDir("image") if err != nil { return nil, err } defer os.RemoveAll(temp) tempArtifact, err := c.context.Config.GetSystem().TempFile("artifact") if err != nil { return nil, err } defer os.RemoveAll(tempArtifact.Name()) for _, uri := range c.RepoData.Urls { imageName := fmt.Sprintf("%s:%s", uri, a.CompileSpec.GetPackage().ImageID()) c.context.Info("Downloading image", imageName) contentstore, err := c.context.Config.GetSystem().TempDir("contentstore") if err != nil { c.context.Warning("Cannot create contentstore", err.Error()) continue } // imageName := fmt.Sprintf("%s/%s", uri, artifact.GetCompileSpec().GetPackage().GetPackageImageName()) info, err := docker.DownloadAndExtractDockerImage(contentstore, imageName, temp, c.auth, c.RepoData.Verify) if err != nil { c.context.Warning(fmt.Sprintf(errImageDownloadMsg, imageName, err.Error())) continue } c.context.Info(fmt.Sprintf("Pulled: %s", info.Target.Digest)) c.context.Info(fmt.Sprintf("Size: %s", units.BytesSize(float64(info.Target.Size)))) c.context.Debug("\nCompressing result ", filepath.Join(temp), "to", tempArtifact.Name()) // We discard checksum, that are checked while during pull and unpack resultingArtifact.Checksums = artifact.Checksums{} resultingArtifact.Path = tempArtifact.Name() // First set to cache file err = resultingArtifact.Compress(temp, 1) if err != nil { c.context.Error(fmt.Sprintf("Failed compressing package %s: %s", imageName, err.Error())) continue } _, _, err = c.Cache.Put(resultingArtifact) if err != nil { c.context.Error(fmt.Sprintf("Failed storing package %s from cache: %s", imageName, err.Error())) continue } fileName, err := c.Cache.Get(resultingArtifact) if err != nil { c.context.Error(fmt.Sprintf("Failed getting package %s from cache: %s", imageName, err.Error())) continue } resultingArtifact.Path = fileName // Cache is persistent. tempArtifact is not downloaded = true break } if !downloaded { return nil, errors.Wrap(err, "no image available from repositories") } } return resultingArtifact, nil } func (c *DockerClient) DownloadFile(name string) (string, error) { var file *os.File = nil var err error var temp, contentstore string // Files should be in URI/repository: ok := false temp, err = c.context.Config.GetSystem().TempDir("tree") if err != nil { return "", err } for _, uri := range c.RepoData.Urls { file, err = c.context.Config.GetSystem().TempFile("DockerClient") if err != nil { continue } contentstore, err = c.context.Config.GetSystem().TempDir("contentstore") if err != nil { c.context.Warning("Cannot create contentstore", err.Error()) continue } imageName := fmt.Sprintf("%s:%s", uri, docker.StripInvalidStringsFromImage(name)) c.context.Info("Downloading", imageName) info, err := docker.DownloadAndExtractDockerImage(contentstore, imageName, temp, c.auth, c.RepoData.Verify) if err != nil { c.context.Warning(fmt.Sprintf(errImageDownloadMsg, imageName, err.Error())) continue } c.context.Info(fmt.Sprintf("Pulled: %s", info.Target.Digest)) c.context.Info(fmt.Sprintf("Size: %s", units.BytesSize(float64(info.Target.Size)))) c.context.Debug("\nCopying file ", filepath.Join(temp, name), "to", file.Name()) err = fileHelper.CopyFile(filepath.Join(temp, name), file.Name()) if err != nil { continue } ok = true break } if !ok { return "", err } return file.Name(), err }