github.com/0xPolygon/supernets2-node@v0.0.0-20230711153321-2fe574524eaa/test/scripts/cmd/dependencies/images.go (about)

     1  package dependencies
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"os/exec"
    10  	"strings"
    11  
    12  	"github.com/0xPolygon/supernets2-node/log"
    13  	"github.com/spf13/afero"
    14  	"gopkg.in/yaml.v3"
    15  )
    16  
    17  // ImagesConfig is the configuration for the images updater.
    18  type ImagesConfig struct {
    19  	Names          []string
    20  	TargetFilePath string
    21  }
    22  
    23  type imageUpdater struct {
    24  	fs afero.Fs
    25  
    26  	targetFilePath string
    27  
    28  	dockerUsername string
    29  	dockerPassword string
    30  
    31  	images []string
    32  }
    33  
    34  func newImageUpdater(images []string, targetFilePath string) *imageUpdater {
    35  	return &imageUpdater{
    36  		fs: afero.NewOsFs(),
    37  
    38  		targetFilePath: targetFilePath,
    39  
    40  		dockerUsername: os.Getenv("DOCKERHUB_USERNAME"),
    41  		dockerPassword: os.Getenv("DOCKERHUB_PASSWORD"),
    42  
    43  		images: images,
    44  	}
    45  }
    46  
    47  func (iu *imageUpdater) update() error {
    48  	for _, image := range iu.images {
    49  		if err := iu.updateImage(image); err != nil {
    50  			return err
    51  		}
    52  	}
    53  	return nil
    54  }
    55  
    56  func (iu *imageUpdater) updateImage(imageName string) error {
    57  	currentDigest, err := iu.readCurrentDigest(imageName)
    58  	if err != nil {
    59  		return err
    60  	}
    61  
    62  	remoteDigest, err := iu.readRemoteDigest(imageName)
    63  	if err != nil {
    64  		return err
    65  	}
    66  
    67  	if currentDigest != remoteDigest {
    68  		if err := iu.updateDigest(imageName, currentDigest, remoteDigest); err != nil {
    69  			return err
    70  		}
    71  	}
    72  	return nil
    73  }
    74  
    75  func (iu *imageUpdater) readCurrentDigest(imageName string) (string, error) {
    76  	data, err := afero.ReadFile(iu.fs, getTargetPath(iu.targetFilePath))
    77  	if err != nil {
    78  		return "", err
    79  	}
    80  
    81  	content := struct {
    82  		Version  string
    83  		Services map[string]struct {
    84  			Image string
    85  		}
    86  	}{}
    87  	err = yaml.Unmarshal(data, &content)
    88  	if err != nil {
    89  		return "", err
    90  	}
    91  	for _, c := range content.Services {
    92  		if c.Image == "" {
    93  			continue
    94  		}
    95  		items := strings.Split(c.Image, "@")
    96  		const requiredItems = 2
    97  		if len(items) < requiredItems {
    98  			continue
    99  		}
   100  		if strings.HasPrefix(items[0], imageName) {
   101  			log.Infof("Current digest of %q is %q", imageName, items[1])
   102  			return items[1], nil
   103  		}
   104  	}
   105  	return "", fmt.Errorf("image %q not found in %q", imageName, iu.targetFilePath)
   106  }
   107  
   108  func (iu *imageUpdater) readRemoteDigest(imageName string) (string, error) {
   109  	err := iu.dockerLogin()
   110  	if err != nil {
   111  		return "", err
   112  	}
   113  	return iu.readLatestTag(imageName)
   114  }
   115  
   116  func (iu *imageUpdater) updateDigest(imageName, currentDigest, remoteDigest string) error {
   117  	targetFilePath := getTargetPath(iu.targetFilePath)
   118  	log.Infof("Updating %q...", targetFilePath)
   119  	oldContent, err := afero.ReadFile(iu.fs, targetFilePath)
   120  	if err != nil {
   121  		return err
   122  	}
   123  	oldImageField := fmt.Sprintf("%s@%s", imageName, currentDigest)
   124  	newImageField := fmt.Sprintf("%s@%s", imageName, remoteDigest)
   125  
   126  	newContent := strings.ReplaceAll(string(oldContent), oldImageField, newImageField)
   127  
   128  	return afero.WriteFile(iu.fs, targetFilePath, []byte(newContent), 0664) //nolint:gomnd
   129  }
   130  
   131  func (iu *imageUpdater) dockerLogin() error {
   132  	c := exec.Command("docker", "login", "--username", iu.dockerUsername, "--password-stdin") // #nosec G204
   133  	stdin, err := c.StdinPipe()
   134  	if err != nil {
   135  		return err
   136  	}
   137  	passReader := strings.NewReader(iu.dockerPassword)
   138  	_, err = io.Copy(stdin, passReader)
   139  	if err != nil {
   140  		return err
   141  	}
   142  	err = stdin.Close()
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	return c.Run()
   148  }
   149  
   150  type result struct {
   151  	RepoDigests []string
   152  }
   153  
   154  func (iu *imageUpdater) readLatestTag(imageName string) (string, error) {
   155  	c := exec.Command("docker", "pull", imageName) // #nosec G204
   156  	err := c.Run()
   157  	if err != nil {
   158  		return "", err
   159  	}
   160  
   161  	c = exec.Command("docker", "inspect", imageName) // #nosec G204
   162  	output, err := c.CombinedOutput()
   163  	if err != nil {
   164  		return "", err
   165  	}
   166  
   167  	reader := bytes.NewReader(output)
   168  	decoder := json.NewDecoder(reader)
   169  
   170  	r := struct {
   171  		Results []result
   172  	}{}
   173  	err = decoder.Decode(&r.Results)
   174  	if err != nil {
   175  		return "", err
   176  	}
   177  	items := strings.Split(r.Results[0].RepoDigests[0], "@")
   178  	const requiredItems = 2
   179  	if len(items) < requiredItems {
   180  		return "", fmt.Errorf("Returned image does not include digest %q", r.Results[0].RepoDigests[0])
   181  	}
   182  	log.Infof("Remote digest of %q is %q", imageName, items[1])
   183  	return items[1], nil
   184  }