diff --git a/pkg/cli/cmd/bicep/publish/publish.go b/pkg/cli/cmd/bicep/publish/publish.go index 941cf78b00..05e7026776 100644 --- a/pkg/cli/cmd/bicep/publish/publish.go +++ b/pkg/cli/cmd/bicep/publish/publish.go @@ -37,6 +37,7 @@ import ( "github.com/spf13/cobra" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content/memory" + oraserr "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote/auth" @@ -293,11 +294,31 @@ func (r *Runner) prepareDestination() (*remote.Repository, error) { return dst, nil } +// The 'br:' prefix indicates this is a Bicep OCI registry reference. +// The target parameter should be the OCI reference without the 'br:' prefix (as stored in Runner.Target). +func enhanceOCIError(target string, err error) error { + const helpMessage = "The target must be a valid Bicep OCI registry reference in the form 'br:/:'." + + // Display the target with the 'br:' prefix to match what the user provided + displayTarget := "br:" + target + + // All OCI validation errors from oras-go (invalid repository, invalid tag, + // invalid registry, missing registry or repository) wrap errdef.ErrInvalidReference. + if errors.Is(err, oraserr.ErrInvalidReference) { + return clierrors.MessageWithCause(err, + "Invalid OCI reference in target %q.\n\n"+helpMessage, + displayTarget) + } + + // Return the original error if we don't recognize it + return err +} + // extractDestination extracts the host, repo, and tag from the target func (r *Runner) extractDestination() (*destination, error) { ref, err := registry.ParseReference(r.Target) if err != nil { - return nil, err + return nil, enhanceOCIError(r.Target, err) } host := ref.Host() diff --git a/pkg/cli/cmd/bicep/publish/publish_test.go b/pkg/cli/cmd/bicep/publish/publish_test.go index b9e1aded4a..691d565d98 100644 --- a/pkg/cli/cmd/bicep/publish/publish_test.go +++ b/pkg/cli/cmd/bicep/publish/publish_test.go @@ -23,6 +23,7 @@ import ( "net/http" "net/url" "reflect" + "strings" "testing" "github.com/opencontainers/go-digest" @@ -98,6 +99,83 @@ func TestRunner_extractDestination(t *testing.T) { } } +func TestRunner_extractDestination_EnhancedErrors(t *testing.T) { + tests := []struct { + name string + target string + wantErr bool + expectedErrContains []string + }{ + { + name: "uppercase in repository name", + target: "localhost:5000/myregistry/Data/mySqlDatabases/kubernetes/kubernetesmysql:latest", + wantErr: true, + expectedErrContains: []string{ + "Invalid OCI reference", + "br:", + }, + }, + { + name: "uppercase at start of repository", + target: "localhost:5000/MyRegistry/data:latest", + wantErr: true, + expectedErrContains: []string{ + "Invalid OCI reference", + "br:", + }, + }, + { + name: "invalid tag starting with hyphen", + target: "localhost:5000/myregistry/data:-invalid", + wantErr: true, + expectedErrContains: []string{ + "Invalid OCI reference", + "br:", + }, + }, + { + name: "missing repository", + target: "localhost:5000", + wantErr: true, + expectedErrContains: []string{ + "Invalid OCI reference", + "br:", + }, + }, + { + name: "valid lowercase repository", + target: "localhost:5000/myregistry/data/mysqldatabases/kubernetes/kubernetesmysql:latest", + wantErr: false, + }, + { + name: "valid with hyphens and underscores", + target: "localhost:5000/my-registry/my_data:v1.0.0", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Runner{ + Target: tt.target, + } + _, err := r.extractDestination() + if (err != nil) != tt.wantErr { + t.Errorf("Runner.extractDestination() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil && tt.wantErr { + errStr := err.Error() + for _, expectedStr := range tt.expectedErrContains { + if !strings.Contains(errStr, expectedStr) { + t.Errorf("Runner.extractDestination() error = %q, expected to contain %q", errStr, expectedStr) + } + } + } + }) + } +} + func Test_pushBlob(t *testing.T) { tests := []struct { name string