// +build remoteclient package adapter import ( "bufio" "context" "encoding/json" "fmt" "io" "io/ioutil" "os" "strings" "text/template" "time" v1 "k8s.io/api/core/v1" "github.com/containers/buildah/imagebuildah" "github.com/containers/image/docker/reference" "github.com/containers/image/types" "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/remoteclientconfig" "github.com/containers/libpod/cmd/podman/varlink" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/events" "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/utils" "github.com/containers/storage/pkg/archive" "github.com/opencontainers/go-digest" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/varlink/go/varlink" ) // ImageRuntime is wrapper for image runtime type RemoteImageRuntime struct{} // RemoteRuntime describes a wrapper runtime struct type RemoteRuntime struct { Conn *varlink.Connection Remote bool cmd cliconfig.MainFlags config io.Reader } // LocalRuntime describes a typical libpod runtime type LocalRuntime struct { *RemoteRuntime } // GetRuntime returns a LocalRuntime struct with the actual runtime embedded in it func GetRuntime(ctx context.Context, c *cliconfig.PodmanCommand) (*LocalRuntime, error) { var ( customConfig bool err error f *os.File ) runtime := RemoteRuntime{ Remote: true, cmd: c.GlobalFlags, } configPath := remoteclientconfig.GetConfigFilePath() if len(c.GlobalFlags.RemoteConfigFilePath) > 0 { configPath = c.GlobalFlags.RemoteConfigFilePath customConfig = true } f, err = os.Open(configPath) if err != nil { // If user does not explicitly provide a configuration file path and we cannot // find a default, no error should occur. if os.IsNotExist(err) && !customConfig { logrus.Debugf("unable to load configuration file at %s", configPath) runtime.config = nil } else { return nil, errors.Wrapf(err, "unable to load configuration file at %s", configPath) } } else { // create the io reader for the remote client runtime.config = bufio.NewReader(f) } conn, err := runtime.Connect() if err != nil { return nil, err } runtime.Conn = conn return &LocalRuntime{ &runtime, }, nil } // Shutdown is a bogus wrapper for compat with the libpod runtime func (r RemoteRuntime) Shutdown(force bool) error { return nil } // ContainerImage type ContainerImage struct { remoteImage } type remoteImage struct { ID string Labels map[string]string RepoTags []string RepoDigests []string Parent string Size int64 Created time.Time InputName string Names []string Digest digest.Digest isParent bool Runtime *LocalRuntime TopLayer string } // Container ... type Container struct { remoteContainer } // remoteContainer .... type remoteContainer struct { Runtime *LocalRuntime config *libpod.ContainerConfig state *libpod.ContainerState } // Pod ... type Pod struct { remotepod } type remotepod struct { config *libpod.PodConfig state *libpod.PodInspectState containers []libpod.PodContainerInfo Runtime *LocalRuntime } type VolumeFilter func(*Volume) bool // Volume is embed for libpod volumes type Volume struct { remoteVolume } type remoteVolume struct { Runtime *LocalRuntime config *libpod.VolumeConfig } // GetImages returns a slice of containerimages over a varlink connection func (r *LocalRuntime) GetImages() ([]*ContainerImage, error) { var newImages []*ContainerImage images, err := iopodman.ListImages().Call(r.Conn) if err != nil { return nil, err } for _, i := range images { name := i.Id if len(i.RepoTags) > 1 { name = i.RepoTags[0] } newImage, err := imageInListToContainerImage(i, name, r) if err != nil { return nil, err } newImages = append(newImages, newImage) } return newImages, nil } func imageInListToContainerImage(i iopodman.Image, name string, runtime *LocalRuntime) (*ContainerImage, error) { created, err := time.ParseInLocation(time.RFC3339, i.Created, time.UTC) if err != nil { return nil, err } ri := remoteImage{ InputName: name, ID: i.Id, Digest: digest.Digest(i.Digest), Labels: i.Labels, RepoTags: i.RepoTags, RepoDigests: i.RepoTags, Parent: i.ParentId, Size: i.Size, Created: created, Names: i.RepoTags, isParent: i.IsParent, Runtime: runtime, TopLayer: i.TopLayer, } return &ContainerImage{ri}, nil } // NewImageFromLocal returns a container image representation of a image over varlink func (r *LocalRuntime) NewImageFromLocal(name string) (*ContainerImage, error) { img, err := iopodman.GetImage().Call(r.Conn, name) if err != nil { return nil, err } return imageInListToContainerImage(img, name, r) } // LoadFromArchiveReference creates an image from a local archive func (r *LocalRuntime) LoadFromArchiveReference(ctx context.Context, srcRef types.ImageReference, signaturePolicyPath string, writer io.Writer) ([]*ContainerImage, error) { var iid string reply, err := iopodman.PullImage().Send(r.Conn, varlink.More, srcRef.DockerReference().String()) if err != nil { return nil, err } for { responses, flags, err := reply() if err != nil { return nil, err } for _, line := range responses.Logs { fmt.Print(line) } iid = responses.Id if flags&varlink.Continues == 0 { break } } newImage, err := r.NewImageFromLocal(iid) if err != nil { return nil, err } return []*ContainerImage{newImage}, nil } // New calls into local storage to look for an image in local storage or to pull it func (r *LocalRuntime) New(ctx context.Context, name, signaturePolicyPath, authfile string, writer io.Writer, dockeroptions *image.DockerRegistryOptions, signingoptions image.SigningOptions, forcePull bool, label *string) (*ContainerImage, error) { var iid string if label != nil { return nil, errors.New("the remote client function does not support checking a remote image for a label") } reply, err := iopodman.PullImage().Send(r.Conn, varlink.More, name) if err != nil { return nil, err } for { responses, flags, err := reply() if err != nil { return nil, err } for _, line := range responses.Logs { fmt.Print(line) } iid = responses.Id if flags&varlink.Continues == 0 { break } } newImage, err := r.NewImageFromLocal(iid) if err != nil { return nil, err } return newImage, nil } // IsParent goes through the layers in the store and checks if i.TopLayer is // the parent of any other layer in store. Double check that image with that // layer exists as well. func (ci *ContainerImage) IsParent(context.Context) (bool, error) { return ci.remoteImage.isParent, nil } // ID returns the image ID as a string func (ci *ContainerImage) ID() string { return ci.remoteImage.ID } // Names returns a string array of names associated with the image func (ci *ContainerImage) Names() []string { return ci.remoteImage.Names } // Created returns the time the image was created func (ci *ContainerImage) Created() time.Time { return ci.remoteImage.Created } // Size returns the size of the image func (ci *ContainerImage) Size(ctx context.Context) (*uint64, error) { usize := uint64(ci.remoteImage.Size) return &usize, nil } // Digest returns the image's digest func (ci *ContainerImage) Digest() digest.Digest { return ci.remoteImage.Digest } // Labels returns a map of the image's labels func (ci *ContainerImage) Labels(ctx context.Context) (map[string]string, error) { return ci.remoteImage.Labels, nil } // Dangling returns a bool if the image is "dangling" func (ci *ContainerImage) Dangling() bool { return len(ci.Names()) == 0 } // TopLayer returns an images top layer as a string func (ci *ContainerImage) TopLayer() string { return ci.remoteImage.TopLayer } // TagImage ... func (ci *ContainerImage) TagImage(tag string) error { _, err := iopodman.TagImage().Call(ci.Runtime.Conn, ci.ID(), tag) return err } // RemoveImage calls varlink to remove an image func (r *LocalRuntime) RemoveImage(ctx context.Context, img *ContainerImage, force bool) (string, error) { return iopodman.RemoveImage().Call(r.Conn, img.InputName, force) } // History returns the history of an image and its layers func (ci *ContainerImage) History(ctx context.Context) ([]*image.History, error) { var imageHistories []*image.History reply, err := iopodman.HistoryImage().Call(ci.Runtime.Conn, ci.InputName) if err != nil { return nil, err } for _, h := range reply { created, err := time.ParseInLocation(time.RFC3339, h.Created, time.UTC) if err != nil { return nil, err } ih := image.History{ ID: h.Id, Created: &created, CreatedBy: h.CreatedBy, Size: h.Size, Comment: h.Comment, } imageHistories = append(imageHistories, &ih) } return imageHistories, nil } // PruneImages is the wrapper call for a remote-client to prune images func (r *LocalRuntime) PruneImages(ctx context.Context, all bool) ([]string, error) { return iopodman.ImagesPrune().Call(r.Conn, all) } // Export is a wrapper to container export to a tarfile func (r *LocalRuntime) Export(name string, path string) error { tempPath, err := iopodman.ExportContainer().Call(r.Conn, name, "") if err != nil { return err } return r.GetFileFromRemoteHost(tempPath, path, true) } func (r *LocalRuntime) GetFileFromRemoteHost(remoteFilePath, outputPath string, delete bool) error { outputFile, err := os.Create(outputPath) if err != nil { return err } defer outputFile.Close() writer := bufio.NewWriter(outputFile) defer writer.Flush() reply, err := iopodman.ReceiveFile().Send(r.Conn, varlink.Upgrade, remoteFilePath, delete) if err != nil { return err } length, _, err := reply() if err != nil { return errors.Wrap(err, "unable to get file length for transfer") } reader := r.Conn.Reader if _, err := io.CopyN(writer, reader, length); err != nil { return errors.Wrap(err, "file transer failed") } return nil } // Import implements the remote calls required to import a container image to the store func (r *LocalRuntime) Import(ctx context.Context, source, reference string, changes []string, history string, quiet bool) (string, error) { // First we send the file to the host tempFile, err := r.SendFileOverVarlink(source) if err != nil { return "", err } return iopodman.ImportImage().Call(r.Conn, strings.TrimRight(tempFile, ":"), reference, history, changes, true) } func (r *LocalRuntime) Build(ctx context.Context, c *cliconfig.BuildValues, options imagebuildah.BuildOptions, dockerfiles []string) error { buildOptions := iopodman.BuildOptions{ AddHosts: options.CommonBuildOpts.AddHost, CgroupParent: options.CommonBuildOpts.CgroupParent, CpuPeriod: int64(options.CommonBuildOpts.CPUPeriod), CpuQuota: options.CommonBuildOpts.CPUQuota, CpuShares: int64(options.CommonBuildOpts.CPUShares), CpusetCpus: options.CommonBuildOpts.CPUSetMems, CpusetMems: options.CommonBuildOpts.CPUSetMems, Memory: options.CommonBuildOpts.Memory, MemorySwap: options.CommonBuildOpts.MemorySwap, ShmSize: options.CommonBuildOpts.ShmSize, Ulimit: options.CommonBuildOpts.Ulimit, Volume: options.CommonBuildOpts.Volumes, } buildinfo := iopodman.BuildInfo{ AdditionalTags: options.AdditionalTags, Annotations: options.Annotations, BuildArgs: options.Args, BuildOptions: buildOptions, CniConfigDir: options.CNIConfigDir, CniPluginDir: options.CNIPluginPath, Compression: string(options.Compression), DefaultsMountFilePath: options.DefaultMountsFilePath, Dockerfiles: dockerfiles, // Err: string(options.Err), ForceRmIntermediateCtrs: options.ForceRmIntermediateCtrs, Iidfile: options.IIDFile, Label: options.Labels, Layers: options.Layers, Nocache: options.NoCache, // Out: Output: options.Output, OutputFormat: options.OutputFormat, PullPolicy: options.PullPolicy.String(), Quiet: options.Quiet, RemoteIntermediateCtrs: options.RemoveIntermediateCtrs, // ReportWriter: RuntimeArgs: options.RuntimeArgs, Squash: options.Squash, } // tar the file outputFile, err := ioutil.TempFile("", "varlink_tar_send") if err != nil { return err } defer outputFile.Close() defer os.Remove(outputFile.Name()) // Create the tarball of the context dir to a tempfile if err := utils.TarToFilesystem(options.ContextDirectory, outputFile); err != nil { return err } // Send the context dir tarball over varlink. tempFile, err := r.SendFileOverVarlink(outputFile.Name()) if err != nil { return err } buildinfo.ContextDir = tempFile reply, err := iopodman.BuildImage().Send(r.Conn, varlink.More, buildinfo) if err != nil { return err } for { responses, flags, err := reply() if err != nil { return err } for _, line := range responses.Logs { fmt.Print(line) } if flags&varlink.Continues == 0 { break } } return err } // SendFileOverVarlink sends a file over varlink in an upgraded connection func (r *LocalRuntime) SendFileOverVarlink(source string) (string, error) { fs, err := os.Open(source) if err != nil { return "", err } fileInfo, err := fs.Stat() if err != nil { return "", err } logrus.Debugf("sending %s over varlink connection", source) reply, err := iopodman.SendFile().Send(r.Conn, varlink.Upgrade, "", int64(fileInfo.Size())) if err != nil { return "", err } _, _, err = reply() if err != nil { return "", err } reader := bufio.NewReader(fs) _, err = reader.WriteTo(r.Conn.Writer) if err != nil { return "", err } logrus.Debugf("file transfer complete for %s", source) r.Conn.Writer.Flush() // All was sent, wait for the ACK from the server tempFile, err := r.Conn.Reader.ReadString(':') if err != nil { return "", err } // r.Conn is kaput at this point due to the upgrade if err := r.RemoteRuntime.RefreshConnection(); err != nil { return "", err } return strings.Replace(tempFile, ":", "", -1), nil } // GetAllVolumes retrieves all the volumes func (r *LocalRuntime) GetAllVolumes() ([]*libpod.Volume, error) { return nil, libpod.ErrNotImplemented } // RemoveVolume removes a volumes func (r *LocalRuntime) RemoveVolume(ctx context.Context, v *libpod.Volume, force, prune bool) error { return libpod.ErrNotImplemented } // GetContainers retrieves all containers from the state // Filters can be provided which will determine what containers are included in // the output. Multiple filters are handled by ANDing their output, so only // containers matching all filters are returned func (r *LocalRuntime) GetContainers(filters ...libpod.ContainerFilter) ([]*libpod.Container, error) { return nil, libpod.ErrNotImplemented } // RemoveContainer removes the given container // If force is specified, the container will be stopped first // Otherwise, RemoveContainer will return an error if the container is running func (r *LocalRuntime) RemoveContainer(ctx context.Context, c *libpod.Container, force, volumes bool) error { return libpod.ErrNotImplemented } // CreateVolume creates a volume over a varlink connection for the remote client func (r *LocalRuntime) CreateVolume(ctx context.Context, c *cliconfig.VolumeCreateValues, labels, opts map[string]string) (string, error) { cvOpts := iopodman.VolumeCreateOpts{ Options: opts, Labels: labels, } if len(c.InputArgs) > 0 { cvOpts.VolumeName = c.InputArgs[0] } if c.Flag("driver").Changed { cvOpts.Driver = c.Driver } return iopodman.VolumeCreate().Call(r.Conn, cvOpts) } // RemoveVolumes removes volumes over a varlink connection for the remote client func (r *LocalRuntime) RemoveVolumes(ctx context.Context, c *cliconfig.VolumeRmValues) ([]string, error) { rmOpts := iopodman.VolumeRemoveOpts{ All: c.All, Force: c.Force, Volumes: c.InputArgs, } return iopodman.VolumeRemove().Call(r.Conn, rmOpts) } func (r *LocalRuntime) Push(ctx context.Context, srcName, destination, manifestMIMEType, authfile, signaturePolicyPath string, writer io.Writer, forceCompress bool, signingOptions image.SigningOptions, dockerRegistryOptions *image.DockerRegistryOptions, additionalDockerArchiveTags []reference.NamedTagged) error { reply, err := iopodman.PushImage().Send(r.Conn, varlink.More, srcName, destination, forceCompress, manifestMIMEType, signingOptions.RemoveSignatures, signingOptions.SignBy) if err != nil { return err } for { responses, flags, err := reply() if err != nil { return err } for _, line := range responses.Logs { fmt.Print(line) } if flags&varlink.Continues == 0 { break } } return err } // InspectVolumes returns a slice of volumes based on an arg list or --all func (r *LocalRuntime) InspectVolumes(ctx context.Context, c *cliconfig.VolumeInspectValues) ([]*Volume, error) { reply, err := iopodman.GetVolumes().Call(r.Conn, c.InputArgs, c.All) if err != nil { return nil, err } return varlinkVolumeToVolume(r, reply), nil } // Volumes returns a slice of adapter.volumes based on information about libpod // volumes over a varlink connection func (r *LocalRuntime) Volumes(ctx context.Context) ([]*Volume, error) { reply, err := iopodman.GetVolumes().Call(r.Conn, []string{}, true) if err != nil { return nil, err } return varlinkVolumeToVolume(r, reply), nil } func varlinkVolumeToVolume(r *LocalRuntime, volumes []iopodman.Volume) []*Volume { var vols []*Volume for _, v := range volumes { volumeConfig := libpod.VolumeConfig{ Name: v.Name, Labels: v.Labels, MountPoint: v.MountPoint, Driver: v.Driver, Options: v.Options, Scope: v.Scope, } n := remoteVolume{ Runtime: r, config: &volumeConfig, } newVol := Volume{ n, } vols = append(vols, &newVol) } return vols } // PruneVolumes removes all unused volumes from the remote system func (r *LocalRuntime) PruneVolumes(ctx context.Context) ([]string, []error) { var errs []error prunedNames, prunedErrors, err := iopodman.VolumesPrune().Call(r.Conn) if err != nil { return []string{}, []error{err} } // We need to transform the string results of the error into actual error types for _, e := range prunedErrors { errs = append(errs, errors.New(e)) } return prunedNames, errs } // SaveImage is a wrapper function for saving an image to the local filesystem func (r *LocalRuntime) SaveImage(ctx context.Context, c *cliconfig.SaveValues) error { source := c.InputArgs[0] additionalTags := c.InputArgs[1:] options := iopodman.ImageSaveOptions{ Name: source, Format: c.Format, Output: c.Output, MoreTags: additionalTags, Quiet: c.Quiet, Compress: c.Compress, } reply, err := iopodman.ImageSave().Send(r.Conn, varlink.More, options) if err != nil { return err } var fetchfile string for { responses, flags, err := reply() if err != nil { return err } if len(responses.Id) > 0 { fetchfile = responses.Id } for _, line := range responses.Logs { fmt.Print(line) } if flags&varlink.Continues == 0 { break } } if err != nil { return err } outputToDir := false outfile := c.Output var outputFile *os.File // If the result is supposed to be a dir, then we need to put the tarfile // from the host in a temporary file if options.Format != "oci-archive" && options.Format != "docker-archive" { outputToDir = true outputFile, err = ioutil.TempFile("", "saveimage_tempfile") if err != nil { return err } outfile = outputFile.Name() defer outputFile.Close() defer os.Remove(outputFile.Name()) } // We now need to fetch the tarball result back to the more system if err := r.GetFileFromRemoteHost(fetchfile, outfile, true); err != nil { return err } // If the result is a tarball, we're done // If it is a dir, we need to untar the temporary file into the dir if outputToDir { if err := utils.UntarToFileSystem(c.Output, outputFile, &archive.TarOptions{}); err != nil { return err } } return nil } // LoadImage loads a container image from a remote client's filesystem func (r *LocalRuntime) LoadImage(ctx context.Context, name string, cli *cliconfig.LoadValues) (string, error) { var names string remoteTempFile, err := r.SendFileOverVarlink(cli.Input) if err != nil { return "", nil } more := varlink.More if cli.Quiet { more = 0 } reply, err := iopodman.LoadImage().Send(r.Conn, uint64(more), name, remoteTempFile, cli.Quiet, true) if err != nil { return "", err } for { responses, flags, err := reply() if err != nil { logrus.Error(err) return "", err } for _, line := range responses.Logs { fmt.Print(line) } names = responses.Id if flags&varlink.Continues == 0 { break } } return names, nil } // IsImageNotFound checks if the error indicates that no image was found. func IsImageNotFound(err error) bool { if errors.Cause(err) == image.ErrNoSuchImage { return true } switch err.(type) { case *iopodman.ImageNotFound: return true } return false } // HealthCheck executes a container's healthcheck over a varlink connection func (r *LocalRuntime) HealthCheck(c *cliconfig.HealthCheckValues) (libpod.HealthCheckStatus, error) { return -1, libpod.ErrNotImplemented } // Events monitors libpod/podman events over a varlink connection func (r *LocalRuntime) Events(c *cliconfig.EventValues) error { var more uint64 if c.Stream { more = uint64(varlink.More) } reply, err := iopodman.GetEvents().Send(r.Conn, more, c.Filter, c.Since, c.Until) if err != nil { return errors.Wrapf(err, "unable to obtain events") } w := bufio.NewWriter(os.Stdout) tmpl, err := template.New("events").Parse(c.Format) if err != nil { return err } for { returnedEvent, flags, err := reply() if err != nil { // When the error handling is back into podman, we can flip this to a better way to check // for problems. For now, this works. return err } if returnedEvent.Time == "" && returnedEvent.Status == "" && returnedEvent.Type == "" { // We got a blank event return, signals end of stream in certain cases break } eTime, err := time.Parse(time.RFC3339Nano, returnedEvent.Time) if err != nil { return errors.Wrapf(err, "unable to parse time of event %s", returnedEvent.Time) } eType, err := events.StringToType(returnedEvent.Type) if err != nil { return err } eStatus, err := events.StringToStatus(returnedEvent.Status) if err != nil { return err } event := events.Event{ ID: returnedEvent.Id, Image: returnedEvent.Image, Name: returnedEvent.Name, Status: eStatus, Time: eTime, Type: eType, } if len(c.Format) > 0 { if err := tmpl.Execute(w, event); err != nil { return err } } else { if _, err := w.Write([]byte(event.ToHumanReadable())); err != nil { return err } } if _, err := w.Write([]byte("\n")); err != nil { return err } if err := w.Flush(); err != nil { return err } if flags&varlink.Continues == 0 { break } } return nil } // Diff ... func (r *LocalRuntime) Diff(c *cliconfig.DiffValues, to string) ([]archive.Change, error) { var changes []archive.Change reply, err := iopodman.Diff().Call(r.Conn, to) if err != nil { return nil, err } for _, change := range reply { changes = append(changes, archive.Change{Path: change.Path, Kind: stringToChangeType(change.ChangeType)}) } return changes, nil } func stringToChangeType(change string) archive.ChangeType { switch change { case "A": return archive.ChangeAdd case "D": return archive.ChangeDelete default: logrus.Errorf("'%s' is unknown archive type", change) fallthrough case "C": return archive.ChangeModify } } // GenerateKube creates kubernetes email from containers and pods func (r *LocalRuntime) GenerateKube(c *cliconfig.GenerateKubeValues) (*v1.Pod, *v1.Service, error) { var ( pod v1.Pod service v1.Service ) reply, err := iopodman.GenerateKube().Call(r.Conn, c.InputArgs[0], c.Service) if err != nil { return nil, nil, errors.Wrap(err, "unable to create kubernetes YAML") } if err := json.Unmarshal([]byte(reply.Pod), &pod); err != nil { return nil, nil, err } err = json.Unmarshal([]byte(reply.Service), &service) return &pod, &service, err } // GetContainersByContext looks up containers based on the cli input of all, latest, or a list func (r *LocalRuntime) GetContainersByContext(all bool, latest bool, namesOrIDs []string) ([]*Container, error) { var containers []*Container cids, err := iopodman.GetContainersByContext().Call(r.Conn, all, latest, namesOrIDs) if err != nil { return nil, err } for _, cid := range cids { ctr, err := r.LookupContainer(cid) if err != nil { return nil, err } containers = append(containers, ctr) } return containers, nil } // GetVersion returns version information from service func (r *LocalRuntime) GetVersion() (libpod.Version, error) { version, goVersion, gitCommit, built, osArch, apiVersion, err := iopodman.GetVersion().Call(r.Conn) if err != nil { return libpod.Version{}, errors.Wrapf(err, "Unable to obtain server version information") } var buildTime int64 if built != "" { t, err := time.Parse(time.RFC3339, built) if err != nil { return libpod.Version{}, nil } buildTime = t.Unix() } return libpod.Version{ RemoteAPIVersion: apiVersion, Version: version, GoVersion: goVersion, GitCommit: gitCommit, Built: buildTime, OsArch: osArch, }, nil }