Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add format for platform string #6

Merged
merged 1 commit into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@

package platforms

// DefaultString returns the default string specifier for the platform.
// DefaultString returns the default string specifier for the platform,
// with [PR#6](https://github.com/containerd/platforms/pull/6) the result
// may now also include the OSVersion from the provided platform specification.
func DefaultString() string {
return Format(DefaultSpec())
return FormatAll(DefaultSpec())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is changing defaults here.

/cc @tonistiigi @tianon

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems expected to me, but highlights a possible issue.

A quick survey of the containerd code calling this function suggests that by-and-large it's used to hold a string that is given back to Parse later; so in the same process it's fine, and in fact due to the removal of auto-insertion of Windows host OS version in Parse, this one needs to be FormatAll or we're changing the observed behaviour of Parse(DefaultString()) on Windows.

The Windows v2 Runtime manager's supported platforms list might need to be hard-coded though. It already hard-codes linux/amd64 (that should probably be linux/$(GOARCH)...?).

Some cases however are in the client library, and perhaps the string is being processed in containerd on the other side of the (stable API!) remote connection. If that's the case then depending on the direction of mismatch, we're either generating strings old containerd can't parse, or generating strings which new containerd will parse into a platform object unexpectedly absent OS version.

I haven't checked to confirm that the stable API actually contains these platform-as-strings, the ones I was able to trace through GitHub's WebUI seemed to still be Parsed in the client library code, so maybe this problematic case doesn't exist here.

Direct consumers of this library will be affected by the defaults change, but AFAIK this library is not 1.0-versioned?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussion in a Moby maintainers call, and there were still some concerns if this could break things 🤔 (we should probably check in what cases)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmcgowan should we cut "1.x" tag from this repo or change DefaultString() to use Format() and have a new function that uses FormatAll() ?

}

// DefaultStrict returns strict form of Default.
Expand Down
2 changes: 1 addition & 1 deletion defaults_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func TestDefault(t *testing.T) {
}

s := DefaultString()
if s != Format(p) {
if s != FormatAll(p) {
t.Fatalf("default specifier should match formatted default spec: %v != %v", s, p)
}
}
2 changes: 1 addition & 1 deletion defaults_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestDefault(t *testing.T) {
}

s := DefaultString()
if s != Format(p) {
if s != FormatAll(p) {
t.Fatalf("default specifier should match formatted default spec: %v != %v", s, p)
}
}
Expand Down
72 changes: 45 additions & 27 deletions platforms.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,12 @@ import (
)

var (
specifierRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
specifierRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
osAndVersionRe = regexp.MustCompile(`^([A-Za-z0-9_-]+)(?:\(([A-Za-z0-9_.-]*)\))?$`)
)

const osAndVersionFormat = "%s(%s)"

// Platform is a type alias for convenience, so there is no need to import image-spec package everywhere.
type Platform = specs.Platform

Expand Down Expand Up @@ -156,7 +159,7 @@ func (m *matcher) Match(platform specs.Platform) bool {
}

func (m *matcher) String() string {
return Format(m.Platform)
return FormatAll(m.Platform)
}

