From 5b1f57bec1f1809edbe59df910e02953d9b249df Mon Sep 17 00:00:00 2001 From: Jordan Brockopp Date: Tue, 26 Oct 2021 14:50:18 -0500 Subject: [PATCH] feat(internal): add logic from pkg-runtime (#221) --- go.mod | 1 + internal/image/doc.go | 11 ++ internal/image/image.go | 87 +++++++++++++++ internal/image/image_test.go | 188 +++++++++++++++++++++++++++++++++ internal/volume/doc.go | 11 ++ internal/volume/volume.go | 70 ++++++++++++ internal/volume/volume_test.go | 140 ++++++++++++++++++++++++ 7 files changed, 508 insertions(+) create mode 100644 internal/image/doc.go create mode 100644 internal/image/image.go create mode 100644 internal/image/image_test.go create mode 100644 internal/volume/doc.go create mode 100644 internal/volume/volume.go create mode 100644 internal/volume/volume_test.go diff --git a/go.mod b/go.mod index 8d6af872..d3055291 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/Masterminds/semver/v3 v3.1.1 + github.com/docker/distribution v2.7.1+incompatible github.com/gin-gonic/gin v1.7.4 github.com/go-vela/compiler v0.10.0 github.com/go-vela/mock v0.10.0 diff --git a/internal/image/doc.go b/internal/image/doc.go new file mode 100644 index 00000000..d740349f --- /dev/null +++ b/internal/image/doc.go @@ -0,0 +1,11 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package image provides the ability for Vela to manage +// and manipulate an image provided for a container. +// +// Usage: +// +// import "github.com/go-vela/worker/internal/image" +package image diff --git a/internal/image/image.go b/internal/image/image.go new file mode 100644 index 00000000..6175ff18 --- /dev/null +++ b/internal/image/image.go @@ -0,0 +1,87 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package image + +import ( + "github.com/docker/distribution/reference" +) + +// Parse digests the provided image into a fully +// qualified canonical reference. If an error +// occurs, it will return the provided image. +func Parse(_image string) string { + // parse the image provided into a fully qualified canonical reference + // + // https://pkg.go.dev/github.com/go-vela/worker/runtime/internal/image?tab=doc#ParseWithError + _canonical, err := ParseWithError(_image) + if err != nil { + return _image + } + + return _canonical +} + +// ParseWithError digests the provided image into a +// fully qualified canonical reference. If an error +// occurs, it will return the last digested form of +// the image. +func ParseWithError(_image string) (string, error) { + // parse the image provided into a + // named, fully qualified reference + // + // https://pkg.go.dev/github.com/docker/distribution/reference?tab=doc#ParseAnyReference + _reference, err := reference.ParseAnyReference(_image) + if err != nil { + return _image, err + } + + // ensure we have the canonical form of the named reference + // + // https://pkg.go.dev/github.com/docker/distribution/reference?tab=doc#ParseNamed + _canonical, err := reference.ParseNamed(_reference.String()) + if err != nil { + return _reference.String(), err + } + + // ensure the canonical reference has a tag + // + // https://pkg.go.dev/github.com/docker/distribution/reference?tab=doc#TagNameOnly + return reference.TagNameOnly(_canonical).String(), nil +} + +// IsPrivilegedImage digests the provided image with a +// privileged pattern to see if the image meets the criteria +// needed to allow a Docker Socket mount. +func IsPrivilegedImage(image, privileged string) (bool, error) { + // parse the image provided into a + // named, fully qualified reference + // + // https://pkg.go.dev/github.com/docker/distribution/reference?tab=doc#ParseAnyReference + _refImg, err := reference.ParseAnyReference(image) + if err != nil { + return false, err + } + + // ensure we have the canonical form of the named reference + // + // https://pkg.go.dev/github.com/docker/distribution/reference?tab=doc#ParseNamed + _canonical, err := reference.ParseNamed(_refImg.String()) + if err != nil { + return false, err + } + + // add default tag "latest" when tag does not exist + _refImg = reference.TagNameOnly(_canonical) + + // check if the image matches the privileged pattern + // + // https://pkg.go.dev/github.com/docker/distribution/reference#FamiliarMatch + match, err := reference.FamiliarMatch(privileged, _refImg) + if err != nil { + return false, err + } + + return match, nil +} diff --git a/internal/image/image_test.go b/internal/image/image_test.go new file mode 100644 index 00000000..999ba3f3 --- /dev/null +++ b/internal/image/image_test.go @@ -0,0 +1,188 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package image + +import ( + "strings" + "testing" +) + +func TestImage_Parse(t *testing.T) { + // setup tests + tests := []struct { + image string + want string + }{ + { + image: "golang", + want: "docker.io/library/golang:latest", + }, + { + image: "golang:latest", + want: "docker.io/library/golang:latest", + }, + { + image: "library/golang", + want: "docker.io/library/golang:latest", + }, + { + image: "library/golang:1.14", + want: "docker.io/library/golang:1.14", + }, + { + image: "docker.io/library/golang", + want: "docker.io/library/golang:latest", + }, + { + image: "docker.io/library/golang:latest", + want: "docker.io/library/golang:latest", + }, + { + image: "index.docker.io/library/golang", + want: "docker.io/library/golang:latest", + }, + { + image: "index.docker.io/library/golang:latest", + want: "docker.io/library/golang:latest", + }, + { + image: "gcr.io/library/golang", + want: "gcr.io/library/golang:latest", + }, + { + image: "gcr.io/library/golang:latest", + want: "gcr.io/library/golang:latest", + }, + { + image: "!@#$%^&*()", + want: "!@#$%^&*()", + }, + } + + // run tests + for _, test := range tests { + got := Parse(test.image) + + if !strings.EqualFold(got, test.want) { + t.Errorf("Parse is %s want %s", got, test.want) + } + } +} + +func TestImage_ParseWithError(t *testing.T) { + // setup tests + tests := []struct { + failure bool + image string + want string + }{ + { + failure: false, + image: "golang", + want: "docker.io/library/golang:latest", + }, + { + failure: false, + image: "golang:latest", + want: "docker.io/library/golang:latest", + }, + { + failure: false, + image: "golang:1.14", + want: "docker.io/library/golang:1.14", + }, + { + failure: true, + image: "!@#$%^&*()", + want: "!@#$%^&*()", + }, + { + failure: true, + image: "1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", + want: "sha256:1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", + }, + } + + // run tests + for _, test := range tests { + got, err := ParseWithError(test.image) + + if test.failure { + if err == nil { + t.Errorf("ParseWithError should have returned err") + } + + if !strings.EqualFold(got, test.want) { + t.Errorf("ParseWithError is %s want %s", got, test.want) + } + + continue + } + + if err != nil { + t.Errorf("ParseWithError returned err: %v", err) + } + + if !strings.EqualFold(got, test.want) { + t.Errorf("ParseWithError is %s want %s", got, test.want) + } + } +} + +func TestImage_IsPrivilegedImage(t *testing.T) { + // setup tests + tests := []struct { + name string + image string + pattern string + want bool + }{ + { + name: "test privileged image without tag", + image: "docker.company.com/foo/bar", + pattern: "docker.company.com/foo/bar", + want: true, + }, + { + name: "test privileged image with tag", + image: "docker.company.com/foo/bar:v0.1.0", + pattern: "docker.company.com/foo/bar", + want: true, + }, + { + name: "test privileged image with tag", + image: "docker.company.com/foo/bar", + pattern: "docker.company.com/foo/bar:v0.1.0", + want: false, + }, + { + name: "test privileged with bad image", + image: "!@#$%^&*()", + pattern: "docker.company.com/foo/bar", + want: false, + }, + { + name: "test privileged with bad pattern", + image: "docker.company.com/foo/bar", + pattern: "!@#$%^&*()", + want: false, + }, + { + name: "test privileged with on extended path image", + image: "docker.company.com/foo/bar", + pattern: "docker.company.com/foo", + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _ := IsPrivilegedImage(test.image, test.pattern) + if got != test.want { + t.Errorf("IsPrivilegedImage is %v want %v", got, test.want) + } + }) + } +} diff --git a/internal/volume/doc.go b/internal/volume/doc.go new file mode 100644 index 00000000..e1f59b80 --- /dev/null +++ b/internal/volume/doc.go @@ -0,0 +1,11 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package volume provides the ability for Vela to manage +// and manipulate a volume provided for a container. +// +// Usage: +// +// import "github.com/go-vela/worker/internal/volume" +package volume diff --git a/internal/volume/volume.go b/internal/volume/volume.go new file mode 100644 index 00000000..d2f531ea --- /dev/null +++ b/internal/volume/volume.go @@ -0,0 +1,70 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package volume + +import ( + "fmt" + "strings" +) + +// Volume represents the volume definition used +// to create volumes for a container. +type Volume struct { + Source string `json:"source,omitempty"` + Destination string `json:"destination,omitempty"` + AccessMode string `json:"access_mode,omitempty"` +} + +// Parse digests the provided volume into a fully +// qualified volume reference. If an error +// occurs, it will return a nil volume. +func Parse(_volume string) *Volume { + // parse the image provided into a fully qualified canonical reference + // + // https://pkg.go.dev/github.com/go-vela/worker/runtime/internal/image?tab=doc#ParseWithError + v, err := ParseWithError(_volume) + if err != nil { + return nil + } + + return v +} + +// ParseWithError digests the provided volume into a +// fully qualified volume reference. If an error +// occurs, it will return a nil volume and the +// produced error. +func ParseWithError(_volume string) (*Volume, error) { + // split each slice element into source, destination and access mode + parts := strings.Split(_volume, ":") + + switch len(parts) { + case 1: + // return the read-only volume with the same source and destination + return &Volume{ + Source: parts[0], + Destination: parts[0], + AccessMode: "ro", + }, nil + // nolint: gomnd // ignore magic number + case 2: + // return the read-only volume with different source and destination + return &Volume{ + Source: parts[0], + Destination: parts[1], + AccessMode: "ro", + }, nil + // nolint: gomnd // ignore magic number + case 3: + // return the full volume with source, destination and access mode + return &Volume{ + Source: parts[0], + Destination: parts[1], + AccessMode: parts[2], + }, nil + default: + return nil, fmt.Errorf("volume %s requires at least 1, but no more than 2, `:`", _volume) + } +} diff --git a/internal/volume/volume_test.go b/internal/volume/volume_test.go new file mode 100644 index 00000000..f1fa9f22 --- /dev/null +++ b/internal/volume/volume_test.go @@ -0,0 +1,140 @@ +// Copyright (c) 2021 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package volume + +import ( + "reflect" + "testing" +) + +func TestVolume_Parse(t *testing.T) { + // setup tests + tests := []struct { + volume string + want *Volume + }{ + { + volume: "/foo", + want: &Volume{ + Source: "/foo", + Destination: "/foo", + AccessMode: "ro", + }, + }, + { + volume: "/foo:/bar", + want: &Volume{ + Source: "/foo", + Destination: "/bar", + AccessMode: "ro", + }, + }, + { + volume: "/foo:/bar:ro", + want: &Volume{ + Source: "/foo", + Destination: "/bar", + AccessMode: "ro", + }, + }, + { + volume: "/foo:/bar:rw", + want: &Volume{ + Source: "/foo", + Destination: "/bar", + AccessMode: "rw", + }, + }, + { + volume: "/foo:/bar:/foo:bar", + want: nil, + }, + } + + // run tests + for _, test := range tests { + got := Parse(test.volume) + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("Parse is %v, want %v", got, test.want) + } + } +} + +func TestImage_ParseWithError(t *testing.T) { + // setup tests + tests := []struct { + failure bool + volume string + want *Volume + }{ + { + failure: false, + volume: "/foo", + want: &Volume{ + Source: "/foo", + Destination: "/foo", + AccessMode: "ro", + }, + }, + { + failure: false, + volume: "/foo:/bar", + want: &Volume{ + Source: "/foo", + Destination: "/bar", + AccessMode: "ro", + }, + }, + { + failure: false, + volume: "/foo:/bar:ro", + want: &Volume{ + Source: "/foo", + Destination: "/bar", + AccessMode: "ro", + }, + }, + { + failure: false, + volume: "/foo:/bar:rw", + want: &Volume{ + Source: "/foo", + Destination: "/bar", + AccessMode: "rw", + }, + }, + { + failure: true, + volume: "/foo:/bar:/foo:bar", + want: nil, + }, + } + + // run tests + for _, test := range tests { + got, err := ParseWithError(test.volume) + + if test.failure { + if err == nil { + t.Errorf("ParseWithError should have returned err") + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ParseWithError is %s want %s", got, test.want) + } + + continue + } + + if err != nil { + t.Errorf("ParseWithError returned err: %v", err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ParseWithError is %v, want %v", got, test.want) + } + } +}