| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482 |
- package docker
- import (
- "context"
- "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- "os"
- "strings"
- "time"
- "github.com/digitalocean/godo"
- "github.com/docker/distribution/reference"
- "github.com/docker/docker/api/types"
- "github.com/docker/docker/api/types/container"
- "github.com/docker/docker/api/types/filters"
- "github.com/docker/docker/api/types/network"
- "github.com/docker/docker/api/types/volume"
- "github.com/docker/docker/client"
- "github.com/moby/moby/pkg/jsonmessage"
- "github.com/moby/term"
- )
- // Agent is a Docker client for performing operations that interact
- // with the Docker engine over REST
- type Agent struct {
- *client.Client
- authGetter *AuthGetter
- ctx context.Context
- label string
- }
- // CreateLocalVolumeIfNotExist creates a volume using driver type "local" with the
- // given name if it does not exist. If the volume does exist but does not contain
- // the required label (a.label), an error is thrown.
- func (a *Agent) CreateLocalVolumeIfNotExist(name string) (*types.Volume, error) {
- volListBody, err := a.VolumeList(a.ctx, filters.Args{})
- if err != nil {
- return nil, a.handleDockerClientErr(err, "Could not list volumes")
- }
- for _, _vol := range volListBody.Volumes {
- if contains, ok := _vol.Labels[a.label]; ok && contains == "true" && _vol.Name == name {
- return _vol, nil
- } else if !ok && _vol.Name == name {
- return nil, fmt.Errorf("volume conflict for %s: please remove existing volume and try again", name)
- }
- }
- return a.CreateLocalVolume(name)
- }
- // CreateLocalVolume creates a volume using driver type "local" with no
- // configured options. The equivalent of:
- //
- // docker volume create --driver local [name]
- func (a *Agent) CreateLocalVolume(name string) (*types.Volume, error) {
- labels := make(map[string]string)
- labels[a.label] = "true"
- opts := volume.VolumeCreateBody{
- Name: name,
- Driver: "local",
- Labels: labels,
- }
- vol, err := a.VolumeCreate(a.ctx, opts)
- if err != nil {
- return nil, a.handleDockerClientErr(err, "Could not create volume "+name)
- }
- return &vol, nil
- }
- // RemoveLocalVolume removes a volume by name
- func (a *Agent) RemoveLocalVolume(name string) error {
- return a.VolumeRemove(a.ctx, name, true)
- }
- // CreateBridgeNetworkIfNotExist creates a volume using driver type "local" with the
- // given name if it does not exist. If the volume does exist but does not contain
- // the required label (a.label), an error is thrown.
- func (a *Agent) CreateBridgeNetworkIfNotExist(name string) (id string, err error) {
- networks, err := a.NetworkList(a.ctx, types.NetworkListOptions{})
- if err != nil {
- return "", a.handleDockerClientErr(err, "Could not list volumes")
- }
- for _, net := range networks {
- if contains, ok := net.Labels[a.label]; ok && contains == "true" && net.Name == name {
- return net.ID, nil
- } else if !ok && net.Name == name {
- return "", fmt.Errorf("network conflict for %s: please remove existing network and try again", name)
- }
- }
- return a.CreateBridgeNetwork(name)
- }
- // CreateBridgeNetwork creates a volume using the default driver type (bridge)
- // with the CLI label attached
- func (a *Agent) CreateBridgeNetwork(name string) (id string, err error) {
- labels := make(map[string]string)
- labels[a.label] = "true"
- opts := types.NetworkCreate{
- Labels: labels,
- Attachable: true,
- }
- net, err := a.NetworkCreate(a.ctx, name, opts)
- if err != nil {
- return "", a.handleDockerClientErr(err, "Could not create network "+name)
- }
- return net.ID, nil
- }
- // ConnectContainerToNetwork attaches a container to a specified network
- func (a *Agent) ConnectContainerToNetwork(networkID, containerID, containerName string) error {
- // check if the container is connected already
- net, err := a.NetworkInspect(a.ctx, networkID, types.NetworkInspectOptions{})
- if err != nil {
- return a.handleDockerClientErr(err, "Could not inspect network"+networkID)
- }
- for _, cont := range net.Containers {
- // if container is connected, just return
- if cont.Name == containerName {
- return nil
- }
- }
- return a.NetworkConnect(a.ctx, networkID, containerID, &network.EndpointSettings{})
- }
- func (a *Agent) TagImage(old, new string) error {
- return a.ImageTag(a.ctx, old, new)
- }
- // PullImageEvent represents a response from the Docker API with an image pull event
- type PullImageEvent struct {
- Status string `json:"status"`
- Error string `json:"error"`
- Progress string `json:"progress"`
- ProgressDetail struct {
- Current int `json:"current"`
- Total int `json:"total"`
- } `json:"progressDetail"`
- }
- var PullImageErrNotFound = fmt.Errorf("Requested image not found")
- var PullImageErrUnauthorized = fmt.Errorf("Could not pull image: unauthorized")
- func getRegistryRepositoryPair(imageRepo string) ([]string, error) {
- named, err := reference.ParseNamed(imageRepo)
- if err != nil {
- return nil, err
- }
- path := reference.Path(named)
- return strings.SplitN(path, "/", 2), nil
- }
- // CheckIfImageExists checks if the image exists in the registry
- func (a *Agent) CheckIfImageExists(imageRepo, imageTag string) bool {
- registryToken, err := a.getContainerRegistryToken(imageRepo)
- if err != nil {
- return false
- }
- ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
- defer cancel()
- if strings.Contains(imageRepo, "gcr.io") {
- gcrRegRepo, err := getRegistryRepositoryPair(imageRepo)
- if err != nil {
- return false
- }
- named, err := reference.ParseNamed(imageRepo)
- if err != nil {
- return false
- }
- req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf(
- "https://%s/v2/%s/%s/tags/list", reference.Domain(named), gcrRegRepo[0], gcrRegRepo[1],
- ), nil)
- if err != nil {
- return false
- }
- req.Header.Add("Content-Type", "application/json")
- req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", registryToken))
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return false
- }
- defer resp.Body.Close()
- var tags struct {
- Tags []string `json:"tags,omitempty"`
- }
- err = json.NewDecoder(resp.Body).Decode(&tags)
- if err != nil {
- return false
- }
- for _, tag := range tags.Tags {
- if tag == imageTag {
- return true
- }
- }
- return false
- } else if strings.Contains(imageRepo, "registry.digitalocean.com") {
- doRegRepo, err := getRegistryRepositoryPair(imageRepo)
- if err != nil {
- return false
- }
- doClient := godo.NewFromToken(registryToken)
- manifests, _, err := doClient.Registry.ListRepositoryManifests(
- ctx, doRegRepo[0], doRegRepo[1], &godo.ListOptions{},
- )
- if err != nil {
- return false
- }
- for _, manifest := range manifests {
- for _, tag := range manifest.Tags {
- if tag == imageTag {
- return true
- }
- }
- }
- return false
- }
- image := imageRepo + ":" + imageTag
- encodedRegistryAuth, err := a.getEncodedRegistryAuth(image)
- if err != nil {
- return false
- }
- _, err = a.DistributionInspect(context.Background(), image, encodedRegistryAuth)
- if err == nil {
- return true
- } else if strings.Contains(err.Error(), "image not found") ||
- strings.Contains(err.Error(), "does not exist in the registry") {
- return false
- }
- return false
- }
- // PullImage pulls an image specified by the image string
- func (a *Agent) PullImage(image string) error {
- opts, err := a.getPullOptions(image)
- if err != nil {
- return err
- }
- // pull the specified image
- out, err := a.ImagePull(a.ctx, image, opts)
- if err != nil {
- if client.IsErrNotFound(err) ||
- (strings.Contains(image, "gcr.io") && strings.Contains(err.Error(), "or it may not exist")) {
- return PullImageErrNotFound
- } else if client.IsErrUnauthorized(err) {
- return PullImageErrUnauthorized
- } else {
- return a.handleDockerClientErr(err, "Could not pull image "+image)
- }
- }
- defer out.Close()
- termFd, isTerm := term.GetFdInfo(os.Stderr)
- return jsonmessage.DisplayJSONMessagesStream(out, os.Stderr, termFd, isTerm, nil)
- }
- // PushImage pushes an image specified by the image string
- func (a *Agent) PushImage(image string) error {
- opts, err := a.getPushOptions(image)
- if err != nil {
- return err
- }
- out, err := a.ImagePush(
- context.Background(),
- image,
- opts,
- )
- if out != nil {
- defer out.Close()
- }
- if err != nil {
- return err
- }
- termFd, isTerm := term.GetFdInfo(os.Stderr)
- err = jsonmessage.DisplayJSONMessagesStream(out, os.Stderr, termFd, isTerm, nil)
- if err != nil {
- return err
- }
- return nil
- }
- func (a *Agent) getPullOptions(image string) (types.ImagePullOptions, error) {
- // check if agent has an auth getter; otherwise, assume public usage
- if a.authGetter == nil {
- return types.ImagePullOptions{}, nil
- }
- authConfigEncoded, err := a.getEncodedRegistryAuth(image)
- if err != nil {
- return types.ImagePullOptions{}, err
- }
- fmt.Println(authConfigEncoded)
- return types.ImagePullOptions{
- RegistryAuth: authConfigEncoded,
- Platform: "linux/amd64",
- }, nil
- }
- func (a *Agent) getContainerRegistryToken(image string) (string, error) {
- serverURL, err := GetServerURLFromTag(image)
- if err != nil {
- return "", err
- }
- _, secret, err := a.authGetter.GetCredentials(serverURL)
- if err != nil {
- return "", err
- }
- return secret, nil
- }
- func (a *Agent) getEncodedRegistryAuth(image string) (string, error) {
- // get using server url
- serverURL, err := GetServerURLFromTag(image)
- if err != nil {
- return "", err
- }
- user, secret, err := a.authGetter.GetCredentials(serverURL)
- if err != nil {
- return "", err
- }
- serverAddress := serverURL
- if !strings.Contains(serverURL, "https://") {
- serverAddress = fmt.Sprintf("https://%s", serverURL)
- }
- authConfig := types.AuthConfig{
- Username: user,
- Password: secret,
- ServerAddress: serverAddress,
- }
- authConfigBytes, err := json.Marshal(authConfig)
- if err != nil {
- return "", fmt.Errorf("unable to marshal docker auth config: %w", err)
- }
- return base64.URLEncoding.EncodeToString(authConfigBytes), nil
- }
- func (a *Agent) getPushOptions(image string) (types.ImagePushOptions, error) {
- pullOpts, err := a.getPullOptions(image)
- return types.ImagePushOptions(pullOpts), err
- }
- func GetServerURLFromTag(image string) (string, error) {
- named, err := reference.ParseNormalizedNamed(image)
- if err != nil {
- return "", err
- }
- domain := reference.Domain(named)
- if domain == "" {
- // if domain name is empty, use index.docker.io/v1
- return "index.docker.io/v1", nil
- } else if matches := ecrPattern.FindStringSubmatch(image); len(matches) >= 3 {
- // if this matches ECR, just use the domain name
- return domain, nil
- } else if strings.Contains(image, "gcr.io") || strings.Contains(image, "registry.digitalocean.com") {
- // if this matches GCR or DOCR, use the first path component
- return fmt.Sprintf("%s/%s", domain, strings.Split(reference.Path(named), "/")[0]), nil
- }
- // otherwise, best-guess is to get components of path that aren't the image name
- pathParts := strings.Split(reference.Path(named), "/")
- nonImagePath := ""
- if len(pathParts) > 1 {
- nonImagePath = strings.Join(pathParts[0:len(pathParts)-1], "/")
- }
- if err != nil {
- return "", err
- }
- if domain == "docker.io" {
- domain = "index.docker.io"
- }
- return fmt.Sprintf("%s/%s", domain, nonImagePath), nil
- }
- // WaitForContainerStop waits until a container has stopped to exit
- func (a *Agent) WaitForContainerStop(id string) error {
- // wait for container to stop before exit
- statusCh, errCh := a.ContainerWait(a.ctx, id, container.WaitConditionNotRunning)
- select {
- case err := <-errCh:
- if err != nil {
- return a.handleDockerClientErr(err, "Error waiting for stopped container")
- }
- case <-statusCh:
- }
- return nil
- }
- // WaitForContainerHealthy waits until a container is returning a healthy status. Streak
- // is the maximum number of failures in a row, while timeout is the length of time between
- // checks.
- func (a *Agent) WaitForContainerHealthy(id string, streak int) error {
- for {
- cont, err := a.ContainerInspect(a.ctx, id)
- if err != nil {
- return a.handleDockerClientErr(err, "Error waiting for stopped container")
- }
- health := cont.State.Health
- if health == nil || health.Status == "healthy" {
- return nil
- } else if health.FailingStreak >= streak {
- break
- }
- time.Sleep(time.Second)
- }
- return errors.New("container not healthy")
- }
- // ------------------------- AGENT HELPER FUNCTIONS ------------------------- //
- func (a *Agent) handleDockerClientErr(err error, errPrefix string) error {
- if strings.Contains(err.Error(), "Cannot connect to the Docker daemon") {
- return fmt.Errorf("The Docker daemon must be running in order to start Porter: connection to %s failed", a.DaemonHost())
- }
- return fmt.Errorf("%s:%s", errPrefix, err.Error())
- }
|