// ParseAll parses a list of platform specifiers into a list of platform.
Expand All @@ -174,9 +177,12 @@ func ParseAll(specifiers []string) ([]specs.Platform, error) {

// Parse parses the platform specifier syntax into a platform declaration.
//
// Platform specifiers are in the format `<os>|<arch>|<os>/<arch>[/<variant>]`.
// Platform specifiers are in the format `<os>[(<OSVersion>)]|<arch>|<os>[(<OSVersion>)]/<arch>[/<variant>]`.
// The minimum required information for a platform specifier is the operating
// system or architecture. If there is only a single string (no slashes), the
// system or architecture. The OSVersion can be part of the OS like `windows(10.0.17763)`
// When an OSVersion is specified, then specs.Platform.OSVersion is populated with that value,
// and an empty string otherwise.
// If there is only a single string (no slashes), the
// value will be matched against the known set of operating systems, then fall
// back to the known set of architectures. The missing component will be
// inferred based on the local environment.
Expand All @@ -186,34 +192,42 @@ func Parse(specifier string) (specs.Platform, error) {
return specs.Platform{}, fmt.Errorf("%q: wildcards not yet supported: %w", specifier, errInvalidArgument)
}

parts := strings.Split(specifier, "/")
// Limit to 4 elements to prevent unbounded split
parts := strings.SplitN(specifier, "/", 4)

for _, part := range parts {
if !specifierRe.MatchString(part) {
return specs.Platform{}, fmt.Errorf("%q is an invalid component of %q: platform specifier component must match %q: %w", part, specifier, specifierRe.String(), errInvalidArgument)
var p specs.Platform
for i, part := range parts {
if i == 0 {
// First element is <os>[(<OSVersion>)]
osVer := osAndVersionRe.FindStringSubmatch(part)
if osVer == nil {
return specs.Platform{}, fmt.Errorf("%q is an invalid OS component of %q: OSAndVersion specifier component must match %q: %w", part, specifier, osAndVersionRe.String(), errInvalidArgument)
}

p.OS = normalizeOS(osVer[1])
p.OSVersion = osVer[2]
} else {
if !specifierRe.MatchString(part) {
return specs.Platform{}, fmt.Errorf("%q is an invalid component of %q: platform specifier component must match %q: %w", part, specifier, specifierRe.String(), errInvalidArgument)
}
}
}

var p specs.Platform
switch len(parts) {
case 1:
// in this case, we will test that the value might be an OS, then look
// it up. If it is not known, we'll treat it as an architecture. Since
// in this case, we will test that the value might be an OS (with or
// without the optional OSVersion specified) and look it up.
// If it is not known, we'll treat it as an architecture. Since
// we have very little information about the platform here, we are
// going to be a little more strict if we don't know about the argument
// value.
p.OS = normalizeOS(parts[0])
if isKnownOS(p.OS) {
// picks a default architecture
p.Architecture = runtime.GOARCH
if p.Architecture == "arm" && cpuVariant() != "v7" {
p.Variant = cpuVariant()
}

if p.OS == "windows" {
p.OSVersion = GetWindowsOsVersion()
}

return p, nil
}

Expand All @@ -228,31 +242,21 @@ func Parse(specifier string) (specs.Platform, error) {

return specs.Platform{}, fmt.Errorf("%q: unknown operating system or architecture: %w", specifier, errInvalidArgument)
case 2:
// In this case, we treat as a regular os/arch pair. We don't care
// In this case, we treat as a regular OS[(OSVersion)]/arch pair. We don't care
// about whether or not we know of the platform.
p.OS = normalizeOS(parts[0])
p.Architecture, p.Variant = normalizeArch(parts[1], "")
if p.Architecture == "arm" && p.Variant == "v7" {
p.Variant = ""
}

if p.OS == "windows" {
p.OSVersion = GetWindowsOsVersion()
}

return p, nil
case 3:
// we have a fully specified variant, this is rare
p.OS = normalizeOS(parts[0])
p.Architecture, p.Variant = normalizeArch(parts[1], parts[2])
if p.Architecture == "arm64" && p.Variant == "" {
p.Variant = "v8"
}

if p.OS == "windows" {
p.OSVersion = GetWindowsOsVersion()
}

return p, nil
}

Expand All @@ -278,6 +282,20 @@ func Format(platform specs.Platform) string {
return path.Join(platform.OS, platform.Architecture, platform.Variant)
}

// FormatAll returns a string specifier that also includes the OSVersion from the
// provided platform specification.
func FormatAll(platform specs.Platform) string {
if platform.OS == "" {
return "unknown"
}

if platform.OSVersion != "" {
OSAndVersion := fmt.Sprintf(osAndVersionFormat, platform.OS, platform.OSVersion)
return path.Join(OSAndVersion, platform.Architecture, platform.Variant)
}
return path.Join(platform.OS, platform.Architecture, platform.Variant)
}

// Normalize validates and translate the platform to the canonical value.
//
// For example, if "Aarch64" is encountered, we change it to "arm64" or if
Expand Down
4 changes: 0 additions & 4 deletions platforms_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,3 @@ func newDefaultMatcher(platform specs.Platform) Matcher {
Platform: Normalize(platform),
}
}

func GetWindowsOsVersion() string {
return ""
}
Loading
Loading