Skip to content

Commit

Permalink
Add optional digest suffix to template locator
Browse files Browse the repository at this point in the history
The suffix is "@digest" which may include an optional algorithm
(defaults to "sha256"). The encoded digest must be at least 7
characters long.

Examples:
- template://my@sha256:60a87371451eabcd211c929759db61746a7c6a1c068f59d868db6aa8dca637bd
- template://my@sha256:60a87371451
- template://my@60a8737

Signed-off-by: Jan Dubois <[email protected]>
  • Loading branch information
jandubois committed Dec 14, 2024
1 parent 138f55c commit 6a0d370
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 15 deletions.
74 changes: 59 additions & 15 deletions pkg/limatmpl/locator.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,70 @@ import (
"os"
"path"
"path/filepath"
"regexp"
"strings"

"github.com/containerd/containerd/identifiers"
"github.com/lima-vm/lima/pkg/ioutilx"
"github.com/lima-vm/lima/pkg/templatestore"
"github.com/opencontainers/go-digest"
"github.com/sirupsen/logrus"
)

type Template struct {
Name string
Locator string
Bytes []byte

algorithm digest.Algorithm
digest string
}

const yBytesLimit = 4 * 1024 * 1024 // 4MiB

// Only sha256, sha384, and sha512 are actually available but we reserve all lowercase and digit strings.
// Note that only lowercase hex digits are accepted.
var digestSuffixRegex = regexp.MustCompile(`^(.+)@(?:([a-z0-9]+):)?([a-f0-9]+)$`)

// splitOffDigest splits off an optional @algorithm:digest suffix from the locator.
func (tmpl *Template) splitOffDigest() error {
matches := digestSuffixRegex.FindStringSubmatch(tmpl.Locator)
if matches != nil {
tmpl.algorithm = digest.Algorithm(matches[2])
if tmpl.algorithm == "" {
tmpl.algorithm = digest.SHA256
}
if !tmpl.algorithm.Available() {
return fmt.Errorf("locator %q uses unavailable digest algorithm", tmpl.Locator)
}
tmpl.digest = matches[3]
if len(tmpl.digest) < 7 {
return fmt.Errorf("locator %q digest has fewer than 7 hex digits", tmpl.Locator)
}
tmpl.Locator = matches[1]
}
return nil
}

// Read fetches the content pointed at by a template locator. If the locator has an optional
// digest suffix, then the digest must match, or Read will return an error.
func Read(ctx context.Context, name, locator string) (*Template, error) {
var err error

tmpl := &Template{
Name: name,
Locator: locator,
}
if err = tmpl.splitOffDigest(); err != nil {
return nil, err
}

isTemplateURL, templateURL := SeemsTemplateURL(locator)
isTemplateURL, templateURL := SeemsTemplateURL(tmpl.Locator)
switch {
case isTemplateURL:
// No need to use SecureJoin here. https://github.com/lima-vm/lima/pull/805#discussion_r853411702
templateName := filepath.Join(templateURL.Host, templateURL.Path)
logrus.Debugf("interpreting argument %q as a template name %q", locator, templateName)
logrus.Debugf("interpreting argument %q as a template name %q", tmpl.Locator, templateName)
if tmpl.Name == "" {
// e.g., templateName = "deprecated/centos-7" , tmpl.Name = "centos-7"
tmpl.Name = filepath.Base(templateName)
Expand All @@ -47,15 +81,15 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
if err != nil {
return nil, err
}
case SeemsHTTPURL(locator):
case SeemsHTTPURL(tmpl.Locator):
if tmpl.Name == "" {
tmpl.Name, err = InstNameFromURL(locator)
tmpl.Name, err = InstNameFromURL(tmpl.Locator)
if err != nil {
return nil, err
}
}
logrus.Debugf("interpreting argument %q as a http url for instance %q", locator, tmpl.Name)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, locator, http.NoBody)
logrus.Debugf("interpreting argument %q as a http url for instance %q", tmpl.Locator, tmpl.Name)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, tmpl.Locator, http.NoBody)
if err != nil {
return nil, err
}
Expand All @@ -68,15 +102,15 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
if err != nil {
return nil, err
}
case SeemsFileURL(locator):
case SeemsFileURL(tmpl.Locator):
if tmpl.Name == "" {
tmpl.Name, err = InstNameFromURL(locator)
tmpl.Name, err = InstNameFromURL(tmpl.Locator)
if err != nil {
return nil, err
}
}
logrus.Debugf("interpreting argument %q as a file url for instance %q", locator, tmpl.Name)
r, err := os.Open(strings.TrimPrefix(locator, "file://"))
logrus.Debugf("interpreting argument %q as a file url for instance %q", tmpl.Locator, tmpl.Name)
r, err := os.Open(strings.TrimPrefix(tmpl.Locator, "file://"))
if err != nil {
return nil, err
}
Expand All @@ -85,15 +119,15 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
if err != nil {
return nil, err
}
case SeemsYAMLPath(locator):
case SeemsYAMLPath(tmpl.Locator):
if tmpl.Name == "" {
tmpl.Name, err = InstNameFromYAMLPath(locator)
tmpl.Name, err = InstNameFromYAMLPath(tmpl.Locator)
if err != nil {
return nil, err
}
}
logrus.Debugf("interpreting argument %q as a file path for instance %q", locator, tmpl.Name)
r, err := os.Open(locator)
logrus.Debugf("interpreting argument %q as a file path for instance %q", tmpl.Locator, tmpl.Name)
r, err := os.Open(tmpl.Locator)
if err != nil {
return nil, err
}
Expand All @@ -102,12 +136,22 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
if err != nil {
return nil, err
}
case locator == "-":
case tmpl.Locator == "-":
tmpl.Bytes, err = io.ReadAll(os.Stdin)
if err != nil {
return nil, fmt.Errorf("unexpected error reading stdin: %w", err)
}
}

