diff --git a/pkg/cmd/image.go b/pkg/cmd/image.go index 7235449..afac226 100644 --- a/pkg/cmd/image.go +++ b/pkg/cmd/image.go @@ -5,6 +5,7 @@ package cmd import ( "context" "fmt" + "net/url" "os" "github.com/kernel/hypeman-cli/internal/apiquery" @@ -145,7 +146,9 @@ func handleImagesDelete(ctx context.Context, cmd *cli.Command) error { return err } - return client.Images.Delete(ctx, requestflag.CommandRequestValue[string](cmd, "name"), options...) + // URL-encode the name to handle slashes in image references (e.g., docker.io/library/nginx:latest) + name := url.PathEscape(requestflag.CommandRequestValue[string](cmd, "name")) + return client.Images.Delete(ctx, name, options...) } func handleImagesGet(ctx context.Context, cmd *cli.Command) error { @@ -170,7 +173,9 @@ func handleImagesGet(ctx context.Context, cmd *cli.Command) error { var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Images.Get(ctx, requestflag.CommandRequestValue[string](cmd, "name"), options...) + // URL-encode the name to handle slashes in image references (e.g., docker.io/library/nginx:latest) + name := url.PathEscape(requestflag.CommandRequestValue[string](cmd, "name")) + _, err = client.Images.Get(ctx, name, options...) if err != nil { return err } diff --git a/pkg/cmd/push.go b/pkg/cmd/push.go index 223a99d..e66f61e 100644 --- a/pkg/cmd/push.go +++ b/pkg/cmd/push.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "net/http" "net/url" "os" "strings" @@ -70,11 +71,21 @@ func handlePush(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("invalid target: %w", err) } - auth := &hypemanAuth{} + token := os.Getenv("HYPEMAN_BEARER_TOKEN") + if token == "" { + token = os.Getenv("HYPEMAN_API_KEY") + } + + // Use custom transport that always sends Basic auth header + transport := &authTransport{ + base: http.DefaultTransport, + token: token, + } err = remote.Write(dstRef, img, remote.WithContext(ctx), - remote.WithAuth(auth), + remote.WithAuth(authn.Anonymous), + remote.WithTransport(transport), ) if err != nil { return fmt.Errorf("push failed: %w", err) @@ -84,15 +95,17 @@ func handlePush(ctx context.Context, cmd *cli.Command) error { return nil } -type hypemanAuth struct{} +// authTransport adds Basic auth header to all requests +type authTransport struct { + base http.RoundTripper + token string +} -func (a *hypemanAuth) Authorization() (*authn.AuthConfig, error) { - token := os.Getenv("HYPEMAN_BEARER_TOKEN") - if token == "" { - token = os.Getenv("HYPEMAN_API_KEY") - } - if token == "" { - return &authn.AuthConfig{}, nil +func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if t.token != "" { + // Clone request to avoid modifying the original (RoundTripper contract) + req = req.Clone(req.Context()) + req.Header.Set("Authorization", "Bearer "+t.token) } - return &authn.AuthConfig{RegistryToken: token}, nil + return t.base.RoundTrip(req) } diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index aadc75b..e23eac3 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "net/url" "os" "strings" "time" @@ -114,7 +115,8 @@ func handleRun(ctx context.Context, cmd *cli.Command) error { client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) // Check if image exists and is ready - imgInfo, err := client.Images.Get(ctx, image) + // URL-encode the image name to handle slashes (e.g., docker.io/library/nginx:latest) + imgInfo, err := client.Images.Get(ctx, url.PathEscape(image)) if err != nil { // Image not found, try to pull it var apiErr *hypeman.Error @@ -272,7 +274,7 @@ func waitForImageReady(ctx context.Context, client *hypeman.Client, img *hypeman case <-ctx.Done(): return ctx.Err() case <-ticker.C: - updated, err := client.Images.Get(ctx, img.Name) + updated, err := client.Images.Get(ctx, url.PathEscape(img.Name)) if err != nil { return fmt.Errorf("failed to check image status: %w", err) }