github.com/mcuadros/ascode@v1.3.1/starlark/module/docker/image.go (about)

     1  package docker
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"sort"
    10  	"sync"
    11  
    12  	"github.com/Masterminds/semver/v3"
    13  	"github.com/containers/image/v5/docker"
    14  	"github.com/containers/image/v5/docker/reference"
    15  	"github.com/containers/image/v5/types"
    16  	"go.starlark.net/starlark"
    17  	"go.starlark.net/starlarkstruct"
    18  )
    19  
    20  const (
    21  	// ModuleName defines the expected name for this Module when used
    22  	// in starlark's load() function, eg: load('docker', 'docker')
    23  	ModuleName = "docker"
    24  
    25  	imageFuncName = "image"
    26  	latestTag     = "lastest"
    27  )
    28  
    29  var (
    30  	once         sync.Once
    31  	dockerModule starlark.StringDict
    32  )
    33  
    34  // LoadModule loads the os module.
    35  // It is concurrency-safe and idempotent.
    36  //
    37  //   outline: docker
    38  //     The docker modules allow you to manipulate docker image names.
    39  //     path: docker
    40  func LoadModule() (starlark.StringDict, error) {
    41  	once.Do(func() {
    42  		dockerModule = starlark.StringDict{
    43  			"docker": &starlarkstruct.Module{
    44  				Name: "docker",
    45  				Members: starlark.StringDict{
    46  					imageFuncName: starlark.NewBuiltin(imageFuncName, Image),
    47  				},
    48  			},
    49  		}
    50  	})
    51  
    52  	return dockerModule, nil
    53  }
    54  
    55  type sString = starlark.String
    56  
    57  // image represents a docker container image.
    58  //
    59  //   outline: docker
    60  //     types:
    61  //       Image
    62  //         Represents a docker container image.
    63  //
    64  //         fields:
    65  //           name string
    66  //             Image name. Eg.: `docker.io/library/fedora`
    67  //           domain string
    68  //             Registry domain. Eg.: `docker.io`.
    69  //           path string
    70  //             Repository path. Eg.: `library/fedora`
    71  //
    72  //         methods:
    73  //           tags() list
    74  //             List of all the tags for this container image.
    75  //           version() string
    76  //             Return the highest tag matching the image constraint.
    77  //             params:
    78  //               full bool
    79  //                 If `true` returns the image name plus the tag. Eg.: `docker.io/library/fedora:29`
    80  type image struct {
    81  	tags       []string
    82  	ref        types.ImageReference
    83  	constraint string
    84  	sString
    85  }
    86  
    87  // Image returns a starlak.Builtin function capable of instantiate
    88  // new Image instances.
    89  //
    90  //   outline: docker
    91  //     functions:
    92  //       image(image, constraint) Image
    93  //         Returns a new `Image` based on a given image and constraint.
    94  //
    95  //         params:
    96  //           image string
    97  //             Container image name. Eg.: `ubuntu` or `quay.io/prometheus/prometheus`.
    98  //           constraint string
    99  //             [Semver](https://github.com/Masterminds/semver/#checking-version-constraints) contraint. Eg.: `1.2.*`
   100  //
   101  func Image(
   102  	thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple,
   103  ) (starlark.Value, error) {
   104  
   105  	var image, constraint string
   106  	err := starlark.UnpackArgs(imageFuncName, args, kwargs, "image", &image, "constraint", &constraint)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  
   111  	return newImage(image, constraint)
   112  }
   113  
   114  func newImage(name, constraint string) (*image, error) {
   115  	ref, err := reference.ParseNormalizedNamed(name)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  
   120  	if !reference.IsNameOnly(ref) {
   121  		return nil, errors.New("no tag or digest allowed in reference")
   122  	}
   123  
   124  	dref, err := docker.NewReference(reference.TagNameOnly(ref))
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	return &image{
   130  		ref:        dref,
   131  		constraint: constraint,
   132  		sString:    starlark.String(ref.Name()),
   133  	}, nil
   134  }
   135  
   136  func (i *image) Attr(name string) (starlark.Value, error) {
   137  	switch name {
   138  	case "name":
   139  		return starlark.String(i.ref.DockerReference().Name()), nil
   140  	case "domain":
   141  		name := i.ref.DockerReference()
   142  		return starlark.String(reference.Domain(name)), nil
   143  	case "path":
   144  		name := i.ref.DockerReference()
   145  		return starlark.String(reference.Path(name)), nil
   146  	case "tags":
   147  		return starlark.NewBuiltin("tags", i.builtinVersionFunc), nil
   148  	case "version":
   149  		return starlark.NewBuiltin("version", i.builtinVersionFunc), nil
   150  	}
   151  
   152  	return nil, nil
   153  }
   154  
   155  func (i *image) AttrNames() []string {
   156  	return []string{"name", "domain", "path", "tags", "version"}
   157  }
   158  
   159  func (i *image) builtinVersionFunc(
   160  	_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple,
   161  ) (starlark.Value, error) {
   162  
   163  	var full bool
   164  	starlark.UnpackArgs("version", args, kwargs, "full", &full)
   165  
   166  	v, err := i.getVersion()
   167  	if err != nil {
   168  		return starlark.None, err
   169  	}
   170  
   171  	if full {
   172  		v = fmt.Sprintf("%s:%s", i.ref.DockerReference().Name(), v)
   173  	}
   174  
   175  	return starlark.String(v), nil
   176  }
   177  
   178  func (i *image) builtinTagsFunc(
   179  	_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple,
   180  ) (starlark.Value, error) {
   181  	return i.getTags()
   182  }
   183  
   184  func (i *image) getTags() (*starlark.List, error) {
   185  	if len(i.tags) != 0 {
   186  		return listToStarlark(i.tags), nil
   187  	}
   188  
   189  	var err error
   190  	i.tags, err = docker.GetRepositoryTags(context.TODO(), imageSystemContext(), i.ref)
   191  	if err != nil {
   192  		return nil, fmt.Errorf("error listing repository tags: %v", err)
   193  	}
   194  
   195  	i.tags = sortTags(i.tags)
   196  	return listToStarlark(i.tags), nil
   197  }
   198  
   199  func (i *image) getVersion() (string, error) {
   200  	if i.constraint == latestTag {
   201  		return latestTag, nil
   202  	}
   203  
   204  	_, err := i.getTags()
   205  	if err != nil {
   206  		return "", err
   207  	}
   208  
   209  	if len(i.tags) == 0 {
   210  		return "", fmt.Errorf("no tags form this image")
   211  	}
   212  
   213  	c, err := semver.NewConstraint(i.constraint)
   214  	if err != nil {
   215  		return i.doGetVersionExactTag(i.constraint)
   216  	}
   217  
   218  	return i.doGetVersionWithConstraint(c)
   219  }
   220  
   221  func (i *image) doGetVersionWithConstraint(c *semver.Constraints) (string, error) {
   222  	// it assumes tags are always sorted from higher to lower
   223  	for _, tag := range i.tags {
   224  		v, err := semver.NewVersion(tag)
   225  		if err == nil {
   226  			if c.Check(v) {
   227  				return tag, nil
   228  			}
   229  		}
   230  	}
   231  
   232  	return "", nil
   233  }
   234  
   235  func (i *image) doGetVersionExactTag(expected string) (string, error) {
   236  	for _, tag := range i.tags {
   237  		if tag == expected {
   238  			return tag, nil
   239  		}
   240  	}
   241  
   242  	return "", fmt.Errorf("tag %q not found in repository", expected)
   243  }
   244  
   245  func sortTags(tags []string) []string {
   246  	versions, others := listToVersion(tags)
   247  	sort.Sort(sort.Reverse(semver.Collection(versions)))
   248  	return versionToList(versions, others)
   249  }
   250  
   251  func listToStarlark(input []string) *starlark.List {
   252  	output := make([]starlark.Value, len(input))
   253  	for i, v := range input {
   254  		output[i] = starlark.String(v)
   255  	}
   256  
   257  	return starlark.NewList(output)
   258  }
   259  
   260  func listToVersion(input []string) ([]*semver.Version, []string) {
   261  	versions := make([]*semver.Version, 0)
   262  	other := make([]string, 0)
   263  
   264  	for _, text := range input {
   265  		v, err := semver.NewVersion(text)
   266  		if err == nil && v.Prerelease() == "" {
   267  			versions = append(versions, v)
   268  			continue
   269  		}
   270  
   271  		other = append(other, text)
   272  	}
   273  
   274  	return versions, other
   275  }
   276  
   277  func versionToList(versions []*semver.Version, other []string) []string {
   278  	output := make([]string, 0)
   279  	for _, v := range versions {
   280  		output = append(output, v.Original())
   281  	}
   282  
   283  	return append(output, other...)
   284  }
   285  
   286  func imageSystemContext() *types.SystemContext {
   287  	cfgFile := os.Getenv("DOCKER_CONFIG_FILE")
   288  	if cfgFile == "" {
   289  		if cfgPath := os.Getenv("DOCKER_CONFIG"); cfgPath != "" {
   290  			cfgFile = filepath.Join(cfgPath, "config.json")
   291  		}
   292  	}
   293  
   294  	return &types.SystemContext{
   295  		AuthFilePath: cfgFile,
   296  	}
   297  }