if tmpl.digest != "" {
actualDigest := digest.Algorithm(tmpl.algorithm).FromBytes(tmpl.Bytes).Encoded()
if len(tmpl.digest) < len(actualDigest) {
actualDigest = actualDigest[:len(tmpl.digest)]
}
if actualDigest != tmpl.digest {
return nil, fmt.Errorf("locator %q digest doesn't match content digest %q", locator, actualDigest)
}
}
return tmpl, nil
}

Expand Down
62 changes: 62 additions & 0 deletions pkg/limatmpl/locator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package limatmpl

import (
"testing"

"github.com/opencontainers/go-digest"
"gotest.tools/v3/assert"
)

type digestTest struct {
locator string
fileRef string
digest string
error string
}

func TestDigestSuffix(t *testing.T) {
tests := []digestTest{
// may looks like a digest, but is (intentionally) allowed as part of a template name
{"tmpl.sh", "tmpl.sh", "", ""},
{"tmpl.sh@v1", "tmpl.sh@v1", "", ""},
{"[email protected]", "[email protected]", "", ""},
// this is an error though because it looks like a (too short) digest
{"tmpl.sh@1", "", "", "fewer than"},

// can always append a file extension to use a digest string as part of a template name
{"[email protected]", "[email protected]", "", ""},
{"template://my@1234567", "template://my", "sha256:1234567", ""},
{"template://[email protected]", "template://[email protected]", "", ""},
{"my@sha256:1234567", "my", "sha256:1234567", ""},
{"my@sha256:1234567.yaml", "my@sha256:1234567.yaml", "", ""},

// digest inside the middle of a URL is always ignored
{"https://example.com/templates@sha256:1234567/my.yaml", "https://example.com/templates@sha256:1234567/my.yaml", "", ""},

// locators with digests
{"tmpl.sh@1234567", "tmpl.sh", "sha256:1234567", ""},
{"tmpl.sh@sha256:1234567", "tmpl.sh", "sha256:1234567", ""},

// invalid locators
{"tmpl.sh@invalid:1234567", "", "", "unavailable digest"},
{"tmpl.sh@abcdef", "tmpl.sh", "", "fewer than"},
}
for _, test := range tests {
t.Run(test.locator, func(t *testing.T) {
tmpl := Template{Locator: test.locator}
err := tmpl.splitOffDigest()
if test.error != "" {
assert.ErrorContains(t, err, test.error, test.locator)
} else {
assert.NilError(t, err)
assert.Equal(t, tmpl.Locator, test.fileRef)
if test.digest == "" {
assert.Equal(t, tmpl.digest, "")
} else {
actual := digest.NewDigestFromEncoded(tmpl.algorithm, tmpl.digest)
assert.Equal(t, actual.String(), test.digest)
}
}
})
}
}

0 comments on commit 6a0d370

Please sign in to comment.