Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
75 changes: 74 additions & 1 deletion pkg/cli/cmd/bicep/publish/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,11 +293,84 @@ func (r *Runner) prepareDestination() (*remote.Repository, error) {
return dst, nil
}

// enhanceOCIError enhances the OCI registry error with detailed information about naming rules
func enhanceOCIError(target string, err error) error {
errMsg := err.Error()

// Check for invalid repository error
if strings.Contains(errMsg, "invalid repository") {
return clierrors.MessageWithCause(err,
"Invalid OCI repository name in target %q.\n\n"+
"OCI repository names must:\n"+
" - Contain only lowercase letters (a-z), digits (0-9), periods (.), underscores (_), and hyphens (-)\n"+
" - Start with a lowercase letter or digit\n"+
" - Separate path components with forward slashes (/)\n"+
" - Not contain consecutive special characters except double underscores (__)\n\n"+
"Example: 'br:localhost:5000/myregistry/data/mysqldatabases:latest'\n\n"+
"For more information on OCI naming conventions, see:\n"+
"https://github.com/opencontainers/distribution-spec/blob/main/spec.md",
target)
}

// Check for invalid registry error
if strings.Contains(errMsg, "invalid registry") {
return clierrors.MessageWithCause(err,
"Invalid OCI registry in target %q.\n\n"+
"The registry must be a valid hostname, optionally with a port number.\n"+
"Example: 'localhost:5000', 'ghcr.io', 'myregistry.azurecr.io'\n\n"+
"For more information on OCI naming conventions, see:\n"+
"https://github.com/opencontainers/distribution-spec/blob/main/spec.md",
target)
}

// Check for invalid tag error
if strings.Contains(errMsg, "invalid tag") {
return clierrors.MessageWithCause(err,
"Invalid OCI tag in target %q.\n\n"+
"OCI tags must:\n"+
" - Start with an alphanumeric character or underscore\n"+
" - Contain only alphanumeric characters, underscores (_), hyphens (-), and periods (.)\n"+
" - Be at most 128 characters long\n\n"+
"Example: 'latest', 'v1.0.0', 'stable'\n\n"+
"For more information on OCI naming conventions, see:\n"+
"https://github.com/opencontainers/distribution-spec/blob/main/spec.md",
target)
}

// Check for missing registry or repository error
if strings.Contains(errMsg, "missing registry or repository") {
return clierrors.MessageWithCause(err,
"Invalid target format %q.\n\n"+
"The target must be in the format: 'br:REGISTRY/REPOSITORY:TAG'\n"+
"Where:\n"+
" - REGISTRY is the hostname (optionally with port), e.g., 'localhost:5000', 'ghcr.io'\n"+
" - REPOSITORY is the path to the artifact, e.g., 'myorg/myrepo'\n"+
" - TAG is the version identifier, e.g., 'latest', 'v1.0.0'\n\n"+
"Example: 'br:ghcr.io/myorg/myrepo:v1.0.0'\n\n"+
"For more information on OCI naming conventions, see:\n"+
"https://github.com/opencontainers/distribution-spec/blob/main/spec.md",
target)
}

// For any other invalid reference errors, provide general guidance
if strings.Contains(errMsg, "invalid reference") {
return clierrors.MessageWithCause(err,
"Invalid OCI reference in target %q.\n\n"+
"The target must comply with OCI naming rules and be in the format: 'br:REGISTRY/REPOSITORY:TAG'\n\n"+
"For more information on OCI naming conventions, see:\n"+
"https://github.com/opencontainers/distribution-spec/blob/main/spec.md",
target)
}

// 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()
Expand Down
79 changes: 79 additions & 0 deletions pkg/cli/cmd/bicep/publish/publish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"net/http"
"net/url"
"reflect"
"strings"
"testing"

"github.com/opencontainers/go-digest"
Expand Down Expand Up @@ -98,6 +99,84 @@ 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 repository name",
"lowercase letters",
"OCI naming",
},
},
{
name: "uppercase at start of repository",
target: "localhost:5000/MyRegistry/data:latest",
wantErr: true,
expectedErrContains: []string{
"Invalid OCI repository name",
"lowercase letters",
},
},
{
name: "invalid tag starting with hyphen",
target: "localhost:5000/myregistry/data:-invalid",
wantErr: true,
expectedErrContains: []string{
"Invalid OCI tag",
"alphanumeric character or underscore",
},
},
{
name: "missing repository",
target: "localhost:5000",
wantErr: true,
expectedErrContains: []string{
"Invalid target format",
"REGISTRY/REPOSITORY:TAG",
},
},
{
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
Expand Down
Loading