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 }