Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 7 additions & 2 deletions pkg/cmd/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package cmd
import (
"context"
"fmt"
"net/url"
"os"

"github.com/kernel/hypeman-cli/internal/apiquery"
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
35 changes: 24 additions & 11 deletions pkg/cmd/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"net/http"
"net/url"
"os"
"strings"
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
6 changes: 4 additions & 2 deletions pkg/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"net/url"
"os"
"strings"
"time"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down