get.porter.sh/porter@v1.3.0/pkg/cnab/oci_reference.go (about)

     1  package cnab
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"github.com/Masterminds/semver/v3"
    10  	"github.com/distribution/reference"
    11  	"github.com/docker/docker/registry"
    12  	"github.com/opencontainers/go-digest"
    13  )
    14  
    15  // ParseOCIReference parses the specified value as an OCIReference.
    16  // If the reference includes a digest, the digest is validated.
    17  func ParseOCIReference(value string) (OCIReference, error) {
    18  	named, err := reference.ParseNormalizedNamed(value)
    19  	if err != nil {
    20  		return OCIReference{}, fmt.Errorf("failed to parse named reference %s: %w", value, err)
    21  	}
    22  
    23  	ref := OCIReference{Named: named}
    24  	if ref.HasDigest() {
    25  		err := ref.Digest().Validate()
    26  		if err != nil {
    27  			return OCIReference{}, fmt.Errorf("invalid digest for reference %s: %w", value, err)
    28  		}
    29  	}
    30  
    31  	return ref, nil
    32  }
    33  
    34  // MustParseOCIReference parses the specified value as an OCIReference,
    35  // panicking on any errors.
    36  // Only use this for unit tests where you know the value is a reference.
    37  func MustParseOCIReference(value string) OCIReference {
    38  	ref, err := ParseOCIReference(value)
    39  	if err != nil {
    40  		panic(err)
    41  	}
    42  	return ref
    43  }
    44  
    45  var (
    46  	_ json.Marshaler   = OCIReference{}
    47  	_ json.Unmarshaler = &OCIReference{}
    48  )
    49  
    50  // OCIReference is a wrapper around a docker reference with convenience methods
    51  // for decomposing and manipulating bundle references.
    52  //
    53  // It is designed to be safe to call even when uninitialized, returning empty
    54  // strings when parts are requested that do not exist, such as calling Digest()
    55  // when no digest is set on the reference.
    56  type OCIReference struct {
    57  	// Name is the wrapped reference that we are providing helper methods on top of
    58  	Named reference.Named
    59  }
    60  
    61  func (r *OCIReference) UnmarshalJSON(bytes []byte) error {
    62  	value := strings.TrimPrefix(strings.TrimSuffix(string(bytes), `"`), `"`)
    63  	ref, err := ParseOCIReference(value)
    64  	if err != nil {
    65  		return err
    66  	}
    67  	r.Named = ref.Named
    68  	return nil
    69  }
    70  
    71  func (r OCIReference) MarshalJSON() ([]byte, error) {
    72  	return []byte(fmt.Sprintf(`"%s"`, r.String())), nil
    73  }
    74  
    75  // Always print the original name provided, not
    76  // the one with docker.io prefixed.
    77  func (r OCIReference) String() string {
    78  	if r.Named == nil {
    79  		return ""
    80  	}
    81  	return reference.FamiliarString(r.Named)
    82  }
    83  
    84  // Repository portion of the reference.
    85  // Example: docker.io/getporter/mybuns:v0.1.1 returns getporter/mybuns
    86  func (r OCIReference) Repository() string {
    87  	if r.Named == nil {
    88  		return ""
    89  	}
    90  	return reference.FamiliarName(r.Named)
    91  }
    92  
    93  // Registry portion of the reference.
    94  // Example: ghcr.io/getporter/mybuns:v0.1.1 returns ghcr.io
    95  func (r OCIReference) Registry() string {
    96  	if r.Named == nil {
    97  		return ""
    98  	}
    99  	return reference.Domain(r.Named)
   100  }
   101  
   102  // IsRepositoryOnly determines if the reference is fully qualified
   103  // with a tag/digest or if it only contains a repository.
   104  // Example: ghcr.io/getporter/mybuns returns true
   105  func (r OCIReference) IsRepositoryOnly() bool {
   106  	return !r.HasTag() && !r.HasDigest()
   107  }
   108  
   109  // HasDigest determines if the reference has a digest portion.
   110  // Example: ghcr.io/getporter/mybuns@sha256:abc123 returns true
   111  func (r OCIReference) HasDigest() bool {
   112  	if r.Named == nil {
   113  		return false
   114  	}
   115  
   116  	_, ok := r.Named.(reference.Digested)
   117  	return ok
   118  }
   119  
   120  // Digest portion of the reference.
   121  // Example: ghcr.io/getporter/mybuns@sha256:abc123 returns sha256:abc123
   122  func (r OCIReference) Digest() digest.Digest {
   123  	if r.Named == nil {
   124  		return ""
   125  	}
   126  
   127  	t, ok := r.Named.(reference.Digested)
   128  	if ok {
   129  		return t.Digest()
   130  	}
   131  	return ""
   132  }
   133  
   134  // HasTag determines if the reference has a tag portion.
   135  // Example: ghcr.io/getporter/mybuns:latest returns true
   136  func (r OCIReference) HasTag() bool {
   137  	if r.Named == nil {
   138  		return false
   139  	}
   140  
   141  	_, ok := r.Named.(reference.Tagged)
   142  	return ok
   143  }
   144  
   145  // Tag portion of the reference.
   146  // Example: ghcr.io/getporter/mybuns:latest returns latest
   147  func (r OCIReference) Tag() string {
   148  	if r.Named == nil {
   149  		return ""
   150  	}
   151  
   152  	t, ok := r.Named.(reference.Tagged)
   153  	if ok {
   154  		return t.Tag()
   155  	}
   156  	return ""
   157  }
   158  
   159  // HasVersion detects if the reference tag is a bundle version (semver).
   160  func (r OCIReference) HasVersion() bool {
   161  	if r.Named == nil {
   162  		return false
   163  	}
   164  
   165  	if tagged, ok := r.Named.(reference.Tagged); ok {
   166  		_, err := semverFromTag(tagged.Tag())
   167  		return err == nil
   168  	}
   169  	return false
   170  }
   171  
   172  // Version parses the reference tag as a bundle version (semver).
   173  func (r OCIReference) Version() string {
   174  	if r.Named == nil {
   175  		return ""
   176  	}
   177  
   178  	if tagged, ok := r.Named.(reference.Tagged); ok {
   179  		v, err := semverFromTag(tagged.Tag())
   180  		if err == nil {
   181  			return v.String()
   182  		}
   183  	}
   184  	return ""
   185  }
   186  
   187  // WithVersion creates a new reference using the repository and the specified bundle version.
   188  // If build metadata is present, "+" is converted to "_".
   189  func (r OCIReference) WithVersion(version string) (OCIReference, error) {
   190  	if r.Named == nil {
   191  		return OCIReference{}, errors.New("OCIReference has not been initialized")
   192  	}
   193  
   194  	v, err := semver.NewVersion(version)
   195  	if err != nil {
   196  		return OCIReference{}, fmt.Errorf("invalid bundle version specified %s: %w", version, err)
   197  	}
   198  
   199  	newRef, err := reference.WithTag(r.Named, tagFromSemver(v))
   200  	if err != nil {
   201  		return OCIReference{}, err
   202  	}
   203  	return OCIReference{Named: newRef}, nil
   204  }
   205  
   206  // WithTag creates a new reference using the repository and the specified tag.
   207  func (r OCIReference) WithTag(tag string) (OCIReference, error) {
   208  	if r.Named == nil {
   209  		return OCIReference{}, errors.New("OCIReference has not been initialized")
   210  	}
   211  	newRef, err := reference.WithTag(r.Named, tag)
   212  	if err != nil {
   213  		return OCIReference{}, err
   214  	}
   215  	return OCIReference{Named: newRef}, nil
   216  }
   217  
   218  // WithDigest creates a new reference using the repository and the specified digest.
   219  func (r OCIReference) WithDigest(digest digest.Digest) (OCIReference, error) {
   220  	if r.Named == nil {
   221  		return OCIReference{}, errors.New("OCIReference has not been initialized")
   222  	}
   223  	newRef, err := reference.WithDigest(r.Named, digest)
   224  	if err != nil {
   225  		return OCIReference{}, err
   226  	}
   227  	return OCIReference{Named: newRef}, nil
   228  }
   229  
   230  // ParseRepositoryInfo returns additional metadata about the repository portion of the reference.
   231  func (r OCIReference) ParseRepositoryInfo() (*registry.RepositoryInfo, error) {
   232  	if r.Named == nil {
   233  		return nil, errors.New("OCIReference has not been initialized")
   234  	}
   235  	return registry.ParseRepositoryInfo(r.Named)
   236  }
   237  
   238  func semverFromTag(tag string) (*semver.Version, error) {
   239  	return semver.NewVersion(strings.Replace(tag, "_", "+", 1))
   240  }
   241  
   242  func tagFromSemver(version *semver.Version) string {
   243  	return fmt.Sprintf("v%s", strings.Replace(version.String(), "+", "_", 1))
   244  }