Skip to content

Commit

Permalink
fix: docker registry/repository parsing (knative#1929)
Browse files Browse the repository at this point in the history
* fix: docker registry/repository parsing

Use go-containerregistry to do parsing.

Signed-off-by: Matej Vasek <mvasek@redhat.com>

* fix: use kebab-case instead of camelCase

Signed-off-by: Matej Vasek <mvasek@redhat.com>

* fix: use kebab-case instead of camelCase

Signed-off-by: Matej Vasek <mvasek@redhat.com>

---------

Signed-off-by: Matej Vasek <mvasek@redhat.com>
  • Loading branch information
matejvasek authored Aug 14, 2023
1 parent c8abe3e commit 1163a5e
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 62 deletions.
4 changes: 2 additions & 2 deletions cmd/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1198,7 +1198,7 @@ func testRegistryLoads(cmdFn commandConstructor, t *testing.T) {

f := fn.Function{
Root: root,
Name: "myFunc",
Name: "my-func",
Runtime: "go",
Registry: "example.com/alice",
}
Expand All @@ -1219,7 +1219,7 @@ func testRegistryLoads(cmdFn commandConstructor, t *testing.T) {
t.Fatal(err)
}

expected := "example.com/alice/myFunc:latest"
expected := "example.com/alice/my-func:latest"
if f.Image != expected {
t.Fatalf("expected image name '%v'. got %v", expected, f.Image)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/functions/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (

const (
// DefaultRegistry through which containers of functions will be shuttled.
DefaultRegistry = "docker.io"
DefaultRegistry = "index.docker.io"

// DefaultTemplate is the default function signature / environmental context
// of the resultant function. All runtimes are expected to have at least
Expand Down
58 changes: 29 additions & 29 deletions pkg/functions/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ var (
// thus implicitly tests Create, Build and Deploy, which are exposed
// by the client API for those who prefer manual transmissions.
func TestClient_New(t *testing.T) {
root := "testdata/example.com/testNew"
root := "testdata/example.com/test-new"
defer Using(t, root)()

client := fn.New(fn.WithRegistry(TestRegistry), fn.WithVerbose(true))
Expand Down Expand Up @@ -210,7 +210,7 @@ func TestClient_New_RuntimeRequired(t *testing.T) {
// TestClient_New_NameDefaults ensures that a newly created function has its name defaulted
// to a name which can be derived from the last part of the given root path.
func TestClient_New_NameDefaults(t *testing.T) {
root := "testdata/example.com/testNameDefaults"
root := "testdata/example.com/test-name-defaults"
defer Using(t, root)()

client := fn.New(fn.WithRegistry(TestRegistry))
Expand All @@ -230,7 +230,7 @@ func TestClient_New_NameDefaults(t *testing.T) {
t.Fatal(err)
}

expected := "testNameDefaults"
expected := "test-name-defaults"
if f.Name != expected {
t.Fatalf("name was not defaulted. expected '%v' got '%v'", expected, f.Name)
}
Expand All @@ -239,7 +239,7 @@ func TestClient_New_NameDefaults(t *testing.T) {
// TestClient_New_WritesTemplate ensures the config file and files from the template
// are written on new.
func TestClient_New_WritesTemplate(t *testing.T) {
root := "testdata/example.com/testWritesTemplate"
root := "testdata/example.com/test-writes-template"
defer Using(t, root)()

client := fn.New(fn.WithRegistry(TestRegistry))
Expand All @@ -262,7 +262,7 @@ func TestClient_New_WritesTemplate(t *testing.T) {
// TestClient_New_ExtantAborts ensures that a directory which contains an extant
// function does not reinitialize.
func TestClient_New_ExtantAborts(t *testing.T) {
root := "testdata/example.com/testExtantAborts"
root := "testdata/example.com/test-extant-aborts"
defer Using(t, root)()

client := fn.New(fn.WithRegistry(TestRegistry))
Expand Down Expand Up @@ -305,7 +305,7 @@ func TestClient_New_NonemptyAborts(t *testing.T) {
// conjunction with other tools (.envrc, etc)
func TestClient_New_HiddenFilesIgnored(t *testing.T) {
// Create a directory for the function
root := "testdata/example.com/testHiddenFilesIgnored"
root := "testdata/example.com/test-hidden-files-ignored"
defer Using(t, root)()

client := fn.New(fn.WithRegistry(TestRegistry))
Expand All @@ -332,7 +332,7 @@ func TestClient_New_HiddenFilesIgnored(t *testing.T) {
// See the CLI for full details, but a standard default location is
// $HOME/.config/func/repositories/boson/go/json
func TestClient_New_RepositoriesExtensible(t *testing.T) {
root := "testdata/example.com/testRepositoriesExtensible"
root := "testdata/example.com/test-repositories-extensible"
defer Using(t, root)()

client := fn.New(
Expand Down Expand Up @@ -479,7 +479,7 @@ func TestClient_New_RegistryRequired(t *testing.T) {
// resultant OCI container is populated.
func TestClient_New_ImageNamePopulated(t *testing.T) {
// Create the root function directory
root := "testdata/example.com/testDeriveImage"
root := "testdata/example.com/test-derive-image"
defer Using(t, root)()

// Create the function which calculates fields such as name and image.
Expand Down Expand Up @@ -508,7 +508,7 @@ func TestClient_New_ImageNamePopulated(t *testing.T) {
// For example "alice" becomes "docker.io/alice"
func TestClient_New_ImageRegistryDefaults(t *testing.T) {
// Create the root function directory
root := "testdata/example.com/testDeriveImageDefaultRegistry"
root := "testdata/example.com/test-derive-image-default-registry"
defer Using(t, root)()

// Create the function which calculates fields such as name and image.
Expand All @@ -533,9 +533,9 @@ func TestClient_New_ImageRegistryDefaults(t *testing.T) {
// Deploy (and confirms expected fields calculated).
func TestClient_New_Delegation(t *testing.T) {
var (
root = "testdata/example.com/testNewDelegates" // .. in which to initialize
expectedName = "testNewDelegates" // expected to be derived
expectedImage = "example.com/alice/testNewDelegates:latest"
root = "testdata/example.com/test-new-delegates" // .. in which to initialize
expectedName = "test-new-delegates" // expected to be derived
expectedImage = "example.com/alice/test-new-delegates:latest"
builder = mock.NewBuilder()
pusher = mock.NewPusher()
deployer = mock.NewDeployer()
Expand Down Expand Up @@ -607,7 +607,7 @@ func TestClient_New_Delegation(t *testing.T) {
// See TestClient_Runner for the test of the default runner implementation.
func TestClient_Run(t *testing.T) {
// Create the root function directory
root := "testdata/example.com/testRun"
root := "testdata/example.com/test-run"
defer Using(t, root)()

// client with the mock runner and the new test function
Expand Down Expand Up @@ -693,7 +693,7 @@ func TestClient_Runner(t *testing.T) {
// .func to .gitignore is an important externally visible "feature", an explicit
// test is warranted.
func TestClient_Run_DataDir(t *testing.T) {
root := "testdata/example.com/testRunDataDir"
root := "testdata/example.com/test-run-data-dir"
defer Using(t, root)()

// Create a function at root
Expand Down Expand Up @@ -795,9 +795,9 @@ func TestClient_RunTimeout(t *testing.T) {
// process, erroring if run on a directory uncreated.
func TestClient_Update(t *testing.T) {
var (
root = "testdata/example.com/testUpdate"
expectedName = "testUpdate"
expectedImage = "example.com/alice/testUpdate:latest"
root = "testdata/example.com/test-update"
expectedName = "test-update"
expectedImage = "example.com/alice/test-update:latest"
builder = mock.NewBuilder()
pusher = mock.NewPusher()
deployer = mock.NewDeployerWithResult(fn.DeploymentResult{
Expand Down Expand Up @@ -954,8 +954,8 @@ func TestClient_Deploy_RegistryUpdate(t *testing.T) {
// the function with the name of the function at the provided root.
func TestClient_Remove_ByPath(t *testing.T) {
var (
root = "testdata/example.com/testRemoveByPath"
expectedName = "testRemoveByPath"
root = "testdata/example.com/test-remove-by-path"
expectedName = "test-remove-by-path"
remover = mock.NewRemover()
)

Expand Down Expand Up @@ -993,8 +993,8 @@ func TestClient_Remove_ByPath(t *testing.T) {
// the function with the name of the function at the provided root.
func TestClient_Remove_DeleteAll(t *testing.T) {
var (
root = "testdata/example.com/testRemoveDeleteAll"
expectedName = "testRemoveDeleteAll"
root = "testdata/example.com/test-remove-delete-all"
expectedName = "test-remove-delete-all"
remover = mock.NewRemover()
pipelinesProvider = mock.NewPipelinesProvider()
deleteAll = true
Expand Down Expand Up @@ -1039,8 +1039,8 @@ func TestClient_Remove_DeleteAll(t *testing.T) {
// the function with the name of the function at the provided root.
func TestClient_Remove_Dont_DeleteAll(t *testing.T) {
var (
root = "testdata/example.com/testRemoveDontDeleteAll"
expectedName = "testRemoveDontDeleteAll"
root = "testdata/example.com/test-remove-dont-delete-all"
expectedName = "test-remove-dont-delete-all"
remover = mock.NewRemover()
pipelinesProvider = mock.NewPipelinesProvider()
deleteAll = false
Expand Down Expand Up @@ -1351,7 +1351,7 @@ func TestClient_Deploy_UnbuiltErrors(t *testing.T) {
// TestClient_New_BuilderImagesPersisted Asserts that the client preserves user-
// provided Builder Images
func TestClient_New_BuildersPersisted(t *testing.T) {
root := "testdata/example.com/testConfiguredBuilders" // Root from which to run the test
root := "testdata/example.com/test-configured-builders" // Root from which to run the test
defer Using(t, root)()
client := fn.New(fn.WithRegistry(TestRegistry))

Expand Down Expand Up @@ -1388,7 +1388,7 @@ func TestClient_New_BuildersPersisted(t *testing.T) {
// TestClient_New_BuildpacksPersisted ensures that provided buildpacks are
// persisted on new functions.
func TestClient_New_BuildpacksPersisted(t *testing.T) {
root := "testdata/example.com/testConfiguredBuildpacks" // Root from which to run the test
root := "testdata/example.com/test-configured-buildpacks" // Root from which to run the test
defer Using(t, root)()

buildpacks := []string{
Expand Down Expand Up @@ -1504,7 +1504,7 @@ func TestClient_Runtimes(t *testing.T) {
// TestClient_New_Timestamp ensures that the creation timestamp is set on functions
// which are successfully initialized using the client library.
func TestClient_New_Timestamp(t *testing.T) {
root := "testdata/example.com/testCreateStamp"
root := "testdata/example.com/test-create-stamp"
defer Using(t, root)()

start := time.Now()
Expand All @@ -1526,7 +1526,7 @@ func TestClient_New_Timestamp(t *testing.T) {
// function using a simple HTTP POST method with the invoke message as form
// field values (as though a simple form were posted).
func TestClient_Invoke_HTTP(t *testing.T) {
root := "testdata/example.com/testInvokeHTTP"
root := "testdata/example.com/test-invoke-http"
defer Using(t, root)()

// Flag indicating the function was invoked
Expand Down Expand Up @@ -1639,7 +1639,7 @@ func TestClient_Invoke_HTTP(t *testing.T) {
// the invoker is sending the invocation message as a CloudEvent rather than
// a standard HTTP form POST.
func TestClient_Invoke_CloudEvent(t *testing.T) {
root := "testdata/example.com/testInvokeCloudEvent"
root := "testdata/example.com/test-invoke-cloud-event"
defer Using(t, root)()

var (
Expand Down Expand Up @@ -1730,7 +1730,7 @@ func TestClient_Invoke_CloudEvent(t *testing.T) {
// TestClient_Instances ensures that when a function is run (locally) its metadata
// is available to other clients inspecting the same function using .Instances
func TestClient_Instances(t *testing.T) {
root := "testdata/example.com/testInstances"
root := "testdata/example.com/test-instances"
defer Using(t, root)()

// A mock runner
Expand Down
22 changes: 9 additions & 13 deletions pkg/functions/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"time"

"github.com/google/go-containerregistry/pkg/name"
"gopkg.in/yaml.v2"
fnlabels "knative.dev/func/pkg/k8s/labels"
"knative.dev/pkg/ptr"
Expand Down Expand Up @@ -567,24 +568,19 @@ func (f Function) ImageName() (image string, err error) {

f.Registry = strings.Trim(f.Registry, "/") // too defensive?

registryTokens := strings.Split(f.Registry, "/")
if len(registryTokens) == 1 { // only namespace provided: ex. 'alice'
image = DefaultRegistry + "/" + f.Registry + "/" + f.Name
} else if len(registryTokens) == 2 || len(registryTokens) == 3 {
// registry/namespace ('quay.io/alice') or
// registry/parent-namespace/namespace ('quay.io/project/alice') provided
image = f.Registry + "/" + f.Name
} else if len(registryTokens) > 3 { // the name of the image is also provided `quay.io/alice/my.function.name`
return "", fmt.Errorf("registry should be either 'namespace', 'registry/namespace' or 'registry/parent/namespace', the name of the image will be derived from the function name")
}

// Explicitly append :latest tag. We expect source control to drive
// versioning, rather than rely on image tags with explicitly pinned version
// numbers, as is seen in many serverless solutions. This will be updated
// to branch name when we add source-driven canary/ bluegreen deployments.

// For pinning to an exact container image, see ImageWithDigest
return image + ":latest", nil
refStr := f.Registry + "/" + f.Name + ":latest"

ref, err := name.ParseReference(refStr)
if err != nil {
return "", fmt.Errorf("cannot determine function image: %w", err)
}

return ref.Name(), nil
}

// Format yaml unmarshall error to be more human friendly.
Expand Down
43 changes: 26 additions & 17 deletions pkg/functions/function_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,29 +140,38 @@ func TestFunction_ImageName(t *testing.T) {
err error
)
tests := []struct {
registry string
name string
registry string
funcName string
expectedImage string
expectError bool
}{
{"alice", "myfunc", DefaultRegistry + "/alice/myfunc:latest", false},
{"quay.io/alice", "myfunc", "quay.io/alice/myfunc:latest", false},
{"docker.io/alice", "myfunc", "docker.io/alice/myfunc:latest", false},
{"docker.io/alice/sub", "myfunc", "docker.io/alice/sub/myfunc:latest", false},
{"alice", "", "", true},
{"", "myfunc", "", true},
{"short-name", "alice", "myfunc", DefaultRegistry + "/alice/myfunc:latest", false},
{"short-name-trailing-slash", "alice/", "myfunc", DefaultRegistry + "/alice/myfunc:latest", false},
{"full-name-quay-io", "quay.io/alice", "myfunc", "quay.io/alice/myfunc:latest", false},
{"full-name-docker-io", "docker.io/alice", "myfunc", DefaultRegistry + "/alice/myfunc:latest", false},
{"full-name-with-sub-path", "docker.io/alice/sub", "myfunc", DefaultRegistry + "/alice/sub/myfunc:latest", false},
{"localhost-direct", "localhost:5000", "myfunc", "localhost:5000/myfunc:latest", false},
{"full-name-with-sub-sub-path", "us-central1-docker.pkg.dev/my-gcpproject/team/user", "myfunc", "us-central1-docker.pkg.dev/my-gcpproject/team/user/myfunc:latest", false},
{"missing-func-name", "alice", "", "", true},
{"missing-registry", "", "myfunc", "", true},
}
for _, test := range tests {
f = Function{Registry: test.registry, Name: test.name}
got, err = f.ImageName()
if test.expectError && err == nil {
t.Errorf("registry '%v' and name '%v' did not yield the expected error",
test.registry, test.name)
}
if got != test.expectedImage {
t.Errorf("expected registry '%v' name '%v' to yield image '%v', got '%v'",
test.registry, test.name, test.expectedImage, got)
}
t.Run(test.name, func(t *testing.T) {
f = Function{Registry: test.registry, Name: test.funcName}
got, err = f.ImageName()
if test.expectError && err == nil {
t.Errorf("registry '%v' and name '%v' did not yield the expected error",
test.registry, test.funcName)
}
if !test.expectError && err != nil {
t.Errorf("unexpected error: %v", err)
}
if got != test.expectedImage {
t.Errorf("expected registry '%v' name '%v' to yield image '%v', got '%v'",
test.registry, test.funcName, test.expectedImage, got)
}
})
}
}

Expand Down

0 comments on commit 1163a5e

Please sign in to comment.